Bootstrap

C++中特殊类设计/单例模式

特殊类设计

​ 本篇将会介绍一些特殊类的设计:不能拷贝的类、只能在堆上创建的类、只能在栈上创建的对象、不能被继承的类、只能创建一个对象的类(单例模式)。

不能被拷贝的类

​ 拷贝只会发生在两个场景中:拷贝构造函数和赋值运算符重载函数,因此想要让一个类禁止老贝,只需要让该类不能调用拷贝构造函数以及赋值运算符重载。如下:

C++98写法:

​ 在C++98中还没有delete关键字,所以只能把拷贝构造和赋值运算符重载给放在private域中,这样类外对象就访问不到这两个对象,也就不能进行拷贝。

class CopyBan {
public:
    // ...
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    // ...
};

C++11之后写法:

​ C++11可以直接使用delete关键字将这两个函数删除掉,如下:

class CopyBan {
public:
    // ...
    CopyBan(const CopyBan&) = delete;
    CopyBan& operator=(const CopyBan&) = delete;
};
只能在堆上创建对象的类

​ 只能在堆上创建对象,也就意味着我们不能在类外直接声明定义,所以我们需要将构造函数私有化(不能禁止,还需要创建),同时还需要将拷贝构造函数禁止掉(或者私有化),因为可能通过拷贝构造在栈上创建对象。

​ 既然不能通过在类外定义,那么我们就需要在类内提供一个静态成员函数用于申请堆上的对象,如下:

class HeapOnly {
public:
    static HeapOnly* CreateObj() {
        return new HeapOnly;
    }

    // ...
    ~HeapOnly() {
        // ...
        delete this;
    }
private:
    HeapOnly() {}

    HeapOnly(const HeapOnly&) {}
    // HeapOnly(const HeapOnly&) = delete;
    // ...
};

​ 同时还需要在析构函数中将自己给删除掉。

只能在栈上创建对象的类

​ 既然是只能在栈上创建的对象,那么我们就应该禁掉new和delete,但是new和delete是两个全局的关键字,我们可以在类内将new和delete重载,然后使用delete删除掉,这样对象不会被new出来了,如下:

// 第一种写法
class StackOnly {
public:
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
    // ...
};

// 第二种写法
class StackOnly {
public:
    static StackOnly CreateObj() {
        return StackOnly();
    }

    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
    // ...
private:
    StackOnly() {}
};
不能被继承

​ 只需要将类的构造函数给私有化,子类调用不到基类的构造函数,就不可以继承基类,如下:

class NonIherit {
public:
    static NonIherit GetInstance() {
        return NonIherit();
    }
private:
    NonIherit() {}
};

// 也可以使用C++11中的关键字final
class NonIherit final {
    // ...
}
单例模式(只可以创建一个对象)

​ 一个类只能创建一个对象,这就是单例模式。该模式可以保证系统中该类中只有一个实例,并提供一个访问它的全局访问点。该实例被所有程序模块共享。比如在某个服务器程序中,将该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过单例对象来获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

​ 单例模式一共有两种设计模式:

饿汉模式

​ 饿汉模式下的单例对象,在程序启动时就创建(进入main函数前),不管将来是否会使用到该单例对象,都会创建出来。如下:

class ConfigInfo {
public:
    static ConfigInfo* GetInstance() {
        return &_sinfo;
    }
	// 删除拷贝构造和赋值重载
    ConfigInfo(const ConfigInfo&) = delete;
    ConfigInfo& operator=(const ConfigInfo&) = delete;
	// ...
private:
    // 构造函数私有化
    ConfigInfo() {}

    std::string _ip;
    uint16_t _port;
    // ...

    // 静态变量,在类内声明
    static ConfigInfo _sinfo;
};

// 类外定义
ConfigInfo ConfigInfo::_sinfo;

​ 如上所示,我们先将构造函数私有化,同时删除掉拷贝构造和赋值重载函数,接着在类内声明静态对象,在类外进行定义,这个时候就创建出了全局唯一的一个对象,我们可以使用GetInstance进行该对象访问。

​ 对于饿汉模式的优缺点如下:

优点:

​ 实现较为简单。

缺点:

​ 当单例对象较多的时候,会导致进程启动慢,因为各种单例对象在初始化的时候可能需要加载较多的资源。

​ 同时单例对象之间若存在互相依赖关系,将进一步导致效率降低,因为单例对象初始化的顺序不固定。

懒汉模式

​ 懒汉模式就是只有在需要使用该单例对象的时候才会加载该单例对象的资源,也就是一种延迟加载。实现如下:

class ConfigInfo {
public:
    // static ConfigInfo* GetInstance() {
    //     // 这种方式在C++11之前,多线程调用会存在线程安全问题
    //     // 可能会创建出多个单例对象
    //     // C++11对该问题进行了特殊处理
    //     static ConfigInfo info;
    //     return &info;
    // }

    static ConfigInfo* GetInstance() {
        if (_spinfo == nullptr) {
            // 判断两次_spinfo是否为nullptr是因为我们只需呀对
            // _spinfo变量初始化一次,也就是上锁初始化一次
            // 假若只判断_spinfo是否为nullptr,则每次都要加锁
            // 较为浪费效率
            std::unique_lock<std::mutex> lock(_mtx);
            if (_spinfo == nullptr) 
                _spinfo = new ConfigInfo;
        }
        return _spinfo;
    }

    // 删除拷贝构造和赋值重载
    ConfigInfo(const ConfigInfo&) = delete;
    ConfigInfo& operator=(const ConfigInfo&) = delete;
    // ...

    void SetIp(const std::string& ip) {
        _ip = ip;
    }

    std::string GetIp() {
        return _ip;
    }
private:
    // 构造函数私有化
    ConfigInfo() {}

    std::string _ip;
    uint16_t _port;
    // ...

    static std::mutex _mtx;
    static ConfigInfo* _spinfo;
};

std::mutex ConfigInfo::_mtx;
ConfigInfo* ConfigInfo::_spinfo = nullptr;

​ 如上所示,实现懒汉单例模式一共存在两种方式:

第一种:

​ 直接在GetInstance函数内定义一个静态的对象,每一次返回即可。这种方式实现得最为简单,不过这种方式在C++11之前会存在线程安全问题。C++11对这样的单例模式进行了特殊处理。

第二种:

​ 定义静态对象指针,以及锁,在类外定义该类和对象指针(两个变量基本没有消耗啥资源),然后在GetInstance函数中判断对象静态指针是否被初始化,没有初始化则new一个对象,已经初始化则直接返回即可。

​ 优缺点:

​ 优点:第一次使用实力对象的时候才创建对象,进程启动无负载。多个单例对象实力启动顺序可以控制。

​ 缺点:实现较为复杂。

;