《C Primer Plus 第六版》读书笔记 - 第十至十二章
- 数组和指针
- 字符串和字符串函数
- 存储类别、链接和内存管理
第 10 章 数组和指针
10.1 数组
10.1.1 初始化数组
如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值但是,如果部分初始化数组,剩余的元素就会被初始化为 0。
10.1.2 指定初始化器(C99)
C99 规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素: 1
int arr[6] = {[5] = 212}; //把 arr[5] 初始化为 212
10.1.3 给数组元素赋值
C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。
10.1.4 数组边界
使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。
10.1.5 指定数组的大小
1 | int n = 5; |
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);
因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针只有在这种情況下,C オ会把 int ar[] 和 int *ar 解释成一样。也就是说,ar 是指向 int 的指针。由于函数原型可以省略参数名,所以下面 4 种原型都是等价的: 1
2
3
4int 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++;
10.5 指针操作
可以使用运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。
递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
可以使用-运算符从一个指针中减去一个整数。指针必须是第 1 个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。
可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。
切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。
10.6 保护数组中的数据
10.6.1 对形式参数使用 const
如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字 const。
这样使用 const 并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。这样使用 const 可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一样。
10.6.2 const 的其他内容
指向 const 的指针不能用于改变值。 1
2
3
4
5double 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 限定1
pd++; /*让 pd 指向 rates[1]——没问题*/
1
2
3
4
5double 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]; //有效1
2
3
4
5double 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 的位置:
1 | double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5}; |
在创建指针时还可以使用 const 两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值
1 | double rates[5]={88, 99, 100, 12, 59.45, 183.11, 340.5}; |
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 的指针
10.7.2 指针的兼容性
1 | int * pt; |
1 | int x = 20; |
1 | const int **pp2; |
标准规定了通过非 const 指针更改 const 数据是未定义的。
10.7.3 函数和多维数组
一般而言,声明一个指向 N 维数组的指针时,只能省略最左边方括号中的值: 1
int sum4d(int ar[][12][20][30], int rows);
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)
1
int sum2d(int ar[rows][cols], int rows, int cols); //无效的顺序
1
int sum2d(int, int, int ar[*][*]); //ar 是一个变长数组 (VLA),省略了维度形参名
下面的复合字面量创建了一个和 diva 数组相同的匿名数组,也有两个 int 类型的值: 1
(int [2]){10, 20} //复合字面
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数: 1
(int []){50, 20, 90} //内含 3 个元素的复合字面量
1
2int * pt1;
pt1 = (int [2]){10, 20};
可以把这种用法应用于二维数组或多维数组。例如,下面的代码演示了如何创建二维 int 数组并储存其地址: 1
2int (*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
2char 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
11const 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"
};
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);
1
char *strrchr(const char * s, int c);
1
char *strstr(const char *s1, const char *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 即是软件(即 C 程序)指定硬件内存中的对象的方式。该声明还提供了储存在对象中的值
变量名不是指定对象的唯一途径。考虑下面的声明: 1
2int * pt = &entity;
int ranks[10];
所有这些示例中,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值 (modifiable lvalue) 现在,考虑下面的声明 1
const char * pc = "Behold a string literal!";
可以用存储期 (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
3int main(void)
{
auto int plox;
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量
变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。
块中声明的变量仅限于该块及其包含的块使用。
如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
自动变量的初始化
可以用非常量表达式 (non-constant expression) 初始化自动变量,前提是所用的变量已在前面定义过:
12.1.5 寄存器变量
变量通常储存在计算机内存中。如果幸运的话,寄存器变量储存在 CPU 的寄存器中,或者概括地说,储存在最快的可用内存中。与普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类别说明符 register 便可声明寄存器变量: 1
2
3int main(void)
{
register int quick;
12.1.6 块作用域的静态变量
具有文件作用域的变量自动具有(也必须是)静态存储期。
可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。计算机在多次函数调用之间会记录它们的值。在块中(提供块作用域和无链接)以存储类别说明符 static(提供静态存储期)声明这种变量。
1 | void trystat(void) |
每次调用 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
17int 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 | int tern = 1; /*tern 被定义*/ |
这里,tern 被声明了两次。第 1 次声明为变量预留了存储空间,该声明构成了变量的定义。第 2 次声明只告诉编译器使用之前已创建的 tern 变量,所以这不是定义。第 1 次声明被称为定义式声明 (defining declaration),第 2 次声明被称为引用式声明 (referencing declaration)。关键字 extern 表明该声明不是定义, 因为它指示编译器去别处査询其定义
假设这样写: 1
2
3extern int tern;
int main(void)
{
外部变量只能初始化一次,且必须在定义该变量时进行。假设有下面的代码: 1
2
3
4
5// file_one.c
char permis = 'N';
...
// file_two.c
extern char permis = 'Y'; /*错误*/
12.1.8 内部链接的静态变量
该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符 static 定义的变量具有这种存储类别 1
2
3static int svil = 1; //静态变量,内部链接
int main(void)
{1
2
3
4
5
6
7int traveler = 1; //外部链接
static int stayhome = 1; //内部链接
int main()
{
extern int traveler; //使用定义在别处的 traveler
extern int stayhome; //使用定义在别处的 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
3double gammaa(double); /*该函数默认为外部函数*/
static double beta(int, int);
extern double delta(double, int);
通常的做法是:用 extern 关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用 static 关键字,否则一般函数声明都默认为 extern
12.2 随机数函数和静态变量
1 | static unsigned long int next = 1; /*种子*/ |
next 是具有内部链接的静态变量(并非无链接)。这是为了方便稍后扩展本例,供同一个文件中的其他函数共享。
ANSI C 有一个 time() 函数返回系统时间。虽然时间单元因系统而异,但是重点是该返回值是一个可进行运算的类型,而且其值随着时间变化而变化。time() 返回值的类型名是 time_t,具体类型与系统有关。这没关系,我们可以使用强制类型转換: 1
2
srand1((unsigned int)time(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
2double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
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 | long * newmem; |
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
3val1 = x;
/*一些不使用 x 的代码*/
val2 = x;
可以同时用 const 和 volatile 限定一个值。例如,通常用 const 把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用 volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要
12.5.3 restrict 类型限定符
restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。 1
2
3
4
5
6
7
8
9
10
11int 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 要求的参数。总而言之,编译器不会检査用户是否遵循这一限制,但是无视它后果自负。
12.5.4 _Atomic 类型限定符 (C11)
C1 通过包含可选的头文件 stdatomic.h 和 threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。
1 | int hogs; //普通声明 |
可以替换成:
1 | _Atomic int hogs; //hogs 是一个原子类型的变量 |
这里,在 hogs 中储存 12 是一个原子过程,其他线程不能访问 hogs。
12.5.5 旧关键字的新位置
C99 允许把类型限定符和存储类别说明符 static 放在函数原型和函数头的形式参数的初始方括号中。对于类型限定符而言,这样做为现有功能提供了一个替代的语法。例如,下面是旧式语法的声明: 1
void ofmouth(int * const a1, int * restrict a1, int n); // 以前的风格
1
void ofmouth(int a1[const], int a2[restrict], int n); // C99 允许
1
double stick(double ar[static 20]);
《C Primer Plus 第六版》读书笔记 - 第十至十二章