0%

《C Primer Plus 第六版》读书笔记 - 第十至十二章

  • 数组和指针
  • 字符串和字符串函数
  • 存储类别、链接和内存管理

第10章 数组和指针

10.1 数组

10.1.1 初始化数组

如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值但是,如果部分初始化数组,剩余的元素就会被初始化为0。

10.1.2 指定初始化器(C99)

C99规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素:

1
int arr[6] = {[5] = 212}; //把arr[5]初始化为212
第一,如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段:[4]=31,30,31,那么后面这些值将被用于初始化指定元素后面的元素。也就是说,在days[4]被初始化为31后,days[5]和days[6]将分别被初始化为30和31。第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。

10.1.3 给数组元素赋值

C不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。

10.1.4 数组边界

使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。

10.1.5 指定数组的大小

1
2
3
4
5
int n = 5;
int m = 8;

float a8[n]; //C99之前不允许
float a9[m]; //c99之前不允许

C99标准允许这样声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称VLA(C11放弃了这一创新的举措,把VLA设定为可选,而不是语言必备的特性)

10.2 多维数组

10.2.1 初始化二维数组

初始化时也可省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化的效果与上面相同。但是如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值。后面没有值初始化的元素被统一初始化为0。

10.3 指针和数组

在C中,指针加1指的是增加一个存储单元。对数组而言,这意味着把加1后的地址是下一个元素的地址,而不是下个字节的地址

间接运算符(*)的优先级高于+,所以*dates+2相当于(*dates)+2

10.4 函数、数组和指针

只有在函数原型或函数定义头中,才可以用int ar[]代替int *ar

1
int sum(int ar[], int n);
int *ar形式和int ar[]形式都表示ar是一个指向int的指针。但是,int ar[]只能用于声明形式参数。第2种形式(int ar[])提醒读者指针ar指向的不仅仅一个int类型值,还是一个int类型数组的元素。

因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针只有在这种情況下,Cオ会把int ar[]和int *ar解释成一样。也就是说,ar是指向int的指针。由于函数原型可以省略参数名,所以下面4种原型都是等价的:

1
2
3
4
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);
但是,在函数定义中不能省略参数名。

10.4.1 使用指针形参

因为是指向int的指针,start递增1相当于其值递增int类型的大小。

10.4.2 指针表示法和数组表示法

C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。这使得while循环的测试条件是有效的,

1
total += *start++;
一元运算符*和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。也就是说,指针start先递增后指向。使用后缀形式(即start++而不是++start)意味着先把指针指向位置上的值加到total上,然后再递增指针。如果使用*++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(*start)++,则先使用start指向的值,再递增该值,而不是递增指针。这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。

10.5 指针操作

可以使用运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。

递增指向数组元素的指针可以让该指针移动至数组的下一个元素。

可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。

可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。

切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。

10.6 保护数组中的数据

10.6.1 对形式参数使用const

如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。

这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一样。

10.6.2 const的其他内容

指向const的指针不能用于改变值。

1
2
3
4
5
double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5};
const double * pd = rates; //pd指向数组的首元素
*pd = 29.89; //不允许
pd[2] = 222.22; //不允许
rates[0] = 99.99; //允许,因为rates未被const限定
无论是使用指针表示法还是数组表示法,都不允许使用pd修改它所指向数据的值。但是要注意,因为rates并未被声明为const,所以仍然可以通过rates修改元素的值。另外,可以让pd指向别处:
1
pd++; /*让pd指向rates[1]——没问题*/
把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的
1
2
3
4
5
double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
const double * pc = rates; //有效
pc = locked; //有效
pc = &rates[3]; //有效
然而,只能把非const数据的地址赋给普通指针:
1
2
3
4
5
double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
double * pnc = rates; //有效
pnc = lockedp; //无效
pnc = &rates[3]; //有效
因此,对函数的形参使用 const不仅能保护数据,还能让函数处理 const数组。

可以声明并初始化一个不能指向别处的指针,关键是const的位置:

1
2
3
4
double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5};
double * const pc = rates; //pc指向数组的开始
pc = &rates[2]; //不允许,因为该指针不能指向别处
*pc = 92.99; //没问题——更改rates[0]的值

在创建指针时还可以使用const两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值

1
2
3
4
double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5};
const double * const pc = rates;
pc = &rates[2];//不允许
*pc = 92.99 //不允许

10.7 指针和多维数组

1
int zippo[4][2];/*内含int数组的数组*/

数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。

因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的值相同。

给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。因此,zippo+1和zippo[0]+1的值不同。

解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。因为zippo[0]是该数组首元素(zippo[0][0])的地址,所以*(zippo[0])表示储存在zippo[0][0]上的值(即一个int类型的值)。与此类似,*zippo代表该数组首元素(zippo[0])的值,但是zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]。对两个表达式应用解引用运算符表明,**zippo与*&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(double indirection)的例子。

zippo 二维数组首元素的地址(每个元素都是内含两个int类型元素的一维数组)

zippo+2 二维数组的第3个元素(即一维数组)的地址

*(zippo+2) 二维数组的第3个元素(即一维数组)的首元素(一个int类型的值)地址

*(zippo+2) + 1 二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)地址

*(*(zippo+2) + 1) 二维数组的第3个一维数组元素的第2个int类型元素的值,即数组的第3行第2列的值(zippo[2][1])

10.7.1 指向多维数组的指针

1
int (* pz)[2]; //pz指向一个内含两个int类型值的数组

以上代码把pz声明为指向一个数组的指针,该数组内含两个int类型值。为什么要在声明中使用圆括号?因为[]的优先级高于*

1
int * pax[2]; //pax是一个内含两个指针元素的数组,每个元素都指向int的指针
虽然pz是一个指针,不是数组名,但是也可以使用pz[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名

10.7.2 指针的兼容性

1
2
3
4
5
6
7
8
9
10
11
12
13
int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; //一个指向指针的指针
pt = &ar1[0][0]; //都是指向int的指针
pt = ar1[0]; //都是指向int的指针
pt = ar1; //无效
pa = ar1; //都是指向内含3个int类型元素数组的指针
pa = ar2; //无效
p2 = &pt; //都是指向int*的指针
*p2 = ar2[0]; //都是指向int的指针
p2 = ar2; //无效
1
2
3
4
5
6
7
8
int x = 20;
const int y = 23;
int * p1 = &x;
const int * p2 = &y;
const int ** pp2;
p1 = p2; //不安全——把const指针赋给非const指针
p2 = p1; //有效——把非const指针赋给const指针
pp2 = &p1; //不安全——嵌套指针类型赋值
1
2
3
4
5
6
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; //允许,但是这导致const限定符失效(根据第1行代码,不能通过*pp2修改它所指向的内容)
*pp2 = &n; //有效,两者都声明为const,但是这将导致p1指向n(*pp2已被修改)
*p1 = 10; //有效,但是这将改变n的值(但是根据第3行代码,不能修改n的值)

标准规定了通过非 const指针更改 const数据是未定义的。

10.7.3 函数和多维数组

一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值:

1
int sum4d(int ar[][12][20][30], int rows);
因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。下面的声明与该声明等价:
1
int sum4d(int (*ar)[12][20][30], int rows); //ar是一个指针

10.8 变长数组(VLA)

变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度

首先,要声明一个带二维变长数组参数的函数,如下所示:

1
int sum2d(int rows, int cols, int ar[rows][cols]); //ar是一个変长数组(VLA)
注意前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。因为ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声明这两个形参。因此,下面的原型是错误的:
1
int sum2d(int ar[rows][cols], int rows, int cols); //无效的顺序
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
1
int sum2d(int, int, int ar[*][*]); //ar是一个变长数组(VLA),省略了维度形参名
C99/C11标准允许在声明变长数组时使用 const变量 ## 10.9 复合字面量

下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值:

1
(int [2]){10, 20} //复合字面
注意,去掉声明中的数组名,留下的int[2]即是复合字面量的类型名。

初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数:

1
(int []){50, 20, 90} //内含3个元素的复合字面量
因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。使用指针记录地址就是一种用法。也就是说,可以这样用:
1
2
int * pt1;
pt1 = (int [2]){10, 20};
注意,该复合字面量的字面常量与上面创建的diva数组的字面常量完全相同。与有数组名的数组类似,复合字面量的类型名也代表首元素的地址,所以可以把它赋给指向int的指针。然后便可使用这个指针。例如,本例中*pt1是10,pt1[1]是20。

可以把这种用法应用于二维数组或多维数组。例如,下面的代码演示了如何创建二维int数组并储存其地址:

1
2
int (*pt2)[4]; //声明一个指向二维数组的指针,该数组内含2个数组元素,每个元素是内含4个int类型值的数组
pt2 = (int [2][4]){ {1,2,3,-9}, {4,5,6,-8} };
复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在

第11章 字符串和字符串函数

11.1 表示字符串和字符串I/O

11.1.1 在程序中定义字符串

字符串字面量(字符串常量)

从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符分隔,C会将其视为串联起来的字符串字面量。例如:

1
2
char greeting[50] = "Hello, and"" how are" " you"
" today!";
与下面的代码等价:
1
char greeting[50] = "Hello, and how are you today!";

字符串常量属于静态存储类别(static storage class),这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串储存位置的指针。这类似于把数组名作为指向该数组位置的指针。

字符串数组和初始化

可以使用指针表示法创建字符串。

1
const char pt1 = "Something is pointing at me.";

数组和指针

数组形式(ar[1])在计算机的内存中分配为个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符申。字符串储存在静态存储区(static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,オ将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是储存在ar1数组中的字符串。

此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别名。这里关键要理解,在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操作。递增运算符只能用于变量名前(或概括地说,只能用于可修改的左值),不能用于常量。

指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置,并把字符串的地址储存在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第2个字符(o)。

字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值(即,pt1指向的位置)。如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const

编译器可以把多次使用的相同字面量储存在一处或多处

数组和指针的区别

数组的元素是变量(除非数组被声明为const),但是数组名不是变量。

建议在把指针初始化为字符串字面量时使用const限定符:

1
const char * p1 = "Klingon"; //推荐用法

如果不修改字符串,不要用指针指向字符串字面量。 #### 字符串数组

1
2
3
4
5
6
7
8
9
10
11
const char *mytalents[LIM] = {
"Adding numbers swiftly",
"Multiplying accurately", "Stashing data",
"Following instructions to the letter",
"Understanding the C language"
};
char yourtalents[LIM][SLEN] = {
"Walking in a straight line",
"Sleeping", "Watching television",
"Mailing letters", "Reading email"
};
综上所述,如果要用数组表示一系列待显示的字符串,请使用指针数组,因为它比二维字符数组的效率高。但是,指针数组也有自身的缺点。mytalents中的指针指向的字符串字面量不能更改;而yourtalents中的内容可以更改。所以,如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。

11.2 字符串输入

11.2.2 不辛的gets()函数

问题出在gets()唯一的参数是words,它无法检査数组是否装得下输入行。上一章介绍过,数组名会被转换成该数组首元素的地址,因此,gets()函数只知道数组的开始处,并不知道数组中有多少个元素。

如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常中止:或者还有其他情况。

11.2.3 gets()的替代品

fgets()函数(和fputs())

fgets()函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。

如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。

fgets()函数的第3个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中。

fputs()函数返回指向char的指针。如果一切进行顺利,该函数返回的地址与传入的第1个参数相同。但是,如果函数读到文件结尾,它将返回一个特殊的指针:空指针(null pointer)。该指针保证不会指向有效的数据,所以可用于标识这种特殊情况。在代码中,可以用数字0来代替,不过在C语言中用宏NULL来代替更常见(如果在读入数据时出现某些错误,该函数也返回NULL)

系统使用缓冲的I/O。这意味着用户在按下Return键之前,输入都被储存在临时存储区(即,缓冲区)中。按下Return键就在输入中增加了一个换行符,并把整行输入发送给fgets()。对于输出,fputs()把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送至屏幕上。

gets_s()函数

gets_s()只从标准输入中读取数据,所以不需要第3个参数。

如果gets_s()读到换行符,会丢弃它而不是储存它。

如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。

11.2.4 scanf()函数

11.3 字符串输出

11.3.1 puts()函数

该函数在遇到空字符时就停止输出,所以必须确保有空字符。

11.3.2 fputs()函数

fputs()函数的第2个参数指明要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h中的stdout(标准输出)作为该参数。

与puts()不同,fputs()不会在输出的末尾添加换行符

11.4 自定义输入/输出函数

当string指向空字符时,*string的值是0,

11.5 字符串函数

11.5.2 strcat()函数

strcat()(用于拼接字符串)函数接受两个字符串作为参数。该函数把第2个字符串的备份附加在第1个字符串末尾,并把拼接后形成的新字符串作为第1个字符串,第2个字符串不变。strcat()函数的类型是char*(即,指向char的指针)。strcat()函数返回第1个参数,即拼接第2个字符串后的第1个字符串的地址。

11.5.3 strncat()函数

该函数的第3个参数指定了最大添加字符数。例如,strncat(bugs,addon,13)将把addon字符串的内容附加给bugs,在加到第13个字符或遇到空字符时停止。因此,算上空字符(无论哪种情况都要添加空字符),bugs数组应该足够大,以容纳原始字符串(不包含空字符)、添加原始字符串在后面的13个字符和末尾的空字符。

11.5.4 strcmp()函数

该函数通过比较运算符来比较字符串,就像比较数字一样如果两个字符串参数相同,该函数就返回0,否则返回非零值。

strcmp()的返回值

strcmp()比较所有的字符,不只是字母。所以,与其说该函数按字母顺序进行比较,不如说是按机器排序序列(machine collating sequence)进行比较,即根据字符的数值进行比较(通常都使用ASCII值)。

strncmp()函数

而strncmp()函数在比较两个字符串时,可以比较到字符不同的地方,也可以只比较第3个参数指定的字符数。

11.5.5 strcpy()和strncpy()函数

strcpy()接受两个字符串指针作为参数,可以把指向源字符串的第2个指针声明为指针、数组名或字符串常量:而指向源字符串副本的第1个指针应指向一个数据对象(如,数组),且该对象有足够的空间储存源字符串的副本。

strcpy的其他属性

第一,strcpy()的返回类型是char*,该函数返回的是第1个参数的值,即一个字符的地址。第二,第1个参数不必指向数组的开始。这个属性可用于拷贝数组的部分。

strcpy()把源字符串中的空字符也拷贝在内。

更谨慎的选择:strncpy()

strcpy()和strcat()都有同样的问题,它们都不能检查目标空间是否能容纳源字符串的副本。拷贝字符串用strncpy()更安全,该函数的第3个参数指明可拷贝的最大字符数。

strncpy(target, source, n)把source中的n个字符或空字符之前的字符(先满足哪个条件就拷贝到何处)拷贝至target中。因此,如果source中的字符数小于n,则拷贝整个字符串,包括空字符。但是,strncpy()拷贝字符串的长度不会超过n,如果拷贝到第n个字符时还未拷贝完整个源字符串,就不会拷贝空字符。所以,拷贝的副本中不一定有空字符。

11.5.6 sprintf()函数

sprintf()函数声明在stdio.h中,而不是在string.h中。该函数和printf()类似,但是它是把数据写入字符串,而不是打印在显示器上。因此,该函数可以把多个元素组合成一个字符串。sprintf()的第1个参数是目标字符串的地址。其余参数和printf()相同,即格式字符串和待写入项的列表。

11.5.7 其他字符串函数

1
char *strchr(const char *s, int c);

如果s字符串中包含c字符,该函数返回指向s字符串首位置的指针(末尾的空字符也是字符串的一部分,所以在査找范围内);如果在字符串s中未找到c字符,该函数则返回空指针。

1
char *strpbrk(const char * s1, const char s2);
如果s1字符中包含s2字符串中的任意字符,该函数返回指向s1字符串首位置的指针;如果在s1字符串中未找到任何s2字符串中的字符,则返回空字符。
1
char *strrchr(const char * s, int c);
该函数返回s字符串中c字符的最后一次出现的位置(末尾的空字符也是字符串的一部分,所以在査找范围内)。如果未找到c字符,则返回空指针。
1
char *strstr(const char *s1, const char *s2);
该函数返回指向s1字符串中s2字符串出现的首位置。如果在s1中没有找到s2,则返回空指针。

11.8 命令行参数

C编译器允许main()没有参数或者有两个参数(一些实现允许main()有更多参数,属于对标准的扩展)。main()有两个参数时,第1个参数是命令行中的字符串数量。过去,这个int类型的参数被称为argc(表示参数计数(argument count))。系统用空格表示一个字符串的结束和下一个字符串的开始。因此,上面的repeat示例中包括命令名共有4个字符串,其中后3个供repeat使用。该程序把命令行字符串储存在内存中,并把每个字符串的地址储存在指针数组中。而该数组的地址则被储存在main()的第2个参数中。按照惯例,这个指向指针的指针称为argv(表示参数值(argument value))。如果系统允许(一些操作系统不允许这样),就把程序本身的名称赋给argv[0],然后把随后的第1个字符串赋给argv[1],以此类推。

11.9 把字符串转换为数字

使用atoi()函数(用于把字母数字转换成整数),该函数接受一个字符串作为参数,返回相应的整数值。

如果字符串仅以整数开头,atio()函数也能处理,它只把开头的整数转换为字符。

该程序中包含了stdlib.h头文件,因为从ANSI C开始,该头文件中包含了atoi()函数的原型。除此之外,还包含了atof()和atol()函数的原型。atof()函数把字符串转换成double类型的值,atol()函数把字符串转换成long类型的值。

ANSI C还提供一套更智能的函数:strtol()把字符串转换成long类型的值,strtoul()把字符串转换成unsigned long类型的值,strtod)把字符串转换成double类型的值。这些函数的智能之处在于识别和报告字符串中的首字符是否是数字。而且,strtol()和 strtoul()还可以指定数字的进制。

strol()函数最多可以转换三十六进制,'a'~'z'字符都可用作数字。strtoul()函数与该函数类似,但是它把字符串转换成无符号值。strtod()函数只以十进制转换,因此它值需要两个参数。

第12章 存储类别、链接和内存管理

12.1 存储类别

从硬件方面来看,被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(object)。对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小

从软件方面来看,程序需要一种方法访问对象。这可以通过声明变量来完成

1
int entity = 3;
该声明创建了一个名为entity的标识符(identifier)。标识符是一个名称,在这种情况下,标识符可以用来指定(designate)特定对象的内容。

在该例中,标识符entity即是软件(即C程序)指定硬件内存中的对象的方式。该声明还提供了储存在对象中的值

变量名不是指定对象的唯一途径。考虑下面的声明:

1
2
int * pt = &entity;
int ranks[10];
第1行声明中,pt是一个标识符,它指定了一个储存地址的对象。但是,表达式*pt不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与entity指定的对象相同。一般而言,那些指定对象的表达式被称为左值(第5章介绍过)。所以,entity既是标识符也是左值:*pt既是表达式也是左值。按照这个思路,ranks + 2 * entity既不是标识符(不是名称),也不是左值(它不指定内存位置上的内容)。但是表达式*(ranks + 2 * entity)是一个左值,因为它的确指定了特定内存位置的值,即ranks数组的第7个元素。顺带一提,ranks的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象。

所有这些示例中,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(modifiable lvalue) 现在,考虑下面的声明

1
const char * pc = "Behold a string literal!";
程序根据该声明把相应的字符串字面量储存在内存中,内含这些字符值的数组就是一个对象。由于数组中的每个字符都能被单独访问,所以每个字符也是一个对象。该声明还创建了一个标识符为pc的对象,储存着字符串的地址。由于可以设置pc重新指向其他字符串,所以标识符pc是一个可修改的左值。const只能保证被pc指向的字符串内容不被修改,但是无法保证pc不指向别的字符串。由于*pc指定了储存'B'字符的数据对象,所以*pc是一个左值,但不是一个可修改的左值。与此类似,因为字符串字面量本身指定了储存字符串的对象,所以它也是一个左值,但不是可修改的左值。

可以用存储期(storage duration)描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域(scope)和链接(linkage)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存。

12.1.1 作用域

作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。

块是用一对花括号括起来的代码区域。

定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾

C99把块的概念扩展到包括for循环、while循环、do whi1e循环和if语句所控制的代码,即使这些代码没有用花括号括起来,也算是块的一部分。

函数作用域(function scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。

函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名),如下所示:

1
int mighty(int mouse, double large);
函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用
1
void use_a_VLA(int n, int m, ar[n][m]);
方括号中必须使用在函数原型中已声明的名称。

变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。

编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)描述一个具有文件作用域的变量时,它的实际可见范国是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件

12.1.2 链接

C变量有3种链接属性:外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。

C标准用“内部链接的文件作用域”描述仅限于一个翻译单元(即一个源代码文件和它所包含的头文件)的作用域,用“外部链接的文件作用域”描述可延伸至其他翻译单元的作用域

一些程序员把“内部链接的文件作用域”简称为“文件作用域”,把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”。

12.1.3 存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期。

线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。

块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚オ为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。

12.1.4 自动变量

属于自动存储类别的变量具有自动存储期、块作用域且无链接。

默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字auto,如下所示

1
2
3
int main(void)
{
auto int plox;
关键字auto是存储类别说明符(storage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C+兼容的程序,最好不要使用auto作为存储类别说明符

块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量

变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。

块中声明的变量仅限于该块及其包含的块使用。

如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。

自动变量的初始化

可以用非常量表达式(non-constant expression)初始化自动变量,前提是所用的变量已在前面定义过:

12.1.5 寄存器变量

变量通常储存在计算机内存中。如果幸运的话,寄存器变量储存在CPU的寄存器中,或者概括地说,储存在最快的可用内存中。与普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类别说明符register便可声明寄存器变量:

1
2
3
int main(void)
{
register int quick;
我们刚才说“如果幸运的话”,是因为声明变量为register类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。

12.1.6 块作用域的静态变量

具有文件作用域的变量自动具有(也必须是)静态存储期。

可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。计算机在多次函数调用之间会记录它们的值。在块中(提供块作用域和无链接)以存储类别说明符static(提供静态存储期)声明这种变量。

1
2
3
4
5
6
7
void trystat(void)
{
int fade = 1;
static int stay = 1;

printf("fade = %d and stay = %d\n", fade++, stay++);
}

每次调用trystat()都会初始化fade,但是stay只在编译strstat()时被初始化一次。如果未显式初始化静态变量,它们会被初始化为0。

第2条声明实际上并不是trystat()函数的一部分。如果逐步调试该程序会发现,程序似乎跳过了这条声明。这是因为静态变量和外部变量在程序被载入内存时已执行完毕。把这条声明放在trystat()函数中是为了告诉编译器只有trystat()函数オ能看到该变量。这条声明并未在运行时执行

不能在函数的形参中使用static

12.1.7 外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。

把变量的定义性声明(defining declaration)放在在所有函数的外面便创建了外部变量。当然,为了指出该函数使用了外部变量,可以在函数中用关键字extern再次声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int Errupt; /*外部定义的变量*/
double Up[100]; /*外部定义的数组*/
extern char Coal; /*如果Coal被定义在另一个文件,则必须这样声明*/

void next(void);

int main(void)
{
extern int Errupt; /*可选的声明*/

extern double Up[]; /*可选的声明*/
...
}
void next(void)
{
...
}
#### 初始化外部变量 与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量

外部名称

C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。

定义和声明

1
2
3
4
5
int tern = 1; /*tern被定义*/
main()
{
extern int tern; /* 使用在别处定义的tern*/
}

这里,tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只告诉编译器使用之前已创建的tern变量,所以这不是定义。第1次声明被称为定义式声明(defining declaration),第2次声明被称为引用式声明(referencing declaration)。关键字extern表明该声明不是定义, 因为它指示编译器去别处査询其定义

假设这样写:

1
2
3
extern int tern;
int main(void)
{
编译器会假设tern实际的定义在该程序的别处,也许在别的文件中。该声明并不会引起分配存储空间。因此,不要用关键字extern创建外部定义,只用它来引用现有的外部定义。

外部变量只能初始化一次,且必须在定义该变量时进行。假设有下面的代码:

1
2
3
4
5
// file_one.c
char permis = 'N';
...
// file_two.c
extern char permis = 'Y'; /*错误*/
file_two中的声明是错误的,因为file_one.c中的定义式声明已经创建并初始化了permis

12.1.8 内部链接的静态变量

该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符static定义的变量具有这种存储类别

1
2
3
static int svil = 1; //静态变量,内部链接
int main(void)
{
普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。可以使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。考虑下面的代码
1
2
3
4
5
6
7
int traveler = 1; //外部链接
static int stayhome = 1; //内部链接
int main()
{
extern int traveler; //使用定义在别处的traveler
extern int stayhome; //使用定义在别处的stayhome
...
对于该程序所在的翻译单元,traveler和stayhome都具有文件作用域,但是只有traveler可用于其他翻译单元(因为它具有外部链接)。这两个声明都使用了extern关键字,指明了main()中使用的这两个变量的定义都在别处,但是这并未改变stayhome的内部链接属性。

12.1.9 多文件

C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。

注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。

12.1.10 存储类别说明符

auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存储期,所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图。

register说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。

用static说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。

extern说明符表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这接取决于该变量的定义式声明。

12.1.12 存储类别的选择

函数也有存储类别,可以是外部函数(默认)或静态函数。

外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。假设个文件中包含了以下函数原型

1
2
3
double gammaa(double); /*该函数默认为外部函数*/
static double beta(int, int);
extern double delta(double, int);
在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以在其他文件中可以使用与之同名的函数。

通常的做法是:用extern关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明都默认为extern

12.2 随机数函数和静态变量

1
2
3
4
5
6
7
8
static unsigned long int next = 1; /*种子*/

unsigned int rand0(void)
{
/*生成伪随机数的模数公式*/
next = next * 1103515245 + 12345;
return (unsigned int) (next / 65536) % 32768;
}

next是具有内部链接的静态变量(并非无链接)。这是为了方便稍后扩展本例,供同一个文件中的其他函数共享。

ANSI C有一个time()函数返回系统时间。虽然时间单元因系统而异,但是重点是该返回值是一个可进行运算的类型,而且其值随着时间变化而变化。time()返回值的类型名是time_t,具体类型与系统有关。这没关系,我们可以使用强制类型转換:

1
2
#include<time.h> /*提供time()的ANSI原型*/
srand1((unsigned int)time(0); /*初始化种子*/
般而言,time()接受的参数是一个time_t类型对象的地址,而时间值就储存在传入的地址上。当然,也可以传入空指针(0)作为参数,这种情况下,只能通过返回值机制来提供值

12.3 掷骰子

把文件名放在双引号中而不是尖括号中,指示编译器在本地査找文件,而不是到编译器存放标准头文件的位置去査找文件。“本地查找”的含义取决于具体的实现。一些常见的实现把头文件与源代码文件或工程文件(如果编译器使用它们的话)放在相同的目录或文件夹中。

12.4 分配内存:malloc()和free()

可以在程序运行时分配更多的内存。主要的工具是malloc()函数,该函数接受个参数:所需的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char表示1字节,malloc()的返回类型通常被定义为指向char的指针。然而,从ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个“通用指针"。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性然而,把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题。如果malloc()分配内存失败,将返回空指针。

我们试着用malloc()创建一个数组。除了用malloc()在程序运行时请求一块内存,还需要一个指针记录这块内存的位置。例如,考虑下面的代码:

1
2
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置。注意,指针ptd被声明为指向一个double类型,而不是指向内含30个double类型值的块。回忆一下,数组名是该数组首元素的地址。因此,如果让ptd指向这个块的首元素,便可像使用数组名一样使用它。也就是说,可以使用表达式ptd[0]访问该块的首元素,ptd[1]访问第2个元素,以此类推。根据前面所学的知识,可以使用数组名来表示指针,也可以用指针来表示数组。

free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因比,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。设想malloc()和free()管理着一个内存池。每次调用malloc()分配内存给程序使用,每次调用free()把内存归还内存池中,这样便可重复使用这些内存。free()的参数应该是一个指针,指向由malloc()分配的一块内存。不能用free()释放通过其他方式(如,声明一个数组)分配的内存。malloc()和free()的原型都在stdlib.h头文件中。

EXIT_FAILURE的值也被定义在stdlib.h中。标准提供了两个返回值以保证在所有操作系统中都能正常工作:EXIT_SUCCESS(或者,相当于0)表示普通的程序结束,EXIT_FAILURE表示程序异常中止。

在C中,不一定要使用强制类型转换(doub1e*),但是在C++中必须使用。所以,使用强制类型转换更容易把C程序转換为C++程序。

free()函数只释放其参数指向的内存块。一些操作系统在程序结東时会自动释放动态分配的内存,但是有些系统不会。为保险起见,请使用free(),不要依赖操作系统来清理

12.4.3 动态内存分配和变长数组

1
2
long * newmem;
newmem = (long *)calloc(100, sizeof (long));

calloc()函数接受两个无符号整数作为参数(ANSI规定是size_t类型)。第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。在该例中,long为4字节,所以,前面的代码创建了100个4字节的存储单元,总共400字节。

calloc()函数还有一个特性:它把块中的所有位都设置为0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。

free()函数也可用于释放calloc()分配的内存。

12.4.4 存储类别和动态内存分配

自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。

动态分配的内存在调用malloc()或相关函数时存在,在调用free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈内存慢。

静态数据(包括字符串字面量)占用一个区域,自动数据占用另一个区域,动态分配的数据占用第3个区域(通常被称为内存堆或自由内存)。

12.5 ANSI C类型限定符

C90还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const和volatile来声明,以这两个关键字创建的类型是限定类型(qualified type)。C99标准新増了第3个限定符:restrict,用于提高编译器优化。C11标准新增了第4个限定符:_Atomic。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。

C99为类型限定符增加了一个新属性:它们现在是幂等的(idempotent)!这个属性听起来很强大,其实意思是可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略

12.5.1 const类型限定符

头文件方案的好处是,方便你偷懒,不用惦记着在一个文件中使用定义式声明,在其他文件中使用引用式声明。所有的文件都只需包含同一个头文件即可。但它的缺点是,数据是重复的。对于前面的例子而言,这不算什么问题,但是如果const数据包含庞大的数组,就不能视而不见了。

12.5.2 volatile类型限定符

volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。

1
2
3
val1 = x;
/*一些不使用x的代码*/
val2 = x;
智能的(进行优化的)编译器会注意到以上代码使用了两次x,但并未改变它的值。于是编译器把x的值临时储存在寄存器中,然后在val2需要使用x时,オ从寄存器中(而不是从原始内存位置上)读取x的值,以节约时间。这个过程被称为高速缓存(caching)。通常,高速缓存是个不错的优化方案,但是如果些其他代理在以上两条语句之间改变了x的值,就不能这样优化了。如果没有volatile关键字,编译器就不知道这种事情是否会发生。因此,为安全起见,编译器不会进行高速缓存。这是在ANSI之前的情况。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。

可以同时用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要

12.5.3 restrict类型限定符

restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。

1
2
3
4
5
6
7
8
9
10
11
int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par = ar;
for (n = 0; n < 10; n++)
{
par[n] += 5;
restar[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}
在本例中,如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。

restrict限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。

restrict关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检査用户是否遵循这一限制,但是无视它后果自负。

12.5.4 _Atomic类型限定符(C11)

C1通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。

1
2
int hogs; //普通声明
hogs = 12; //普通赋值

可以替换成:

1
2
_Atomic int hogs; //hogs是一个原子类型的变量
atomic_store(&hogs,12); //stdatomic.h中的宏

这里,在hogs中储存12是一个原子过程,其他线程不能访问hogs。

12.5.5 旧关键字的新位置

C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中。对于类型限定符而言,这样做为现有功能提供了一个替代的语法。例如,下面是旧式语法的声明:

1
void ofmouth(int * const a1, int * restrict a1, int n); // 以前的风格
该声明表明a1是一个指向int的const指针,这意味着不能更改指针本身,可以更改指针指向的数据。除此之外,还表明a2是一个restrict指针,如上一节所述。新的等价语法如下
1
void ofmouth(int a1[const], int a2[restrict], int n); // C99允许
static的情况不同,因为新标准为static引入了一种与以前用法不相关的新用法。现在,static除了表明静态存储类别变量的作用域或链接外,新的用法告知编译器如何使用形式参数。例如,考虑下面的原型:
1
double stick(double ar[static 20]);
static的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。