C++11(四):特殊类与单例模式设计精要

目录

特殊类的设计

[1. 设计一个只能在堆上创建的对象](#1. 设计一个只能在堆上创建的对象)

[2. 设计一个只能在栈上创建的对象](#2. 设计一个只能在栈上创建的对象)

[3. 设计一个类,不能被拷贝](#3. 设计一个类,不能被拷贝)

[4. 设计一个类,不能被继承](#4. 设计一个类,不能被继承)

设计模式

[1. 单例模式](#1. 单例模式)


特殊类的设计

1. 设计一个只能在堆上创建的对象

思路:正常的类是栈和堆都可以创建对象,一定会调用构造函数或者拷贝构造函数

所以我们应该把正常的路径给禁掉,然后通过某个public的函数,函数内部new即可

cpp 复制代码
class HeapOnly 
{ 
public: 
 static HeapOnly* CreateObject() 
 { 
 return new HeapOnly; 
 }
private: 
 HeapOnly() {}
 
 // C++98
 // 1.只声明,不实现。因为实现可能会很麻烦,而你本身不需要
 // 2.声明成私有
 HeapOnly(const HeapOnly&);
 
 // or
 
 // C++11 
 HeapOnly(const HeapOnly&) = delete;
}

注意这里的函数接口必须是static的,否则你必须要创建对象才能调用这个函数,但是创建对象又得通过这个函数接口,所以就死局了,所以必须使用static修饰变为静态成员函数,这样可以通过类名::函数名直接调用

拷贝构造禁掉是防止HeapOnly copy(hp); //通过拷贝构造创建一个栈上的对象

2. 设计一个只能在栈上创建的对象

思路:仿照第一个的思想,也是把正常的禁掉,然后提供一个接口进行创建即可

cpp 复制代码
class StackOnly 
{ 
public: 
 static StackOnly CreateObject() 
 { 
 return StackOnly(); 
 }
private:
 StackOnly() {}
};

注意此时不能禁掉拷贝构造函数,因为你return的时候返回出去会使用拷贝构造函数创建临时对象,临时对象最后在赋值出去,这里又用到拷贝构造,禁掉的话两次都会失败

第二种思路:由于new是堆空间,new=operator new+定位new

所以我们可以重载一个operator new,然后禁掉,这样new的时候就不会调用全局的operator new,但是类内禁掉了,所以调用不了

cpp 复制代码
class StackOnly 
{ 
public: 
 StackOnly() {}
private: 
 void* operator new(size_t size);
 void operator delete(void* p);
};

缺陷:就是不能阻止我创建静态区的对象

3. 设计一个类,不能被拷贝

思路:把拷贝构造函数和operator =禁掉即可

cpp 复制代码
class CopyBan
{
 // ...
 
private:
 CopyBan(const CopyBan&);
 CopyBan& operator=(const CopyBan&);
 //...
};

4. 设计一个类,不能被继承

c++98中构造函数私有化,派生类中调不到基类的构造函数,则无法继承

cpp 复制代码
class NonInherit
{
public:
 static NonInherit GetInstance()
 {
 return NonInherit();
 }
private:
 NonInherit()
 {}
};

c++11 final关键字,final修饰类,表示该类不能被继承

cpp 复制代码
class A final
{
 // ...
};

设计模式

设计模式是「解决通用业务场景的可复用解决方案(如单例模式、工厂模式)」,属于架构层面的通用方法论

简单来说:就是前人总结过的一种通用的、反复被使用的、有套路的代码设计经验,目的就是为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

1. 单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。(比如全局就一个内存池,一个线程池,服务器的一个配置文件)只要你的进程当中只允许只有一份实例的就需要设计成单例模式

cpp 复制代码
class Singleton {
public:
    // 全局访问点
    static Singleton* GetInstance() {
          if(_pinst==nullptr){
            _pinst=new Singleton;
           }
            return _pinst;
    }

    // 禁用拷贝/赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default; // 私有构造
    ~Singleton() = default; // 私有析构

    static Singleton* _pinst;
};
Singleton* Singleton::_pinst = nullptr;

函数必须使用static修饰,否则一开始无法创建对象,成员变量必须使用static修饰,因为你是全局唯一,所以这个与静态成员变量的特性一致,属于类的共有

同时禁掉拷贝和赋值,防止创建新的实例化对象

注意:但是此时存在线程安全问题,一开始判断_pinst==nullptr进去后,还没有开始new,时间片就到了,别的线程来了判断也为nullptr,就会导致多线程进去new,但是new出来的不一样,后面的会把前面的new覆盖掉,这样就会导致内存泄漏问题(找不到原来new的对象了)

解决方案:对临界区进行加锁

cpp 复制代码
class Singleton {
public:
    // 全局访问点
    static Singleton* GetInstance() {
          _mtx.lock();
          if(_pinst==nullptr){
            _pinst=new Singleton;
           }
           _mtx.unlock();
            return _pinst;
    }

    // 禁用拷贝/赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default; // 私有构造
    ~Singleton() = default; // 私有析构

    static Singleton* _pinst;
    static std::mutex _mtx; // 互斥锁保证线程安全
};
Singleton* Singleton::_pinst = nullptr;
std::mutex Singleton::_mtx;

这个代码还是有问题,如果new抛异常怎么办,没有捕获异常,直接会异常挂掉,对于一几年的app就会这样,没有捕获异常,客户端直接挂掉了,客户体验感极差。

还有一种就是你捕获了异常,但是执行流改变了,导致你没有进行解锁

解决方案:使用unique_lock<mutex>,RAII的思想进行管理

cpp 复制代码
class Singleton {
public:
    // 全局访问点
    static Singleton* GetInstance() {
         // _mtx.lock();
         {
            unique_lock<mutexx> lock(_mtx);
             if(_pinst==nullptr){
            _pinst=new Singleton;
           }
         }
           //_mtx.unlock();
            return _pinst;
    
    }

    // 禁用拷贝/赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default; // 私有构造
    ~Singleton() = default; // 私有析构

    static Singleton* _pinst;
    static std::mutex _mtx; // 互斥锁保证线程安全
};
Singleton* Singleton::_pinst = nullptr;
std::mutex Singleton::_mtx;

注意:这里使用unique_lock解决,如果中间抛异常也没事,出作用域自动析构,自动解锁,避免死锁问题

并且这里有个优化,就是加一对{},这样可以避免锁的粒度过大,在c++中{}代表作用域,出了花括号自动析构;

优化空间:对于加锁,我们只需要针对第一次即可,因为后面判断都会失败,只要第一次new出来,后面不可能进入if判断语句了

cpp 复制代码
class Singleton {
public:
    // 全局访问点
    static Singleton* GetInstance() {
         // _mtx.lock();
         if(_pinst==nullptr)
         {
            unique_lock<mutexx> lock(_mtx);
             if(_pinst==nullptr){
            _pinst=new Singleton;
           }
         }
           //_mtx.unlock();
            return _pinst;
    
    }

    // 禁用拷贝/赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default; // 私有构造
    ~Singleton() = default; // 私有析构

    static Singleton* _pinst;
    static std::mutex _mtx; // 互斥锁保证线程安全
};
Singleton* Singleton::_pinst = nullptr;
std::mutex Singleton::_mtx;

你只要进了第一个检查还有一把锁保证,如果同时有很多进程进了第一个检查,因为有锁,所有只有持有锁的才能进入第二个,进了第二个就第一次new,然后后面的在持有锁它判断false,就会解锁出去return _pinst;所以能够保证只能一个实例

然后后面的它就判断第一个检查都进不了。都不会拿到锁,直接就return了(第一个锁是为了防止对象创建好了之后,还需要每次加锁,就浪费了)
一般来说,是不需要析构的,因为这个全局只有一个对象,程序运行结束自动释放即可,一般都是一直存在

注意释放的时候需要加锁,防止释放的过程当中有人来申请

注意:以上写的单例模式叫做懒汉模式,就是不创建,用的时候再来创建

饿汉模式:一开始就创建对象,main函数之前创建好

cpp 复制代码
//饿汉模式
class Singleton {
public:
    // 全局访问点
    static Singleton* GetInstance() {
        return &_inst; // 实例已在程序启动时创建
    }

    // 禁用拷贝/赋值(必做,防止实例复制)
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {
        cout << "饿汉模式:实例创建(程序启动时)" << endl;
    }

    // 静态成员变量:程序启动时(main函数前)初始化,全局唯一
    static Singleton _inst; 
};

// 类外初始化静态实例(程序启动时执行)
Singleton Singleton::_inst; 

main函数之前,类的静态成员变量已经初始化好了,后面创建对象仅仅需要返回地址即可,根本没有线程安全问题

懒汉:相当于你打开一个网站,懒加载,不是一下子加载完,而是你刷新到下面它就加载对应的数据出来,类似按需获取,防止你第一次打开的网页太大而加载不出来

饿汉:你上来就加载完成,不存在线程安全问题

1.虽然懒汉存在线程安全问题,但是这个是可以解决的,所以问题不大

2.一些大型软件打开慢可能是因为单例模式使用了饿汉,有些使用了动态库也会变慢,因为动态库是启动的时候再加载,静态库是编译好的,但是会导致文件过大

3.使用饿汉,无法控制静态变量的创建顺序,这个取决于编译器(对于单个文件来说,你写代码写在最上面可以保证先初始化,但是项目一般都是很多个源文件,链接阶段:链接器把所有的.o文件合成可执行文件,链接器无法保证先链接哪个.o文件,这样就无法保证创建初始化顺序)

4.而且使用饿汉模式,构造函数不能创建线程和使用动态库的,你连main函数都没启动,你去创建别的线程是不可以的,而且可能连动态库也用不了

程序双击运行 → 操作系统加载器启动 → 加载可执行文件到内存 → 扫描可执行文件的依赖列表 → 加载所有隐式依赖的动态库到内存 → 执行动态库的初始化(全局变量/构造函数)→ 执行主程序的全局变量/饿汉单例初始化 → main函数启动

注意这个动态库的初始化和你自己写的单例的初始化是无法由程序员本身决定顺序的

相关推荐
代码不行的搬运工2 小时前
面向RDMA网络的Swift协议
开发语言·网络·swift
明月别枝惊鹊丶2 小时前
【C++】GESP 三级手册
java·开发语言·c++
不如打代码KK2 小时前
Java SPI与Spring Boot SPI的区别
java·开发语言·spring boot
代码or搬砖2 小时前
自定义注解全面详解
java·开发语言
心动啊1213 小时前
简单学下chromaDB
开发语言·数据库·python
江上鹤.1483 小时前
Day33类装饰器
开发语言·python
ZouZou老师3 小时前
C++设计模式之责任链模式:以家具生产为例
c++·设计模式·责任链模式
二川bro3 小时前
性能分析指南:Python cProfile优化实战
开发语言·python
lynnlovemin3 小时前
从暴力到高效:C++ 算法优化实战 —— 排序与双指针篇
java·c++·算法