C++中特殊类的设计方法,包括禁止拷贝的类、仅限堆栈创建的对象、禁止继承的类以及单例模式。文章详细解释了如何通过私有化构造函数、删除拷贝构造函数和赋值运算符来阻止对象拷贝,以及如何设计类以确保对象只能在堆或栈上创建。此外,还介绍了单例模式的两种实现方式:饿汉模式和懒汉模式,包括它们的优缺点和线程安全问题。这些设计模式对于控制对象的创建和管理具有重要意义。

特殊类设计

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

文章目录

  • 特殊类设计

    • 不能被拷贝的类
    • 只能在堆上创建对象的类
    • 只能在栈上创建对象的类
    • 不能被继承
    • 单例模式(只可以创建一个对象)

      • 饿汉模式
      • 懒汉模式

不能被拷贝的类

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

C++98写法:

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

C++11之后写法:

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;

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

  • 优点:实现较为简单。
  • 缺点:当单例对象较多的时候,会导致进程启动慢,因为各种单例对象在初始化的时候可能需要加载较多的资源。同时单例对象之间若存在互相依赖关系,将进一步导致效率降低,因为单例对象初始化的顺序不固定。

懒汉模式

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

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一个对象,已经初始化则直接返回即可。

优缺点:

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

标签: C++, 设计模式

添加新评论