本文内容主要包括:
- 关键字:auto、extern、static、register、const、volatile、restricted、_Thread_local、_Atomic
- 函数:rand()、srand()、time()、malloc()、calloc()、free()
- 如何确定变量的作用域(可见的范围)和生命期(它存在多长时间)
- 设计更复杂的程序
从硬件方面来看,被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(object)。
对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小(你不要总想着面向对象里面的对象,面向对象编程中的对象指的是类对象,其定义包括数据和允许对数据进行的操作,C不是面向对象编程语言)。
程序需要一种方法访问对象。这可以通过声明变量来完成:
int a = 3;
该声明创建了一个名为 a 的标识符(identifier)。标识符是一个名称,在这种情况下,标识符可以用来指定(designate)特定对象的内容。标识符遵循变量的命名规则。在该例中,标识符 a 即是软件(即C程序)指定硬件内存中的对象的方式。该声明还提供了储存在对象中的值。
标识符,包括变量名、常量名、对象名、函数名、类型名等等
变量名不是指定对象的唯一途径:
int *pt = &a;
int ranks[10];
第1行声明中,pt
是一个标识符(指针变量名),它指定了一个储存地址的对象(它指定了一块内存,这里存储着它指向值的地址)。但是,表达式 *pt
不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与 a 指定的对象相同。
一般而言,那些指定对象的表达式被称为左值。所以,a 既是标识符也是左值;*pt
既是表达式也是左值。按照这个思路,ranks + 2 *a
既不是标识符(不是名称),也不是左值(它不指定内存位置上的内容)。
但是表达式*(ranks + 2 * a)
是一个左值,因为它的确指定了特定内存位置的值,即ranks数组的第7个元素。顺带一提,ranks的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象。
所有这些示例中,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(modifiable lvalue)。
const char *pc = "Behold a string literal!";
程序根据该声明把相应的字符串字面量储存在内存中,内含这些字符值的字符串字面量就是一个对象。
由于字符串字面量中的每个字符都能被单独访问,所以每个字符也是一个对象。
该声明还创建了一个标识符为pc的对象,储存着字符串的地址。由于可以设置pc重新指向其他字符串,所以标识符pc是一个可修改的左值。
const只能保证被pc指向的字符串内容不被修改,但是无法保证pc不指向别的字符串。
由于pc指定了储存’B’字符的数据对象,所以pc是一个左值,但不是一个可修改的左值。与此类似,因为字符串字面量本身指定了储存字符串的对象,所以它也是一个左值,但不是可修改的左值。
可以用 存储期(storage duration) 描述对象,所谓存储期是指对象在内存中保留了多长时间。
标识符用于访问对象,可以用 作用域(scope)和链接(linkage) 描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。
不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。
对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存
作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
(1)块作用域(block scope)
块是用一对花括号括起来的代码区域。
例如,整个函数体是一个块,函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。另外,虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。
(2)函数作用域(function scope)
仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。(goto很少有人使用它)
(3)函数原型作用域(function prototype scope)
用于函数原型中的形参名(变量名),如下所示:
int mighty(int mouse, double large);
函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用:
void use_a_VLA(int n, int m, ar[n][m]);
方括号中必须使用在函数原型中已声明的名称。
(4)文件作用域
变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。
#include
int units = 0; /* 该变量具有文件作用域 */
void critic(void);
int main(void)
{
...
}
void critic(void)
{
...
}
变量units具有文件作用域,main()和critic()函数都可以使用它(更准确地说,units具有外部链接文件作用域,稍后讲解)。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)。
多个文件在编译器中可能以一个文件出现。例如,通常在源代码(.c扩展名)中包含一个或多个头文件(.h扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。但是,C预处理实际上是用包含的头文件内容替换#include指令。所以,编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)。 描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。 如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。
C变量有3种链接属性:外部链接、内部链接或无链接。
具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。
具有文件作用域的变量可以是外部链接或内部链接。
外部链接变量可以在多文件程序中使用(在其他文件中使用时需要使用extern关键字),内部链接变量只能在一个翻译单元中使用。
一些程序员把“内部链接的文件作用域”简称为“文件作用域”,把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”。
如何知道文件作用域变量是内部链接还是外部链接:
int giants = 5; // 文件作用域,外部链接
static int dodgers = 3; // 文件作用域,内部链接
int main()
{
...
}
...
作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字static |
属于自动存储类别的变量具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字auto
:
...
int main(void)
{
auto int plox;
...
关键字auto是存储类别说明符(storage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。
变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。
如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
注意以下两点:
for (int a = 0; a < 8; a++)
printf("%d\n",a);
int main(void)
{
int repid;
int tents = 5;
tents变量被初始化为5,**但是repid变量的值是之前占用分配给repid的空间中的任意值(如果有的话),不要认为这个值是0。**可以用非常量表达式(non-constant expression)初始化自动变量,前提是所用的变量已在前面定义过:
int main(void)
{
int ruth = 1;
int rance = 5 * ruth; // 使用之前定义的变量
变量通常储存在计算机内存中。寄存器变量有可能储存在CPU的寄存器中,或者概括地说,储存在最快的可用内存中。与普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类别说明符register
便可声明寄存器变量。
int main(void)
{
register int quick;
声明变量为register类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。
在函数头中使用关键字register,便可请求形参是寄存器变量:
void macho(register int n)
可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存double类型的值。
静态变量,即与程序有着相同生命周期的变量。那么具有文件作用域的变量肯定都属于静态变量。
那么,在块中,用static
关键字声明一个变量,它也具有静态存储期,也即具有静态存储期的局部变量,完整的来说是:具有静态存储期、块作用域、无链接的变量。(记住一个变量的3个要素:作用域、连接、存储期)。
如果未显式初始化静态变量,它们会被初始化为0(这与自动变量不同)。
不能在函数的形参中使用static:
int wontwork(static int flu); // 不允许
你可能要问了:这种变量只有块作用域、却有着静态存储期,它有什么用处呢?
静态变量用处很多,它只会被初始化一次(不初始化时自动初始化为0),以后的值就是上一次调用后的值。比如,你可以用它来检查某个函数是否被调用过,调用了几次。
看如下的测试程序:
#include
int * static_test();
int *auto_test();
int main()
{
int* pa;
pa = static_test();
printf("用指针访问局部静态变量:%d\n\n", *pa);
*pa += 6;
static_test();
printf("用指针访问自动变量:%d\n\n", *auto_test());
for (int i = 1; i < 4; i++)
{
static_test();
auto_test();
}
return 0;
}
int *static_test()
{
static int a;
printf("局部静态变量的值:%d\n", a++);
return &a;
}
int *auto_test()
{
int b=2;
printf("自动变量的值:%d\n", b++);
return &b;
}
输出:
局部静态变量的值:0
用指针访问局部静态变量:1
局部静态变量的值:7
自动变量的值:2
用指针访问自动变量:3
局部静态变量的值:8
自动变量的值:2
局部静态变量的值:9
自动变量的值:2
局部静态变量的值:10
自动变量的值:2
解读:
除了具有块作用域的静态变量外,还有具有文件作用域的静态变量,它们又分为外部链接的静态变量和内部链接的静态变量。
外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable)。把变量的定义性声明(defining declaration)放在所有函数的外面便创建了外部变量。当然,为了指出该函数使用了外部变量,可以在函数中用关键字extern再次声明。 如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern
在该文件中声明该变量 。
(1)初始化外部变量
外部变量和自动变量类似,也可以被显式初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0。这一原则也适用于外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量:
int x = 10; // 没问题,10是常量
int y = 3 + 20; // 没问题,用于初始化的
是常量表达式
size_t z = sizeof(int); //没问题,用于初始化的是
常量表达式
int x2 = 2 * x; // 不行,x是变量
(只要不是变长数组,sizeof表达式可被视为常量表达式。
)
(2)定义和声明
例:
int tern = 1; /* 定义具有外部链接的静态变量 */
int num = 2;
extern int TEST; /* 声明在别的文件中定义的变量TEST */
main()
{
extern int tern; /* 使用在函数外面定义的tern,在定义该变量的文件中,也可以直接使用。即注释掉这句,直接使用tern变量 */
int num = 3; /* 这个定义会覆盖外面定义的num,即在这个函数中,外面定义的那个num失效。(这两个num虽然名称相同,但存储期、作用域都不容易*/)
这里,tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只告诉编译器使用之前已创建的
tern变量,所以这不是定义。第1次声明被称为定义式声明(defining declaration),第2次声明被称为引用式声明(referencingdeclaration)。关键字extern表明该声明不是定义,因为它指示编译器去别处查询其定义。
不要用关键字extern创建外部定义,只用它来引用现有的外部定义。
extern char permis = 'Y'; /* 错误 */
该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符static
定义的变量具有这种存储类别:
static int svil = 1; // 静态变量,内部链接
int main(void)
{
内部链接的静态变量只能用于同一个文件中的函数。也可以使用存储类别说明符extern
,在本文件的函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性(内部链接)。
int traveler = 1; // 外部链接
static int stayhome = 1; // 内部链接
int main()
{
extern int traveler; // 使用定义在别处的
traveler
extern int stayhome; // 使用定义在别处的
stayhome
...
有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。
复杂的C程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。
注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。
函数也有存储类别,可以是外部函数(默认)或静态函数。C99新增了第3种类别——内联函数。
外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。假设一个文件中包含了以下函数原型:
double gamma(double); /* 该函数默认为外部函
数 */
static double beta(int, int);
extern double delta(double, int);
在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以在其他文件中可以使用与之同名的函数。
通常的做法是:用extern关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明都默认为extern。
小结:
看一下从标准库stdlib.h中的随机数生成函数的用法:
C语言的随机数生成函数是rand();
int rand(void);
该函数的机制是:有一个具有内部链接的静态变量,声明时初始化为1。调用一次rand()函数,该函数就会将这个变量修改一次(会按照某个数学公式修改),根据前面讲的,每次调用rand函数时,这个静态变量都会保存上一次的值,而他的初始值和修改公式是不变的,所以你每次运行程序,得到的随机数是一样的(伪随机)。
for (int i = 0; i < 4; i++)
{
printf("%d\n", rand());
}
每次运行上面的代码,都会得到:
41
18467
6334
26500
c语言使用srand()函数来改变这一点:
void srand(unsigned int _Seed);
给srand()一个输入值,他就会把rand()函数使用的那个静态变量更改为这个值,我们可以通过控制srand()的参数,使得每次rand()输出不同的随机数。
(1)srand()用1作为参数
此时,和之前一样,因为静态变量本来的额初始化值就是1。
srand(1);
for (int i = 0; i < 4; i++)
{
printf("%d\n", rand());
}
现在输出的随机数和前面是一样的。
(2)srand()用系统时间做参数
由于时间是一直变化的,运行程序时那个静态变量每次都会被置为不同的值,所以每次产生的随机数也不同。
srand((unsigned int)time(0));
for (int i = 0; i < 4; i++)
{
printf("%d\n", rand());
}
第一次输出:
30242
11899
15367
11662
第二次输出:
30304
19510
27106
10113
注:使用求模可以控制随机数的范围:rand()%5
,产生[0,4]
的随机数。
这就是具有内部链接的静态变量的一个应用。
前面讨论的存储类别有一个共同之处:在确定用哪种存储类别后,根据已制定好的内存管理规则,将自动选择其作用域和存储期。然而,还有更灵活地选择,即用库函数分配和管理内存。
以下声明:
float x;
char place[] = "Dancing Oxen Creek";
为一个float类型的值和一个字符串预留了足够的内存,或者可以显式指定分配一定数量的内存:
int plates[100];
该声明预留了100个内存位置,每个位置都用于储存int类型的值。声明还为内存提供了一个标识符。因此,可以使用x或place识别数据。
静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。
C语言可以在程序运行时分配更多的内存。即使用malloc()
函数,该函数接受一个参数:所需的内存字节数。
malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char表示1字节,malloc()的返回类型通常被定义为指向char的指针。
ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个“通用指针”。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性。然而,把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题。如果malloc()分配内存失败,将返回空指针。
例:
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置。(ptd指向分配的内存的起始位置,即数组首元素,使用sizeof而不是数字,是为了提高代码的可移植性)
创建数组的方法:
使用第2种和第3种方法可以创建动态数组(dynamic array)。这种数组和普通数组不同,可以在程序运行时选择数组的大小和分配内存。
通常,malloc()要与free()
配套使用。free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。
free()函数只释放其参数指向的内存块。一些操作系统在程序结束时会自动释放动态分配的内存,但是有些系统不会。为保险起见,请使用free(),不要依赖操作系统来清理。
malloc()和free()的原型都在stdlib.h
头文件中。
分配内存还可以使用calloc()
,典型的用法如下:
long * newmem;
newmem = (long *)calloc(100, sizeof (long));
alloc()函数还有一个特性:它把块中的所有位都设置为0。
free()函数也可用于释放calloc()分配的内存。
变长数组(VLA)和malloc()函数功能是相似的,都可以在程序运行时确定数组大小。
不同之处在于:
int n = 5;
int m = 6;
int ar2[n][m]; // 变长数组
int (* p2)[6];
int (* p3)[m];
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组
ar2[1][2] = p2[1][2] = 12;
可以认为程序把它可用的内存分为 3部分:
静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可访问储存在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。
然而,自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。
动态分配的内存在调用malloc()或相关函数时存在,在调用free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈内存慢。
常用类型和存储类别来描述一个变量。C90还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const
和volatile
来声明,以这两个关键字创建的类型是限定类型(qualified type)。C99标准新增了第3个限定符:restrict
,用于提高编译器优化。C11标准新增了第4个限定符:_Atomic
。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。
以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。(const修饰的变量可称为只读变量,而不是常量)
声明普通变量和数组时使用const关键字很简单。指针则复杂一些,因为要区分是限定指针本身为const还是限定指针指向的值为const。
其实很简单,看const修饰的是什么就可以了:const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。
例:
const float * pf
指针指向的数据不能变,但指针本身可以变,即它可以指向不同的位置。
float * const pt
指针本身的值不能变,即他只能指向同一个地址,但他指向的值可以改变。
const float * const ptr;
指针以及它指向的值都不能变。
float const * pfc;
和第一个一样。
const关键字的常见用法是声明为函数形参的指针。例如,假设有一个函数要调用display()显示一个数组的内容。要把数组名作为实际参数传递给该函数,但是数组名是一个地址。该函数可能会更改主调函数中的数据,但是下面的原型保证了数据不会被更改:
void display(const int array[], int limit);
使用全局变量是一种冒险的方法,因为这样做暴露了数据,程序的任何部分都能更改数据。
如果把数据设置为const,就可避免这样的危险,因此用const限定符声明全局数据很合理。可以创建const变量、const数组和const结构。(当然了,这种方式适用于程序只需要读取const变量值的情况)
2种使用方式:
再次强调:以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。但它不是不可修改,比如我们可以使用指针来修改:
const int a = 9;
int* pa = &a;
*pa = 10;
printf("%d\n",a);
输出:10
或者在不支持变长数组的编译器下:
const int a = 9;
int arr[a];
是会报错的。
volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据
restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。