目录
1、请设计一个类,不能被拷贝
2、请设计一个类,不能被继承
3、请设计一个类,只能在堆上创建对象
4、请设计一个类,只能在栈上创建对象
5、请设计一个类,只能创建一个对象(单例模式)
饿汉模式
懒汉模式
- 拷贝只会出现在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。在C++98和C++11都有相对应的方法来解决此问题,下面我们分别讨论。
C++98:
- 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。示例如下:
class CopyBan { public: //…… private: //只声明不定义 CopyBan(const CopyBan&);//拷贝构造 CopyBan& operator=(const CopyBan&);//赋值运算符重载 };
原因:
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁止拷贝了
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了
C++11:
- C++11拓展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后加上=delete,即表示让编译器删除掉该默认成员函数。
class CopyBan { CopyBan(const CopyBan&) = delete;//删除拷贝构造 CopyBan& operator=(const CopyBan&) = delete;//删除赋值运算符重载 };
库里面如下都是经典的防拷贝:
- unique_ptr
- thread线程
- mutex锁
- istream
- ostream
法一:C++98
- 将该类的构造函数私有化即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但无论是何种继承方式,父类的私有成员在子类是不可见的,所以创建子类对象时子类就无法调用父类的构造函数对父类的成员初始化,继而该类无法被继承。
class NonInherit { public: static NonInherit GetInstance() { return NonInherit(); } private: NonInherit()//私有化构造函数 {} };
- C++98的这种方式其实不够彻底,因为这个类仍然可以被继承(编译器不会报错),只不过被继承后无法实例化出对象而已。因此我们推出C++11的方法
法二:C++11
- 使用final关键字,final修饰类,表示该类不能被继承。此时就算继承后没有创建对象也会编译出错。
class A final { // .... };
总结:C++98是委婉的不能让你继承,C++11是直接的不能让你继承。
像我们平时创建对象,常见有如下三种在不同区域创建对象的方式:
class HeapOnly { //…… }; int main() { HeapOnly h1; static HeapOnly h2; HeapOnly* h3 = new HeapOnly; return 0; }
既然只能在堆上创建对象,也就是只能通过new操作创建对象,有如下两种方式。
法一:
- 将类的构造函数私有,拷贝构造声明成私有,防止别人调用拷贝在栈上生成对象
- 提供一个静态的成员函数,在该静态成员函数中完成对象的创建
class HeapOnly { public: //静态成员函数完成对象的创建 static HeapOnly* CreateObj() { return new HeapOnly; } private: //构造函数私有 HeapOnly() {} //防拷贝 //C++98,只声明不实现,且声明成私有 HeapOnly(const HeapOnly&); //C++11 HeapOnly(const HeapOnly&) = delete; }; int main() { HeapOnly* ph1 = HeapOnly::CreateObj(); HeapOnly* ph2 = HeapOnly::CreateObj(); delete ph1; delete ph2; //HeapOnly h1;//栈-错误 //static HeapOnly h2;//静态区-错误 //HeapOnly copy(*ph2);//调用拷贝生成对象,在栈区,错误 return 0; }
注意:
- 我们没有必要对赋值运算符重载设置为私有&&只声明不实现或者加上=delete(禁掉),因为赋值运算符重载是两个已经存在的对象,既然已经存在,那势必这俩对象就已经在堆区创建好了,所以它们之间进行赋值操作并不会出错。除非你不想用,那你可以把赋值运算符重载给禁掉。
- 而拷贝构造是拿一个已经存在的对象去构造一个对象,此对象是先前未存在的,且拷贝构造后是在栈上的,自然不符合题意,因此需要把拷贝构造给禁掉,而赋值运算符重载不需要禁掉。
法二:
- 将析构函数私有化
class HeapOnly { public: private: //析构函数私有化 ~HeapOnly() {} }; int main() { //HeapOnly ph1;err //static HeapOnly ph2;err HeapOnly* ph3 = new HeapOnly; return 0; }
为何析构函数私有化就能确保只能在堆上创建对象呢?
- C++是一个静态绑定的语言。在编译过程中,所有的非虚函数调用都必须分析完成。即使是虚函数,也需检查可访问性。因此, 当在栈上生成对象时,对象会自动调用析构函数释放对象,也就说析构函数必须可以访问 ,否则编译出错。而在堆上生成对象,由于析构函数由程序员调用(通过使用delete),所以不一定需要析构函数。
既然析构函数私有化,如何delete你new出的资源呢?
- 因为delete操作会调用析构函数,而析构函数已经被置为私有了,那就无法调用,为了解决此问题,我们只需要在类的内部提供一个静态成员函数,既然你类外不能调用私用成员,但是类里是可以调用的,因此我们在此成员函数中调用析构函数完成delete操作
class HeapOnly { public: //静态成员函数释放new的对象 static void DelObj(HeapOnly* ptr) { delete ptr; } private: //析构函数私有化 ~HeapOnly() {} }; int main() { HeapOnly* ph3 = new HeapOnly; //释放ph3 HeapOnly::DelObj(ph3); return 0; }
当然,我也可以使用delete this来释放new出的资源:
class HeapOnly { public: void DelObj() { delete this; } private: //析构函数私有化 ~HeapOnly() {} }; int main() { HeapOnly* ph3 = new HeapOnly; //释放ph3 ph3->DelObj(); return 0; }
delete this--对象请求自杀,执行后不能再访问this指针。换句话说,你不能去检查它、将它和其他指针比较、和 NULL比较、打印它、转换它,以及其它的任何事情。不是很推荐这种方式。
法一:
- 将构造函数设为私有,防止外部直接调用构造函数在堆上创建对象
- 提供静态成员函数,内部调用私有的构造函数完成对象的创建
class StackOnly { public: //静态成员函数,内部调用构造函数创建对象 static StackOnly CreateObj() { return StackOnly();//传值返回 —— 拷贝构造 } private: //构造函数私有 StackOnly() {} }; int main() { StackOnly h1 = StackOnly::CreateObj(); //static StackOnly h2; 错误 //StackOnly* h3 = new StackOnly; 错误 return 0; }
此法有一缺陷,无法避免外部调用拷贝构造函数在静态区、堆区……创建对象
int main() { StackOnly h1 = StackOnly::CreateObj();//栈区 static StackOnly h2(h1);//调用拷贝构造在静态区创建对象 StackOnly* h3 = new StackOnly(h1);//调用拷贝构造在堆区创建对象 return 0; }
- 但是我们又不能将拷贝构造函数设为私有,因为上述的静态成员函数CreateObj是传值返回,势必会调用拷贝构造函数,为了解决此问题,我们推出法二。
法二:
- 把构造函数设为公有
- 屏蔽operator new函数和operator delete函数
class StackOnly { public: StackOnly() {} private: //C++98 void* operator new(size_t size); void operator delete(void* p); //C++11 //void* operator new(size_t size) = delete; //void operator delete(void* p) = delete; }; int main() { StackOnly h1; //StackOnly* h3 = new StackOnly(h1);不能使用new在堆区创建对象 return 0; }
解释原因:
- new和delete默认调用的是全局的operator new函数和operator delete函数,但如果一个类重载了专属的operator new函数和operator delete函数,那么new和delete就会调用这个专属的函数。所以只要把operator new函数和operator delete函数屏蔽掉,那么就无法再使用new在堆上创建对象了。
上述做法虽然成功避免了在堆区创建对象,但是无法避免在静态区或全局创建对象。
class StackOnly { private: void* operator new(size_t size) = delete; void operator delete(void* p) = delete; }; StackOnly h1;//全局区 int main() { static StackOnly h2;//静态区 return 0; }
综上,其实无论是法一还是法二多少都会存在点瑕疵,总是会有老六的出现,只能说是不那么严谨的情况下来看,法一算是ok的。
设计模式:
- 设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
- 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
现在已经总结出了23种设计模式:
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
其中用的最多的就是单例模式。下面来展开讨论。
单例模式:
- 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式。下面展开讨论
- 饿汉模式就是不管你将来用不用,程序启动时就创建一个唯一的实例对象。
实现方式如下:
- 将构造函数私有化,并将拷贝构造和拷贝赋值设为私有或删除,防止外部随意创建对象或拷贝
- 在类里创建一个static静态对象的指针,在进入程序入口之前就完成单例对象的初始化
- 提供一个static静态成员函数,用来获取单例对象的指针
- 将拷贝构造和拷贝赋值私有化,防止类外调用拷贝构造创建对象
class Singleton { public: //静态成员函数内部获取单例对象的指针 static Singleton* GetInstance() { return _spInst; } void print(); private: Singleton() {} //C++98 防拷贝 Singleton(const Singleton&); Singleton& operator=(Singleton const&); //C++11 防拷贝 //Singleton(const Singleton&) = delete; //Singleton& operator=(Singleton const&) = delete; static Singleton* _spInst;//声明 int _a = 0; }; Singleton* Singleton::_spInst = new Singleton;//定义,在程序入口之前就完成单例对象的初始化 void Singleton::print() { cout << _a << endl; } int main() { Singleton::GetInstance()->print(); //Singleton st1;err //Singleton* st2 = new Singleton;err //Singleton copy(*Singleton::GetInstance());err return 0; }
再比如我现在有一个信息管理的类,需要保证进程里只有一份这样的信息,那么就需要把它设定为单例,整体框架和上面差不多其实,具体实现细节有所变动罢了:
//InfoMgr —— 单例 class InfoMgr { public: //静态成员函数获取单例对象指针 static InfoMgr* GetInstacne() { return _spInst; } //修改信息 void SetAddress(const string& s) { _address = s; } //获取信息 string& GetAddress() { return _address; } private: //构造函数私有化 InfoMgr() {} //删除拷贝构造 InfoMgr(const InfoMgr&) = delete; string _address; int _secretKey; static InfoMgr* _spInst;//声明 }; InfoMgr* InfoMgr::_spInst = new InfoMgr;//定义 int main() { //全局只有一个InfoMgr对象 InfoMgr::GetInstacne()->SetAddress("江苏省南京市"); cout << InfoMgr::GetInstacne()->GetAddress() << endl;//江苏省南京市 return 0; }
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
还是以上述信息管理的类为例,懒汉模式的实现方式如下:
- 将构造函数置为私有,并将拷贝构造函数和赋值运算符重载函数设为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
- 提供一个static静态成员函数,只有当static指针为空时才初始化(也就是第一次调用此成员函数才创建对象),最后返回单例对象的指针
- 将拷贝构造和拷贝赋值私有化,防止类外调用拷贝构造创建对象
//懒汉 -- 一开始不创建对象,第一次调用GetInstacne再创建对象 class InfoMgr { public: //静态成员函数获取单例对象指针 static InfoMgr* GetInstacne() { if (_spInst == nullptr) { _spInst = new InfoMgr; } return _spInst; } //修改信息 void SetAddress(const string& s) { _address = s; } //获取信息 string& GetAddress() { return _address; } private: //构造函数私有化 InfoMgr() {} //删除拷贝构造 InfoMgr(const InfoMgr&) = delete; string _address; int _secretKey; static InfoMgr* _spInst;//声明 }; InfoMgr* InfoMgr::_spInst = nullptr;//定义 int main() { //全局只有一个InfoMgr对象 InfoMgr::GetInstacne()->SetAddress("江苏省南京市"); cout << InfoMgr::GetInstacne()->GetAddress() << endl;//江苏省南京市 return 0; }
懒汉模式这样写是有问题的,还需要加锁(双检查加锁),饿汉模式不需要加锁。这个后面再来补充,未完待续…