智能指针背景
智能指针是针对内存泄漏的问题进行处理。
下面是一个内存泄漏的场景:
场景1
场景2
我们写一个模拟除法函数,调用一下。该函数会对除数为0的情况抛异常:
#include<iostream>
using namespace std;
double chu(int a, int b)
{
if (b == 0)
{
throw invalid_argument("除数不能为0!");
}
else
{
return (double)a / b;
}
}
void funb()
{
int* p = new int[10];
int a = 0, b = 0; cin >> a >> b;
cout << chu(a, b) << endl;
delete[] p;
cout << "deletc[]" << p << endl;
}
int main()
{
try
{
funb();
}
catch (const exception &e)
{
cout << e.what() << endl;
}
return 0;
}
抛异常不调用析构:
正常情况会释放:
我们发现如果没有抛异常就程序最后释放(调用析构),如果抛异常了那么最后不会析构(不调用析构)。
那么如果遇到抛异常的情况,没有调用析构,最后可能会导致内存泄漏的问题。
这个时候就可以用聪明的指针来解决
智能指针一般都是一个模板类,如下:
创建一个SmartPtr.h文件:
SmartPtr.h
#pragma once
template<typename T>
class SmartPtr {
public:
SmartPtr(T* ptr) : _ptr(ptr) {};
~SmartPtr()
{
cout << "deleting SmartPtr" << endl;
delete _ptr;
}
private:
T* _ptr;
};
我们在主文件里声明一个SmartPtr类型的对象,把delete交给p1这个对象去管理,那么无论如何最后都会调用析构,进行释放。
#include<iostream>
using namespace std;
#include"smartptr.h"
int main()
{
SmartPtr<int> p1(new int);
return 0;
}
RAII
智能指针的思想就是RAII,也就是资源获取即初始化。
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
那么重载*,->的场景用于哪里呢?比如说我们就可以像正常访问指针一样去访问了:
test.cpp
SmartPtr<int> p1(new int(10));
cout << *p1 << endl;
其次我们也可以用于键值对的场景:
SmartPtr<pair<string, int>>p(new pair<string, int>("hello", 10));
p->first = "world";
p->second = 20;
cout<<(p->second+=1)<<endl;
cout << (p.operator->()->second += 1) <<endl;
系统提供的智能指针
auto_ptr
我们知道两个对象管理同一块地址会出问题,但是auto_ptr提供了管理权转移这么一个策略:
即:把管理权限给最后一个对象后,前面的对象置空,由最后一个对象管理权限。
如下所示就是p1把管理权限给了p2:
缺点:
前面的对象把权限给最后一个对象后,前面的对象都置空了。假设用户不懂的话对前面的对象进行操作,就会造成对空指针进行操作,就会段错误:
unique_ptr
既然拷贝有问题(管理权转移,导致前面的对象悬空),那我干脆不让你拷贝了。
因此引入第二个指针:unique_ptr。
怎样才能让不被拷贝呢?把拷贝构造和赋值重载函数给禁掉就行了(c++98是把这俩函数谁为私有,c++11是给这俩函数设delete)。
介绍
-
独占所有权:
unique_ptr
实现独占所有权语义,即在任何时刻,只能有一个unique_ptr
指向一个动态分配的资源对象。这意味着不能有两个unique_ptr
同时管理同一个资源。
-
不可复制:
unique_ptr
不支持拷贝操作,无法通过复制构造函数或赋值操作来复制unique_ptr
34。
-
移动语义:
- 虽然不支持拷贝,但
unique_ptr
支持移动操作。可以使用std::move
函数将一个unique_ptr
的所有权转移给另一个unique_ptr
25。
- 虽然不支持拷贝,但
-
自动释放资源:
- 当
unique_ptr
超出作用域或被销毁时,它所管理的资源会自动被释放,从而防止内存泄漏5。
- 当
-
自定义删除器:
unique_ptr
允许自定义删除器,可以在释放资源时执行特定操作12。
使用方法
#include <iostream>
#include <memory>
class Widget {
public:
Widget(int x, int y, int z) : m_x(x), m_y(y), m_z(z) {
std::cout << "Widget constructed\n";
}
void print() {
std::cout << m_x << "," << m_y << "," << m_z << std::endl;
}
~Widget() {
std::cout << "Widget destroyed\n";
}
private:
int m_x, m_y, m_z;
};
int main() {
{
std::unique_ptr<Widget> w1(new Widget(1, 2, 3));
w1->print();
auto w2 = std::move(w1);
if (w1 == nullptr) {
std::cout << "w1 is nullptr\n";
}
w2->print();
} // w2被销毁,释放Widget对象
std::cout << "End of main\n";
return 0;
}
在这个示例中,w1
最初管理一个Widget
对象,然后通过std::move
将所有权转移给w2
。当w2
超出作用域时,Widget
对象被自动销毁。
通过使用unique_ptr
,可以有效地管理动态分配的内存,避免内存泄漏和其他与内存管理相关的问题。
release()函数返回裸指针,释放所有权。
正常情况下裸指针不能和智能指针混合:
正确的应该是用智能指针接收智能指针。但是通过release()函数可以让裸指针接收unique_ptr的返回值,并且获得unique_ptr对象的资源:
shared_ptr(共享指针)
shared_ptr还可以像我们之前自己写的智能指针一样去使用:
shared_ptr<int> p1(new int(10));
cout << *p1 << endl;
它的特性如下:
shared_ptr<int> p1(new int(10));
shared_ptr<int> p2 = p1;
cout << *p1 << " " << *p2 << endl;
如上所示,是否会报错?p1会自动释放,那么p2就指向已经释放的地址了,变成野指针了。
但是实际上程序是正常运行的:
这是因为智能指针引入了"引用计数"的技术。就是统计有多少个指针指向了该对象。p1,p2都指向了该地址,那么计数器就是2,每delete一个指针,计数器就会减1,只有当计数器数值为0时才会释放这块地址。
use_count()函数可以显示当前计数器个数:
shared_ptr<int> p1(new int(10));
cout<<"计数:"<<p1.use_count() << endl;
shared_ptr<int> p2 = p1;
cout<< "计数:" << p2.use_count()<<endl;
cout << *p1 << " " << *p2 << endl;
通过rest()函数可以释放该指针:
shared_ptr<int> p1(new int(10));
cout<<"计数:"<<p1.use_count() << endl;
shared_ptr<int> p2 = p1;
p1.reset();
cout<< "计数:" << p2.use_count()<<endl;
cout << *p1 << " " << *p2 << endl;
p1已经释放了就不能再访问这块空间了,所以cout<<*p1<<" "<<*p2<<endl;就会造成段错误。
unique()函数用来检查当前指针是否为独占指针,也就是计数器是否为1.
shared_ptr<int> p1(new int(10));
cout<<"计数:"<<p1.use_count() << endl;
shared_ptr<int> p2 = p1;
p1.reset();
cout<< "计数:" << p2.use_count()<<endl;
if (p2.unique())cout << "p2是独占指针" << endl;