C++智能指针shared_ptr、unique_ptr以及weak_ptr

目录

shared_ptr类

shared_ptr和unique_ptr都支持的操作

shared_ptr独有的操作

make_shared函数

shared_ptr自动销毁所管理的对象

由普通指针管理的动态内存在被显式释放前一直会存在

shared_ptr和new结合使用

定义和改变shared_ptr的其他方法

不要混合使用普通指针和智能指针

多个独立的shared_ptr指向同一块内存,会发生内存泄漏问题:

值传递时,实参会被拷贝,会递增其引用计数,局部变量被释放后,计数减一

不要使用get初始化另一个智能指针或为智能指针赋值

其他shared_ptr操作

 unique_ptr类

unique_ptr操作:release和reset

unique_ptr作为函数参数和返回值

weak_ptr类

常用函数

shared_ptr循环引用问题


使用智能指针的目的:为了更容易更安全的使用动态内存

智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。
新标准库提供的两个智能指针shared_ptr和unique_ptr的区别在于管理底层指针的方式:

  •  shared_ptr允许多个指针指向同一个对象 
  •  unique_ptr则“独占”所指向的对象
  •  weak_ptr则是一种弱引用,指向shared_ptr所管理的对象

shared_ptr类

智能指针也是模板,所以在创建一个智能指针时,必须提供指针指向的类型:

shared_ptr p1;

shared_ptr > p2;
  • 默认初始化的智能指针中保存着一个空指针。
  • 使用方式与普通指针类似,解引用一个智能指针返回它指向的对象。
  • 如果在一个条件判断中使用智能指针,效果就是检测它是否为空

shared_ptr和unique_ptr都支持的操作

  • shared_ptr sp 、unique_ptr up   空智能指针,可以指向类型为T的对象
  • p  将p用作一个条件判断,若p指向一个对象,则为true
  • *p 解引用p,获得它指向的对象
  • p->mem 等价于(*p).men
  • p.get() 返回p中保存的指针,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
  • swap(p,q) 交换p和q中指针 ,p.swap(q)

shared_ptr独有的操作

  • make_shared (args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象。
  • shared_ptrp(q)  p是shared_ptr q的拷贝:此操作会递增q中的计数器。q中的指针必须能转换为T*
  • p=q  p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数。若p的引用计数变为0,则将其管理的原内存释放
  • p.unique() 若p.use_count()为1,返回true,否则返回false
  • p.use_count()  返回与p共享对象的智能指针数量:可能很慢,主要用于调试

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数 

	//指向一个值为666的int的shared_ptr
	shared_ptr p1 = make_shared(666);
	//指向一个值为“999999999”的string
	shared_ptr p2 = make_shared(10,'9');
	//指向一个值初始化的int,即值为0;
	shared_ptr p3 = make_shared();
	cout << *p1 << endl;//666
	cout << *p2 << endl;//9999999999
	cout << *p3 << endl;//0

类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如调用make_shared时传递的参数必须与string的某个构造函数相匹配。如果不传递任何参数,对象就会进行值初始化。

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。其析构函数会递减它所指向对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

void use1(T arg){

    shared_ptr p = factory(arg);
//使用p
}//p离开了作用域,它指向的内存会被自动释放掉

由于p是use1的局部变量,在use1结束时它将被销毁,当p被销毁时,将递减其引用计数并检查它是否为0。在此例中,p是唯一引用factory返回的内存的对象。

但是有其他shared_ptr也指向这块内存,她就不会被释放掉:

void use1(T arg){

    shared_ptr p = factory(arg);
//使用p
    return p;//当我们返回p时,引用计数进行了递增操作
}//p离开了作用域,它指向的内存不会被自动释放掉

由普通指针管理的动态内存在被显式释放前一直会存在

void use2(T arg){

    Foo* p = factory(arg);
//使用p,但不delete它
  
}//p离开了作用域,它指向的内存不会被自动释放掉

shared_ptr和new结合使用

接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须用直接初始化的方式来初始化一个智能指针:

shared_ptr p1 = new int(1024); //错误
shared_ptr p2(new int(1024)); //正确

shared_ptr clone(int p){

    return new int(p); //错误
}

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存。

定义和改变shared_ptr的其他方法

  • shared_ptr p(q)    p管理内置指针q所指向的对象;q必须是指向new分配的内存,且能够转换为T*类型
  • shared_ptr p(u)     p从unique_ptr u那里接管了对象的所有权:将u置为空
  • shared_ptr p(q,d)    p接管了内置指针q所指向的对象所有权,q必须能够转换为T*类型。p将使用可调用对象d来代替delete
  • p.reset() ,p.reset(q)   若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空

不要混合使用普通指针和智能指针

shared_ptr可以协助对象的析构,但这仅限于自身的拷贝之间。

推荐使用make_shared而不是new的原因:

再分配对象的同时就将shared_ptr与之绑定,从而避免无意中将同一块内存绑定到多个独立创建的shared_ptr上

多个独立的shared_ptr指向同一块内存,会发生内存泄漏问题:

	int * a = new int(999);
	shared_ptr p1(a);
	shared_ptr p2(a);
	cout << *p1 << endl;//999
	cout << p1.use_count() << endl;//1
	cout << *p2 << endl;//999
	cout << p2.use_count() << endl;//1
	//会发生内存泄漏,因为两个独立的shared_ptr同时指向了一个内存空间,该内存会被释放两次

正确做法:使用另一个智能指针初始化其他智能指针

	int * a = new int(999);
	shared_ptr p1(a);
	shared_ptr p2(p1);//使用p1初始化p2,两个指针的计数器都为2,安全
	cout << *p1 << endl;//999
	cout << p1.use_count() << endl;//2
	cout << *p2 << endl;//999
	cout << p2.use_count() << endl;//2

值传递时,实参会被拷贝,会递增其引用计数,局部变量被释放后,计数减一

void test(shared_ptr p) {
	++*p;
	cout <<"值传递时,引用计数加一:"<< p.use_count() << endl;//2
}
int main(void) {
	int * a = new int(999);
	shared_ptr p1(a);

	cout << *p1 << endl;//999
	cout << p1.use_count() << endl;//2
	
	test(p1);
	cout << *p1 << endl;//1000
	cout << p1.use_count() << endl;//1
	
	return 0;
}

当一个shared_ptr绑定到一个普通指针时,我们就将内存的管理交给了这个shared_ptr,就不应该再使用普通指针来访问shared_ptr所指向的内存。 

void test(shared_ptr p) {
	++*p;
	cout <<"引用计数为1:"<< p.use_count() << endl;//1
}
int main(void) {

	int *x(new int(1024));//x 是一个普通指针,不是智能指针
	//test(x); 错误,不能将int*转换为一个shared_ptr
	test(shared_ptr(x));//合法,但离开函数体后内存会被释放,里面的引用计数为1
	cout << *x << endl;//此时x是个悬空指针,其值未定义

	
	return 0;
}

不要使用get初始化另一个智能指针或为智能指针赋值

get()函数返回一个内置指针,指向智能指针管理的对象。使用场景:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete指针。

其他shared_ptr操作

使用reset来将一个新的指针赋予一个shared_ptr:

p = new int(1024); //错误:不能将一个指针直接赋值给shared_ptr
p.reset(new int(1024)); //正确:p指向一个新对象

与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。

智能指针使用总结:

  • 不能使用相同的内置指针初始化(或reset)多个智能指针(会出现多个独立且指向同一内存的指针)
  • 不delete get()返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • 如果使用get()返回的指针,在最后一个对应的智能指针销毁后,该指针无效
  • 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

 unique_ptr类

一个unique_ptr”拥有“它所指向的对象。与shared_ptr不同,某时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化的形式:

	unique_ptr p1(new string("hello MTK"));
	//unique_ptr p2(p1);//错误:unique_ptr不支持拷贝
	unique_ptr p3;
	//p3 = p1;//错误:unique_ptr不支持赋值

unique_ptr操作:release和reset

  • unique_ptr u1   空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;
  • unique_ptr u2 使用一个类型为D的可调用对象来释放它的指针
  • unique_ptr u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
  • u=nullptr  释放u指向的对象,将u置为空
  • u.release()  u放弃对指针的控制权,返回指针,并将u置为空
  • u.reset()  释放u指向的对象
  • u.reset(q) 如果提供了内置指针q,令u指向这个对象;否则将u置为空
  • u.reset(nullptr)

虽然我们不能拷贝或赋值unique_ptr,但可以通过release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr:

	unique_ptr p1(new string("hello MTK"));
	cout << *p1 << endl;
	//将所有权从p1转移给p2
	unique_ptr p2(p1.release());//release将p1置为空
	
	unique_ptr p3(new string("hello world"));
	//将所有权从p3转移给p2
	p2.reset(p3.release());//reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。

reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用“hello MTK”初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。

调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常用来初始化另一个智能指针或给另一个智能指针赋值。如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

p2.release(); //错误,p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); //正确,当时我们必须记得delete(p)

unique_ptr作为函数参数和返回值

不能拷贝unique_ptr的规则有个例外:我可以拷贝或赋值一个将要被销毁的unique_ptr,比如作为函数参数或返回值:

unique_ptr clone1(int p) {
	//正确:从int*创建一个unique_ptr
	return unique_ptr(new int(p));
}
unique_ptr clone2(int p) {
	//正确:从int*创建一个unique_ptr
	unique_ptr ret(new int(p));
	return ret;
}

weak_ptr类

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。它是一种弱引用。

常用函数

  • weak_ptr w  空weak_ptr可以指向类型为T的对象
  • weak_ptr w(sp)  与shared_ptr sp 指向相同对象的weak_ptr。T必须能转换为sp指向的类型
  • w  = p ,p可以是一个shared_ptr或者weak_ptr,赋值后w与p共享对象
  • w.reset()  将w置为空
  • w.use_count()  与w共享对象的的shared_ptr的数量
  • w.expired() 若w.use_count()为0,返回true,否则返回false
  • w.lock() 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared(42);

weak_ptr wp(p); //wp弱共享p;p的引用计数不会改变

本例中,wp和p指向相同的对象,由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。

	shared_ptr p1(new int(666));
	weak_ptr wp(p1);
	cout << wp.use_count() << endl;//1
	if (shared_ptr sp = wp.lock()) {
		(*sp)++;
		cout << sp.use_count() << endl;//2
		cout << p1.use_count() << endl;//2

	}

shared_ptr循环引用问题

class B;
class A
{
public:
	shared_ptr pb_;
	~A()
	{
		cout << "析构A" << endl;
	}
};
class B 
{
public:
	shared_ptr pa_;
	~B()
	{
		cout << "析构B" << endl;
	}
};
void testShared_ptr() {
	shared_ptr pb(new B);
	shared_ptr pa(new A);
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << "pb 的引用计数:"<< pb.use_count() << endl;
	cout << "pa 的引用计数:"<< pa.use_count() << endl;
}
int main(void) {
	//pb 的引用计数:2
	//pa 的引用计数:2
	// 且未调用析构函数,说明两块内存一直没被释放
	testShared_ptr();
    //跳出函数后,二者的引用计数减一,但还是1,不为0,所以内存得不到释放
}

如上程序,会造成shared_ptr间的循环引用问题,A类中有shared_ptr 指针,B类中有shared_ptr指针。现创建A类和B类对象,并使用shared_ptr,pb,pa指向它们。同时使A对象和B对象的成员变量--各自的shared_ptr指针pa_,pb_指向对方。这就造成了循环引用:

有两个内存块,但是有四个shared_ptr指针,两个shared_ptr指向同一块内存空间:

  • pa和pa_都指向A对象的内存空间
  • pb和pb_都指向B对象的内存空间

使得其该内存块的引用计数为2。当跳出函数时,局部变量pb,pa被销毁,所以引用计数减一,但引用计数仍为1,不为0,所以A对象和B对象的内存得不到释放(其析构函数没有被调用)。

解决办法:使用weak_ptr

class B;
class A
{
public:
	weak_ptr pb_;
	~A()
	{
		cout << "析构A" << endl;
	}
};
class B 
{
public:
	shared_ptr pa_;
	~B()
	{
		cout << "析构B" << endl;
	}
};
void testShared_ptr() {
	shared_ptr pb(new B);
	shared_ptr pa(new A);
	pb->pa_ = pa;
	pa->pb_ = pb;//shared_ptr可以直接给weak_ptr赋值
	cout << "pb 的引用计数:"<< pb.use_count() << endl;
	cout << "pa 的引用计数:"<< pa.use_count() << endl;
}
int main(void) {
	//  pb 的引用计数:1
	//	pa 的引用计数:2
	//	析构B
	//	析构A
	testShared_ptr();
}

 运行过程解析:

  1. 使用weak_ptr指向shared_ptr指向的内存,不会使shared_ptr内的引用计数加一(弱引用),所以指向B对象内存的引用计数为1,指向A对象的引用计数仍为2.
  2. 跳出函数后,局部变量pb和pa被析构,其指向的内存引用计数都要减一,所以指向B对象的引用计数为0,指向A对象的引用计数为1。此时将析构B对象,所以先调用了B的析构函数。
  3. B对象被析构,其内部的pa_成员也将被析构,导致指向A对象的引用计数减一,为0。
  4. A对象的引用计数为0,析构A对象。所以最后调用A的析构函数。

参考书籍:

《C++ Primer 第五版》 

你可能感兴趣的:(CPP,指针,指针,内存管理,c++)