C++智能指针全景剖析及面试20问

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(可选)

关键点:

  1. 两个指针:shared_ptr对象本身存两个指针------对象指针和控制块指针,共16字节(64位系统)。
  2. 原子计数器 :strong_count和weak_count都是atomic,线程安全的引用计数操作。
  3. 控制块是堆分配的 :每次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的三大优势:

  1. 性能:一次堆分配 vs 两次堆分配(对象+控制块),减少malloc开销
  2. 局部性:对象和控制块在连续内存中,CPU缓存命中率更高
  3. 异常安全:不会出现"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。

相关推荐
青春逝如流水1 小时前
Java 中线程的创建方式:从基础到现代
java·开发语言·多线程·ai文章
铅笔小新z1 小时前
【C语言】深入理解指针
c语言·开发语言
Peregrine91 小时前
C++ - > 类和对象
java·c++·算法
余醉 | dtminer2 小时前
56%的保护效应藏在乳腺密度里:Nat Commun 高分中介孟德尔随机化全 R 管线拆解(附核心图)
开发语言·r语言·孟德尔随机化
2501_933923252 小时前
SpringCloud组件:Eureka服务注册与发现
java·开发语言·后端·spring cloud·eureka
染指11102 小时前
142.Agent-多Agent框架-通过Skills实现多Agent
开发语言·人工智能·python·langchain·agents
小宋10212 小时前
GRPO训练工具Agent:环境、Rollout、可验证奖励与失败复盘
人工智能·深度学习·机器学习
hunteritself2 小时前
10 层 4K 实时剪辑,国产工作站进入广电制作核心
大数据·前端·人工智能·深度学习·transformer
好好学习O(∩_∩)O2 小时前
[QT] 多线程moveToThread 和易踩的8个坑
开发语言·qt