目录
[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&);
// ...
};
设计细节:
- 设置为私有访问权限:如果仅声明不设置为 private,用户可在类外手动定义这两个函数,无法彻底禁止拷贝;私有化后,类外和派生类都无法访问这两个函数,从根本上堵死拷贝入口。
- 只声明不定义:该函数本身永远不会被正常调用,定义无实际意义;同时如果完整定义,类内的成员函数仍可调用它,无法彻底防止类内的拷贝行为。
这种方案的缺陷是:如果类内不小心调用了拷贝函数,只会在链接阶段报错,错误定位难度较高。
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;
}
设计细节:
- 构造函数私有化 :核心操作,禁止类外直接通过
栈定义、直接new的方式创建对象,只能通过类内的静态函数创建。- 禁用拷贝构造 :防止出现
HeapOnly* p = HeapOnly::CreateObject(); HeapOnly obj(*p);这种通过拷贝构造在栈上创建对象的漏洞。- 静态创建接口:静态成员函数不依赖类的实例,类外可直接调用;内部完成堆对象的创建,是唯一合法的实例化入口。
三、设计一个只能在栈上创建对象的类
需求与原理
与堆上唯一创建相反,该场景需要保证对象只能在栈上创建,随作用域自动析构,禁止通过
new在堆上创建对象,避免内存泄漏、生命周期失控问题。堆上创建对象的本质是:先调用全局 / 类内的
operator new分配内存,再调用构造函数初始化对象。因此实现核心是:禁用operator new和operator 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;
}
设计细节与边界说明:
- 禁用 operator new/delete:核心操作,无论直接 new 还是通过拷贝构造 new,都会调用类内的 operator new,禁用后彻底堵死堆创建路径。
- 静态创建接口返回值:返回栈对象的拷贝,C++17 标准强制开启复制消除(RVO),不会产生额外的拷贝构造开销,性能无损失。
- 边界局限性 :该方案无法禁止在静态区创建对象(如
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 种经典设计模式中最常用的创建型模式,它能保证一个类在整个程序的生命周期中,有且仅有一个实例,并提供一个全局唯一的访问点,该实例会被程序的所有模块共享。
典型适用场景:
- 全局配置管理:服务器的配置信息由一个单例对象统一读取、分发,保证所有模块拿到的配置一致。
- 日志系统:全局唯一的日志写入对象,避免多实例写入导致的日志乱序、文件句柄竞争。
- 资源池:线程池、数据库连接池、内存池,保证全局只有一个池实例,统一管理资源分配。
- 硬件设备管理器:如打印机、摄像头驱动,保证同一时间只有一个实例操作硬件。
单例模式核心设计要点:
- 私有化构造函数,堵死类外创建实例的所有路径;
- 禁用拷贝构造与拷贝赋值,防止通过拷贝创建第二个实例;
- 提供一个静态的全局访问接口,返回唯一的实例;
- 多线程环境下,必须保证实例初始化的线程安全。
单例模式有两种经典实现方案,以及现代 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;
}
关键设计细节:
- 双检锁机制:第一层检查过滤 99% 的已初始化场景,避免频繁加锁的性能开销;第二层加锁后的检查,彻底杜绝多线程并发创建的问题。
- RAII 锁保护 :使用
std::lock_guard代替手动lock/unlock,即使构造函数抛出异常,也能保证锁正常释放,避免死锁。- 自动垃圾回收 :内嵌的
CGarbo类利用静态变量程序结束时自动析构的特性,自动释放单例对象,避免内存泄漏。- 手动释放接口 :提供
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;
}
方案核心优势:
- 代码简单:无手动锁、无指针管理、无额外的垃圾回收逻辑,代码量极少,可读性极强。
- 线程安全:编译器保证初始化的线程安全,无需手动处理锁逻辑,彻底杜绝线程安全问题。
- 延迟加载 :只有第一次调用
GetInstance时,才会初始化实例,完全符合懒加载的核心需求。- 自动释放资源:局部静态变量在程序结束时会自动析构,无需手动编写释放逻辑,无内存泄漏风险。
注意:该特性仅在 C++11 及以后的标准中支持。
六、总结:
特殊类设计的核心,是利用 C++ 的语法特性,对类的创建、拷贝、继承、生命周期、实例数量做出精准的约束,从而贴合业务场景的核心需求。
从禁用拷贝的简单场景,到单例模式这种经典设计模式,本质都是对类的默认行为的精细化管控。在现代 C++ 开发中,我们应优先使用 C++11 及以后的新特性(=delete、final、局部静态变量线程安全保证等),写出更简洁、更安全、语义更明确的代码。
但需要注意:单例模式,会引入全局状态,增加代码耦合度,仅在真正需要全局唯一实例的场景下使用,不要滥用。
感谢阅读,本文如有错漏之处,烦请斧正。