C++特殊类设计全解析:从禁止拷贝到单例模式实战
🎯 本节目标
掌握C++中常见特殊类的设计方式,为面试和实际开发打下坚实基础。
📖 前言
在C++开发中,我们经常需要设计一些具有特殊限制的类,比如不能被拷贝的类、只能在特定内存区域创建对象的类等。这些特殊类设计不仅是面试中的高频考点,更是实际项目中保证代码安全性和性能的重要手段。本文将详细讲解5种常见特殊类的设计方法,并提供可直接运行的完整代码示例。
🔒 1. 请设计一个类,不能被拷贝
🛡️ 问题分析
拷贝主要发生在两个场景中:
- 拷贝构造函数 - 用于对象初始化时的拷贝
- 赋值运算符重载 - 用于对象之间的赋值操作
要禁止一个类的拷贝,只需让该类不能调用这两个函数即可。
📝 C++98实现方式
#include <iostream>
class CopyBan_Cpp98 {
public:
CopyBan_Cpp98() {
std::cout << "CopyBan_Cpp98 构造函数被调用" << std::endl;
}
void show() {
std::cout << "这是一个不能被拷贝的类(C++98版本)" << std::endl;
}
private:
// 关键:将拷贝构造函数和赋值运算符声明为私有且不实现
CopyBan_Cpp98(const CopyBan_Cpp98&); // 只声明,不定义
CopyBan_Cpp98& operator=(const CopyBan_Cpp98&); // 只声明,不定义
};
void test_CopyBan_Cpp98() {
CopyBan_Cpp98 obj1;
obj1.show();
// 以下代码编译时会报错:
// CopyBan_Cpp98 obj2(obj1); // 错误:拷贝构造函数不可访问
// CopyBan_Cpp98 obj3 = obj1; // 错误:拷贝构造函数不可访问
// obj2 = obj1; // 错误:赋值运算符不可访问
}
🚀 C++11实现方式(推荐)
#include <iostream>
class CopyBan_Cpp11 {
public:
CopyBan_Cpp11() {
std::cout << "CopyBan_Cpp11 构造函数被调用" << std::endl;
}
void show() {
std::cout << "这是一个不能被拷贝的类(C++11版本)" << std::endl;
}
// 使用delete关键字明确删除拷贝操作
CopyBan_Cpp11(const CopyBan_Cpp11&) = delete;
CopyBan_Cpp11& operator=(const CopyBan_Cpp11&) = delete;
};
void test_CopyBan_Cpp11() {
CopyBan_Cpp11 obj1;
obj1.show();
// 以下代码编译时会报更清晰的错误信息
// CopyBan_Cpp11 obj2(obj1); // 错误:使用已删除的函数
}
💡 设计原理说明
- 设置为私有:防止用户在类外定义这些函数
- 只声明不定义:即使通过友元或其他方式访问,链接时也会失败
- C++11的delete:更清晰、更安全,编译器会给出明确的错误信息
🏗️ 2. 请设计一个类,只能在堆上创建对象
🎯 设计思路
- 将构造函数设为私有,防止直接栈上创建
- 将拷贝构造函数设为私有或删除,防止通过拷贝在栈上创建
- 提供静态成员函数来创建堆对象
📝 完整实现代码
#include <iostream>
#include <memory>
class HeapOnly {
public:
// 静态工厂方法:创建堆对象
static HeapOnly* CreateObject() {
return new HeapOnly();
}
// 使用智能指针的版本(推荐)
static std::unique_ptr<HeapOnly> CreateUniqueObject() {
return std::unique_ptr<HeapOnly>(new HeapOnly());
}
void show() {
std::cout << "这是一个只能在堆上创建的对象" << std::endl;
std::cout << "对象地址: " << this << std::endl;
}
// 必须提供析构函数
~HeapOnly() {
std::cout << "HeapOnly 对象被销毁" << std::endl;
}
private:
// 构造函数私有化
HeapOnly() {
std::cout << "HeapOnly 构造函数(私有)" << std::endl;
}
// 禁止拷贝构造
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
// C++11:禁止移动构造和移动赋值
HeapOnly(HeapOnly&&) = delete;
HeapOnly& operator=(HeapOnly&&) = delete;
};
void test_HeapOnly() {
// 正确:在堆上创建对象
HeapOnly* ptr1 = HeapOnly::CreateObject();
ptr1->show();
delete ptr1;
// 更安全的方式:使用智能指针
auto ptr2 = HeapOnly::CreateUniqueObject();
ptr2->show();
// 错误:不能在栈上创建
// HeapOnly obj; // 编译错误:构造函数不可访问
// 错误:不能拷贝
// HeapOnly obj2(*ptr1); // 编译错误:拷贝构造函数被删除
}
🔍 使用场景
- 需要控制对象生命周期的场景
- 对象较大,避免栈溢出
- 需要延迟初始化的对象
🏠 3. 请设计一个类,只能在栈上创建对象
🎯 设计思路
- 将构造函数设为私有
- 提供静态方法创建并返回栈对象
- 重载operator new为delete,禁止new操作
📝 完整实现代码
#include <iostream>
class StackOnly {
public:
// 静态工厂方法:创建栈对象
static StackOnly CreateObject() {
return StackOnly();
}
void show() {
std::cout << "这是一个只能在栈上创建的对象" << std::endl;
std::cout << "对象值: " << _value << std::endl;
}
void setValue(int val) {
_value = val;
}
int getValue() const {
return _value;
}
// 关键:禁止在堆上创建
void* operator new(size_t size) = delete;
void operator delete(void* ptr) = delete;
// 禁止在堆上创建数组
void* operator new[](size_t size) = delete;
void operator delete[](void* ptr) = delete;
private:
// 构造函数私有化
StackOnly() : _value(0) {
std::cout << "StackOnly 构造函数(私有)" << std::endl;
}
// 禁止拷贝构造(可选,根据需求)
// StackOnly(const StackOnly&) = delete;
int _value;
};
void test_StackOnly() {
// 正确:在栈上创建对象
StackOnly obj = StackOnly::CreateObject();
obj.setValue(42);
obj.show();
// 错误:不能在堆上创建
// StackOnly* ptr = new StackOnly(); // 编译错误:operator new被删除
// StackOnly* arr = new StackOnly[5]; // 编译错误:operator new[]被删除
// 错误:不能通过new调用拷贝构造
// StackOnly obj2 = StackOnly::CreateObject();
// StackOnly* ptr3 = new StackOnly(obj2); // 编译错误:operator new被删除
}
💡 注意事项
- 这种设计会影响类的正常使用,需谨慎考虑
- 通常用于需要严格控制内存分配的场景
🚫 4. 请设计一个类,不能被继承
📝 C++98实现方式
#include <iostream>
class NonInheritable_Cpp98 {
public:
// 静态方法获取实例
static NonInheritable_Cpp98 GetInstance() {
return NonInheritable_Cpp98();
}
void show() {
std::cout << "这是一个不能被继承的类(C++98版本)" << std::endl;
}
private:
// 构造函数私有化
NonInheritable_Cpp98() {
std::cout << "NonInheritable_Cpp98 构造函数" << std::endl;
}
// 关键:派生类无法访问基类的私有构造函数
// 因此无法继承
};
// 尝试继承会编译失败
// class Derived : public NonInheritable_Cpp98 {}; // 错误:基类构造函数不可访问
🚀 C++11实现方式(推荐)
#include <iostream>
class NonInheritable_Cpp11 final {
public:
NonInheritable_Cpp11() {
std::cout << "NonInheritable_Cpp11 构造函数" << std::endl;
}
void show() {
std::cout << "这是一个不能被继承的类(C++11 final版本)" << std::endl;
}
};
// 使用final关键字后,尝试继承会直接编译失败
// class Derived : public NonInheritable_Cpp11 {}; // 错误:不能继承final类
void test_NonInheritable() {
NonInheritable_Cpp11 obj;
obj.show();
}
🔍 final关键字详解
final可以修饰类,表示该类不能被继承final也可以修饰虚函数,表示该虚函数不能被重写- C++11引入,代码更清晰,意图更明确
👑 5. 请设计一个类,只能创建一个对象(单例模式)
📚 设计模式简介
设计模式是一套被反复使用、多数人知晓的、经过分类的代码设计经验总结。就像《孙子兵法》总结战争规律一样,设计模式总结软件设计的规律。
单例模式:保证一个类只有一个实例,并提供一个全局访问点。
🍔 饿汉模式(Eager Singleton)
#include <iostream>
class SingletonEager {
public:
// 获取单例实例
static SingletonEager* GetInstance() {
return &m_instance;
}
void show() {
std::cout << "饿汉单例模式,实例地址: " << this << std::endl;
std::cout << "计数器: " << ++counter << std::endl;
}
// 禁止拷贝和赋值
SingletonEager(const SingletonEager&) = delete;
SingletonEager& operator=(const SingletonEager&) = delete;
private:
// 构造函数私有
SingletonEager() : counter(0) {
std::cout << "饿汉单例初始化(程序启动时)" << std::endl;
}
static SingletonEager m_instance; // 静态成员,程序启动时初始化
int counter;
};
// 静态成员初始化(在程序入口之前完成)
SingletonEager SingletonEager::m_instance;
void test_SingletonEager() {
SingletonEager* p1 = SingletonEager::GetInstance();
SingletonEager* p2 = SingletonEager::GetInstance();
p1->show();
p2->show();
std::cout << "p1 == p2: " << (p1 == p2) << std::endl;
}
✅ 优点:实现简单,线程安全
❌ 缺点:可能导致程序启动慢,多个单例初始化顺序不确定
😴 懒汉模式(Lazy Singleton)
#include <iostream>
#include <mutex>
#include <memory>
class SingletonLazy {
public:
// 获取单例实例(线程安全版本)
static SingletonLazy* GetInstance() {
// 双重检查锁定(Double-Checked Locking)
if (m_instance == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_instance == nullptr) {
m_instance = new SingletonLazy();
}
}
return m_instance;
}
// 使用现代C++的智能指针版本(C++11以上)
static std::shared_ptr<SingletonLazy> GetInstanceSmart() {
static std::once_flag onceFlag;
static std::shared_ptr<SingletonLazy> instance;
std::call_once(onceFlag, []() {
instance = std::shared_ptr<SingletonLazy>(new SingletonLazy());
});
return instance;
}
void show() {
std::cout << "懒汉单例模式,实例地址: " << this << std::endl;
std::cout << "调用次数: " << ++callCount << std::endl;
}
// 禁止拷贝和赋值
SingletonLazy(const SingletonLazy&) = delete;
SingletonLazy& operator=(const SingletonLazy&) = delete;
private:
// 构造函数私有
SingletonLazy() : callCount(0) {
std::cout << "懒汉单例初始化(第一次调用时)" << std::endl;
}
~SingletonLazy() {
std::cout << "懒汉单例析构" << std::endl;
}
// 内部垃圾回收类
class CGarbo {
public:
~CGarbo() {
if (SingletonLazy::m_instance != nullptr) {
delete SingletonLazy::m_instance;
SingletonLazy::m_instance = nullptr;
}
}
};
static SingletonLazy* m_instance;
static std::mutex m_mutex;
static CGarbo garbo; // 静态成员,程序结束时自动析构
int callCount;
};
// 静态成员初始化
SingletonLazy* SingletonLazy::m_instance = nullptr;
std::mutex SingletonLazy::m_mutex;
SingletonLazy::CGarbo SingletonLazy::garbo;
void test_SingletonLazy() {
SingletonLazy* p1 = SingletonLazy::GetInstance();
SingletonLazy* p2 = SingletonLazy::GetInstance();
p1->show();
p2->show();
std::cout << "p1 == p2: " << (p1 == p2) << std::endl;
// 使用智能指针版本
auto p3 = SingletonLazy::GetInstanceSmart();
auto p4 = SingletonLazy::GetInstanceSmart();
p3->show();
p4->show();
}
✅ 优点:延迟加载,启动速度快,内存使用更高效
❌ 缺点:实现复杂,需要注意线程安全
🧪 Meyers’ Singleton(C++11最优雅实现)
#include <iostream>
class SingletonMeyers {
public:
static SingletonMeyers& GetInstance() {
static SingletonMeyers instance; // C++11保证线程安全
return instance;
}
void show() {
std::cout << "Meyers单例模式,实例地址: " << this << std::endl;
}
// 禁止拷贝和赋值
SingletonMeyers(const SingletonMeyers&) = delete;
SingletonMeyers& operator=(const SingletonMeyers&) = delete;
private:
SingletonMeyers() {
std::cout << "Meyers单例初始化" << std::endl;
}
~SingletonMeyers() {
std::cout << "Meyers单例析构" << std::endl;
}
};
void test_SingletonMeyers() {
SingletonMeyers& s1 = SingletonMeyers::GetInstance();
SingletonMeyers& s2 = SingletonMeyers::GetInstance();
s1.show();
s2.show();
std::cout << "&s1 == &s2: " << (&s1 == &s2) << std::endl;
}
🎮 完整测试程序
#include <iostream>
// 所有类的定义放在这里...
int main() {
std::cout << "=== C++特殊类设计测试程序 ===" << std::endl << std::endl;
std::cout << "1. 测试不可拷贝类:" << std::endl;
// test_CopyBan_Cpp98();
test_CopyBan_Cpp11();
std::cout << "\n2. 测试堆上创建类:" << std::endl;
test_HeapOnly();
std::cout << "\n3. 测试栈上创建类:" << std::endl;
test_StackOnly();
std::cout << "\n4. 测试不可继承类:" << std::endl;
test_NonInheritable();
std::cout << "\n5. 测试单例模式:" << std::endl;
std::cout << "5.1 饿汉模式:" << std::endl;
test_SingletonEager();
std::cout << "\n5.2 懒汉模式:" << std::endl;
test_SingletonLazy();
std::cout << "\n5.3 Meyers单例:" << std::endl;
test_SingletonMeyers();
std::cout << "\n=== 测试完成 ===" << std::endl;
}
📊 设计模式对比总结
| 模式 | 实现方式 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|---|
| 不可拷贝 | C++98私有声明 / C++11 delete | 不涉及 | 无影响 | 资源管理类、唯一性对象 |
| 堆上创建 | 私有构造+静态工厂 | 是 | 中等 | 需要控制生命周期的对象 |
| 栈上创建 | 删除operator new | 是 | 好 | 小型临时对象 |
| 不可继承 | 私有构造 / final关键字 | 是 | 无影响 | 工具类、最终实现类 |
| 饿汉单例 | 静态成员初始化 | 是 | 好 | 启动时就需要初始化的单例 |
| 懒汉单例 | 双重检查锁定 | 需要额外同步 | 中等 | 延迟初始化、资源消耗大的单例 |
| Meyers’单例 | 局部静态变量 | 是(C++11+) | 好 | C++11及以上环境推荐使用 |
🎯 本章小结
本章深入探讨了C++中五种经典的特殊类设计模式,每种模式都针对特定的设计需求:
-
不可拷贝类:通过禁用拷贝构造函数和赋值运算符,保护资源不被意外复制,适用于文件句柄、网络连接等资源管理类。
-
堆上创建类:将构造函数私有化,通过静态工厂方法在堆上创建对象,实现对对象生命周期的完全控制。
-
栈上创建类:删除
operator new和operator delete,强制对象只能在栈上分配,适用于小型临时对象。 -
不可继承类:使用私有构造函数或C++11的
final关键字,防止类被继承,适用于工具类或最终实现类。 -
单例模式:确保一个类只有一个实例,并提供全局访问点。重点介绍了三种实现方式:
- 饿汉模式:线程安全但可能浪费资源
- 懒汉模式:延迟初始化但需要同步机制
- Meyers’单例:C++11及以上最优雅的实现
这些设计模式体现了C++面向对象编程的精髓,通过语言特性(构造函数、析构函数、访问控制、静态成员等)实现设计约束,是高质量C++代码的重要组成部分。
💼 大厂面试题精选
腾讯面试题
题目:请设计一个线程安全的单例模式,要求支持延迟初始化,并解释为什么你的实现是线程安全的。
参考答案:
class ThreadSafeSingleton {
private:
ThreadSafeSingleton() = default;
~ThreadSafeSingleton() = default;
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
static std::atomic<ThreadSafeSingleton*> instance;
static std::mutex mtx;
public:
static ThreadSafeSingleton* getInstance() {
ThreadSafeSingleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new ThreadSafeSingleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// 业务方法
void doSomething() {
// 业务逻辑
}
};
// 初始化静态成员
std::atomic<ThreadSafeSingleton*> ThreadSafeSingleton::instance(nullptr);
std::mutex ThreadSafeSingleton::mtx;
线程安全分析:
- 使用
std::atomic确保指针操作的原子性 - 使用双重检查锁定减少锁竞争
- 使用内存序保证可见性和顺序一致性
阿里巴巴面试题
题目:设计一个不能被拷贝的智能指针类,要求支持RAII和移动语义。
参考答案:
template<typename T>
class UniquePtr {
private:
T* ptr;
// 禁用拷贝
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
public:
// 构造函数
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
// 移动构造函数
UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// 移动赋值运算符
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 析构函数
~UniquePtr() {
delete ptr;
}
// 操作符重载
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
// 资源释放
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
// 资源转移
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
};
字节跳动面试题
题目:请解释C++11中final关键字的两种用法,并说明为什么final类不能被继承。
参考答案:
// 用法1:类不能被继承
class Base final {
public:
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
// 错误:不能继承final类
// class Derived : public Base {};
// 用法2:虚函数不能被重写
class Base2 {
public:
virtual void bar() final {
std::cout << "Base2::bar()" << std::endl;
}
};
class Derived2 : public Base2 {
public:
// 错误:不能重写final函数
// void bar() override {}
};
原理分析:
final关键字在编译期进行检查,违反规则会导致编译错误- 对于
final类,编译器会阻止任何继承它的尝试 - 对于
final函数,编译器会阻止任何重写它的尝试 final提供了更强的设计约束,有助于代码维护和性能优化(某些情况下允许编译器做更多优化)
华为面试题
题目:设计一个只能在栈上创建的数组类,要求支持动态大小和边界检查。
参考答案:
template<typename T, size_t N>
class StackArray {
private:
T data[N];
// 禁止堆上分配
void* operator new(size_t) = delete;
void operator delete(void*) = delete;
void* operator new[](size_t) = delete;
void operator delete[](void*) = delete;
public:
StackArray() = default;
~StackArray() = default;
// 访问元素(带边界检查)
T& operator[](size_t index) {
if (index >= N) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
const T& operator[](size_t index) const {
if (index >= N) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
// 获取大小
size_t size() const { return N; }
// 迭代器支持
T* begin() { return data; }
T* end() { return data + N; }
const T* begin() const { return data; }
const T* end() const { return data + N; }
// 数据访问
T* get() { return data; }
const T* get() const { return data; }
};
面试技巧与建议
- 理解原理:不仅要会写代码,更要理解每种设计模式背后的原理和适用场景
- 考虑线程安全:在多线程环境下,设计模式需要考虑线程安全性
- 掌握现代C++:熟悉C++11/14/17/20的新特性,如
delete、final、移动语义等 - 代码规范:注意代码的健壮性、可读性和可维护性
- 性能考虑:了解不同实现方式的性能影响,特别是在高频调用场景下
更多推荐

所有评论(0)