1 | 智能指针:auto_ptr,unique_ptr,shared_ptr和weak_ptr功能特性与应用
RALL特性的使用(resource acquisition is initialization,资源获取既初始化),即把资源的生命周期与对象的生命周期绑定在一起,(使用构造和析构函数来控制资源的分配)
背景:在利用堆内存存储数据的时候,需要手动显式地用new delete为资源进行内存的分配和释放。这种情况下,因为代码逻辑复杂,所以会出现忘记释放内存(内存泄漏)、重复释放内存(导致程序奔溃)、多个指针指向同一块内存空间导致该内存所有权不清晰,不知道何时释放的问题。因此提出“资源获取即初始化”的概念,设计出智能指针。智能指针能够实现自动释放、异常安全(异常情况依旧安全释放内存)、所有权明确**。
指针封装代替裸指针
#include<memory> //头文件
0、auto_ptr(现已弃用,会莫名其妙把空间交出去 C++98)
vector<auto_ptr<People>> peoples;
auto_ptr<People> one = peoples[5];
cout<<peoples[5]->get_name()<<endl;
这里会发生错误:auto_ptr支持operator=,为了使空间所有者唯一,所有权也随 = 交出去了,原指针变成null。
void doSomeThing(auto_ptr<People> peoples){}
……
auto_ptr<People> people(new People"joy");
doSomeThing(people);
cout<<people->get_name()<<endl;
同样发生错误:auto_ptr支持拷贝构造,但拷贝指针的同时也会把所有权移交。
所以,拷贝和赋值是使用指针非常频繁的场景,但是对于auto_ptr来说,就意味着非主动的所有权移交,使用场景严重脱离实际,所以被弃用。
1、unique_ptr 唯一所属权的智能指针
唯一所属权的智能指针,能够有效避免auto_ptr的无知觉交出所有权的问题:只能通过主动书写std::move实现,将所有权转化外显出来,只要写了就意味着自己是清楚的。即,不支持拷贝,但支持移动。
不足:特点唯一所属权导致:使用麻烦,只适合在单例模式使用。
适用场景:
1、RAII管理文件资源、数据库等;
2、工厂模式,支持多态;
3、容器存储,容器销毁后自动释放。
unique_ptr<A> ap1(new A);
unique_ptr<A> ap2;
// ap2 = ap1; error:赋值重载操作符
// unique_ptr<A> ap2 = ap1; error: 拷贝构造
ap2 = move(ap1); // 右值引用(把左值转化成右值)
// 使用
// 1 创建
// new
unique_ptr<int> ptr1(new int(42));
// make_unique(C++14) 最佳,提供异常安全
unique_ptr<int> ptr2 = make_unique<int>(42);
// 默认构造 延迟初始化或条件性对象的创建
unique_ptr<int> ptr3;
// 从原始指针构造 用于接管已有指针,但要确保不会重复删除
int* raw_ptr = new int(42);
unique_ptr<int> ptr4(raw_ptr);
// 2 访问
unique_ptr<int> ptr1 = make_unique<int>(42);
// 1 解引用操作符
int value = *ptr; // 获取值42
cout << *ptr << endl;
// 2 ->
unique_ptr<People> people = make_unique<people>();
people->name = "Alice";
int* raw = ptr.get(); //获取原始指针,但不转移所有权
// 3 释放所有权 很容易引入内存泄漏
int* raw_ptr = ptr_release(); //ptr变为nulptr返回原始指针
// 注意:必须手动delete raw_ptr!
// 4 重置
ptr.reset(); // 删除当前对象,设为nullptr;
ptr.reset(wnew int(100)); // 删除当前对象,指向新对象
// 5 交换两个unique_ptr 避免临时对象
unique_ptr<int> ptr1 = make_unique<int>(1);
unique_ptr<int> ptr2 = make_unique<int>(2);
ptr1.swap(ptr2);
swap(ptr1. ptr2);
// 6 自定义删除器
// 在用到一些特殊库的时候(比如FILE) 关闭回收不只是delete内存就好了,所以unique_ptr能够支持自定义删除器,以更完整地遵循RAII,不需要再手动调用(fclose)
// 自定义删除器 - 函数对象
struct FileDeleter {
void operator()(FILE* f) {
if (f) {
std::fclose(f);
std::cout << "文件已关闭" << std::endl;
}
}
};
std::unique_ptr<FILE, FileDeleter> file_ptr(std::fopen("test.txt", "w"));
// <要管理的类型是文件传输句柄,删除的时候调用的自定义函数> 接管该删除任务的指针名称(条件:以写指令打开名为test.txt)
C++14的make_unique
2、shared_ptr
引用计数实现多指针共享内存,当该内存的引用计数为0的时候,堆内存才会被自动释放。
每一块被shared_ptr指向的内存块都会对应着一个控制块(Control Block)。这个控制块中会存储关于这个内存块的强引用计数和弱引用计数。shared_ptr创建之初就会被分配两个指针,一个指向实际共享的内存区(ptr),一个指向对应的控制块(rep)。千万不能用同一个原始指针重复创建相同指向的shared_ptr,否则同一个内存块将对应两个控制块,出现异常!
控制块和内存区域都是被创建在堆区域中的。它们之间的地址会有什么联系?
1、用make_shared创建,控制块和内存块同时被创建,并且所属的物理地址是相邻的。这可以提高性能,减少局部性。
2、直接使用new来构造,那么要分两次分别给内存块和控制块分配空间,并且可能是分开的地址。
// 1 创建
std::shared_ptr<int> p1; // 不传入任何实参
std::shared_ptr<int> p2(new int(2)); // 有两个int类型的空间
std::shared_ptr<int> p3 = std::make_shared<int>(2);
// 2 拷贝和移动
//调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
//调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);
// 3 自定义删除器
// 1定义一个普通的删除器函数
void myFileDeleter(FILE* file) {
if (file) {
std::fclose(file);
std::cout << "文件已关闭并释放\n";
}
}
// 使用函数指针作为删除器
std::shared_ptr<FILE> file_ptr(std::fopen("test.txt", "w"), myFileDeleter);
// 2使用 lambda 表达式作为删除器
std::shared_ptr<FILE> file_ptr(
std::fopen("test.txt", "w"),
[](FILE* file) {
if (file) {
std::fclose(file);
std::cout << "文件已关闭并释放\n";
}
}
);
常用场景
| 资源类型 | 默认释放的问题 | 自定义删除器的作用 | 示例 |
|---|---|---|---|
| 文件句柄 | delete无法关闭 |
调用 fclose, CloseHandle等函数 |
管理 FILE*, HANDLE |
| 网络套接字 | delete无法关闭 |
调用 closesocket, shutdown等函数 |
管理 SOCKET类型 |
| 数据库连接 | delete无法断开 |
调用特定的连接关闭 API | 归还连接到连接池 |
| C风格内存 | delete行为未定义 |
调用 free()或 delete[] |
释放 malloc, new[] |
| 特殊对象 | delete可能不合适 |
调用厂商提供的销毁函数 | 图形资源、第三方库对象 |
注意:自定义删除器的目的是替代,而不是追加!
循环引用:两个shared_ptr相互引用,引用计数永远无法变成0,出现异常。
3、weak_ptr 解决循环引用的问题
weak_ptr不直接管理对象的生命周期。它通常由一个shared_ptr创建,并且与这个shared_ptr共享一个控制块。不影响强引用计数use_count,弱引用计数weak_count+1。
当最后一个shared_ptr被释放,它所管理的对象会被立刻析构,但是对应的控制块并不会被立即释放。直到所有指向它的weak_ptr都被销毁,即weak_count记为0时,控制块的内存才会被彻底清理。
// 1 创建
std::weak_ptr<int> ptr1;
// 假设已经被某shared_ptr赋值过
// 2 观察 lock()函数,获取一个新建的有效的shared_ptr来安全地访问对象,访问是强引用计数+1(至少为1,所以一定是安全的);如果对象已经被释放,就返回一个空的shared_ptr.
if(auto sp = ptr1.lock()){
cout<<"值还存在,为"<< *sp <<endl;
}else{
cout<<"对象已经被释放了"<<endl;
}
// 3 expired() 观察是否已经被释放(既观察强引用计数是否为0),true表示已经失效,false表示还没有
// 但expired()之后,对象依然随时有可能被释放,所以还是用lock()更合适
// 4 use_count() 观察当前对象有一个shared_ptr,并返回(观察强引用计数值究竟是多少)
// 但是具体使用的时候也不一定准确,所以只用于调试
// 5 reset() 将当前的weak_count和对应的控制块取消连接,使该weak_ptr不再观察任何对象。
观察者模式
观察者模式存在于多线程环境中。观察者观测到某事件发生的时候,需要通知监听者进行事件处理的模式。
但是也由于处在多线程环境中,会存在多线程的安全问题(可以使用智能指针解决)。观察者观测到某事件发生的时候,其实并不能确定监听者是否存在,也许依旧存在,也许已经被析构。所以观察者在通知监听者之前,需要确定监听者的存在状态。假如监听者已经被析构,那么就不必再通知,同时进入监听者队列把对饮的对象删除;加入监听者依旧存在,那么就进行正常的通知。
观察者只需要知道被观测对象是否存在或其状态就好,无需拥有,这一点很适合用weak_ptr
同时weak_ptr更加安全,
// 存储监听者注册的感兴趣的事件
unordered_map<int, list<weak_ptr<Listener>>> listenerMap;
// 观察者观察到事件发生,转发到对该事件感兴趣的监听者
void dispatchMessage(int msgid) {
auto it = listenerMap.find(msgid);
if (it != listenerMap.end()) {
for (auto it1 = it->second.begin(); it1 != it->second.end(); ++it1) {
shared_ptr<Listener> ps = it1->lock(); // 智能指针的提升操作,用来判断监听者对象是否存活
if (ps != nullptr) { // 监听者对象如果存活,才通知处理事件
ps->handleMessage(msgid);
} else {
it1 = it->second.erase(it1); // 监听者对象已经析构,从map中删除这样的监听者对象
}
}
}
}
questions
1、为什么要用指针?
1、提高内存使用效率,实现高效的数据共享。指针的存在能让多段内容直接使用同一段内存空间
2、实现内存动态分配。malloc new等可以实现自由的手动获取、释放内存空间。
3、直接修改传参。由于直接给定同样地址的参数,各不同的函数都可以对同一个值进行修改和共享。
4、能够使用和编写更加复杂的数据结构。例如链表、图、树。
2、右值是什么?
右值就是为了对某一个参数赋值而产生的临时性存在的数值。临时性存在,它的生命周期仅存在于对应表达式运行这段时间之内。
右值临时存储在哪?
右值临时存储的位置并没有一个准确的定义。这取决于该右值本身的大小和CPU的优化策略:如果该右值是小型、简单的值(比如说简单数据类型),这是编译器优先选择的,那么它将会存储在CPU寄存器内;如果该右值更加简单,比如说只是一个常量,那么会直接变成CPU指令的一部分;如果该右值大型且复杂,或者说当前计算机寄存器剩余空间不足,那么它将被储存在栈内存中。
为什么要关心右值?
浅拷贝&深拷贝&引用拷贝?
3、什么会导致内存泄漏?如何检测?避免?
new和delete的具体用法?
什么是悬空指针?
堆内存和栈内存有什么区别?
RAII的典型运用?

浙公网安备 33010602011771号