面试题答案
一键面试1. std::shared_ptr引用计数的底层实现机制
引用计数的数据结构
std::shared_ptr
通常使用一个控制块(control block)来管理引用计数。这个控制块是一个动态分配的对象,包含以下主要成员:
- 引用计数:记录当前有多少个
std::shared_ptr
指向同一个对象。它一般是一个std::size_t
类型的变量。 - 弱引用计数:用于
std::weak_ptr
,记录当前有多少个std::weak_ptr
指向同一个对象。同样是std::size_t
类型。 - 指向被管理对象的指针:指向实际被
std::shared_ptr
管理的对象。
例如,一个简化的控制块类可能如下:
class ControlBlock {
public:
std::size_t refCount;
std::size_t weakCount;
void* object;
ControlBlock(void* obj) : refCount(1), weakCount(0), object(obj) {}
};
更新策略
- 初始化:当创建一个
std::shared_ptr
指向一个新对象时,会分配一个新的控制块,并将引用计数初始化为 1。
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
- 拷贝构造:当通过拷贝构造函数创建一个新的
std::shared_ptr
时,它会共享同一个控制块,并将引用计数加 1。
std::shared_ptr<int> ptr2 = ptr1;
- 赋值:当进行赋值操作时,旧的
std::shared_ptr
(如果存在)的引用计数减 1,新的std::shared_ptr
的引用计数加 1。如果旧的std::shared_ptr
的引用计数变为 0,会释放其管理的对象和控制块。
std::shared_ptr<int> ptr3;
ptr3 = ptr2;
- 析构:当
std::shared_ptr
析构时,引用计数减 1。如果引用计数变为 0,会释放被管理的对象以及控制块。
{
std::shared_ptr<int> ptr4 = std::make_shared<int>(10);
} // 这里 ptr4 析构,引用计数减1,若变为0则释放对象和控制块
2. 多线程环境下的线程安全问题
- 竞争条件:在多线程环境下,多个线程同时对引用计数进行操作(如增加或减少)可能会导致竞争条件。例如,一个线程读取引用计数的值,另一个线程同时修改了该值,可能导致引用计数出现不一致的情况,进而可能导致对象被提前释放或未被释放。
- 双重释放:由于竞争条件,可能会出现两个线程同时认为引用计数变为 0,从而导致对象被释放两次,这会导致程序崩溃。
3. 解决线程安全问题的方法
使用互斥锁(std::mutex)
可以在控制块中使用 std::mutex
来保护对引用计数的操作。每次对引用计数进行增加、减少或读取操作时,都需要先锁定互斥锁。
class ControlBlock {
public:
std::size_t refCount;
std::size_t weakCount;
void* object;
std::mutex mtx;
ControlBlock(void* obj) : refCount(1), weakCount(0), object(obj) {}
};
在更新引用计数的函数中:
void incrementRefCount(ControlBlock* cb) {
std::lock_guard<std::mutex> lock(cb->mtx);
cb->refCount++;
}
void decrementRefCount(ControlBlock* cb) {
std::lock_guard<std::mutex> lock(cb->mtx);
cb->refCount--;
if (cb->refCount == 0) {
// 释放对象和控制块
}
}
使用原子操作(std::atomic)
C++
标准库中的 std::atomic
类型提供了原子操作,这些操作是线程安全的。可以将引用计数定义为 std::atomic<std::size_t>
类型。
class ControlBlock {
public:
std::atomic<std::size_t> refCount;
std::atomic<std::size_t> weakCount;
void* object;
ControlBlock(void* obj) : refCount(1), weakCount(0), object(obj) {}
};
这样,对 refCount
和 weakCount
的操作就不需要额外的锁来保证线程安全,因为原子操作本身就是线程安全的。
void incrementRefCount(ControlBlock* cb) {
cb->refCount++;
}
void decrementRefCount(ControlBlock* cb) {
if (--cb->refCount == 0) {
// 释放对象和控制块
}
}
在实际的 std::shared_ptr
实现中,通常会综合使用原子操作和互斥锁来优化性能并确保线程安全。例如,对引用计数的简单增减操作可以使用原子操作,而对于涉及复杂逻辑(如释放对象和控制块)的操作,可以使用互斥锁来保证一致性。