相关阅读
C语言https://blog.csdn.net/weixin_45791458/category_12423166.html?spm=1001.2014.3001.5482
作用域
作用域(Scope)描述程序中标识符的生效区域(可访问区域),一个C语言变量的作用域可以是块作用域、函数作用域、函数原型作用域、文件作用域。
块作用域
块是用一对花括号括起来的代码区域,例如整个函数体是一个块,函数中的复合语句也是一个块。定义在块中的变量具有块作用域,块作用域变量的可访问区域是从定义位置到变量所属块结尾。即使没有在代码块中,函数的形参也被视为具有块作用域。例1给出了一个简单的例子。
// 例1
void func(int t) { // t具有块作用域int a = 10; // a具有块作用域{int b = 20; // b具有子块作用域} // b的作用域结束// 这里可以继续使用a和t,但不能再使用b
}
变量定义位置
在C90及以前的标准(也叫ANSI C)中,所有具有块作用域的变量,必须在块的最开头声明,在任何可执行语句(比如if、for语句)之前,例2所示的语句在C90及以前的标准(也叫ANSI C)中是不合法的。
// 例2
void func() {int a = 10;printf("%d\n", a);int b = 20; // C90标准不允许这样在执行语句后再声明变量
}// 报错:error: ISO C90 forbids mixed declarations and code [-Werror=declaration-after-statement]
C99标准放宽了这一限制,允许在块中的任意位置声明变量,甚至在for语句头中定义变量,如例3所示。需要注意的是,for语句头中定义的变量和循环块中定义的变量具有块作用域,即使循环体没有使用花括号括起来。
// 例3
void func() {for (int i = 0; i < 5; i++) { // i具有子块作用域int square = i * i; // square具有子块作用域}
}
函数作用域
函数作用域是只针对标签名而言的,即标签名在整个函数内都有效,如例4所示,需要注意的是,标签名是指goto语句跳转用的标签,而不是普通的变量名。
// 例4
void func() {
label1: // 这是一个标签,具有函数作用域printf("At label1\n");goto label1; // 在函数内任何地方都能跳到label1
}
函数原型作用域
函数原型作用域用于函数声明中的形参定义,范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要,即使有形参名,也不必与函数定义中的形参名相匹配,如例5所示。
// 例5
int add(int a, int b); // a和b具有函数原型作用域int add(int x, int y) { // 这里即使参数名换了,也没问题return x + y;
}
特殊情况
当变长数组(VLA)出现在函数原型作用域时,作为变长数组索引的形参名不能省略,如例6所示。同样,形参名依旧不必与函数定义中的形参名相匹配。
// 例6
int use_a_VLA(int n, int m, int ar[n][m]); // a、b和ar具有函数原型作用域int use_a_VLA(int x, int y, int br[x][y]) { // 这里即使参数名换了,也没问题return 0;
}
文件作用域
定义在函数外的变量具有文件作用域,文件作用域变量的可访问区域至少是从定义位置到变量所属翻译单元(translation unit)的结尾,如例7所示,这里的“至少”是因为文件作用域变量可以拥有内部链接和外部链接两种属性,由于可以被多个函数访问,因此文件作用域变量又被称为全局变量。
// 例7
// 文件test1.c
int var = 100; // var具有文件作用域
#include "test1.h"
void global() {int a = var;
}// 文件test1.h
int b = var;
翻译单元指的是由预处理器完成了所有插入展开后,并作为gcc的命令行参数的一个源代码文件。如果gcc的命令行参数有多个文件,将在编译、汇编后使用链接器对目标文件(.o)进行链接。
链接
链接(Linkage)描述程序中标识符在多个翻译单元之间的生效区域(可访问区域),一个C语言变量的链接属性可以是无链接、内部链接和外部链接。
无链接
具有块作用域、函数作用域和函数原型作用域的变量都是无链接的,意味着这些变量只能在一个翻译单元中的部分区域访问(私有)。
内部链接
具有文件作用域的变量可以是内部链接,内部链接代表其可以在一个翻译单元内使用(如文件作用域一节所述)。
外部链接
具有文件作用域的变量可以是外部链接,外部链接代表其可以在多个翻译单元共之间享,如例8所示。
// 例8
// 文件test1.c
extern int var; // var具有文件作用域,外部链接
void external_global() {int a = var;
}// 文件test2.c
int var = 100;// gcc test1.c test2.c -o my_program
存储期
作用域和链接描述了变量的生效区域,而存储期(Storage Duration)指的是变量在内存/寄存器中存在的时间,一个C语言变量的存储期可以是自动存储期、静态存储期、动态存储期和线程存储期。
自动存储期
块作用域的变量通常具有自动存储期,当程序进入定义这些变量的块时,为这些变量分配内存(但不一定立刻初始化),当退出这个块时,释放为变量分配的内存。需要注意的是,子块的变量一般在函数调用时就会分配内存(大部分现代编译器都是这么做的),而不是等进入子块时才分配内存;在函数返回时才释放内存,而不是退出子块就释放内存,这也就导致了可以通过指针间接访问,如例9所示。
// 例9
void func(int t) { // t具有自动存储期int *a; // a具有自动存储期{int b = 20; // b具有自动存储期a = &b;}printf("%d", *a); // 大部分现代编译器没问题,但是不要这么做!
}
静态存储期
如果变量具有静态存储期,那么该变量在程序执行期间一直存在。所有具有文件作用域的变量都是具有静态存储期,对于具有块作用域的变量,如果使用static关键字定义,则其具有静态存储期,否则其具有自动存储期,如例10所示。具有静态存储期的变量只能用常量初始化,如果不进行显式初始化,则初始化为0。
// 例10
int c; // c具有静态存储期,初始化为0
int* func() { int *d;static int a; // a具有静态存储期,初始化为0 {static int b = 20; // b具有静态存储期,初始化为20d = &b;}printf("%d", *d); // 没有任何问题return &a;
}
对于例10而言,变量a、b、c都具有静态存储期,即变量a、b在程序开始执行时就存在,并不是执行到函数时才分配内存,但依旧受到作用域的访问限制(和例9一样,可以使用指针间接访问,也可以返回静态存储期变量的指针给其他函数访问)。
动态存储期
使用malloc()函数可以创建动态存储期的内存块,完全由程序员管理,因此需要记得在使用完毕后用free()函数释放,否则会造成内存泄漏,如例11所示。
// 例11
void func() {int *d;{d = (int*)malloc(sizeof(int));*d = 20;}printf("%d\n", *d);free(d);
}
线程存储期
线程存储期用于并发程序设计,程序可创建多个子线程,每个线程拥有属于自己的专属变量,该变量在线程结束时释放,使用_Thread_local关键字定义(C11标准引入),如例12所示。
// 例12
#include <stdio.h>
#include <pthread.h>_Thread_local int x = 0; // 每个线程有自己的xvoid* thread_func(void* arg) {x++;printf("Thread %d: x = %d\n", *(int*)arg, x);return NULL;
}int main() {pthread_t t1, t2;int id1 = 1, id2 = 2;pthread_create(&t1, NULL, thread_func, &id1);pthread_create(&t2, NULL, thread_func, &id2);pthread_join(t1, NULL);pthread_join(t2, NULL);return 0;
}