开篇
C/C++开发过程中,动态内存的管理通过new/delete完成。new
在动态内存中为对象分配一块空间并返回一个指向该对象的指针;delete
指向一个动态独享的指针,销毁对象,并释放与之关联的内存。
在日常动态内存的使用中,经常会出现以下问题:
- 申请动态内存后忘记释放,造成内存泄漏,长时间运行会导致内存耗尽;
- 尚有指针引用动态内存的情况下就释放了它,造成引用非法内存指针,导致程序异常coredump。
为解决上述问题,C++11引入了智能指针的概念。
智能指针初识
智能指针就是RAII(资源获取即初始化)模板类,其将基本类型指针封装为(模板)类对象指针,在离开作用域时调用析构函数,delete指向的内存空间。C++11在头文件<memory>,提供了shared_ptr、unique_ptr、weak_ptr。
注 auto_ptr也是一种智能指针,不过已经被unique_ptr取代,本篇不讨论此指针。
shared_ptr
shared_ptr
采用引用计数的智能指针。如果需要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),可以使用该指针。 直至所有shared_ptr
所有者结束生命周期或放弃所有权,才会delete原始指针。
创建方式
- 构造空shared_ptr指针
std::shared_ptr<T> p1; //不传入任何实参
std::shared_ptr<T> p2(nullptr); //传入空指针 nullptr
空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。
- 明确指向的shared_ptr指针
std::shared_ptr<T> p3(new T()); // new方式
std::shared_ptr<T> p3 = std::make_shared<T>(); // make_shared方式
此两种方式创建的p3完全相同,《Effective Modren C++》
第21条款推荐优先使用make_shared
而非new
。
- 拷贝构造函数和移动构造函数
//调用拷贝构造函数
std::shared_ptr<T> p4(p3);//或者 std::shared_ptr<T> p4 = p3;
//调用移动构造函数
std::shared_ptr<T> p5(std::move(p4)); //或者 std::shared_ptr<T> p5 = std::move(p4);
p3 和 p4 都是shared_ptr
类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。
而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得 p5 拥有了 p4 的堆内存,而 p4 则变成了空智能指针。
- 自定义所指堆内存的释放规则
shared_ptr
支持自定义释放规则,即在智能指针生命结束时调用自定义函数。
// 空智能指针p,在删除共享指针时调用删除函数d
shared_ptr<T> p(d);
// 非空智能指针p, 管理原始指针q,在删除共享指针时调用删除函数d
shared_ptr<T> p(q, d);
// E.g,可配合lambda表达式
shared_ptr<FILE> fp(fopen("./tmp.txt","r"), fclose);
在某些场景中,自定义释放规则很有必要。比如,对于申请的动态数组来说,shared_ptr
指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。
shared_ptr常用函数
成员函数名 | 功 能 |
---|---|
operator=() | 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。 |
operator*() | 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。 |
operator->() | 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。 |
swap() | 交换 2 个相同类型 shared_ptr 智能指针的内容。 |
reset() | 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。 |
get() | 获得 shared_ptr 对象内部包含的普通指针。 |
use_count() | 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。 |
unique() | 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。 |
operator bool() | 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。 |
使用示例
static void help_info()
{
LOG("usage: \n"
"a. Regular test.\n"
"b. Custom class test.\n"
"c. Custorm delete test.\n"
"q. exit.\n"
);
}
static void regular_use(shared_ptr<string> &str)
{
shared_ptr<string> pTmpStr(str);
LOG("pTmpStr: %s. memory count: %ld.\n", pTmpStr->c_str(), pTmpStr.use_count());
}
class CTestSharedPtr
{
public:
CTestSharedPtr(string desc) : mDescription(desc) {
LOG("Enter %s.\n", __FUNCTION__);
}
~CTestSharedPtr() {
LOG("Enter %s.\n", __FUNCTION__);
}
string GetDesc() {
return mDescription;
}
private:
string mDescription;
};
int main(int argc, char *argv[])
{
char a;
help_info();
do
{
scanf("%c", &a);
switch (a)
{
case 'a': // shared_ptr 标准类型
{
shared_ptr<string> pStr1(new string("hello world"));
LOG("pStr1: %s. memory count: %ld.\n", pStr1->c_str(), pStr1.use_count());
regular_use(pStr1);
shared_ptr<string> pStr2(pStr1);
LOG("pStr2: %s. memory count: %ld.\n", pStr2->c_str(), pStr2.use_count());
}
break;
case 'b': // shared_ptr 自定义类型
{
shared_ptr<CTestSharedPtr> pCTest1 = make_shared<CTestSharedPtr>("ClassTest");
LOG("pCTest1: %s. memory count: %ld.\n", pCTest1->GetDesc().c_str(), pCTest1.use_count());
shared_ptr<CTestSharedPtr> pCTest2(pCTest1);
LOG("pCTest2: %s. memory count: %ld.\n", pCTest2->GetDesc().c_str(), pCTest2.use_count());
}
break;
case 'c': // 自定义shared_ptr删除器
{
char *pArry = nullptr;
auto fStart = [](char *p) {
p = (char *)malloc(sizeof(char) * 6);
strncpy(p, "hello", 6);
cout << "Enter fStart(). malloc p:" << p << endl;
return p;
};
auto fStop = [](char *p) {
cout << "Enter fStop(). p:" << p;
free(p);
p = nullptr;
cout << " free." << endl;
};
// 自定义删除器,当pDTest1生命周期结束时,通过delete_test(pDTest1)释放内存,不再调用delete
shared_ptr<char> pDTest1(fStart(pArry), fStop);
LOG("pDTest1.use_count: %ld. %s\n", pDTest1.use_count(), pDTest1.get());
}
break;
default:
break;
}
} while(a != 'q');
return 0;
}
执行输出
$ ./exe
usage:
a. Regular test.
b. Custom class test.
c. Custorm delete test.
q. exit.
a
pStr1: hello world. memory count: 1.
pTmpStr: hello world. memory count: 2.
pStr2: hello world. memory count: 2.
b
Enter CTestSharedPtr.
pCTest1: ClassTest. memory count: 1.
pCTest2: ClassTest. memory count: 2.
Enter ~CTestSharedPtr.
c
Enter fStart(). malloc p:hello
pDTest1.use_count: 1. hello
Enter fStop(). p:hello free.
unique_ptr
只允许基础指针的一个所有者。可以移到新所有者,但不会复制或共享。替换已弃用的auto_ptr
。必要情况下,可以转化为shared_ptr
。
创建方式
- 创建空unique_ptr指针
std::unique_ptr<T> p1();
std::unique_ptr<T> p2(nullptr);
- 明确指向的unique_ptr指针
std::unique_ptr<T> p3(new T);
C++11 标准中并没有为unique_ptr
类型指针添加类似的模板函数。C++14提供了make_unique<T>()
模板函数用于初始化unique_ptr
指针。
- 移动构造函数
std::unique_ptr<T> p4(new T);
// std::unique_ptr<T> p5(p4);//编译错误,堆内存不共享
std::unique_ptr<T> p5(std::move(p4));//正确,调用移动构造函数
unique_ptr
指针不共享拥有的堆内存,因此C++11标准中的 unique_ptr
模板类没有提供拷贝构造函数,只提供了移动构造函数。
- 自定义所指堆内存的释放规则
// 空unique_ptr指针, 删除智能指针时,执行d而非delete
unique_ptr<T, D> u1(d);
// 非空unique_ptr指针, 管理指针p; 删除智能指针时,执行d而非delete
unique_ptr<T, D> u2(p, d);
unique_ptr常用函数
使用示例
static void help_info()
{
LOG("usage: \n"
"a. reset() test.\n"
"b. Custom class test.\n"
"c. Custorm delete test.\n"
);
}
class CTestUniquePtr
{
public:
CTestUniquePtr(string desc) : mDescription(desc) {
LOG("Enter %s.\n", __FUNCTION__);
}
~CTestUniquePtr() {
LOG("Enter %s.\n", __FUNCTION__);
}
string GetDesc() {
return mDescription;
}
private:
string mDescription;
};
int main(int argc, char *argv[])
{
char a;
help_info();
do {
scanf("%c", &a);
switch(a)
{
case 'a':
{
unique_ptr<CTestUniquePtr> pUnPtr1(new CTestUniquePtr("unique_ptr"));
pUnPtr1.reset();
LOG("pUnPtr1 is %d.\n", pUnPtr1 ? 1 : 0);
}
break;
case 'b':
{
unique_ptr<CTestUniquePtr> pUnPtr1(new CTestUniquePtr("unique_ptr"));
unique_ptr<CTestUniquePtr> pUnPtr2(pUnPtr1.release());
LOG("pUnPtr1 is %s. pUnPtr2 is %s.\n", pUnPtr1 ? pUnPtr2->GetDesc().c_str() : "nullptr",
pUnPtr2 ? pUnPtr2->GetDesc().c_str() : "nullptr");
}
break;
case 'c':
{
std::unique_ptr< int, function<void(int*)> > ptr1(new int[100],
[](int*p)->void {
cout << "Delete int[]" << endl;
delete []p;
}
);
std::unique_ptr< FILE, function<void(FILE*)> > ptr2(fopen("data.txt","w"),
[](FILE*p)->void {
cout << "Delet FILE" << endl;
fclose(p);
}
);
}
break;
default:
break;
}
} while (a != 'q');
return 0;
}
执行输出
$./exe
usage:
a. reset() test.
b. Custom class test.
c. Custorm delete test.
a
Enter CTestUniquePtr.
Enter ~CTestUniquePtr.
pUnPtr1 is 0.
b
Enter CTestUniquePtr.
pUnPtr1 is nullptr. pUnPtr2 is unique_ptr.
Enter ~CTestUniquePtr.
c
Delet FILE
Delete int[]
weak_ptr
结合shared_ptr
使用的弱智能指针。weak_ptr
提供对一个或多个shared_ptr
实例拥有的对象的访问,但不参与引用计数。 如果需要观察某个对象但不需要其保持活动状态,可使用该实例。可解决shared_ptr
实例间的循环引用导致的内存泄漏问题。
weak_ptr
没有提供常用的指针操作,无法直接访问内存,需要先通过lock方法提升为shared_ptr
强智能指针,才能访问资源。
创建方式
- 创建空weak_ptr指针
std::weak_ptr<T> wp1;
- 拷贝构造函数
std::weak_ptr<T> wp2(wp1);
- 通过
shared_ptr
构建weak_ptr
auto = make_shared<T>();
std::weak_ptr<T> wp2(sp); // 不增加sp内部引用计数
weak_ptr常用函数
成员函数名 | 功 能 |
---|---|
operator=() | 重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。 |
swap(x) | 其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。 |
reset() | 将当前 weak_ptr 指针置为空指针。 |
use_count() | 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。 |
expired() | 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。 |
lock() | 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。 |
使用示例
{
shared_ptr<string> pShPtr1 = make_shared<string>("ptr"); // 创建共享指针, 内存引用 +1
LOG("1. pShPtr1.use_count: %ld.\n", pShPtr1.use_count());
shared_ptr<string> pShPtr2(pShPtr1); // 共享指针拷贝,内存引用 +1
LOG("2. pShPtr1.use_count: %ld.\n", pShPtr1.use_count());
weak_ptr<string> pWeakPtr(pShPtr1); // 弱引用智能指针,内存引用不增加
LOG("3. pWeakPtr.use_count: %ld.\n", pWeakPtr.use_count());
shared_ptr<string> pShPtr3(pWeakPtr.lock()); // 拷贝弱指针返回的共享指针,内存引用 +1
LOG("4. pShPtr3.use_count: %ld.\n", pShPtr3.use_count());
}
执行输出
1. pShPtr1.use_count: 1.
2. pShPtr1.use_count: 2.
3. pWeakPtr.use_count: 2.
4. pShPtr3.use_count: 3.
总结
通过本篇对三种指针的介绍,大致梳理出三者的使用场景:
独占内存用unique_ptr
;
内存被多个指针引用shared_ptr
;
当作为内存观察者或者解决循环引用时使用weak_ptr
。C++智能指针的使用注意事项:
① 优先使用unique_ptr
而非auto_ptr
。
②shared_ptr
不支持动态数组,若默认使用delete来释放管理资源,delete只会调用第一个元素的析构函数,导致内存泄漏;(可通过自定义删除器管理)unique_ptr
支持动态数组,默认detele会自动使用delete[]。
③使用unique_ptr
可转化为shared_ptr
,反之则不行。
④shared_ptr
消耗资源比unique_ptr
大,若无内存共享需求,优先考虑unique_ptr
。
⑤ 禁止使用静态分配对象指针初始化智能指针,否则智能指针生命周期结束时,会试图删除指向非动态分配对象的指针,导致未定义的行为。
⑥ 谨慎使用智能指针的get()
与release()
方法。
当使用get()
方法返回裸指针时,智能指针并没有释放指向对象的所有权,因此避免裸指针的使用导致崩溃。
unique_ptr.release()
返回裸指针并让出内存控制权,需要及时接管或者delete,避免导致内存泄漏。
⑦ 禁止使用一个裸指针初始化多个智能指针;禁止手动delete智能指针的裸指针。
⑧ 不要把类对象指针(this)作为shared_ptr
返回,改用enable_shared_from_this
。
⑨ 通过weak_ptr.lock()
方法获取shared_ptr
时,必须判断该shared_ptr
是否有效。
⑩ 优先考虑使用std::make_unique
和std::make_shared
而非new
(C++11暂未提供std::make_shared
)。
⑪shared_ptr
没有保证共享对象的线程安全性。
⑫ 循环引用shared_ptr
会导致内存泄漏,应替换为weak_ptr
。