目录
1、禁止拷贝
这种特殊类的需求就是不让进行拷贝。
设计的核心就是禁止调用拷贝构造和赋值运算符重载即可。
c++98中就是对这两个只声明不定义,且放在私有里。
(如果没有放私有,用户还是可以在类外进行定义)
(只声明放私有里,用户就不能调用赋值或拷贝,这样的话不管有没有定义,都无法使用)
cppclass pl { private: pl(const pl& s); pl& operator=(const pl& s); };
c++11中增加了默认成员函数后加=delete,可以禁止调用该函数
cppclass pl { public: pl(const pl& s) = delete; pl& operator=(const pl& s) = delete; };
推荐用c++11的用法
2、只在堆上创建对象
cppclass 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; }
特殊写法
cppclass pl { public: pl(int a) { //.... } pl() { } //封掉析构函数 ~pl() = delete; private: }; int main() { //这样就可以 //pl s1;这样的写法就会报错,因为栈上的对象都会生命周期结束都会自动调用析构函数 //而析构函数被禁,就会出问题,所以编译器会直接报错。 pl *s1 = new pl(); return 0; }
但这一种不能释放资源
所以下面再改下
cppclass 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; }
如果再配合智能指针
cppclass 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、只在栈上创建对象
cppclass 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,父类构造私有化
cppclass pl { public: private: //构造私有化即可,因为派生类继承父类的时候,父类私有成员是无法被继承的 //(或者说虽然被继承下来了,但是派生类也没法调用,相当于隐藏了) // pl() { } };
第二种,利用c++11特性,final,加了final的类无法被继承
cppclass pl final { public: private: };
5、设计一个类,只能创建一个对象(单例模式)
设计模式:一套反复使用、多数人知晓、经过分类的、代码设计经验的总结。
目的:代码可重用性、代码易读性、代码可靠性。
让代码编写真的工程化,是软件工程的基石
**单例模式:**一个类在当前进程中只能创建一个对象,即单例模式,该模式保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如内存池(希望一个进程都只访问这一个内存池)、配置信息(服务器配置信息放在一个文件中,这些配置数据都一个单例对象统一读取,然后服务器进程中其他对象再通过这个单例对象获取这些配置信息)。旨在简化复杂环境下的配置管理。
饿汉模式:
cpp//饿汉: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对象先初始化,饿汉无法保证。
懒汉模式:
cppclass 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函数调用顺序即可。
释放问题:
一般来说懒汉模式下,单例对象也不需要释放,因为我们需要的就是整个程序进程中一直可以访问这个单例对象。
但特殊情况:某个需求,要提前释放单例对象;要求释放的时候把数据写入文件中等等。
上面就是一种释放的写法,但是不一定是类,也可以弄个全局的智能指针来管理。
下面是更简洁的懒汉
cppclass 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; }