C语言中储存类别与内存管理的深入理解
作者:黑尾鸥
储存类别
C语言提供了多种储存类别供我们使用,并且对应的有对应的内存管理策略,在了解C中的储存类型前,我们先了解一下与储存类型相关的一些概念。
1. 基础概念
对象:不同于面向对象编程中的对象的含义,C语言是面向过程编程,不存在这样对象的概念,这个对象指的是值储存所占据物理内存空间。
左值:左值是可以指定对象的表达式,它的最简单形式即为标识符,复杂的可以为为指针之类。一个表达式成为左值的前提是它确实指定了一块作为对象的储存空间,例如:
int a = 1;//a作为标识符,也作基础表达式,指定了一个对象,是左值 int *pa = &a;//pa同a也指示了一个储存地址对象,是一个左值;*pa是一个表示式,指示了a相同的对象也是一个左值 int arr[5] = {0}; arr+a*3;// 这段表达式就不是一个标识符,也不是一个左值了,因为它没有指定内存上的任意位置 *(arr + a * 3);// 不同于上面的,这也是一个左值,因为它确实指定了内存上的位置
左值分为可修改左值和不可修改左值。
我们通常用储存期来描述对象,表明对象在内存中留存的时间。用标识符指定对象时,使用作用域和链接来描述标识符,其中作用域表明标识符可以可以被程序使用的范围,链接表明程序的哪些其他文件也可以使用它。
不同的储存类别之间的区别即在于它们的储存期、作用域和连接形式的不相同。我们来分别了解他们一下。
储存期:储存期分为静态储存期,自动储存期,线程储存期和动态分配储存期(线程储存期暂时不多赘述),它们分别对应不同的在内存中的储存位置,也有不同的特点。
静态储存期:对应静态存储位置,它在程序开始运行时就被分配,这段空间不可增加和减少,所以从程序开始运行到停止运行,静态储存期的数据一直存在。通常在函数外的变量和static表示的变量具有静态储存期。
自动储存期:对应栈空间,它随着程序的运行可以自动进行分配,增加或减少。程序进入到一个块为其中的变量分配栈空间,退出一个块后则会释放相应的空间。一般的创建的变量都具有自动储存期。
动态分配储存期:对应堆空间,它需要通过特殊的语法进行申请,申请后也需要主动进行销毁,存在时间为从申请内存开始到主动释放内存为止。需要通过专门的语句来获得具有动态分配储存期的变量。
作用域:一个变量能被使用的范围称为作用域,作用域分为 块作用域、函数作用域、函数原型作用域和 文件作用域。
块作用域:由一个花括号开始到与之对应的花括号为止,其中的变量都具有块作用域,一般情况下任何在块内的定义的变量可以在块内任何位置使用,但是不可以在块外进行使用。(特例后面会举出),而且对于内部块也可以定义与外部块同名的变量,这时候内部块将隐去向内隐去外部块的同名变量,在内部使用自己定义的该变量。
函数作用域:针对的是goto语句标签,一个标签首次出现在含糊内层,它的作用域将会延伸至整个函数,这表示我们不能使用同名的标签。
函数原型作用域:对于函数的声明,该作用域开始与形参定义处知道函数函数原型结束。编译器只注重形式参数的类型而不会注意具体的变量名,甚至可以不使用变量名。
文件作用域:声明在函数外的变量具有文件作用域,他们可以在同一源文件下的任何块和函数中使用,具有文件作用域的变量也被称为全局变量。
对于分别属于同类型作用域但是不同一个作用域的变量它们可以任意重名,例如不同块的函数中变量属于不同块他们可以重名。对于具有文件作用域的变量它们对于所属的文件块都有作用,所以不建议块中变量与全局变量重名,但是在重名后块使用对应名称变量时将以块中自身定义的变量为准。
#include <stdio.h> void showA(int a, int type) { switch (type) { case 1: printf("outer : "); break; case 2: printf("inter : "); break; case 3: printf("circle : "); default: ; } printf("a = %d\n", a); } int main (void) { int a = 1; showA(a, 1); { int a = 2; showA(a, 2); } showA(a, 1); while (a++ < 5) { int a = 5; showA(a, 3); } showA(a, 1); return 0; } /** outer : a = 1 inter : a = 2 outer : a = 1 circle : a = 5 circle : a = 5 circle : a = 5 circle : a = 5 outer : a = 6 * **/
我们发现在在外部a为在外部定义的值,a输出为1;第一块内部,a读取的是内部的a的值,这一点没有任何问题;然后我们到外部,我们再让程序输出a值,仍然为2,没有问题;但是进入循环后,我们发现很奇怪的现象,通过输出我们发现循环执行了4次,很明显这是基于外部的a,但是内部的a在输出时却总是显示内部的a,这一点是因为:内部循环定义的a作用域只在块内,并不会作用于循环条件判断的部分,所以在进行循环条件判断时始终使用外部的a。注意递增条件一定要在循环判断条件中,否则循环将变成死循环。但是,没有必要使用同名变量。
链接:链接是程序中变量可以被其他文件使用的描述,有三种类型的链接:外部链接、内部链接和 无链接。
如果一个变量具有文件作用域它才可能具有外部链接和内部链接,其他变量都是无链接的。在具体了解内部链接和外部链接之前,我们先理解下 翻译单元的概念。
翻译单元:我们经常使用#include指令来包含头文件,C通过预处理将对应头文件内容直接替换掉该条命令,他们虽然表面上看起来不是一个文件但是被编译器看做了一个文件,这个文件就被称为一个翻译单元,一个具有文件作用域的变量它的实际可见范围就是整个翻译单元。一个翻译单元由一个源文件和多个它所包含的文件组成。
所以外部链接可以在多文件程序中使用,而内部链接只可以在一个翻译单元使用。区别二者在于是否使用了储存类别说明符static,使用了static则为内部链接,反之则为外部链接。
//main.c 文件 #include <stdio.h> int main (void) { extern int a;// 声明,让编译器在别处查找a的定义 // extern int b; // printf("b = %d", b);这一段不可使用,因为b只具有内部链接,不可在其他源文件访问,运行 // 报错 printf("a = %d", a); return 0; } /** a = 5 * **/ // 和它一同编译的another.c文件 int a = 5;// 具有外部链接,可以在多个源文件之间进行共享 static int b = 2;// 具有内部链接,只能在一个源文件内共享
在这里我们使用了外部链接变量a,在两个翻译单元之间实现了变量的传递。其中main.c文件为了调用变量a必须有extern声明语句,这段语句声明了一个int型变量a但是并不会为它分配内存,使用它只是为了告诉编译器在别处寻找变量a的定义,这是必不可少的,否则程序将会报错。
// main.c文件 #include <stdio.h> #include "main.h" int main (void) { extern int a; printf("a = %d", a); return 0; } /** a = 7 * **/ // main.h文件 static int a = 7;
不同于源文件,对于头文件,通过#include指令包含头文件,编译器将自动将对应文件内容替代到对应位置,它们属于同一个翻译单元,所以及时具有内部链接的变量仍可以使用。
2. 储存类别分类
介绍了一些基础概念后我们来根据这些基础概念对于储存类别进行分类:
储存类别 | 储存期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内声明 |
寄存器 | 自动 | 块 | 无 | 块内声明,加入关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 函数外,加入关键字static |
静态无链接 | 静态 | 块 | 无 | 块内声明,计入关键字static |
下面我呢来分别具体对于每种类型所对应的变量进行说明。
3. 自动变量
自动变量具有自动储存期,块作用域,无链接,在块内进行声明即可。自动储存期意味着它在开始执行块时被创建,在对应块到结尾时被销毁,不能再被通过任何途径访问;块作用域表明只能在块中使用变量名对于变量进行访问,但是在处于变量可使用的储存期内(这点必要,因为我们无法控制编译器的回收机制),我们也可以通过指针传递地址的方式来继续使用;无链接表明不能再其他文件中对于该变量进行访问。
对于自动变量,默认情况下声明的变量都具有这样的储存类别,但是有时候为了更明显的表现意图,并且在外部具有同名变量时,为了更好覆盖它,表明自己变量的自动储存类型,可以使用关键字auto,例如:
#include <stdio.h> int a = 1; int main (void) { auto int a;// int a;也是等价 printf("a = %d", a); return 0; }
在外部有同名变量时,使用auto关键字还是用来标识a作为块内的自动变量,覆盖外部的a,即使不加auto也是可以的,但是使用后可以起到更好的标识性。
4. 寄存器变量
寄存器变量在多个方面与自动变量相同,不同在于自动变量通常储存在计算机内存中,而寄存器变量储存在计算机CPU的寄存器中,因此它具有高效的运算率,而且因为它在寄存器中所以无法获得其地址。通过在变量定义中使用register修饰既可以声明寄存器变量:register int a;
但是,值得注意的是,使用register企图创建寄存器变量是一种请求而不是命令,编译器很可能不会通过你的请求,而且寄存器也有可能没有足够大空间储存double类型变量,所以可以声明register的数据类型也是有限的。即使失败,也能够创建相应的自动变量,但是我们仍然不能获得其地址。
5. 具有块作用域的静态变量
具有块作用域的静态变量,对应的储存类别为静态无链接,其具有静态储存期,块作用域和无链接。它在程序开始运行时被创建,程序结束后被销毁。只在它被定义的块内调用,无法被其他文件访问。
相较于自动变量,它只是拥有了静态储存期,所以我们使用static类别修饰符获得该类型变量static int a;。值得注意的是,由于该变量具有静态储存期,所以它始终储存在系统中的某一段内存空间中,我们可以利用指针在块外的区域对于该变量进行访问。
#include <stdio.h> int* fun(); int main (void) { int *p = fun(); *p += 2; fun(); return 0; } int *fun() { static int a = 1; printf("a = %d\n", a); return &a; } /** a = 1 a = 3 * **/
通过上面的运行结果我们发现通过函数返回指向静态变量的指针,我们可以对静态变量进行访问和修改,这使得我们在块外对块内无链接的静态变量进行访问。
6. 内部链接的静态变量
内部链接的静态变量对应储存类别为静态内部链接。它具有静态作用期,文件作用域和内部链接。它在程序开始被创建,在同一个翻译单元内可以任意访问。在前面已经有它的用例。
7. 外部链接的静态变量
外部链接的静态变量对应储存类别为静态外部链接。它具有静态作用器,文件作用域和外部链接。大体上与内部链接的静态变量相同,但是它可以在多个翻译单元(多个源文件之间)进行共享。
但是仍有一些事项注意:
声明时可以显示初始化,但是必须使用常量进行初始化(对于sizeof表达式也是常量),如果未进行初始化,无论如何它将被初始化为0。
#include <stdio.h> int a; int b = 3; int c = 3 * sizeof b; // int d = 3 * a;非常量无法初始化 char d; int main (void ) { printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); printf("d = %d d = %c\n", d, d); return 0; } /** a = 0 b = 3 c = 12 d = 0 d = * **/
如何跨文件使用具有外部链接的变量?正常情况下直接使用将会报错,我们需要通过引用性声明来使用,通过extern关键字来实现,在变量声明前加上关键字,编译器就会明白根据指示在其他源文件中查找变量的声明,这样的声明不会申请新的内存,也不可以进行赋值。
// main.c #include <stdio.h> int main (void ) { // extern int a = 1;是不能赋值的 extern int a; printf("a = %d\n", a); return 0; } /** a = 5 * **/ // anothor.c int a = 5;
8. 储存类别说明符小结
C中储存类别说明符有多个,并且不同说明符在不同位置也有不同意义。一共有一下说明符:auto、extern、register、static、_Thread_local和typedef。最后者被归为此类属于一些语法上的原因。它们大多都是单独使用,但是_Thread_local可以和static和extern一起使用。
auto表明变量具有自动储存期,在块内的变量默认具有自动储存期,使用auto只是明确表示要使用与外部变量重名的局部变量的意图。
register表明希望将变量储存在寄存器中,希望以最快的速度读取变量,同时不希望获得该变量的地址。
static表明变量具有静态储存期,它并不改变块内变量的链接类型,但是对于快外的变量,将会限制它的链接类型为内部链接。
extern表明该变量定义在别处,希望编译器在别处查找,包含extern的声明具有文件作用域,那么变量的定义一定有外部链接;如果只是具有块作用域,那么变量的定义可以有内部链接,也可以外部链接。
9. 储存类别的选用
到最后了,我们来考虑下储存类比的选用,一般情况下我们只建议使用自动储存类别,使用外部变量在程序间通信是方便的但同时也是危险的,所以我们希望尽力避免使用,同时我们要明白保护性程序设计的“按需知道”法则,尽量在函数内部解决该函数的任务,只共享哪些必须共享的变量。
动态内存管理
C语言除了自身建立了自动储存类型和静态储存类型来进行自主的内存管理来方便我们编程,同时也给我提供了一些工具是的我们能够进行灵活的内存使用和管理,我们通过了解使用C语言的内存分配来具体了解。
1. 内存分配之malloc
malloc函数声明在头文件stdlib.h中,其函数原型为
void* malloc(size_t size);
我们通过参数size(单位字节)来获得指定大小的一段空间同时返回指向该空间的空指针,我们可以通过强制类型转换来获得我们需要类型的指针,如果申请内存失败,它就会返回空指针NULL。我们通过具体的用例来了解它的使用:
#include <stdio.h> #include <stdlib.h> int main (void) { int *a = (int *)malloc(sizeof(int));// 创建一个int int *b = (int *)malloc(sizeof(int)*5);// 创建长度为5的数组 *a = 4; for (int i = 0; i < 5; ++i) b[i] = i*i; printf("*a = %d\n", *a); for (int i = 0; i < 5; ++i) printf("b[%d] = %d\n", i, b[i]); free(a); free(b); return 0; } /** *a = 4 b[0] = 0 b[1] = 1 b[2] = 4 b[3] = 9 b[4] = 16 * **/
我们发现可以通过内存申请可以灵活的创建变量和数组,然后对他们进行访问和修改,但是千万不要忘记调用free函数接受被分配空间的指针,来释放对应空间。不然大量的空间将无法被再利用造成内存的浪费,同时一些操作系统在程序运行结束后可能不会自动释放这些被分配的内存,甚至可能耗尽内存,产生可怕的 内存泄漏。
同时通过动态分配内存也有更加灵活的用途,例如创建变长数组:
#include <stdio.h> #include <stdlib.h> int main (void) { int len; scanf("input the len: %d", &len); int *arr = (int *)malloc(sizeof(int) * len); return 0; }
通过这样一段程序我们就实现了,创建用户输入数字大小的整形数组。
2. 内存分配值calloc
calloc函数与malloc函数功能大体相同,它的函数原型:
void *calloc(size_t num, size_t size);
接受两个参数,第一个为希望分配的该大小的内存块数,第二个为希望一个空间大小(单位字节)。同样的我们要求在每次使用过后通过free函数将对应指针指向的分配的空间进行释放。
储存类别和动态内存分配
储存类别和内存分配有着密切可分的关系,我们来讲述一个理想中的模型:
程序将它的内存分为三个部分,一部分给静态变量使用;一部分给自动变量使用,最后一部分提供给动态内存分配。为什么这样分配呢?
静态变量使用的内存在程序编译时确定,在程序运行的整个周期都可以别访问,最后在程序结束时被销毁,所以我们可以单独使用一块区域对于其进行管理。
自动变量在进入对应变量定义的块时被分配空间,在离开块后被销毁。伴随着函数的调用和结束,自动变量使用的内存数量也对应的增加和减少,这部分内存通常使用栈来管理,新建的变量将按顺序入栈,销毁时按照相反的方向进行销毁。
使用动态分配内存的内容,他们创建于malloc或者calloc函数被调用的时候,在调用free函数被销毁,这些内存完全依赖于程序员自身的管理,我们可以在一个函数中创建它然后在另一个函数中销毁它。这样就使得这样的一部分内存被分配的支离破碎,有可能未分配的内存处于分配的内存之间,使用这样的内存往往是比使用栈来的更慢的。
我们通过一个程序来更好了解变量处于的空间:
#include <stdio.h> #include <stdlib.h> static int a = 1; static int b = 2; int main (void) { static int c = 3; int d = 4; int e = 5; int f = 6; int *p1 = (int *)malloc(sizeof(int)); int *p2 = (int *)malloc(sizeof(int)); int *p3 = (int *)malloc(sizeof(int)); printf("static: %p\n", &a); printf("static: %p\n", &b); printf("static: %p\n", &c); printf("auto: %p\n", &d); printf("auto: %p\n", &e); printf("auto: %p\n", &f); printf("dynasty: %p\n", p1); printf("dynasty: %p\n", p2); printf("dynasty: %p\n", p3); return 0; } /** static: 00405004 static: 00405008 static: 0040500C auto: 0061FF10 auto: 0061FF0C auto: 0061FF08 dynasty: 00791930 dynasty: 007918B8 dynasty: 007918C8 * **/
可以发现不同类型的变量储存在不同地址附近。
总结
对于C中的变量我们可以通过类型和储存类别来描述,除此之外C新的标准也更新了一些特殊的修饰符const之类来修饰,灵活的使用他们能让程序运行的更有效率,实现更多的功能。
到此这篇关于C语言中储存类别与内存管理的文章就介绍到这了,更多相关C语言储存类别与内存管理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!