目录
  • 设计一个类,只能在堆上创建对象
  • 设计一个类,只能在栈上创建对象
  • 设计一个类,不能被拷贝
  • 设计一个类,不能继承
  • 设计一个类,只能创建一个对象(单例模式)
    • 单例模式的概念
    • 单例模式的实现
      • 饿汉模式
      • 懒汉模式
      • 饿汉模式和懒汉模式的对比
      • 懒汉模式的优化
    • 单例对象的释放
      • 单例对象的直接释放
      • 内部垃圾回收类
  • 总结

    设计一个类,只能在堆上创建对象

    想要的效果实际是没法直接在栈上创建对象。

    首先cpp只要创建对象就要调用构造函数,因此先要把构造函数ban掉,把构造函数设计成private。但是单这样自己也创建不了了。

    因此提供一个创建的接口,只能调用该接口,该接口内部写new。而且要调用该接口需要先有对象指针调用,而要有对象先得调用构造函数实例化,因此必须设计成静态函数

    但是注意这样还有拷贝函数可以调用HeapOnly copy(*p)。此时生成的也是栈上的对象。因此要拷贝构造私有,并且只声明不实现(实现也是可以的,但是没人用)。这种方式在c++98中叫防拷贝,比如互斥锁。

    #include<iostream>
    using namespace std;
    class HeapOnly
    {
    private:
    	HeapOnly()
    	{ }
        //C++98——防拷贝
        HeapOnly(const HeapOnly&);
    public:
    	static HeapOnly* CreateObj()
    	{
    		return new HeapOnly;
    	}
    };
    int main()
    {
    	HeapOnly* p = HeapOnly::CreateObj();
    	return 0;
    }
    

    对于防拷贝,C++11中有新的方式。函数=delete

    #include<iostream>
    using namespace std;
    class HeapOnly
    {
    private:
    	HeapOnly()
    	{ }
    public:
    	static HeapOnly* CreateObj()
    	{
    		return new HeapOnly;
    	}
        //C++11——防拷贝
        HeapOnly(const HeapOnly&) =delete;
    };
    int main()
    {
    	HeapOnly* p = HeapOnly::CreateObj();
    	return 0;
    }
    

    总结:

    1.将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。

    2.提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

    设计一个类,只能在栈上创建对象

    • 方法一:同上将构造函数私有化,然后设计静态方法创建对象返回即可。

    由于返回临时对象,因此不能禁掉拷贝构造。

    class StackOnly 
    { 
        public: 
        static StackOnly CreateObject() 
        { 
            return StackOnly(); 
        }
        private:
        StackOnly() {}
    };
    
    • 方法二:调用类自己的专属的operator new和operator delete,设置为私有。

    因为new在底层调用void* operator new(size_t size)函数,只需将该函数屏蔽掉即可。注意:也要防止定位new。new先调用operator new申请空间,然后调用构造函数。delete先调用析构函数释放对象所申请的空间,再调用operator delete释放申请的对象空间。

    class StackOnly 
    { 
        public: 
        StackOnly() {}
        private: //C++98
        void* operator new(size_t size);
        void operator delete(void* p);
    };
    int main()
    {
      	static StackOnly st;//缺陷,没有禁掉静态区的。  
    }
    
    class StackOnly 
    { 
        public: 
        StackOnly() {}
        //C++11
        void* operator new(size_t size) = delete;
        void operator delete(void* p) = delete;
    };
    int main()
    {
      	static StackOnly st;//缺陷,没有禁掉静态区的。  
    }
    

    设计一个类,不能被拷贝

    拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

    • C++98将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
    class CopyBan
    {
        // ...
        private:
        CopyBan(const CopyBan&);
        CopyBan& operator=(const CopyBan&);
        //...
    };
    

    原因:

    1.设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了

    2.只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

    • C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
    class CopyBan
    {
        // ...
        CopyBan(const CopyBan&)=delete;
        CopyBan& operator=(const CopyBan&)=delete;
        //...
    };
    

    设计一个类,不能继承

    C++98

    // C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
    class NonInherit
    {
        public:
        static NonInherit GetInstance()
        {
            return NonInherit();
        }
        private:
        NonInherit()
        {}
    };
    class B : public NonInherit
    {};
    int main()
    {
        //C++98中这个不能被继承的方式不够彻底,实际是可以继承,限制的是子类继承后不能实例化对象
        B b;
        return 0;
    }
    

    C++11为了更直观,加入了final关键字

    class A final
    {   };
    class C: A
    {};
    

    设计一个类,只能创建一个对象(单例模式)

    之前接触过了适配器模式和迭代器模式。

    可以再看看工厂模式,观察者模式等等常用一两个的。

    单例模式的概念

    设计模式:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

    为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

    使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。

    设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

    • 单例模式

    一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

    1.如何保证全局(一个进程中)只有一个唯一的实例对象

    参考只能在堆上创建对象和在栈上创建对象,禁止构造和拷贝构造及赋值。

    提供一个GetInstance获取单例对象。

    2.如何提供只有一个实例呢?

    饿汉模式和懒汉模式。

    单例模式的实现

    饿汉模式

    饿汉模式:程序开始main执行之前就创建单例对象,提供一个静态指向单例对象的成员指针,初始时new一个对象给它。

    class Singleton
    {
        public:
        	static Singleton* GetInstance()
            {
                return _inst;
            }
        	void Print()
            {
                cout<<"Print() "<<_val<<endl;
            }
        private:
        	Singleton()
            :_val(0)
            {}
        	Singleton(const Singleton& ) =delete;
        	Singleton(const Singleton& ) =delete;
        	static Singleton* _inst;
        	int _val;
    };
    Singleton* Singleton::_inst = new Singleton;
    int main()
    {
        cout<<Singleton::GetInstance()<<endl;
        cout<<Singleton::GetInstance()<<endl;
        cout<<Singleton::GetInstance()<<endl;
        Singleton::GetInstance()->Print();
    }
    

    懒汉模式

    懒汉模式

    懒汉模式出现的原因,单例类的构造函数中要做很多配置初始化工作,那么饿汉就不合适了,会导致程序启动很慢。

    linux是Posix的pthread库,windows下有自己的线程库。因此要使用条件编译保证兼容性。因此c++11为了规范提供了语言级别的封装(本质也是条件编译,库里实现了)。

    关于保护第一次需要加锁,后面都不需要加锁的场景的可以使用双检查加锁。

    #include<mutex>
    #ifdef _WIN32
    //windos 提供多线程api
    #else
    //linux pthread
    #endif //
    class Singleton
    {
        public:
        	static Singleton* GetInstance()
            {
                //保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
                //特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率
                if( _inst == nullptr)
                {
                    _mtx.lock();
                    if( _inst == nullptr ) 
                    {
                        _inst = new Singleton;
                    }
                    _ntx.unlock();
                }
                return _inst;
            }
        	void Print()
            {
                cout<<"Print() "<<_val<<endl;
            }
        private:
        	Singleton()
            :_val(0)
            {}
        	Singleton(const Singleton& ) =delete;
        	Singleton(const Singleton& ) =delete;
        	static Singleton* _inst;
        	static std::mutex _mtx;
        	int _val;
    };
    Singleton* Singleton::_inst = nullptr;
    std::mutex Singleton::_mtx;//()默认无参构造函数
    int main()
    {
        Singleton::GetInstance()->Print();
    }
    

    饿汉模式和懒汉模式的对比

    • 饿汉模式
      • 优点:简单
      • 缺点:
        • 如果单例对象构造函数工作比较多,会导致程序启动慢,迟迟进不了入口main函数。
        • 如果有多个单例对象,他们之间有初始化的依赖关系,饿汉模式也会有问题。比如有A和B两个单例类,要求A单例先初始化,B必须在A之后初始化,那么饿汉无法保证。这种场景下用懒汉模式,懒汉可以先调用A::GetInstance(),再调用B::GetInstance()。
    • 懒汉模式
      • 优点:解决了饿汉的缺点,因为他是第一次调用GetInstance时创建初始化单例对象
      • 缺点:相对饿汉复杂一点。

    懒汉模式的优化

    实现了”更懒“。

    缺点:单例对象在静态区,如果单例对象太大,不合适。再挑挑刺,这个静态对象无法主动控制释放。

    #include<mutex>
    #ifdef _WIN32
    //windos 提供多线程api
    #else
    //linux pthread
    #endif //
    //其他版本懒汉
    class Singleton
    {
        public:
        	static Singleton* GetInstance()
            {
                static Singleton inst;
                return &inst;
            }
        	void Print()
            {
                cout<<"Print() "<<_val<<endl;
            }
        private:
        	Singleton()
            :_val(0)
            {}
        	Singleton(const Singleton& ) =delete;
        	Singleton(const Singleton& ) =delete;
        	static std::mutex _mtx;
        	int _val;
    };
    std::mutex Singleton::_mtx;//()默认无参构造函数
    int main()
    {
        Singleton::GetInstance()->Print();
    }
    #include<mutex>
    #ifdef _WIN32
    //windos 提供多线程api
    #else
    //linux pthread
    #endif //
    //其他版本懒汉
    class Singleton
    {
        public:
        	static Singleton* GetInstance()
            {
                static Singleton inst;
                return &inst;
            }
        	void Print()
            {
                cout<<"Print() "<<_val<<endl;
            }
        private:
        	Singleton()
            :_val(0)
            {}
        	Singleton(const Singleton& ) =delete;
        	Singleton(const Singleton& ) =delete;
        	static std::mutex _mtx;
        	int _val;
    };
    std::mutex Singleton::_mtx;//()默认无参构造函数
    int main()
    {
        Singleton::GetInstance()->Print();
    }
    

    单例对象的释放

    单例对象一般不需要释放。全局一直用的不delete也没问题,进程如果正常销毁,进程会释放对应资源。

    单例对象的直接释放

    #include<mutex>
    #ifdef _WIN32
    //windos 提供多线程api
    #else
    //linux pthread
    #endif //
    class Singleton
    {
        public:
        	static Singleton* GetInstance()
            {
                //保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
                //特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率
                if( _inst == nullptr)
                {
                    _mtx.lock();
                    if( _inst == nullptr ) 
                    {
                        _inst = new Singleton;
                    }
                    _ntx.unlock();
                }
                return _inst;
            }
        	static void DelInstance()/*调的很少,可以双检查也可以不双检查*/
            {
                _mtx.lock();
                if(!_inst)
                {
                    delete _inst;
                    _inst=nullptr;
                }
                _mtx.unlock();
            }
        	void Print()
            {
                cout<<"Print() "<<_val<<endl;
            }
        private:
        	Singleton()
            :_val(0)
            {
              	//假设单例类构造函数中,需要做很多配置初始化   
            }
        	~Singletion()
            {
                //程序结束时,需要处理一下,持久化保存一些数据
            }
        	Singleton(const Singleton& ) =delete;
        	Singleton(const Singleton& ) =delete;
        	static Singleton* _inst;
        	static std::mutex _mtx;
        	int _val;
    };
    Singleton* Singleton::_inst = nullptr;
    std::mutex Singleton::_mtx;//()默认无参构造函数
    int main()
    {
        Singleton::GetInstance()->Print();
    }
    

    内部垃圾回收类

    上述场景其实还是可以扩展的。

    假设析构函数有一些数据需要保存一下,持久化一下,不调用析构函数会存在问题,因此需要调用析构函数的时候处理。这就得保证main函数结束的时候保证调用析构(private)。

    但是显式调用DelInstance可能会存在遗忘。

    #include<mutex>
    #ifdef _WIN32
    //windos 提供多线程api
    #else
    //linux pthread
    #endif //
    class Singleton
    {
        public:
        	static Singleton* GetInstance()
            {
                //保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
                //特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率
                if( _inst == nullptr)
                {
                    _mtx.lock();
                    if( _inst == nullptr ) 
                    {
                        _inst = new Singleton;
                    }
                    _ntx.unlock();
                }
                return _inst;
            }
        	void Print()
            {
                cout<<"Print() "<<_val<<endl;
            }
        private:
        	Singleton()
            :_val(0)
            {
              	//假设单例类构造函数中,需要做很多配置初始化   
            }
        	~Singletion()
            {
                //程序结束时,需要处理一下,持久化保存一些数据
            }
        	Singleton(const Singleton& ) =delete;
        	Singleton(const Singleton& ) =delete;
        	//实现一个内嵌垃圾回收类
        	class CGarbo{
                public:
                	~CGarbo()
                    {
                        //DelInstance();
                    	if(_inst)
                        {
                             delete _inst;
                            _inst = nullptr;
                        }
                    }
            }
        	static Singleton* _inst;
        	static std::mutex _mtx;
        	static GCarbo _gc;//定义静态gc对象,帮助我们进行回收
        	int _val;
    };
    Singleton* Singleton::_inst = nullptr;
    std::mutex Singleton::_mtx;//()默认无参构造函数
    Singleton::CGarbo Singleton::_gc;
    int main()
    {
        Singleton::GetInstance()->Print();
    }
    

    总结

    本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注的更多内容!   

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。