美文网首页
C 内存管理

C 内存管理

作者: 苏沫离 | 来源:发表于2018-10-10 23:15 被阅读0次

1、存储类别

C 语言提供了多个不同的存储类别在内存中存储数据。要理解这些存储类别,我们先来理解一些概念。

一块物理内存.png
1.1、C 语言中的内存对象

在 C 语言中,我们使用的数据都存储在内存中。从硬件方面看,被存储的每个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(object)。对象可以存储一个或者多个值。一个对象也可能并未存储实际的值,但是它在存储适当的值时一定具有相应的大小。

面向对象编程中的对象指的是类对象,其定义包括数据和允许对数据进行的操作。C 不是面向对象编程语言。

1.2、标识符

C99 和 C11标准要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符

从软件方面看,程序需要一种方法访问硬件内存(对象)。例如如下语句:

int index = 369;

该声明创建了一个名为index的标识符。标识符是一个名称,是 C 程序用来指定特定硬件内存的对象的一种方式

1.3、作用域

作用域是描述程序中可访问标识符的区域。

一个 C 变量的作用域可以是:块作用域函数作用域函数原型作用域文件作用域

1.3.1、块作用域

块是用一对花括号括起来的代码区域。例如,整个函数体是一个块,函数中的任意复合语句块也是一个块。

定义在块中的变量具有 块作用域(block scope),块作用域变量的可见范围是从定义处到该定义的块的末尾。我们使用的局部变量(包括函数的形参)都具有块作用域;虽然函数形参声明在函数的左花括号之前,但是它们具有块作用域,属于 函数体 这个块。

如下面代码块:函数形参 a与函数体内部变量 bi 都具有块作用域:

int blockArea(int a){
    int b = a;
    for (int i = 0; i < 10; i ++) {
        b += i;
    }// i 的作用域结束
    return b;
}

声明在内层块中的变量,其作用域仅局限于该声明所在的块。
上述例子中的变量 i 被视为 for 循环快的一部分,作用域仅限于 for 循环快内部,一旦程序离开 for 循环就不能再访问变量 i

1.3.2、函数作用域
1.3.3、函数原型作用域

函数原型作用域(function prototype scope)用于函数原型中的形参名。

char * s_gets(char * st, int n);

函数原型作用域的范围是从形参定义处到原型声明结束。这意味着编译器在处理函数原型中的形参时只关心它的类型,而形参名通常无关紧要。形参名不必与函数定义中的形参名相匹配,只有在变长数组中,形参名才有作用。

1.3.4、文件作用域

变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。由于文件作用域可用于多个函数,所以文件作用域变量也称之为全局变量(global variable)。

在下述例子中:变量 unit的作用域从从它的定义处到该定义所在文件的末尾,该变量可用于多个函数,我们称之为全局变量;

int unit = 10;//变量 unit 具有文件作用域

int main(int argc, const char * argv[]) {

    return 0;
}

int blockArea(int a){
    int b = a;
    for (int i = 0; i < 10; i ++) {
        b += i;
    }// i 的作用域结束
    return b;
}
1.4、链接

我们知道:作用域描述了程序中可访问标识符的区域。而 链接 也表明了程序的哪些部分可以使用它。

在 C 语言中,变量有 3 种链接属性:外部链接、内部链接、无链接。

  • 无链接:具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量,这些变量属于定义它们的块、函数原型、函数私有。
  • 内部链接:具有文件作用域的变量,只能在一个源代码文件(即 .c 文件)中使用;我们把 “内部链接的文件作用域” 称为 “文件作用域”。
  • 外部链接:具有文件作用域的变量,外部链接变量可以在多个文件中使用;我们把 “外部链接的文件作用域” 称为 “全局作用域”或“程序作用域”。
1.4.1、存储类别说明符static

如何知道 文件作用域变量 是内部链接还是外部链接?
存储类别说明符 static表明一个文件作用域变量是内部链接:

int index = 5;//文件作用域,外部链接
static int count = 6;//文件作用域,内部链接

变量 index 在该程序的任何文件、任何函数均可使用;变量 count 只能在该文件中的任意函数使用;

注意: 关键字static表明了其链接属性,而非存储期

1.4.2、多文件

只有当程序由多个单元文件组成时,才体现区别内部链接和外部链接的重要性。

复杂的 C 程序通常由多个单独的源代码文件组成。有时,这些文件共享一个外部变量。C 通过在一个文件中进行定义式声明,然后在其它文件进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其它声明都要使用extern关键字;而且,只有定义式声明才能初始化变量。

1.5、存储期

我们在上文讲解的 作用域和链接 描述了标识符的可见性。而 存储期 描述了通过这些标识符访问的硬件内存(对象)的生存期。

在 C 语言中硬件内存(对象)有 4 种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

1.5.1、静态存储期

如果一块内存具有静态存储期,那么它在程序执行期间一直存在。文件作用域变量(无论内部链接还是外部链接)具有静态存储期 。

块作用域变量使用关键字static修饰,则具有静态存储期,从程序被载入到程序结束期间都存在,但是它的作用域仅限于此函数内部,其它函数可以通过该内存地址间接访问该对象

1.5.2、线程存储期

线程存储期:用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从声明到线程结束一直存在。

1.5.3、自动存储期

块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个快时,释放掉刚才为变量分配的内存自动变量占用的内存为一个可重复使用的暂存区 (栈区 stack)。例如一个函数调用后,其变量占用内存可用于存储下一个被调用的函数的变量。

在下述例子中:变量 abi 在每次调用函数 blockArea 时被创建,在离开函数时被销毁

int blockArea(int a){
    static int c = 40;
    int b = a;
    for (int i = 0; i < 10; i ++) {
        b += i;
    }// i 的作用域结束
    return b;
}

然而:块作用域变量也能具有静态存储期。如 变量 c 因为使用存储类别说明符 static 修饰,表明变量 c 具有内部链接的文件作用域,所以被存储在静态内存中

1.5.4、动态分配存储期

稍后提及。

1.6、存储类别

C 语言使用作用域、链接、存储期为变量定义了多种存储方案:使用存储期描述对象在硬件内存的保留时间;使用作用域与链接描述了程序的哪些部分可以使用该对象。

不同的存储类别具有不同的存储期、作用域、链接:

存储类别 存储期 作用域 链接 声明方式
自动 自动 块内
寄存器 自动 块内,使用关键字 register
静态外部链接 静态 文件 外部 所有函数外
静态内部链接 静态 文件 内部 所有函数外,使用关键字 static
静态无链接 静态 块内,使用关键字 static

2、变量

我们根据不同的存储类别,来区分定义不同的变量。

根据不同的存储类别来区分定义不同的变量.png
2.1、自动变量

属于自动存储类别的变量具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。

块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量。
变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失自动变量占用的内存为一个可重复使用的暂存区 (栈区 stack)

int autoVariable(int count)
{
    int age = 0;//age 的作用域
    {
        int m;//age 和 m 的作用域
        for (m = age; m < count; m++)
        {
            printf("m -- %d \n",m);
        }
    }
    return age;//age 的作用域,m 已消失
}

如果内层块中声明的变量与外层块中的变量同名会怎么样?内层块会隐藏外层块的变量定义,但是离开内层块后,外层块变量的作用域又回到了原来的作用域。

2.1.1、存储类别说明符auto

为了更清晰的表达程序意图,如:为了表明有意覆盖一个外部变量定义、或者强调不要把该变量改为其它存储类别,可以显示使用关键字auto

void autoVariable(void)
{
    auto int age;
}

注意:关键字auto 在 C 中与 C++ 中用法不同,若要编写 C/C++ 兼容的程序,需谨慎使用 auto

2.1.2、自动变量的初始化

自动变量不会初始化,除非显示初始化它。

void autoVariable(void)
{
    int m;
    int n = 9;
}

变量 n 被初始化为 5,但是 m 变量的值是之前占用分配给 m 的空间的任意值。

2.2、寄存器变量

寄存器变量存储在 CPU 的寄存器中,或者说存储在最快的可用内存中。

一般来说,寄存器变量和自动变量都一样,都是块作用域、无链接、自动存储期。与其他变量相比,访问和处理寄存器变量的速度更快;但是寄存器变量存储在寄存器而非内存中,所以无法获取寄存器变量的地址。

我们使用使用存储类别说明符 register声明寄存器变量:

void blockArea(register int a)
{
    register int conut;
}

声明变量为 register 类别与直接命令相比,更像是请求。
编译器必须根据寄存器或最快可用内存的数量来衡量该请求;可能忽略该请求,在这种情况下,寄存器变量变为普通变量,但是仍不能对该变量使用地址运算符。

声明为 register 的数据类型有限:如寄存器没有足够大的空间来存储double 类型的值。

2.3、块作用域的静态变量

静态变量:听起来像是一个不可变的变量,但实际是该变量在内存中原地不动。也就是说一个静态变量的内存地址不变,而非内存地址存储的值不变。

具有文件作用域的变量自动具有静态存储期,也必须是静态存储期。

我们在前文提过:块作用域变量使用关键字static修饰,则具有静态存储期;这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说:这种变量具有块作用域、无链接、静态存储期
计算机在多次函数调用之间会记录它们的值,在块中提供块作用域和无链接以存储类别说明static声明这种变量。
块作用域的静态变量 也可以称为 “局部静态变量”。

void trystat(int index);

int main(int argc, const char * argv[]) {

    for (int i = 1; i <= 3; i++){
        trystat(i);
    }
    return 0;
}

void trystat(int index)
{
    int partVar = 1;//运行时行为: 每次调用该函数都会执行这条声明

    /* 块作用域的静态变量 partStaticVar 的声明:
     * 并未在运行时执行
     * 在程序被载入内存时已执行完毕
     * 放在函数中仅仅是为了告诉编译器只有在该函数才能访问该变量
     */
    static int partStaticVar = 1;
    printf("第 %d 次调用:partVar = %d and partStaticVar = %d\n" , index, partVar++, partStaticVar++);
}

运行该程序,获得输出:

第 1 次调用:partVar = 1 and partStaticVar = 1
第 2 次调用:partVar = 1 and partStaticVar = 2
第 3 次调用:partVar = 1 and partStaticVar = 3

块作用域的静态变量partStaticVar保存了它被递增 1 后的值,但是自动变量partVar每次都是 1。这表明了初始化的不同:每次调用 trystat() 函数,都会初始化自动变量partVar,但是块作用域的静态变量partStaticVar只在编译 trystat() 函数时被初始化一次。
如果未显示初始化静态变量,它们会被初始化为 0。

观察这两条声明类似:

int partVar = 1;
static int partStaticVar = 1;

第 1 条声明是trystat() 函数的一部分,每次调用该函数都会执行这条声明,这是 运行时行为
第 2 条声明并不是trystat() 函数的一部分,通过逐步调试可以发现:程序似乎跳过了这条声明。因为静态变量和外部变量在程序被载入内存时已执行完毕;把这条声明放在trystat() 函数中仅仅是为了告诉编译器只有在该函数才能访问该变量。这条声明并未在运行时执行。

注意: 不能在函数形参中使用static

void trystat(static int index);//不被允许:函数声明符中的存储类说明符无效
//error: Invalid storage class specifier in function declarator
2.4、外部链接的静态变量

外部链接的静态变量 具有 文件作用域、外部链接、静态存储期。该类别称之为:外部存储类别;属于该类别的变量称为 外部变量。

2.4.1、定义 和 声明

考虑下面的例子:

int externalVar = 1;//定义式声明

int main(int argc, const char * argv[]) {
    extern int externalVar;//引用式声明
    return 0;
}

该例中,外部变量externalVar被声明了两次:

  • 第一次声明为变量externalVar预留了内存空间,该声明构成了变量externalVar的定义,称为“定义式声明”。
  • 第二次声明告诉编译器使用之前已创建的externalVar变量,称为“引用式声明”。

关键字extern表明该声明不是定义,它指示编译器去别处查询其定义。因此,不要使用extern创建外部定义。

2.4.2、初始化外部变量

外部变量和自动变量类似,可以被显式初始化;与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为 0。

只能使用常量表达式初始化文件作用域变量:
int externalVar = 1;
int externalVarB = externalVar * 2;
//Initializer element is not a compile-time constant
外部变量只能初始化一次,且必须在定义该变量时进行:
int externalVar = 1;//定义式声明

int main(int argc, const char * argv[]) {
    extern int externalVar = 2;//引用式声明
    //error:'extern' variable cannot have an initializer
    return 0;
}
2.4.3、使用外部变量

我们在文件GlobalVar.c中声明和定义一个外部变量:

#include "GlobalVar.h"

int externalVar = 1;//定义式声明

我们在文件main.c中使用一个外部变量:

extern int externalVar;//引用式声明

int main(int argc, const char * argv[]) {
    printf("externalVar === %d \n",externalVar);
    return 0;
}

如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern在该文件中声明该变量。

在执行块中的语句时,块作用域中的变量将“隐藏”文件作用域中的同名变量。

2.5、内部链接的静态变量

内部链接的静态变量具有静态存储期、文件作用域和内部链接。在所有函数外部(和外部变量类似),用存储类别说明符static定义的变量具有这种存储类别:

int externalVar = 1;//定义式声明:静态变量,外部链接
static int innerlVar = 1;//静态变量,内部链接

int main(int argc, const char * argv[]) {

    extern int externalVar;//使用定义在别处的 externalVar
    extern int innerlVar;//使用定义在别处的 innerlVar,并未改变内部链接属性
    return 0;
}

普通的外部变量可用于同一程序任意文件中的函数,但是内部链接的静态变量只能用于同一文件中的函数。

使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量,这样的声明并不会改变其链接属性:

  • 变量externalVar可用于同一程序任意文件中的函数;
  • 变量innerlVar虽然使用存储类别说明符extern修饰,但是并未改变内部链接属性。

通过前文的讲述,我们应该明白关键字staticextern的含义取决于上下文。

2.6、存储类别说明符

C 语言有 6 个关键字作为存储类别说明符:

  • auto 表明变量是自动存储期、只能用于块作用域的变量声明中。由于块中声明的变量本身具有自动存储期,所以使用auto 主要是为了明确表达要使用与外部变量同名的局部变量的意图。
  • register 只用于块作用域的变量,它把变量归为存储期类别,请求最快速度访问该变量;同时,还保护了该变量的地址不被获取。
  • static 创建的变量具有静态存储期,载入程序时创建,当程序结束时消失。如果static 用于文件作用域声明,作用域受限于该文件;如果static 用于块作用域声明,作用域受限于该块。因此,只要程序在运行变量就存在并保存其值,但是在执行块内的代码时,才能通过标识符访问。
  • extern 表明声明的变量定义在别处。如果包含extern 的声明具有文件作用域,则引用的变量必须具有外部链接;如果包含extern 的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。
  • _Thread_local 可以和externstatic 一起使用;在大多数情况下,不能在声明中同时使用多个存储类别说明符。
  • typedef 与任何内存存储无关,由于语法的原因将它归为存储类别说明符。

3、存储类别和函数

函数的存储类别.png

函数也有存储类别,可以是外部函数(默认)、静态函数、内联函数:

  • 外部函数:可以被其他文件的函数访问
  • 静态函数:只能用于其定义所在的文件
//默认为外部函数
int sumFunction(int x,int y);

/* 外部函数
 以存储类别说明符extern声明定义在其它文件中的函数;
 这样表明了当前文件使用的函数被定义在别处
 */
extern int cubicFunction(int x);

/* 静态函数
 以存储类别说明符static创建的函数属于该文件私有;
 这样避免了名称冲突的问题;
 在其它文件可以使用与之同名的文件
 */
static int squareFunction(int x);

4、分配内存:malloc()free()

所有程序都必须预留足够的内存来存储程序使用的数据。
在前文学习到的存储类别都是:在确定用哪种存储类别后,根据已制定好的内存管理规则,将自动选择其作用域和存储期:

  • 具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,在退出该块时释放之前分配的内存;如果未初始化,自动变量是垃圾值。
  • 具有静态存储期的变量,程序在编译时为其分配内存,并在程序运行中一直保留该内存;如果未初始化,该变量被设置为 0。

然而,还有更灵活的选择,在程序运行时使用库函数分配和管理内存;主要的函数为malloc()free() :

void    *malloc(size_t __size);
void    free(void *);

malloc()函数传入一个参数:所需的内存字节数;该函数会找到合适的空闲内存块,这样的内存块是匿名的。也就是说:malloc()分配内存,但是不会为其赋名。然而,它返回动态分配内存块的首字节地址;因此,可以把该地址赋值给一个指针变量,并使用指针访问这块内存。如果malloc()分配内存失败,将返回空指针。

int *lengths;
//malloc() 返回的地址赋值给一个指针变量
lengths = (int *)malloc(sizeof(int));
...
//malloc()一般和 free()配套使用
free(lengths);

free() 函数的入参是之前使用malloc()分配内存得到的内存地址,该函数释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。

4.1、内存泄漏

我们知道:具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,在退出该块时释放之前分配的内存;具有静态存储期的变量,程序在编译时为其分配内存,并在程序运行中一直保留该内存。
但是动态分配的内存数量只会增加,除非调用free()释放内存。如果没有调用free()释放内存,内存数量慢慢增加,可能耗尽内存。这类问题称为内存泄漏。

4.2、calloc()函数

分配内存还可以使用calloc()函数:

void    *calloc(size_t __count, size_t __size);

该函数接受两个参数:

  • size_t __count 所需的存储单元数量
  • size_t __size 存储单元大小

我们使用calloc()函数创建了 10 个 sizeof(int)字节的存储单元

int *lengths = calloc(10,sizeof(int));
...
free(lengths);

同样的。我们需要使用调用free()释放内存。

5、内存分配区域

存储类别和动态内存分配有何关系?我们姑且认为程序把它可用的内存分为 3 部分:

  • 一部分供静态变量使用:静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可以访问该部分数据。该类别的变量在程序程序开始执行时创建,在程序结束时被销毁。
  • 一部分供自动变量使用:自动存储类别的变量在程序进入变量定义所在的块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也在相应的减少。这部分的内存通常作为栈来处理:新创建的变量按顺序加入内存,然后以相反的顺序销毁
  • 一部分供动态内存分配使用:这部分内存支离破碎,未使用的内存块分部在已使用的内存块之间,通常称为内存堆。使用动态内存通常比使用栈内存慢。

总问言之,程序把静态对象、自动对象和动态分配的对象存储在不同的区域:

程序的内存区域分配.png
  • 堆区 heap:堆是由程序员使用malloc()分配和free()释放,用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用malloc()等函数分配内存时,新分配的内存就被动态添加到堆上 (堆被扩张);当利用free()释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
  • 栈区 stack:栈是由编译器自动分配并释放,用户存放程序临时创建的局部变量,存放函数的参数值,局部变量等。也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。除此以外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也回被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把栈看成一个临时数据寄存、交换的内存区。
  • 全局静态区:数据区:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
  • 全局静态区:BSS区:BSS段包含了程序中未初始化全局变量。

6、动态分配内存和变长数组

变长数组VLA 和调用malloc()类似,两者都可用于在运行时确定数组大小,不同点:

  • 变长数组是自动存储类型,存储在栈中;程序离开数组定义所在的块时,占用的内存空间被自动释放,不必使用free()释放
  • malloc()创建的数组存放在内存堆中,需要调用free()释放。
int n = 5,m = 6;
int array[n][m];// n * m 的变长数组VLA
int (* p)[6];//C99 之前的写法
int (* q)[m];//变长数组
p = (int (*)[6])malloc(n * 6 * sizeof(int));//n * 6数组
q = (int (*)[m])malloc(n * m * sizeof(int));//n * m数组,变长数组
//由于 malloc() 返回一个指针,所以 p、q 必须是一个指向合适类型的指针

相关文章

  • c++内存管理

    c++内存管理长文 c++内存管理

  • 内存管理

    内容包括: C++内存管理 Java内存管理 C++内存管理 内存分配方式 在C++中,内存分成5个区,分别是栈、...

  • Java GC

    概述 GC => 垃圾回收 = 回收可用空间 + 压缩内存 内存管理 手动内存管理 => C | C++ 自动内存...

  • Redis源码:内存管理与事件处理

    Redis内存管理 Redis内存管理相关文件为zmalloc.c/zmalloc.h,其只是对C中内存管理函数做...

  • iOS的内存管理

    Objective-C需要自己考虑内存管理,但是如果熟悉Objective-C的内存管理机制,手工管理内存其实也并...

  • Objective-C 内存管理基础

    前言 之前的两篇拙文C语言-内存管理基础、C语言-内存管理深入 介绍了关于C语言在内存管理方面的相关知识。但是对于...

  • 学了指针没学动态内存一切都白搭!C语言基础教程之内存管理

    C语言内存管理 本文将讲解 C 中的动态内存管理。C 语言为内存的分配和管理提供了几个函数。这些函数可以在头文件中...

  • C++之内存布局

    在C++之内存管理一文中,我们已经了解到C++的内存管理,这里介绍C++的典型内存布局结构。 1、总体来说,C/C...

  • Swift中的内存管理

    1、内存管理,weak和unowned2、@autoreleasepool3、C 指针内存管理 1、内存管理,we...

  • 第二章 Java内存区域和内存溢出异常

    概述 java内存管理相比于C和C++自己管理内存方便了很多,不用自己手动去管理和释放内存,不必为每一个对象...

网友评论

      本文标题:C 内存管理

      本文链接:https://www.haomeiwen.com/subject/ljmguftx.html