一、智能指针概念
C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。简要的说,智能指针利用了 C++ 的 RAII 机制,在智能指针对象作用域结束后,会自动做内存释放的相关操作,不需要我们再手动去操作内存。
C++ 中有四种智能指针:
- auto_ptr:已经废弃
- unique_ptr:独占式指针,同一时刻只能有一个指针指向同一个对象
- shared_ptr:共享式指针,同一时刻可以有多个指针指向同一个对象
- weak_ptr:用来解决shared_ptr相互引用导致的死锁问题
二、auto_ptr
auto_ptr
是 C++98 中引入的智能指针,用于自动管理动态分配的对象的生命周期。然而,它在 C++11 中已被标记为已废弃,并且在 C++17 中已被移除,因为它存在一些严重的缺陷和安全问题。在此我们不做过多讲述,只针对其缺陷加以说明。
缺陷:
- 没有共享所有权:
auto_ptr
不能共享所有权,这意味着多个指针不能指向同一个对象- 不支持拷贝语义:
auto_ptr
在拷贝或者赋值时进行的是资源所有权转移,而并不是真正的拷贝和赋值
三、unique_ptr
1.unique_ptr的使用
用unique_ptr来替代new关键字创建出来的动态对象;
class Person
{
public:
Person(){cout << "构造函数" << endl;}
void Demo(){cout << "Person Demo" << endl;}
~Person(){cout << "析构函数" << endl;}
};
// 1. unique_ptr 创建方式
void test01()
{
//管理单一动态对象
unique_ptr<Person> up1(new Person);
up1->Demo();
//管理动态对象数组
unique_ptr<Person[]> up2(new Person[2]);
up2[0].Demo();
up2[1].Demo();
}
// 2. unique_ptr 操作函数
void test02()
{
unique_ptr<Person> up(new Person);
//get 成员函数返回 unique_ptr 管理的动态对象指针
Person* person1 = up.get();
//release 成员函数使 unique_ptr 不再持有动态对象指针(并不销毁管理的对象),并返回其管理的动态指针
Person* person2 = up.release();
delete person2;
//reset 成员函数有两个重载的版本,具体功能如下:
up.reset(); // 释放并销毁 unique_ptr 所管理的动态对象指针。
up.reset(new Person); // 释放并销毁原来的管理的动态对象,并重新持有新创建的动态对象
//swap 成员函数交换两个 unique_ptr 对象管理的动态对象
unique_ptr<Person> sp(new Person);
up.swap(sp);
}
2.unique_ptr的特性
unique_ptr 特点:
- 同时只能有一个 unique_ptr 对象来持有动态对象资源
- unique_ptr 对象不支持默认拷贝、默认赋值语义
- unique_ptr 对象支持移动拷贝、移动赋值语义
- unique_ptr 对象不能以值的方式用做函数参数,也不能存储在STL的容器中(容器要求元素必须能够被拷贝)
class Person
{
public:
Person(){cout << "构造函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
void test()
{
unique_ptr<Person> up1(new Person);
unique_ptr<Person> up2(new Person);
// 1. 禁止拷贝、赋值
// unique_ptr(const unique_ptr&) = delete;
// unique_ptr& operator=(const unique_ptr&) = delete;
// 2. 允许移动拷贝、赋值
unique_ptr<Person> up3(move(up1)); // 移动拷贝
up2 = move(up3); // 移动赋值
// unique_ptr 不允许作为容器元素
// vector<unique_ptr<Person>> vec;
// vec.push_back(up1);
}
3.unique_ptr自定义删除器
unique_ptr 可用于管理 new 出来的动态对象,也可以管理其他需要手动关闭的资源。例如:文件对象。
由于 unique_ptr 默认使用 delete、delete[] 来释放被管理的资源。所以,当管理的对象不能通过 delete、delete[] 来释放时,就需要自定义删除器。
class Person
{
public:
Person(){cout << "构造函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
struct Deleter
{
void operator()(FILE* fp)
{
cout << "文件被自动关闭" << endl;
if (fp != nullptr)
{
fclose(fp);
fp = nullptr;
}
}
};
void my_deleter(FILE* fp)
{
cout << "文件被自动关闭" << endl;
if (fp != nullptr)
{
fclose(fp);
fp = nullptr;
}
}
void test()
{
// 1. 函数对象作为删除器
// unique_ptr<FILE, Deleter> up(fopen("./demo.txt", "w"), Deleter());
// unique_ptr<FILE, function<void(FILE*)>> up(fopen("./demo.txt", "w"), Deleter());
// 2. 普通函数作为删除器
// unique_ptr<FILE, decltype(&my_deleter)> up(fopen("./demo.txt", "w"), my_deleter);
// unique_ptr<FILE, void(*)(FILE *)> up(fopen("./demo.txt", "w"), my_deleter);
// unique_ptr<FILE, function<void(FILE*)>> up(fopen("./demo.txt", "w"), my_deleter);
// 3. 匿名函数作为删除器
unique_ptr<FILE, function<void(FILE *)>> up(fopen("./demo.txt", "w"), [](FILE *fp) {
cout << "文件被自动关闭" << endl;
if (fp != nullptr)
{
fclose(fp);
fp = nullptr;
}
});
if (!up)
{
cout << "文件打开失败" << endl;
return;
}
fputs("hello world\n", up.get());
}
四、shared_ptr
C++ 的 shared_ptr
是 C++11 标准引入的智能指针之一,用于管理动态分配的对象的所有权。它允许多个 shared_ptr
实例共享对同一对象的所有权,而不会出现内存泄漏或者悬空指针的情况。shared_ptr
使用引用计数技术来跟踪有多少个 shared_ptr
实例指向同一个对象,并在最后一个实例销毁时自动释放对象。
1.share_ptr使用
- shared_ptr 的创建和使用方法。
- 支持管理动态对象和对象数组。
- 提供多种操作函数,如 get(), swap(), 和 reset()。
class Person
{
public:
Person(){cout << "无参构造函数" << endl;}
Person(int, int){cout << "有参构造函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
// 1. 创建 shared_ptr 对象
void test01()
{
// 1.1 使用 shared_ptr 的构造函数创建对象
shared_ptr<Person> sp1(new Person(10, 20));
// 1.2 使用 shared_ptr 管理动态对象数组
shared_ptr<Person[]> sp2(new Person[5]);
}
// 2. shared_ptr 操作函数
void test02()
{
shared_ptr<Person> sp1(new Person(10, 20));
shared_ptr<Person> sp2(new Person(100, 200));
// 2.1 get 成员函数可以获得 shared_prt 管理的动态对象指针
Person* person = sp1.get();
// 2.2 swap 成员函数可以交换两个 shared_ptr 管理的动态对象指针
sp1.swap(sp2);
// 2.3 reset 成员函数存在两个重载版本的函数,其作用分别如下:
sp1.reset(); // 释放其管理动态指针,此时 sp1 对象管理的动态指针指向为 nullptr
sp1.reset(new Person(1, 2)); // 释放原来的动态对象,并指向新的动态对象
}
2.share_ptr特性
- 允许多个 shared_ptr 对象同时管理同一动态对象。
- 支持对象拷贝和赋值,以及对象移动拷贝和赋值。
- 可以将 shared_ptr 存储在容器中,如 vector。
class Person
{
public:
Person(){cout << "无参构造函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
void test()
{
shared_ptr<Person> sp1(new Person);
// 1. 允许对象拷贝、对象赋值
shared_ptr<Person> sp2(sp1);
shared_ptr<Person> sp3;
sp3 = sp1; // 对象赋值
// 2. 允许对象移动拷贝、移动赋值
shared_ptr<Person> sp4 = move(sp3);
shared_ptr<Person> sp5;
sp5 = move(sp1); // 移动赋值
cout << "sp1:" << sp1.get() << endl; // 已被移动
cout << "sp2:" << sp2.get() << endl;
cout << "sp3:" << sp3.get() << endl; // 已被移动
cout << "sp4:" << sp4.get() << endl;
cout << "sp5:" << sp5.get() << endl;
// 允许存储到容器中
vector<shared_ptr<Person>> vec;
vec.push_back(sp2);
vec.push_back(sp4);
vec.push_back(sp5);
}
3.share_ptr引用计数
- shared_ptr 通过内部维护的引用计数来实现对象共享。
- 当 shared_ptr 的生命周期结束时,不会导致其他 shared_ptr 管理无效指针。
- 提供 use_count() 方法查看当前引用计数。
struct Person
{
Person(){cout << "Person 构造函数" << endl;}
~Person(){cout << "Person 析构函数" << endl;}
};
void test()
{
// 初始化智能指针,引用计数为 0
shared_ptr<Person> sp1(new Person);
cout << "sp1:" << sp1.use_count() << endl;
// 发生拷贝,引用计数 +1
shared_ptr<Person> sp2(sp1);
cout << "sp2:" << sp2.use_count() << endl;
// 发生赋值,引用计数 + 1
shared_ptr<Person> sp3;
sp3 = sp2;
cout << "sp3:" << sp3.use_count() << endl;
// 判断是否独占资源
cout << "sp1 是否独占资源:" << sp1.unique() << endl;
// sp2 释放资源所有权,通过该对方访问的引用次数为 0
sp2.reset();
cout << "sp2:" << sp2.use_count() << endl;
// sp1 和 sp2 引用计数为 2
cout << "sp1:" << sp1.use_count() << endl;
cout << "sp3:" << sp3.use_count() << endl;
}
4.share_ptr自定义删除器
shared_ptr 和 unique_ptr 一样,并不仅仅管理 new 出来的动态对象,但是,智能指针默认使用 delete、delete[] 来释放被管理的对象。
此时,如果被管理的对象并不是 new、new[] 出来的,需要自定义删除器。其删除器可以是以下任何一种形式:
- 普通函数
- 函数对象
- lambda 匿名函数对象
void my_deleter(FILE* fp)
{
cout << "文件自动关闭" << endl;
fclose(fp);
fp = nullptr;
}
struct MyDeleter
{
void operator()(FILE* fp)
{
cout << "文件自动关闭" << endl;
fclose(fp);
fp = nullptr;
}
};
void test()
{
// 1. 使用普通函数作为自定义删除器
shared_ptr<FILE> sp1(fopen("./demo.txt", "w"), my_deleter);
// 2. 使用函数对象作为自定义删除器
shared_ptr<FILE> sp2(fopen("./demo.txt", "w"), MyDeleter());
// 3. 使用 lambda 匿名函数对象作为自定义删除器
shared_ptr<FILE> sp3(fopen("./demo.txt", "w"), [](FILE* fp) {
cout << "文件自动关闭" << endl;
fclose(fp);
fp = nullptr;
});
}
五、weak_ptr
std::weak_ptr
是 C++ 标准库中的一个智能指针类,用于解决 std::shared_ptr
可能引发的循环引用问题。循环引用可能导致内存泄漏,因为引用计数无法降为零,从而无法释放对象。
std::weak_ptr
是一种弱引用,它允许你观测由 std::shared_ptr
管理的对象,但不会增加对象的引用计数。换句话说,std::weak_ptr
不拥有所指向对象的所有权,因此不会影响对象的生命周期。当 std::shared_ptr
管理的对象被销毁后,对应的 std::weak_ptr
会自动失效,指向空值。
1.weak_ptr创建和使用
weak_ptr 是作为 shared_ptr 辅助角色存在,不会被直接用来管理动态对象。所以:
- 我们不会直接创建 weak_ptr 去管理动态对象
- weak_ptr 只能通过 shared_ptr 对象创建
- weak_ptr 引用 shared_ptr 对象时,并不会增加引用计数
- weak_ptr 不直接操作 shared_ptr 管理的对象,但允许间接操作 shared_ptr 管理的对象
class Person
{
public:
Person(int, int){cout << "构造函数" << endl;}
void show(){cout << "Person::show 函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
void test()
{
// weak_ptr 是对 shared_ptr 的辅助,其自身并不拥有资源所有权
shared_ptr<Person> sp1 = make_shared<Person>(10, 20);
// 通过拷贝 shared_ptr 对象创建 weak_ptr 对象
weak_ptr<Person> wp1(sp1);
// weak_ptr 使用时,不能直接访问对象成员
// 必须使用 lock 方法返回一个 shared_ptr 对象(会增加引用)
if (wp1.expired())
{
return;
}
auto sp2 = wp1.lock();
sp2->show();
// 可以将 shared_ptr 赋值给 weak_ptr 对象
weak_ptr<Person> wp2;
wp2 = sp1;
// weak_ptr 相当于一个不增加引用的 shared_ptr
cout << "sp1:" << sp1.use_count() << endl;
cout << "sp2:" << sp2.use_count() << endl;
cout << "wp1:" << wp1.use_count() << endl;
cout << "wp2:" << wp2.use_count() << endl;
}
2.shared_ptr 引用计数的缺陷
shared_ptr 使用引用计数来管理对象,但在双向链表的情况下,两个节点互相引用会导致循环引用,导致内存泄漏。
在这种情况下,两个对象的引用计数都保持在 2,导致它们的析构函数不会被调用。
例如:
// 双向链表节点 class LinkNode { public: LinkNode(int value) { cout << "LinkNode 构造函数" << endl; data = value; } ~LinkNode() { cout << "LinkNode 析构函数" << endl; } public: int data; shared_ptr<LinkNode> prev; shared_ptr<LinkNode> next; }; void test() { // 创建两个链表节点 shared_ptr<LinkNode> node1(new LinkNode(10)); shared_ptr<LinkNode> node2(new LinkNode(20)); // 建立节点之间的关系 node1->next = node2; node2->prev = node1; }
我们对链表节点使用 shared_ptr 进行了管理,防止出现忘记释放而导致的内存泄漏。但是,通过程序运行的结果看到,两个 LinkNode 只调用了构造函数,并没有调用析构函数,出现了内存泄漏产生。
这是由于什么原因导致的呢?
上图中, node1 对象内部引用 node2 对象,node2 对象内部引用 node1 这种现象,叫做循环引用。
- 由于循环引用,导致两个 LinkNode 关联的引用计数为 2
- 当 test 函数生命周期结束,node1 和 node2 的析构函数调用,两个 LinkNode 对象引用计数 -1,引用计数为 1
- 只有当 LinkNode 实际被销毁时,才会调用 prev 和 next 的析构函数。
第 2、3 点是一对矛盾的存在:
- 只有 prev 和 next 被销毁,LinkNode 才会被真正释放
- 只有两个 LinkNode 被释放,prev 和 next 才会被销毁
3.weak_ptr 使用示例
为了解决循环引用的问题,可以在节点之间使用 weak_ptr 替代 shared_ptr。这样可以打破循环引用,确保对象能够正确释放。
示例代码展示了如何使用 weak_ptr 来管理双向链表节点,从而避免内存泄漏。
// 双向链表节点
class LinkNode
{
public:
LinkNode(int value)
{
cout << "LinkNode 构造函数" << endl;
data = value;
}
~LinkNode(){cout << "LinkNode 析构函数" << endl;}
public:
int data;
// 使用 weak_ptr 代替 shared_ptr
weak_ptr<LinkNode> prev;
weak_ptr<LinkNode> next;
};
void test()
{
// 创建两个链表节点
shared_ptr<LinkNode> node1(new LinkNode(10));
shared_ptr<LinkNode> node2(new LinkNode(20));
// 建立节点之间的关系
node1->next = node2;
node2->prev = node1;
}
六、工厂函数
1.make_unique
std::make_unique
是一个 C++14 中引入的函数模板,用于创建std::unique_ptr
实例。它的目的是简化创建动态分配对象的过程,并且提供了更好的异常安全性。在使用std::make_unique
时,你只需提供要动态分配的对象的类型和构造函数参数,函数会返回一个包装了这个动态分配对象的std::unique_ptr
。
class Person
{
public:
Person(){cout << "构造函数" << endl;}
Person(int, int){cout << "构造函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
void test()
{
// 使用默认构造
unique_ptr<Person> up1 = make_unique<Person>();
// 使用有参构造
unique_ptr<Person> up2 = make_unique<Person>(10, 20);
// 创建对象数组
unique_ptr<Person[]> up3 = make_unique<Person[]>(3);
}
make_unique函数和unique_ptr构造函数创建智能对象时的区别是什么呢?
1. 从语法角度:make_unique 函数比 unique_ptr 语法更加简洁
2. 从安全角度:make_unique 是异常安全的,而 unique_ptr 构造函数创建方式则不是。异常安全性是指当函数在执行过程中发生异常时,程序依然能够保持数据结构的一致性和资源的正确释放。对于
make_unique
,它保证了在异常发生时内存会被正确释放,即使在内存分配后抛出异常也不会导致内存泄漏。
异常安全
#include <iostream>
#include <memory>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person 构造函数" << endl;
}
~Person()
{
cout << "Person 析构函数" << endl;
}
};
void do_logic(unique_ptr<Person> uq, int number) {}
int get_number()
{
cout << "get_number" << endl;
throw exception();
return 100;
}
int main()
{
try
{
// 1. 构造函数 异常不安全
do_logic(unique_ptr<Person>(new Person), get_number());
// 2. make_unique 异常安全
// do_logic(make_unique<Person>(), get_number());
}
catch (...)
{
cout << "错误处理..." << endl;
}
return 0;
}
我们运行这段代码时发现:
在vc++里输出结果为:
// unique_ptr Person 构造函数 get_number 错误处理... // make_unique get_number 错误处理...
对于上面的运行结果,显然发生了内存泄漏。这是因为 do_logic 函数在执行前,要先构造参数,而参数的顺序并没有规定,会导致不同的编译器不同的顺序。
2.make_shared
class Person
{
public:
Person(int, int) {cout << "无参构造函数" << endl;}
~Person(){cout << "析构函数" << endl;}
};
void test()
{
shared_ptr<Person> sp = make_shared<Person>(10, 20);
}
注意:make_shared 函数并不支持创建用于管理动态对象数组的 shared_ptr 对象,这个和 make_unique 是有区别的。
1.make_shared 函数和 shared_ptr 构造函数创建的 shared_ptr 对象有什么不同呢?
- make_shared 是异常安全的,而构造函数创建方式并不总是异常安全
- make_shared 创建 shared_ptr 对象的效率更高
我们着重了解一下为什么make_shared 创建 shared_ptr 对象的效率更高?
shared_ptr<Person> sp(new Person(10, 20));
当创建 sp 对象时,过程如下:
- 首先,在堆上创建 Person 动态对象,需要分配一次内存。
- 然后,在堆上创建引用计数对象,需要分配一次内存。
- 最后,在栈上创建 shared_ptr 对象。
从上面过程来看,很显然,我们需要为 Person 动态对象、引用计数对象动态分配两次内存。
而当使用 make_shared 函数时:
- 一次性分配 Person 动态对象和引用计数对象所需要的内存,并在该内存中构建 Person 对象和引用计数对象。
- 然后,在栈上创建 shared_ptr 对象。
从该过程中,我们发现 make_shared 函数会比 shared_ptr 构造函数创建方式减少一次动态内存的申请和释放,进而提升了 shared_ptr 的构建效率。