C++基础:智能指针

为何需要智能指针

在C++的程序设计中,对堆内存的使用是非常频繁的,并且堆内存的申请和释放都是程序员自己管理的。但总体来说,堆内存的管理是麻烦的,使用智能指针可以更好的管理堆内存。
相对于原始指针来说,智能指针不需要手动释放内存,其内存会自动释放。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

示例

#include <memory>
#include <iostream>

class Person{
public:
	Person() {
		std::cout << "构造函数" << std::endl;
	}

	~Person() {
		std::cout << "析构函数" << std::endl;
	}
};

int main(){
	{
		std::shared_ptr<Person> p(new Person);
	}
	return 0;
}

这里在智能指针p离开{}的作用域时,会自动释放内存并调用其析构函数。

指针指针解决的问题

智能指针主要解决了以下两种问题:

  1. 内存泄漏:因为智能指针的内存会自动释放,合理的使用可以避免内存泄漏问题
  2. 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题。

智能指针的类型

C++有四种智能指针,分别是auto_ptr, unique_ptr, shared_ptr, weak_ptr,其中第一种智能指针已经被弃用,如果你不知道其用法也不必去主动了解,后三种均为C++11支持。

shared_ptr共享型智能指针

特性

std::shared_ptr使用引用计数来保证浅拷贝的内存释放问题,每一个shared_ptr的拷贝都会指向同一块内存,只有当最后一个shared_ptr拷贝析构的时候,才会将其指向的内存释放。

std::shared_ptr的内存模型

std::shared_ptr的组成主要可以看成两部分

  1. 一个指向堆上创建的对象的裸指针
  2. 该智能指针的引用计数
shared_ptr1
raw_ptr
usr_count = 1

而当其进行拷贝时std::shared_ptr shared_ptr2 = shared_ptr1结构会变成如下状态

shared_ptr1
raw_ptr
usr_count = 2
shared_ptr2

两个智能指针指向的是同一块堆内存,并且共享引用计数的内存,每发生一次拷贝,引用计数都会加1.
注意 :智能指针本身是在栈上分配的,其成员指针指向的是一块分配在堆区的内存所以当一个std::shared_ptr的拷贝析构时,会将usr_count的值减一,当引用计数为0时,会自动释放指向堆区的内存raw_ptr。

线程安全

shared_ptr在多线程环境下,其引用计数时线程安全的,不过其raw_ptr的修改并非线程安全。如果需要数据安全,需要使用锁机制。

初始化

shared_ptr可以通过以下几种方式初始化

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));
auto sp1 = make_shared<int>(100);

注意reset方法,如果ptr未初始化,调用reset可以为其初始化一个值。如果ptr有数据,reset会改变其指向,并减少原先智能指针的引用计数。
不能将一个原始指针直接赋值给智能指针

std::shared_ptr<int> p = new int(1);	//错误

常用方法

  1. reset : 重置shared_ptr
  2. get :返回shared_ptr内部的裸指针
  3. use_count:返回shared_ptr的引用计数
  4. unique:若use_count == 1则返回true,否则未false
#include <iostream>
#include <memory>


int main() {
	std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
	std::cout << ptr1.use_count() << ' ';
	auto ptr2 = ptr1;
	std::cout << ptr1.use_count() << ' ';
	std::cout << ptr2.use_count() << ' ';
	ptr2.reset(new int(5));
	std::cout << ptr1.use_count() << ' ';
	std::cout << ptr2.use_count() << ' ';

	auto ptr3 = ptr2;
	std::cout << ptr1.unique() << ' ' << ptr2.unique() <<" "<< ptr3.unique() << std::endl;

	int* p = new int(10);
	std::shared_ptr<int> ptr4(p);
	std::cout << (ptr4.get() == p) << std::endl;

	return 0;
}

//输出: 1 2 2 1 1 1 0 0 1

删除器

在创建智能指针时,可以给对象指定一个删除器,当智能指针的引用计数为零时,会自动调用该删除器来释放对象的内存,比如我们用std::shared_ptr来管理动态数组,std::shared_ptr的默认删除器不支持删除数组对象,我们就可以指定删除器。

std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});

需注意的问题

1.不要用同一个裸指针初始化多个智能指针

int* p = new int(10);
std::shared_ptr<int> ptr1(p);
std::shared_ptr<int> ptr2(p);	//错误

2.尽量不要在函数实参中创建智能指针

f(std::shared_ptr(new int(10)), g());

观察上面这个函数调用,函数f接受了两个实参,一个是智能指针,一个是函数g()的返回值。这里可能会存在一种风险,智能指针还没有创建,g()函数调用过程中出现了异常,这样可能会导致内存泄漏,建议采用以下写法。

shared_ptr<int> p(new int);
function(p, g());

3.不要将this指针作为shared_ptr返回出来

#include <iostream>
#include <memory>

class A {
public:
	std::shared_ptr<A> getSelf() {
		return std::shared_ptr<A>(this);
	}
};

int main() {

	std::shared_ptr<A> ptr1(new A);
	std::shared_ptr<A> ptr2 = ptr1->getSelf();
	return 0;
}

这里其实是相当于用this来初始化了两个智能指针ptr1和ptr2,会出现重复释放的问题。如果我们需要函数返回本对象的智能指针,应该让该类继承std::enable_shared_from_this类, 再调用基类成员函数shared_from_this来获取this的shared_ptr。 示例如下:

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A> {
public:
	std::shared_ptr<A> getSelf() {
		return shared_from_this();
	}
};

int main() {

	std::shared_ptr<A> ptr1(new A);
	std::shared_ptr<A> ptr2 = ptr1->getSelf();
	std::cout << ptr1.use_count() << ' ' << ptr2.use_count() << std::endl;
	return 0;
}
//输出:2 2

4. 避免循环引用。
循环引用可能会导致内存泄漏,如下:

#include <memory>
#include <iostream>

class B;

class A {
public:
	std::shared_ptr<B> aptr;
};

class B {
public:
	std::shared_ptr<A> aptr;
};

int main()
{
	std::shared_ptr<A> ptr1(new A);
	std::shared_ptr<B> ptr2(new B);
	ptr1->aptr = ptr2;	//ptr2被拷贝,引用计数加一
	ptr2->aptr = ptr1;	//ptr1被拷贝,引用计数加一
	return 0;
}

因为循环引用的原因,ptr1和ptr2的引用计数都为2,且进行析构后,引用计数也不会减到0,两个指针都不会释放内存,将会出现内存泄漏。

unique_ptr独占型智能指针

特性

std::unique_ptr是独占型的智能指针, 不允许其他的智能指针共享其内部的指针,即不允许通过赋值操作,将一个unique_ptr赋值给另一个unique_ptr。

unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制

虽然我们不同复制unique_ptr,但是可以通过std::move来转移unique_ptr指针的所有权。

unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确

unique_ptr可以管理数组

std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的

和shared_ptr的选择

如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

weak_ptr弱引用型智能指针

特性

std::weak_ptr与前两种智能指针不同, 是一种不控制对象生命周期的智能指针,它指向一个由std::shared_ptr管理的对象,其只提供了对管理对象的访问手段。weak_ptr只可以用shared_ptr或者另一个weak_ptr来构造,这两个智能指针之间也可以互相转化,并且它的构造和析构都不会改变引用计数,仅仅作为一个旁观者来监视shared_ptr中管理的资源是否存在, 同时其也没有重载操作符*和->.

weak_ptr解决的问题

weak_ptr的设计是为了配合shared_ptr工作,可以用于解决shared_ptr循环引用产生的内存泄漏问题。

基本用法

1.use_count()获取当前观察资源的引用计数
注意:weak_ptr的构造不会改变引用计数

#include <iostream>
#include <memory>

int main() {
	std::shared_ptr<int> ptr1(new int(10));
	std::weak_ptr<int> wp(ptr1);
	std::cout << wp.use_count() << ' ';
	std::shared_ptr<int> ptr2 = ptr1;
	std::cout << wp.use_count() << std::endl;
	return 0;
}
//输出: 1 2

2. expired()方法判断所观察资源是否已经释放

#include <iostream>
#include <memory>

int main() {
	std::weak_ptr<int> wp;
	{
		std::shared_ptr<int> ptr(new int(10));
		wp = ptr;
		std::cout << wp.expired() << " ";
	}
	std::cout << wp.expired() << std::endl;
	return 0;
}
//输出:0 1

3.lock()获取所监视的智能指针
注意:调用lock方法后,引用计数会加一

#include <iostream>
#include <memory>

std::weak_ptr<int> wp;

void f() {
	if (wp.expired()) {
		std::cout << "资源无效" << ' ';
	}
	else {
		std::cout << *wp.lock() << ' ';
	}
}

int main() {
	{
		std::shared_ptr<int> ptr(new int(10));
		wp = ptr;
		f();
	}
	f();


	return 0;
}

解决循环引用问题

因为weak_ptr并不会增加引用计数,只要将类A或B中的任一个shared_ptr成员改为weak_ptr成员就可以解决问题。

#include <memory>
#include <iostream>

class B;

class A {
public:
	std::weak_ptr<B> aptr;
};

class B {
public:
	std::shared_ptr<A> aptr;
};

int main()
{
	std::shared_ptr<A> ptr1(new A);
	std::shared_ptr<B> ptr2(new B);
	ptr1->aptr = ptr2;	//引用计数不变
	ptr2->aptr = ptr1;	//ptr1被拷贝,引用计数加一
	return 0;
}

在离开作用域之后,ptr2的引用计数为减为0,B指针会被析构,析构后其内部的aptr的引用计数会被减为1,然后在离开作用域后ptr1引用计数又从1减为0,A对象也被析构,不会发生内存泄漏。

  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值