前几天技术面试,发现C语言还有许多平时不常用的关键字的使用自己还不会,特别是auto,答得一塌糊涂,因此通过上网搜索,总结一下有关存储类修饰符的解释与使用,有关存储类修饰符static,auto,register,extern,typedef的使用,可根据需求直接点击目录达到相应位置.
先瞧瞧C语言的内存分配
内存分配
随便写一个C语言程序,然后生成目标运行二进制文件,然后在当前目录下运行,就会返回这个二进制文件的构成,上面是区的名,下面是给它分配的大小
size test.exe //Windows
//或
size test //Linux
运行前:
以Windows为例(别看路径,这是一个C程序不是C++):
text:
代码段,存储你写的代码(指令),是只读但共享的.
只读是为了防止程序运行时意外修改了他的指令.
共享是为了其他其他合法程序可以调用运行他.
data:
数据区,该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(全局静态变量)和常量数据(如字符串常量)。
bss:
未初始化数据区:未初始化的全局变量和静态变量(int a, static int a),在程序执行前会被初始化为0或NULL.
后面几个是文件所占字节数大小,dec是十进制,hex是十六进制.最后就是文件名.
进行运行命令后,会把该二进制文件加载到内存中,此时会为这个进程分配内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
运行时:
请好好牢记这个图,很重要.
代码区(text segment)
存放程序的二进制代码.所有的可执行代码都加载到代码区,这块内存是在运行期间是不可修改的。
数据段/静态存储区(data segment):
主要存放全局变量和静态变量。分成了以下几部分:
已初始化数据段.data:已初始化的全局变量和静态变量(int a = 0, static int a = 0)
只读数据段.rodata:只读常量(const , "zifuchuan")
未初始化数据段.bss:未初始化的全局变量和静态变量(int a, static int a)
堆区(heap)
不是从磁盘文件加载到内存的,这段空间称为堆(Heap),堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
用malloc函数动态分配内存时是在这里分配的。从堆空间结束地址开始是共享库的映射空间,每
个共享库也分为几个Segment,每个Segment有不同的访问权限。可以看到,从堆空间的结束
地址到共享库映射空间的起始地址之间有很大的地址空洞,
在动态分配内存时堆空间是可以向高地址增长的。堆空间的地址上限称为Break,堆空间要向高地址增长就要抬高Break,映射新的虚拟内存页面到物理内存,这是通过系统调用brk实现的,malloc函数也是调用brk向内核请求分配内存的
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
在图其中高地址的部分保存着进程的环境变量和命令行参数,低地址的部分保存函数栈帧,栈空间是向低地址增长的,但显然没有堆空间那么大的可供增长的余地,因为实际的应用程序动态分配大量内存的并不少见,但是有几十层深的函数调用并且每层调用都有很多局部变量的非常少见。总之,栈空间是可能用尽的,并且比堆空间更容易用尽.
先等等,这文章不是直接告诉你这些关键字的作用,还会讲一下它的属性,这样会更好地理解其作用.
为了理解,再看看关键字的属性
1.作用域(Scope)
关键字的作用范围.这个概念适用于所有标识符,而不仅仅是变量,C语言的作用域分为以下几类:
1.函数作用域(Function Scope)
标识符在整个函数中都有效。只有语句标号属于函数作用域。标号在函数中不需要先声明后使用,在前面用一个goto语句也可以跳转到后面的某个标号,但仅限于同一个函数之中。
2.文件作用域(File Scope)
标识符从它声明的位置开始直到这个程序文件的末尾都有效。main函数外面的全局变量,还有main也算,printf其实是在stdio.h中声明的,被包含到这个程序文件中了,所以也算文件作用域的。
3.块作用域(Block Scope)
标识符位于一对{}括号中(函数体或语句块),从它声明的位置开始到右 '}' 括号之间有效。如:if语句里定义的变量,函数定义中的形参,从声明的位置开始到函数末尾之间有效。
4.函数原型作用域(Function Prototype Scope)
标识符出现在函数原型中,这个函数原型只是一个声明而不是定义(没有函数体),那么标识符从声明的位置开始到在这个原型末尾之间有效。例如:
int foo(int a, int b);中的a和b。
对属于同一命名空间的重名标识符,内层作用域的标识符将覆盖外层作用域的标识符,例如局部变量名在它的函数中将覆盖重名的全局变量。
2.命名空间(Name Space):
对属于同一命名空间(Name Space)的重名标识符,内层作用域的标识符将覆盖外层作用域的标识符,例如局部变量名在它的函数中将覆盖重名的全局变量
1.语句标号
语句标号单独属于一个命名空间。例如在函数中局部变量和语句标号可以重名,互不影响。由于使用标号的语法和使用其它标识符的语法都不一样,编译器不会把它和别的标识符弄混。如:
#include <stdio.h>
int main()
{
int label = 1;
goto label;
printf("hello world\n");
label:
printf("变量label = %d\n", label);
return 0;
}
其中 label 既是 int 变量,又是一个语句标号.可以同时使用而不会出现报错
2.struct,enum和union
struct,enum和union的类型Tag属于一个命名空间。由于Tag前面总是带struct,enum或union关键字,所以编译器不会把它和别的标识符弄混。
3.struct和union的成员名
struct和union的成员名属于一个命名空间。由于成员名总是通过.或->运算符来访问而不会单独使用,所以编译器不会把它和别的标识符弄混。
2,3举例:
#include <stdio.h>
struct test1
{
int test1;
int test2;
};
enum test2{
test1,
test2,
test3,
};
union test3
{
char test1;
int test2;
double test3;
};
int main()
{
int test1 = 1, test2 = 2, test3 = 3;
struct test1 a = {.test1 = 11, .test2 = 22};
enum test2 b = test2;
union test3 c;
c.test3 = 1.23;
printf("结构体成员 test1 = %d, 变量 test1 = %d \n", a.test1, test1);
printf("枚举变量 test2 = %d, 变量 test2 = %d \n", b, test2);
printf("联合体成员 test3 = %f, 变量 test3 = %d \n", c.test3, test3);
}
可发现互不影响,不会报错.
4.所有其它标识符
例如变量名、函数名、宏定义、typedef的类型名、enum成员等等都属于同一个命名空间。如果有重名的话,宏定义覆盖所有其它标识符,因为它在预处理阶段而不是编译阶段处理,除了宏定义之外其它几类标识符按上面所说的规则处理,内层作用域覆盖外层作用域
#include <stdio.h>
#define HELLO hello
int main()
{
int HELLO = 1;
printf("HELLO = %d\n", HELLO);
printf("hello = %d\n", hello);
}
3.链接属性(Linkage)
若最终的可执行文件由多个程序文件链接而成,而且一个标识符在任意程序文件中声明,此时根据是否表示同一函数或变量来区分它的链接属性.
1.外部链接(External Linkage)
如果最终的可执行文件由多个程序文件链接而成,一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有External Linkage。具有External Linkage的标识符编译后在符号表中是GLOBAL的符号。
2.内部链接(Internal Linkage)
如果一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有Internal Linkage。例如在main函数外面的变量a。如果有另一个foo.c程序和main.c链接在一起,在foo.c中也声明一个static int a;,则那个a和这个a不代表同一个变量。具有Internal Linkage的标识符编译后在符号表中是LOCAL的符号,但main函数里面那个变量b不能算Internal Linkage的,因为即使在同一个程序文件中,在不同的函数中声明多次,也不代表同一个变量。
3.无链接(No Linkage)
除以上情况之外的标识符都属于No Linkage的,例如函数的局部变量,以及不表示变量和函数的其它标识符.
变量的生存期
变量的生存期是指变量存在的时间,也就是从变量被创建到被销毁的时间段.变量的生存期往往与变量的作用域有关,不同类型的变量有不同的生存期.
1.静态生存期(Static Storage Duration)
具有外部或内部链接属性,或者被static修饰的变量,在程序开始执行时分配和初始化一次,此后便一直存在直到程序结束。这种变量通常位于.rodata,.data或.bss段.
2.自动生存期(Automatic Storage Duration)
链接属性为无链接并且没有被static修饰的变量,这种变量在进入块作用域时在栈上或寄存器中分配,在退出块作用域时释放。如:局部变量.
3.动态分配生存期(Allocated Storage Duration)
用户调用malloc函数在进程的堆空间中分配内存,调用free函数可以释放这种存储空间.
回到正文,开始讲解关键字.
现在只需根据定义去上面找答案就行了
1.static
指定符指定静态存储期和内部链接(除非用于块作用域)。它能用于在文件作用域的函数,以及文件和块作用域的对象,但不能用于函数参数列表.
用它修饰的变量的存储空间是静态分配的,用它修饰的文件作用域的变量或函数具有Internal Linkage.
1.修饰变量
在程序开始执行时分配和初始化一次,此后便一直存在直到程序结束.
修饰全局变量,只在本文件内访问,不能被外部文件直接访问。
2.修饰函数
限制函数作用范围为当前文件.
2.auto
指定符只对声明于块作用域的对象(除了函数参数列表)允许。它指示自动存储期与无链接,也是这种声明的默认属性.
用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返回时自动释放
#include <stdio.h>
int main()
{
auto int a = 1;
printf("自动变量auto a = %d\n", a);
}
如上面main函数里的a其实就是用auto修饰的,只不过auto可以省略不写
#include <stdio.h>
auto int b = 2;
int main()
{
printf("自动变量auto b = %d\n", b);
}
例如上例中,auto不能修饰文件作用域的变量.
说白了,auto不能修饰全局变量,只能在{}里使用,即用于修饰局部变量,而修饰局部变量时可以省略不写.
注意:
auto不能修饰全局变量
使用auto修饰变量时应初始化变量,这点同const一样.
使用auto不带变量类型修饰变量时,会初始化为默认类型int
3.extern
指定静态存储期和外部链接。它能用于文件和块作用域中的函数和对象声明(除了函数参数列表)。若 extern
出现在已经声明带内部链接的标识符的再声明上,则链接仍为内部。否则(若前一声明为外部、无链接或不在作用域内)链接为外部。
extern关键字表示这个标识符具有External Linkage.上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的,extern关键字就用于多次声明同一个标识符.
1.修饰变量
修饰的变量可被其他文件引用.
//test2.c
static int a = 11111;
int b = 22222;
//test1.c
#include <stdio.h>
#include "test2.c"
extern int a;
int main()
{
extern int b;
printf("外部变量a = %d", a);
printf("外部变量b = %d", b);
}
2.修饰函数
同修饰变量一样,可被其他文件使用.
//test2.c
static int a = 11111;
int b = 22222;
extern int add(int a, int b)
{
return a + b;
}
#include <stdio.h>
#include "test2.c"
extern int a;
int main()
{
extern int b;
printf("外部变量a = %d", a);
printf("外部变量b = %d", b);
int c = add(a, b);
printf("局部变量c = %d", c);
}
4.typedef
它并不是用来修饰变量的,而是定义一个类型名。在那一节也讲过,看typedef声明怎么看呢,首先去掉typedef把它看成变量声明,看这个变量是什么类型的,那么typedef就定义了一个什么类型,也就是说,typedef在语法结构中出现的位置和是面几个关键字一样,也是修饰变量定义的,所以从语法(而不是语义)的角度把它和前面几个关键字归类到一起.
typedef int INT32
INT32 a = 1;
//等价于:
int a = 1;
在此提一下,虽然宏定义也能实现上述效果,但有些时候宏定义会出现难以发现的错误:
//typedef:
typedef int* INT32_ptr1
//宏定义:
#define INT32_ptr2 int*
INT32_ptr1 a, b;
//等价于: int* a, * b;//此时a, b都是指针
INT32_ptr2 c, d;
//等价于: int* c, d;//此时c为指针,d为int变量
产生这样的结果是因为宏定义只是一个简单的替换而已.而typedef是别名,把别名看成新的类型
5.register
指定符只对声明于块作用域的对象允许,包括函数参数列表,它指示自动存储期与无链接(即这种声明的默认属性),但另外提示优化器,若可能则将此对象的值存储于 CPU 寄存器中。无论此优化是否发生,声明为 register
的对象不能用作取址运算符的参数,而且 register
数组不能转换为指针
注:寄存器变量,用于经常使用的变量.编译器对于用register修饰的变量会尽可能分配一个专门的寄存器来存储,让该变量的访问速度达到最快.但如果实在分配不开寄存器,编译器就把它当auto变量处理了,register不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在register关键字也用得比较少了.
在使用寄存器变量时,请注意:
1.待声明为寄存器变量的类型应该是CPU寄存器所能接受的类型,寄存器变量是单个变量,变量长度应该小于等于寄存器长度。
2.不能对寄存器变量使用取地址符“&”,因为该变量没有内存地址。
3.尽量在大量、频繁操作时使用寄存器变量,且声明的变量个数应该尽量少.
注意:
若不提供存储类指定符,则默认为:
对所有函数为 extern
对在文件作用域的对象为 extern
对在块作用域的对象为 auto
对于任何用存储类指定符声明的结构体或联合体,存储期(但非链接)递归地应用到其成员。
在块作用域的函数声明能使用 extern
或完全不使用存储类指定符。在文件作用域的函数声明能使用 extern
或 static
。
函数参数不能使用异于 register
的存储类指定符。注意 static
在数组类型的函数参数中有特殊含义
转自:
C语言——register_c语言register-CSDN博客
欢迎指正!
参考:
Linux C编程一站式学习.pdf