使用C++智能指针可以方便地管理动态内存,本笔记参考《C++ Primer》,简要记录C++智能指针的用法。
内存区域概述
静态内存用来保存局部static对象、类static数据成员以及任何函数之外的变量。
栈内存用来保存定义在函数内的非static对象。
程序用堆来存储动态分配的对象,即那些在程序运行时分配的对象。
前二者保存的对象由编译器自动创建和销毁:栈对象在定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
堆对象的生存期由程序控制,当其不再使用时,必须显式地销毁。
注:有关内存方面的知识,我的了解尚浅,仅看过相关技术博客,然而其中总有评论对某些论断提出质疑,因此我这里也不引述技术博客中的话,仅引述《C++ Primer》中的内容进行说明。
动态内存管理(new、delete)
C++提供new
和delete
两个运算符进行动态内存的分配和释放。
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的区别
new
、delete
是运算符,malloc
、free
是函数;new
、delete
调用构造函数和析构函数,malloc
、free
不调用构造函数和析构函数;- 使用
new
返回类型为对应的对象类型的指针,malloc
返回的指针类型为void*
; - 使用
new
分配内存时无须指定内存块的大小,使用malloc
则需要显式地指出所需内存的大小。
个人认为就使用的便捷性而言,无疑是new
、delete
远胜于malloc
、free
。
动态内存管理中的问题
对于动态内存的使用,如果忘记释放内存,将产生内存泄漏,泄露的多了就会耗尽内存。如果在尚有指针引用内存的情况下释放内存,将会产生引用非法内存的指针。释放同一块内存(可能有多个指针指向同一块内存,对一个指针执行delete操作后又对其他指针进行delete操作)将会破坏自由空间。为了更方便安全地使用动态内存,应该使用标准库提供的智能指针。智能指针会自动释放指向的对象,且能保证在一块内存没有任何智能指针指向其时才会自动释放它。
智能指针
智能指针是模板,在创建时必须提供指针可指向的类型信息。智能指针的使用方式与普通指针类似。各个智能指针还有独有的操作。以下三种智能指针都定义在头文件memory
中。
share_ptr
share_ptr
允许多个指针指向同一个对象,unique_ptr
独占所指向的对象。
share_ptr
和unique_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_ptr
与new
结合使用,用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
。
可以通过release
和reset
转移指针所有权。
示例:
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 []的区别
查阅一些技术博客,对delete
和delete []
的区别进行了解释。对于由基本类型组成的动态数组,二者的结果相同。对于类类型组成的动态数组,若使用delete
只会调用首个对象的析构函数,释放首个对象。使用delete []
就对数组中的每个对象都调用析构函数,释放全体对象。
参考资料:
[1]《C++ Primer》