C++中的智能指针
文章目录
笔者在学习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_ptr
auto ptr = std::make_shared<int> (10);
std::cout << *ptr << std::endl; // cout 10
add_(ptr);
std::cout << *ptr << std::endl; // cout 11
return 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_ptr
auto ptr1 = std::make_shared<int> (10);
// Reference count add twice
auto ptr2 = ptr1;
auto ptr3 = ptr1;
// Check the value of ptr1,ptr2,ptr3
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() << ", `*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 ptr2
std::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 ptr3
std::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 0x573a3a29eec0
ptr1.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 0x573a3a29eec0
ptr1.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
ptr1.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_ptr
auto ptr1 = std::make_unique<Foo>(1);
if (ptr1 != nullptr) ptr1->printval();
{
auto ptr2 = std::move(ptr1); // move unique_ptr to ptr2
addone(*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 ptr1
if (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++)