C++ 动态内存与智能指针

使用C++智能指针可以方便地管理动态内存,本笔记参考《C++ Primer》,简要记录C++智能指针的用法。

内存区域概述

静态内存用来保存局部static对象、类static数据成员以及任何函数之外的变量。
栈内存用来保存定义在函数内的非static对象。
程序用来存储动态分配的对象,即那些在程序运行时分配的对象。

前二者保存的对象由编译器自动创建和销毁:栈对象在定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
堆对象的生存期由程序控制,当其不再使用时,必须显式地销毁。

:有关内存方面的知识,我的了解尚浅,仅看过相关技术博客,然而其中总有评论对某些论断提出质疑,因此我这里也不引述技术博客中的话,仅引述《C++ Primer》中的内容进行说明。

动态内存管理(new、delete)

C++提供newdelete两个运算符进行动态内存的分配和释放。

new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化。
delete:接收一个动态对象的指针,销毁该对象,并释放与之相关的内存

使用new分配动态内存

使用new动态分配内存,默认情况下动态分配的对象是默认初始化的:

  • 内置类型或组合类型的对象值未定义;
  • 类类型对象用默认构造函数初始化。
int *pi = new int;//指向一个未初始化的int
string *ps = new string;//指向一个空string

可以使用直接初始化的方式来初始化动态分配的对象,使用传统的构造方式(使用圆括号)、列表初始化(使用花括号)。也可以使用值初始化,在类型名后加空括号。

值初始化和默认初始化的区别:
对于定义了自己的构造函数的类类型,都会用默认构造函数初始化,但对于内置类型,值初始化的类型对象有良好定义的值,而默认初始化的对象的值则是未定义的。

//传统的构造方式(使用圆括号)示例:
int *pi = new int(1024);

//列表初始化(使用花括号)示例:
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

//值初始化与默认初始化对比:
int *pi1 = new int();//值初始化,指向的int对象值为0
int *pi2 = new int;//默认初始化,值未定义
string *ps1 = new string();//值初始化为空string
string *ps2 = new string;//值初始化为空string

使用delete释放动态内存

为防止内存耗尽,在动态内存使用完毕后用delete销毁对象并释放内存。

传递给delete的指针必须指向动态分配的内存,释放一块非new分配的内存、或将相同的指针值释放多次,其行为未定义。

内置指针管理的动态内存在被显式释放前一直都会存在。

题外话:new、delete与malloc、free的区别

  • newdelete是运算符,mallocfree是函数;
  • newdelete调用构造函数和析构函数,mallocfree不调用构造函数和析构函数;
  • 使用new返回类型为对应的对象类型的指针,malloc返回的指针类型为void*
  • 使用new分配内存时无须指定内存块的大小,使用malloc则需要显式地指出所需内存的大小。

个人认为就使用的便捷性而言,无疑是newdelete远胜于mallocfree

动态内存管理中的问题

对于动态内存的使用,如果忘记释放内存,将产生内存泄漏,泄露的多了就会耗尽内存。如果在尚有指针引用内存的情况下释放内存,将会产生引用非法内存的指针。释放同一块内存(可能有多个指针指向同一块内存,对一个指针执行delete操作后又对其他指针进行delete操作)将会破坏自由空间。为了更方便安全地使用动态内存,应该使用标准库提供的智能指针。智能指针会自动释放指向的对象,且能保证在一块内存没有任何智能指针指向其时才会自动释放它。

智能指针

智能指针是模板,在创建时必须提供指针可指向的类型信息。智能指针的使用方式与普通指针类似。各个智能指针还有独有的操作。以下三种智能指针都定义在头文件memory中。

share_ptr

share_ptr允许多个指针指向同一个对象,unique_ptr独占所指向的对象。

share_ptrunique_ptr共有的操作

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

swap(p, q)       //交换p和q中的指针
p.swap(q) 	

share_ptr的操作

make_shared<T> (args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T>p (q) 	  //p是shared_ptr的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q 	              //p和q都是shared_ptr,所保存的指针必须能相互转换
					  //此操作会递减p的引用计数,递增q的引用计数
					  
p.unique() 	          //若p.use_count()为1,返回true;否则返回false
p.use_count() 	      //返回与p共享对象的智能指针数量

示例

shared_ptr<string> ps1 = make_shared <string> ("abcdefg");
shared_ptr<string> ps2 = make_shared <string> ("higklmn");
shared_ptr<string> ps3(ps1);
cout << *ps1 << endl;//输出abcdefg
cout << *ps2 << endl;//输出higklmn
cout << *ps3 << endl;//输出abcdefg
cout << ps1.use_count() << endl;//输出2
cout << ps2.use_count() << endl;//输出1
ps3 = ps2;
cout << *ps3 << endl;//输出higklmn
cout << ps1.use_count() << endl;//输出1
cout << ps2.use_count() << endl;//输出2

可以认为每个share_ptr都有一个关联的计数器,称为引用计数,拷贝一个share_ptr,引用计数加一,可以理解为指向的对象多了一个引用者。给share_ptr赋予一个新值或share_ptr被销毁,引用计数减一,可以理解为指向的对象少了一个引用者。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象,可以理解为指向的对象没有引用者了,因此自动释放。

shared_ptr类使用其析构函数完成指向对象的销毁工作。shared_ptr的析构函数会递减它所指对象的引用计数,若降为0,则shared_ptr的析构函数就会销毁指向的对象,并释放占用的内存。

share_ptrnew结合使用,用new返回的指针初始化智能指针。

接收指针参数的智能指针的构造函数是explicit的。因此不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式。相同的,一个返回share_ptr的函数不能在其返回语句中隐式转换一个普通指针,必须将share_ptr显式绑定到一个想要返回的指针上。

示例

shared_ptr<string> ps1 = new string("abcdefg");//错误
shared_ptr<string> ps2(new string ("abcdefg"));//正确

在这里插入图片描述
在这里插入图片描述

unique_ptr

unique_ptr的操作

unique_ptr<T> u1 	      //空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;
unique_ptr<T, D> u2 	  //u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> 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时,需将其绑定到一个new返回的指针上,类似shared_ptr,初始化unique_ptr必须采用直接初始化。由于unique_ptr独占他所指向的对象,因此不支持普通的拷贝和幅值操作,但可以拷贝将要被销毁的unique_ptr,例如函数返回一个unique_ptr
可以通过releasereset转移指针所有权。

示例

unique_ptr<string> ps1(new string ("abcdefg"));
unique_ptr<string> ps2(ps1);//错误,不能拷贝
unique_ptr<string> ps3;
ps3 = ps1;//错误,不能幅值
unique_ptr<string> ps1(new string ("abcdefg"));
unique_ptr<string> ps2(ps1.release());//将所有权从ps1转移给ps2
cout << *ps2 << endl;//输出abcdefg
unique_ptr<string> ps3(new string ("higklmn"));
ps2.reset(ps3.release());
cout << *ps2 << endl;//输出higklmn

weak_ptr

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

weak_ptr的操作:

weak_ptr<T> w 	    //空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) 	//与shared 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的对象的share_ptr

创建weak_ptr时用share_ptr初始化它。对象可能不存在,所以不能用weak_ptr直接访问对象,必须调用lock检查对象是否存在。

示例

shared_ptr<string> ps1(new string ("abcdefg"));
cout << ps1.use_count() << endl;//输出1
weak_ptr<string> ps2(ps1);
cout << ps2.use_count() << endl;//输出1,引用计数并未增加
if (shared_ptr<string> ps3 = ps2.lock()) {
    cout << *ps3 << endl;//输出abcdefg
    cout << ps2.use_count() << endl;//输出2,所指对象多了一个引用者
}
cout << ps2.use_count() << endl;//ps3出了作用域就销毁了,因此引用计数减1,输出1
shared_ptr<string> ps4(new string("higklmn"));
ps1 = ps4;
cout << ps2.use_count() << endl;//输出0,对象已经不存在了

动态数组

使用new分配对象数组,在类型名后面跟一个方括号,其中指明要分配的对象的数目;释放动态数组时delete后面也要跟一个方括号,即delete []形式,数组中的元素逆序销毁

《C++ Primer》中指出:

大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。

所以为了避免麻烦,还是多用标准库容器类型吧。

题外话:delete和delete []的区别

查阅一些技术博客,对deletedelete []的区别进行了解释。对于由基本类型组成的动态数组,二者的结果相同。对于类类型组成的动态数组,若使用delete只会调用首个对象的析构函数,释放首个对象。使用delete []就对数组中的每个对象都调用析构函数,释放全体对象。

参考资料:
[1]《C++ Primer》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值