目录
1.请设计一个类,不能被拷贝
2.请设计一个类,只能在堆上创建对象
3.请设计一个类,只能在栈上创建对象
4.请设计一个类,不能被继承
5.请设计一个类,只能创建一个对象(单例模式)
5.1.饿汉模式
5.2.懒汉模式
拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98的实现方式:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。class CopyBan { // ... private: CopyBan(const CopyBan&); CopyBan& operator=(const CopyBan&); //... };
原因:1.设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不禁止拷贝了。2.只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。3.c++98这种方法的缺陷:这种方法防止了在类外面的拷贝,但是在类里面还是可以拷贝的。
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
实现思路:1. 将类的构造函数私有,并禁掉拷贝构造,防止别人调用拷贝在栈等地方生成对象。2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。这里设置成静态的原因是调用普通成员函数需要对象,但这里无法正常创建对象,而设置成静态的就可以直接使用类进行调用。注:1.禁拷贝构造的原因:即使没有构造函数,也可以使用拷贝构造函数辅助创建对象。如果不禁拷贝构造,那么如下图所示的情况,就可以在栈等地方创建对象。HeapOnly* ph = HeapOnly::CreateObject(); HeapOnly a = *ph;
2.不用禁赋值运算符重载函数因为赋值运算符重载函数只有对象创建好后才能赋值,而我们要限制的是不能创建对象。
C++98的实现方式:
class HeapOnly { public: static HeapOnly* CreateObject() { return new HeapOnly; } private: HeapOnly() {} // 1.只声明,不实现。因为实现可能会很麻烦,而你本身不需要 // 2.声明成私有 HeapOnly(const HeapOnly&); }; int main() { HeapOnly* ph = HeapOnly::CreateObject(); return 0; }
C++11的实现方式:
class HeapOnly { public: static HeapOnly* CreateObject() { return new HeapOnly; } private: HeapOnly() {} // C++11 HeapOnly(const HeapOnly&) = delete; }; int main() { HeapOnly* ph = HeapOnly::CreateObject(); return 0; }
方法一实现思路:1.将类的构造函数私有。2.提供一个静态的成员函数,在该静态成员函数中调用构造函数完成栈对象的创建。注:这里不能禁掉拷贝构造,因为静态成员函数要传值返回需要调用拷贝构造函数。class StackOnly { public: static StackOnly CreateObj() { return StackOnly(); } private: StackOnly() :_a(0) {} private: int _a; }; int main() { StackOnly ph = StackOnly::CreateObj(); return 0; }
方法一的缺陷:因为可以拷贝构造,这种方法没有限制住下面代码所示的在堆上创建对象的方法。
StackOnly ph = StackOnly::CreateObj(); StackOnly ptr = new StackOnly(obj);
方法二实现思路:
在类中直接禁掉operator new和operator delete。
解释:new和delete调用函数优先调用类中的operator new和operator delete如果类中没有才会去调用全局的operator new和operator delete,我们在类中声明了operator new和operator delete因此编译器不会去调用全局的,又因为我们删除禁掉了类中的operator new和operator delete,因此new和delete无法使用。
class StackOnly { public: void* operator new(size_t size) = delete; void operator delete(void* p) = delete; StackOnly() :_a(0) {} private: int _a; }; int main() { StackOnly ph(); return 0; }
方法二的缺陷:这种方法只能限制不在堆区创建对象,但可以创建静态的对象或全局的对象,这两种对象不再栈中。
方法三实现思路:
将方法二和方法一相结合,使用方法二的禁用operator new和operator delete来解决方法一new空间时拷贝构造的缺陷。
class StackOnly { public: static StackOnly CreateObj() { return StackOnly(); } void* operator new(size_t size) = delete; void operator delete(void* p) = delete; private: StackOnly() :_a(0) {} private: int _a; }; int main() { StackOnly ph = StackOnly::CreateObj(); return 0; }
方法三的缺陷:方法三没有解决下面代码在静态区创建变量的问题。
StackOnly obj = StackOnly::CreateObj(); static StackOnly copy(obj);
C++98的实现方式:
将构造函数私有化,而派生类的构造函数必须要调用基类的构造函数初始化,私有的基类构造函数在派生类中不可见,派生类中调不到基类的构造函数,则无法继承。
class NonInherit { public: static NonInherit GetInstance() { return NonInherit(); } private: NonInherit() {} };
C++11的实现方式:
final关键字,final修饰类,表示该类不能被继承。
class A final { // .... };
设计模式:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人和人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。设计模式的分类:总体来说设计模式分为创建型模式、结构型模式、行为型模式三大类,三大类共有23种设计模式,其中单例模式属于创建型模式中的一种。
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中或当前进程中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。单例模式有两种实现模式:饿汉模式 、懒汉模式。
饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。实现思路:在Singleton类中创建一个静态的Singleton类型的 m_instance对象(在类中的部分只是声明还需要在类外面定义)。将构造函数私有化,这样就不能在外面随意创建对象。提供一个 GetInstance函数返回m_instance对象地址 。禁掉拷贝构造,防止利用创建的唯一对象去拷贝辅助创建相同的对象。那么对于Singleton这个类就只有 m_instance一个对象。注:1.因为是在Singleton类中创建一个静态的Singleton成员对象,静态的成员变量属于整个类而不属于某一对象,因此Singleton类对象中其实是没有Singleton类型的 m_instance成员变量的。2.静态的和全局的变量会在一开始(main函数之前)就创建。3.在Singleton类中创建一个静态的Singleton类型的对象和直接在类外面创建一个全局的Singleton类型对象其实效果近似,但是这里不允许我们在类外面创建一个全局的Singleton对象,因为在类外面创建全局的Singleton对象无法调用私有的构造函数,而在Singleton类中创建一个静态的Singleton类型 m_instance对象本质是在类中声明在类外定义,因为在类中声明即使是在类外定义的对象但对象仍属于类的作用域内,因此可以调用私有的构造函数。c++98的实现:class Singleton { public: static Singleton* GetInstance() { return &m_instance; } void Print(); private: // 构造函数私有 Singleton() {}; // C++98 防拷贝 Singleton(Singleton const&); static Singleton m_instance; int _a = 0; }; Singleton Singleton::m_instance; //在程序入口之前就完成单例对象的初始化 void Singleton::Print() { cout << _a << endl; } int main() { Singleton::GetInstance(); //获取Singleton类唯一对象的地址 Singleton::GetInstance()->Print(); //打印Singleton类唯一对象的成员变量值。 return 0; }
c++11的实现:
class Singleton { public: static Singleton* GetInstance() { return &m_instance; } void Print(); private: // 构造函数私有 Singleton() {}; // C++11 防拷贝 Singleton(Singleton const&) = delete; static Singleton m_instance; int _a = 0; }; Singleton Singleton::m_instance; //在程序入口之前就完成单例对象的初始化 void Singleton::Print() { cout << _a << endl; } int main() { Singleton::GetInstance(); //获取Singleton类唯一对象的地址 Singleton::GetInstance()->Print(); //打印Singleton类唯一对象的成员变量值。 return 0; }
饿汉模式优点:
1.简单2.如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好饿汉模式缺点:1.如果饿汉对象初始化慢或有多个饿汉单例对象,可能会导致进程启动慢2.如果有多个单例类对象,各单例对象实例初始化启动顺序不确定,如果多个单例类的资源初始化有依赖关系就会有问题
举例:一个进程里面有且仅有一份地址_address和密钥信息,我们采用单例模式,GetInstance函数用来获取唯一的对象地址,SetAddress用来修改唯一对象的_address地址成员变量,GetAddress用来获取唯一对象的_address地址成员变量。代码:class InfoMgr { public: static InfoMgr* GetInstance() { 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::GetInstance()->SetAddress("陕西省西安市碑林区"); cout << InfoMgr::GetInstance()->GetAddress() << endl; return 0; }
注:单例类对象的创建有两种方式,第一种方式是单例类中声明的是单例对象,类外定义单例对象,第二种方式是单例类中声明的是单例对象的指针变量,类外定义初始化单例对象的指针变量,两种方式皆可。
懒汉模式:
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。实现思路:在Singleton类内声明一个静态的Singleton*类型的 _spInst对象,在类外定义 _spInst对象并初始化为空。第一次使用Singleton类唯一对象 时,即调用GetInstance成员函数时,如果 _spInst为空即对象还没有创建,那么调用new函数创建对象,如果 _spInst不为空即对象已经创建,则返回对象地址_spInst。注:1.在GetInstance成员函数中要进行双检查加锁。饿汉模式不需要加锁是因为饿汉模式中唯一对象是在main函数之前初始化,main函数之前没有多线程,因此饿汉模式是线程安全的。懒汉模式第一次调用GetInstance成员函数,有可能两个线程同时调用,同时调用GetInstance那么同时new创建对象, _spInst最后只能保存一个地址,那么必定有一个对象被挤掉,并且这种情况同时创建出了两个对象,不是单例的,因此多线程的情况下如果不进行双检查加锁不能保证单例。2.无论是饿汉还是懒汉,如果唯一的对象是new出来的,一般情况下其实是不用释放的,也就是说单例对象一般是不用释放的。因为单例对象只有一个,内存泄漏影响不大,其实严格意义上将单例对象不释放不能认为是内存泄漏,因为单例对象一般是在整个进程中都要使用的,而进程结束后会将空间释放。一些极特殊的情况下可能需要通过析构做一些操作并释放单例对象,比如析构的时候需要将信息写到文件持久化。如果是单例类中声明的是单例对象,类外定义单例对象的情况,那么编译器会自动去调用单例类的析构函数进行释放,但是如果是单例类中声明的是单例对象的指针变量,类外定义初始化单例对象的指针变量的情况,编译器并不会自动去调用单例类的析构函数进行释放。解决方式是在单例类中内嵌一个垃圾回收的内部类CGarbo,内部类CGarbo的析构函数中delete单例对象,在单例类Singleton中声明一个静态的CGarbo对象并在类外定义该CGarbo对象,通过编译器自动调用CGarbo对象析构函数进而delete释放单例对象空间并调用单例对象析构函数。实现:class Singleton { public: static Singleton* GetInstance() { // 还需要加锁,这个后面讲 -- 双检查加锁 if (_spInst == nullptr) { _spInst = new Singleton; } return _spInst; } // 实现一个内嵌垃圾回收类 class CGarbo { public: ~CGarbo() { if (_spInst) delete _spInst; } }; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象 static CGarbo Garbo; private: Singleton() {} ~Singleton() { // 假设析构时需要信息写到文件持久化 } Singleton(const Singleton&) = delete; int _a; static Singleton* _spInst; // 声明 }; Singleton* Singleton::_spInst = nullptr; // 定义 Singleton::CGarbo Garbo; int main() { Singleton::GetInstance(); return 0; }
懒汉优点:
1.第一次使用实例对象时,创建对象,进程启动无负载2.如果有多个单例类,各单例类实例启动顺序与各单例类的创建对象函数(类似上面的GetInstance)第一次调用顺序相同,可以自由控制懒汉缺点:1.复杂