目录
1、禁止拷贝
2、只在堆上创建对象
3、只在栈上创建对象
4.不能被继承
5、设计一个类,只能创建一个对象(单例模式)
饿汉模式:
懒汉模式:
1、禁止拷贝
这种特殊类的需求就是不让进行拷贝。
设计的核心就是禁止调用拷贝构造和赋值运算符重载即可。
c++98中就是对这两个只声明不定义,且放在私有里。
(如果没有放私有,用户还是可以在类外进行定义)
(只声明放私有里,用户就不能调用赋值或拷贝,这样的话不管有没有定义,都无法使用)
class pl { private:pl(const pl& s);pl& operator=(const pl& s); };
c++11中增加了默认成员函数后加=delete,可以禁止调用该函数
class pl { public:pl(const pl& s) = delete;pl& operator=(const pl& s) = delete; };
推荐用c++11的用法
2、只在堆上创建对象
class pl { public:template<class ...Args>//这个是可变参数模板,可不加,不加的话有多少种构造就要多少种createpl//重点是写一个静态的成员函数,用来在外面调用,函数内部强制new对象即可。static pl* createpl(Args&& ...args){return new pl(args...);}//注意,还要禁掉拷贝和赋值,因为拷贝和赋值的时候,不管里面是浅拷贝还是深拷贝//对象都是开辟在栈上的。pl(const pl& s) = delete;pl& operator=(const pl& s) = delete; private:pl(int a)//将构造私有化,这样外面的人就不能通过各种各样的方式直接构造对象了{//....}pl() {} };int main() {//如果不写静态的话,没有对象就不能调用成员函数,没有成员函数又不能创建对象,所以必须静态pl*s1 = pl::createpl(3);return 0; }
特殊写法
class pl { public:pl(int a){//....}pl() {}//封掉析构函数~pl() = delete;private:};int main() {//这样就可以//pl s1;这样的写法就会报错,因为栈上的对象都会生命周期结束都会自动调用析构函数//而析构函数被禁,就会出问题,所以编译器会直接报错。pl *s1 = new pl();return 0; }
但这一种不能释放资源
所以下面再改下
class pl { public:pl(int a){//....}pl() {}void Destory() {delete this;}private://不封了,而是放私有,通过成员函数delete this的方式来调用析构~pl() {cout << 1;}};int main() {pl *s1 = new pl();s1->Destory();return 0; }
如果再配合智能指针
class pl { public:pl(int a){//....}pl() {}void Destory() {delete this;}private://不封了,而是放私有,通过成员函数delete this的方式来调用析构~pl() {cout << 1;}};int main() {pl *s1 = new pl();//这种情况下必须手动给个删除器,不然默认的删除器是直接delete ptr,但此时的析构是私有的//所以必须通过下面的方式调用对象的成员函数,再通过成员函数间接调用析构函数。shared_ptr<pl>s2(new pl(), [](pl* ptr) {ptr->Destory(); });return 0; }
3、只在栈上创建对象
class pl { public:template<class ...Args>static pl createpl(Args&& ...args){return pl(args...);}//注意,这里跟堆的不一样,拷贝构造不能封住,因为传值返回,编译器优化之后也是要经过一次拷贝构造的。//pl(const pl& s) = delete;pl& operator=(const pl& s) = delete;//注意,因为没有封住拷贝构造,所以外面仍然可以通过拷贝的方式构造函数//当然不是pl s2(s1)这种,这种还是在栈上的,防的是这种pl s2=new pl(s1);// 所以重载new即可,默认是调用全局std的new,我们只要重载该类的new,并让这个new禁言掉即可。void* operator new(size_t n) = delete; private:pl(int a){//....}pl() {} };int main() {pl s1 = pl::createpl(3);return 0; }
4.不能被继承
第一种,c++98,父类构造私有化
class pl { public:private://构造私有化即可,因为派生类继承父类的时候,父类私有成员是无法被继承的//(或者说虽然被继承下来了,但是派生类也没法调用,相当于隐藏了)//pl() {}};
第二种,利用c++11特性,final,加了final的类无法被继承
class pl final { public:private:};
5、设计一个类,只能创建一个对象(单例模式)
设计模式:一套反复使用、多数人知晓、经过分类的、代码设计经验的总结。
目的:代码可重用性、代码易读性、代码可靠性。
让代码编写真的工程化,是软件工程的基石
单例模式:一个类在当前进程中只能创建一个对象,即单例模式,该模式保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如内存池(希望一个进程都只访问这一个内存池)、配置信息(服务器配置信息放在一个文件中,这些配置数据都一个单例对象统一读取,然后服务器进程中其他对象再通过这个单例对象获取这些配置信息)。旨在简化复杂环境下的配置管理。
饿汉模式:
//饿汉:main之前,就创建出对象 class Singleton { public://静态成员函数static Singleton* GetInstance() {return &_ps;}void OutPut(){cout << a1 << endl;cout << a2 << endl;cout << str << endl;}void Adds(const string& s){str += s;}//记得封拷贝构造和赋值Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;private://先构造私有化Singleton(int a11=0,int a22=0,const string&s="wdawdad"):a1(a11),a2(a22),str(s){}int a1;int a2;string str;//注意,静态成员,不存在对象中,而是在静态区,//相当于全局,声明在类中,定义在类外,受类域限制。//所以是可以在类里面写个同样类的静态对象的。static Singleton _ps; }; Singleton Singleton::_ps(3,4,"wwwwwww"); int main() {//这样就能用静态成员函数,访问静态成员(对象),再调用相应的成员函数Singleton::GetInstance()->OutPut();auto s1 = Singleton::GetInstance();s1->OutPut();s1->Adds("dwadwa");s1->OutPut();return 0; }
饿汉自动释放,对象是存在静态区的。
饿汉的问题:
1、如果单例数据太多,构造初始化的成本较高,会影响程序的启动速度。
比如有好多个单例,每个单例都有一堆信息要存储。这样的话速度就很慢,导致迟迟进不了
main()函数。在进main()函数前,是单线程进行工作的,只有在进了main()函数之后才是多线程工作,这样的话,如果因为单例加载太慢,导致一直堵在main()之前,那程序的启动速度就会非常慢。
2、多个单例类有初始化启动依赖关系,饿汉无法控制。
对于多个单例类的情况,如果对其中一些对象有初始化顺序的要求,比如a对象要求比b对象先初始化,饿汉无法保证。
懒汉模式:
class Singleton { public://调用对象static Singleton* GetInstance() {//第一次调用才会开辟空间if (_ps == nullptr){_ps = new Singleton;}return _ps;}//释放对象static void DelInstance() {if (_ps){delete _ps;_ps = nullptr;//考虑到可能中途释放了单例对象后,//还要重新有一个单例对象,要把指针重新设为空指针//这样下次调用GetInstance就能重新开辟空间了。//这个空指针,还要考虑到如果重复调用了DelInstance,防止多次释放空间//防止释放空指针}}void OutPut(){cout << a1 << endl;cout << a2 << endl;cout << str << endl;}void Adds(const string& s){str += s;}Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;private:Singleton(int a11=0,int a22=0,const string&s="wdawdad"):a1(a11),a2(a22),str(s){}~Singleton() {cout << "~";//可以做很多事情,比如释放的时候把数据写入某个文件等}int a1;int a2;string str;static Singleton*_ps;//考虑到加入了DelInstance之后,如果不显示调用,因为空间是new出来的,不会自动释放//所以加入下面的内部类class DS {public:~DS() {Singleton::DelInstance();}};//注意,如果是放在内部的话,ds一定得开静态。//因为如果是普通的成员的话,只有单例对象释放的时候才会调用ds的析构//但为了让单例对象释放,我们需要调用ds的析构,这样就死循环了,无法达到目的//所以利用静态成员放在静态区的特性,这样的话,程序结束的时候,静态区的ds也会结束生命周期//从而让ds自动调用析构,使得调用DelInstance()static DS ds; }; //注意,也可以放在外面,这时候ds开不开静态无所谓。 //因为全局的对象也是放在静态区,所以开不开静态都一样,生命周期 //class DS { //public: // ~DS() { // Singleton::DelInstance(); // } //}; //DS ds; Singleton* Singleton::_ps = nullptr; Singleton::DS Singleton::ds;int main() {Singleton::GetInstance();return 0; }
注意
这个不是完整的懒汉,完整的懒汉还需要设计到线程安全问题,需要加锁,具体的我后面会统一补充文章。目前涉及到线程安全问题的:shared_ptr,懒汉模式。
对于饿汉模式的缺陷,懒汉完美解决了。
懒汉不再是一开始就创建对象。
而是在进入main()函数后,根据需求调用单例的GetInstance函数,只有第一次调用该单例类的GetInstance函数才会开辟空间并赋予该单例对象。类对象本身在未调用GetInstance函数前,单例对象指针保持空指针的状态。这样就算main()前有好多个单例类,但单例对象指针都是空指针,不需要多少时间。
而对于初始化顺序的要求,我们手动控制不同单例类的GetInstance函数调用顺序即可。
释放问题:
一般来说懒汉模式下,单例对象也不需要释放,因为我们需要的就是整个程序进程中一直可以访问这个单例对象。
但特殊情况:某个需求,要提前释放单例对象;要求释放的时候把数据写入文件中等等。
上面就是一种释放的写法,但是不一定是类,也可以弄个全局的智能指针来管理。
下面是更简洁的懒汉
class Singleton { public://调用对象static Singleton* GetInstance() {//局部的静态对象只有第一次调用函数的时候才会构造对象。//因此可以做到控制多个单例对象初始化顺序。//且又因为局部的静态对象,生命周期也是全局的,所以也可以做到//不显示调用析构,也可以随着生命周期结束自动调用析构。static Singleton _ps;return &_ps;//这个写法只能在c++11及之后写。因为c++11之前无法保证这里的线程安全。}void OutPut(){cout << a1 << endl;cout << a2 << endl;cout << str << endl;}void Adds(const string& s){str += s;}Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;private:Singleton(int a11=0,int a22=0,const string&s="wdawdad"):a1(a11),a2(a22),str(s){}~Singleton() {cout << "~";//可以做很多事情,比如释放的时候把数据写入某个文件等}int a1;int a2;string str;};int main() {Singleton::GetInstance();return 0; }