一、什么是智能指针
智能指针是C++11标准引入的一种高级指针类型,用于管理动态分配的内存资源,旨在减少内存泄漏和悬挂指针等问题的风险。
在传统的指针使用中,我们使用new来动态分配内存,并使用指针管理地址:
{
int* p = new int(5); //动态分配一个int类型的内存
int* arr = new int[10]; //动态分配一个int型数组的内存
delete p;
delete[] arr;
}
但是这样分配内存,一旦程序在指针释放前发生异常或我们不小心忘了释放指针,或者使用了已经delete的指针,都会很容易造成程序崩溃。
所以引入智能指针,由C++标准库自动管理动态内存的生命周期,避免由于程序员对指针的管理疏忽造成的问题。
什么是内存泄漏?
内存泄漏是指程序在使用完动态分配内存(如通过new、malloc分配的内存)后,没有及时地释放,导致这些内存块在程序运行期间无法再被利用,最终程序会随着的运行占用越来越多的内存,甚至崩溃。
虽然这些内存会在程序退出后被操作系统回收,但是内存泄漏问题会造成系统资源利用率降低,影响操作系统或其他应用程序的运行,在高负载或长时间运行时,会导致程序性能下降、产生崩溃,十分影响程序稳定性。
例如下面代码就会造成内存泄漏
int func(){ int* p = new int(5); //动态分配一个int类型的内存 return *p; }
什么是悬挂指针、野指针?
指针悬挂是指 指针指向的内存已经被释放,但是指针仍然指向这块地址,这样的指针可以叫悬挂指针,也可以叫野指针。
野指针指的是指针变量不为nullptr,但是指向的地址是未被初始化或者已经被释放(不清楚是什么数据的一块内存)的指针。
对悬挂指针和野指针指向的内存进行的操作,都有可能造成篡改未知数据,可能产生数据损坏、程序崩溃或其他无法预测的错误。
void func(){ int* p = new int(5); delete p; // 释放p指向的内存 *p = 6; // 错误行为!p指向的内存已经被释放,p是悬挂指针,不可以再修改*p的数据! p = nullptr; // 正确行为!及时置空,避免指针悬挂 }
为了避免悬挂指针,当指针被释放后,要及时将该指针置为 nullptr,避免随后该指针被误用。
二、智能指针的使用
1、unique_ptr
unique_ptr是一种独占所有权的智能指针,表示一个指针只能被一个unique_ptr所有,不能被复制,只能被移动。
unique_ptr的创建
指向单个对象的unique_ptr
1)使用make_unique创建unique_ptr(见下面代码A)
2)使用new创建unique_ptr(见下面代码A)
3)使用unique_ptr管理已经存在的指针(见下面代码A)
代码A 使用unique_ptr
#include <iostream>
#include <algorithm>
using namespace std;
class A
{
public:
A(int i)
{
id = i;
cout << "A Constructor. id=" << id <<endl;
}
~A(){
cout << "A Destructor. id=" << id <<endl;
}
void show(){
cout << "This is A. id=" << id << endl;
}
private:
int id = -1;
};
void func()
{
//1) 使用make_unique创建unique_ptr
unique_ptr<A> p1 = std::make_unique<A>(1);
p1->show();
//2) 使用new创建unique_ptr -- 不推荐
unique_ptr<A> p2(new A(2));
p2->show();
//3) unique_ptr管理已经存在的指针
A *pa = new A(3); //new出一个新的对象
unique_ptr<A> p3(pa); //使用unique_ptr进行管理
//注意!这之后及时将pa置空,不要再使用
pa = nullptr;
p3->show();
}
int main(){
cout << "main begin..." << endl;
func();
cout << "main end..." << endl;
return 0;
}
输出:
从结果看,三个unique_ptr管理的对象,在超出unique_ptr作用于后都正确进行了释放。
如果,不使用只能指针呢?我们简单使用new来动态构建一个A的对象,并且退出函数不进行释放,看看会发生什么
代码B:不使用智能指针
在代码A的基础上增加funcc函数,并调用
void funcc()
{
A* p = new A(0);
p->show();
}
int main(){
cout << "main begin..." << endl;
funcc();
cout << "main end..." << endl;
return 0;
}
输出:
从输出看,退出funcc后,我们new出来的对象都没有被及时释放,而指针p是临时变量,已经丢失,这样就发生了内存泄漏。
调用new操作符时都发生了什么?
第一步:new会请求系统分配一块适当大小的内存(通常通过malloc或底层内存分配函数实现)。如果系统无法分配足够内存,new将抛出一个std::bad_alloc的异常,内存不会被分配;
第二步:如果内存分配成功,new会调用类构造函数初始化对象。如果在构造函数中发生了异常,则该异常被抛出,new将不会返回一个有效指针。但因为第一步已经分配了内存,所以将造成内存泄漏。
为什么不推荐使用2)使用new来创建unique_ptr?
使用unique_ptr<A> p2(new A(2));这种方式创建unique_ptr并不推荐,主要是因为它会导致一些内存管理和资源管理的复杂性,make_unique更加安全简洁,可读性更高。
make_unique具有异常安全的特性,若发生异常,make_unique会确保不会分配内存,不会造成内存泄漏;而调用new后,如果内存已经分配后发生异常,有可能导致内存泄漏。
我们将上述A类的构造函数中抛出一个异常,模拟动态分配内存时发生异常的情况
A(int i){ id = i; cout << "A Constructor. id=" << id <<endl; throw runtime_error("throw exeption..."); }
void func1() { unique_ptr<A> p1; //将p1声明在外部,使能在catch块能访问到 try { p1 = std::make_unique<A>(1); cout << "p1=" << p1.get() <<endl; //unique_ptr的get()函数可以获取其所管理的地址 //unique_ptr<A> p2(new A(2)); //cout << "p2=" << p2.get() <<endl; }catch (const exception& e) { cout << "p1=" << p1.get() <<endl; cout << e.what() << endl; } } int main(){ cout << "main begin..." << endl; func1(); cout << "main end..." << endl; return 0; }
输出:
从结果看,由于A的构造函数中会抛出异常,p1的指针是0,并没有建立成功。
指向数组的unique_ptr
unique_ptr支持指向数组类型,但是要注意使用数组类型的特化版本声明指针 unique_ptr<T[]> 而不是 unique_ptr<T>。这样在其销毁时调用的将是delete[],而不是delete。
示例代码:
void func()
{
//1) 使用make_unique创建unique_ptr
unique_ptr<int[]> p1 = std::make_unique<int[]>(5); //创建指向数据(具有5个元素)的智能指针
for(int i = 0; i < 5; ++i)
{
p1[i] = i; // 可以通过[]访问数组
}
//2) 使用new创建unique_ptr -- 同样不推荐
unique_ptr<int[]> p2(new int[5]);
cout << p1[3] <<endl;
}
unique_ptr 的拷贝和移动
上面我们就提到过,unique_ptr是一种独占所有权的指针。同一时刻,一个unique_ptr独占其指向的内存。其独占性体现在其不可以被复制,但可以被移动。
int main(){
unique_ptr<A> p1 = std::make_unique<A>(1);
p1->show();
cout << "p1 -> " << p1.get() << endl;
//unique_ptr<A> p2 = p1; //错误。无法编译通过,unique_ptr不可以被复制!
unique_ptr<A> p2 = std::move(p1); //正确。unique_ptr支持被移动,转移所有权
cout << "\n move after...\n";
p2->show();
cout << "p1 -> " << p1.get() << endl; //此时会发现p1的值已经为0
cout << "p2 -> " << p2.get() << endl;
return 0;
}
输出:
但是unique_ptr并不能完美控制它管理的内存块一定没有其他指针指向了。如下代码,编译并不会报错,也可以正常运行。但从输出结果可见这块内存被二次释放,这就会产生无法预测的问题,所以我们编写代码一定要小心!
int main(){
A* tmp = new A(0);
unique_ptr<A> p1(tmp);
unique_ptr<A> p2(tmp);
cout << "p1 -> " << p1.get() << endl;
cout << "p2 -> " << p2.get() << endl;
return 0;
}
输出:
unique_ptr获取原始底层指针
unique_ptr可以通过get()获取原始底层指针,即如果是unique_ptr<int>的智能指针,通过get可以获得其对应的 int* 的指针,并可以当成 int* 指针使用。
void show_a(A* ap)
{
ap->show();
}
int main(){
unique_ptr<A> p = make_unique<A>(6);
//show_a(p); //错误!类型不匹配
show_a(p.get()); //正确
cout << p.get() <<endl; //可以输出所指向的地址
return 0;
}
输出:
unique_ptr的释放
unique_ptr除了在生命周期结束前自动释放所指资源外,还能调用reset()函数进行主动释放
int main(){
unique_ptr<A> p = make_unique<A>(1);
cout << p.get() <<endl;
p.reset(); //主动释放资源
cout << "p reseted!!!" <<endl;
cout << p.get() <<endl;
return 0;
}
unique_ptr释放所有权
如果一个资源已经交给unique_ptr管理,想要释放该内存的管理权,我们可以使用release释放指针的所有权。
int main(){
unique_ptr<A> p = make_unique<A>(1);
cout << p.get() <<endl;
auto p1 = p.release(); //释放unique_ptr的所有权
cout << endl << "after release..." << endl;
cout << "p->" << p.get() <<endl;
cout << "p1->" << p1 <<endl;
delete p1; //由于是普通指针,注意使用完释放内存
return 0;
}
unique_ptr自定义删除器
std::unique_ptr可以使用自定义删除器,以支持不同的资源释放策略。
通俗的说,有的资源只让unique_ptr自动调用delete或者delete[] 无法完全释放。这时,我们可以自定义释放策略,传给unique_ptr,让其释放资源的时候自动执行。
unique_ptr支持传入函数指针和函数对象作为删除器。注意,make_unique不支持自定义删除器的使用,所以
1)函数指针作为删除器
//定义删除器函数
void func_deleter(A* a)
{
cout << "func_deleter run..." << endl;
delete a;
}
int main(){
unique_ptr<A, void(*)(A*)> p(new A(6), func_deleter);
p->show();
cout << "main finished" << endl;
return 0;
}
输出
2)函数对象作为删除器
我们还可以使用重载了()的函数对象作为删除器,可以用来构造更加复杂的释放操作。
//定义删除器类
struct ClassDeleter
{
void operator()(A* a) const{
cout << "ClassDeleter run..." << endl;
delete a;
}
};
int main(){
unique_ptr<A, ClassDeleter> p(new A(6), ClassDeleter());
p->show();
cout << "main finished" << endl;
return 0;
}
输出:
2、shared_ptr
shared_ptr和引用计数
shared_ptr是提供了共享所有权的共享指针。也就是说,和unique_ptr不同的是,shared_ptr可以进行复制,并且会自动管理计数,并在最后一个共享同一对象的指针被销毁后才对所指对象进行释放。
shared_ptr是基于引用计数管理共享指针。引用计数初始为0,每当有一个新的shared_ptr实例指向同一资源时,引用计数就会+1,当有一个shared_ptr被销毁时,引用计数-1.当引用计数为0时,资源将被释放。
shared_ptr类会为每组指向相同内存的shared_ptr维护一个“控制块”对象,用于存储引用计数、原始指针等数据,每个shared_ptr实例都维护一个指向“控制块”的指针。
shared_ptr的使用
shared_ptr的使用大部分可以参考unique_ptr,只是由于shared_ptr是可共享的,所以会在相关使用上有所不同。
下面代码简单写下shared_ptr的创建、销毁以及引用计数的打印
使用shared_ptr类的额use_count()函数,可以获取当前指针的引用计数数值
int main(){
shared_ptr<A> p1 = make_shared<A>(1);
cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
shared_ptr<A> p2 = p1; // p2由p1复制而来,指向相同资源
shared_ptr<A> p3 = make_shared<A>(2);
cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
cout << "p3 -> " << p3.get() << " use count=" << p3.use_count() <<endl;
cout << endl << "when reset p1..." << endl;
p1.reset(); // 销毁p1,p1指针失效。但是p1之前指向的资源由于仍被p2引用,所以还未释放
cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
return 0;
}
输出:
从输出可以看引用计数的变化情况
需要注意的是,shared_ptr对引用计数的增加,限制在发生了拷贝或赋值。并不能完全将指向同一资源的指针都归为一组。例如下列代码,p1,p2虽然指向同一资源,但是创建p2后并没有计数+1,而是分在了不同“控制块”。我们写代码时要注意
int main(){
A* tmp = new A(0);
shared_ptr<A> p1(tmp);
cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
shared_ptr<A> p2(tmp);
cout << endl;
cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
shared_ptr<A> p3 = p2;
cout << endl;
cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
cout << "p3 -> " << p3.get() << " use count=" << p3.use_count() <<endl;
return 0;
}
输出:
其他shared_ptr的使用方法可以参考unique_ptr,这里不多做介绍。想了解更多的小伙伴可以参考官方文档。
shared_ptr线程安全
shared_ptr的引用计数操作是原子操作,所以可以安全地在多线程环境下使用。
#include <iostream>
#include <algorithm>
#include <thread>
using namespace std;
void shared_thread_func(shared_ptr<A> p)
{
cout << "thread id = " << this_thread::get_id() << endl;
cout << "p->" << p.get() << " use_count=" << p.use_count() <<endl;
this_thread::sleep_for(chrono::seconds(1));
}
int main(){
shared_ptr<A> p1 = make_shared<A>(6);
thread th1(shared_thread_func, p1);
thread th2(shared_thread_func, p1);
th1.join();
th2.join();
return 0;
}
输出:
从结果看,指针参数传入shared_thread_func函数时,进行了复制,所以引用计数为3,但是形参的销毁并没有导致资源被错误释放。程序运行完成后,正确释放了资源。
shared_ptr循环引用问题
循环引用是指两个或多个对象批次持有对方的引用的现象。这会导致它们的引用计数无法降为0,无法释放资源,引起内存泄漏。
如下列代码:
当B的实例想要释放时,首先要释放C的引用,但是在释放C的资源时,需要首先释放B的对象。这样循环引用,导致谁也无法释放资源、完成析构。
class C;
class B
{
public:
B(){
cout << "B Constructor." <<endl;
}
~B(){
cout << "B Destructor." << endl;
}
std::shared_ptr<C> c_ptr; //声明一个指向C对象的指针
};
class C
{
public:
C(){
cout << "C Constructor." <<endl;
}
~C(){
cout << "C Destructor." << endl;
}
std::shared_ptr<B> b_ptr; //声明一个指向B对象的指针
};
int main(){
shared_ptr<B> b_p = make_shared<B>();
shared_ptr<C> c_p = make_shared<C>();
cout << "b_p->" << b_p.get() << " count=" << b_p.use_count() << endl;
cout << "c_p->" << c_p.get() << " count=" << c_p.use_count() << endl;
b_p->c_ptr = c_p;
c_p->b_ptr = b_p;
cout << "b_p->" << b_p.get() << " count=" << b_p.use_count() << endl;
cout << "c_p->" << c_p.get() << " count=" << c_p.use_count() << endl;
return 0;
}
输出可见,最后B、C的对象没有被释放
避免循环引用问题,除了更加合理地设计程序外,还可以使用weak_ptr来解决。
3、weak_ptr
weak_ptr主要为了解决shared_ptr的循环引用问题引入。weak_ptr允许持有一个对象的引用,但是不会增加该对象的引用计数。所以weak_ptr不负责控制对象的生命周期。
weak_ptr解决引用计数问题
将上述类C的指针改为weak_ptr,则可以解决循环引用。
class C
{
public:
C(){
cout << "C Constructor." <<endl;
}
~C(){
cout << "C Destructor." << endl;
}
std::weak_ptr<B> b_ptr; //声明一个指向B对象的weak_ptr指针
};
输出
weak_ptr的使用
上面提到了,weak_ptr并不负责管理所指对象的生命周期。可想而知,直接使用weak_ptr获取指向的对象是危险的。
1)构造weak_ptr指针
如下所示,weak_ptr主要使用shared_ptr对象构建,指向该shared_ptr的对象
int main(){
shared_ptr<A> p_shared = make_shared<A>(1);
// 1)从shared_ptr对象创建
weak_ptr<A> p_weak1(p_shared);
weak_ptr<A> p_weak2 = p_shared;
// 2)默认构造
weak_ptr<A> p_weak3; //weak_ptr具有默认构造方式,不指向任何对象
return 0;
}
2)获取weak_ptr所指对象
从上面有提到,weak_ptr不进行对象生命周期的管理。所以直接使用weak_ptr是不安全的,所以一般使用weak_ptr的lock()函数获取一个对应的shared_ptr指针,由shared_ptr指针进行对资源的访问。
lock的声明如下
shared_ptr<T> lock() const noexcept;
如下代码
int main(){
shared_ptr<A> p_shared = make_shared<A>(1);
cout << p_shared.get() << " " << p_shared.use_count() << endl;
weak_ptr<A> p_weak1(p_shared);
cout << "\nweak_ptr lock\n";
if(auto p_s = p_weak1.lock()) //调用lock()
{
p_s->show();
cout << p_s.get() << " " << p_s.use_count() << endl;
}
return 0;
}
输出
从输出看,调用lock()后,返回了一个shared_ptr的指针p_s,并且增加了资源的引用计数。这样,在p_s的生命周期内,其所指向的资源由于一直在被p_s引用,所以不会被释放。这样,p_s对资源的使用就是安全的。
另外,当在调用lock()之前,weak_ptr观察的资源已经被释放,那么lock()将返回空的shared_ptr对象。
int main(){
shared_ptr<A> p_shared = make_shared<A>(1);
cout << p_shared.get() << " " << p_shared.use_count() << endl;
weak_ptr<A> p_weak1(p_shared);
p_shared.reset(); //释放资源
cout << "\nweak_ptr lock\n";
if(auto p_s = p_weak1.lock()) //调用lock()
{
p_s->show();
cout << p_s.get() << " " << p_s.use_count() << endl;
}
else
{
cout << "p_s is " << p_s << endl;
}
return 0;
}
输出
3)判断weak_ptr指向资源是否有效
上一小节,我们知道可以通过lock()的返回值来判断weak_ptr指向资源是否还有效。这一小节,介绍另外一个函数expired。
expired()函数的声明如下,用于判断所属权是否已经过期,如果过期,则返回true,否则返回false.
bool expired() const noexcept;
使用如下
int main(){
shared_ptr<A> p_shared = make_shared<A>(1);
cout << p_shared.get() << " " << p_shared.use_count() << endl;
weak_ptr<A> p_weak1(p_shared);
cout << "p_weak1.expired = " << p_weak1.expired() << endl;
p_shared.reset(); //释放资源
cout << "after reset...\n";
cout << "p_weak1.expired = " << p_weak1.expired() << endl;
return 0;
}
输出
4)其他使用
weak_ptr也支持使用use_count获取所指对象的引用计数;
支持使用reset()对资源进行释放,但要注意的是,调用weak_ptr不会使引用计数-1,而是直接归0,也就是说资源将会直接被释放;
但是不支持get()函数直接获取对象地址。
int main(){
shared_ptr<A> p_shared = make_shared<A>(1);
shared_ptr<A> p_s2 = p_shared;
cout << p_shared.get() << " " << p_shared.use_count() << endl;
weak_ptr<A> p_weak1(p_shared);
cout << "p_weak1.use_count = " << p_weak1.use_count() << endl;
p_weak1.reset(); //释放资源
cout << "after reset...\n";
cout << "p_weak1.use_count = " << p_weak1.use_count() << endl;
return 0;
}
输出
【参考资料】
unique_ptr 类 | Microsoft Learn