2019年,某自动驾驶公司的感知模块在连续运行72小时后崩溃。排查发现------不是算法Bug,不是硬件故障,而是 三个shared_ptr形成了循环引用,导致GPU显存以每小时200MB的速度泄漏。72小时后,显存耗尽,整个感知管线崩溃。
这不是段子,这是C++内存管理的经典陷阱。智能指针用对了是利器,用错了就是定时炸弹。
面试官最爱问的也是这块:
- "shared_ptr的引用计数是怎么实现的?"
- "循环引用怎么解决?"
- "make_shared和new有什么区别?为什么推荐前者?"
这些题不背源码答不深,不写代码记不牢。这篇文章,带你从RAII思想到源码实现,从面试高频题到深度学习框架底层,一次性把智能指针的底层逻辑打通。
一、RAII思想与智能指针演进史
1.1 为什么需要RAII?
C++内存管理的核心矛盾:手动管理效率高,但人不可靠。
看一段"经典翻车"代码:
cpp
void processData() {
int* data = new int[1024];
if (someCondition) {
// 异常抛出,data永远不会被delete
throw std::runtime_error("something went wrong");
}
// 如果上面没抛异常,正常路径执行delete
delete[] data;
}
这段代码的问题:如果someCondition为真,异常抛出后data永远不会释放。你可能说"用try-catch不就行了?"------可以,但每个new都包try-catch,代码可读性急剧下降。
RAII(Resource Acquisition Is Initialization)的核心思想 :把资源的生命周期绑定到对象的生命周期上。对象构造时获取资源,对象析构时释放资源。C++的栈对象在离开作用域时必然析构------无论是正常返回还是异常展开。
cpp
class IntArrayRAII {
public:
explicit IntArrayRAII(size_t size) : data_(new int[size]), size_(size) {}
~IntArrayRAII() { delete[] data_; } // 析构必然执行,无论是否异常
// 禁止拷贝(后面会讲为什么)
IntArrayRAII(const IntArrayRAII&) = delete;
IntArrayRAII& operator=(const IntArrayRAII&) = delete;
int& operator[](size_t i) { return data_[i]; }
private:
int* data_;
size_t size_;
};
void processData() {
IntArrayRAII data(1024); // 构造时分配
if (someCondition) {
throw std::runtime_error("something went wrong");
}
// 离开作用域,析构自动执行,无论是否异常
}
这就是RAII。智能指针就是标准库提供的、泛化后的RAII封装。
面试官会问 :"RAII的本质是什么?" 参考回答:RAII的本质是将资源获取与对象构造绑定,资源释放与对象析构绑定。利用C++栈语义保证------对象离开作用域时析构函数必然执行,无论正常返回还是异常展开。这是C++异常安全的基础,也是智能指针、锁_guard等的设计根基。
1.2 智能指针的三代演进
auto_ptr (C++98, 已废弃)
│
│ 问题:拷贝时转移所有权,拷贝后原指针悬空
│ 改进方向:区分"独占"和"共享"
▼
unique_ptr (C++11) ←── 独占式,轻量高效
shared_ptr (C++11) ←── 共享式,引用计数
weak_ptr (C++11) ←── 弱引用,不增加引用计数,解决循环引用
【配图建议:智能指针演进时间线,三代对比卡片】
| 特性 | auto_ptr | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|---|
| 所有权 | 转移(危险) | 独占 | 共享 | 不拥有 |
| 拷贝语义 | 危险转移 | 禁止拷贝,允许move | 允许,引用计数+1 | 允许,计数不变 |
| 引用计数 | 无 | 无 | 强引用计数 | 弱引用计数 |
| 开销 | ~0 | ~0 | 控制块+原子操作 | 原子操作 |
| 引入版本 | C++98 | C++11 | C++11 | C++11 |
一句话总结:独占用unique_ptr,共享用shared_ptr,打破循环用weak_ptr,别用auto_ptr。
二、unique_ptr:独占式智能指针
2.1 基本用法
unique_ptr是零开销抽象------编译后的产物和裸指针几乎一样大,没有引用计数开销。
cpp
#include <memory>
#include <iostream>
class Widget {
public:
Widget() { std::cout << "Widget constructed\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
void doWork() { std::cout << "Widget working\n"; }
};
int main() {
// 创建unique_ptr
std::unique_ptr<Widget> ptr1 = std::make_unique<Widget>(); // C++14+
// ptr2 = ptr1; // 编译错误!不允许拷贝
std::unique_ptr<Widget> ptr2 = std::move(ptr1); // OK,转移所有权
// ptr1现在为nullptr
if (!ptr1) {
std::cout << "ptr1 is null after move\n";
}
ptr2->doWork(); // 通过ptr2访问
// 离开main作用域,ptr2析构,Widget自动销毁
return 0;
}
2.2 自定义删除器
unique_ptr的杀手锏之一:可以自定义删除器,实现RAII管理任意资源。
cpp
#include <memory>
#include <cstdio>
#include <mysql/mysql.h>
// 用unique_ptr管理FILE*
struct FileCloser {
void operator()(FILE* fp) const {
if (fp) {
std::fclose(fp);
std::cout << "File closed\n";
}
}
};
using UniqueFile = std::unique_ptr<FILE, FileCloser>;
// 用unique_ptr管理MySQL连接
struct MysqlCloser {
void operator()(MYSQL* conn) const {
if (conn) {
mysql_close(conn);
std::cout << "MySQL connection closed\n";
}
}
};
using UniqueMysql = std::unique_ptr<MYSQL, MysqlCloser>;
void processFile() {
UniqueFile fp(std::fopen("data.txt", "r"));
if (!fp) return;
// 使用fp.get()获取裸指针传给C接口
char buf[256];
while (std::fgets(buf, sizeof(buf), fp.get())) {
// 处理数据...
}
// 离开作用域,FileCloser自动执行fclose
}
// 用lambda作为删除器(更灵活)
void processWithLambda() {
auto deleter = [](MYSQL* conn) {
if (conn) mysql_close(conn);
};
std::unique_ptr<MYSQL, decltype(deleter)> conn(mysql_init(nullptr), deleter);
mysql_real_connect(conn.get(), "localhost", "user", "pass", "db", 0, nullptr, 0);
// 执行查询...
// 离开作用域自动清理
}
面试官会问 :"unique_ptr和shared_ptr的自定义删除器有什么区别?" 参考回答 :unique_ptr的删除器类型是类型签名的一部分(
unique_ptr<T, D>),不同删除器=不同类型,影响函数签名和容器存储,但运行时零开销。shared_ptr的删除器不是类型签名的一部分(都是shared_ptr<T>),更灵活,但需要类型擦除(存入控制块),有微小运行时开销。
2.3 unique_ptr管理数组
C++11的unique_ptr对数组做了特化:
cpp
// C++11之前:需要自定义删除器 delete[]
// C++11之后:unique_ptr<T[]> 特化版本
std::unique_ptr<int[]> arr(new int[100]);
arr[0] = 42; // 提供operator[],方便访问
// 但更好的做法:用std::array(编译期大小)或std::vector(动态大小)
// unique_ptr<T[]>主要用于:
// 1. 对接C API返回的堆数组
// 2. 自定义分配器的场景
三、shared_ptr:引用计数的源码级拆解
这是面试重灾区。很多人会用shared_ptr,但说不出引用计数的实现细节。
3.1 shared_ptr的内存布局
shared_ptr<T> 对象本身(栈上)
│
├─ T* ptr → 指向实际对象
└─ control_block* cb → 指向控制块
│
├─ atomic<long> strong_count (强引用计数)
├─ atomic<long> weak_count (弱引用计数)
├─ custom_deleter(可选)
└─ allocator(可选)
关键点:
- 两个指针:shared_ptr对象本身存两个指针------对象指针和控制块指针,共16字节(64位系统)。
- 原子计数器 :strong_count和weak_count都是
atomic,线程安全的引用计数操作。 - 控制块是堆分配的 :每次
new+shared_ptr构造,会在堆上额外创建一个控制块。
3.2 引用计数源码拆解
我们手写一个简化版的shared_ptr,把核心逻辑讲透:
cpp
#include <atomic>
#include <iostream>
#include <mutex>
// 简化版控制块
class ControlBlock {
public:
ControlBlock() : strong_count_(1), weak_count_(0) {}
void add_ref() {
strong_count_.fetch_add(1, std::memory_order_relaxed);
}
bool release() {
// 原子递减,如果变为0则返回true(需要销毁)
if (strong_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
// 强引用计数归零,对象可以销毁
// 但控制块不能立即销毁,因为可能有weak_ptr还在观察
// weak_count_不为0时,控制块保留
return true;
}
return false;
}
void add_weak_ref() {
weak_count_.fetch_add(1, std::memory_order_relaxed);
}
void release_weak() {
if (weak_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
// 弱引用也归零,控制块可以销毁
delete this;
}
}
long use_count() const {
return strong_count_.load(std::memory_order_relaxed);
}
private:
std::atomic<long> strong_count_;
std::atomic<long> weak_count_;
};
// 简化版shared_ptr
template<typename T>
class MySharedPtr {
public:
// 构造
explicit MySharedPtr(T* ptr = nullptr)
: ptr_(ptr), cb_(ptr ? new ControlBlock() : nullptr) {}
// 拷贝构造
MySharedPtr(const MySharedPtr& other)
: ptr_(other.ptr_), cb_(other.cb_) {
if (cb_) cb_->add_ref();
}
// 移动构造
MySharedPtr(MySharedPtr&& other) noexcept
: ptr_(other.ptr_), cb_(other.cb_) {
other.ptr_ = nullptr;
other.cb_ = nullptr;
}
// 析构
~MySharedPtr() {
if (cb_ && cb_->release()) {
delete ptr_; // 销毁被管理对象
// 注意:控制块此时可能还存在(如果有weak_ptr)
// 控制块在weak_count归零时才销毁
}
}
// 赋值
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
// 先释放当前的
if (cb_ && cb_->release()) {
delete ptr_;
}
// 再接管新的
ptr_ = other.ptr_;
cb_ = other.cb_;
if (cb_) cb_->add_ref();
}
return *this;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
long use_count() const { return cb_ ? cb_->use_count() : 0; }
private:
T* ptr_;
ControlBlock* cb_;
};
// ===== 使用示例 =====
int main() {
MySharedPtr<int> p1(new int(42));
std::cout << "p1 use_count: " << p1.use_count() << "\n"; // 1
{
MySharedPtr<int> p2 = p1; // 拷贝,引用计数+1
std::cout << "after copy, p1 use_count: " << p1.use_count() << "\n"; // 2
std::cout << "p2 value: " << *p2 << "\n"; // 42
} // p2析构,引用计数-1
std::cout << "p2 destroyed, p1 use_count: " << p1.use_count() << "\n"; // 1
return 0; // p1析构,引用计数归0,对象销毁
}
面试官会问 :"shared_ptr的引用计数是线程安全的吗?" 参考回答 :引用计数本身是线程安全的------使用原子操作(
std::atomic)保证。但引用计数线程安全 ≠ 对象本身线程安全。多个线程同时读同一个shared_ptr是安全的,但一个线程写(赋值、reset)另一个线程读,需要额外同步。对象本身的线程安全需要开发者自己保证。
面试官会问 :"shared_ptr的原子操作用了什么内存序?" 参考回答 :递增用memory_order_relaxed(不需要排序,只保证原子性);递减用memory_order_acq_rel------acquire保证读到其他线程对对象最后的修改,release保证对象销毁前所有写入对其他线程可见。这是性能与正确性的精细权衡。
3.3 控制块的生命周期
这是很多人忽略的细节:控制块的生命周期可能比对象长。
对象生命结束(strong_count = 0)
│
├── 销毁被管理对象(delete ptr_)
│
└── 控制块继续存活,直到 weak_count 也 = 0
│
└── 最后一个weak_ptr析构时,控制块销毁
为什么?因为weak_ptr需要观察"对象是否已销毁"------它必须访问控制块来判断strong_count是否为0。如果控制块和对象同时销毁,weak_ptr就无法安全判断了。
四、weak_ptr:破解循环引用
4.1 什么是循环引用?
先看一个经典的内存泄漏场景:
cpp
#include <memory>
#include <iostream>
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 双向引用
~Node() { std::cout << "Node destroyed\n"; }
};
void createCycle() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // a → b,b的引用计数 = 2
b->prev = a; // b → a,a的引用计数 = 2
std::cout << "a use_count: " << a.use_count() << "\n"; // 2
std::cout << "b use_count: " << b.use_count() << "\n"; // 2
}
// 离开作用域后,a和b的局部变量析构,引用计数各-1
// 但 a->next 持有b,b->prev 持有a
// a的引用计数 = 1(被b->prev持有),b的引用计数 = 1(被a->next持有)
// 两者互相持有,引用计数永远不会归零 → 内存泄漏!
int main() {
createCycle();
std::cout << "function returned, but Nodes never destroyed!\n";
return 0;
}
4.2 weak_ptr的原理
weak_ptr是一种不增加强引用计数 的智能指针。它只能从shared_ptr构造,不能直接访问对象(需要调用lock()提升为shared_ptr才能访问)
面试官会问 :"weak_ptr的lock()做了什么?" 参考回答 :
lock()尝试将weak_ptr提升为shared_ptr。它原子地检查控制块中的strong_count:如果strong_count > 0(对象还活着),则原子地+1并返回一个新的shared_ptr;如果strong_count = 0(对象已销毁),则返回空的shared_ptr。这个操作必须是原子的,否则会出现"检查时活着,提升时已销毁"的竞态条件。
4.3 weak_ptr的完整使用模式
#include <memory>
#include <iostream>
#include <vector>
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
// 被观察对象,持有观察者的弱引用
class Subject {
public:
void addObserver(std::shared_ptr<Observer> obs) {
observers_.push_back(obs); // 存weak_ptr,不增加引用计数
}
void notifyAll() {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto sp = it->lock()) {
// 观察者还活着,调用update
sp->update();
++it;
} else {
// 观察者已销毁,从列表中移除
it = observers_.erase(it);
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers_;
};
// 具体观察者
class ConcreteObserver : public Observer {
public:
ConcreteObserver(const std::string& name) : name_(name) {}
void update() override {
std::cout << name_ << " received update\n";
}
private:
std::string name_;
};
int main() {
Subject subject;
{
auto obs1 = std::make_shared<ConcreteObserver>("Observer1");
auto obs2 = std::make_shared<ConcreteObserver>("Observer2");
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.notifyAll(); // 两个观察者都收到通知
}
// obs1和obs2离开作用域销毁
subject.notifyAll(); // 自动清理已销毁的观察者,不崩溃
return 0;
}
这是观察者模式的经典实现------被观察对象用weak_ptr持有观察者,观察者随时可以销毁,不需要手动反注册。
五、make_shared vs new:内存布局与效率对比
5.1 两种创建方式的内存布局差异
cpp
// 方式1:new + shared_ptr构造
std::shared_ptr<Widget> p1(new Widget());
// 方式2:make_shared
std::shared_ptr<Widget> p2 = std::make_shared<Widget>();
内存布局对比:
方式1(new + 构造):
堆内存分配1:Widget对象
堆内存分配2:ControlBlock控制块
[Widget对象] ←── ptr_
[ControlBlock] ←── cb_
方式2(make_shared):
堆内存分配1:Widget对象 + ControlBlock(连续内存)
[Widget对象 | ControlBlock] ←── ptr_指向Widget,cb_指向ControlBlock
【配图建议:两种内存布局对比图,展示make_shared的单次分配vs两次分配】
5.2 make_shared的优势
cpp
#include <memory>
#include <chrono>
#include <iostream>
class BigObject {
public:
BigObject() {
for (int i = 0; i < 1000; ++i) data_[i] = i;
}
private:
int data_[1000];
};
void benchmarkNew() {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
std::shared_ptr<BigObject> p(new BigObject());
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "new: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
}
void benchmarkMakeShared() {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
auto p = std::make_shared<BigObject>();
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "make_shared: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
}
int main() {
benchmarkNew(); // 典型结果:~180ms
benchmarkMakeShared(); // 典型结果:~120ms(快约30%)
return 0;
}
make_shared的三大优势:
- 性能:一次堆分配 vs 两次堆分配(对象+控制块),减少malloc开销
- 局部性:对象和控制块在连续内存中,CPU缓存命中率更高
- 异常安全:不会出现"new成功但shared_ptr构造前异常导致泄漏"
5.3 make_shared的"陷阱"
make_shared并非完美------对象内存延迟释放。
cpp
auto sp = std::make_shared<BigObject>(); // 对象和控制块在同一块内存
std::weak_ptr<BigObject> wp = sp;
sp.reset(); // 强引用归零
// 此时BigObject的析构函数已经执行
// 但内存没有释放!因为weak_ptr还在,控制块还活着
// 而控制块和对象是同一块内存,所以整块内存都不能释放
// 直到wp也析构,这块内存才真正释放
结论 :如果对象很大、且有weak_ptr长期存活,用new + shared_ptr构造更合适(对象和控制块分离,对象可以及时释放)。否则优先make_shared。
面试官会问 :"什么时候不用make_shared?" 参考回答:三种情况:(1) 需要自定义删除器时,make_shared不支持;(2) 对象很大且weak_ptr可能长期存活时,避免内存延迟释放;(3) 构造函数是私有/受保护时,make_shared无法访问(需要用enable_shared_from_this或友元技巧)。
六、enable_shared_from_this:在对象内部获取自身shared_ptr
一个经典坑:在成员函数中需要返回自身的shared_ptr,怎么做?
cpp
// 错误写法!创建第二个控制块,导致引用计数错乱
class BadWidget {
public:
std::shared_ptr<BadWidget> getSelf() {
return std::shared_ptr<BadWidget>(this); // 灾难!新的控制块
}
};
// 正确写法:继承enable_shared_from_this
class GoodWidget : public std::enable_shared_from_this<GoodWidget> {
public:
std::shared_ptr<GoodWidget> getSelf() {
return shared_from_this(); // 安全,复用已有控制块
}
};
int main() {
auto gw = std::make_shared<GoodWidget>();
auto self = gw->getSelf(); // OK,和gw共享同一个控制块
// 错误示例:
// auto bw = std::make_shared<BadWidget>();
// auto self = bw->getSelf(); // 两个控制块,bw析构后self悬空!
return 0;
}
面试官会问 :"enable_shared_from_this的实现原理是什么?" 参考回答:enable_shared_from_this内部持有一个weak_ptr成员。当shared_ptr首次管理该对象时,会检查对象是否继承自enable_shared_from_this,如果是,则将该weak_ptr初始化为指向自身的弱引用。后续调用shared_from_this()时,通过weak_ptr的lock()提升为shared_ptr,复用同一个控制块。
七、面试20问:智能指针高频考点全梳理
基础概念类
Q1:智能指针的原理是什么?
利用RAII机制,将动态分配的对象生命周期绑定到栈上的智能指针对象。析构时自动释放内存。unique_ptr独占所有权,shared_ptr通过引用计数共享所有权,weak_ptr是弱引用不增加计数。
Q2:unique_ptr为什么不能拷贝?
独占式所有权语义------同一时刻只能有一个unique_ptr指向对象。如果允许拷贝,两个unique_ptr都会在析构时delete同一块内存,导致double free。拷贝被删除(
= delete),只允许move语义转移所有权。
Q3:shared_ptr的引用计数存在哪里?
存在控制块(control block)中,控制块是堆分配的。shared_ptr对象本身只存两个指针:对象指针和控制块指针。控制块包含强引用计数、弱引用计数、删除器和分配器。
Q4:shared_ptr是线程安全的吗?
引用计数操作是线程安全的(原子操作)。但对象本身不是------多个线程同时读写对象需要额外加锁。多个线程同时读同一个shared_ptr是安全的,但一个线程写(赋值/reset)一个线程读不是安全的。
原理深挖类
Q5:为什么引用计数用原子操作而不是mutex?
原子操作比mutex轻量得多------mutex需要系统调用(用户态→内核态切换),原子操作是CPU指令级(LOCK前缀/CAS指令)。引用计数的增减是高频操作,用mutex会严重拖慢性能。
Q6:shared_ptr的引用计数递增和递减分别用什么内存序?
递增用
memory_order_relaxed------不需要排序,只保证原子性。递减用memory_order_acq_rel------acquire保证读到其他线程对对象的修改,release保证对象销毁前的写入对其他线程可见。
Q7:weak_ptr如何判断对象是否还活着?
通过
lock()方法原子地检查控制块中的strong_count。如果strong_count > 0,原子地+1并返回shared_ptr;如果 = 0,返回空shared_ptr。整个过程必须是原子的,否则会TOCTOU(Time-of-Check-to-Time-of-Use)竞态。
Q8:控制块什么时候创建?什么时候销毁?
创建时机:(1) make_shared时;(2) 从unique_ptr构造shared_ptr时;(3) 用new构造shared_ptr时。销毁时机:strong_count归零时销毁对象,weak_count也归零时销毁控制块。所以控制块的生命周期 >= 对象的生命周期。
实战陷阱类
Q9:循环引用是什么?怎么解决?
两个或多个对象通过shared_ptr互相引用,形成引用环。每个对象的引用计数都 >= 1(被环中其他对象持有),永远不会归零,导致内存泄漏。解决方案:将环中的一条边改为weak_ptr,打破循环。
Q10:make_shared有什么缺点?
(1) 对象内存延迟释放------对象和控制块在同一块内存,weak_ptr存活时整块内存都无法释放;(2) 不支持自定义删除器;(3) 如果构造函数抛异常,make_shared会正确清理(不会泄漏),但某些旧编译器可能有兼容性问题。
Q11:shared_ptr的control block什么时候创建第二次?
用
shared_ptr<T>(rawPtr)从同一个裸指针构造两次时,会创建两个控制块,导致double free。避免方法:永远不要从同一个裸指针构造多个shared_ptr,用make_shared或拷贝现有shared_ptr。
Q12:enable_shared_from_this的shared_from_this()在什么情况下会崩溃?
在对象不是由shared_ptr管理时调用shared_from_this()。因为内部weak_ptr未初始化,lock()返回空shared_ptr,如果直接解引用会UB(未定义行为)。
性能优化类
Q13:shared_ptr的大小是多少?
16字节(64位系统)------两个指针:对象指针和控制块指针。unique_ptr的大小通常是一个指针(8字节),如果有自定义删除器则更大(取决于删除器大小)。
Q14:shared_ptr的拷贝和move哪个快?
move更快------move只移动两个指针,不操作引用计数。拷贝需要原子递增引用计数,虽然原子操作比mutex快,但仍然有LOCK指令开销(在多核上会触发缓存行失效)。
Q15:atomic的引用计数在多核CPU上有什么性能问题?
原子递增/递减使用LOCK前缀指令,会导致缓存行在多个核心之间来回传递(cache line bouncing)。在多线程高频拷贝shared_ptr的场景下,这是性能瓶颈。解决方案:减少不必要的shared_ptr拷贝,多用const引用传递。
场景应用类
Q16:什么场景用unique_ptr?什么场景用shared_ptr?
默认用unique_ptr------零开销,表达独占语义。需要共享所有权时用shared_ptr(如多个组件共享同一资源、缓存、观察者模式)。能用unique_ptr就别用shared_ptr,性能差很多。
Q17:unique_ptr可以转成shared_ptr吗?反过来呢?
unique_ptr可以move到shared_ptr(
shared_ptr<T> sp = std::move(uptr);),反过来不行------shared_ptr无法转回unique_ptr,因为共享所有权不能变成独占。
Q18:如何实现一个线程安全的单例模式?
C++11之后,使用Meyers' Singleton(局部静态变量),编译器保证线程安全初始化。或者用
std::call_once。避免双重检查锁(DCLP)------在C++11之前不安全,C++11之后可以用atomic实现但Meyers'更简洁。
cpp
// Meyers' Singleton,C++11保证线程安全
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
Q19:智能指针和裸指针混用时有什么陷阱?
(1) 不要用裸指针构造多个shared_ptr(double free);(2) 不要用get()返回的指针初始化另一个智能指针;(3) 不要delete get()返回的指针;(4) 函数参数尽量传const引用而不是值(避免不必要的引用计数操作)。
Q20:shared_ptr的custom deleter怎么用?有什么陷阱?
自定义删除器允许管理非new分配的资源(如fopen返回的FILE*、malloc分配的内存、第三方库的释放函数)。陷阱:不同删除器类型的shared_ptr是不同类型(shared_ptr和shared_ptr<T, D>),影响容器存储和函数签名。不过可以用lambda或function包装来统一类型。
核心知识点速览
| 智能指针 | 所有权 | 拷贝 | 大小 | 适用场景 |
|---|---|---|---|---|
| unique_ptr | 独占 | 禁止(move) | ~8字节 | 默认选择,独占资源 |
| shared_ptr | 共享 | 允许 | ~16字节 | 共享资源、观察者模式 |
| weak_ptr | 不拥有 | 允许 | ~16字节 | 打破循环引用、缓存 |
独占用unique,共享用shared,破环用weak,别用auto。