C++模块3:内存管理,智能指针

C++模块3:内存管理,智能指针

1. 程序加载时的内存分布:

在多任务操作系统中,每个进程都运行在一个属于自己的虚拟内存中,而虚拟内存被分为许多页,并映射到物理内存中,被加载到物理内存中的文件才能够被执行。这里我们主要关注程序被装载后的内存布局,其可执行文件包含了代码段,数据段,BSS段,堆,栈等部分,其分布如下图所示。

在这里插入图片描述

代码段(.text):用来存放可执行文件的机器指令。存放在只读区域,以防止被修改。

只读数据段(.rodata):用来存放常量存放在只读区域,如字符串常量、全局const变量等。

可读写数据段(.data):用来存放可执行文件中已初始化全局变量,即静态分配的变量和全局变量。

BSS段(.bss):未初始化的全局变量和局部静态变量一般放在.bss的段里,以节省内存空间。

堆:用来容纳应用程序动态分配的内存区域。当程序使用malloc或new分配内存时,得到的内存来自堆。堆通常位于栈的下方。
栈:用于维护函数调用的上下文。栈通常分配在用户空间的最高地址处分配。

动态链接库映射区:如果程序调用了动态链接库,则会有这一部分。该区域是用于映射装载的动态链接库。

保留区:内存中受到保护而禁止访问的内存区域。

2 堆与栈的区别:

A:申请管理方式:

(1)栈:由编译器自动管理,无需我们手工控制。
(2)堆:堆的申请和释放工作由程序员控制,容易产生内存泄漏。

B. 申请后系统的响应:

1)栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
(2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

C:申请大小的限制:

(1)栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是1M(可修改),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
(2)堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大

D:申请效率的比较:

(1)栈由系统自动分配,速度较快。但程序员是无法控制的。
(2)堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活

E:栈和堆中的存储内容:

1)栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
(2)堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排

总结:堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;并且可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,ebp和局部变 量都采用栈的方式存放。所以,推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,但是向堆申请内存更加灵活,有时候分配大量的内存空间,还是用堆好一些。

3. 常见的内存错误及其对策:

A:内存分配未成功,却使用了它,因为没有意识到内存分配会不成功。
解决办法:在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
B:内存分配虽然成功,但是尚未初始化就引用它。犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
解决方法:不要忘记为数组和动态内存赋初值,即便是赋零值也不可省略。防止将未被初始化的内存作为右值使用。
C:内存分配成功并且已经初始化,但操作越过了内存的边界。例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
解决方法:避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
D:忘记了释放内存,造成内存泄露。含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
解决方法:动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
E:释放了内存却继续使用它。有三种情况
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
解决方法:用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

4. 智能指针:

智能指针是在 标头文件中的std命名空间中定义的,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它们对RAII“获取资源即初始化”编程至关重要,RAII的主要原则是为将任何堆分配资源(如动态分配内存或系统对象句柄)的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。在C++11中,定义了3种智能指针(unique_ptr、shared_ptr、weak_ptr),并删除了C++98中的auto_ptr。

智能指针的设计思想:
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

unique_ptr 只允许基础指针的一个所有者。unique_ptr小巧高效;大小等同于一个指针且支持rvalue引用,从而可实现快速插入和对STL集合的检索。

shared_ptr采用引用计数的智能指针,主要用于要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时)的情况。当所有的shared_ptr所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。最安全的分配和使用动态内存的方法是调用make_shared标准库函数,此函数在动态分配内存中分配一个对象并初始化它,返回对象的shared_ptr。

智能指针支持的操作:

使用重载的->和*运算符访问对象。

使用get成员函数获取原始指针,提供对原始指针的直接访问。你可以使用智能指针管理你自己的代码中的内存,还能将原始指针传递给不支持智能指针的代码。

使用删除器定义自己的释放操作。

使用release成员函数的作用是放弃智能指针对指针的控制权,将智能指针置空,并返回原始指针。(只支持unique_ptr)

使用reset释放智能指针对对象的所有权。

智能指针的使用示例

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class base
{
    
    
public:
    base(int _a): a(_a)    {
    
    cout<<"构造函数"<<endl;}
    ~base()    {
    
    cout<<"析构函数"<<endl;}
    int a;
};

int main()
{
    
    
    unique_ptr<base> up1(new base(2));
    // unique_ptr<base> up2 = up1;   //编译器提示未定义
    unique_ptr<base> up2 = move(up1);  //转移对象的所有权 
    // cout<<up1->a<<endl; //运行时错误 
    cout<<up2->a<<endl; //通过解引用运算符获取封装的原始指针 
    up2.reset(); // 显式释放内存 
    
    shared_ptr<base> sp1(new base(3));
    shared_ptr<base> sp2 = sp1;  //增加引用计数 
    cout<<"共享智能指针的数量:"<<sp2.use_count()<<endl;  //2
    sp1.reset();  //
    cout<<"共享智能指针的数量:"<<sp2.use_count()<<endl;  //1
    cout<<sp2->a<<endl; 
    auto sp3 = make_shared<base>(4);//利用make_shared函数动态分配内存

猜你喜欢

转载自blog.csdn.net/weixin_40734514/article/details/109806660