详解C++中特殊类设计
作者:南猿北者
设计一个不能拷贝的类
类之间的拷贝主要是通过拷贝构造和赋值运算符之间来实现的;
为此,我们只要让外部用户无法正常调用这两个函数就行了;
C++98作法
将拷贝构造、赋值运算符的声明进行私有化处理;(不建议设为protected,不然的话在子类中就能进行拷贝了)
这样在外部就会因为访问限定符的限制不能进行拷贝:
class NoCopy { public: NoCopy(int val=0):_val(val) {} private: NoCopy(const NoCopy&); NoCopy& operator=(NoCopy&); int _val; };
C++11作法
C++只用delete关键字可以不让默认拷贝构造和默认赋值运算符重载默认生成,我们只要自己不去手写拷贝构造和赋值运算符重载就可以了:
class NoCopy { public: NoCopy(const NoCopy&) = delete; NoCopy& operator=(NoCopy&) = delete; NoCopy(int val=0):_val(val) {} private: int _val; };
设计一个类只允许在堆上创建对象
思路1:
1.封构造:防止在类外随意创建对象
2.设计一个成员函数接口来创建对象,让用户只能通过这个函数接口来创建对象,并且创建出来的对象一定是堆上的;(该函数不一定非要是静态的,非静态的函数也是可以完成用于创建对象的);
3.封拷贝:防止在在类外利用特定函数接口创建出来的对象在其它地方利用拷贝构造实例化对象;(移动构造也可以考虑封一下)
class HeapOnly { public: //利用静态成员函数来创建对象 //调用方法:HeapOnly::GetHeapOnly1(); static HeapOnly& GetHeapOnly1(int val=0) { return *(new HeapOnly(val)); } //利用非静态成员函数创建对象 //调用方法: // HeapOnly*p=nullptrl; //p->GetHeapOnly2(); HeapOnly& GetHeapOnly2(int val=0) { return *(new HeapOnly(val)); } private: //封拷贝:放在在类外利用GetHeapOnly()创建出来的对象在其它地方拷贝构造 //移动构造也可以考虑封一下 HeapOnly(HeapOnly&&) = delete; HeapOnly(const HeapOnly&) = delete; HeapOnly&operator=(const HeapOnly&) = delete; //封构造:防止在类外随意创建对象 HeapOnly(int val = 0) :_val(val) {} int _val; };
思路2:
1.封析构:那么就无法在静态区、栈区创建对象,因为这些地方在对象生命周期到了过后编译器无法在外部调用析构,自然的编译器就不允许在这些地方创建对象;
2.那么自然的我们最后在堆区上创建的对象也无法使用delete来释放资源,但是为了避免内存泄漏,我们可以在类内封装一个destory函数来进行手动调用释放资源;
class HeapOnly { public: void destroy() { delete this; } HeapOnly(int val = 0) :_val(val) {} private: ~HeapOnly() {} int _val; };
设计一个类只允许在栈上创建对象
思路:
1.封构造:防止在类外任意构造
2.设计一个在栈上创建对象的接口,让用户只能通过该函数获取对象
3.注意不要封拷贝!之所以能够在栈上创建对象就多亏了拷贝构造!(利用接口获取到的对象拷贝构造类外栈上的对象)
class StackOnly { public: static StackOnly GetStackOnly1(int val = 0) { return StackOnly(val); } StackOnly GetStackOnly2(int val = 0) { return StackOnly(val); } private: StackOnly(int val=0) :_val(val) {} int _val; };
设计一个类不能被继承
思路1:
1、封构造:也就是将构造私有化,那么子类就无法访问父类的私有成员,子类在初始化的时候就无法调用父类的构造函数来初始化,就无法完成对于父类的继承!
class A { private: A(int a = 0) :_a(a) {} int _a; }; class B :public A { };
思路2:
利用final关键字修饰类,被final关键字修饰的类无法被继承;
class A final { public: A(int a = 0) :_a(a) {} int _a; }; class B :public A {};
设计一个类只能实例化一次对象(单例模式)
单例模式简介:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理.
单例模式有两种实现模式
1. 饿汉模式
还没启动main函数之前就已经将对象创建好了;
2. 懒汉模式
程序启动的时候,第一次获取对象时进行创建;
饿汉模式
思路:
1.封构造:避免在类外随意创建对象;
2.提供一个函数接口(GetInstance())来获取这个唯一对象;
3.用一个封装一个本类的静态指针,一开始就初始化这个指针,该指针指向一个堆区的对象;
class Hungry { public: static Hungry&GetInstance() { return *_ptr; } private: Hungry(int x=int (),const Date&d=Date()):_x(x),_d(d) {} int _x; Date _d;//自定义类型 static Hungry* _ptr; }; Hungry* Hungry::_ptr = new Hungry(1,Date(2023,12,31));
饿汉模式总结:
1.使用饿汉模式会加慢程序响应的时间,因为饿汉模式是一上来就创建对象,要是这个对象非常大呢?那么创建起来就比较耗时间,这样会拖慢main函数的调用时机!
2.我们无法保证我们程序一运行起来就会立马使用饿汉模式创建出来的对象,这就会造成饿汉模式下的对象白白占用着空间,却无法得到有效利用,这是一种浪费!
3.如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
懒汉模式
思路:
1.封构造:避免在类外随意进行实例化对象;
2.提供一个接口(GetInstance())来专门获取这个单例对象,同时这个函数接口具有判断是不是第一次获取单例对象的能力;
3.封装一个static本类指针用于指向单例对象(初始化时为nullptr);
以上就是懒汉模式的雏形,可是上面的懒汉模型真的没有问题吗?
当然有,在单线程下没问题,可是在多线程下,要是多个线程第一次并发访问GetInstance()呢?
会不会造成多份sluggard对象被开辟呢?但是_ptr只能存一个sluggard对象的地址,也就意味着那么没有被_ptr记录下来的多余sluggard对象会造成严重的内存泄漏问题,为此我们需要给_ptr指针配一把锁来保护它的线程安全;
以上就是我们的第一个懒汉模型,上面的懒汉模式还有问题吗?
还有的!就是当我们每一次都用GetInstance()获取对象的时候都需要申请锁和释放锁,有一点影响效率,实际上这把锁只需要第一次访问时使用就够了,后面的访问我们就不需要再进行申请锁、释放锁了,为此我们可以按照如下的方式改:
可是上面的懒汉还是有一点小问题,就是new的时候是会失败的,new失败是会抛出异常的,此时执行流会直接跳到捕获该异常的地方,这样就会造成死锁问题,因为执行流在跳之前是已经申请了锁的,然后是以没解锁的状态走的,这就必然造成了死锁问题!为此这把锁我们需要交给具有RAII风格的智能锁来管理:
class sluggard { public: static sluggard& GetInstance() { if (!_ptr) { lock_guard<mutex> lock(_mux); if (!_ptr) _ptr = new sluggard; } return *_ptr; } private: sluggard(int c = int(), const Date& d=Date()) :_x(c), _d(d) {} int _x; Date _d; static sluggard* _ptr; static mutex _mux; }; mutex sluggard::_mux; sluggard* sluggard::_ptr=nullptr;
懒汉模式总结:
1.相比于饿汉模式,懒汉模式不会拖慢程序的启动速度,同时是只有需要的时候才进行创建,提高了空间资源的利用率;
2.缺点就是设计比较复杂!
以上就是详解C++中特殊类设计的详细内容,更多关于C++特殊类的资料请关注脚本之家其它相关文章!