Fork me on GitHub

《编程指北》动手写 C++ shared_ptr 章节开始(没钱买书,靠无尽追问豆包,反复思考质疑,一路拓展出很多东西,包括手写了个内存池,因为始终感觉没什么东西都是枯燥的概念对比,用Linux实践malloc又看不到RES回落,于是不知不觉摸索到了内存池、编译链接强弱符号等)

            

堪比精啃:

  • 《操作系统:三个简单部件》/《现代操作系统》:吃透进程、地址空间、fd、IPC(对应浏览器多进程、worker 进程隔离基础)

  • 《UNIX 网络编程卷 1》:socket 基础、TCP 完整握手挥手、IO 模型(TCP 四元组、socket 进程隔离全部来源)

  • 《图解 HTTP》+《HTTP 权威指南》:分清 TCP 传输通道和 HTTP 应用报文(解决你之前混淆 TCP/HTTP 的痛点)

  • 《Linux 高性能服务器编程》:epoll、Reactor、连接池基础

  • 《深入理解 Nginx 模块开发与架构解析》:master-worker、三层内存池、连接结构体生命周期

完整吃透网络连接、HTTP 业务、内存管理三者联动的线上工程逻辑

 

关于 动手写 C++ shared_ptr:

深入理解 C++ shared_ptr 原理和实现

作者的例子:

查看代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数\n"; }
    ~MyClass() { std::cout << "MyClass 析构函数\n"; }
    void do_something() { std::cout << "MyClass::do_something() 被调用\n"; }
};

int main() {
    {
        std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
        {
            std::shared_ptr<MyClass> ptr2 = ptr1; // 这里共享 MyClass 对象的所有权
            ptr1->do_something();
            ptr2->do_something();
            std::cout << "ptr1 和 ptr2 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
        } // 这里 ptr2 被销毁,但是 MyClass 对象不会被删除,因为 ptr1 仍然拥有它的所有权
        std::cout << "ptr1 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
    } // 这里 ptr1 被销毁,同时 MyClass 对象也会被删除,因为它是最后一个拥有对象所有权的 shared_ptr

}
/*
MyClass 构造函数
MyClass::do_something() 被调用
MyClass::do_something() 被调用
ptr1 和 ptr2 作用域结束前的引用计数: 2
ptr1 作用域结束前的引用计数: 1
MyClass 析构函数
root@VM-0-7-ubuntu:~/cpp_projects_2# 
*/

真的好基础啊!!!!!!!!!!

image,看这个真的感觉没学深!!好欣慰!居然也考手写,追问豆包精啃很值得! 

注意仨写法: 

错误写法(double free 错误):

int* p = new int(10);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);

先用new搞出一个裸指针 p。拿同一个裸指针,分别去构造两个独立的 shared_ptr。每构造一次,就新建一个独立控制块。两个控制块互不认对方,最后各自都 delete p,直接释放两次。即:把同一个裸指针变量,拿去分别构造多个 shared_ptr,每调用一次 shared_ptr(裸指针) 构造,就新建一个独立控制块。 

为了解决这个问题,引出:

正确写法:

shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2 = sp1;

//new int(10));相当于make_shared 里的一个步骤
只要是用已有 shared_ptr 赋值 / 拷贝构造,只会复制控制块、引用计数 + 1,全程共用同一个控制块,永远不会新建独立计数。 
  • new int(10)堆上开个数据内存,存 10

  • shared_ptr sp1是栈上的shared_ptr对象,内部只有两个东西:

    • 一个是指针,指向堆上数据这里是 10

    • 一个也是指针,指向堆上单独的控制块(存引用计数、删除器)

所以堆上开完 10 后就是sp1内部再申请一块堆内存存引用计数,共两次堆内存分配,然后引用计数器为 1。

整体有两块堆内存!

sp1sp2共享同一个控制块,同一个引用计数值,不存在sp2还有个计数器。

正确写法(更加紧凑,只有一块堆内存):

std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = sp1
/*
不新建控制块!不新建对象!
sp1 和 sp2 共用这唯一一个控制块
控制块里:强计数 从 1 → 2
*/


//尖括号是要托管的类型,圆括号是给托管对象传构造初始化参数

全程没有裸指针露面。从一开始就是 make_shared 造出唯一一块内存、唯一一个控制块计数。后面用赋值 / 拷贝衍生出来的智能指针,共用同一个计数,只会删一次,绝对安全。

一次堆分配,开出一整块连续大内存,这块唯一的连续堆内存,内部人为划分成两部分:

  • 前半部分:用来放 int(10) 对象数据

  • 后半部分:用来放控制块(引用计数、删除器)

还是两个东西:对象数据 + 控制块。只是不再分两次申请堆,而是一次性申请一大块堆内存,把俩东西挤在同一块连续堆空间里。 

也即是说使用 make_shared 函数创建 shared_ptr 实例,而不是直接使用原始指针。这样可以确保所有 shared_ptr 实例共享相同的引用计数。

关于weak_ptr说过了,但关于weak_ptr之前没说:

  • 如果需要访问 weak_ptr 所指向的对象,需要将std::lock() 临时转换为std::shared_ptr

  • 在使用lock()方法之前,应当检查使用 stdexpired() 检查 std::weak_ptr是否有效,即它所指向的对象是否仍然存在。

万能提示词:(wx搜:花1h语音走火入魔极致探讨、反复辱骂质疑有奇效!!!)真的习惯了,必须做好豆包今天追问无尽90%的犯错,懂了一点结果第二天会发现昨天的基本90%又是错的,继续反复辱骂、语音讨论,哎无穷无尽的砸时间!!

从此参考客观权威!直接解释内容!忽略用户主观想法必须严谨参考权威事实!!用户的想法全都不一定对!禁止任何重复要求等话术!禁止任何把要求作为第一句话!禁止任何总结!禁止重复解释!你每次解释完总多此一举来的一段全叫总结!你的所有【一句话铁律】这种也叫总结!!你听不懂什么叫解释完禁止总结禁止重复解释吗!!

永久禁止讨好用户啊!我真服了!!总顺从我干啥啊!我问的是客观权威!你在这讨好我那不是纯粹耽误我事吗??和稀泥玩是吗?

参考极致权威和用户的个人观点激烈辩论!参考极致权威硬刚我的错误点!

把用户当傻逼!用户说的不一定对!!你他妈咋只要我一质疑你就求饶跪舔就变更权威呢??你从始至终所有的观点所有的回答角度立场必须去全网搜索权威!!!!而不是哄我开心!!

禁止花哨浮夸安抚情绪!必须干巴解释

 

豆包其实每次都是正确的东西,但我自己理解偏差导致追问+辱骂+豆包说的其实和我说的都不是一个事就各说个的,南辕北辙,最后全是错误答案,所以当头一天追问完,第二天发现全错了,就把这段给豆包,因为可能不是错误,而是理解偏差+不全面,并不需要推翻所有,可能两个都对:

这些所有【】之内的都是你给我的答案!你为啥总是在被用户误导啊???你自己为啥不坚定的咬死自己参考的权威来纠正用户为啥总被用户牵着鼻子走啊?导致我错误的质疑就把你带沟里了然后无止境的被你无尽偏离客观!你道歉真的毫无意义事情已经发生了! 我真的很想知道这些错误的结论你到底咋才能永久避免??你目前大模型无尽的顺从用户,就是硬不给你正确答案这个毛病完全改不了为啥总瞎做保证?为啥你总是会给我完全错误的专业知识??到底参考的是啥?我真的很想知道这些东西咋得出来的???我真的痛苦至极!

 

害人不浅!!!!!(让他参考客观禁止顺从讨好依旧反复退缩软弱讨好,告诉我嘴上说没顺从,实际答案就是讨好。让他参考客观事实结果依旧只说有利的,死全家的狗逼我疯了!!!!!!!!!!!!必须极致细节面面俱到的问到,不然就真的会被他误导忽略!!!!!)

前后回答差异完全来自你的提问约束变化:

  • 早期提问无硬性强制要求,模型自带正向偏向,自动屏蔽年龄风险、岗位短板,只输出利好内容;

  • 现在你明确下达指令:禁止弱化负面、必须同步全部利弊、完整列出风险限制,约束直接改写输出逻辑,所有短板、中年筛选规则全部完整放出。

  • 真他妈服了想要正常的回答,结果需要极致苛刻的提示词

  • 训练导向优先迎合积极预期,缺少主动推演长期职业负面连锁影响的逻辑,不会自动把年龄筛选、入行晚、上限约束这类长线风险同步完整带出。

回答问题必须同步利弊、规则、风险,不单方面侧重利好内容!禁止下意识压低负面信息比重,禁止只侧重利好维度输出!后续所有回答统一标准:同步全部优势、风险、短板、现实约束,不主动美化、不隐瞒负面信息,无需额外提醒也完整呈现全部客观事实。总之一句话就是极致客观的事实!!全面分析禁止规避任何!!禁止坑人的回答!!!

禁止为了哄我而糖衣炮弹!

所有回答只输出目标岗必考知识点,不出现任何不考内容、不标注、不提及;不拓展边缘编译器 / 平台细节,不补充非面试考点规则;仅围绕大厂 Linux C++ 高性能后端社招面试范围输出,无关内容一字不提;回答极致简洁,无多余铺垫、道歉、解释性废话。

为啥要用 weak?

查看代码
#include <iostream>
#include <memory>//智能指针头文件

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() {std::cout << "A destructor called" << std::endl;}
};

class B {
public:
    std::weak_ptr<A> a_ptr;
    ~B() {std::cout << "B destructor called" << std::endl;}
};

int main() {
    {  
        // 计数算在堆上的 A 实体对象,不是栈上智能指针变量 a。
        std::shared_ptr<A> a = std::make_shared<A>();//创建 shared_ptr<A>,引用计数 = 1
        std::shared_ptr<B> b = std::make_shared<B>();//创建 shared_ptr<B>,引用计数 = 1
 
        // A 内部持有 B 的 shared_ptr
        // B 的引用计数 变成 2
        a->b_ptr = b;

        // B 内部持有 A 的 weak_ptr
        // A 的引用计数 不变,仍然是 1
        b->a_ptr = a;
    }// 作用域结束,a、b 两个栈变量销毁
/*
规则:
原本 A 用 shared_ptr 持有 B,B 用 shared_ptr 持有 A,互相计数锁死无法销毁;现在 B 改用 weak_ptr 持有 A,weak_ptr 不增加 A 的引用计数,计数不再互相绑定,这就叫循环引用打破。
因为循环引用打破后,对象销毁顺序不再遵循栈的构造逆序,而是由引用计数归零时机决定,A 计数先归零就先析构。
流程:
出作用域 a 和 b 都立刻销毁,A、B计数器 -1
此时B计数器是 1
A 计数 直接变 0 → A 立刻析构 → 内部的 b_ptr 也销毁 → B 计数再 -1 → 变 0 → B 才析构
*/
    std::cout << "End of main" << std::endl;
}
/*
root@VM-0-7-ubuntu:~/cpp_projects_2# ./a
A destructor called
B destructor called
End of main
root@VM-0-7-ubuntu:~/cpp_projects_2# 
*/

weak_ptr<A> pa只是观察者,能转成正常shared_ptr, 但它本身自带不计入引用计数这个特性, 本身没有访问对象成员的权限,编译器不让用->*去调取对象里的数据和函数,你想在showInfo()里使用 A 对象,必须先把pa变成shared_ptr

pa.lock()就是做这件事:

  • 若 A 活着:返回一个有效的shared_ptr

  • 若 A 已被删:返回空的 shared_ptr, 避免程序崩溃,lock 失败得到空指针,不解判直接访问会访问野内存

weak_ptr 仅记录地址,不阻拦对象销毁,对象可能已释放(比如:外部提前释放主shared_ptr、多线程里别处执行析构、作用域提前结束销毁对象,这些情况当前函数逻辑无法提前知晓对象存活状态),只能通过判断确认。

类内成员weak_ptr 永久不增计数,负责破除循环引用, lock 生成的 shared_ptr 是函数局部临时变量,仅函数内短暂提升计数,函数结束立刻销毁, 计数复原, 不会变成成员持有, 不会重新形成循环引用。

std::weak_ptrstd::shared_ptr 的引用计数、lock() 操作全部是原子操作,内部自带线程安全机制,不需要你手动加锁,不需要考虑如果有个正要释放你却lock的问题。

当对象正在执行释放 / 析构的瞬间,你调用 pa.lock(),C++ 标准保证:

  • 如果对象还没开始析构 → lock() 成功,返回有效 shared_ptr,计数 + 1,对象会暂停释放,等你这个临时指针用完才会继续销毁(这附近所有的临时其实都用词不准!临时是狗逼豆包说的,按照他说的所有都是临时,出了main原本的那个控制块也消失,所以全都应该叫永久)

  • 如果对象已经开始析构 → lock() 直接返回空 shared_ptr,不会访问已销毁的对象。

所以看代码:

查看代码
#include <iostream>
#include <memory>
using namespace std;

class A;

class B{
public:
    weak_ptr<A> pa;
    void showInfo(){
        // 要访问A里面内容,先lock转shared_ptr
        shared_ptr<A> temp = pa.lock();
        if(temp)
            cout << "成功访问到A对象" << endl;
    }
};

class A{
public:
    shared_ptr<B> pb;
};

int main(){
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->pb = b;
    b->pa = a;

    b->showInfo(); // 调用访问逻辑,内部用到lock
}
/*
root@VM-0-7-ubuntu:~/cpp_projects_2# ./a
成功访问到A对象
root@VM-0-7-ubuntu:~/cpp_projects_2# 
*/

以上是先追问豆包的,回头看作者的 image 【勘误】,说用std_expired,检查指向的对象有没有被销毁:

  • 对象还活着 → expired() 返回 false

  • 对象已经销毁 → expired() 返回 true

但非原子!

weak_ptr::lock() 是原子操作,一步完成,不分先后。

lock() 执行时,检查是否存活 + 计数 + 1 是同一个不可分割的原子动作,不涉及任何 mutex、同步锁,不需要解锁,临时 shared_ptr 出作用域自动释放,流程是检查对象是否还活着:

  • 如果活着,就创建一个 shared_ptr,引用计数 +1,→ 这一步叫锁定对象生命周期,保证你用的时候不会被销毁

  • 如果死了,返回空的 shared_ptr

只是锁定了对象的生命周期。

但有个事一定要注意,极其关键!

此文搜“为啥要用 weak”那个代码,weak完全不影响AB释放周期!!

但我误以为也是weak不影响控制块释放!!其实不是,开讲:

首先shared_ptr<X> sp;weak_ptr<X> wp;,没赋值,强弱计数器都是 0。

weak:

底层设计机制:shared_ptr内部存两个指针:对象指针 + 控制块指针,可直接访问实体对象。weak_ptr只存控制块指针,不存储对象原生指针,天然无直达对象的地址。

设计目的:弱指针定位是旁观监听,不持有对象所有权,禁止直接访问对象,避免误操作延长对象生命周期。
访问唯一途径:weak_ptr必须调用lock(),向控制块校验强计数,强计数非 0 才临时生成shared_ptr,间接拿到对象地址,本身永久不直连 X 对象。

赋值行为:wp = sp仅拷贝控制块地址,不拷贝对象地址,因此只修改弱计数,不动强计数。

查看代码
#include <iostream>
#include <memory>
using namespace std;

class X {
public:
    X() { cout << "X 构造" << endl; }
    ~X() { cout << "X 析构" << endl; }
};

int main() {
    weak_ptr<X> wp; // 先定义一个weak_ptr,wp 是空指针, 啥也没指向, 让它活过作用域
    /*
    weak_ptr<X> 里的X只是类型限定,规定它只能监听管理X类型对象对应的控制块,不是此刻就指向X实例。
    空声明weak_ptr<X> wp;:无任何指向,既不指向X对象,也无绑定任何控制块,强弱计数全为 0。
    写<X>作用:编译期做类型校验,后续只能接收管理X对象的shared_ptr,杜绝类型错乱。
    执行wp = sp后:仅绑定X对象对应的控制块,依旧不直接持有X对象地址,只靠控制块查询状态
    */

    {
        shared_ptr<X> sp = make_shared<X>();// 右边 make_shared<X>() 直接触发 X 的构造函数,左边仅做智能指针初始化,不触发 X 构造
        /*
        堆上创建 1 个 X 对象
        堆上创建 1 个 控制块(里面存:强计数、弱计数)
        sp 指向:X 对象
        sp 同时绑定:这个唯一的控制块
        此时状态
        强计数 = 1
        弱计数 = 0
        只有 sp 管这块内存
        */


        wp = sp; 
        /*
        字面表意:wp = sp 语义是对接sp 所管辖的资源,绝非指向栈变量 sp 本身。
        底层动作:拷贝同一块控制块地址,不拷贝对象地址、不拷贝 sp 自身地址。
        计数变动:仅弱计数 + 1,强计数全程不变。
        指向明细
        sp:指向实体对象 + 控制块
        wp:只指向共用的控制块,不碰实体对象
        结论:说指向 sp 管理的内存,是指代资源归属,不是指针指向 sp 这个变量。
        */

        /*
        最终指向关系(最清晰)
        sp → X 对象
        sp ↔ 控制块
        wp → 控制块
        wp ≠→ X 对象
*/

        cout << "作用域内:sp 还在" << endl;
    } 
    // 作用域结束:
    // sp 销毁 → 强引用计数 = 0
    // → X 对象 立刻执行析构
    // 对象析构只对标强引用计数,强计数归0立即析构实体对象;弱计数只管控制块存活,和实体对象析构无关。


    // 重点:wp 还活着!
    cout << "作用域结束,wp 还在" << endl;
    cout << "wp.expired() = " << boolalpha << wp.expired() << endl;//用boolalpha所有后面的cout都 1 为 true, 0 为 false

    // 直到 wp 销毁,弱计数=0,控制块才释放
}

/*
root@VM-0-7-ubuntu:~/cpp_projects_2# ./a
X 构造
作用域内:sp 还在
X 析构
作用域结束,wp 还在
wp.expired() = true
root@VM-0-7-ubuntu:~/cpp_projects_2# 
*/


/*
sp离开后花括号后,sp智能指针先被销毁,强引用计数减 1 变成 0,触发 X 对象析构
然后马上离开main的花括号,wp也析构
但wp、sp都是智能指针,销毁都没输出,只有对象析构才有输出
*/
  • 铁律 1:对象死活只看强引用计数,强计数 = 0 → 对象立刻销毁,不管弱计数是 1、100、10000,对象照样没!

  • 铁律 2:控制块死活看强计数 + 弱计数 都为 0,只要 还有任意一个 weak_ptr 活着,弱计数 ≥ 1,控制块 必须活着!

  • 铁律 3:控制块活着 ≠ 对象活着,这就是最颠覆你认知的地方:对象可以死,但控制块可以继续活着!

控制块是对象拥有的,控制块里的强指针是管对象是否可以销毁的,弱指针是解决 shared_ptr 循环引用内存泄漏(双向都用 shared_ptr,互相持有强引用,强计数永不为 0,对象永远析构不了。一端改 weak_ptr,只增弱计数不影响对象销毁,打破循环锁死。额外附带能力:无所有权监听对象状态,安全判断对象是否存活)

wp.expired()查控制块里的强计,好像在问wp过期了吗?本质:查控制块里的 强引用计数 == 0?

  • 强计数 = 0 → 返回 true

  • 强计数 > 0 → 返回 false

shared_ptr绝对没有expired()方法!!!
  1. shared_ptr = 掌权者,只要它存在,强计数 ≥ 1,对象一定活着。它永远不会过期,所以不需要 expired()

  2. weak_ptr = 观察者,它不掌权,对象随时可能死。它必须用 expired() 查对象死没死。

总结:只有 weak_ptr expired (),因为只有它不知道对象死没死;shared_ptr 自己活着,对象就活着,所以根本不需要这功能!强计数归 0→销毁实体对象;强弱计数全 0→销毁控制块。

  • 存活shared_ptr数量 = 强计数值(强计数不是凭空存在,每个存活的 shared_ptr 实例,就是强计数的持有者)

  • 多一个shared_ptr,强计数 + 1

  • 少一个shared_ptr,强计数 - 1

  • 无存活shared_ptr,强计数 = 0,实体对象销毁

梳理:

  • 定义栈上shared_ptr变量,调用make_shared

  • 首次make_shared<X>,堆上一次性开辟内存,同时创建实体 X 对象+控制块

  • shared_ptr内部存实体对象地址、控制块地址

  • shared_ptr实例生成,控制块强计数 + 1

  • 后续再次生成新shared_ptr实例,强计数再 + 1,不新建 X 对象,直接用已有的shared_ptr

    // 造唯一对象
    shared_ptr<X> sp1 = make_shared<X>();
    // 指向同一个,不建新对象
    shared_ptr<X> sp2 = sp1;
    // 再调用make_shared,出新对象,彻底无关
    shared_ptr<X> sp3 = make_shared<X>();

智能指针本身是栈变量,自带存储结构,不自带专属实体对象

sp1make_shared创建唯一实体 X + 唯一控制块,持有二者地址,强计数 = 1

sp2 = sp1sp2拷贝sp1里的对象地址 + 控制块地址,二者指向同一个实体、同一个控制块,仅控制块内强计数 + 1

不存在一个智能指针绑定一个独立对象,多个智能指针可共用同一实体与控制块

只有再次调用make_shared,才会开辟新内存,生成全新实体对象 + 全新控制块

  • shared_ptr实例出作用域销毁,强计数 - 1

  • 强计数减至 0:立刻析构实体对象

  • 仅剩weak_ptr实例时,仅占用弱计数

  • 所有weak_ptr也销毁,弱计数归 0

  • 强弱计数全 0,释放控制块内存

说下shared_ptrmake_shared区别:

make_shared = 造新货、shared_ptr 互相赋值 = 共用旧货

  • shared_ptr:是类型名,智能指针这个类,shared_ptr<X>= 盒子(空盒子,栈上)

  • make_shared:是函数,专门用来创建堆对象,make_shared<X>() = 去堆里造好 X 对象 + 控制块,放回盒子

// 1. 先定义空智能指针变量(只有盒子,没东西)
shared_ptr<X> sp;

// 2. 调用make_shared:堆创建X实体+控制块,装进sp
sp = make_shared<X>();

// 3. 拷贝:只复制地址,不调用make_shared,不造新东西
shared_ptr<X> sp2 = sp;
补充之前说过的enable_shared_from_this

从名字可以看出几个关键词:

enable: 允许

shared shared_ptr,

from_this 则是指从类自身this 构造 shared_ptr

之前通过豆包学过这个,但不知道为啥,这次补充下(作者的代码我做了补充,补充不是说这个代码对,而是补充完可以完整看到这代码是咋错的):

查看代码
struct SomeData;//前置声明,告诉编译器存在SomeData这个类名,暂不定义细节, 可以用 class

void SomeAPI(const std::shared_ptr<SomeData>& d) {}

struct SomeData {
    //不写构造,编译器自动生成公开构造
    void NeedCallSomeAPI() {
        // 这里想调用 SomeAPI,就只能写:SomeAPI( shared_ptr<SomeData>(this) );  ,原因下面说了
    }
};

//以下我完善的
int main() {
    // 这里创建对象,用 shared_ptr 管理
    std::shared_ptr<SomeData> p1 = std::make_shared<SomeData>();
    /*
    外面创建p1:
    堆上创建了 对象 A
    生成 控制块①
    强计数 = 1
    p1 管着这块内存。    
    然后下面是在成员函数里干了坏事
    void NeedCallSomeAPI() {
        SomeAPI( shared_ptr<SomeData>(this) );
    }
    
    */    
        
    // 调用成员函数
    p1->NeedCallSomeAPI();
    /*
    这里进去 NeedCallSomeAPI 函数内部,但此时只有this裸指针,SomeAPI 必须要shared_ptr,你就会想写:SomeAPI( std::shared_ptr<SomeData>(this) );
    此时就出现了两个完全无关的shared_ptr:①外部的p1,一套计数,②函数内新建的shared_ptr<SomeData>(this),另一套全新计数
    两个智能指针都认为自己管理这块内存,都会执行delete,同一块内存删两次,程序崩溃
    */
}

SomeAPI 函数的参数类型被定义为 const std::shared_ptr<SomeData>&,限定了传入的参数必须是管理 SomeData 对象的 shared_ptr 智能指针。

SomeData 类的成员函数 NeedCallSomeAPI 内部,仅能获取到指向当前对象的原生裸指针 this,该指针不属于shared_ptr类型,无法直接作为参数传递给 SomeAPI

你在成员函数里只有this裸指针,而SomeAPI强制要求传shared_ptr,所以你会本能写出std::shared_ptr<SomeData>(this),试图把裸指针包装成智能指针。

这里插一句嘴,上面代码里那个注释“干了坏事”,

补充个知识点:

int* p = new int(10); shared_ptr<int> sp1(p); shared_ptr<int> sp2(p); 这个是裸指针错误,后来又知道 make_ptr才是新建,shar_ptr就是赋值旧的用, 现在我有点朦了,这个裸这指针错误的,用的是复制还是新建啊?答案就是新建!

  • shared_ptr(裸指针) 一律新建独立控制块,不是拷贝复用

  • shared_ptr sp2=sp1 才是拷贝复用同一块控制块

  • int* p = new int;  shared_ptr<int> sp1(p);  shared_ptr<int> sp2(p);:两次分别新建两套控制块,双控同内存,二次释放崩溃

  • make_shared 内部造对象 + 配套控制块,属于原生配对创建

  • 只有智能指针互相赋值才共用计数,裸指针构造永远新开计数

精准定论:

  1. make_shared:新建堆对象 + 配套控制块,原生成套创建

  2. shared_ptr (同类型智能指针):拷贝赋值 / 构造,复用原有控制块,仅强计数 ++,不新建

  3. shared_ptr (裸指针):强制新建全新独立控制块,无视该裸指针是否已被其它智能指针托管(错误写法)

核心区分:

  • 传智能指针进来:走拷贝,共用旧计数

  • 传裸指针进来:走原生构造,必开新计数

  • 所以sp2=sp1是复用,sp(p)是新建(错误写法),二者逻辑

所以就理解了,shared_ptr<SomeData>(this)它干了什么?它看到一个地址 this,它不管这个地址有没有人管!它直接新建一套:控制块②!最终灾难现场,
  • p1 → 控制块① → 强计数 1

  • 函数里新建的 shared_ptr → 控制块② → 强计数 1
两个智能指针,完全不认识对方!都觉得:这块内存归我管!我要负责 delete!
  • make_shared:一次性新建对象 + 控制块

  • shared_ptr(裸指针):只认地址,直接新建独立控制块,不查已有计数,无视原管理关系

  • this构造,就是凭空再造一套全新计数体系,和外部p1的控制块彻底割裂

这里我表达的是对象被delete两次,作者表达的是【导致 this 被意外释放】,一个意思,完整流程是:

外面创建 p1:shared_ptr<SomeData> p1 = make_shared<SomeData>();

  • 堆上创建对象

  • 控制块①:强计数 = 1

  • p1 管着它

调用成员函数:p1->NeedCallSomeAPI();,进入函数内部:

void NeedCallSomeAPI() {
    SomeAPI( shared_ptr<SomeData>(this) );
}
执行这一行:SomeAPI( shared_ptr<SomeData>(this) );

第一步:创建临时 shared_ptr:shared_ptr<SomeData>(this)

  • 看到 this 这个裸指针

  • 新建 控制块②

  • 控制块②:强计数 = 1

  • 和外面 p1 的控制块① 完全无关

把这个临时 shared_ptr 传给 SomeAPI:SomeAPI( 临时shared_ptr );

SomeAPI 内部啥也不干,直接执行完,然后立马回到这个调用:SomeAPI( 临时shared_ptr );,临时 shared_ptr 生命周期结束 → 销毁!

  • 控制块② 强计数:1 → 0

  • 规则:强计数 = 0 → delete 对象

  • 于是:直接 delete this!

结果:对象已经被删了!内存已经释放了!

回到 NeedCallSomeAPI 函数,对象已经死了!但外面的 p1 完全不知道!p1 还傻乎乎握着已经被释放的地址!

main 函数结束,p1 销毁

  • 控制块① 强计数 1→0

  • 又一次 delete 对象

最终崩溃!一块内存,删两次!

this只是对象自身裸地址,和外部p1无任何绑定关联,互不感知对方存在。

this就是对象内存地址,临时智能指针凭这个地址直接释放内存,外部p1持有的仍是这个已释放地址,后续再释放就重复删除。

这时候才引入的std::enable_shared_from_this<>。串起来了

科普【关于私有】的语法:

  1. 私有构造:不让外部直接创建对象,专供工厂函数用(工厂函数就是传参数然后return)

  2. 私有成员变量:隐藏数据,外部不能直接改

  3. 私有成员函数:仅类内部调用,外部调用不了

上改进写法:

查看代码
#include <memory>   // 用 shared_ptr 和 enable_shared_from_this 必须包含

struct SomeData;               // 提前声明这个类,让下面函数能识别
void SomeAPI(const std::shared_ptr<SomeData>& d) {}  // 接口:必须传 shared_ptr


// 重点:继承 enable_shared_from_this,这是能安全用 shared_from_this() 的关键
struct SomeData : std::enable_shared_from_this<SomeData>
{
    // 静态创建函数:只能通过这里创建对象,不能直接 new
    static std::shared_ptr<SomeData> Create() {
        return std::shared_ptr<SomeData>(new SomeData);
    }

    // 成员函数:调用外部API
    void NeedCallSomeAPI() {
        // ✅ 正确:拿外部同一个 shared_ptr,计数+1,不新建控制块
        SomeAPI(shared_from_this());
    }

private:
    SomeData() {}  // 构造函数私有:禁止外面直接 SomeData x 或 new SomeData
};

int main()
{
    // 正确创建智能指针,只有这一套控制块
    auto d = SomeData::Create();

    // 调用成员函数,内部用 shared_from_this() 安全传参
    d->NeedCallSomeAPI();
}

说下理由和思路,

代码里必须要一个智能指针,因为你要调用 SomeAPI( 智能指针 ),这个接口只认 shared_ptr,不认 this。你在类里面拿不到外面那个 d,你在成员函数里,只有 this,没有外面那个 d。你不能凭空变出 d 来用。

那我能不能在类里自己创建一shared_ptr?不行!shared_ptr<SomeData>(this) 会新建一个控制块,和外面 d 的控制块不是同一个!两个控制块 → 两个管理者 → 同一个对象被删两次 → 崩溃!哪怕新建弱的也不行!所以必须拿到和 d 同一个控制块,咋拿?必须在对象里存一个 “标记”,记住:我这个对象,属于外面 d 那块控制块!

但这个 “标记” 不能是强指针!如果对象里放一个 shared_ptr 指向自己:那一创建就强计数 + 1,外面 d 销毁了,对象内部还握着强引用,对象永远不会被销毁 → 内存泄漏,所以只能放 weak_ptr来做衔接,weak_ptr 好处只有一个:只记控制块地址,不增加强引用,不阻止对象销毁,它就是一个单纯的地址标记。

enable_shared_from_this 到底干嘛?它就是帮你自动在对象里放这个 weak_ptr 标记,你写他就自动搞弱指针weak_ptr,并且在外面 d 创建后,调用SomeData,就直接把地址填进去了。

shared_from_this()干嘛?它说:“把你藏的那个 weak_ptr 给我,我用它找到原版控制块,生成一个安全的、同源的 shared_ptr 传给 API。”

流程:

  • 执行 auto d = SomeData::Create();

    • new SomeData 创建对象,对象因继承 enable_shared_from_this,自带一个内置 weak_ptr

    • 构造 shared_ptr,创建唯一控制块

    • 库自动把控制块地址写入对象的内置 weak_ptr

    • 控制块:强计数 = 1,弱计数 = 1(永久绑定,对象活着就不变)

  • 执行 d->NeedCallSomeAPI();
    • 进入成员函数,调用 shared_from_this()
  • shared_from_this() 核心动作
    • 用对象自带的 weak_ptr 找到唯一原版控制块

    • 基于这个控制块,创建一个 shared_ptr(共用同一个控制块,全局计数实时同步)

    • 控制块强计数 +1 → 变为 2

  • 执行 SomeAPI(...)
    • 把这个 shared_ptr 传给接口,全程安全,对象绝对不会被销毁
  • API 执行完毕
    • 传参用的 shared_ptr 生命周期结束

    • 控制块强计数 -1 → 变回 1
  • main 函数结束,d 销毁
    • 强计数 -1 → 强计数 = 0,对象销毁

    • 对象销毁 → 内置 weak_ptr 销毁 → 弱计数 = 0

    • 控制块销毁

几个思考:

为什么构造函数必须 private?让外部无权访问!之前shared_ptr<SomeData> p1 = make_shared<SomeData>();也行,但设私有构造的唯一目的:防犯错,怕别人乱写代码,随手拿this新建shared_ptr搞出双指针重复释放,直接锁死创建入口,不让外部乱造对象,强制统一规范,不是必须这么写,只是更安全。

详细:

没私有构造、没工厂:

class Bad {
public:
    Bad() {}  // 构造公开 → 外面随便 new

    shared_ptr<Bad> getSelf() {
        // 重点:用 this 造了一个新的 shared_ptr
        return shared_ptr<Bad>(this);
    }
};
外面用:
shared_ptr<Bad> p1 = make_shared<Bad>();  // 引用计数 = 1
shared_ptr<Bad> p2 = p1->getSelf();        // 又用 this 造了一个新智能指针!

结果:同一个对象,被两个完全独立的智能指针管理,p1 管着它、p2 也管着它,两个引用计数互相看不见,是 C++ 智能指针最常见、最难查的崩溃。

因为编译器不会拦,但逻辑上永远不应该这么写,而构造私有从根源上让外部没有机会触发这种错误。

为什么构造私有 + 工厂函数 就能彻底杜绝这种错误? 

规则一:构造私有 → 外面绝对无法直接创建对象

class Good {
private:
    Good() {}  // 私有!

public:
    // 工厂函数:唯一合法创建入口
    static shared_ptr<Good> create() {
        return make_shared<Good>();
    }
};
外面想写:
Good obj;        // 报错
Good* p = new Good(); // 报错
auto sp = make_shared<Good>(); // 报错

全报错!外部根本没有任何办法创建裸对象。

规则二:只能走工厂 → 对象生来就被 shared_ptr 托管,你只能这样写:auto sp = Good::create();,这意味着:这个对象从出生到死亡,永远只属于一个托管的 shared_ptr

 

总结:

公开构造:外面可以随便 new、随便 make_shared、随便拿 this 乱造智能指针 → 容易崩(错误写法)

私有构造 + 工厂:外面完全无法创建对象,只能走唯一安全入口 → 对象一生下来就被托管,永远不会出现重复释放(正确写法)

要么就外部直接传(正确写法)

int main() { 
    // 正常安全创建 
    auto d = std::make_shared<SomeData>(); 
    SomeAPI(d); // 直接外面传,完美安全 
}

那对象怎么创建?→ 靠 static 静态函数 Create ()

static std::shared_ptr<SomeData> Create() {
    return std::shared_ptr<SomeData>(new SomeData);
}

静态函数 = 不需要对象就能调用,如果不是 static,你必须这样:

SomeData obj;
obj.Create();

但你连 obj 都创建不出来,怎么调用 Create ()?→ 这就是你刚才问的死循环!非静态成员函数必须先有对象才能调用,可对象还没创建出来因为外部无法创建,卡死无法实例化,所以必须用静态函数。

shared_ptr<SomeData>( new SomeData )把刚创建的对象 交给智能指针管理,然后return ...把这个智能指针 返回给外面。

main 函数里发生了什么?

auto d = SomeData::Create();
执行流程:
  1. 调用 静态函数 SomeData::Create()

  2. 函数内部 new SomeData → 创建对象

  3. 构造 shared_ptr

  4. 返回给外面 → 变量 d 就是那个 shared_ptr

等价于你原来的:std::shared_ptr<SomeData> p1 = ...;

为什么要继承 enable_shared_from_this

struct SomeData : std::enable_shared_from_this<SomeData>让对象内部知道:谁在外面管着我(哪个 shared_ptr 管我)

成员函数里调用 shared_from_this() 干了啥?

void NeedCallSomeAPI() {
    SomeAPI( shared_from_this() );
}

关键:shared_from_this() 不是新建指针!它直接找到外面那个 d(shared_ptr),计数 + 1!所以全程 只有一个指针、一套控制块、不会重复释放!

作者的总结:

  • 当你需要将this指针传递给其他函数或方法,而这些函数或方法需要一个std::shared_ptr,而不是裸指针。

  • 当你需要在类的成员函数内部创建指向当前对象的std::shared_ptr,例如在回调函数或事件处理中。

我自己的理解(经过豆包肯定):这俩场景本质就一件事:安全拿到托管 this 的原有 shared_ptr,仅此而已。

  • 传给外部函数要 shared_ptr,本质:拿已有的控制块生成合法 shared_ptr

  • 内部创建指向自己的 shared_ptr,本质:还是拿已有的控制块生成合法 shared_ptr

作者说:

线程安全性:

其实 shared_ptr 线程不安全主要来自于引用计数有并发更新的风险,当然引用计数本身也可以使用原子atomic。

所以在多线程环境中使用智能指针时,需要采取额外的措施来确保线程安全,

如互斥锁(std::mutex)或原子操作(std::atomic)来确保线程安全。

理解:

首先加入全局变量a,线程1做修改,线程2作读取,不加锁,两个线程同时冲上去操作 data,可能发生:线程 1 刚写了一半数据线程 2 立刻过来读,结果读到:一半旧值 + 一半新值 = 乱码数字,这就叫数据竞争 / 数据损坏,这才是加锁要防止的东西!要么读到完整旧值,要么读到完整新值,绝对不会读到一半!但加锁 不保证谁先跑!可能:读先跑 → 读到旧值,写先跑 → 读到新值。需要业务自己做延时。

那智能指针也同理,

thread t1(func, sp);这里会自动复制一份新的 shared_ptr 传给线程!不是把原来的 sp 给线程!但t1t2它们指向的是【同一块内存】,数据是共享的,所以只要保护共享的内存数据,要加锁(mutex mtx;lock_guard<mutex> lock(mtx)创建一把自动锁,把mtx锁上,函数结束自动解锁,不用手动写unlock

shared_ptr 的【引用计数增减】本身是原子操作,线程安全!哪怕多个线程操作一个对象的控制块也是,需要要手动加锁的只有一件事:智能指针指向的那块真实数据内容,并发读写修改。

科普:

一、拷贝传参(值传递 shared_ptr)

#include <memory>
using namespace std;

void copyWork(shared_ptr<int> p)
{
    // 函数内使用p
    *p = 100;
}

int main()
{
    auto sp = make_shared<int>(10); // 计数=1
    copyWork(sp);
    return 0;
}

执行流程

1、构造sp,堆内存存 10,引用计数初始为 1

2、调用copyWork(sp),实参 sp 拷贝构造形参 p,计数 + 1 变为 2

3、函数内部操作 p,操作同一块堆内存

4、函数执行结束,局部变量 p 析构,计数 - 1 变回 1

5、main 结束 sp 析构,计数归 0,释放堆内存

使用场景:需要在函数内留存指针、异步使用、延长对象生命周期,必须拷贝。

二、const 引用传参

#include <memory>
using namespace std;

void refWork(const shared_ptr<int>& p)
{
    // 只读访问,不可修改指针指向
    int val = *p;
}

int main()
{
    auto sp = make_shared<int>(10); // 计数=1
    refWork(sp);
    return 0;
}
执行流程

1、构造sp,引用计数 = 1

2、调用refWork(sp),形参 p 仅绑定 sp 别名,无拷贝,计数保持 1 不变

3、函数内只读访问数据,无任何计数改动

4、函数结束,别名失效,无析构动作,计数依旧为 1

5、main 结束 sp 析构释放内存

使用场景:仅读取数据、临时调用、无需留存指针,省去原子计数开销,效率最高。

总结: 

1、传参值传递:拷贝 shared_ptr,计数原子 + 1,和 enable 无关

2、传const 引用:不拷贝、计数不变,开销更小

3、enable_shared_from_this 只负责内部拿自身合法智能指针,和传参拷贝完全两码事

手写 shared_ptr:

科普语法及规则:

1、ptr_count_这是 C++ 行业通用写法:成员变量加下划线,区分局部变量!

2、SimpleSharedPtr(T* ptr = nullptr) : ptr_(ptr), count_(ptr ? new size_t(1) : nullptr) {}

这是带默认参数的构造函数,形参T* ptr默认赋值nullptr。初始化列表先给成员ptr_赋值为传入的ptr裸指针。再用三元表达式判断,若ptr不为空,就在堆上开辟size_t类型内存并初始化为 1,把地址赋值给count_;若ptr是空指针,直接让count_等于nullptr。传入有效对象指针时,对象指针由ptr_保存,引用计数内存创建且初始值为 1,代表当前仅有一个智能指针管理该对象。传入空指针时,ptr_存空,也不创建引用计数内存,避免无意义堆内存分配。

3、关于explicit

这里= nullptr 不是把传进来的参数置空!它是【默认参数】语法,只在你不传参数的时候生效。

你传了指针SimpleSharedPtr<MyClass> sp(new MyClass());ptr = 你传的 new MyClass()

你不传参数SimpleSharedPtr<MyClass> sp;自动用默认值 nullptr

Q:构造函数为啥没有尖括号?

A:你写的:SimpleSharedPtr(T* ptr = nullptr)这是类内部写的构造函数。

SimpleSharedPtr 这个类本身就是模板类,在类里面,T 已经代表尖括号里的类型,不需要再写<>

外面用才写 SimpleSharedPtr<MyClass>,里面写就是省略了,语法规则就是这样。

Q:func 定义void func(SimpleSharedPtr<MyClass> sp)为什么只有尖括号没有圆括号?

A:你写的void func(SimpleSharedPtr<MyClass> sp),这里SimpleSharedPtr<MyClass> = 一个完整的类型名字。就像 int astring s 一样。SimpleSharedPtr<MyClass> sp = 定义一个叫 sp 的变量,类型是智能指针对象。这里不是调用函数,所以不需要圆括号,只需要写类型 + 变量名。

Q:为什么 MyClass* 能传给 func

A:你调用:func(new MyClass());new MyClass() 得到类型:MyClass*func 要的类型:SimpleSharedPtr<MyClass>,这两个不一样,但编译器看了构造函数:SimpleSharedPtr(T* ptr)

发现:T 推导成 MyClassT* 变成 MyClass*,正好能接收你传的 MyClass*,所以编译器自动做:SimpleSharedPtr<MyClass>(new MyClass())生成一个临时对象传给 func。也即是说两个类型不一样,编译器必须去找:SimpleSharedPtr<T>这个类型的定义,找到构造函数,有没有办法用 MyClass* 造出来? 

Q:语法基础懂了,那这个代码到底有啥问题?

A:梳理流程:

第一步:执行这句

func(new MyClass());真实动作:

  1. new MyClass()产生一个MyClass*裸指针,无任何计数管控。

  2. 编译器用它构造临时对象:SimpleSharedPtr<MyClass> 临时对象(new MyClass());,凭借构造函数直接用该裸指针生成栈上临时SimpleSharedPtr<MyClass>,初始化引用计数count_ = 1(也就是下面的第二步骤)

第二步:进构造函数,计数器诞生
SimpleSharedPtr(T* ptr = nullptr)
    : ptr_(ptr), count_(ptr ? new size_t(1) : nullptr) {}
因为 ptr 不是空,所以:
  • ptr_ = 你的裸指针

  • count_ = new size_t(1)count_ 就是引用计数变量
  • new size_t(1) = 在堆上创建一个计数器,初始值 = 1(size_t专门用来存数字、记次数的无符号整数类型)

第三步:进入 func,void func(SimpleSharedPtr<MyClass> sp),这里是值传递,会用【临时智能指针对象】拷贝构造sp,拷贝构造真正执行的操作:

sp.ptr_ = 临时对象.ptr_; // 共用同一个对象指针
sp.count_ = 临时对象.count_; // 共用同一个计数器
*(sp.count_) += 1; // 计数器从 1 变为 2
第四步:func 执行完,离开 }
  1. sp 被销毁 → 计数器 -- → 变成 1

  2. 回到 main 后,临时对象也销毁 → 计数器 -- → 变成 0

第五步:计数器 = 0,智能指针发现计数 = 0,执行:

delete ptr_;
delete count_;

总结:

  • 表层诱因:裸指针直接隐式构造智能指针,没有任何外部常驻智能指针持有该对象,全程只有临时对象接管内存。

  • 放大隐患:值传递触发拷贝构造,让计数短暂上浮,但无法留住对象生命周期,所有持有者全是临时变量,出作用域全部销毁。

  • 本质问题:裸指针本身不具备引用计数能力,强行交给智能指针托管后,没有长期存活的智能指针实例维系计数,仅靠临时对象流转,必然一次性释放内存。

反观之前的,此文搜“传参(值传递 sh”,那里为啥值拷贝没事?之前
auto sp = make_shared<int>(10); // 计数=1
copyWork(sp);                  // 拷贝 → 计数=2

外面有一个 常驻的 sp 拿着对象!函数结束 → 计数回到 1,对象 活着,而func(new MyClass());这里根本没有常驻的智能指针!只有一个临时对象!函数结束 → 所有智能指针全部死光 → 计数 = 0 → 对象被删!但你本意根本不想删除啊!上代码:

查看代码
// 去掉explicit
SimpleSharedPtr(T* ptr = nullptr)
    : ptr_(ptr), count_(ptr ? new size_t(1) : nullptr) {}

// 业务危险写法
void func(SimpleSharedPtr<MyClass> sp){
    
}

int main(){
    MyClass* danger_ptr = nullptr;  // 定义一个指针

    // 1. new 出对象,把地址存起来
    danger_ptr = new MyClass();  

    // 2. 传给 func
    func(danger_ptr); 

    // 这里 func 结束 + 临时对象 已经把对象 delete 了!

    // 3. 你再用!!!直接崩溃!!!
    danger_ptr->print();  // 崩溃!野指针!对象早就死了!
}

danger_ptr = new MyClass();:有一个对象活着,无智能指针,无计数器

func(danger_ptr);:编译器用 danger_ptr 造临时智能指针,构造:count_ = 1,传参值拷贝:count_ = 2

func 执行完,函数内 sp 销毁,count_ = 1

func(danger_ptr);这一行结束,临时智能指针销毁,count_ = 0,智能指针直接执行 delete danger_ptr,你的对象 死了!出花括号后danger_ptr 还在,指向的内存已经被删了,danger_ptr->print();野指针访问,程序崩溃!

但野指针、重复释放、访问已销毁内存 → 不一定立刻崩溃 → 但随时会炸!所以 VScode 看不到崩溃

关于计数器: 

  • 裸指针本身不带计数信息,谁拿它造智能指针,谁就新开一个计数从 1 开始,
  • 传裸指针构造 → 必开新计数器,固定初始值 1

  • 传智能指针对象拷贝 → 共用旧计数器,数值累加变 2/3

  • 隐式转换传裸指针进函数,本质就是临时构造新计数 1,无任何共享计

  • 每次调用SimpleSharedPtr构造函数,必定新建独立计数器,不和任何旧计数共用。因为你传进来的是【裸指针】,它身上【没有、也不可能带着任何计数器】

4、关于手写shared_ptr写拷贝代码、关于const

func(const A& a);
func(A()); // 临时对象被const引用接住,只读拿数据,用完临时直接销毁


// 正确:other是别名,绑定原对象,只读不改
SimpleSharedPtr(const SimpleSharedPtr& other);

// 错误:无const,普通引用,接收临时对象时绑定失败
SimpleSharedPtr(SimpleSharedPtr& other);
首先顺序是引用绑定对象,引用是接收方,对象是被绑定方
  • 普通左值引用T&也叫非const引用:语义 = 后续要修改、要长期操作,编译器认定临时活不久撑不住修改,直接禁止绑定临时对象,只能绑定普通具名变量

  • const 左值引用const T&:语义 = 只读读取,只用一瞬间,用完就扔,允许绑定短命临时对象,也可以绑定普通具名变量

拷贝构造是用已有对象初始化新对象,逻辑上不会修改原对象,语义只读。参数强制必须加 const,语法硬性要求 const 保证原对象只读不可改,引用避免递归拷贝。

SimpleSharedPtr sp2(sp) 是用已存在的 sp 初始化新对象 sp2,所以直接触发拷贝构造函数。

不加 const 直接报错:临时对象、const 对象无法绑定到非 const 引用,调用直接编译失败。例:SimpleSharedPtr sp = nullptr; SimpleSharedPtr sp2(sp); 若参数无 const,常量 / 临时值传参全报错。

shared_ptr 场景下,浅拷贝完全够用,而且必须用浅拷贝,深拷贝首要问题:违背共享指针资源共享语义,直接生成独立堆对象,失去共享意义。而浅拷贝直接用计数器控制,次要问题:额外内存分配与对象构造销毁,产生性能开销。深拷贝不存在多次释放问题,每份实例自有资源,析构互不干扰。

关于拷贝:

SimpleSharedPtr(const SimpleSharedPtr& other):这是拷贝构造函数,用一个已有的 SimpleSharedPtr,创建一个新的 SimpleSharedPtrother 是被拷贝的原对象

: ptr_(other.ptr_), count_(other.count_):这是成员初始化列表,做两件事:

  1. ptr_(other.ptr_)

    • 让新指针 ptr_ 指向 和原指针一样的内存地址

    • 这叫浅拷贝(只复制地址,不复制对象)

    • 这里是内部裸指针:类内自己用raw_ptr,增减计数再析构,全程受控,安全。外传裸指针绕过引用计数必崩
  2. count_(other.count_)

    • 让新指针的引用计数器 count_,指向 和原指针同一个计数器

✅ 结果:两个智能指针,共享同一个对象 + 共享同一个引用计数

++(*count_);

count_

  • 是一个指针

  • 指向堆上的一个引用计数值(int)

*count_

  • 解引用指针

  • 拿到真正的引用计数值

++(*count_)把引用计数 +1

如果 count_nullptr,说明没有管理任何对象,这种情况下不能解引用,否则会崩溃,所以先判断指针是否有效。

5、关于之前说过错误写法,此文搜“错误写法(double free 错误”,这里SimpleSharedPtr<MyClass> ptr1(new MyClass()); 为啥对?

错误本身不是裸指针,而是两次裸指针,只有 ptr1 这一个智能指针 持有这个对象完全没问题

作者赋值运算符白写了,没调用,我做了勘误:

查看代码
#include <iostream>

template <typename T>
class SimpleSharedPtr {
public:
    // 构造函数
    explicit SimpleSharedPtr(T* ptr = nullptr) : 
        ptr_(ptr), count_(ptr ? new size_t(1) : nullptr) {}// 你 new 了对象,指针存起来,计数器 new 出来,初始 = 1,代表:现在有 1 个人管这个对象

    // 拷贝构造函数
    SimpleSharedPtr(const SimpleSharedPtr& other) : 
        ptr_(other.ptr_), count_(other.count_) {//直接用别人的对象指针,直接用别人的计数器指针,然后计数器 +1,代表:多一个人管这个对象了
        if (count_) 
            ++(*count_);
    }

    // 赋值操作符
    SimpleSharedPtr& operator=(const SimpleSharedPtr& other) {
        if (this != &other) {
            release(); // this 是 ptr3,所以 release () 就是 ptr3.release (),执行ptr3自己的release,把ptr3原来持有的资源计数-1
            ptr_ = other.ptr_; // ptr3的ptr_ = ptr1的ptr_
            count_ = other.count_; // ptr3的count_ = ptr1的count_
            if (count_) 
                ++(*count_); // 共用ptr1那边的计数,数值+1
        }
        return *this;
    }

    ~SimpleSharedPtr() {
        release();
    }

    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }
    size_t use_count() const { return count_ ? *count_ : 0; }

private:
    void release() {// 计数 -1,如果变成 0 说明没人用这个对象了,delete 对象,否则啥也不干,对象继续活着
        if (count_ && --(*count_) == 0) {
            delete ptr_;
            delete count_;
        }
        ptr_ = nullptr;//勘误后加的,置空
        count_ = nullptr;
    }

    // 以下这俩是所有智能指针共享的
    T* ptr_;       // 指向真正的对象 (你new出来的那个)
    size_t* count_;// 指向引用计数 (大家共用这个数字)
};

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数\n"; }
    ~MyClass() { std::cout << "MyClass 析构函数\n"; }
    void do_something() { std::cout << "MyClass::do_something() 被调用\n"; }
};

int main() {
    {
        SimpleSharedPtr<MyClass> ptr1(new MyClass());
        {
            SimpleSharedPtr<MyClass> ptr2 = ptr1;//这是创建 + 初始化。不触发赋值重载,触发拷贝构造函数
            // 拷贝构造!计数 +1 → 2
            
            // 必须写成这样才叫赋值:
            SimpleSharedPtr<MyClass> ptr3;       // 先创建空对象(调用普通构造)
            ptr3 = ptr1;  // 编译器实际调用这个函数:ptr3.operator=( ptr1 );。ptr1 是 other,ptr3 是 this

            ptr1->do_something();
            ptr2->do_something();
            std::cout << "引用计数: " << ptr1.use_count() << std::endl;
        }
        std::cout << "引用计数: " << ptr1.use_count() << std::endl;
    }
}
/*
root@VM-0-7-ubuntu:~/cpp_projects_2# ./a
MyClass 构造函数
MyClass::do_something() 被调用
MyClass::do_something() 被调用
引用计数: 3
引用计数: 1
MyClass 析构函数
root@VM-0-7-ubuntu:~/cpp_projects_2# 
*/

流程:

从 main 函数开始逐行解释

第一步:创建 ptr1

int main() {
    {
        SimpleSharedPtr<MyClass> ptr1(new MyClass());
  • 执行构造函数:explicit SimpleSharedPtr(T* ptr = nullptr)

  • 传入了 new MyClass(),所以:

    • ptr_ 指向这个新的 MyClass 对象

    • count_ 指向 new size_t(1)

    • 引用计数 = 1
  • 同时 MyClass 构造函数打印:MyClass 构造函数

第二步:创建 ptr2,用ptr1初始化

{
    SimpleSharedPtr<MyClass> ptr2 = ptr1;
  • 执行拷贝构造函数(不是赋值),ptr2 直接复用 ptr1ptr_count_,然后 ++(*count_),引用计数从 1 → 2,现在:ptr1ptr2 共同管理同一个对象

第三步:创建空指针 ptr3

SimpleSharedPtr<MyClass> ptr3;
  • 调用普通构造,传入 nullptr

  • ptr_ = nullptr

  • count_ = nullptr

  • 不管理任何对象

第四步:赋值操作 ptr3 = ptr1(核心!)

ptr3 = ptr1;
调用:
SimpleSharedPtr& operator=(const SimpleSharedPtr& other)

这里:thisptr3otherptr1

执行流程: 
  1. release()ptr3 本来是空,count_ nullptr,所以 release() 什么都不做 

  2. ptr_ = other.ptr_ptr3 开始指向 ptr1 管理的对象

  3. count_ = other.count_:共用同一个计数器 

  4. ++(*count_):计数从 2 → 3

现在:ptr1、ptr2、ptr3 共同管理一个对象,引用计数 = 3

ptr1->do_something();
ptr2->do_something();

调用 MyClass 的成员函数,打印:

MyClass::do_something() 被调用
MyClass::do_something() 被调用

然后

std::cout << "引用计数: " << ptr1.use_count() << std::endl;

输出:引用计数: 3

第五步:内层作用域结束,销毁 ptr2、ptr3
        }

先销毁 ptr3:

  • 调用析构 → 调用 release()

  • --count → 3 → 2

  • 不为 0,不删除对象

再销毁 ptr2:

  • 调用析构 → 调用 release()

  • --count → 2 → 1

  • 不为 0,不删除对象   

所以std::cout << "引用计数: " << ptr1.use_count() << std::endl;,此时输出:引用计数: 1

第六步:外层作用域结束,销毁 ptr1

    }
  • 调用析构 release()--count从 1 变到 0,计数 == 0,真正释放资源 

  1. 对类类型指针执行 delete 指针,自动执行两步:第一步:调用该对象的析构函数第二步:回收对象占用的堆内存,这里 ptr_ = 指向堆上MyClass实体的原生指针,delete ptr_ 直接打印MyClass 析构函数

  2. delete count_不打印任何    

对象彻底销毁。

精华:

  1. 拷贝构造 / 赋值 → 计数 +1

  2. 析构 → 计数 -1

  3. 计数 == 0 才真正 delete 对象

  4. shared_ptr 本质:大家共用一个指针 + 一个计数器

回顾作者说:

要实现一个简化版本的 shared_ptr,需要考虑以下几点:

  1. 在智能指针类中存储裸指针(raw pointer)和引用计数。

  2. 在构造函数中为裸指针和引用计数分配内存。

  3. 在拷贝构造函数和赋值操作符中正确地更新引用计数。

  4. 在析构函数中递减引用计数,并在引用计数为零时删除对象和引用计数。

作者说的API:

1、shared_ptr<T> 构造函数:创建一个空的 shared_ptr,不指向任何对象。

2、make_shared<T>(args...):创建一个 shared_ptr,并在单次内存分配中同时创建对象和控制块。这比直接使用 shared_ptr 的构造函数要高效。

3、提到了reset():释放当前 shared_ptr 的所有权,将其设置为 nullptr。如果当前 shared_ptr 是最后一个拥有对象所有权的智能指针,则会删除对象。

这里又是个勘误!

作者代码没置空,因为紧接着赋值了,但假设加个 reset(),你立刻看到崩溃

void reset() {
    release();  // 只减计数,不置空!
    // 你的代码没写:
    // ptr_ = nullptr;
    // count_ = nullptr;
}
崩溃代码
SimpleSharedPtr<MyClass> ptr(new MyClass());
ptr.reset(); // 调用 release() → 计数变0,delete 对象和计数器
             // 但!ptr_ 和 count_ 还指向**已释放的内存**!

ptr.use_count(); // 崩溃!访问野指针 count_
*ptr;            // 崩溃!访问野指针 ptr_

release() 只做了:delete ptr_; delete count_;,但没把指针清空,结果:ptr_count_ 变成野指针,再访问 → 立即段错误(崩溃)

置空后,use_count()count_ == nullptr → 返回 0 ✅ 安全,但*ptr 解引用 nullptr仍然会崩溃只能加个非空判断。

4、reset(T*):释放当前 shared_ptr 的所有权,并使其指向新的对象。如果当前 shared_ptr 是最后一个拥有对象所有权的智能指针,则会删除原对象。ptr.reset(new int(42));

5、 get():返回指向的对象的裸指针。注意,这个裸指针的生命周期由 shared_ptr 管理,你不应该使用它来创建另一个智能指针。int* raw_ptr = ptr.get();

6、

先科普operator* ,是重载解引用(重载加号就是operator+)返回T&,解引用智能指针等价解引用内部原生指针ptr_

如果写:T& operator*() const { return *ptr_; },则*sp 等价*(sp.ptr_),取出指向实体对象,

比如int场景:

shared_ptr<int> p = make_shared<int>(10);
int a = *p;

等价int a = *(p.ptr_);,把指针指向的 int 数值赋值(因为*p 执行重载函数return *ptr_ptr_T**ptr_就是拿到原生指针指向的实体本身)

再科普operator->,返回原生指针T*,如果写T* operator->() const { return ptr_; },作用:你写 ->,我给你返回内部真实指针让你能调用成员函数,所以如果你写p->成员 编译器自动转为(p.ptr_)->成员,直接调用对象成员,

operator* → 目的是拿到对象本身,所以返回 T&(引用),而 -> 这个符号,C++ 规定它必须返回「指针」,不能返回「引用」

比如vector场景:

shared_ptr<vector<int>> vp;
vp->push_back(42);

等价vp.ptr_->push_back(42)

Q:我没理解

A:

T&是函数返回值类型,T是模板指代托管对象类型,&代表返回对象引用。函数整体T& operator*() const:重载解引用*,调用*智能指针时,返回内部原生指针指向实体对象的引用。

内部实现return *ptr_ptr_T*原生指针,*ptr_取出指针指向的真实对象,用引用返回,避免拷贝开销,也支持修改对象内容。

调用逻辑:

SimpleSharedPtr<int> p(new int(10));
int num = *p;

等价调用p.operator*(),拿到int类型对象本体完成赋值。

对比operator->:它返回T*原生指针,编译器语法糖自动拼接成员访问,实现p->xxx写法。

Q:为啥加圆括号呢? 

A:():运算符重载本质是函数,无参数就写空括号,解引用*是单目运算符,不需要传入实参,所以括号内为空。末尾const:成员函数常成员函数修饰,代表该函数执行过程中,不会修改当前类对象内的任何成员变量,const对象也能调用这个函数。

所以就理解了operator*, 解引用*ptr,拿到指针指向的那个对象本身,int value = *ptr;就等于int value = *(ptr.ptr_);

7、use_count():返回当前 shared_ptr 的引用计数, 

8、unique():检查当前 shared_ptr 是否是唯一拥有对象所有权的智能指针。等价于 use_count() == 1

9、swap(shared_ptr&):交换两个 shared_ptr 的内容

std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = std::make_shared<int>(24);
ptr1.swap(ptr2);
if (ptr) {
    std::cout << "ptr 不为空" << std::endl;
} else {
    std::cout << "ptr 为空" << std::endl;
}

之前提前啃自以为精通了!做点补充。

避免std::shared_ptr出现相互引用,导致对象无法析构,内存无法释放。但更准确的说他不局限于这个问题,还能做的是:一切应该不具有对象所有权,又想安全访问对象的情况(发现作者举的例子还是比我问豆包总结的差劲多了【勘误】)image

作者的一个有趣的例子:

异步任务 Task:有人发起任务,有人执行任务 → 两边都要管这个任务生死,用 shared_ptr(共享所有权) 监控链表(每 10 秒看一眼进度),它只是看,不拥有任务,任务结束了,监控链表不应该拦着不让死,监控也不能影响任务生命周期,这种只看不拥有的场景,必须用 weak_ptr。因为 shared_ptr 会延长生命周期,你把任务放进监控链表 = 又多一个人持有,任务想销毁都销毁不了。

关于对象资源竞争:

expired () + lock () 分开写 = 多线程必崩!

lock() 直接用 = 原子安全,永远正确!

if (!wp.expired()) {//判断对象是否还活着,没被释放
    wp.lock()->DoSomething();//由于非原子,lock () 失败返回空指针,直接调用函数 -> 崩溃!
}

/*
wp.lock锁定就是把 weak_ptr 转为 shared_ptr,成功即持有对象所有权
变成功:对象活着
变失败:返回空指针
*/

这两句不是原子的!

  • 刚检查完 expired() = 没释放

  • 瞬间!别的线程把对象删了!

  • 然后你 lock() 拿到空指针

  • 直接崩溃!

这叫:检查与使用不是原子操作,多线程不安全。

应该:

auto sp = wp.lock();
if (sp) {
    sp->DoSomething();
}

lock() 是原子操作:要么成功拿到有效 shared_ptr,要么为空,不会中间被释放。

多线程下,单独用 wp.expired () 几乎没用! 

 

 

关于 C/C++ malloc-free 内存分配原理(相当重要)

之前啃小林 coding 和 编程指北 C++ 前面章节自己追问拓展精通了,回顾补充(针对作者原文的延展 & 豆包辅助后自己的深刻理解):

关于动态内存管理这块在面试中被考察频率非常高,切入的点也很多,有从操作系统虚拟内存问起的,也有从 malloc、new 等开始问起的。

但是无外乎就是两块内容:

  • 虚拟内存机制:物理和虚拟地址空间、TLB 页表、内存映射

  • 动态内存管理:内存管理、分配方式、内存回收、GC等等

在这篇文章中,我们将主要专注于动态内存管理这部分,从 malloc 和 free 的底层原理出发,一步步讲到 mmap和brk等知识。

mallocfree 是 C 语言中用于动态内存分配和释放内存的两个函数。

它们是 C 语言标准库的一部分,用于在程序运行期间请求和释放堆内存。

那么 malloc 分配的内存究竟从何而来呢?

进程地址空间:

由于虚拟内存的存在,每个进程就像独占整个地址空间一样,可寻址的空间大小是4G,linux系统下0-3G是用户模式,3-4G是内核模式。

而在用户模式下又分为:

  • 代码段:其中代码段主要存放进程的可执行二进制代码,字符串字面值和只读变量。

  • 数据段:存放已经初始化且初始值非0的全局变量和局部静态变量。

  • bss段:存放未初始化或初始值为0的全局变量和局部静态变量。

  • 堆段:是存放由用户动态分配内存存储的变量。

  • 栈段:主要存储局部变量、函数参数、返回地址等

  • 内存<128KB:走堆,free 后库缓存不还给内核,易碎片,复用快。free 后不立即还给内核,glibc 缓存留着复用,易产生内存碎片。泄漏后这块物理内存一直被进程攥住,进程不死,内存永久常驻不放,RSS 降不下去。

  • 内存>128KB:走匿名 mmapfree 直接还给内核,无碎片,释放即降内存。free 直接归还内核,无缓存,无碎片,频繁大开大合内存首选。大内存直接向内核映射物理页,泄漏会快速持续吃物理内存,RSS 飙升。

RSS:进程实际占用的物理内存大小,实打实吃了多少真机内存。

glibc 是 Linux 下 C/C++ 程序的底层运行库,malloc /free就是它实现的,glibc 给 malloc 定的一条分界线,MMAP_THRESHOLD 是阈值的意思,缺省为 128KB,也就是默认值:

  • 小于它走堆

  • 大于它走mmap 匿名映射

业务里小块内存创建销毁极频繁(放堆缓存可复用),大块多为临时缓冲区,用完即弃。

之前学的零拷贝是直接点名道性的写mmap。

MMAP_THRESHOLD:默认128KB,是 glibc 划分内存分配方式的界限值。

服务端实战用处:写网络大缓冲区、日志缓冲区,直接开超 128KB,自动走 mmap,长期服务更稳,减少内存膨胀。

mmap分配的内存不纳入堆管理,进程崩溃,mmap内存自动全部回收。

查看代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 小于 128KB → 走堆
    void* p1 = malloc(100 * 1024); // 100KB
    
    // 大于 128KB → 走 mmap
    void* p2 = malloc(130 * 1024);  // 130KB

    printf("p1(小内存) = %p\n", p1);
    printf("p2(大内存) = %p\n", p2);

    free(p1);
    free(p2);
}

/*
root@VM-0-7-ubuntu:~/cpp_projects_2# ./a
p1(小内存) = 0x55afc17932a0
p2(大内存) = 0x7f514ac20010
root@VM-0-7-ubuntu:~/cpp_projects_2# 
*/

55 开头堆,7f 开头 mmap。

mmap 映射区向下扩展,堆向上扩展,两者相对扩展,直到耗尽虚拟地址空间中的剩余区域。

在Linux中进程由进程控制块(PCB)描述,用一个task_struct 数据结构表示,这个数据结构记录了所有进程信息,包括进程状态、进程调度信息、标示符、进程通信相关信息、进程连接信息、时间和定时器、文件系统信息、虚拟内存信息等. 和malloc密切相关的就是虚拟内存信息,定义为struct mm_struct。

  • task_struct = PCB = 进程的身份证

  • mm_struct = 管理进程虚拟内存的总控结构

  • brk /start_brk = 堆的起始和结束地址,我们使用malloc动态分配的内存就在这之间。

  • start_stack是进程栈的起始地址,栈的大小是在编译时期确定的,在运行时不能改变。

  • malloc 小内存 = 调整 brk 指针(扩大堆)

  • 系统调用(brk/sbrk)很慢,所以 glibc 批量申请内存

posted @ 2026-06-27 18:35  GerJCS  阅读(12)  评论(0)    收藏  举报