C++ 智能指针的使用

从 C++ 11 开始引入了智能指针的概念,并使用了引用计数的想法,让程序员不再需要关心手动释放内存。使用智能指针需要包含头文件 <memory>

std::shared_ptr

shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。

复制与移动

从语义上来看,shared_ptr 是支持复制的。

1
2
3
4
5
6
auto w = std::make_shared<foobar>();
{
auto w2 = w;
cout << w.use_count() << endl; // 2
}
cout << w.use_count() << endl; // 1

shared_ptr 也支持移动。从语义上来看,移动指的是所有权的传递。

1
2
auto w = std::make_shared<foobar>();
auto w2 = std::move(w); // 此时 w 等于 nullptr,w2.use_count() 等于 1

性能

  1. 内存占用高
    shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。
  2. 原子操作性能低
    考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
  3. 使用移动优化性能
    shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。

shared_from_this()

在类的内部获取 shared_ptr 是在所难免的。

在日常 C++ 编程中,为了更好的管理资源,我们通常借助 shared_ptr 来达到对资源的自动管理。由于其原理是通过跟踪引用计数实现的,也就是说在使用了 shared_ptr 后就不能再使用裸指针 this。比如说在类的内部直接使用 std::shared_ptr<XX>(this),就会导致智能指针失效。

shared_from_this()enable_shared_from_this<T> 的成员函数,返回 shared_ptr<T>需要注意的是,这个函数仅在 shared_ptr<T> 的构造函数被调用之后才能使用。原因是 enable_shared_from_this::weak_ptr 并不在构造函数中设置,而是在 shared_ptr<T> 的构造函数中设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <memory>
#include <iostream>

struct Good: std::enable_shared_from_this<Good> // 注意:继承
{
std::shared_ptr<Good> getptr() {
return shared_from_this();
}
};

struct Bad
{
// 错误写法:用不安全的表达式试图获得 this 的 shared_ptr 对象
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}
~Bad() { std::cout << "Bad::~Bad() called\\n"; }
};

int main()
{
// 正确的示例:两个 shared_ptr 对象将会共享同一对象
std::shared_ptr<Good> gp1 = std::make_shared<Good>();
std::shared_ptr<Good> gp2 = gp1->getptr();
std::cout << "gp2.use_count() = " << gp2.use_count() << '\\n';

// 错误的使用示例:调用 shared_from_this 但其没有被 std::shared_ptr 占有
try {
Good not_so_good;
std::shared_ptr<Good> gp1 = not_so_good.getptr();
} catch(std::bad_weak_ptr& e) {
// C++17 前为未定义行为; C++17 起抛出 std::bad_weak_ptr 异常
std::cout << e.what() << '\\n';
}

// 错误的示例,每个 shared_ptr 都认为自己是对象仅有的所有者
std::shared_ptr<Bad> bp1 = std::make_shared<Bad>();
std::shared_ptr<Bad> bp2 = bp1->getptr();
std::cout << "bp2.use_count() = " << bp2.use_count() << '\\n';
} // UB : Bad 对象将会被删除两次
1
2
3
4
5
6
gp2.use_count() = 2
bad_weak_ptr
bp2.use_count() = 1
Bad::~Bad() called
Bad::~Bad() called
*** glibc detected *** ./test: double free or corruption

throwing an instance of 'std::bad_weak_ptr' 错误排查

  • 在构造函数中调用 shared_from_this() ,此时类的实例本身尚未构造成功,weak_ptr 也就尚未设置,所以程序抛出 bad_weak_ptr 异常。
  • 类的实例是在栈上构造,或直接使用 new 的裸指针。
  • 存在继承情况下确保 std::enable_shared_from_this<T> 声明为 public

std::unique_ptr

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:

1
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入,C++11 没有提供 `std::make_unique`

独占性

unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有,所以,unique_ptr 不支持复制赋值

1
2
auto w = std::make_unique<foobar>();
auto w2 = w; // 编译错误

unique_ptr 只支持移动,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。

1
2
auto w = std::make_unique<foobar>();
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr

释放对象

unique_ptr 被销毁时(如离开作用域),对象也就自动释放了,也可以通过其他方式下显示释放对象。

1
2
3
up = nullptr; // 置为空,释放 up 指向的对象
up.release(); // 放弃控制权,返回裸指针,并将 up 置为空
up.reset(); // 释放 up 指向的对象

release()reset() 的区别在于,前者会释放控制权,返回裸指针,你还可以继续使用,而后者直接释放了指向对象。

作为参数

因为不允许被复制,所以直接把 unique_ptr 作为参数就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<memory>

void test(std::unique_ptr<int> p)
{
*p = 10;
}

int main()
{
std::unique_ptr<int> up = std::make_unique<int>(42);
test(up); // 试图传入 unique_ptr,编译报错
std::cout << *up << std::endl;
return 0;
}

可以向函数中传递普通指针,使用 get 函数就可以获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<memory>

void test(int *p)
{
*p = 10;
}

int main()
{
std::unique_ptr<int> up = std::make_unique<int>(42);
test(up.get()); // 传入裸指针作为参数
std::cout << *up << std::endl; // 输出 10
return 0;
}

或者使用引用作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<memory>

void test(std::unique_ptr<int> &p)
{
*p = 10;
}

int main()
{
std::unique_ptr<int> up = std::make_unique<int>(42);
test(up);
std::cout << *up << std::endl; // 输出 10
return 0;
}

作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<memory>

std::unique_ptr<int> test(int i)
{
std::unique_ptr<int> p = std::make_unique<int>(i);
return p;
}

int main()
{
std::unique_ptr<int> up = test(10);
std::cout << *up << std::endl;
return 0;
}

性能

因为 C++ 的 zero cost abstraction 的特点,unique_ptr 在默认情况下和裸指针的大小是一样的,所以内存上没有任何的额外消耗,性能是最优的

std::shared_ptr 转换

可以把 std::unique_ptr 转换成 std::shared_ptr ,但是不能把 std::shared_ptr 转换成 std::unique_ptr

1
2
3
4
5
// 1
std::unique_ptr<std::string> unique = std::make_unique<std::string>("test");
std::shared_ptr<std::string> shared = std::move(unique);
// 2
std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");

std::weak_ptr

weak_ptr 是为了解决 shared_ptr 双向引用的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct A;
struct B;

struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A 被销毁" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B 被销毁" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}

运行结果是 A 和 B 都不会被销毁,这是因为 a 与 b 内部的指针同时又引用了 a 与 b,这使得 a 与 b 的引用计数均变为了 2,而离开作用域时,a 与 b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a 与 b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。

解决这个问题的办法就是使用弱引用指针 std::weak_ptrstd::weak_ptr 是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加。

std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true

lock()

为了解决循环引用的问题而引入 weak_ptr ,但 std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作。但如果想访问 weak_ptr所引用的堆内存实体,必须调用 lock() 方法转换成一个 shared_ptr 对象。

const 与智能指针

1
2
3
4
shared_ptr<T> p;             ---> T * p;                                    : nothing is const
const shared_ptr<T> p; ---> T * const p; : p is const
shared_ptr<const T> p; ---> const T * p; <=> T const * p; : *p is const
const shared_ptr<const T> p; ---> const T * const p; <=> T const * const p; : p and *p are const.

dynamic_pointer_cast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// static_pointer_cast example
#include <iostream>
#include <memory>

struct A {
static const char* static_type;
const char* dynamic_type;
A() { dynamic_type = static_type; }
};
struct B: A {
static const char* static_type;
B() { dynamic_type = static_type; }
};

const char* A::static_type = "class A";
const char* B::static_type = "class B";

int main () {
std::shared_ptr<A> foo;
std::shared_ptr<B> bar;

bar = std::make_shared<B>();

foo = std::dynamic_pointer_cast<A>(bar);

std::cout << "foo's static type: " << foo->static_type << '\n';
std::cout << "foo's dynamic type: " << foo->dynamic_type << '\n';
std::cout << "bar's static type: " << bar->static_type << '\n';
std::cout << "bar's dynamic type: " << bar->dynamic_type << '\n';

return 0;
}

在函数中按值返回智能指针

在函数中建议按值返回智能指针,理由如下:

  1. 智能指针是由移动语义提供支持:它们所持有的动态分配的资源是被移动,而不是被复制。
  2. 现代编译器使用返回值优化 (RVO) 技术。这能够检测到正在按值返回的对象,使用一种返回捷径来避免无用的拷贝,并且从 C++17 标准开始保证了这一点,所以即使是智能指针本身也会被优化掉。
  3. 通过引用返回 std::shared_ptr 不会正确增加引用计数,这会在错误时间带来删除某些内容的风险。

参考