存储类、作用域、生命周期和链接属性中概念的关系往往是相互关联的,都是我们描述变量、函数的关键概念。
存储类即存储类型。变量空间开辟于内存之中,存储类就是用于描述变量空间开辟于内存什么地方。内存被分为栈、堆、数据段、bss段和text段等不同管理方法的内存段,变量空间开辟于这些内存段中。如局部变量被分配在栈中,那么它的存储类就是栈;被显式初始化为非0的全局变量分配在data段,那么该全局变量的存储类就是data段;显式初始化为0和没有显式初始化(默认为0)的全局变量以及静态变量分配在bss段,该变量的存储类就是bss。同理,当变量空间在其他段时,那么它的存储类型就是该存储段。
int var1 = 1; //数据段
int var2; //bss段
int var3 = 0; //bss段
int main(){
int var4 = 1; //栈
return 0;
}
作用域是描述这个变量起作用的代码范围。
基本来讲,C语言变量的作用域规则是代码块作用域。即该变量起作用的范围是当前的代码段,代码段是一对括号{}括起来的范围,所以一个变量的作用域,为这个变量定义所在的大括号{}范围内从这个变量定义开始往后的部分,来看个例子:
#include
int var = 1; //作用域为本文件
int main(void){
printf("in file ,var = %d.\n",var);
int var = 2; //作用域为main函数
if(1){
int var = 3; //作用域为if
printf("in if,var = %d.\n",var);
}
printf("var = %d.\n",var);
return 0;
}
运行结果
in file ,var = 1.
in if,var = 3.
var = 2.
#include
int main(){
int i;
for(i = 0;i<0;i++){
int a = 5;
printf("i = %d.\n",i);
}
/*error:'a' undeclared (first use in this function)*/
printf("a = %d\n",a);
return 0;
}
运行结果
[Error] 'a' undeclared (first use in this function)
代码块基本可以理解为一对大括号{}括起来的部分,代码块不等于函数,因为if while for都有{}。
局部变量的作用域是代码块作用域,即一个局部变量可以被访问的范围,为定义该局部变量开始到代码块结束。
函数和全局变量的文件作用域
文件作用域是全局的访问权限,即整个.c文件中都可以访问这些东西。这就是平时所说的全局。函数和全局变量的作用域为.c文件中该函数或全局变量的定义位置开始到文件结束。
若想要在定义前访问变量时,可以先声明
#include
int func();
/*夸文件访问全局变量*/
extern int var;
int main(){
func();
return 0;
}
int func(){
int a = 1;
printf("%d\n",a);
}
变量的掩蔽规则
编程时出现变量同名,如果两个同名变量作用域不同,这种情况下同名没有任何影响;但如果两个同名变量作用域有交叠,C语言规定作用域小的一个变量会掩蔽掉作用域大的那个。
0~4G内存空间示意图:
对应程序中的代码(函数)、代码段在linux中又叫文本段(.text)。
rodata段常常用于存储常量数据,它又被称为只读段,它在程序运行期间只能读不能写,如const修饰的常量有可能存储在rodata段,说“可能”因为const常量的实现方法在不同平台是不一样的。
数据段、bss段
data段:存放被初始化为非0的全局变量;被初始化为非0的static局部变量。
bss段:存放未被初始化的全局变量;未被初始化的static修饰的局部变量。
#include
int data1 = 1; //存放在data段
int bss1; //存放在bss段
int main(void){
static int data2 = 2; //存放在data段
static int bss2; //存放在bss段
return 0;
}
C语言中什么变量存放在堆内存中?C语言不会自动操作堆内存空间,堆的操作由程序员用自己手工完成。在使用的过程中,程序员自己根据需求判断要不要使用堆内存,需要时使用malloc申请空间,使用完成之后,必须再用free方法释放空间,否则会造成内存泄露。
文件映射区是进程打开文件后,将这个文件内容从硬盘读到进程的文件映射区,以后就直接在内存中操作该文件,读写完成之后保存时,在将内存中的文件写到硬盘中去。
栈内存区,局部变量分配在栈上,函数调用传参过程也会用到栈。
内核映射区
内核映射区是将系统内核程序映射到这个区域。
对于linux中的每一个进程来说,它都以为整个系统中只有它自己和内核, 它认为内存地址0xC0000000以下都是它自己的活动空间,0xC0000000以上是操作系统内核的活动空间;每一进程都活在自己独立的进程空间中,0~3GB的空间每一个进程都是不同的(因为用了虚拟地址技术),但是内核是唯一的。每个进程都自认为有1~4GB的空间,但是每个进程都用不了这么多。
C语言程序运行时对环境有一定要求,即单独个人写的C代码没法直接在内存中运行,需要一定的外部协助,即加载运行代码(构建C运行时环境代码,它在操作系统上是别人写好的,会自动添加到我们所写的代码上,该段代码用于给全局变量赋值,清bss段)。
在裸机上写代码,定义一个全局变量初始化为0,但实际不为0,这是应该在裸机的start.S中加入清bss段代码。裸机上程序中没人替我们做该段加载运行时代码,要程序员自己做(即start.S中的重定位和清bss段)。在操作系统运行程序时,会自动完成重定位和清bss段,表面现象是C语言中未初始化的全局变量默认为0。
auto
auto关键字在C语言中只有一个作用,修饰局部变量;
auto修饰局部变量,表示该局部变量是自动局部变量,它在栈上分配内存空间,若不初始化,那其值为随机值。平时定义局部变量是就是定义auto,只是省略了auto关键字;auto的局部变量就是默认定义的普通局部变量。
#include
auto var1 = 20; //错误,auto用来定义局部变量而非全局变量
int main(){
auto int var2 = 15; //等价于int var2 = 15;
printf("%d\n",var);
return 0;
}
static
static关键字在C语言中有两种用法,两种之间没有关联;
第一种:用来修饰局部变量,形成静态局部变量。
第二种:用来修饰全局变量,形成静态全局变量。
静态局部变量和自动局部变量(auto)本质区别试存储类不同,自动局部变量分配在栈上,而静态局部变量分配在data或bss段上;
静态局部变量和全局变量的相似之处
静态局部变量在存储类方面(数据段)和全局变量一样;
静态局部变量在生命周期方面,和全局变量一样;
#include
int data1 = 1; //存放在data段
int bss1; //存放在bss段
int main(void){
static int data2 = 2; //存放在data段
static int bss2; //存放在bss段
return 0;
}
静态局部变量和全局变量的区别
作用域、链接属性不同,静态局部变量作用域是代码块作用域(和自动局部变量是一样的)、链接属性是无连接;全局变量作用域是文件作用域(和函数是一样的)、链接属性是外连接。
register
register关键字不常用,使用register关键字修饰变量,编译器会尽量将它分配在寄存器中,平时变量空间都是分配在内存中,register修饰的被称为寄存器变量,和普通变量的使用方式没有什么区别,但寄存器变量的读写效率会高很多,所以对读写频率很高的变量,使用register关键字定义变量,可提高其访问效率。如在uboot中使用的register类型变量,该变量在整个uboot中被访问频率很高;平时写代码慎用register关键字,编译器只能承诺尽量将register修饰的变量放到寄存器中,但是不保证一定放在寄存器中,因为寄存器数量有限,不一定能够分配上。
extern
编译C程序时,是以单个.c文件为单位的,当b.c中用到a.c中定义的变量时,编译器会报错。在这种情况下,我们可使用extern关键字。extern修饰全局变量,实现夸文件访问变量,如在a.c中使用extern来修饰声明一个全局变量var,但是在b.c中可使用该变量,告诉a.c,变量var在别的文件中也定义了var,并且它的原型和声明格式一样,将来在链接时链接器会在别的.o文件中找到该同名变量。
/* a.c*/
#include
extern int var; //bss段
int main(){
printf("%d\n",var);
return 0;
}
/* b.c */
#include
int var = 1 ; //data段
int main (){
printf("%d\n",var);
return 0;
}
声明和定义区别:定义是编译器创建具体变量,并为该变量分配了内存;声明没有分配内存,只告诉编译器该变量名字已经被分配内存,不能在被分配内存了;定义本身就有声明,对于编译器拉埃讲,不区分声明和定义,这里只不过为了学习时便于理解。
volatile
volatile意思为可变的、易变的,C 语言中使用volatile来修饰变量时,表示该变量可以被编译器之外的东西改变。“编译器之内”表示变量值的改变是代码作用的结果;“编译器之外”表示该改变不是由代码造成的,或者不是由当前代码造成的,编译器在编译当前代码时无法预知。如中断isr中应用的变量:中断处理程序isr中更改了改变量的值;在多线程中别的线程更改了改变量的值;以及硬件自动更改了改变量的值(一般来讲该变量值是一个寄存器的值)。
对于中断isr中应用的变量、多线程中共用的变量;硬件会更改的变量都是编译器在编译时无法预知的,此时应使用volatile告诉编译器该变量属于这种(可变的)情况。编译器在遇到volatile修饰的变量时就不会对其进行优化,因此这时优化会造成错误。
#include
int main(){
int a,b,c; //声明变量
a = 3; //变量赋值
b = a;
c = b;
/*编译器优化时,会变成c=b=a=3的形式,但如果在a=3后面发生
中断或硬件改变,就会出现错误,此时需要用volatile关键字进行修饰
volatile int a,b,c; //这时编译器就不会对其利用优化
*/
return 0;
}
编译器在一般情况下优化效果是非常好的, 可帮助其提升程序效率。但在特殊请款(volatile)下,变量会被编译器之外的力量所改变,编译器的优化错误会带来执行错误,而且该错误很难被发现。添加volatile关键字,程序会降低效率,对于volatile的使用:该加的时候加,不该加的时候不要加,若不确定是否添加,为了保险起见就加上。
restrict
restrict关键字是由c99标准引入的,被用于限定和约束指针。当使用restrict修饰指针,会告诉编译器,所有希望修改该指针指向的内存时,都必须使用该指针该可进行,目的是为了让编译器能够更好的优化。
/* 指针p 所指向的内存单元只能被p所访问,任何同样指向该内存的指针都是无效*/
int *restrict p;
#include
int function(int *x,int *y){
*x = 111;
*y = 222;
/*function函数绝大多数情况下回返回111
但极少情况下(硬件、多线程、中断isr)结果会改变
因此编译器不会优化为return 111,这是需要restrict关键字*/
return *x;
}
#include
int function(int *restrict x,int *restrict y){
*x = 111;
*y = 222;
/*function函数绝大多数情况下回返回111
所以编译器可以放心地将其优化为return 111*/
return *x;
}
关键字是GCC所支持的,利用“-std = c99”来开启GCC对C99的支持。
typedef
typedef关键字用于定义新的类型(或类型重名)属于C语言中存储类关键字,但实际上和存储类没有关系、;C中类型有两种:一种是编译器定义原生类型(基础数据类型),第二种是用户自定义类型,不是语言自带的,而是程序员自己定义的(如数组、结构体、函数等)。有时自定义类型太长,不方便使用,所以用typedef给它重命名一个简短的名字;typedef本身并不生产类型,只是负责给类型起一个好听的名字。
/* 使用typedef定义新的类型size,实际上即为int类型别名*/
typedef int size;
typedef char Line[81];
char t[81]; //原型行
Line t; //应用行
这里定义出应用行Line t;与原型行是完全等价的,只不过使用typedef重新定义了一个别名,在看个函数指针例子:
typedef int (*fun_ptr)(int,int) //typedef行
int (*fp)(int,int); //原型行
fun_ptr fp; //应用行
“typedef行”和“原型行”的区别在于,fun_ptr是类的别名,fp是该类的变量。两者的编译结果是一样的,原型行创建一个类型为int(*)(int,int)的函数指针fp。只是fun_ptr fp(应用行)比int(*fp)(int,int)的形式更简洁,便于书写和理解。
typedef在语法上是一种存储类的关键字(如auto、extern、static、register),而变量只能被一种存储类的关键字修饰。若变量被两种以上的关键字修饰则导致编译报错。
typedef static int a; //错误示范
typedef与#define宏区别
#define是单纯的替换,替换发生在预编译过程,可以把#defined的每个参数看成一堆字母,#define只是将一堆字母用另一堆字母替换。注意#define是没有分号的
#define dpChar char*
typedef char* tpChar;
dpChar p1,p2;
tpChar p3,p4;
const
const关键字用来定义常量(即不能被想修改的量),const实现方法至少两种:第一种在编译时会将const修饰的变量放在代码段中,以达到不能修改的目的,因为代码段是只读的,如在单片机开方中比较常见;第二种是让编译器会帮忙实现,如编译器在编译时检测到变量被const修饰,当发现程序试图去修改变量时,就会报编译错误。
int const a = 10; //与const int a = 10;是等价的
a = 12; // 编译报错,提醒a是常量,不能被修改
const 修饰指针的三种形式
int const *p等价于const int *p
int a = 10;
int b = 20;
int const *p = &a; //p指向了a
*p = 100; //编译时报错,p指向的空间不能被修改
p = &b; //正确,p本身可以修改
例子中试图通过p访问到a的空间,将a的值修改为100,显然编译器是不允许的,即这种修饰p所指向的空间的值是不能被修改,p本身的内容是可以修改的。
int *const p
int a2 = 10;
int b2 = 20;
int *const p = &a; //p指向了a
*p = 100; //可以
p = &b2'; //编译时,报错,因为指针变量p的内容不能被修改
//int *const p,就是为了保持p的指向不能发生改变,但是指向空间内容可以改变
与 int const *p,相反,指针变量p本身不能被修改,但是p所指向空间的内容可被修改。
int const* const p;
这其实是第一种和第二种情况的综合,p的指向不能发生改变,p所指向空间内容也不能发生改变。
const的变量值真的不能改吗,接下来,看个例子
int const a = 100;
int *p = (int *)&a; //p指向了a
*p = 100; //运行后a = 100,值被改了
尽管a被标记了const,并且代码中并没有直接对a进行修改,但是a可以被指针变量p引用,间接地被修改为100。const只是说明了a不能被修改,并没有说a的地址不可以被引用。若不希望a的地址被引用,可以将p的修饰改为:int const *p;
生命周期是描述变量什么时候诞生,什么时候死亡,即运行时分配内存空间给该变量,使用后收回该内存空间,此后内存地址已经和该变量无关了。变量和内存的关系,就和人(变量)去图书馆借书(内存)一样。变量的生命周期就好像借书的这段周期一样,研究变量的生命周期可以帮助我们理解程序在运行时的一些特殊现象。
栈变量
局部变量空间(自动变量)开辟于栈中,生命周期是临时的,在变量空间代码运行时开辟,运行结束后就释放。如一个函数内定义的局部变量,该函数每一次被调用时都会创建一次,然后使用, 最后在函数返回的时候消亡。函数内局部变量在函数运行结束时就释放了,所以局部变量的有效期等于函数的有效期。
堆变量
堆每次空间是客观存在的,是由操作系统维护的,程序只是去申请使用然后释放而已。堆变量在malloc申请时诞生,然后使用,直到free时消亡,因此开辟于堆内存变量在malloc之前和free之后不能被访问。
数据段、bss段变量
全局变量空间(另静态全局或局部变量)开辟于数据段或者bss段中,因此全局变量的生命周期是永久的。所谓“永久”是从程序开始运行到终止时都会一直存在。全局变量所占内存时不能被程序自己所释放的,所以程序如果申请了过多的全局变量,就会导致该程序一直占用大量内存。在Linux内核中大量使用malloc/free目的,就是为了避免内存被大量占用。
代码段、只读段
程序执行的代码指是是函数,它生命周期是永久的。有时候放在代码段不只是代码,还有const类型的常量和字符串常量有时候放在rodata段(如GCC),有时候放在代码段(如单片机),取决于平台。
程序从源代码到最终可执行程序,经历的过程为预编译、编译、汇编和链接,其中编译目的是把源码翻译成xx.o的目标文件,目标文件里有很多的符号和代码段、数据段、bss段等分段。链接是为了将各个独立分开的二进制的函数链接起来,形成一个整体的二进制可执行程序。
符号是编程中的变量名、函数名等,运行时变量名、函数名能够和相应的内存对应起来,靠符号来连接。xxx.o目标文件链接生成最终的可执行程序的时候,其实就是把符号和对应的段连接起来,C语言中的符号有三种链接属性:外链接属性,内连接属性和无链接属性。
外链接是所需函数与变量可以在外部文件中找到。即跨文件(.c程序)访问,例如extern修饰的全局变量和函数就是属于外链接内容。
内链接与外链接相反,所需函数和变量在当前文件的内部就可以找到,对于内链接函数与变量来说,一般使用static修饰。一旦该函数和全局变量被static修饰,外部文件将无法访问,只有文件内部才能访问。内链接方法可用来解决函数和全局变量的命名冲突问题,即外部文件无法访问内部文件内的函数与全局变量,避免各文件之间命名冲突,但C语言本身语言特性(只有一个全局作用域namespace)会导致大型项目时,有一定难度。
无链接是符号本身不参与链接,与链接无关,例如局部变量(auto、static修饰的)都是无链接。