【C++】特殊类设计与单例模式

目录

一、设计一个不能被拷贝的类

原理

[C++98 实现方案](#C++98 实现方案)

设计细节:

[C++11 实现方案](#C++11 实现方案)

方案优势:

二、设计一个只能在堆上创建对象的类

需求与原理

完整实现方案

设计细节:

三、设计一个只能在栈上创建对象的类

需求与原理

完整实现方案

设计细节与边界说明:

四、设计一个不能被继承的类

需求与原理

[C++98 实现方案](#C++98 实现方案)

[C++11 实现方案(推荐)](#C++11 实现方案(推荐))

方案优势:

五、单例模式(只能创建一个对象的类)

模式概述:

典型适用场景:

单例模式核心设计要点:

[1. 饿汉模式](#1. 饿汉模式)

核心原理

完整实现代码

优缺点与适用场景:

[2. 懒汉模式(延迟加载)](#2. 懒汉模式(延迟加载))

核心原理:

完整实现代码

关键设计细节:

优缺点与适用场景:

[3. 现代 C++ 最佳实践:Meyers 单例](#3. 现代 C++ 最佳实践:Meyers 单例)

完整实现代码

方案核心优势:

六、总结:


一、设计一个不能被拷贝的类

原理

C++ 中,对象的拷贝行为只会发生在两个核心场景:拷贝构造函数拷贝赋值运算符重载。想要彻底禁止一个类的拷贝能力,本质就是禁用这两个成员函数。

C++98 实现方案

C++98 标准下,我们通过「私有化 + 只声明不定义」的方式实现拷贝禁用:

cpp 复制代码
class CopyBan
{
// ... 其他类成员
private:
    // 只声明不定义,且设置为私有访问权限
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
// ...
};
设计细节:
  1. 设置为私有访问权限:如果仅声明不设置为 private,用户可在类外手动定义这两个函数,无法彻底禁止拷贝;私有化后,类外和派生类都无法访问这两个函数,从根本上堵死拷贝入口。
  2. 只声明不定义:该函数本身永远不会被正常调用,定义无实际意义;同时如果完整定义,类内的成员函数仍可调用它,无法彻底防止类内的拷贝行为。

这种方案的缺陷是:如果类内不小心调用了拷贝函数,只会在链接阶段报错,错误定位难度较高。

C++11 实现方案

C++11 扩展了delete关键字的用法,在默认成员函数后跟上=delete,可以让编译器直接删除该默认函数,在编译期就拦截拷贝行为,是当前的首选方案:

cpp 复制代码
class CopyBan
{
// ... 其他类成员
public:
    CopyBan() = default;
    // 直接删除拷贝构造与拷贝赋值函数
    CopyBan(const CopyBan&) = delete;
    CopyBan& operator=(const CopyBan&) = delete;
// ...
};
方案优势:
  • 语义更明确:=delete直接告诉编译器和代码阅读者,这个函数被禁用,可读性极强。
  • 报错时机更早:任何尝试拷贝的行为都会在编译期触发报错,问题定位更简单。
  • 无副作用:不会影响类的其他默认行为,也不会出现类内误调用的问题。

二、设计一个只能在堆上创建对象的类

需求与原理

在一些场景中(如大体积对象、生命周期需要手动控制、多态基类设计),我们需要保证类的对象只能通过new在堆上创建,禁止在栈、全局 / 静态区创建对象。

栈上、全局区创建对象的核心前提是:编译器能访问到类的构造函数,完成对象的构造与内存分配。因此实现的核心就是:堵死栈上创建的所有路径,仅开放堆上创建的唯一入口。

完整实现方案

cpp 复制代码
class HeapOnly
{
public:
    // 静态成员函数:提供堆对象创建的唯一全局入口
    static HeapOnly* CreateObject()
    {
        // 内部调用构造函数,在堆上创建对象
        return new HeapOnly();
    }

    // 可选:提供对象销毁接口,规范内存管理
    static void DestroyObject(HeapOnly* ptr)
    {
        delete ptr;
    }

    // 禁用拷贝构造,防止通过拷贝在栈上创建对象
    HeapOnly(const HeapOnly&) = delete;
    HeapOnly& operator=(const HeapOnly&) = delete;

private:
    // 构造函数私有化:堵死类外直接创建对象的所有路径
    HeapOnly() = default;
    // 可选:私有化析构函数,进一步限制栈对象创建(栈对象析构需要访问析构函数)
    ~HeapOnly() = default;
};

// 使用示例
int main()
{
    // HeapOnly obj; // 编译报错:构造函数私有,无法在栈上创建
    // HeapOnly* p = new HeapOnly(); // 编译报错:构造函数私有,无法直接new
    HeapOnly* p = HeapOnly::CreateObject(); // 正常创建堆对象
    HeapOnly::DestroyObject(p); // 规范销毁
    return 0;
}
设计细节:
  1. 构造函数私有化 :核心操作,禁止类外直接通过栈定义直接new的方式创建对象,只能通过类内的静态函数创建。
  2. 禁用拷贝构造 :防止出现HeapOnly* p = HeapOnly::CreateObject(); HeapOnly obj(*p);这种通过拷贝构造在栈上创建对象的漏洞。
  3. 静态创建接口:静态成员函数不依赖类的实例,类外可直接调用;内部完成堆对象的创建,是唯一合法的实例化入口。

三、设计一个只能在栈上创建对象的类

需求与原理

与堆上唯一创建相反,该场景需要保证对象只能在栈上创建,随作用域自动析构,禁止通过new在堆上创建对象,避免内存泄漏、生命周期失控问题。

堆上创建对象的本质是:先调用全局 / 类内的operator new分配内存,再调用构造函数初始化对象。因此实现核心是:禁用operator newoperator delete,堵死堆内存分配的入口。

完整实现方案

cpp 复制代码
class StackOnly
{
public:
    // 静态成员函数:提供栈对象创建的唯一入口
    static StackOnly CreateObj()
    {
        // 直接在栈上创建并返回对象(C++17后复制消除,无额外拷贝开销)
        return StackOnly();
    }

    // 禁用operator new/delete,彻底堵死堆上创建的路径
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
    // 可选:禁用数组版本的new/delete,堵死数组对象的堆创建
    void* operator new[](size_t size) = delete;
    void operator delete[](void* p) = delete;

private:
    // 构造函数私有化,禁止类外直接创建
    StackOnly() : _a(0) {}

private:
    int _a;
};

// 使用示例
int main()
{
    // StackOnly obj; // 编译报错:构造函数私有
    // StackOnly* p = new StackOnly(); // 编译报错:operator new被删除
    StackOnly obj = StackOnly::CreateObj(); // 正常创建栈对象
    return 0;
}
设计细节与边界说明:
  1. 禁用 operator new/delete:核心操作,无论直接 new 还是通过拷贝构造 new,都会调用类内的 operator new,禁用后彻底堵死堆创建路径。
  2. 静态创建接口返回值:返回栈对象的拷贝,C++17 标准强制开启复制消除(RVO),不会产生额外的拷贝构造开销,性能无损失。
  3. 边界局限性 :该方案无法禁止在静态区创建对象(如static StackOnly obj = StackOnly::CreateObj();),因为静态区内存分配不依赖 operator new。但绝大多数业务场景中,核心需求是禁止堆上创建,该方案完全满足需求。

四、设计一个不能被继承的类

需求与原理

在工具类、基础库封装等场景中,我们希望一个类的行为固定,不允许被派生类继承、重写,避免破坏原有逻辑。

C++ 中,派生类实例化时,必须先调用基类的构造函数完成基类部分的初始化。如果基类的构造函数无法被派生类访问,那么派生类就无法完成实例化,也就无法被继承。

C++98 实现方案

cpp 复制代码
// C++98 禁止继承的实现
class NonInherit
{
public:
    // 静态接口提供类的实例化入口
    static NonInherit GetInstance()
    {
        return NonInherit();
    }

private:
    // 构造函数私有化,派生类无法访问
    NonInherit() {}
};

// 尝试继承会编译报错:基类构造函数私有,无法调用
// class Derived : public NonInherit {};

该方案的缺陷是:类本身也无法在类外直接实例化,只能通过静态接口创建对象,有副作用,仅能实现「无法被继承」的需求,使用场景有限。

C++11 实现方案(推荐)

C++11 新增了final关键字,专门用于修饰类,表示该类不能被任何派生类继承。

cpp 复制代码
// final修饰类,禁止被继承
class A final
{
    // 类的正常成员,不影响实例化、拷贝等行为
public:
    A() = default;
};

// 编译直接报错:无法将final类A作为基类
// class B : public A {};
方案优势:
  • 语义清晰:final关键字直接表达「禁止继承」的设计意图,可读性高。
  • 无副作用:不影响类本身的实例化、拷贝、移动等所有默认行为,仅限制继承能力。
  • 编译期强校验:任何尝试继承该类的行为都会在编译期直接报错,提前拦截问题。

五、单例模式(只能创建一个对象的类)

模式概述:

单例模式是 23 种经典设计模式中最常用的创建型模式,它能保证一个类在整个程序的生命周期中,有且仅有一个实例,并提供一个全局唯一的访问点,该实例会被程序的所有模块共享。

典型适用场景:
  • 全局配置管理:服务器的配置信息由一个单例对象统一读取、分发,保证所有模块拿到的配置一致。
  • 日志系统:全局唯一的日志写入对象,避免多实例写入导致的日志乱序、文件句柄竞争。
  • 资源池:线程池、数据库连接池、内存池,保证全局只有一个池实例,统一管理资源分配。
  • 硬件设备管理器:如打印机、摄像头驱动,保证同一时间只有一个实例操作硬件。
单例模式核心设计要点:
  1. 私有化构造函数,堵死类外创建实例的所有路径;
  2. 禁用拷贝构造与拷贝赋值,防止通过拷贝创建第二个实例;
  3. 提供一个静态的全局访问接口,返回唯一的实例;
  4. 多线程环境下,必须保证实例初始化的线程安全。

单例模式有两种经典实现方案,以及现代 C++ 的最佳实践方案,下面逐一讲解。

1. 饿汉模式

核心原理

不管程序后续是否使用这个实例,在程序启动时(main 函数执行前)就完成唯一实例的初始化,提前创建好实例,等待被使用,(还不确定是否会用到,就急不可耐地去创建了)因此被称为饿汉模式。

完整实现代码
cpp 复制代码
// 饿汉模式单例实现
class SingletonHungry
{
public:
    // 全局唯一的访问接口,返回实例的引用/指针
    static SingletonHungry& GetInstance()
    {
        return m_instance;
    }

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

private:
    // 构造函数私有化
    SingletonHungry() = default;
    // 私有析构函数,控制生命周期
    ~SingletonHungry() = default;

    // 静态成员变量:程序启动时就完成初始化的唯一实例
    static SingletonHungry m_instance;
};

// 类外定义静态成员变量,程序入口前完成初始化
SingletonHungry SingletonHungry::m_instance;

// 使用示例
int main()
{
    // 全局唯一访问方式
    SingletonHungry& instance = SingletonHungry::GetInstance();
    return 0;
}
优缺点与适用场景:
优点 缺点
实现极其简单,代码量极少 程序启动时初始化,会增加进程启动耗时
天然线程安全,无并发问题(main 函数前单线程初始化) 多个单例类跨编译单元时,初始化顺序无法保证,存在依赖风险
运行时访问效率极高,无任何额外开销 若实例从未被使用,会造成内存与资源浪费

适用场景:单例对象构造逻辑简单、耗时极短,无跨单例依赖关系,且程序运行中一定会被使用的场景。


2. 懒汉模式(延迟加载)

核心原理:

只有第一次调用获取实例的接口时,才会创建唯一实例,程序启动时不做任何初始化,实现延迟加载,避免启动耗时和资源浪费,(不用我就不创建,等用到了再说,和拖延症类似)因此被称为懒汉模式。

懒汉模式的核心是解决多线程环境下的线程安全问题:多个线程同时第一次调用接口时,可能会重复创建实例,因此需要通过双检锁(DCL, Double-Check Locking) 保证安全与性能。

完整实现代码
cpp 复制代码
#include <mutex>
#include <iostream>

// 懒汉模式(双检锁)单例实现
class SingletonLazy
{
public:
    // 全局唯一访问接口
    static SingletonLazy* GetInstance()
    {
        // 第一层检查:实例已创建则直接返回,避免每次调用都加锁
        if (m_pInstance == nullptr)
        {
            // 加锁:保证临界区同一时间只有一个线程进入
            std::lock_guard<std::mutex> lock(m_mtx);
            // 第二层检查:防止多个线程同时通过第一层检查,重复创建实例
            if (m_pInstance == nullptr)
            {
                m_pInstance = new SingletonLazy();
            }
        }
        return m_pInstance;
    }

    // 手动释放接口:适用于中途需要释放、程序结束前需要持久化的场景
    static void DelInstance()
    {
        std::lock_guard<std::mutex> lock(m_mtx);
        if (m_pInstance != nullptr)
        {
            delete m_pInstance;
            m_pInstance = nullptr;
        }
    }

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

private:
    // 构造函数私有化
    SingletonLazy() = default;

    // 析构函数:可实现持久化、资源释放等逻辑
    ~SingletonLazy()
    {
        // 示例:程序结束前将数据写入文件,完成持久化
        // FILE* fout = fopen("config.txt", "w");
        // if (fout) {
        //     // 数据写入逻辑
        //     fclose(fout);
        // }
        std::cout << "~SingletonLazy() 析构执行" << std::endl;
    }

    // 单例对象指针
    static SingletonLazy* m_pInstance;
    // 互斥锁:保证线程安全
    static std::mutex m_mtx;

    // 内嵌垃圾回收类:程序结束时自动释放单例
    class CGarbo
    {
    public:
        ~CGarbo()
        {
            if (SingletonLazy::m_pInstance != nullptr)
            {
                delete SingletonLazy::m_pInstance;
            }
        }
    };
    // 静态垃圾回收对象:程序结束时自动析构,触发单例释放
    static CGarbo Garbo;
};

// 类外静态成员初始化
SingletonLazy* SingletonLazy::m_pInstance = nullptr;
std::mutex SingletonLazy::m_mtx;
SingletonLazy::CGarbo SingletonLazy::Garbo;

// 多线程测试示例
#include <thread>
int main()
{
    // 两个线程同时获取实例,验证地址完全一致
    std::thread t1([](){ std::cout << SingletonLazy::GetInstance() << std::endl; });
    std::thread t2([](){ std::cout << SingletonLazy::GetInstance() << std::endl; });
    t1.join();
    t2.join();

    std::cout << SingletonLazy::GetInstance() << std::endl;
    return 0;
}
关键设计细节:
  1. 双检锁机制:第一层检查过滤 99% 的已初始化场景,避免频繁加锁的性能开销;第二层加锁后的检查,彻底杜绝多线程并发创建的问题。
  2. RAII 锁保护 :使用std::lock_guard代替手动lock/unlock,即使构造函数抛出异常,也能保证锁正常释放,避免死锁。
  3. 自动垃圾回收 :内嵌的CGarbo类利用静态变量程序结束时自动析构的特性,自动释放单例对象,避免内存泄漏。
  4. 手动释放接口 :提供DelInstance,适配中途释放、析构前持久化等特殊业务场景。
优缺点与适用场景:
优点 缺点
延迟加载,不占用程序启动时间,不浪费资源 实现相对复杂,需要处理线程安全问题
可自由控制多个单例的初始化顺序,解决跨编译单元依赖问题 存在微小的锁开销(可忽略不计)

适用场景

单例对象构造耗时、占用资源多,不一定会被程序使用,或多个单例之间存在初始化依赖的场景。


3. 现代 C++ 最佳实践:Meyers 单例

C++11 标准明确规定:当局部静态变量在多线程环境下第一次被初始化时,只有一个线程会执行初始化逻辑,其他线程会等待初始化完成,不会出现并发初始化问题

基于这个特性,Scott Meyers 提出了最简洁、最安全的单例实现方案,被称为 Meyers 单例,是当前现代 C++ 开发中的首选方案。

完整实现代码
cpp 复制代码
// Meyers单例:现代C++最佳实践
class Singleton final
{
public:
    // 全局唯一访问接口
    static Singleton& GetInstance()
    {
        // 局部静态变量:第一次调用时初始化,编译器保证线程安全
        static Singleton instance;
        return instance;
    }

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

private:
    // 构造函数私有化
    Singleton() = default;
    // 析构函数私有化
    ~Singleton() = default;
};

// 使用示例
int main()
{
    Singleton& instance = Singleton::GetInstance();
    return 0;
}
方案核心优势:
  1. 代码简单:无手动锁、无指针管理、无额外的垃圾回收逻辑,代码量极少,可读性极强。
  2. 线程安全:编译器保证初始化的线程安全,无需手动处理锁逻辑,彻底杜绝线程安全问题。
  3. 延迟加载 :只有第一次调用GetInstance时,才会初始化实例,完全符合懒加载的核心需求。
  4. 自动释放资源:局部静态变量在程序结束时会自动析构,无需手动编写释放逻辑,无内存泄漏风险。
    注意:该特性仅在 C++11 及以后的标准中支持。

六、总结:

特殊类设计的核心,是利用 C++ 的语法特性,对类的创建、拷贝、继承、生命周期、实例数量做出精准的约束,从而贴合业务场景的核心需求。
从禁用拷贝的简单场景,到单例模式这种经典设计模式,本质都是对类的默认行为的精细化管控。在现代 C++ 开发中,我们应优先使用 C++11 及以后的新特性(=deletefinal、局部静态变量线程安全保证等),写出更简洁、更安全、语义更明确的代码。
但需要注意:单例模式,会引入全局状态,增加代码耦合度,仅在真正需要全局唯一实例的场景下使用,不要滥用。


感谢阅读,本文如有错漏之处,烦请斧正。

相关推荐
朱一头zcy2 小时前
设计模式入门:最简单的单例模式
笔记·单例模式·设计模式
2301_789015622 小时前
DS进阶:红黑树
c语言·开发语言·数据结构·c++·算法·r-tree·lsm-tree
¿i?2 小时前
吃什么?作业复习LinkedList==DEBUG
数据结构·c++·学习
liuyao_xianhui2 小时前
动态规划_简单多dp问题_打家劫舍_打家劫舍2_C++
java·开发语言·c++·算法·动态规划
2301_789015627 小时前
DS进阶:AVL树
开发语言·数据结构·c++·算法
CoderCodingNo11 小时前
【GESP】C++五级练习题 luogu-P1182 数列分段 Section II
开发语言·c++·算法
Qt学视觉12 小时前
AI2-Paddle环境搭建
c++·人工智能·python·opencv·paddle
myloveasuka14 小时前
C++进阶:利用作用域解析运算符 :: 突破多态与变量隐藏
开发语言·c++
keep intensify14 小时前
康复训练 5
linux·c++