C++ 借来的资源,如何还的潇洒?

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"正文"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所谓的资源就是,一旦用了它,将来必须还给系统。如果不是这样,糟糕的事情就会发生。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"C++ 程序内常见的资源:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"动态分配内存"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件描述符"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"互斥锁"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"图形页面中的字型和笔刷"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"数据库连接"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网络 sockets"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"无论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统,有借有还是个好习惯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"细节 01 : 以对象管理资源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"把资源放在析构函数,交给析构函数释放资源"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设某个 class 含有个工厂函数,该函数获取了对象的指针:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"A* createA(); // 返回指针,指向的是动态分配对象。\n // 调用者有责任删除它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上述注释所言,createA 的调用端使用了函数返回的对象后,有责任删除它。现在考虑有个"},{"type":"codeinline","content":[{"type":"text","text":"f"}]},{"type":"text","text":"函数履行了这个责任:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"void f()\n{\n A *pa = createA(); // 调用工厂函数\n ... // 其他代码\n delete pa; // 释放资源\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这看起来稳妥,但存在若干情况"},{"type":"codeinline","content":[{"type":"text","text":"f"}]},{"type":"text","text":"函数可能无法执行到"},{"type":"codeinline","content":[{"type":"text","text":"delete pa"}]},{"type":"text","text":"语句,也就会造成"},{"type":"text","marks":[{"type":"strong"}],"text":"资源泄漏"},{"type":"text","text":",例如如下情况:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或许因为「...」区域内的一个过早的 return 语句;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或许因为「...」区域内的一个循环语句过早的continue 或 goto 语句退出;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或许因为「...」区域内的语句抛出异常,无法执行到 delete。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然可以通过谨慎地编写程序可以防止这一类错误,但你必须想想,代码可能会在时间渐渐过去后被修改,如果是一个新手没有注意这一类情况,那必然又会再次有内存泄漏的可能性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为确保 A 返回的资源都是被回收,我们需要将资源放进对象内,当对象离开作用域时,该对象的析构函数会自动释放资源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「智能指针」是个好帮手,交给它去管理指针对象。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"对于是由动态分配(new)于堆内存的对象,指针对象离开了作用域并不会自动调用析构函数(需手动delete)"},{"type":"text","text":",为了让指针对象能像普通对象一样,离开作用域自动调用析构函数回收资源,我们需要借助「智能指针」的特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常用的「智能指针」有如下三个:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"std::auto_ptr( C++ 98 提供、C++ 11 建议摒弃不用 )"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"std::unique_ptr( C++ 11 提供 )"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"std::shared_ptr( C++ 11 提供 )"}]}]}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"std::auto_ptr"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面示范如何使用 std::auto_ptr 以避免 "},{"type":"codeinline","content":[{"type":"text","text":"f"}]},{"type":"text","text":" 函数潜在的资源泄漏可能性:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"void f()\n{\n std::auto_ptr pa (createA()); // 调用工厂函数\n ... // 一如既往的使用pa\n} // 离开作用域后,经由 auto_ptr 的析构函数自动删除pa;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个简单的例子示范「以对象管理资源」的两个关键想法:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"获得资源后立刻放进管理对象内"},{"type":"text","text":"。以上代码中 createA 返回的资源被当做其管理者 auto_ptr 的初值,也就立刻被放进了管理对象中。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"管理对象运用析构函数确保资源释放"},{"type":"text","text":"。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为什么在 C++11 建议弃用 auto"},{"type":"text","marks":[{"type":"italic"}],"text":"ptr 吗?当然是 auto"},{"type":"text","text":"ptr 存在缺陷,所以后续不被建议使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"auto_ptr 有一个不寻常的特质:若通过「复制构造函数或赋值操作符函数」 "},{"type":"text","marks":[{"type":"strong"}],"text":"copy"},{"type":"text","text":" 它们,它们会变成 "},{"type":"text","marks":[{"type":"strong"}],"text":"null"},{"type":"text","text":" ,而复制所得的指针将获取资源的唯一拥有权!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"见如下例子说明:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::auto_ptr pa1(createA()); // pa1 指向 createA 返回物\n\nstd::auto_ptr pa2(pa1); // 现在 pa2 指向对象,pa1将被设置为 null\n\npa1 = pa2; // 现在 pa1 指向对象,pa2 将被设置为 null"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这一诡异的复制行为,如果再次使用指向为 null 的指针,那必然会导致程序"},{"type":"text","marks":[{"type":"strong"}],"text":"奔溃"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"意味着 auto_ptr 并非管理动态分配资源的神兵利器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"std::unique_ptr"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"unique_ptr 也采用所有权模型,但是在使用时,是直接"},{"type":"text","marks":[{"type":"strong"}],"text":"禁止"},{"type":"text","text":"通过复制构造函数或赋值操作符函数 copy 指针对象,如下例子在编译时,会出错:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::unique_ptr pa1(createA()); // pa1 指向 createA 返回物\n\nstd::unique_ptr pa2(pa1); // 编译出错!\n\npa1 = pa2; // 编译出错!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"shared_ptr 在使用复制构造函数或赋值操作符函数后,"},{"type":"text","marks":[{"type":"strong"}],"text":"引用计会数累加并且两个指针对象指向的都是同一个块内存"},{"type":"text","text":",这就与 unique"},{"type":"text","marks":[{"type":"italic"}],"text":"ptr、auto"},{"type":"text","text":"ptr 不同之处。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"void f()\n{\n std::shared_ptr pa1(createA()); // pa1 指向 createA 返回物\n \n std::shared_ptr pa2(pa1); // 引用计数+1,pa2和pa1指向同一个内存\n \n pa1 = pa2; // 引用计数+1,pa2和pa1指向同一个内存\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当一个对象离开作用域,shared_ptr 会把引用计数值 -1 ,"},{"type":"text","marks":[{"type":"strong"}],"text":"直到引用计数值为 0 时,才会进行删除对象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于 shared_ptr 释放空间时会事先要判断"},{"type":"text","marks":[{"type":"strong"}],"text":"引用计数值的大小"},{"type":"text","text":",因此不会出现多次删除一个对象的错误。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小结 - 请记住"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为防止资源泄漏,请使用 RAII(Resource Acquisition Is Initaliaztion - 资源取得时机便是初始化时机) 对象,它们在构造函数中获取资源,并在析构函数中是释放资源"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"两个建议使用的 RAII classes 分别是 std::unique"},{"type":"text","marks":[{"type":"italic"}],"text":"ptr 和 std::shared"},{"type":"text","text":"ptr。前者不允许 copy 动作,后者允许 copy 动作。但是不建议用 std::auto"},{"type":"text","marks":[{"type":"italic"}],"text":"ptr,若选 auto"},{"type":"text","text":"ptr,复制动作会使它(被复制物)指向 null 。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"细节 02:在资源管理类中小心 copying 行为"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设,我们使用 C 语音的 API 函数处理类型为 Mutex 的互斥对象,共有 lock 和 unlock 两函数可用:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"void locak(Mutex *pm); // 锁定 pm 所指的互斥器\nvoid unlock(Mutex* pm); // 将互斥器解除锁定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为确保绝不会忘记一个被锁住的 Mutex 解锁,我们可能会希望创立一个 class 来管理锁资源。这样的 class 要遵守 RAII 守则,也就是「资源在构造期间获得,在析构释放期间释放」:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"class Lock\n{\npublic:\n explicit Lock(Mutex *pm) // 构造函数\n : pMutex(pm)\n {\n lock(pMutex);\n }\n \n ~Lock() // 析构函数\n {\n unlock(pMutex);\n }\nprivate:\n Mutex* pMutex;\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样定义的 Lock,用法符合 RAII 方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"Mutex m; //定义你需要的互斥锁\n... \n{ // 建立一个局部区块作用域\n Lock m1(&m); // 锁定互斥器\n ...\n} // 在离开区块作用域,自动解除互斥器锁定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这很好,但如果 Lock 对象被复制,会发生什么事情?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"Lock m1(&m); // 锁定m\nLock m2(&m1); // 将 m1 复制到 m2身上,这会发生什么?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这是我们需要思考和面对的:「当一个 RAII 对象被复制,会发生什么事情?」大多数时候你会选择以下两种可能:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"禁止复制"},{"type":"text","text":"。如果 RAII 不允许被复制,那我们需要将 class 的复制构造函数和赋值操作符函数声明在 private。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"使用引用计数法"},{"type":"text","text":"。有时候我们希望保有资源,直到它直的最后一个对象被消耗。这种情况下复制 RAII 对象时,应该将资源的「被引用数」递增。std::shared_ptr 便是如此。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果前述的 Lock 打算使用"},{"type":"text","marks":[{"type":"strong"}],"text":"使用引用计数法"},{"type":"text","text":",它可以使用 std::shared"},{"type":"text","marks":[{"type":"italic"}],"text":"ptr 来管理 pMutex 指针,然后很不幸 std::shared"},{"type":"text","text":"ptr 的默认行为是「当引用次数为 0 时删除其所指物」那不是我们想要的行为,"},{"type":"text","marks":[{"type":"strong"}],"text":"因为要对 Mutex 释放动作是解锁而非删除。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幸运的是 std::shared_ptr 允许指定"},{"type":"text","marks":[{"type":"strong"}],"text":"自定义的删除方式"},{"type":"text","text":",那是一个函数或函数对象。如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"class Lock\n{\npublic:\n explicit Lock(Mutex *pm) \n : pMutex(pm, unlock) // 以某个 Mutex 初始化 shared_ptr,\n // 并以 unlock 函数为删除器。\n {\n lock(pMutex.get()); // get 获取指针地址\n }\n \nprivate:\n std::shared_ptr pMutex; // 使用 shared_ptr\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"请注意,本例的 Lock class 不再声明析构函数。因为编译器会自动创立默认的析构函数,来自动调用其 non-static 成员变量(本例为 pMutex )的析构函数。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 pMutex 的析构函数会"},{"type":"text","marks":[{"type":"strong"}],"text":"在互斥器的引用次数为 0 时,自动调用 std::shared_ptr 的删除器(本例为 unlock )"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小结 - 请记住"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"复制 RAII 对象必须一并复制它的所管理的资源(深拷贝),所以资源的 copying 行为决定 RAII 对象的 copying 行为。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"普通而常见的 RAII class copying 行为是:禁止 copying、施行引用计数法。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"细节 03 :在资源类中提供对原始资源的访问"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"智能指针「显式」转换,也就是通过 get 成员函数的方式转换为原始指针对象。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面提到的「智能指针」分别是:std::auto"},{"type":"text","marks":[{"type":"italic"}],"text":"ptr、std::unique"},{"type":"text","text":"ptr、std::shared_ptr。它们都有访问原始资源的办法,都提供了一个 get 成员函数,用来执行"},{"type":"text","marks":[{"type":"strong"}],"text":"显式转换"},{"type":"text","text":",也就是它会"},{"type":"text","marks":[{"type":"strong"}],"text":"返回智能指针内部的原始指针(的复件)"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"举个例子,使用智能指针如 std::shared_ptr 保存 createA() 返回的指针对象 :"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::shared_ptr pA(createA());"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设你希望以某个函数处理 A 对象,像这样:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"int getInfo(const A* pA);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你想这么调用它:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::shared_ptr pA(createA());\ngetInfo(pA); // 错误!!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"会编译错误,因为 getInfo 需要的是 "},{"type":"codeinline","content":[{"type":"text","text":"A"}]},{"type":"text","text":" 指针对象,而不是类型为 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 的对象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这时候就需要用 std::shared_ptr 智能指针提供的 "},{"type":"codeinline","content":[{"type":"text","text":"get"}]},{"type":"text","text":" 成员函数访问原始的资源:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::shared_ptr pA(createA());\ngetInfo(pA.get()); // 很好,将 pA 内的原始指针传递给 getInfo"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"智能指针「隐式」转换的方式,是通过指针取值操作符。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"智能指针都重载了指针取值操作符(operator->和operator*),它们允许"},{"type":"text","marks":[{"type":"strong"}],"text":"隐式转换"},{"type":"text","text":"至底部原始指针:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"class A\n{\npublic:\n bool isExist() const;\n ...\n};\n\nA* createA(); // 工厂函数,创建指针对象\n\nstd::shared_ptr pA(createA()); // 令 shared_ptr 管理对象资源\n\nbool exist = pA->isExist(); // 经由 operator-> 访问资源\nbool exist2 = (*pA).isExist(); // 经由 operator* 访问资源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多数设计良好的 classes 一样,它隐藏了程序员不需要看到的部分,但是有程序员需要的所有东西。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以对于自身设计 RAII classes 我们也要提供一个「取得其所管理的资源」的办法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小结 - 请记住"}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"APIs 往往要求访问原始资源,所以每一个 RAII class 应该提供一个「取得其所管理的资源」的办法。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换比较方便。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"细节 04:成对使用 new 和 delete "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下动作有什么错?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::string* strArray = new std::string[100];\n...\ndelete strArray;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每件事情看起来都井然有序。使用了 new,也搭配了对应的 delete。但还是有某样东西完全错误。strArray 所含的 100 个 string 对象中的 99 个不太可能被适当删除,因为它们的析构函数很可能没有被调用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当使用 new ,有两件事发生:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内存被分配出来(通过名为 operator new 的函数)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"针对此内存会有"},{"type":"text","marks":[{"type":"strong"}],"text":"一个或多个"},{"type":"text","text":"构造函数被调用"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当使用 delete,也会有两件事情:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"针对此内存会有"},{"type":"text","marks":[{"type":"strong"}],"text":"一个或多个"},{"type":"text","text":"析构函数被调用"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后内存才被释放(通过名为 operator delete 的函数)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"delete 的最大问题在于:"},{"type":"text","marks":[{"type":"strong"}],"text":"即将被删除的内存之内究竟有多少对象?这个答案决定了需要执行多少个析构函数。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对象数组所用的内存通常还包括「数组大小」的记录,以便 delete 知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。你可以把两者不同的内存布局想象如下,其中 n 是数组大小:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当你对着一个指针使用 delete,唯一能够让 delete 知道内存中是否存在一个「数组大小记录」的办法就是:由你告诉它。如果你使用 delete 时加上中括号"},{"type":"text","marks":[{"type":"strong"}],"text":"[]"},{"type":"text","text":",delete 便认定指针指向一个数组,否则它便认定指针指向一个单一对象:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::string* strArray = new std::string[100];\nstd::string* strPtr = new std::strin;\n... \ndelete [] strArray; // 删除一个对象\ndelete strPtr; // 删除一个由对象组成的数组"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"游戏规则很简单:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你在 new 表达式中"},{"type":"text","marks":[{"type":"strong"}],"text":"使用[]"},{"type":"text","text":",必须在相应的 delete 表达式也"},{"type":"text","marks":[{"type":"strong"}],"text":"使用[]"},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你在 new 表达式中"},{"type":"text","marks":[{"type":"strong"}],"text":"不使用[]"},{"type":"text","text":",一定"},{"type":"text","marks":[{"type":"strong"}],"text":"不要"},{"type":"text","text":"在相应的 delete 表达式"},{"type":"text","marks":[{"type":"strong"}],"text":"使用[]"},{"type":"text","text":"。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小结 - 请记住"}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你在 new 表达式中使用[],必须在相应的 delete 表达式也使用[]。如果你在 new 表达式中不使用[],一定不要在相应的 delete 表达式使用[]。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"细节 05:以独立语句将 newed (已被 new 的)对象置入智能指针"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设我们有个以下示范的函数:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"int getNum();\nvoid fun(std::shared_ptr pA, int num);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在考虑调用 fun:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"fun(new A(), getNum());"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它不能通过编译,因为 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 构造函数需要一个原始指针,而且该构造函数是个 "},{"type":"codeinline","content":[{"type":"text","text":"explicit"}]},{"type":"text","text":" 构造函数,无法进行隐式转换。如果写成这样就可以编译通过:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"fun(std::shared_ptr(new A), getNum());"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"令人想不到吧,上述调用却可能"},{"type":"text","marks":[{"type":"strong"}],"text":"泄露资源"},{"type":"text","text":"。接下来我们来一步一步的分析为什么存在内存泄漏的可能性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在进入 "},{"type":"codeinline","content":[{"type":"text","text":"fun"}]},{"type":"text","text":" 函数之前,肯定会先执行各个实参。上述第二个实参只是单纯的对 "},{"type":"codeinline","content":[{"type":"text","text":"getNum"}]},{"type":"text","text":" 函数的调用,但第一个实参 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr(new A)"}]},{"type":"text","text":" 由两部分组成:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"执行 "},{"type":"codeinline","content":[{"type":"text","text":"new A"}]},{"type":"text","text":" 表达式"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调用 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 构造函数"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"于是在调用 "},{"type":"codeinline","content":[{"type":"text","text":"fun"}]},{"type":"text","text":" 函数之前,先必须做以下三件事:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调用 "},{"type":"codeinline","content":[{"type":"text","text":"getNum"}]},{"type":"text","text":" 函数"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"执行 "},{"type":"codeinline","content":[{"type":"text","text":"new A"}]},{"type":"text","text":" 表达式"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调用 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 构造函数"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么他们的执行次序是一定如上述那样的吗?可以确定的是 "},{"type":"codeinline","content":[{"type":"text","text":"new A"}]},{"type":"text","text":" 一定比 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 构造函数先被执行。但对 getNum 调用可以排在第一或第二或第三执行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果编译器选择以第二顺位执行它:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"执行 "},{"type":"codeinline","content":[{"type":"text","text":"new A"}]},{"type":"text","text":" 表达式"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"调用 "},{"type":"codeinline","content":[{"type":"text","text":"getNum"}]},{"type":"text","text":" 函数"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"调用 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 构造函数"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"万一在调用 "},{"type":"codeinline","content":[{"type":"text","text":"getNum"}]},{"type":"text","text":" 函数发生了异常,会发生什么事情?在此情况下 "},{"type":"codeinline","content":[{"type":"text","text":"new A"}]},{"type":"text","text":" 返回的指针将不会置入 "},{"type":"codeinline","content":[{"type":"text","text":"std::shared_ptr"}]},{"type":"text","text":" 智能指针里,就"},{"type":"text","marks":[{"type":"strong"}],"text":"存在内存泄漏的现象"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"避免这类问题的办法很简单:使用"},{"type":"text","marks":[{"type":"strong"}],"text":"分离语句"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分别写出:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"创建 A"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"将它置入一个智能指针内"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"然后再把智能指针传递给 "},{"type":"codeinline","content":[{"type":"text","text":"fun"}]},{"type":"text","text":" 函数。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"std::shared_ptr pA(new A); // 先构造智能指针对象\nfun(pA, getNum()); // 这个调用动作绝不至于造成泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上的方式,就能避免原本由于次序导致内存泄漏发生。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小结 - 请记住"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以独立语句将 newed (已 new 过) 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"最后"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文部分内容参考了《Effective C++ (第3版本)》第三章节内容,前两章节的内容可看旧文"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"《"},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s/3dUQpmKsA-6LHopYY9u4HQ","title":""},"content":[{"type":"text","text":"学过 C++ 的你,不得不知的这 10 条细节!"}]},{"type":"text","text":"》"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c5/c5a651ce460ea82cc1e4a612c6a374ab.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章