跳至主要內容

数据内存管理思想

LincZero大约 15 分钟

数据内存管理思想

数据的属性性质

持续性(存储方案)(4种)

描述了数据保存在内存中的时间

持续性(存储方案)持续性说明定义方法
自动存储持续性执行完函数或代码块时,内存被释放函数中定义(包括函数参数)的变量
声明为static则例外
静态存储持续性程序整个运行过程中都存在全局声明、static声明
线程存储持续性(C++11)声明周期与所属线程一样长
动态存储持续性new运算符分配的内存一直存在,直到delete运算符将其释放或程序结束
有时被称为自动存储空间free store)或heap)(定位new除外)
其内存管理更复杂,难以跟踪新分配内存的位置
new - deletenew[] - delete[]

属性性质 > 作用域(scope

描述了名称在文件(翻译单元)的多大范围内可见

作用域说明定义方式
局部作用域只在定义它的代码块可用在代码块(包括函数参数)内定义
全局作用域(也叫文件作用域)在定义位置到文件结尾之间可用在文件中定义
函数原型作用域(function prototype scope只在包含参数列表的括号内可以(可理解为占位符)函数原型的定义

局部作用域的覆盖/隐藏性

  • 局部变量隐藏全局变量
    • 局部变量名与外部变量名相同时,我们说新的定义隐藏了(hide)以前的定义
    • 在代码块内新定义可见,旧定义暂时不可见。当程序离开代码块时,原来的定义又重新可见
  • 仍然使用全局变量版本
    • 当定义了与外部变量名同名的局部变量后仍然想使用全局变量版本的变量,可使用作用域解析运算符::

文件的“自动变量”

好像可以用auto来隐藏外面导入的变量,不太记得了,有没有记错?

作用域、潜在作用域、声明区域 补充

  • 三者
    • 声明区域declaration region):是可以在其中进行声明的区域
      • 例:可以在函数外面声明全局变量,即全局变量的声明区域为其声明所在的文件
    • 潜在作用域potential scope):从声明点开始到声明区域的结尾(包括不可见的区域)
    • 作用域scope):变量对程序而言可见的范围
  • 关系
    • 一般而言:声明区域 > 潜在作用域 >= 作用域
    • 潜在作用域与作用域区分
      • 如局部变量隐藏全局变量时的语句块,语句块内部是对应的全局变量的潜在作用域而不是作用域
      • 虽然仍然能使用作用域解析运算符使用,但并不是常规的变量的作用域

属性性质 > 链接性(linkage

描述了名称如何在不同单元间共享

链接性说明举例
外部链接性可在文件间共享全局的非static声明
内部链接性只能由一个文件中的函数共享static声明的变量、或使用匿名名称空间声明
没有链接性不能共享自动变量名
  • 注:具有外部链接性的变量,可在其他文件中使用关键字extern声明来使用它
  • 注:还有另一种形式的链接性:叫语言链接性。并不是指变量的链接性,而是指语言所使用的链接性
    • 这里的“链接性”,更像是指语言使用什么方法去翻译函数名如:
      • C语言链接性C language linkage
        • 例如:将函数名spiff翻译成_spiff
        • (C语言没有名称修饰)
      • C++语言链接性C++ language linkage
        • 例如:将函数名spiff(int)转换为_spoff_i
        • 例如:将函数名spiff(double,double)转换为_spiff_d_d
        • (C++有名称修饰)
    • 因为C++的名称修饰所带来的影响,使C++的语言链接性与C不同。这导致了一些问题:(详见名称修饰所带来的影响)
      • C++使用C的库文件中预编译的函数时,可能找不到(C语言不允许函数重载,并没有名称修饰)
      • 解决方案:用函数原型来指出要使用的约定:
      • extern "C" void spiff(int);,使用C语言链接性 查找函数名
      • extern void spoff(int);,默认使用C++语言链接性 查找函数名
      • extern "C++" void spoff(int);,显示使用C++语言链接性 查找函数名

限制链接性的方法

有的名称是外部或内部链接性,但并不意味着可以直接访问

可能会需要通过一些方式(如名称空间、作用域解析符、句点或箭头运算符)才能访问到

需要访问的内容访问手段的限制
普通的外部链接变量使用extern声明后访问
被当局部变量隐藏的全局变量使用作用域解析运算符::访问
名称空间使用名称空间名+作用域解析运算符::,或using声明访问(3种方式)
结构体成员使用句点运算符,或箭头运算符(指针时)访问
类数据成员或成员方法在类声明文件的类作用域中:可直接访问
在类实现文件中:用作用域解析运算符访问公有私有函数
在公有派生类中:用作用域解析运算符访问公有保护成员
在类实现文件的成员函数中:可直接访问
在类实现文件的非成员友元函数中:可直接访问
在客户文件中:通过实例对象与句点运算符,或箭头运算符(指针时)访问
类实现代码以外访问类私有成员通过友元访问(友元函数友元类

导入的方式共享

链接性主要是描述名称在不同单元间如何共享,而使用导入的方式(在C++通过预编译的方式实现)则不受此限制

(但本质是通过预编译的方式无视链接性合并两个文件)

#include 头文件名

数据的底层情况

底层本质

持续性存储区域底层本质与表现
静态存储全局/静态区域在编译期就为其分配内存(声明为const例外),在程序结束时释放
(const全局变量)只读数据段第一次使用时为其分配内存,在程序结束时释放
自动存储栈中内存随函数的开始和结束而增减,并遵守LIFO(先进后出)
动态存储自由存储区/堆new时分配内存,delete或程序结束时释放
线程存储

编译器使用的内存(内存四区)

  • 内存四区
    • 栈区
    • 堆区
    • 全局区(/静态区)
    • 代码区
  • 通常,编译器使用三块独立的内存
    • 一块用于静态变量(可能再细分)(全局/静态存储区)
    • 一块用于自动变量(栈)
    • 一块用于动态存储(堆)

所有到底是四区还是三区?

栈与堆

函数调用与结束时的栈

  • 程序使用两个指针来跟踪栈,一个指向栈底(栈开始的位置),一个指向栈顶(下一个可用的内存单元)
  • 遵循LIFO(后进先出)原则
  • 注意:当函数结束时,栈顶指针被重新设置。这时传入栈的数值实际没有被删除,只是不再被标记了

栈、堆,与虚拟地址空间

  • Linux进程的虚拟地址空间设计:(地址从下往上增大)
虚拟地址空间补充说明
内核虚拟内存【顶部区域】不允许应用程序读写和调用,必须通过调用内核来执行这些操作
用户
(往下增长)
运行时创建
【动态大小】每次调用函数栈增长,函数返回时栈会收缩
共享库的内存映射区域
(往上增长)
【动态大小】存放像C标准库和数学库这样共享库代码和数据的地方
运行时
(往上增长)
运行时由malloc创建
【动态大小】调用malloc和free这样的C标准库函数时可动态扩展和收缩
读/写数据从hello可执行文件加载进来的程序代码和数据
只读的代码和数据【开始区域】从hello可执行文件加载进来的程序代码和数据

栈、堆,与内存泄露

内存泄露

  • 根本原因:使用new运算符在自由存储空间(或堆)上创建变量后若没有调用delete
  • 直接原因:即使指向该内存的指针因为作用域规则和对象周期的原因被释放,在堆上分配的变量或内存也将继续存在,即导致了内存泄露
  • 严重后果:被泄露的内存将在程序的整个生命周期内都不可用。这些内存被分配出去但却再也无法收回
  • 极端后果:内存泄露严重,以至于应用程序可用的内存被耗尽,出现内存耗尽错误,程序崩溃
  • 避免手段:同时使用new和delete运算符,而C++智能指针有助于自动完成这种任务

数据的储存方式(具体方案)

  • 上面的持续性、作用域、链接性并非可以任意组合,有的组合是没有的
    • 比如:作用域代码块函数原型作用域时,链接性必然是

注:这里没有写new、动态、类变量、函数、线程存储

存储描述持续性作用域链接性如何声明
自动自动代码块在代码块中
寄存器自动代码块在代码块中,使用关键字register
静态,无链接性静态代码块在代码块中,使用关键字static
静态,外部链接性静态文件外部不再任何函数内
静态,内部链接性静态文件内部不再任何函数内,使用关键字static
动态new动态代码块/文件无/内部/外部new - deletenew[] - delete[]
线程存储

下面具体介绍每种具体方案

自动变量

  • 简概

    • 是一个局部变量,其作用域为包括它的代码块
  • 定义

    • 函数内部定义常规变量使用自动存储空间,被称为自动变量automatic variable
    • auto补充:在C++11中,auto用于自动类型推断,但在此之前其含义用于显示指出变量为自动存储
  • 存储

    • 栈中,后进先出(LIFO),在程序执行过程中,栈将不断地增大和缩小
    • 在所属函数被调用时自动产生(被分配内存),在该函数结束时自动消亡(释放内存)
  • 初始化(旧版本问题)

    • 传统的K&R C,不允许初始化自动数组和结构,值允许初始化静态数组和结构
    • ANSI CC++,允许对这两种数组和结构进行初始化

寄存器变量

  • 简概
    • 最初C语言用于将一个自动变量存储于CPU寄存器,以提高访问变量的速度
    • 其只能用于原本就是自动的变量(这样设计的原因可能是要让它视情况销毁,而不要一直留在寄存器中占用内存)
  • 用法
    • register声明,如:register int count_fast;
  • 使用场景
    • 现在几乎没有任何用处
      • 硬件越来越复杂:还有多级缓存
      • 编译器越来越智能:当某一变量用得多了,编译器可自动对其做特殊处理,而不需要手动去注明
    • 使用它的唯一原因
      • 指出程序员想使用一个自动变量,当auto

静态存储

  • 使用:两种方法
    • 全局静态存储:在函数外面定义
    • 局部静态存储:在声明变量时使用关键字static
  • 初始化
    • 如果没有显式初始化静态变量,则进行零初始化(可以简单理解为设置为0)
    • 但这里的零指对于标量类型,会转换为合适的类型。比如:
      • 指针的零初始化:会被初始化相应的内部表示(0可以表示空指针,但内部可能采用非0表示)
      • 结构成员的零初始化:其填充位会被设置为零
  • 初始化的类型
    • 静态初始化零初始化常量表达式初始化的统称(C++新增关键字constexpr可增加创建常量表达式的方式)
    • 动态初始化:变量将在编译后初始化
  • 补充:批量声明静态变量的方法
    • 可以使用匿名名称空间

自由存储空间(动态联编)

使用new和delete运算符,管理一个内存池

自由存储空间 - 简概(与C不同)

  • 指针使用注意项、new方式使用指针

    • C++创建指针时,计算机分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存

      即不能给未有地址指向的指针的解除引用值赋值,像这样:long * pi_val; *pi_val = 1;会报错

    • 但使用new运算符可以立即返回指针一个未命名地址空间,这种情况下只能通过指针来访问该内存

  • 内存的分配与释放

    • 分配:mallocnewnew[]new()new()[]这种函数被称为分配函数alloction function),它们位于全局名称空间中
    • 释放:deletedelete[]这种函数被称为释放函数deallocation function
      • 使用内存后要将内存还给内存池,归还或释放(free)的内存可供程序的其他部分使用
  • C语言和C++方法区别

    • C语言:使用库函数malloc()来分配内存
    • C++:newnew[]运算符分配内存
  • 注意规则

    • 不要使用delete来释放不是new分配的内存

      • 后果:一定要配对地使用newdelete,否则将发生内存泄露(memory leak)

        也就是说,被分配的内存再也无法使用了。如果内存泄露严重,则程序将由于不断寻找更多内存而终止

      • 举例:不能使用delete来释放声明变量所获得的内存

      • 其他补充:然而对空指针使用delete是安全的

    • 不要使用delete释放同一个内存块两侧

      • 后果:这样做的结果将是不确定的,意味着什么情况都可能发生
    • 如果使用new[]为数组分配内存,则应使用delete[]来释放

    • 如果使用new为一个实体分配内存,则应使用delete来释放

    • 释放内存不会删除指针本身,可以继续将该指针重新分配一个新的内存块

    • 不要创建两个指向同一内存块的指针,这将增加错误地删除同一个内存块两侧的可能性

定位new运算符

定位(placement)new运算符,能指定要使用的位置

  • 使用
    • 先包含头文件new:#include <new>
    • 写法举例:char buffer[50]; p = new(buffer) chaff; ap = new(buffer) int[20];
  • 定位new与new的区别
    • 定位new不跟踪哪些内存单元已经被使用,也不查找未使用的内存块 所以通常需要先创建一块可用的内存区域(如使用定长数组来声明一块区域)再使用定位new,否则可能会覆盖原有的一些数据(坑)
    • 当定位new不是指向堆(一般情况下都不指向堆),而指向全局/静态内存区或者栈中 则不应该对其使用delete/delete[]delete只能对堆区域使用,否则可能会引起程序崩溃(大坑)

【功能拓展】new x 动态数组 / 字符串

  • 内存创建和释放

    • 创建通用格式:typeName * pointer_name = new typeName

      例:int * pn = new int,会分配一个适合存储int的内存并返回其地址

    • 释放通用格式:delete pointer_name

  • 创建和释放动态数组(new方式)(用武之地)

    • 创建通用格式:type_name * pointer_name = new type_name [num_elements] 例:int * pn = new int [10]、字符串:char * ps = new char [10]
  • 释放通用格式:delete [] pointer_name (程序在new时就会跟踪分配的内存量,delete时不需要再次输入也不能输出。但这里的内存量不公用,即不能使用sizeof运算符来确定) 例:delte [] psome

  • 使用动态数组

    • 可以把指针当数组名用,pointer_name[n]访问序列为n的元素

【功能拓展】new x 动态结构

(类与结构非常方式,这里的技术也适用于类)

  • 使用
    • 创建实例:和创建动态数组一样,例inflatable * ps = new inflatable;
    • 访问成员:不能直接使用句点运算符使用(没有名称,只有地址)
      • 可以使用箭头成员运算符->,例ps -> price
      • 也可以先接触引用为结构体本身再使用句点运算符,例(*ps).price
      • 两种方法完全等价

【总结】什么时候不delete

一般来说new完都需要delete,否则内存泄露,只有以下情况例外

  • delete过一次
    • QT的对象树机制中,QObject对象不需要手动delete
    • 智能指针、智能引用
  • 定位new
  • 程序结束,系统释放

其他补充

  • 容易忘记delete:构造函数中new完要及时或在析构函数中delete
  • 容易多次delete:析构函数中delete构造函数的new,当关闭程序时可能会多析构一次
  • delete对象指针会自动调用析构函数

线程存储(C++新增)