极客时间已完结课程限时免费阅读

02 | 自己动手,实现C++的智能指针

02 | 自己动手,实现C++的智能指针-极客时间

02 | 自己动手,实现C++的智能指针

讲述:吴咏炜

时长14:26大小13.18M

你好,我是吴咏炜。
上一讲,我们描述了一个某种程度上可以当成智能指针用的类 shape_wrapper。使用那个智能指针,可以简化资源的管理,从根本上消除资源(包括内存)泄漏的可能性。这一讲我们就来进一步讲解,如何将 shape_wrapper 改造成一个完整的智能指针。你会看到,智能指针本质上并不神秘,其实就是 RAII 资源管理功能的自然展现而已。
在学完这一讲之后,你应该会对 C++ 的 unique_ptrshared_ptr 的功能非常熟悉了。同时,如果你今后要创建类似的资源管理类,也不会是一件难事。

回顾

我们上一讲给出了下面这个类:
class shape_wrapper {
public:
explicit shape_wrapper(
shape* ptr = nullptr)
: ptr_(ptr) {}
~shape_wrapper()
{
delete ptr_;
}
shape* get() const { return ptr_; }
private:
shape* ptr_;
};
这个类可以完成智能指针的最基本的功能:对超出作用域的对象进行释放。但它缺了点东西:
这个类只适用于 shape
该类对象的行为不够像指针
拷贝该类对象会引发程序行为异常
下面我们来逐一看一下怎么弥补这些问题。

模板化和易用性

要让这个类能够包装任意类型的指针,我们需要把它变成一个类模板。这实际上相当容易:
template <typename T>
class smart_ptr {
public:
explicit smart_ptr(T* ptr = nullptr)
: ptr_(ptr) {}
~smart_ptr()
{
delete ptr_;
}
T* get() const { return ptr_; }
private:
T* ptr_;
};
shape_wrapper 比较一下,我们就是在开头增加模板声明 template <typename T>,然后把代码中的 shape 替换成模板参数 T 而已。这些修改非常简单自然吧?模板本质上并不是一个很复杂的概念。这个模板使用也很简单,把原来的 shape_wrapper 改成 smart_ptr<shape> 就行。
目前这个 smart_ptr 的行为还是和指针有点差异的:
它不能用 * 运算符解引用
它不能用 -> 运算符指向对象成员
它不能像指针一样用在布尔表达式里
不过,这些问题也相当容易解决,加几个成员函数就可以:
template <typename T>
class smart_ptr {
public:
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
operator bool() const { return ptr_; }
}

拷贝构造和赋值

拷贝构造和赋值,我们暂且简称为拷贝,这是个比较复杂的问题了。关键还不是实现问题,而是我们该如何定义其行为。假设有下面的代码:
smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1};
对于第二行,究竟应当让编译时发生错误,还是可以有一个更合理的行为?我们来逐一检查一下各种可能性。
最简单的情况显然是禁止拷贝。我们可以使用下面的代码:
template <typename T>
class smart_ptr {
smart_ptr(const smart_ptr&)
= delete;
smart_ptr& operator=(const smart_ptr&)
= delete;
};
禁用这两个函数非常简单,但却解决了一种可能出错的情况。否则,smart_ptr<shape> ptr2{ptr1}; 在编译时不会出错,但在运行时却会有未定义行为——由于会对同一内存释放两次,通常情况下会导致程序崩溃。
我们是不是可以考虑在拷贝智能指针时把对象拷贝一份?不行,通常人们不会这么用,因为使用智能指针的目的就是要减少对象的拷贝啊。何况,虽然我们的指针类型是 shape,但实际指向的却应该是 circletriangle 之类的对象。在 C++ 里没有像 Java 的 clone 方法这样的约定;一般而言,并没有通用的方法可以通过基类的指针来构造出一个子类的对象来。
我们要么试试在拷贝时转移指针的所有权?大致实现如下:
template <typename T>
class smart_ptr {
smart_ptr(smart_ptr& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr& rhs)
{
smart_ptr(rhs).swap(*this);
return *this;
}
T* release()
{
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
}
};
在拷贝构造函数中,通过调用 otherrelease 方法来释放它对指针的所有权。在赋值函数中,则通过拷贝构造产生一个临时对象并调用 swap 来交换对指针的所有权。实现上是不复杂的。
如果你学到的赋值函数还有一个类似于 if (this != &rhs) 的判断的话,那种用法更啰嗦,而且异常安全性不够好——如果在赋值过程中发生异常的话,this 对象的内容可能已经被部分破坏了,对象不再处于一个完整的状态。
上面代码里的这种惯用法(见参考资料 [1])则保证了强异常安全性:赋值分为拷贝构造和交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this 对象完全不受任何影响。无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。
如果你觉得这个实现还不错的话,那恭喜你,你达到了 C++ 委员会在 1998 年时的水平:上面给出的语义本质上就是 C++98 的 auto_ptr 的定义。如果你觉得这个实现很别扭的话,也恭喜你,因为 C++ 委员会也是这么觉得的:auto_ptr 在 C++17 时已经被正式从 C++ 标准里删除了。
上面实现的最大问题是,它的行为会让程序员非常容易犯错。一不小心把它传递给另外一个 smart_ptr,你就不再拥有这个对象了……

“移动”指针?

在下一讲我们将完整介绍一下移动语义。这一讲,我们先简单看一下 smart_ptr 可以如何使用“移动”来改善其行为。
我们需要对代码做两处小修改:
template <typename T>
class smart_ptr {
smart_ptr(smart_ptr&& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs)
{
rhs.swap(*this);
return *this;
}
};
看到修改的地方了吗?我改了两个地方:
把拷贝构造函数中的参数类型 smart_ptr& 改成了 smart_ptr&&;现在它成了移动构造函数。
把赋值函数中的参数类型 smart_ptr& 改成了 smart_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。
根据 C++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用(记住,C++ 里那些复杂的规则也是为方便编程而设立的)。于是,我们自然地得到了以下结果:
smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1}; // 编译出错
smart_ptr<shape> ptr3;
ptr3 = ptr1; // 编译出错
ptr3 = std::move(ptr1); // OK,可以
smart_ptr<shape> ptr4{std::move(ptr3)}; // OK,可以
这个就自然多了。
这也是 C++11 的 unique_ptr 的基本行为。

子类指针向基类指针的转换

哦,我撒了一个小谎。不知道你注意到没有,一个 circle* 是可以隐式转换成 shape* 的,但上面的 smart_ptr<circle> 却无法自动转换成 smart_ptr<shape>。这个行为显然还是不够“自然”。
不过,只需要额外加一点模板代码,就能实现这一行为。在我们目前给出的实现里,只需要增加一个构造函数即可——这也算是我们让赋值函数利用构造函数的好处了。
template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.release();
}
这样,我们自然而然利用了指针的转换特性:现在 smart_ptr<circle> 可以移动给 smart_ptr<shape>,但不能移动给 smart_ptr<triangle>。不正确的转换会在代码编译时直接报错。
需要注意,上面这个构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 了(见“拷贝构造和赋值”一节)。不过,更通用的方式仍然是同时定义标准的拷贝 / 移动构造函数和所需的模板构造函数。下面的引用计数智能指针里我们就需要这么做。
至于非隐式的转换,因为本来就是要写特殊的转换函数的,我们留到这一讲的最后再讨论。

引用计数

unique_ptr 算是一种较为安全的智能指针了。但是,一个对象只能被单个 unique_ptr 所拥有,这显然不能满足所有使用场合的需求。一种常见的情况是,多个智能指针同时拥有一个对象;当它们全部都失效时,这个对象也同时会被删除。这也就是 shared_ptr 了。
unique_ptrshared_ptr 的主要区别如下图所示:
多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一对象时也需要同时共享同一个计数。当最后一个指向对象(和共享计数)的 shared_ptr 析构时,它需要删除对象和共享计数。我们下面就来实现一下。
我们先来写出共享计数的接口:
class shared_count {
public:
shared_count();
void add_count();
long reduce_count();
long get_count() const;
};
这个 shared_count 类除构造函数之外有三个方法:一个增加计数,一个减少计数,一个获取计数。注意上面的接口增加计数不需要返回计数值;但减少计数时需要返回计数值,以供调用者判断是否它已经是最后一个指向共享计数的 shared_ptr 了。由于真正多线程安全的版本需要用到我们目前还没学到的知识,我们目前先实现一个简单化的版本:
class shared_count {
public:
shared_count() : count_(1) {}
void add_count()
{
++count_;
}
long reduce_count()
{
return --count_;
}
long get_count() const
{
return count_;
}
private:
long count_;
};
现在我们可以实现我们的引用计数智能指针了。首先是构造函数、析构函数和私有成员变量:
template <typename T>
class smart_ptr {
public:
explicit smart_ptr(T* ptr = nullptr)
: ptr_(ptr)
{
if (ptr) {
shared_count_ =
new shared_count();
}
}
~smart_ptr()
{
if (ptr_ &&
!shared_count_
->reduce_count()) {
delete ptr_;
delete shared_count_;
}
}
private:
T* ptr_;
shared_count* shared_count_;
};
构造函数跟之前的主要不同点是会构造一个 shared_count 出来。析构函数在看到 ptr_ 非空时(此时根据代码逻辑,shared_count 也必然非空),需要对引用数减一,并在引用数降到零时彻底删除对象和共享计数。原理就是这样,不复杂。
当然,我们还有些细节要处理。为了方便实现赋值(及其他一些惯用法),我们需要一个新的 swap 成员函数:
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
swap(shared_count_,
rhs.shared_count_);
}
赋值函数可以跟前面一样,保持不变,但拷贝构造和移动构造函数是需要更新一下的:
smart_ptr(const smart_ptr& other)
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& other)
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}
除复制指针之外,对于拷贝构造的情况,我们需要在指针非空时把引用数加一,并复制共享计数的指针。对于移动构造的情况,我们不需要调整引用数,直接把 other.ptr_ 置为空,认为 other 不再指向该共享对象即可。
不过,上面的代码有个问题:它不能正确编译。编译器会报错,像:
fatal error: ‘ptr_’ is a private member of ‘smart_ptr<circle>’
错误原因是模板的各个实例间并不天然就有 friend 关系,因而不能互访私有成员 ptr_shared_count_。我们需要在 smart_ptr 的定义中显式声明:
template <typename U>
friend class smart_ptr;
此外,我们之前的实现(类似于单一所有权的 unique_ptr )中用 release 来手工释放所有权。在目前的引用计数实现中,它就不太合适了,应当删除。但我们要加一个对调试非常有用的函数,返回引用计数值。定义如下:
long use_count() const
{
if (ptr_) {
return shared_count_
->get_count();
} else {
return 0;
}
}
这就差不多是一个比较完整的引用计数智能指针的实现了。我们可以用下面的代码来验证一下它的功能正常:
class shape {
public:
virtual ~shape() {}
};
class circle : public shape {
public:
~circle() { puts("~circle()"); }
};
int main()
{
smart_ptr<circle> ptr1(new circle());
printf("use count of ptr1 is %ld\n",
ptr1.use_count());
smart_ptr<shape> ptr2;
printf("use count of ptr2 was %ld\n",
ptr2.use_count());
ptr2 = ptr1;
printf("use count of ptr2 is now %ld\n",
ptr2.use_count());
if (ptr1) {
puts("ptr1 is not empty");
}
}
这段代码的运行结果是:
use count of ptr1 is 1
use count of ptr2 was 0
use count of ptr2 is now 2
ptr1 is not empty
~circle()
上面我们可以看到引用计数的变化,以及最后对象被成功删除。

指针类型转换

对应于 C++ 里的不同的类型强制转换:
static_cast
reinterpret_cast
const_cast
dynamic_cast
智能指针需要实现类似的函数模板。实现本身并不复杂,但为了实现这些转换,我们需要添加构造函数,允许在对智能指针内部的指针对象赋值时,使用一个现有的智能指针的共享计数。如下所示:
template <typename U>
smart_ptr(const smart_ptr<U>& other,
T* ptr)
{
ptr_ = ptr;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
这样我们就可以实现转换所需的函数模板了。下面实现一个 dynamic_pointer_cast 来示例一下:
template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(
const smart_ptr<U>& other)
{
T* ptr =
dynamic_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}
在前面的验证代码后面我们可以加上:
smart_ptr<circle> ptr3 =
dynamic_pointer_cast<circle>(ptr2);
printf("use count of ptr3 is %ld\n",
ptr3.use_count());
编译会正常通过,同时能在输出里看到下面的结果:
use count of ptr3 is 3
最后,对象仍然能够被正确删除。这说明我们的实现是正确的。

代码列表

为了方便你参考,下面我给出了一个完整的 smart_ptr 代码列表:
#include <utility> // std::swap
class shared_count {
public:
shared_count() noexcept
: count_(1) {}
void add_count() noexcept
{
++count_;
}
long reduce_count() noexcept
{
return --count_;
}
long get_count() const noexcept
{
return count_;
}
private:
long count_;
};
template <typename T>
class smart_ptr {
public:
template <typename U>
friend class smart_ptr;
explicit smart_ptr(T* ptr = nullptr)
: ptr_(ptr)
{
if (ptr) {
shared_count_ =
new shared_count();
}
}
~smart_ptr()
{
if (ptr_ &&
!shared_count_
->reduce_count()) {
delete ptr_;
delete shared_count_;
}
}
smart_ptr(const smart_ptr& other)
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_->add_count();
shared_count_ = other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& other,
T* ptr) noexcept
{
ptr_ = ptr;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
smart_ptr&
operator=(smart_ptr rhs) noexcept
{
rhs.swap(*this);
return *this;
}
T* get() const noexcept
{
return ptr_;
}
long use_count() const noexcept
{
if (ptr_) {
return shared_count_
->get_count();
} else {
return 0;
}
}
void swap(smart_ptr& rhs) noexcept
{
using std::swap;
swap(ptr_, rhs.ptr_);
swap(shared_count_,
rhs.shared_count_);
}
T& operator*() const noexcept
{
return *ptr_;
}
T* operator->() const noexcept
{
return ptr_;
}
operator bool() const noexcept
{
return ptr_;
}
private:
T* ptr_;
shared_count* shared_count_;
};
template <typename T>
void swap(smart_ptr<T>& lhs,
smart_ptr<T>& rhs) noexcept
{
lhs.swap(rhs);
}
template <typename T, typename U>
smart_ptr<T> static_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = static_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}
template <typename T, typename U>
smart_ptr<T> reinterpret_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = reinterpret_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}
template <typename T, typename U>
smart_ptr<T> const_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = const_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}
template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = dynamic_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}
如果你足够细心的话,你会发现我在代码里加了不少 noexcept。这对这个智能指针在它的目标场景能正确使用是十分必要的。我们会在下面的几讲里回到这个话题。

内容小结

这一讲我们从 shape_wrapper 出发,实现了一个基本完整的带引用计数的智能指针。这个智能指针跟标准的 shared_ptr 比,还缺了一些东西(见参考资料 [2]),但日常用到的智能指针功能已经包含在内。现在,你应当已经对智能指针有一个较为深入的理解了。

课后思考

这里留几个问题,你可以思考一下:
不查阅 shared_ptr 的文档,你觉得目前 smart_ptr 应当添加什么功能吗?
你想到的功能在标准的 shared_ptr 里吗?
你觉得智能指针应该满足什么样的线程安全性?
欢迎留言和我交流你的看法。

参考资料

[1] Stack Overflow, GManNickG’s answer to “What is the copy-and-swap idiom?”. https://stackoverflow.com/a/3279550/816999
[2] cppreference.com, “std::shared_ptr”. https://en.cppreference.com/w/cpp/memory/shared_ptr
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 26

提建议

上一篇
01 | 堆、栈、RAII:C++里该如何管理资源?
下一篇
03 | 右值和移动究竟解决了什么问题?
 写留言

精选留言(182)

  • frazer
    2019-11-26
    有点吃力了,得反复看几遍

    作者回复: 没关系。我打赌你看的时间肯定没我写稿的时间长。😁

    共 4 条评论
    92
  • yuchen
    2019-11-26
    有深度的专栏,不错。市面上讲解C++的课程一般太基础了。这一章推荐读者可以看看《Professional C++ 4th edition》第九章。

    作者回复: 谢谢。 《Professional C++》之前没看过,扫了两眼,觉得内容不错,推荐。内容还挺多挺深的,适合决心在 C++ 上深入的同学。

    共 5 条评论
    30
  • 流浪地球
    2019-11-26
    老师您好,问一个比较基础的问题,我理解这个语句 smart_ptr<shape> ptr1{create_shape(shape_type::circle)}; 是调用ptr1的拷贝构造函数。 为什么{create_shape(shape_type::circle)}是使用大括号,不应该是小括号吗? 谢谢

    作者回复: 嘻嘻,我在偷偷地塞进C++11的语法。对象初始化可以统一用大括号。(小括号这儿也行。)

    共 5 条评论
    29
  • 皓首不倦
    2019-11-30
    请问下老师smart ptr 的拷贝构造函数为什么有一个泛型版本 还有一个非泛型版本 但是函数体内容又一模一样 不是代码冗余的吗 是有什么特殊设计意图吗 请老师指教下

    作者回复: 这是一个很特殊的、甚至有点恼人的情况。如果没有非泛型版本,编译器看到没有拷贝构造函数,会生成一个缺省的拷贝构造函数。这样,同样类型的smart_ptr的拷贝构造会是错误的。“子类指针向基类指针的转换”这一节里我也提到了这点。这不是我讲智能指针想讲的内容,所以就淡化了。

    共 3 条评论
    27
  • NEVER SETTLE
    2019-11-27
    老师这块没想明白 // 1、调用构造函数 smart_ptr ptr1{create_shape(shape_type::circle)}; // 2、因为拷贝构造被禁用,随意编译出错 smart_ptr ptr2{ptr1}; // 编译出错 smart_ptr ptr3; // 3、没明白为啥会出错 ptr3 = ptr1; // 编译出错 // 4、没明白为啥OK,=重载函数的参数不是右值引用呀 ptr3 = std::move(ptr1); // OK, 请老师指定 3 与 4
    展开

    作者回复: 3. 赋值需要一个对象(不是引用),因而在进入执行前就要引发一个构造。没有合适的构造函数可用。 4. 同样,要先构造。这回可以用右值引用的构造函数了。

    共 3 条评论
    23
  • yyfx
    2019-11-28
    "移动"指针部分有个问题。在使用模板泛化拷贝构造时,p2=p1编译通过,程序异常。测试发现,由于拷贝构造并没有被自动禁用导致。查了下资料,effective45条提到,member templates不影响语言规则,声明member templates用于泛化copy构造时,还需要声明正常的copy构造。

    作者回复: 多谢抓虫。正文已更新。 这也证明了,修改代码、没有完整测试,是极易招虫的啊……

    17
  • hdongdong123
    2019-11-27
    真的好难啊,呜呜呜

    作者回复: 一遍看不懂,就再看一遍。所有的代码自己试验一下。😀 学习无捷径。掌握 C++ 不是 30 个课时能解决的事情。一万小时理论对于任何复杂领域都是基本适用的。

    14
  • 总统老唐
    2019-12-12
    吴老师,关于如何使用移动改善了 auto_ptr 的行为,实现了与 unique_ptr 相近的语义,这里的“改善”是否应该这样理解: 1, 在没有引入右值引用和移动构造的情况下,构造的 auto_ptr 也能工作,但是会令用户困惑,当用户执行了语句 other_ptr = some_ptr 后,some_ptr 就为空了 2, 引入右值引用和移动构造后,用户要想用重载的 operator=,必须采用 other_ptr = std::move(some_ptr) 的方式,通过显示调用 std::move,让用户aware到 some_ptr 的内容已经被移动到 other_ptr 了,并且同时默认禁用了参数为左值引用的拷贝构造,导致 other_ptr = some_ptr 无法通过编译,就不会在使用时产生歧义了,这就是 unique_ptr 期望的语义
    展开

    作者回复: 对。 另外额外加一点,调用移动不一定靠 move。如果函数返回一个 unique_ptr 一样是自然的移动。

    共 2 条评论
    13
  • 贵子
    2019-12-13
    为什么shared_count类作为smart_ptr的内部类编译不过,而必须作为外部类呢?老师能解释一下吗?谢谢!

    作者回复: 移进去的话,smart_ptr<circle>::shared_count 和 smart_ptr<shape>::shared_count 成了两个完全不相关的类型,它们的指针(在不做强制类型转换时)也不能互相赋值,不好。

    共 2 条评论
    11
  • Lilin
    2019-11-26
    才第二节,就有点吃力了。这篇专栏真是满满的干货

    作者回复: 希望是难,但还能看得下去。哈哈。

    10
  • NEVER SETTLE
    2019-11-27
    老师,问个比较基础的问题,就是运算符重载的时候,我老师搞不清 该返回值 还是 返回引用,这取决于什么?

    作者回复: 如果你搞不清,那多半就是不该返回引用的情况。下面会讲到这个话题。简单来说,大部分情况下,应该直接返回对象值。

    共 2 条评论
    8
  • W.jyao
    2019-11-26
    对不熟悉C++ 11的程序员来看,有的地方不是很懂

    作者回复: 特别有困难的点可以提出。大部分概念的引入我应该都是有解释的。

    8
  • nullptr
    2019-12-07
    我一直在纠结那个拷贝赋值参数不是引用的问题,不是引用的话,会产生一次拷贝构造函数,所以已经增加了引用计数,接着进行简单的swap操作就ok了,读者需要思考这个问题,很多人会忽略

    作者回复: 是这样的。所以第3讲我把这个单独作为问题提出来了。

    8
  • lyfei
    2019-12-16
    老师您好,就是我们使用智能指针创建一个对象以后, 包括这个私有成员T* ptr_以及其他都是放在栈空间上的吧?这样就可以对象不在作用域以后,自动调用析构函数,然后delete开辟的相关空间了吧? 智能指针实质上就是使用一个"栈"上的对象来管理一块内存地址(可能是堆可能是栈),用"栈"就可以保证析构函数的进行,也就保证了delete的进行,也就避免了内存泄露的问题。 是不是可以这样理解? 那我们应该什么时候使用new delete来管理内存,什么时候该使用智能指针呢? 这些方面有些疑惑,谢谢老师您的讲解与回复。
    展开

    作者回复: 是,一般情况下是。但没人阻挡你写 new shared_ptr…。 被管理的对象不会在栈上。只能是堆或者你的内存管理器使用的特殊区域。 目前的推荐是不要使用裸指针。使用值,或者智能指针。

    共 2 条评论
    6
  • robonix
    2019-11-29
    老师,文中提到的发生异常,this对象被破坏,具体是指的对象里的什么成员被破坏呢,这个没看的太明白。

    作者回复: 比如,你的对象有两个成员,管理着两个对象,如果你在拷贝赋值时逐个地复制另外一个对象的相应的两个成员对象,在复制第二个时发生了异常,那 this 对象就处于一个奇怪的状态:既没成功完成复制,也没有了原先的对象。

    7
  • 文若
    2020-02-27
    老师,能否推荐一些比较经典的C++开源项目,特别是关于后端开发的。

    作者回复: 23 到 27 讲的内容可以看一下。 专注后端开发的,可以看 Boost.Asio。专栏讨论了 C++ REST SDK(其中用到了 Boost.Asio)。 其他可以考虑的(没有用过,没有明确推荐): https://www.treefrogframework.org/ https://www.webtoolkit.eu/wt http://pistache.io/ https://github.com/ipkn/crow https://github.com/qicosmos/cinatra

    5
  • czh
    2020-01-31
    这种优质文章,对于像我一样的初学者,还是要换气读,要不容易呛水,哈哈哈哈哈
    5
  • metalmac.kyle
    2019-12-01
    通过老师的例子来学习深入学习智能指针,感觉这种案例式都谢谢代入感很强,再难的知识点也不枯燥还能直接学以致用,醍醐灌顶,有种重读csapp的感觉!Cpp一大特性就是各种复杂的语法以及符号设计,这让她被爱的人奉上神坛,恨的人也恨之入骨,指针作为双刃剑充满了恶魔般的魔力,虽然会让人望而却步但也是吸引我一直学习cpp的动力所在,对底层的操作和理解实在让人着魔哈哈。 言归正传,小小的构造智能指针课蕴含了对Cpp一系列知识的复习巩固,构造函数,运算符重载,指针引用特性,模板等各类知识点的设计与实现,想真正掌握还需要反复上机实践呢,还是应了古话,纸上得来终觉欲知此事须躬行浅!🧐
    展开

    作者回复: 没错,相关的知识点很多,一定要多实践才行。 不过,回过头来看,我觉得我拿智能指针开头,难度可能是有点设高了。哈哈。

    5
  • 净莲妖火
    2019-11-29
    完整的还缺++,- -,+=,-=,<,>等普通指针支持的操作

    作者回复: 嗯,是的。你是最早想起指针还有这些操作的。😁

    共 3 条评论
    5
  • 姜姜
    2019-11-26
    这个smart_ptr目前已经实现了:获取所有权,共享所有权,拷贝赋值语意,计数器,自动增减等功能。 还缺少: 转移所有权reset的操作(我记得reset有三种重载方式); 获取原始指针方法,重写解引用*,重写指向操作符->,重写比较操作符,获取引用计数器值的方法,判断当前引用是否为1的方法,以及线程安全的设计。 最后,shared_ptr本身还需要解决“循环引用”,“自引用”的问题,一般需要搭配弱指针weak_ptr使用。 老师,不知我回答的是否全面,有无差错?
    展开

    作者回复: 很全。有几个小问题哈(像reset有几个重载不算😄): 获取原始指针的 get 方法我提供了; * 和 -> 代码里是提供的; 引用计数器值也是有的。 实践层面,最大的区别,除了不支持弱引用外,可能是不支持自定义删除器了。

    共 2 条评论
    5