C++智能指针

1.为什么要使用智能指针

以下例子中:
demo1:遇到new没有进行释放
demo2:在throw前没有进行释放,throw会直接出函数,导致后面的释放代码不会运行。

#include <iostream>
#include <string>
#include  <exception>

using namespace std;


void memory_leak_demo1() {
    string* str = new string("今天又敲了一天代码,太累了,回家休息了!!!");

    cout << *str << endl;
    return;

}


int memory_leak_demo2() {
    string* str = new string("这个世界到处是坑,所以异常处理要谨记在心!!!");
    /***********************************************
     * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
     * 的文件中读取某些数据,而文件此时不存在
     ************************************************/
    {
        throw exception("文件不存在");
    }
    cout << *str << endl;
    delete str;
    return 0;
}


int main()
{
    memory_leak_demo1();
    try {
        memory_leak_demo2();
    }
    catch (exception e) {
        cout<<"catch exception: "<<e.what()<<endl;
    }

    system("pause");
    return 0;
}

以上两种情况都会出现内存泄漏!

更好的解决方案: 把string 定义为auto 变量,在函数生命周期结束时释放!

void memory_leak_demo1() {
    string str("今天又敲了一天代码,太累了,回家休息了!!!");
    cout << str << endl;
    return;
}


int memory_leak_demo2() {
    string str("这个世界到处是坑,所以异常处理要谨记在心!!!");
    /***********************************************
     * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
     * 的文件中读取某些数据,而文件此时不存在
     ************************************************/
    {
        throw exception("文件不存在");
    }
    cout << str << endl;
    return 0;
}

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!
C++98 提供了 auto_ptr 模板的解决方案
C++11 增加unique_ptr、shared_ptr 和weak_ptr

2.auto_ptr 使用详解 (C++98)

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象当对象过期时(函数结束),其析构函数将使用delete 来释放内存!

用法:
头文件: #include < memory>
用 法: auto_ptr<类型> 变量名(new 类型)

总结:
创建对象的方法 如:

auto_ptr< string> str(new string(“很牛逼!”));
auto_ptr<vector< int>> ap(new vector< int>(10));
auto_ptr< Test> t(new Test());

访问对象的方法

  1. t->getDebug()
  2. (*t).getDebug()
  3. Test* tmp = t.get(); //注意get方法
    tmp->getDebug()

几种内置方法:

1.get方法,拿到对象指针
2.release方法,取消指针指针对动态内存的托管,之前分配的内存必须手动释放
3.reset方法,重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

#include <iostream>
#include <string>
#include  <exception>
#include <memory>

using namespace std;

//auto_ptr< Test> t(new Test());  //忠告1: 智能指针不要定义为全局变量

class Test
{
public:
    Test() {
        cout << "Test is construct" << endl;
        debug = 1; 
    }

    ~Test() { cout << "Test is destruct" << endl; }

    int getDebug() {
        return debug;
    }

private:
    int debug;
};

//用  法:    auto_ptr<类型> 变量名(new 类型)

void memory_leak_demo1() {
    auto_ptr< Test> t(new Test());

    //忠告3: 除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
    //auto_ptr< Test> t1;
    //t1 = t;

	//忠告2: 不要定义指向智能指针对象的指针变量
    //auto_ptr<Test>* tp = new auto_ptr<Test>(new Test()); 

    //在使用智能指针访问对象时,使用方式和普通指针一样
    //1
    cout<< "-> debug: "<<t->getDebug()<< endl;	// auto_ptr内部运算符重载->间接访问符
    //2
    cout << "* debug: " << (*t).getDebug() << endl;
	//3  通过get方法拿到Test指针
    //Test* tmp = t.get();
    //cout << "get debug: " << tmp->getDebug() << endl;

    //release方法取消指针指针对动态内存的托管,之前分配的内存必须手动释放
    //Test*  tmp = t.release();  
    //delete tmp; 

    //reset方法重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
    //t.reset();
    t.reset(new Test());

    return;

}


int memory_leak_demo2() {
    //Test* t = new Test();
    auto_ptr< Test> t(new Test());

    /***********************************************
     * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
     * 的文件中读取某些数据,而文件此时不存在
     ************************************************/
    {
        throw exception("文件不存在");
    }
    
    //delete t;
    return 0;
}


int main()
{
    memory_leak_demo1();

    /*try {
        memory_leak_demo2();
    }
    catch (exception e) {
        cout << "catch exception: " << e.what() << endl;
    }*/

    system("pause");
    return 0;
}

使用建议:

1.尽可能不要将auto_ptr 变量定义为全局变量或指针,因为没有意义。
2. 不要定义指向智能指针对象的指针变量,因为同样是newi出来的,不会在函数结束时析构掉。
2.除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
3.C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!

几大弊端:
auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。 auto_ptr 主要有几大问题:

  • 复制和赋值会改变资源的所有权,不符合人的直觉。
  • 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
  • 不支持对象数组的操作
  • auto_ptr 陷阱,不能把同一段内存交给多个auto_ptr 变量去管理
#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;

int main() {

	//弊端1. auto_ptr 被C++11 抛弃的主要理由 p1= p2 ,复制或赋值都会改变资源的所有权
	auto_ptr<string> p1(new string("I 'm p1."));
	auto_ptr<string> p2(new string("I 'm p2."));
	printf("p1: %p\n", p1.get());
	printf("p2: %p\n", p2.get());
	//p1会把管理的内存释放掉然后接收p2的管理权,p2会把自己管理的内存置空。
	p1 = p2;
	printf("after p1 = p2\n");
	printf("p1: %p\n", p1.get());
	printf("p2: %p\n", p2.get());

	//弊端2. 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
	vector<auto_ptr<string>> va;
	auto_ptr<string> p3(new string("I 'm p3."));
	auto_ptr<string> p4(new string("I 'm p4."));
	//现在p3p4都是左值,要右值化后才能放入容器中。
	va.push_back(std::move(p3));	
	va.push_back(std::move(p4));

	cout <<"va[0]: "<< *va[0] << endl;
	cout <<"va[1]: "<< *va[1] << endl;

	//风险来啦,和前面的第一种情况一样
	va[0] = va[1];
	cout << "va[0]: " << *va[0] << endl;
	cout << "va[1]: " << *va[1] << endl;

	//弊端3. 不支持对象数组的内存管理
	//auto_ptr<int[]> ai(new int[5]);  //不能这样定义

	//auto_ptr 陷阱,不能把同一段内存交给多个auto_ptr 变量去管理
	/*{
		auto_ptr<string> p2;

		string* str = new string("智能指针的内存管理陷阱");
		p2.reset(str);
		{
			auto_ptr<string> p1;
			p1.reset(str);
		}
		cout <<"str: " << *p2 << endl;	//越界
	}*/
	system("pause");
	return 0;
}

3.unique_ptr 使用详解 (C++11)

根据上述的弊端,C++11用更严谨的unique_ptr 取代了auto_ptr!

①unique_ptr特性

  • 基于排他所有权模式:两个指针不能指向同一个资源
  • 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值(用move函数),但是对于auto_ptr这里实质没有变,只是做了一个优化。
  • 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  • 在容器中保存指针是安全的
#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;

int main() {

	//auto_ptr 弊端1. 被C++11 抛弃的主要理由 p1= p2 ,复制或赋值都会改变资源的所有权
	//unique_ptr 如何解决这个问题? 不允许显示的右值赋值和构造
	unique_ptr<string> p1(new string("I 'm p1."));
	unique_ptr<string> p2(new string("I 'm p2."));
	printf("p1: %p\n", p1.get());
	printf("p2: %p\n", p2.get());

	//如果一定要转移,使用move把左值转成右值
	p1 = std::move(p2);
	printf("p1: %p\n", p1.get());
	printf("p2: %p\n", p2.get());

	//p1 = p2;  //左值赋值禁止
	unique_ptr<string> p3(new string("I 'm p3."));
	//左值拷贝构造也不行,必须转成右值
	unique_ptr<string> p4(std::move(p3));  

	//弊端2. 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
	vector<unique_ptr<string>> vu;
	unique_ptr<string> p5(new string("I 'm p5."));
	unique_ptr<string> p6(new string("I 'm p6."));

	vu.push_back(std::move(p3));
	vu.push_back(std::move(p4));

	cout << "va[0]: " << *vu[0] << endl;
	cout << "va[1]: " << *vu[1] << endl;
	//unique_ptr不支持直接赋值,没有风险
	//vu[0] = vu[1];  
	
	//弊端3. auto_ptr不支持对象数组的内存管理,unique_ptr 支持
	//auto_ptr<int[]> ai(new int[5]);  //不能这样定义
	unique_ptr<int[]> ui(new int[5]);  //自动会调用 delete []函数去释放
	system("pause");
	return 0;
}

②构造函数

unique_ptr<T> up ; 	//空的unique_ptr,可以指向类型为T的对象
unique_ptr<T> up1(new T()) ;//定义unique_ptr,同时指向类型为T的对象
unique_ptr<T[]> up ; //空的unique_ptr,可以指向类型为T[]的数组对象
unique_ptr<T[]> up1(new T[]) ;//定义unique_ptr,同时指向类型为T[]的数组对象
unique_ptr<T,D> up(); //空的unique_ptr,接受一个D类型的删除器d,使用d释放内存
unique_ptr<T,D> up(new T()); //定义unique_ptr,同时指向类型为T的对象,接受一个D							  类型的删除器d,使用删除器d来释放内存

删除器一般是自定义的仿函数,用来先执行一个想执行的函数后再释放,而不是直接释放。

③赋值

unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(new int(11));
up1 = std::move(up2);//必须使用移动语义,结果,up1原内存释放, up2 交由up1 管理

④主动释放对象

up = nullptr ;//释放up指向的对象,将up置为空
或 up = NULL; //作用相同
或利用reset

⑤放弃对象控制权

up.release(); //放弃对象的控制权,返回指针,将up置为空,不会释放内存

⑥重置

up.reset(…) ; //参数可以为 空、内置指针,先将up所指对象释放,然后重置up的值

⑦交换

up.swap(up1); //将智能指针up 和up1管控的对象进行交换

4.shared_ptr 使用详解 (C++11)

熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?

如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!
这就是 shared_ptr 采用的策略!

在这里插入图片描述

①构造函数

shared_ptr<T> sp ; //空的shared_ptr,可以指向类型为T的对象
shared_ptr<T> sp1(new T()) ;//定义shared_ptr,同时指向类型为T的对象
shared_ptr<T[]> sp2 ; //空的shared_ptr,可以指向类型为T[的数组对象 C++17后支持
shared_ptr<T[]> sp3(new T[]{...}) ;//指向类型为T的数组对象 C++17后支持
shared_ptr<T> sp4(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D
释放内存
shared_ptr<T> sp5(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D							     类型的删除器,使用D删除器来释放内存

②初始化

方式一 构造函数

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

方式二 使用make_shared 初始化对象,分配内存效率更高
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;
用法:
make_shared<类型>(构造类型对象需要的参数列表);

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

注意:推荐使用make_shared

③赋值

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; //作用同上 

⑤重置

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

⑥交换

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

⑦使用陷阱

shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!

定义一个boy和girl类,其中各有set_girl_friend/set_boy_friend的成员函数,传入girl/boy类的共享指针,并且数据成员是共享指针。然后创建男生女生的共享指针对象,并把对方设为自己的伴侣。结果会发现,两者都不会被析构。
原因:
sp_girl管控了new girl(),当男生设定女生为伴侣时,其内部的girl_friend也会管控new girl(),同理boy。这样它们的计数都是2。当函数结束时,只释放sp_girl和sp_boy的管控,导致计数还剩1,无法释放。

#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>

using namespace std;

class girl;

class boy {
public:
    boy() {
        cout << "boy construct!" << endl;
    }

    ~boy() {
        cout << "boy destruct!" << endl;
    }

    void set_girl_friend(shared_ptr<girl> &g) {
        girl_friend = g;
    }
private:
    shared_ptr<girl> girl_friend;
};

class girl {
public:
    girl() {
        cout << "girl construct!" << endl;
    }

    ~girl() {
        cout << "girl destruct!" << endl;
    }

    void set_boy_friend(shared_ptr<boy> &b) {
        boy_friend = b;
    }
private:
    shared_ptr<boy> boy_friend;
};


void use_trap() {
    shared_ptr<girl> sp_girl(new girl());
    shared_ptr<boy> sp_boy(new boy()); 

    sp_girl->set_boy_friend(sp_boy);
    sp_boy->set_girl_friend(sp_girl);

}

int main() {

	use_trap();
	system("pause");
	return 0;
}

总结:所以两个对象不要互相持有对方的共享指针,导致循环引用。
除非使用下面的weak_ptr。

5.weak_ptr 使用详解 (自从C++11)

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少. 同时weak_ptr 没有重载*和->,但可以使用 lock 获得一个可用的 shared_ptr 对象。(即可以强转为共享指针)
目的:就是防止共享指针的弊端,交叉引用。

对于上面的代码,可以在两个对象持有对方的共享指针时,将其中一个变成弱指针即可。
比如将boy类中的girl_friend变成弱指针。就不会发生循环引用。当需要调用时,只需要加上.lock()变成共享指针即可

6.智能指针的使用陷阱

  1. 不要把一个原生指针给多个智能指针管理
int *x = new int(10);
unique_ptr<int> up1(x);
unique_ptr<int> up2(x);
//警告! 以上代码使up1 up2指向同一个内存,非常危险
或以下形式:
up1.reset(x);
up2.reset(x);
  1. 记得使用u.release()的返回值
    在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了

  2. 禁止delete 智能指针get 函数返回的指针
    如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!

  3. 禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!
    shared_ptr< int> sp1(new int(10));
    //一个典型的错误用法 shared_ptr sp4(sp1.get());

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值