C++ 智能指针和使用陷阱

引用:windows程序员面试指南

背景

人为的new和delete操作动态内存,容易出现两种问题:
1.忘记释放内存,会造成内存泄漏;
2.尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

一.auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。
当对象过期时,其析构函数将使用delete 来释放内存!
使用方法:
1.两个指针不能指向同一个资源:复制或赋值都会改变资源的所有权
2.重载了 * 和 -> ,像普通指针一样操作
3.get() // 智能指针托管的指针地址

auto_ptr<Test> test(new Test);
Test *tmp = test.get();		// 获取指针返回
// get() 实现
_NODISCARD _Ty * get() const noexcept
{	// return wrapped pointer
	return (_Myptr);
}

4.release() // 取消智能指针对动态内存的托管

auto_ptr<Test> test(new Test);
Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管
delete tmp2;	// 之前分配的内存需要自己手动释放
// release() 实现
_Ty * release() noexcept
{	// return wrapped pointer and give up ownership
	_Ty * _Tmp = _Myptr;
	_Myptr = nullptr;
	return (_Tmp);
}

5.reset() // 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构

auto_ptr<Test> test(new Test);
test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULL
test.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之
// reset() 实现
void reset(_Ty * _Ptr = nullptr)
{	// destroy designated object and store new pointer
	if (_Ptr != _Myptr)
		delete _Myptr;
	_Myptr = _Ptr;
}

弃用原因
1.复制或者赋值都会改变资源的所有权

// auto_ptr 被C++11抛弃的主要原因
auto_ptr<string> p1(new string("I'm Li Ming!"));
auto_ptr<string> p2(new string("I'm age 22."));
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;
// p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
// 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
p1 = p2;	
cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;   // p1 为 p2 的地址
cout << "p2:" << p2.get() << endl;   // p2 为 0x00000000

2.在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值

vector<auto_ptr<string>> vec;
auto_ptr<string> p3(new string("I'm P3"));
auto_ptr<string> p4(new string("I'm P4"));
// 必须使用std::move修饰成右值,才可以进行插入容器中
vec.push_back(std::move(p3));
vec.push_back(std::move(p4));
cout << "vec.at(0):" <<  *vec.at(0) << endl;
cout << "vec[1]:" <<  *vec[1] << endl;
// 风险来了:
vec[0] = vec[1];	// 如果进行赋值,问题又回到了上面一个问题中。
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;  // 被释放,访问越界

3.不支持对象数组的内存管理

auto_ptr<int[]> array(new int[5]);	// 不能这样定义

二.unique_ptr

c++11,替代了C++98的auto_ptr
使用方法
1.对象独有权:两个unique_ptr不能指向同一个对象
2.无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

unique_ptr<string> p1(new string("I'm Dangwei!"));
unique_ptr<string> p2(new string("I'm age 100."));
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;
p1 = p2;					// 禁止左值赋值
unique_ptr<string> p3(p2);	// 禁止左值赋值构造
unique_ptr<string> p3(std::move(p1));    // 使用移动语义
p1 = std::move(p2);	// 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样
cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

3.保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象
4.在容器中保存指针需要使用move,可读性高易理解

vector<unique_ptr<string>> vec;
unique_ptr<string> p3(new string("I'm P3"));
unique_ptr<string> p4(new string("I'm P4"));
vec.push_back(std::move(p3));
vec.push_back(std::move(p4));
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;
vec[0] = vec[1];	/* 不允许直接赋值 */
vec[0] = std::move(vec[1]);		// 需要使用move修饰,使得程序员知道后果
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;  // 非法访问

5.支持对象数组

// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]);	// 支持这样定义

6.get(), release(), reset() 和 auto_ptr 一直
使用陷阱
auto_ptr 与 unique_ptr智能指针的内存管理陷阱

auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str);	// p1托管str指针
{
	auto_ptr<string> p2;
	p2.reset(str);	// p2接管str指针时,会先取消p1的托管,然后再对str的托管
}

// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;
// 使用 shared_ptr 来解决该问题

三.shared_ptr

通过引用技术实现共享指针
构造

shared_ptr<int> up1(new int(10));  // int(10) 的引用计数为1
shared_ptr<int> up2(up1);  // 使用智能指针up1构造up2, 此时int(10) 引用计数为2

初始化

shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
shared_ptr<string> up4 = make_shared<string>("字符串");
shared_ptr<Person> up5 = make_shared<Person>(9);

赋值

shared_ptrr<int> up1(new int(10));  // int(10) 的引用计数为1
shared_ptr<int> up2(new int(11));   // int(11) 的引用计数为1
up1 = up2;	// int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2

主动释放对象

shared_ptrr<int> up1(new int(10));
up1 = nullptr ;	// int(10) 的引用计数减1,计数归零内存释放 
up1 = NULL; // 作用同上 

重置

p.reset() ; 将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器

交换

std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2);    // 交换p1 和p2 管理的对象,原对象的引用计数不变

使用陷阱:线程安全问题
shared_ptr 包含两个成员变量,一个是指向变量的指针,一个是资源被引用的次数,其中:
1.引用次数加减操作内部自动加锁解锁,是线程安全的
2.指向对象的指针不是线程安全的

智能指针的赋值拷贝,首先拷贝指向对象的指针,再使引用次数加减操作,虽然引用次数加减是原子操作,
但是指针拷贝和引用次数两步操作 并不是原子操作,线程不安全,需要手动加锁解锁

使用陷阱:循环引用
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!
譬如:Boy类中有Girl的智能指针;Girl类中有Boy的智能指针;当他们交叉互相持有对方的管理对象时…

class Girl;
class Boy {
public:
	Boy() {
		cout << "Boy 构造函数" << endl;
	}
	~Boy() {
		cout << "~Boy 析构函数" << endl;
	}
	void setGirlFriend(shared_ptr<Girl> _girlFriend) {
		this->girlFriend = _girlFriend;
	}
private:
	shared_ptr<Girl> girlFriend;
};

class Girl {
public:
	Girl() {
		cout << "Girl 构造函数" << endl;
	}
	~Girl() {
		cout << "~Girl 析构函数" << endl;
	}
	void setBoyFriend(shared_ptr<Boy> _boyFriend) {
		this->boyFriend = _boyFriend;
	}
private:
	shared_ptr<Boy> boyFriend;
};

void useTrap() {
	shared_ptr<Boy> spBoy(new Boy());
	shared_ptr<Girl> spGirl(new Girl());
	// 陷阱用法
	spBoy->setGirlFriend(spGirl);
	spGirl->setBoyFriend(spBoy);
	// 此时boy和girl的引用计数都是2
}

int main(void) {
	useTrap();
	system("pause");
	return 0;
}
// 当我们执行useTrap函数时,注意,是没有结束此函数,boy和girl指针其实是被两个智能指针托管的,所以他们的引用计数是2
// useTrap函数结束后,函数中定义的智能指针被清掉,boy和girl指针的引用计数减1,还剩下1,对象中的智能指针还是托管他们的
// 所以函数结束后没有将boy和gilr指针释放的原因就是于此
// 所以在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!

为了解决该交叉引用的问题,可以使用 weak_ptr

四.weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针,来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
1.weak_ptr 没有重载*和->,但可以使用 lock 获得一个可用的 shared_ptr 对象

shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
// 弱指针的使用
weak_ptr<Girl> wpGirl_1;			// 定义空的弱指针
weak_ptr<Girl> wpGirl_2(spGirl);	// 使用共享指针构造
wpGirl_1 = spGirl;					// 允许共享指针赋值给弱指针
cout << "spGirl \t use_count = " << spGirl.use_count() << endl;
cout << "wpGirl_1 \t use_count = " << wpGirl_1.use_count() << endl;

// 弱指针不支持 * 和 -> 对指针的访问
/*wpGirl_1->setBoyFriend(spBoy);
(*wpGirl_1).setBoyFriend(spBoy);*/

// 在必要的使用可以转换成共享指针
shared_ptr<Girl> sp_girl;
sp_girl = wpGirl_1.lock();

cout << sp_girl.use_count() << endl;
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;

继续刚才的例子,注意BOY类

class Girl;
class Boy {
public:
	Boy() {
		cout << "Boy 构造函数" << endl;
	}
	~Boy() {
		cout << "~Boy 析构函数" << endl;
	}
	void setGirlFriend(shared_ptr<Girl> _girlFriend) {
		this->girlFriend = _girlFriend;
		// 在必要的使用可以转换成共享指针
		shared_ptr<Girl> sp_girl;
		sp_girl = this->girlFriend.lock();
		cout << sp_girl.use_count() << endl;
		// 使用完之后,再将共享指针置NULL即可
		sp_girl = NULL;
	}
private:
	weak_ptr<Girl> girlFriend;
};

class Girl {
public:
	Girl() {
		cout << "Girl 构造函数" << endl;
	}
	~Girl() {
		cout << "~Girl 析构函数" << endl;
	}
	void setBoyFriend(shared_ptr<Boy> _boyFriend) {
		this->boyFriend = _boyFriend;
	}
private:
	shared_ptr<Boy> boyFriend;
};

void useTrap() {
	shared_ptr<Boy> spBoy(new Boy());
	shared_ptr<Girl> spGirl(new Girl());

	spBoy->setGirlFriend(spGirl);
	spGirl->setBoyFriend(spBoy);
}

int main(void) {
	useTrap();
	system("pause");
	return 0;
	// 在类中使用弱指针接管共享指针,在需要使用时就转换成共享指针去使用即可!
}

五.智能指针的使用陷阱

1.不要把一个原生指针给多个智能指针管理;

int *x = new int(10);
unique_ptr< int > up1(x);
unique_ptr< int > up2(x);
// 警告! 以上代码使up1 up2指向同一个内存,非常危险
或以下形式:
up1.reset(x);
up2.reset(x);

2.记得使用u.release()的返回值;
在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.
3.禁止delete 智能指针get 函数返回的指针;
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
4.禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!

shared_ptr< int > sp1(new int(10));
shared_ptr< int > sp4(sp1.get()); // 一个典型的错误用法

参考:https://zhuanlan.zhihu.com/p/526147194

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值