💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
1. 设计一个不能被拷贝的类
1.1 核心原理【必考核心】
考察场景:笔试选择题、面试口述、手撕代码基础题
类的拷贝行为仅通过两个成员函数触发:拷贝构造函数 、拷贝赋值运算符重载。禁止拷贝的核心思路是禁用这两个函数,让编译器在编译期阻止拷贝行为。
1.2 C++11 标准实现:= delete【必考核心】
C++11 引入 = delete 语法,显式告知编译器删除该默认成员函数,编译期直接报错,是校招面试的首选写法,也是工程中的规范实现。
cpp
class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 核心:禁用拷贝构造与拷贝赋值
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 可选拓展:同时禁用移动语义,根据业务需求补充
NonCopyable(NonCopyable&&) = delete;
NonCopyable& operator=(NonCopyable&&) = delete;
};
面试标准话术:
C++11 的=delete是最规范的禁止拷贝方式,优势:
① 编译期即可发现错误,报错信息明确;
② 语法语义清晰,代码可读性强;
③ 不受友元、成员函数等场景的绕过风险。
1.3 C++98 兼容实现(旧标准)【高频易错】
考察场景:笔试概念辨析、代码找错题
C++98 无=delete语法,通过「私有声明拷贝构造和拷贝赋值 + 只声明不定义」实现,存在多处易错陷阱:
cpp
class NonCopyable {
private:
// 仅声明,不提供实现;且必须设为私有
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
public:
NonCopyable() {}
~NonCopyable() {}
};
高频踩坑点
- 必须设为
private:若为公有访问权限,类外可自行定义函数实现,无法真正禁止拷贝; - 只声明不定义:若完整实现函数,类内成员函数、友元函数仍可正常调用拷贝,失去禁用效果;
- 报错时机为链接期 而非编译期:类外调用拷贝时编译不报错,链接时才提示找不到函数定义,排查成本远高于
=delete。
面试官常见追问:
C++98 的写法有没有漏洞?
答:存在漏洞。类的友元函数、成员函数内部仍可尝试调用拷贝,此时会触发链接错误 而非编译错误,排查成本高;且语法不直观,可读性差。因此工程与面试中优先使用 C++11 的
=delete。
2. 设计一个只能在堆上创建对象的类
2.1 主流核心思路【高频易错】
考察场景:面试口述、代码改错题、手撕代码拓展题
C++ 对象可创建在栈区、全局静态区、堆区,想要对象仅能在堆上创建,就必须禁止在栈区、全局静态区实例化对象
➡ 对象创建阶段,编译器提前校验该对象生命周期结束后能否正常析构,未来无法析构直接禁止创建
➡ 栈对象、全局对象 离开作用域 / 程序结束时,编译器自动调用析构函数 释放;堆对象 由程序员手动 delete释放
➡ 析构函数设为私有 / 保护 ,可直接阻止栈对象、全局对象的创建(编译报错),仅允许通过new在堆上创建
2.2 实现方案
2.2.1 析构私有 + 公有释放接口(装delete)
cpp
class HeapOnly {
public:
HeapOnly() = default;
// 提供公有释放接口
void destroy() const {
delete this; // 堆对象手动delete
}
private:
// 析构函数私有
~HeapOnly() {}
// ⚠️【高频易错】必须禁用拷贝构造,否则可绕过限制
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete; // 习惯拷贝赋值一起禁用
};
int main() {
HeapOnly* ptr = new HeapOnly;
ptr->destroy();
// ✅ 禁用拷贝构造:栈上创建,编译报错
// HeapOnly stackObj;
// ❌ 未禁用拷贝构造:用堆对象拷贝构造【栈对象】
// HeapOnly* p = new HeapOnly;
// HeapOnly obj(*p);
return 0;
}
2.2.2 优化:protected 析构支持继承【进阶拔高】
派生类析构时,自动向上逐层调用基类析构函数。
- 基类析构 private :仅基类内部可访问,派生类完全无法调用基类析构,派生类无法完成析构,基类不能被继承;
- 基类析构 protected :基类内部、所有派生类均可访问,外部不可访问,派生类能正常调用析构。
改为 protected 可兼顾「禁止栈对象」和「支持继承」:
cpp
class HeapOnly {
public:
void destroy() const { delete this; }
protected:
~HeapOnly() {} // 派生类可访问,类外不可访问
};
2.3 替代实现:普通构造私有 + 静态创建接口【高频易错】
- 普通构造私有:定义栈对象、全局对象,编译器在类外调用普通构造完成初始化,普通构造私有类外就无法调用。
- 静态创建接口 :
① 无需对象实例,外部可直接通过 类名::静态函数 调用创建接口;
② 静态成员函数属于类,能够访问私有构造,从而在内部使用 new 创建堆对象。
cpp
class HeapOnly {
public:
static HeapOnly* createInstance() {
return new HeapOnly;
}
void destroy() { delete this; }
private:
HeapOnly() = default;
// ⚠️【高频易错】必须禁用拷贝构造,否则可绕过限制
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
};
int main() {
HeapOnly* ptr = HeapOnly::createInstance();
ptr->destroy();
return 0;
}
3. 设计一个只能在栈上创建对象的类
3.1 核心思路【高频易错】
考察场景:面试口述、笔试选择题、代码找错题
只有使用new运算符时,对象才会分配在堆上。new的执行流程是:调用operator new申请内存 ➡ 调用构造函数。因此禁用operator new和operator delete,即可阻止堆上创建。
- 主流常规方案:只禁用
operator new/operator delete阻断堆对象创建,无法限制全局、静态对象;- 若要彻底禁止全局 / 静态对象,必须额外将构造函数私有,对外提供静态函数仅返回临时栈对象。
3.2 实现方案
3.2.1 常规方案:只禁用 operator new/operator delete
cpp
class StackOnly {
public:
StackOnly() = default;
~StackOnly() = default;
// 禁用普通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:
int _a;
};
// 仍可创建全局对象、静态对象
StackOnly globalObj;
void func() {
static StackOnly staticObj;
}
int main() {
StackOnly localObj; // 栈对象
// 堆上创建,编译报错
// StackOnly* ptr = new StackOnly;
return 0;
}
3.2.2 严格方案【进阶拔高】
cpp
class StackOnly {
public:
// 唯一合法创建入口,仅返回栈临时对象
static StackOnly CreateObj() {
return StackOnly();
}
// 禁用所有堆内存分配入口
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
void* operator new[](size_t size) = delete;
void operator delete[](void* p) = delete;
private:
StackOnly() :_a(0) {}
int _a;
};
int main() {
// 合法:栈临时对象
StackOnly obj = StackOnly::CreateObj();
// 非法1:直接定义栈对象,构造私有,编译报错
// StackOnly obj2;
// 非法2:全局/静态对象,构造私有,编译报错
// static StackOnly s_obj;
// StackOnly g_obj;
// 非法3:堆对象,new被禁用,编译报错
// StackOnly* p = new StackOnly;
return 0;
}
3.3 常见陷阱与边界
3.3.1 禁用new语法:拷贝构造绕不过【高频易错】
只要类内删除了operator new,无论调用普通构造还是拷贝构造,只要搭配new语法创建堆对象,一定会调用operator new,直接编译报错。
3.3.2 placement new 绕过【进阶拔高】
考察场景:二面深度追问
placement new(定位 new)不调用operator new,直接在已分配的内存上构造对象,内存来源可以是堆、栈、静态缓冲区,能够绕开 new 禁用限制:
cpp
void* buf = malloc(sizeof(StackOnly));
StackOnly* ptr = new(buf) StackOnly; // 定位new,可在堆内存上构造对象
工程开发几乎不会使用 placement new;笔试基础题默认不考察该漏洞,二面深度追问时说明该漏洞即可,无需编写封堵代码。
4. 设计一个不能被继承的类
4.1 C++11 标准实现:final【必考核心】
考察场景:笔试选择题、面试口述
C++11 引入final关键字,修饰类时表示该类不可被继承,是校招的标准作答,语法简单且编译期校验。
cpp
class NonInherit final {
// 类内容
};
// 编译报错:无法继承final类
// class Derived : public NonInherit {};
补充关联考点 :final还可修饰虚函数,表示该虚函数不可被派生类重写,属于同一系列高频考点。
4.2 C++98 兼容实现【冷门了解】
C++98 无final,通过「构造函数私有」实现不可继承:派生类构造必须调用基类构造,若基类构造私有,派生类无法初始化基类,因此无法继承。
cpp
class NonInherit {
private:
NonInherit() {} // 构造私有
public:
static NonInherit createInstance() {
return NonInherit();
}
};
说明 :该写法同时会限制类的实例化方式,使用不便,工程中已被final完全替代,2024-2026 大厂校招几乎不考察该写法。
5. 设计一个只能创建一个对象的类(单例模式)
5.1 核心概念【必考核心】
考察场景:手撕代码必考题、面试连环追问、笔试代码分析题
单例模式:保证一个类在整个程序运行周期中有且仅有一个实例,并提供全局统一的访问接口。
- 典型应用场景:配置管理、日志器、线程池、数据库连接池等全局唯一资源。
- 核心实现三要素:① 构造函数私有;② 禁用拷贝构造与拷贝赋值;③ 静态接口获取唯一实例。
【关联】与只能在堆上创建对象的类 2.3 替代实现:普通构造私有 + 静态创建接口 类似
5.2 饿汉模式(Eager Initialization)【必考核心】
5.2.1 实现代码
饿汉模式将单例实例定义为类静态全局变量,在 main 函数执行前完成初始化。此时所有业务线程还未创建,不存在多线程并发创建对象的竞争,因此天然线程安全。
cpp
class SingletonEager {
public:
// ③ 成员m_instance私有,只能通过getInstance获取
static SingletonEager* getInstance() {
return &m_instance;
}
// ② 禁用拷贝构造与拷贝赋值
SingletonEager(const SingletonEager&) = delete;
SingletonEager& operator=(const SingletonEager&) = delete;
private:
// ① 构造函数私有
SingletonEager() = default;
~SingletonEager() = default;
// 类内声明静态成员(同一个类的静态成员变量,整个程序全局永远只有 1 份)
static SingletonEager m_instance;
};
// 静态变量类外初始化
// 类型是类名SingletonEager,初始化类对象
SingletonEager SingletonEager::m_instance;
int main() {
auto p = SingletonEager::getInstance();
return 0;
}
5.2.2 优缺点与适用场景
- 优点:实现简单,天生线程安全,无加锁开销,获取实例速度快;
- 缺点:程序启动时就创建实例,若实例初始化耗时久、占用资源多,会拖慢启动速度;多个编译单元的单例之间初始化顺序无保证。
- 适用场景:实例初始化快、体积小,程序启动后必然使用的场景。
⚠️【高频易错】饿汉模式的实例是静态对象,存储在全局 / 静态区,不需要手动释放,程序结束时自动析构。
5.3 懒汉模式(Lazy Initialization):双检锁实现【必考核心】
- 懒汉模式程序启动时不立刻创建对象,第一次调用创建接口 getInstance 时才创建对象;
- 多线程并发会导致多次 new,破坏单例,因此需要双重检查锁定 DCL(Double-Checked Locking)。
cpp
#include <mutex>
#include <atomic>
class SingletonLazy {
public:
// ③ 静态接口获取唯一实例
static SingletonLazy* getInstance() {
// 第一次检查:无锁快速判断,99%直接返回,避免每次加锁
if (m_instance == nullptr) {
// 保证同一时刻仅一个线程进入后续实例创建代码段
std::lock_guard<std::mutex> lock(m_mutex);
// 第二次检查:防止多线程同时通过第一次检查,重复创建实例
if (m_instance == nullptr) {
m_instance = new SingletonLazy;
}
}
return m_instance;
}
// ② 禁用拷贝与赋值
SingletonLazy(const SingletonLazy&) = delete;
SingletonLazy& operator=(const SingletonLazy&) = delete;
private:
// ① 构造私有
SingletonLazy() = default;
~SingletonLazy() = default;
// 原子指针:防止CPU/编译器指令重排,避免new乱序造成野指针
static std::atomic<SingletonLazy*> m_instance;
// 全局互斥锁
static std::mutex m_mutex;
};
// 类外初始化
// 类型是指针,仅指针赋空,未初始化类对象
std::atomic<SingletonLazy*> SingletonLazy::m_instance{ nullptr };
std::mutex SingletonLazy::m_mutex;
5.3.2 双检锁核心考点【高频易错】
5.3.2.1 为什么要两次检查?
- 第一次外层无锁检查:唯一实例创建后,后续绝大多数调用无需加锁阻塞,仅做一次空判断即可直接返回,避免高并发时锁竞争带来的性能损耗;
- 第二次内层加锁检查:多线程场景下,可能有多个线程同时通过第一次检查。先拿到锁的线程创建实例并释放锁后,下一个线程会立刻获取锁。若无内层二次判断,该线程会再次执行 new,生成第二个实例,单例被破坏。
5.3.2.2 指令重排问题
普通指针执行 m_instance = new SingletonLazy; ,底层分 3 步:
① 分配堆内存;
② 指针 m_instance 赋值;
③ 调用构造函数初始化对象。
编译器、CPU 会指令重排,可能乱序为①→②→③: 线程 A 指针已赋值,但对象未初始化,线程 B 判断指针非空,返回未初始化的野指针,野指针访问成员导致崩溃。
💡标准解决方案 :使用std::atomic原子指针,默认内存序顺序一致,禁止指令重排,避免野指针。
5.3.3 实例释放方案【进阶拔高】
堆上创建的懒汉单例需要释放资源,常见三种实现方式:
-
内嵌垃圾回收类:利用静态对象析构时自动释放
cppclass CGarbo { public: ~CGarbo() { if (SingletonLazy::m_instance) { delete SingletonLazy::m_instance.load(); } } }; static CGarbo garbo; // 静态成员,程序结束自动析构触发释放 -
atexit()注册释放函数:程序退出时自动调用 -
手动释放接口:提供静态 destroy 方法,业务主动调用(不推荐,易遗漏)
5.4 进阶实现:Meyers' Singleton(最推荐)【进阶拔高】
考察场景:二面高频追问
利用 C++11 标准特性:函数内静态局部变量的初始化是线程安全的,实现最简线程安全懒汉单例。
cpp
class SingletonMeyers {
public:
static SingletonMeyers& getInstance() {
static SingletonMeyers instance; // 首次调用时初始化,C++11保证线程安全
return instance;
}
SingletonMeyers(const SingletonMeyers&) = delete;
SingletonMeyers& operator=(const SingletonMeyers&) = delete;
private:
SingletonMeyers() = default;
~SingletonMeyers() = default;
};
- 优势:代码极简,无需手动加锁,无指令重排问题,C++11 及以上标准保证线程安全;
- 面试价值:主动提出该方案,可体现对现代 C++ 标准的熟悉度,拉开与普通候选人的差距。