内存相关
博客截止到目前的C++部分都只使用了静态内存和栈内存。
静态内存用于保存局部static对象、类的static数据成员和任何定义在函数之外的变量。
栈内存用于保存定义在函数内的非静态变量。
分配在静态内存和栈内存中的对象都由编译器控制它们的创建和销毁:C++中静态对象在该对象被首次用到时分配内存(C中是在编译期间初始化);栈内存在其定义的程序块运行时分配内存。
除了以上两个内存区,每个程序还拥有一个内存池,这部分被称作自由空间或堆。堆用于存储动态分配的对象,即由程序本身来控制生存期的对象。
动态内存与智能指针
由于动态内存对象是由程序控制,因此其内存的使用很容易出问题。为了更安全、方便地使用动态内存,标准库提供了两种智能指针,该指针与常规指针的重要区别是智能指针负责自动释放所指向的对象,并且智能指针是模板。两种智能制造的区别在于管理底层指针的方式:
- shared_ptr类型允许多个指针指向同一个对象
- unique_ptr则只允许存在一个指针指向一个对象
它们被定义在头文件memory中
1、shared_ptr类
智能指针也是一个模板,类似于vector等类型,对智能指针的初始化也要提供指针指向的类型这一信息。
1 | shared_ptr<string> p1; |
智能指针的操作基本于常规指针相同,当然其作为一个模板也有自己特有的操作:
shared_ptr和unique_ptr均支持的操作
| p | 可用作一个条件判断,若p指向了一个对象则返回true |
|---|---|
| *p | ~ |
| p.get() | 返回p中保存的指针 |
| swap(p1,p2)/p1.swap(q) | 交换p和q中的指针 |
make_shared函数
make_shared函数被定义在标准库中,该函数可以在动态内存中分配一个对象并初始化,并返回指向该对象的shared_ptr。
1 | shared_ptr<string> sp=make_shared<string>("dhk"); |
因为该函数会返回一个对应指向类型的shared_prt,因此也可以直接使用auto来保存函数make_shared的结果:
1 | auto sp=make_shared<string>("dhk"); |
另外,make_shared函数支持参数用作构造,其用法于顺序容器的成员emplace相似,参数顺序等需严格遵守构造函数内容,详见C++-顺序容器 | 小董的BLOG (gitee.io)
1 | auto sp=make_shared<string>(10,'d');//使用构造 |
shared_ptr的拷贝和赋值
当在进行拷贝和赋值时,每个shared_ptr都会实时地记录当前有多少个shared_ptr指向相同的对象,称之为引用计数,一但一个shared_ptr的计数器变为0,则会自动释放自己所管理的对象。
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁这个对象。该操作通过类的一个成员函数——析构函数完成。
如果该shared_ptr是作为局部变量导致被自动销毁,析构函数同样会对引用计数进行递减操作
2、直接管理内存
C++定义了两个运算符来直接分配和释放内存:new用于分配内存,delete用于释放内存
使用new分配动态内存
在堆中的分配的内存是无名的,因此无论是make_shared还是new都无法为分配空间的对象命名,取而代之的是返回一个指向该内存的指针。
几种初始化方式:
1 | int *p= new int; |
也可以使用new分配const对象:
1 | auto p = new const int(100); |
需要注意的是,new和delete都是运算符,而不是关键字!因此const new int()是不合法的
如果堆内存耗尽,无法再分配新的内存,则new表达式会失败,抛出一个bad_alloc,如果不想其抛出异常,可以使用定位new的方式阻止其抛出异常:
1 | auto p = new(nothrow) int;//此时如果分配失败,则只会返回一个空指针 |
使用定位new需要包含头文件new
使用delete释放内存
通过delete表达式可以将指定的动态内存释放,使用时需注意:
- delete的对象必须是一个指针
- 该指针只能指向一块动态内存或为空,不能指向静态内存或栈内存
- 动态内存中的const对象可以被正常释放
- 不能多次释放同一块内存
内置指针的缺陷
上一节中讲到,对于shared_ptr管理的内存,是会根据引用计数自动释放内存的。而通过常规指针保存动态内存的方式无法像智能指针一样自动释放,也就是通过new表达式创建的动态内存空间必须显式地手动释放
因此,使用者必须记得在不使用时删除这块内存,如果指向该内存的常规指针指向改变,这块内存就无法被释放了(因为已经找不到了),所以最好的办法是将该指针定义为常量指针,虽然这样也仅仅能避免地址丢失的发生
混合使用shared_ptr和new
可以使用一个指向动态内存的内置指针来初始化一个智能指针:
1 | shared_ptr<int> p1(new int(100)); |
使用时需注意:
用于初始化智能指针的参数必须是一个指向动态内存的指针
必须用直接初始化的方式初始化智能指针,而不是赋值初始化,因为接受指针为参数的构造函数是explicit的
1
shared_ptr<int> p1= new int(100);//错误!必须直接初始化
内置指针不能实现隐式转化为智能指针,这也是不能通过赋值初始化的原因👆
1
2
3
4
5shared_ptr<int> test(int a)
{
return (new int(a));//错误,不能隐式转化
return shared_ptr<int> (new int(a));//正确
}通过这个方法构造的shared_ptr默认使用delete来释放内存,而不是之前讲到的析构函数。因此如果要将该shared_ptr指向其他的动态内存,但是这样做就必须提供自己的操作来代替delete,这个操作后面会讲到。