C++中的智能指针
文章目录
- C++中的智能指针
- 1.为什么需要智能指针?
- 2.智能指针的类型
- 2.1 `std::shared_ptr`
- 2.2 `std::unique_ptr`
- 2.3 `std::weak_ptr`
- Reference
笔者在学习ROS2的过程中,遇到了std::make_shared
这种用法,一眼看不懂,才发现笔者对于Cpp 11/17/20
的一些新特性还不太了解,于是查找了许多资料写成此文,感谢诸君的分享。
1.为什么需要智能指针?
在C++中,智能指针是一种用于管理动态分配对象的内存工具,它们自动处理内存的分配和释放,并且提供方便的对象所有权管理。说到动态分配内存,很多读者肯定想到了new
和delete
这两个关键字,智能指针也是类似的内存管理的功能,只不过智能指针会更加安全。举个例子:
#include <iostream>class Foo{
public:Foo(int value) {this-> myval = value;std::cout << "This is construct function!" << std::endl;}~Foo() {std::cout << "This is destruct function!" << std::endl;}int getValue() {return this->myval;}private:int myval = 10;
};int main() {Foo *foo = new Foo(10);delete foo;return 0;
}
Output:
This is construct function!
This is destruct function!
我们正确使用new
和delete
的情况下,程序正常运行,但如果我们忘记使用delete
来恢复空间的时候,会产生如下的异常从而导致内存泄漏
int main() {Foo *foo = new Foo;return 0;
}
如图所示
使用智能指针可以在超过其作用域的时候,自动进行释放而无需手动delete
,提升了程序的安全性。举个例子:
#include <iostream>
#include <memory>class Foo{
private:int myval;public:Foo(int value) {this->myval = value;std::cout << "This is construct function!" << std::endl;}~Foo() {std::cout << "This is destruct function!" << std::endl;}int getValue() {return this->myval;}
};int main() {std::unique_ptr<Foo> foo_up(new Foo(42));std::cout << "Call of operator '->' that value of foo is " << foo_up->getValue() << std::endl;std::cout << "Call of operator '*' that value of foo is " << (*foo_up).getValue() << std::endl;return 0;
} // foo_up is deleted automatically here
Output:
This is construct function!
Call of operator '->' that value of foo is 10
Call of operator '*' that value of foo is 10
This is destruct function!
可见我们使用智能指针std::unique_ptr
可以有效地保证安全性,使用智能指针首先需要先包含头文件<memory>
。智能指针可以像普通指针一样使用指针运算符(->
和*
)访问封装指针,这是因为智能指针重载了这些运算符。
2.智能指针的类型
一共有四种类型的智能指针std::auto_ptr
、std::unique_ptr
、std::shared_ptr
、std::weak_ptr
,其中std::auto_ptr
是C++11标准推出的,在C++17已经被弃用了。我们重点关注std::unique_ptr
、std::shared_ptr
、std::weak_ptr
这三个智能指针,使用这三个智能指针需要包含头文件<memory>
。
在介绍智能指针之前,我们先了解一下引用计数,引用计数的基本思想是对于动态分配的对象,进行引用计数的时候,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次,每删除一次对同一个对象的引用,引用计数就会删除一次,当一个对象的引用计数为零的时候,就自动释放该对象存放的内存。
2.1 std::shared_ptr
std::shared_ptr
是一种智能指针,它能够记录有多少个shared_ptr
指向同一个对象,当引用计数为零的时候会将对象自动删除,这样就避免了显示地调用delete
。
引用计数可以帮助我们不显示调用delete
,但是我们还需要显示地调用new
进行创建,这是一种不对等的方式,所以在C++ 17
中提出了std::make_shared
方法来创建智能指针shared_ptr
,std::make_shared
会分配创建传入参数中的对象,并且返回这个对象类型的std::shared_ptr
,这样就避免了我们显示使用new
。但我们也可以显示地使用new
来构建一个shared_ptr
,如下:
auto ptr = std::make_shared<int> (10); // use 'std::make_shared' to construct a shared_ptr
std::shared_ptr<int> ptr (new int(10)); // use 'new' to construct a shared_ptr
举个基本的使用例子:
#include <iostream>
#include <memory>
void add_(std::shared_ptr<int> p) {(*p) ++;return;
}int main() {// Constructed a std::shared_ptr// std::shared_ptr<int> ptr (new int(10)); // we also can use 'new' to construct a shared_ptrauto ptr = std::make_shared<int> (10);std::cout << *ptr << std::endl; // cout 10add_(ptr);std::cout << *ptr << std::endl; // cout 11return 0;
} // The shared_ptr will be destructed here
Output:
10
11
std::shared_ptr
顾名思义是可以进行共享的,我们可以将其复制给别的对象,并且曾加一次对原对象的引用,可以使用get()
方法来返回一个原始指针,通过reset()
方法来减少一个引用计数,即抛弃当前的指针,通过use_count()
方法来查看一个对象的引用次数。举个例子:
#include <iostream>
#include <memory>
void add_(std::shared_ptr<int> p) {(*p) ++;return;
}int main() {// Constructed a std::shared_ptrauto ptr1 = std::make_shared<int> (10);// Reference count add twiceauto ptr2 = ptr1;auto ptr3 = ptr1;// Check the value of ptr1,ptr2,ptr3std::cout << "ptr1.use_count() is " << ptr1.use_count() << ", `*ptr1` is " << *ptr1 << ", address of ptr1 is " << ptr1 << std::endl;std::cout << "ptr2.use_count() is " << ptr2.use_count() << ", `*ptr2` is " << *ptr2 << ", address of ptr2 is " << ptr2 << std::endl;std::cout << "ptr3.use_count() is " << ptr3.use_count() << ", `*ptr3` is " << *ptr3 << ", address of ptr3 is " << ptr3 << std::endl;std::cout << std::endl;ptr2.reset();std::cout << "ptr1.use_count() is " << ptr1.use_count() << ", `*ptr1` is " << *ptr1 << ", address of ptr1 is " << ptr1 << std::endl;std::cout << "ptr2.use_count() is " << ptr2.use_count() << ", address of ptr2 is " << ptr2 << std::endl; // ptr2 reset, clear the address of ptr2std::cout << "ptr3.use_count() is " << ptr3.use_count() << ", `*ptr3` is " << *ptr3 << ", address of ptr3 is " << ptr3 << std::endl;std::cout << std::endl;ptr3.reset();std::cout << "ptr1.use_count() is " << ptr1.use_count() << ", `*ptr1` is " << *ptr1 << ", address of ptr1 is " << ptr1 << std::endl;std::cout << "ptr2.use_count() is " << ptr2.use_count() << ", address of ptr2 is " << ptr2 << std::endl;std::cout << "ptr3.use_count() is " << ptr3.use_count() << ", address of ptr3 is " << ptr3 << std::endl; // ptr3 reset, clear the address of ptr3std::cout << std::endl;auto original_ptr = ptr1.get();std::cout << "ptr1.use_count() is " << ptr1.use_count() << ", `*ptr1` is " << *ptr1 << ", address of ptr1 is " << ptr1 << std::endl;std::cout << "ptr2.use_count() is " << ptr2.use_count() << ", address of ptr2 is " << ptr2 << std::endl;std::cout << "ptr3.use_count() is " << ptr3.use_count() << ", address of ptr3 is " << ptr3 << std::endl; std::cout << "*original_ptr is " << *original_ptr << ", address of original_ptr is " << original_ptr <<std::endl;return 0;
} // The shared_ptr will be destructed here
Output:
ptr1.use_count() is 3, `*ptr1` is 10, address of ptr1 is 0x573a3a29eec0
ptr2.use_count() is 3, `*ptr2` is 10, address of ptr2 is 0x573a3a29eec0
ptr3.use_count() is 3, `*ptr3` is 10, address of ptr3 is 0x573a3a29eec0ptr1.use_count() is 2, `*ptr1` is 10, address of ptr1 is 0x573a3a29eec0
ptr2.use_count() is 0, address of ptr2 is 0
ptr3.use_count() is 2, `*ptr3` is 10, address of ptr3 is 0x573a3a29eec0ptr1.use_count() is 1, `*ptr1` is 10, address of ptr1 is 0x573a3a29eec0
ptr2.use_count() is 0, address of ptr2 is 0
ptr3.use_count() is 0, address of ptr3 is 0ptr1.use_count() is 1, `*ptr1` is 10, address of ptr1 is 0x573a3a29eec0
ptr2.use_count() is 0, address of ptr2 is 0
ptr3.use_count() is 0, address of ptr3 is 0
*original_ptr is 10, address of original_ptr is 0x573a3a29eec0
可以看出,使用get()
返回一个原始指针并不会减少引用计数。
2.2 std::unique_ptr
std::unique_ptr
是一种独占所有权的智能指针,它确保在任何时候都只有一个指针可以管理对象,它不进行共享,也无法复制到其他的unique_ptr
,无法通过值传递到函数,只能移动unique_ptr
。同样,我们也有两种方式可以来构建unique_ptr
std::unique_ptr<int> ptr = std::make_unique<int>(20); // use 'std::make_unique' to construct a unique_ptr
std::unique_ptr<int> ptr (new int(20)); // use 'new' to construct a unique_ptr
unique_ptr
同样提供了get()
方法用于获取原始指针,我们不能将unique_ptr
副值给别的变量,但是我们可以使用std::move()
方法将其转移给其他的unique_ptr
,举个例子:
#include <iostream>
#include <memory>class Foo {
public:Foo(int value) {this-> myval = value;std::cout << "Foo::Foo" << std::endl;}~Foo() {std::cout << "Foo::~Foo" << std::endl;}int myval;void printval() {std::cout << "Value is " << myval << std::endl;}
};void addone(Foo& foo) {foo.myval++;std::cout << "add 1" << std::endl;
}int main() {// Constructed a std::unique_ptrauto ptr1 = std::make_unique<Foo>(1);if (ptr1 != nullptr) ptr1->printval();{auto ptr2 = std::move(ptr1); // move unique_ptr to ptr2addone(*ptr2);if (ptr2 != nullptr) ptr2->printval();if (ptr1 != nullptr) ptr1->printval();else std::cout << "ptr1 is destoryed" << std::endl;ptr1 = std::move(ptr2); // move unique_ptr to ptr1if (ptr2 != nullptr) ptr2->printval();else std::cout << "ptr2 is destoryed" << std::endl;}return 0;
} // The unique_ptr will be destructed here
Output:
Foo::Foo
Value is 1
add 1
Value is 2
ptr1 is destoryed
ptr2 is destoryed
Foo::~Foo
2.3 std::weak_ptr
按理来说,使用shared_ptr
和unique_ptr
已经能够满足大多数场景的需求了,为什么还需要一个weak_ptr
呢,这是因为,shared_ptr
当存在交叉引用的时候,仍然会导致内存泄漏的问题,举个例子:
#include <iostream>
#include <memory>
class A;
class B;class A {
public:std::shared_ptr<B> pointer;A() {std::cout << "Construct A" << std::endl;}~A() {std::cout << "Destory A" << std::endl;}
};class B {
public:std::shared_ptr<A> pointer;B() {std::cout << "Construct B" << std::endl;}~B() {std::cout << "Destory B" << std::endl;}
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->pointer = b;b->pointer = a;return 0;
}
Output:
Construct A
Construct B
可以看到A和B并没有析构,说明这两块内存还没有得到释放,这是由于我们存在交叉引用,下面这张图很好的说明了这一点
这是因为a,b内部的pointer
同时又引用了a,b
,这使得a,b
的引用计数变为2了,离开作用域的时候,a,b
的智能指针被析构,只能让这块区域的引用计数-1
,这样就导致了a,b
的引用计数不为零,造成了内存泄漏。
解决这个问题的好方式是使用std::weak_ptr
,std::weak_ptr
是一种弱引用,这种弱引用不会引起引用计数的增加,当换作弱引用的时候,最终的释放流程如图所示
#include <iostream>
#include <memory>
class A;
class B;class A {
public:std::weak_ptr<B> pointer;A() {std::cout << "Construct A" << std::endl;}~A() {std::cout << "Destory A" << std::endl;}
};class B {
public:std::weak_ptr<A> pointer;B() {std::cout << "Construct B" << std::endl;}~B() {std::cout << "Destory B" << std::endl;}
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->pointer = b;b->pointer = a;return 0;
}
Output:
Construct A
Construct B
Destory B
Destory A
需要注意的是std::weak_ptr
只提供对一个或多个 shared_ptr
实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例,也就是说weak_ptr
不能使用->
和*
来访问实例对象的方法。
Reference
[1]智能指针(现代 C++)
[2]现代C++教程:高速上手C++ 11/14/17/20