C++中的特殊类设计

目录​​​​​​​

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

2. 请设计一个类,只能在堆上创建对象

3.请设计一个类,只能在栈上创建对象

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

5.请设计一个类,只能创建一个对象(单利模式)


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

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

C++98中

将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可

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

原因:

✸ 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了

✸ 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

C++11中

C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。

class CopyBan
{
// ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

2. 请设计一个类,只能在堆上创建对象

实现方式:

✸ 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。

✸ 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

#include <iostream>

class HeapOnly {
private:
    // 私有构造函数,防止在栈上创建对象
    HeapOnly() {}

public:
    // 阻止拷贝构造函数和赋值操作符,防止栈上对象的拷贝和赋值
    HeapOnly(const HeapOnly&) = delete;
    HeapOnly& operator=(const HeapOnly&) = delete;

    // 提供一个公有的静态方法用于创建对象
    static HeapOnly* CreateInstance() {
        return new HeapOnly();
    }

    // 提供一个公有的静态方法用于销毁对象
    static void DeleteInstance(HeapOnly* instance) {
        delete instance;
    }

    // 其他公有成员函数...
    void DoSomething() {
        std::cout << "Doing something..." << std::endl;
    }

    // 公有析构函数,确保对象可以正常销毁
    ~HeapOnly() {
        std::cout << "HeapOnly object destroyed" << std::endl;
    }
};

int main() {
    // 在栈上创建对象是不允许的,下面的代码会导致编译错误
    // HeapOnly stackOnly;

    // 在堆上创建对象
    HeapOnly* heapOnly = HeapOnly::CreateInstance();
    heapOnly->DoSomething();

    // 销毁堆上的对象
    HeapOnly::DeleteInstance(heapOnly);

    return 0;
}

在这个类中:

❍ 构造函数被声明为私有,这意味着你无法在类的外部直接创建对象。

❍ 拷贝构造函数和赋值操作符被删除,这防止了通过这些操作在栈上创建或复制对象。

❍ 提供了一个公有的静态成员函数 CreateInstance,它使用 new 运算符在堆上创建对象,并返回指向该对象的指针。

❍ 提供了一个公有的静态成员函数 DeleteInstance,它接收一个指向对象的指针并使用 delete 运算符销毁对象。

❍ 公有的析构函数确保了堆上分配的对象可以被正确地销毁。

通过上述设计,HeapOnly 类的对象只能通过 CreateInstance 方法在堆上创建,并且必须通过 DeleteInstance 方法来销毁。

3.请设计一个类,只能在栈上创建对象

在C++中,要设计一个类使其只能在栈上创建对象,我们需要做以下几件事情:

❍ 将类的构造函数设为公有,这样就可以在栈上创建对象。

❍ 将析构函数设为公有,以确保对象可以在栈上正常销毁。

❍ 将拷贝构造函数和赋值操作符设为私有或删除,以防止通过这些操作在堆上创建对象。

#include <iostream>

class StackOnly {
private:
    // 防止在堆上通过 new 创建对象
    void* operator new(size_t size) = delete;
    void* operator new[](size_t size) = delete;

    // 防止拷贝构造和赋值操作
    StackOnly(const StackOnly&) = delete;
    StackOnly& operator=(const StackOnly&) = delete;

public:
    // 公有的构造函数,允许在栈上创建对象
    StackOnly() {
        std::cout << "StackOnly object created on the stack." << std::endl;
    }

    // 公有的析构函数,确保对象可以正常销毁
    ~StackOnly() {
        std::cout << "StackOnly object destroyed." << std::endl;
    }

    // 其他公有成员函数...
    void DoSomething() {
        std::cout << "Doing something..." << std::endl;
    }
};

int main() {
    // 在栈上创建对象
    StackOnly stackOnly;
    stackOnly.DoSomething();

    // 尝试在堆上创建对象将导致编译错误
    // StackOnly* heapOnly = new StackOnly(); // 编译错误

    return 0;
}

在这个类中:

❍ 构造函数是公有的,允许在栈上创建对象。

❍ 析构函数也是公有的,确保对象可以正常销毁。

❍ 重载了 new 操作符并将其设置为私有或删除,以防止在堆上分配内存。

❍ 拷贝构造函数和赋值操作符被删除,防止对象被拷贝,这样即使有人试图通过拷贝构造函数在堆上创建对象,也会导致编译错误。

这样设计的 StackOnly 类,确保了它的对象只能通过栈分配创建,而不能通过堆分配创建。任何尝试在堆上创建 StackOnly 对象的操作都会导致编译错误。

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

C++98方式

// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承 
class NonInherit
{
public:
    static NonInherit GetInstance()
    {
        return NonInherit();
    }
private:
    NonInherit()
		{}
};

C++11方法

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

class A  final
{
//....
};

5.请设计一个类,只能创建一个对象(单利模式)

设计模式:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的 总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打 仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后 来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模 式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式:

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

单例模式有两种实现模式:

◉ 饿汉模式

就是说无轮你将来用不用,程序加载时就创建一个唯一的实例对象

#include <iostream>

class Singleton {
private:
    // 私有静态成员变量,在程序启动时初始化
    static Singleton instance;

    // 私有构造函数,防止外部创建实例
    Singleton() {}

    // 防止复制构造函数和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 提供一个公共的静态方法来获取单例实例
    static Singleton& getInstance() {
        return instance;
    }

    // 示例方法
    void doSomething() {
        std::cout << "Doing something..." << std::endl;
    }
};

// 在程序启动时初始化静态成员变量
Singleton Singleton::instance;

int main() {
    // 获取单例实例并调用方法
    Singleton& singleton = Singleton::getInstance();
    singleton.doSomething();

    return 0;
}

在这个例子中,Singleton 类的私有静态成员变量 instance 在程序启动时被初始化,确保了单例实例的唯一性。由于构造函数是私有的,因此无法通过 new 或其他方式在类外部创建 Singleton 的实例。

注意以下几点:

静态成员初始化:静态成员 instance 必须在类定义之外进行初始化。这是C++的规定。

删除复制构造函数和赋值操作:通过使用 delete 关键字,我们防止了复制构造函数和赋值操作符的使用,确保了单例不会被复制。

线程安全:在大多数情况下,C++的静态局部变量的初始化是线程安全的。但是,如果编译器不支持线程安全的静态局部变量初始化,那么这个饿汉模式实现就不能保证线程安全。在C++11及以后的版本中,静态局部变量的初始化是线程安全的。

资源占用:由于单例实例在程序启动时就已经创建,如果这个实例很大或者初始化过程很耗时,但实际并不一定会被使用,那么就会造成资源的浪费。

饿汉模式适用于单例对象较小,初始化开销不大,且几乎肯定会被使用的情况。如果需要延迟加载单例实例,可以考虑使用懒汉模式。

◉ 懒汉模式

懒汉模式(Lazy Initialization)是单例模式的一种实现方式,它将对象的实例化推迟到第一次使用时,而不是在类加载时。这样可以节省资源,特别是当单例对象初始化成本较高,或者不确定是否真的需要使用该对象时。

#include <iostream>
#include <mutex>

class Singleton {
private:
    // e用于指向单例对象的实例
    static Singleton* instance;
    static std::mutex mutex; // 用于线程同步

    // 私有构造函数,防止外部创建实例
    Singleton() {}

    // 防止复制构造函数和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 提供一个公共的静态方法来获取单例实例
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex); // 加锁
            if (instance == nullptr) { // 再次检查,以防止多个线程同时通过第一次检查
                instance = new Singleton();
            }
        }
        return instance;
    }

    // 示例方法
    void doSomething() {
        std::cout << "Doing something..." << std::endl;
    }

    // 提供一个公共的静态方法来释放单例实例
    static void releaseInstance() {
        std::lock_guard<std::mutex> lock(mutex);
        delete instance;
        instance = nullptr;
    }
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

int main() {
    // 获取单例实例并调用方法
    Singleton* singleton = Singleton::getInstance();
    singleton->doSomething();

    // 释放单例实例
    Singleton::releaseInstance();

    return 0;
}

在这个例子中,Singleton 类的静态成员 instance 被初始化为 nullptr,意味着单例对象在第一次调用 getInstance() 方法时才会被创建。

注意以下几点:

线程安全:为了确保在多线程环境下正确地创建单例实例,我们使用了互斥锁(std::mutex)来同步线程。在 getInstance() 方法中,我们首先检查实例是否为 nullptr,如果是,则加锁并再次检查实例是否为 nullptr,这是双重检查锁定模式(Double-Checked Locking Pattern)。

复制构造函数和赋值操作符被删除:与饿汉模式相同,我们通过 delete 关键字来删除复制构造函数和赋值操作符,以防止复制单例实例。

资源释放:在懒汉模式中,我们需要提供一个释放单例实例的方法,通常在程序结束时调用,以确保正确地释放资源。

懒汉模式适用于单例对象较大,初始化开销大,或者不确定是否需要使用该对象的情况。但是,由于需要考虑线程同步,它的实现比饿汉模式复杂,并且可能会有性能开销。

为什么私有静态成员变量,和私有静态指针变量,就能决定懒汉模式和饿汉模式?​​​​​​​

私有静态成员变量和私有静态指针变量在懒汉模式和饿汉模式中起到关键作用,它们的差异在于初始化的时机和方式,这决定了它们各自模式的特性。

饿汉模式(Eager Initialization)

在饿汉模式中,单例对象是通过私有静态成员变量直接实例化的。以下是关键点:

初始化时机:私有静态成员变量在类加载时自动初始化。这意味着无论是否需要使用该单例,它都会在程序启动时被创建。

内存分配:私有静态成员变量直接持有单例对象的实例,因此,一旦类被加载,内存就被分配。

例如

class Singleton {
private:
 static Singleton instance; // 私有静态成员变量

 Singleton() {}
 // ... 其他成员
};

在这个例子中,instance 在程序启动时由编译器自动初始化。

懒汉模式(Lazy Initialization)

在懒汉模式中,单例对象是通过私有静态指针变量来延迟实例化的。以下是关键点:

初始化时机:私有静态指针变量在类加载时被初始化为 nullptr(或空指针),它并不持有单例对象的实例。单例对象只在第一次调用 getInstance() 方法时被创建。

内存分配:内存是在第一次调用 getInstance() 方法时分配的,而不是在类加载时。

例如:

class Singleton {
private:
 static Singleton* instance; // 私有静态指针变量

 Singleton() {}
 // ... 其他成员
};

在这个例子中,instance 被初始化为 nullptr,单例对象只在 getInstance() 方法被首次调用时创建。

总结

饿汉模式:私有静态成员变量在类加载时自动实例化单例对象,确保了单例的唯一性和立即可用性,但可能会浪费资源。

懒汉模式:私有静态指针变量在类加载时不实例化单例对象,而是在第一次使用时才创建,这节省了资源,但需要考虑线程安全和延迟初始化的复杂性。

因此,是否使用私有静态成员变量或私有静态指针变量,以及它们的初始化时机,是区分饿汉模式和懒汉模式的关键。

接下来让我们观察下列代码

// 饿汉模式
// 1、多个饿汉模式的单例,某个对象初始化内容较多(读文件),会导致程序启动慢
// 2、A和B两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证
class InfoMgr
{
public:
    static InfoMgr& GetInstance()
    {
        return _ins;
    }

    void Print()
    {
        cout << _ip << endl;
        cout << _port << endl;
        cout << _buffSize << endl;
    }
private:
    InfoMgr(const InfoMgr&) = delete;
    InfoMgr& operator=(const InfoMgr&) = delete;

    InfoMgr()
    {
        cout << "InfoMgr()" << endl;
    }
private:
    string _ip = "127.0.0.1";
    int _port = 80;
    size_t _buffSize = 1024 * 1024;
    //...

    static InfoMgr _ins;
};

InfoMgr InfoMgr::_ins;


int main()
{		
		int a = 0;
    InfoMgr::GetInstance();
    //InfoMgr copy(InfoMgr::GetInstance());

    return 0;
}

通过观察我们发现,程序还未走到调用函数,就已经实例化出来_ins对象

class InfoMgr
{
public:
    static InfoMgr& GetInstance()
    {
        // 第一次调用时创建单例对象
        // 线程安全的风险
        if (_pins == nullptr)
        {
            _pins = new InfoMgr;
        }

        return *_pins;
    }

    void Print()
    {
        cout << _ip << endl;
        cout << _port << endl;
        cout << _buffSize << endl;
    }

    static void DelInstance()
    {
        delete _pins;
        _pins = nullptr;
    }

private:
    InfoMgr(const InfoMgr&) = delete;
    InfoMgr& operator=(const InfoMgr&) = delete;

    InfoMgr()
    {
        cout << "InfoMgr()" << endl;
    }
private:
    string _ip = "127.0.0.1";
    int _port = 80;
    size_t _buffSize = 1024 * 1024;
    //...

    static InfoMgr* _pins;
};

InfoMgr* InfoMgr::_pins = nullptr;

int main()
{
    InfoMgr::GetInstance().Print();
    InfoMgr::GetInstance().Print();

    return 0;
}

 而这个程序未走到定义部分只会初始化空指针

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值