【C++进阶】特殊类的设计,从堆栈限制到单例模式的实战,代码助你理解特殊类的实际应用
大家好,我是程序员小青蛙。今天介绍特殊类的设计。

在 C++ 面向对象编程中,我们通常通过类的封装、继承和多态来实现通用的对象模型。但在实际工程场景中,我们经常需要对类的行为施加特殊限制:比如某个类只能创建一个实例、只能在堆上分配内存、不能被拷贝或继承。这些特殊类设计是 C++ 进阶开发的核心技巧,也是后端开发面试的高频考点。本文将结合代码示例,系统讲解 5 种最常用的特殊类设计方法,并深入解析单例模式的线程安全实现。
一、只能在堆上创建对象的类(HeapOnly)
需求背景
有些类的对象生命周期需要完全由程序员手动控制,或者对象体积过大不适合放在栈上(栈空间通常只有几 MB),此时需要限制该类只能通过new在堆上创建。
实现思路
核心是阻止栈上构造和拷贝构造,只提供静态工厂方法来创建堆对象:
- 将构造函数和拷贝构造函数设为私有,防止外部直接调用构造函数在栈上创建对象,也防止通过拷贝栈对象生成新对象
- 提供一个静态成员函数,内部通过
new创建堆对象并返回其指针
完整代码实现
class HeapOnly {
public:
// 静态工厂方法:唯一的对象创建入口
static HeapOnly* CreateObject() {
return new HeapOnly();
}
// 禁用拷贝构造和赋值运算符(C++11推荐)
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
private:
// 构造函数私有:禁止外部直接构造
HeapOnly() {}
// C++98兼容写法:只声明不定义,且设为私有
// HeapOnly(const HeapOnly&);
// HeapOnly& operator=(const HeapOnly&);
};
关键注意点
如果不禁用拷贝构造,会出现漏洞:
HeapOnly* ptr = HeapOnly::CreateObject();
HeapOnly obj = *ptr; // 拷贝构造生成栈对象,破坏限制
因此必须同时禁用拷贝构造和赋值运算符。
二、只能在栈上创建对象的类(StackOnly)
需求背景
有些类的对象生命周期应该与函数调用栈绑定,比如轻量级的临时对象、不允许动态分配的资源句柄,此时需要限制该类只能在栈上创建。
实现方法一:静态工厂返回栈对象
与堆上类思路类似,构造函数私有,静态方法返回栈上的临时对象(依赖 C++ 返回值优化 RVO):
class StackOnly {
public:
static StackOnly CreateObject() {
return StackOnly(); // 返回栈临时对象,RVO优化后无拷贝
}
private:
StackOnly() {} // 构造函数私有
};
缺点:无法阻止在堆上用定位 new 构造对象(需要额外屏蔽)。
实现方法二:屏蔽operator new和operator delete
new运算符的底层会调用全局的void* operator new(size_t)函数,我们可以将该函数私有化,从而禁止外部使用new创建对象:
class StackOnly {
public:
StackOnly() {} // 构造函数公有,允许栈上直接构造
private:
// 屏蔽普通new和delete
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
// 屏蔽定位new(防止在堆内存上手动构造)
void* operator new(size_t, void* place) = delete;
};
优点:可以直接在栈上定义对象StackOnly obj;,使用更自然,是更推荐的写法。
三、不能被拷贝的类(CopyBan)
需求背景
对于独占资源的类(如互斥锁、文件句柄、数据库连接),拷贝会导致资源重复释放或所有权混乱,因此需要禁止类的拷贝行为。
实现原理
C++ 中对象的拷贝只会发生在两个场景:拷贝构造函数和赋值运算符重载。只要禁用这两个函数,就能完全禁止拷贝。
两种实现方式
C++98 方式:私有声明不定义
class CopyBan {
public:
CopyBan() {}
private:
// 只声明不定义,且设为私有
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
};
原理:
- 设为私有:防止外部调用这两个函数
- 不定义:防止类内部成员函数不小心调用,导致链接错误
C++11 方式:= delete关键字
C++11 扩展了delete的用法,用= delete修饰默认成员函数,会让编译器直接删除该函数,调用时直接编译报错,语义更清晰:
class CopyBan {
public:
CopyBan() {}
// 直接删除拷贝构造和赋值运算符
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
四、不能被继承的类(NonInherit)
需求背景
当一个类的设计不希望被扩展,或者继承会破坏其封装性时,需要禁止该类被继承。
两种实现方式
C++98 方式:构造函数私有化
派生类构造时必须先调用基类的构造函数,如果基类构造函数私有,派生类无法调用,因此无法继承:
class NonInherit {
public:
static NonInherit GetInstance() {
return NonInherit();
}
private:
NonInherit() {} // 构造函数私有
};
C++11 方式:final关键字(推荐)
C++11 新增了final关键字,用final修饰的类表示 "最终类",任何试图继承该类的行为都会直接编译报错:
class A final {
// 类的成员
};
// 编译错误:无法继承final类A
class B : public A {};
这种方式语义明确,代码简洁,是 C++11 及以后的标准写法。
五、只能创建一个对象的类(单例模式)
单例模式是最常用的设计模式之一,它保证一个类在整个程序中只有一个实例,并提供一个全局访问点。典型应用场景:配置管理器、日志器、数据库连接池、线程池等。
单例模式主要分为饿汉模式和懒汉模式两种实现。
1. 饿汉模式(Eager Singleton)
核心思想
不管程序是否会用到这个单例,在程序启动(main 函数执行前)就完成实例的初始化。
代码实现
class Singleton {
public:
// 全局唯一访问点
static Singleton* GetInstance() {
return &m_instance;
}
// 禁用拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
// 构造函数私有
Singleton() {}
// 静态成员变量:程序入口前初始化
static Singleton m_instance;
};
// 类外初始化静态成员
Singleton Singleton::m_instance;
优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,代码量少 | 程序启动慢(如果单例初始化耗时) |
| 天然线程安全(静态变量在 main 前单线程初始化) | 多个单例的初始化顺序无法控制(若 A 依赖 B 但 A 先初始化,会出错) |
适用场景
单例对象初始化耗时短、程序启动时必须使用、多线程高并发场景下对性能要求高。
2. 懒汉模式(Lazy Singleton)
核心思想
延迟加载:只有第一次调用GetInstance()获取实例时,才创建对象。解决了饿汉模式启动慢和初始化顺序不可控的问题。
线程安全问题与解决方案
懒汉模式在多线程环境下存在线程安全问题:多个线程同时进入GetInstance()时,可能会创建多个实例。下面逐步讲解正确的线程安全实现。
❌ 错误写法 1:无锁
static Singleton* GetInstance() {
if (_psins == nullptr) {
_psins = new Singleton(); // 多线程同时进入会创建多个实例
}
return _psins;
}
❌ 错误写法 2:单检查加锁
static Singleton* GetInstance() {
_smtx.lock();
if (_psins == nullptr) {
_psins = new Singleton();
}
_smtx.unlock();
return _psins;
}
问题:每次调用GetInstance()都要加锁,性能极低,尤其是单例创建完成后,加锁完全是多余的。
✅ 正确写法 1:双检查锁(DCL)
双检查锁(Double-Checked Locking)是兼顾线程安全和性能的经典实现:
#include <mutex>
class Singleton {
public:
static Singleton* GetInstance() {
// 第一次检查:对象创建后,直接返回,无需加锁
if (_psins == nullptr) {
std::lock_guard<std::mutex> lock(_smtx); // RAII智能锁,自动释放
// 第二次检查:防止多个线程通过第一次检查后,排队创建多个对象
if (_psins == nullptr) {
_psins = new Singleton();
}
}
return _psins;
}
// 禁用拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
static Singleton* _psins;
static std::mutex _smtx;
// 内嵌垃圾回收类:程序结束时自动释放单例
class CGarbo {
public:
~CGarbo() {
if (Singleton::_psins) {
delete Singleton::_psins;
}
}
};
static CGarbo Garbo;
};
// 类外初始化静态成员
Singleton* Singleton::_psins = nullptr;
std::mutex Singleton::_smtx;
Singleton::CGarbo Singleton::Garbo;
双检查锁原理:
- 第一次
nullptr检查:提高性能,对象创建后不再加锁 - 加锁:保证同一时间只有一个线程进入临界区
- 第二次
nullptr检查:防止多个线程通过第一次检查后,排队创建多个实例
✅ 正确写法 2:静态局部变量懒汉(C++11 + 推荐)
C++11 标准规定:函数内的静态局部变量初始化是线程安全的,编译器会保证只有一个线程执行初始化代码。这是目前最简洁的单例实现:
class Singleton {
public:
static Singleton& GetInstance() {
// 静态局部变量:第一次调用时初始化,线程安全
static Singleton instance;
return instance;
}
// 禁用拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};
优点:
- 代码最简洁,无需手动管理锁和内存
- 线程安全(C++11 及以后)
- 程序结束时自动调用析构函数释放资源
注意:C++11 之前的编译器不保证静态局部变量初始化的线程安全,请勿使用。
饿汉模式 vs 懒汉模式对比
| 对比维度 | 饿汉模式 | 懒汉模式(双检查锁) | 懒汉模式(静态局部变量) |
|---|---|---|---|
| 初始化时机 | 程序启动时 | 第一次使用时 | 第一次使用时 |
| 线程安全 | 是 | 是 | 是(C++11+) |
| 实现复杂度 | 简单 | 中等 | 最简单 |
| 启动速度 | 慢 | 快 | 快 |
| 初始化顺序控制 | 不能 | 能 | 能 |
| 推荐指数 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
总结
特殊类设计的核心思想是通过控制成员访问权限和利用 C++ 语言特性,限制类的默认行为,从而满足特定的业务需求:
- 限制堆 / 栈创建:通过私有构造函数或屏蔽
operator new实现 - 禁止拷贝:删除拷贝构造和赋值运算符
- 禁止继承:使用
final关键字(C++11+) - 单例模式:优先使用 C++11 静态局部变量懒汉实现,兼顾简洁性和线程安全
这些技巧不仅能提高代码的健壮性和可维护性,也是后端开发面试中的必考点。掌握这些特殊类设计,能让你在实际开发中写出更优雅、更符合工程规范的 C++ 代码。
更多推荐

所有评论(0)