【C++】内存管理总结

1.内存管理基础

  • 程序运行时映像

    image

  • 进程映像运行时变化

    • 栈上变化:随函数执行过程动态变化

      可使用gdb打印观察,教学见https://www.icourse163.org/course/NJU-1449521162

    • 堆上变化:通过malloc/free动态在堆上分配和释放

  1. 堆内存管理
  • 基础

    • mm_struct

      进程的内存结构,在内核中,是用mm_struct来表示的,其定义如下:

      struct mm_struct {
       ...
       unsigned long (*get_unmapped_area) (struct file *filp,
       unsigned long addr, unsigned long len,
       unsigned long pgoff, unsigned long flags);
       ...
       unsigned long mmap_base; /* base of mmap area */
       unsigned long task_size; /* size of task vm space */
       ...
       unsigned long start_code, end_code, start_data, end_data;
       unsigned long start_brk, brk, start_stack;
       unsigned long arg_start, arg_end, env_start, env_end;
       ...
      }
      

      在上述mm_struct结构中:

      • [start_code,end_code)表示代码段的地址空间范围。
      • [start_data,end_start)表示数据段的地址空间范围。
      • [start_brk,brk)分别表示heap段的起始空间和当前的heap指针。
      • [start_stack,end_stack)表示stack段的地址空间范围。
      • mmap_base表示memory mapping段的起始地址。

      C语言的动态内存分配基本函数是 malloc(),在 Linux 上的实现是通过内核的 brk 系统调用。brk()是一个非常简单的系统调用, 只是简单地改变mm_struct结构的成员变量 brk 的值。

    • brk 与 sbrk

      在前面有提过,有两个函数可以直接从堆(Heap)申请内存,brk()函数为系统调用,sbrk()为c库函数。

      系统调用通常提过一种最小的功能,而库函数相比系统调用,则提供了更复杂的功能。在glibc中,malloc就是调用sbrk()函数将数据段的下界移动以来代表内存的分配和释放。sbrk()函数在内核的管理下,将虚拟地址空间映射到内存,供malloc()函数使用。

      下面为brk()函数和sbrk()函数的声明。

      #include <unistd.h>
      int brk(void *addr);
      
      void *sbrk(intptr_t increment);
      
    • malloc/free模拟实现

      https://www.bilibili.com/video/BV16o4y1S7Gj?spm_id_from=333.999.0.0

  1. C++构造函数与析构函数

    由于很多复杂的C++对象会持有指向堆对象的指针,在使用默认拷贝构造函数和默认的赋值运算重载函数就会发生浅拷贝,即只是拷贝了指向堆对象的指针的值,造成两个对象指向同一个堆对象。

    为了达到深拷贝的效果,所以需要自定义拷贝构造函数和赋值运算重载函数。

  2. 智能指针

    • 设计目的:
      • 对象离开作用域自动析构,简化程序员的负担
      • 多线程情况下,存在堆对象的共享,避免错误的析构
    • 分类:
      • 不带引用计数的智能指针:仅满足对象离开作用域自动析构
        • auto_ptr 不推荐使用,不能在容器中使用
        • scoped_ptr 禁止拷贝构造和赋值构造
        • unique_ptr 推荐使用,提供了带右值引用的拷贝构造和赋值重载函数,优点:通过std::move()显式转移了资源的所有权
      • 带引用计数的指针智能指针:满足线程安全的堆内存共享
        • shared_ptr : 强指针,可以改变资源的引用计数
        • weak_ptr : 弱指针,不可以改变资源的引用计数,没有operator* operator->重载函数,必须使用lock()方法,提升为shared_ptr,才能调用成员方法
    • shared_ptr存在的问题:
      • 循环引用导致内存泄漏
      • 解决方法:定义对象用shared_ptr, 引用对象用weak_ptr
  3. 临时变量与对象移动

    • 临时对象产生的时机:

      • 以传值方式给函数传递参数
      • 类型转换生成临时对象
      • 函数返回对象时候
      • 类外的运算符重载
    • 优化思路:

      • 函数参数传递过程中,对象优先按引用传递,不要按值传递

      • 函数返回对象,应该优先返回一个临时对象,不要返回一个定义对象

      • 按初始化方式接收返回值是对象的函数调用

    • 问题:

      可能存在场景无法返回临时对象

      因此需要从函数栈帧返回到主函数时发生一次拷贝构造生成一个临时对象,而在主函数中对象为了深拷贝又调用了一次赋值运算符重载函数,这造成了资源的浪费

      为了解决这个问题,C++提供了

      • 移动构造函数和移动赋值运算符

      • 引用折叠:配合forward()

      • std::move() 因为右值引用本身是一个左值,所以需要用std::move()强转为右值

      • std::forward() 可以在传参时区分出左值与右值

  4. 内存池

    为什么要内存池?

    • 减少malloc分配控制信息带来的额外内存占用
    • 减少malloc调用次数,减少系统调用带来的资源损耗

    SGI STL的二级空间分配器设计:

    • 一级分配器:分配大内存块
    • 二级分配器:分配小内存块

    ngnix也有内存池设计

    暂略

  5. 其他

    Linux内核也对内存分配做了很多优化;

    • 伙伴系统(buddy )与SLAB,SLUB
    • fork写时拷贝
    • 内存去重(KSM)
    • 内存压缩(zswap机制)
    • 大页

    但是已经超过C++语言的范畴,不表

posted on 2021-11-04 21:30  yangzhe97  阅读(90)  评论(0编辑  收藏  举报