版权声明:本文为
gfson
原创文章,转载请注明出处。
注:作者水平有限,文中如有不恰当之处,请予以指正,万分感谢。
第2章 C 语言基本概念
2.1. 字符串字面量(String Literal)
字符串字面量是用一对双引号包围的一系列字符。
2.2. 把换行符加入到字符串中
- 错误方式
printf("hello
World\n");
- 正确方式
printf("hello "
"World");
根据 C 语言标准,当两条或者更多条字符串字面量相连时(仅用空白字符分割),编译器必须把它们合并成单独一条字符串。这条规则允许把字符串分割放在两行或更多行中。
2.3. 编译器是完全移走注释还是用空格替换掉注释?
根据标准 C,编译器必须用一个空格符替换每条注释语句。
a/**/b = 0
相当于 a b = 0
。
2.4. 在注释中嵌套一个新的注释是否合法?
在标准 C 中是不合法的,如果需要注释掉一段包含注释的代码,可以使用如下方法:
#if 0
printf("hello World\n");
#endif
这种方式经常称为「条件屏蔽」。
2.5. C 程序中使用 //
作为注释的开头,如下所示,是否合法?
// This is a comment.
在标准 C 中是不合法的,//
注释是 C++ 的方式,有一些 C 编译器也支持,但是也有一部分编译器不支持,为了保持程序的可移植性,因此应该尽量避免使用 //
。
第3章 格式化的输入/输出
3.1. 转换说明(Conversion specification)
转换说明以 %
开头,转化说明用来表示填充位置的占位符。
3.2. printf 中转换说明的格式
转化说明的通用格式为:
%m.pX 或者 %-m.pX
对于这个格式的解释如下:
- m 和 p 都是整形常量,而 X 是字母。
- m 和 p 都是可选项。
- 如果省略 m,小数点要保留,例如
%.2f
。 - 如果省略 p,小数点也要一起省略,例如
%10f
。
- 如果省略 m,小数点要保留,例如
- m 表示的是最小字段宽度(minimum field width),指定了要显示的最小字符数量。
- 如果要打印的数值比 m 个字符少,那么值在字段内是右对齐的(换句话说,在数值前面放置额外的空格)。
- 如果要打印的数值比 m 个字符多,那么字段宽度会自动扩展为需要的尺寸,而不会丢失数字。
- 在 m 前放上一个负号,会发生左对齐。
- 举例如下:
printf("%4d\n",1); printf("%4d\n",11); printf("%4d\n",111); printf("%4d\n",1111); printf("---------------\n"); printf("%4d\n",12345); printf("---------------\n"); printf("%-4d\n",1); printf("%-4d\n",11); printf("%-4d\n",111); printf("%-4d\n",1111);
- p 表示的是精度(precision),p 的含义依赖于转换说明符(conversion specifier) X 的值,X 表明需要对数值进行哪种转换。常见的转换说明符有:
- d —— 表示十进制的整数。当 x 为 d 时,p 表示可以显示的数字的最少个数(如果需要,就在数前加上额外的零),如果忽略掉 p,则默认它的值为 1。
- e —— 表示指数(科学计数法)形式的浮点数。当 x 为 e 时,p 表示小数点后应该出现的数字的个数(默认为 6),如果 p 为 0,则不显示小数点。
- f —— 表示「定点十进制」形式的浮点数,没有指数。p 的含义与在说明符 e 时一样。
- g —— 将 double 值转化为 f 形式或者 e 形式,形式的选择根据数的大小决定。仅当数值的指数部分小于
-4,或者指数部分大于或等于精度时,会选择 e 形式显示。当 x 为 g 时,p 表示可以显示的有效数字的最大数量(默认为 6)。与 f 不同,g 的转换将不显示尾随零。 - 举例如下:
printf("%.3d\n",1); printf("%.5d\n",1); printf("%.6d\n",1); printf("--------------------\n"); printf("%e\n",12.1); printf("%.2e\n",12.1); printf("%.0e\n",12.1); printf("--------------------\n"); printf("%f\n",12.1); printf("%.2f\n",12.1); printf("%.0f\n",12.1); printf("--------------------\n"); printf("%g\n",12.120000); printf("%g\n",12.12345678); printf("%.2g\n",12.1); printf("%.0g\n",12.1);
3.3. %i 和 %d 有什么区别?
在 printf 中,两者没有区别。在 scanf 中,%d 只能和十进制匹配,而 %i 可以匹配八进制、十进制或者十六进制。如果用户意外将 0 放在数字的开头处,那么用 %i 代替 %d 可能有意外的结果。由于这是一个陷阱,所以坚持使用 %d。
3.4. printf 如何显示字符 %?
printf 格式串中,两个连续的 % 将显示一个 %,如下所示:
printf("%%");
第4章 表达式
4.1. 运算符 /
和 %
注意的问题
- 运算符
/
通过丢掉分数部分的方法截取结果。因此,1/2
的结果是 0 而不是 0.5。 - 运算符
%
要求整数操作数,如果两个操作数中有一个不是整数,无法编译通过。 - 当运算符
/
和%
用于负的操作数时,其结果与实现有关。-
-9/7
的结果既可以是 -1 也可以是 -2。 -
-9%7
的结果既可以是 2 也可以是 -2。
-
4.2. 由实现定义(implementation-defined)
- 由实现定义是一个术语,出现频率很高。
- C 语言故意漏掉了语言未定义部分,并认为这部分会由「实现」来具体定义。
- 所谓实现,是指软件在特定平台上编译、链接和执行。
- C 语言为了达到高效率,需要与硬件行为匹配。当 -9 除以 7 时,一些机器产生的结果可能是 -1,而另一些机器的结果可能是 -2。C 标准简单的反映了这一现实。
- 最好避免编写与实现行为相关的程序。
4.3. 赋值运算符「=」
- 许多语言中,赋值是语句,但是 C 语言中,赋值是运算符,换句话说,赋值操作产生结果。
- 赋值表达式
v = e
产生的结果就是赋值运算后 v 的值。 - 运算符
=
是右结合的,i = j = k = 0
相当与(i = (j = (k = 0)))
。 - 由于结果发生了类型转换,串联赋值运算的最终结果不是期望的结果,如下所示:
int i;
float f;
f= i =33.6;
printf("i=%d,f=%f",i,f);
4.4. 左值
- 左值(lvalue)表示储存在计算机内存中的对象,而不是常量或计算结果。
- 变量是左值,诸如 10 或者 2*i 这样的表达式不是左值。
- 赋值运算符要求它左边的操作数必须是左值,以下表达式是不合法的,编译不通过:
- 12 = i;
- i + j = 0;
- -i = j;
4.5. 子表达式的求值顺序
C 语言没有定义子表达式的求值顺序(除了含有逻辑与运算符及逻辑或运算符、条件运算符以及逗号运算符的子表达式)。因此,在表达式 (a + b) * (c - d)
中,无法确定子表达式 (a + b)
是否在子表达式 (c - d)
之前求值。
- 这样的规定隐含着陷阱,如下所示:
a = 5; c = (b = a + 2) - (a = 1);
- 如果先计算
b = a + 2
,则 b = 7,c = 6。 - 如果先计算
a = 1
,则 b = 3,c = 2。
- 如果先计算
为了避免此问题,最好不要编写依赖子表达式计算顺序的程序,一个好的建议是:不在字表达式中使用赋值运算符,如下所示:
a = 5;
b = a + 2;
a = 1;
c = b - a;
4.6. v += e
一定等价与 v = v + e
么?
不一定,如果 v 有副作用,则两者不想等。
- 计算
v += e
只是求一次 v 的值,而计算v = v + e
需要求两次 v 的值。任何副作用都能导致两次求 v 的值不同。如下所示:a [i++] += 2; // i 自增一次 a [i++] = a [i++] + 2; // i 自增两次
4.7. ++ 和 -- 是否可以处理 float 型变量?
可以,自增和自减可以用于所有数值类型,但是很少使用它们处理 float 类型变量。如下所示:
float f = 1.3;
printf("%f",++f);
4.8. 表达式的副作用(side effect)
表达式有两种功能,每个表达式都产生一个值(value),同时可能包含副作用(side effect)。副作用是指改变了某些变量的值。如下所示:
20 // 这个表达式的值是 20,它没有副作用,因为它没有改变任何变量的值。
x=5 // 这个表达式的值是 5,它有一个副作用,因为它改变了变量 x 的值。
x=y++ // 这个表达示有两个副作用,因为改变了两个变量的值。
x=x++ // 这个表达式也有两个副作用,因为变量 x 的值发生了两次改变。
4.9. 顺序点(sequence point)
表达式求值规则的核心在于顺序点。
- 顺序点的意思是在一系列步骤中的一个「结算」的点,C 语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。
- C 标准规定代码执行过程中的某些时刻是 Sequence Point,当到达一个 Sequence Point 时,在此之前的 Side Effect 必须全部作用完毕,在此之后的 Side Effect 必须一个都没发。至于两个 Sequence Point 之间的多个 Side Effect 哪个先发生哪个后发生则没有规定,编译器可以任意选择各 Side Effect 的作用顺序。
- C 语言中常见顺序点的位置有:
- 分号
;
- 未重载的逗号运算符的左操作数赋值之后,即
;
处。 - 未重载的
||
运算符的左操作数赋值之后,即||
处。 - 未重载的
&&
运算符的左操作数赋值之后,即&&
处。 - 三元运算符
? :
的左操作数赋值之后,即?
处。 - 在函数所有参数赋值之后但在函数第一条语句执行之前。
- 在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前。
- 在每一个完整的变量声明处有一个顺序点,例如
int i, j;
中逗号和分号处分别有一个顺序点。 - for 循环控制条件中的两个分号处各有一个顺序点。
- 分号
第5章 选择语句
5.1. 表达式 i < j < k
是否合法?
此表达式是合法的,相当于 (i < j) < k
,首先比较 i 是否小于 k,然后用比较产生的结果 1 或 0 来和 k 比较。
5.2. 如果 i 是 int 型,f 是 float 型,则条件表达式 i > 0 ? i : f
是哪一种类型的值?
当 int 和 float 混合在一个表达式中时,表达式类型为 float 类型。如果 i > 0 为真,那么变量 i 转换为 float 型后的值就是表达式的值。
第7章 基本类型
7.1. 读 / 写整数
- 读写无符号数时,使用 u、o、x 代替 d。
- u:表示十进制。
- o : 表示八进制。
- x:表示十六进制。
- 读写短整型时,在 d、u、o、x 前面加上 h。
- 读写长整型时,在 d、u、o、x 前面加上 l。
7.2. 转义字符(numeric escape)
在 C 语言中有三种转义字符,它们是:一般转义字符、八进制转义字符和十六进制转义字符。
- 一般转义字符:这种转义字符,虽然在形式上由两个字符组成,但只代表一个字符。常用的有:
-
\a
\n
\t
\v
\b
\r
\f
\\
\’
\"
-
- 八进制转义字符:
- 它是由反斜杠
\
和随后的 1~3 个八进制数字构成的字符序列。例如,\60
、\101
、\141
分别表示字符0
、A
和a
。因为字符0
、A
和a
的 ASCII 码的八进制值分别为 60、101 和 141。字符集中的所有字符都可以用八进制转义字符表示。如果你愿意,可以在八进制数字前面加上一个 0 来表示八进制转移字符。
- 它是由反斜杠
- 十六进制转义字符:
- 它是由反斜杠
/
和字母 x(或 X)及随后的 1~2 个十六进制数字构成的字符序列。例如,\x30
、\x41
、\X61
分别表示字符0
、A
和a
。因为字符0
、A
和a
的 ASCII 码的十六进制值分别为
0x30、0x41 和 0x61。可见,字符集中的所有字符都可以用十六进制转义字符表示。
- 它是由反斜杠
- 由上可知,使用八进制转义字符和十六进制转义字符,不仅可以表示控制字符,而且也可以表示可显示字符。但由于不同的计算机系统上采用的字符集可能不同,因此,为了能使所编写的程序可以方便地移植到其他的计算机系统上运行,程序中应少用这种形式的转义字符。
7.3. 读字符的两种惯用法
while (getchar() != '\n') /* skips rest of line */
;
while ((ch = getchar()) == ' ') /* skips blanks */
;
7.4. sizeof 运算符
sizeof 运算符返回的是无符号整数,所以最安全的办法是把其结果转化为 unsigned long 类型,然后用 %lu 显示。
printf("Size of int: %lu\n", (unsigned long)sizeof(int));
7.5. 为什么使用 %lf 读取 double 的值,而用 %f 进行显示?
- 一方面,函数 scanf 和 printf 有可变长度的参数列表,当调用带有可变长度参数列表的函数时,编译器会安排 float 自动转换为 double,其结果是 printf 无法分辨 float 和 double。所以在 printf 中 %f 既可以表示 float 又可以表示 double。
- 另一方面,scanf 是通过指针指向变量的。%f 告诉 scanf 函数在所传地址上存储一个 float 类型的值,而 %lf 告诉 scanf 函数在所传地址上存储一个 double 类型的值。这里两者的区别很重要,如果给出了错误的转换,那么 scanf 可能存储错误的字节数量。
第11章 指针
11.1 指针总是和地址一样么?
通常是,但不总是。在一些计算机上,指针可能是偏移量,而不完全是地址。
char near *p; /*定义一个字符型“近”指针*/
char far *p; /*定义一个字符型“远”指针*/
char huge *p; /*定义一个字符型“巨”指针*/
近指针、远指针、巨指针是段寻址的 16bit 处理器的产物(如果处理器是 16 位的,但是不采用段寻址的话,也不存在近指针、远指针、巨指针的概念),当前普通 PC 所使用的 32bit 处理器(80386 以上)一般运行在保护模式下的,指针都是 32 位的,可平滑地址,已经不分远、近指针了。但是在嵌入式系统领域下,8086 的处理器仍然有比较广泛的市场,如 AMD 公司的 AM186ED、AM186ER 等处理器,开发这些系统的程序时,我们还是有必要弄清楚指针的寻址范围。
- 近指针
- 近指针是只能访问本段、只包含本段偏移的、位宽为16位的指针。
- 远指针
- 远指针是能访问非本段、包含段偏移和段地址的、位宽为32位的指针。
- 巨指针
- 和远指针一样,巨指针也是 32 位的指针,指针也表示为 16 位段:16 位偏移,也可以寻址任何地址。它和远指针的区别在于进行了规格化处理。远指针没有规格化,可能存在两个远指针实际指向同一个物理地址,但是它们的段地址和偏移地址不一样,如 23B0:0004 和 23A1:00F4 都指向同一个物理地址 23B04!巨指针通过特定的例程保证:每次操作完成后其偏移量均小于 10h,即只有最低 4 位有数值,其余数值都被进位到段地址上去了,这样就可以避免 Far 指针在 64K 边界时出乎意料的回绕的行为。
11.2. const int * p、int * const p、const int * const p
- const int * p
- 保护 p 指向的对象。
- int * const p
- 保护 p 本身。
- const int * const p
- 同时保护 p 和它指向的对象。
第12章 指针和数组
12.1. * 运算符和 ++ 运算符的组合
表达式 | 含义 |
---|---|
*p++ 或 *(p++) | 自增前表达式的值是 *p,然后自增 p |
(*p)++ | 自增前表达式的值是 *p,然后自增 *p |
*++p 或 *(++p) | 先自增 p,自增后表达式的值是 *p |
++*p 或 ++(*p) | 先自增 *p,自增后表达式的值是 *p |
12.2. i[a] 和 a[i] 是一样的?
是的。
对于编译器而言,i[a] 等同与 *(i+a),a[i] 等同与 *(a+i),所以两者相同。
12.3. *a 和 a[]
- 在变量声明中,指针和数组是截然不同的两种类型。
- 在形式参数的声明中,两者是一样的,在实践中,*a 比 a[] 更通用,建议使用 *a。
第13章 字符串
13.1. 字符串字面量的赋值
char *p;
p = "abc";
这个赋值操作不是复制 "abc" 中的字符,而是使 p 指向字符串的第一个字符。
13.2. 如何存储字符串字面量
- 从本质上讲,C 语言将字符串字面量作为字符数组来处理,为长度为 n 的字符串字面量分配 n+1 的内存空间,最后一个空间用来存储空字符
\0
。 - 既然字符串字面量作为数组来储存,那么编译器会将他看作 char* 类型的指针。
13.3. 对指针添加下标
char ch
ch = "abc"[1];
ch 的新值将是 b。
如下,将 0 - 15 的数转化成等价的十六进制:
char digit_to_hex_char (int digit)
{
return "0123456789ABCDEF"[digit];
}
13.4. 允许改变字符串字面量中的字符
char *p = "abc";
*p = 'b'; /* string literal is now "bbc" */
不推荐这么做,这么做的结果是未定义的,对于一些编译器可能会导致程序异常。
- 针对 "abc" 来说,会在 stack 分配 sizeof(char *) 字节的空间给指针 p,然后将 p 的值修改为 "abc" 的地址,而这段地址一般位于只读数据段中。
- 在现代操作系统中,可以将一段内存空间设置为读写数据、只读数据等等多种属性,一般编译器会将 "abc" 字面量放到像 ".rodata" 这样的只读数据段中,修改只读段会触发 CPU 的保护机制 (#GP) 从而导致操作系统将程序干掉。
13.5. 字符数组和字符指针
char ch[] = "hello world";
char *ch = "hello world";
两者区别如下:
- 在声明为数组时,就像任意元素一样,可以修改存储在 ch 中的字符。在声明为指针时,ch 指向字符串字面量,而修改字符串字面量会导致程序异常。
- 在声明为数组时,ch 是数组名。在声明为指针时,ch 是变量,这个变量可以在程序执行期间指向其他字符串。
13.6. printf 和 puts 函数写字符串
- 转换说明 %s 允许 printf 写字符串,printf 会逐个写字符串的字符,直到遇到空字符串为止(如果空字符串丢失,则会越过字符串末尾继续写,直到在内存某个地方找到空字符串为止)。
char p[] = "abc"; printf("p=%s\n",p);
- 转换说明
%m.ps
和%-m.ps
显示字符串- m 表示在大小为 m 的域内显示字符串,对于超过 m 个字符的字符串,显示完整字符串;对于少于 m 个字符的字符串,在域内右对齐。为了强制左对齐,在 m 前加一个负号。
- p 代表要显示的字符串的前 p 个字符。
-
%m.ps
表示字符串的前 p 个字符在大小为 m 的域内显示。
- puts 的使用方式如下,str 就是需要显示的字符串。在写完字符串后,puts 总会添加一个额外的换行符。
puts(str);
13.7. scanf 和 gets 函数读字符串
- 转换说明 %s 允许 scanf 函数读入字符串,如下所示。不需要在 str 前加运算符 &,因为 str 是数组名,编译器会自动把它当作指针来处理。
scanf("%s", str);
- 调用时,scanf 会跳过空白字符,然后读入字符,并且把读入的字符存储到 str 中,直到遇到空白字符为止。scanf 始终会在字符串末尾存储一个空字符
\0
。 - 用 scanf 函数读入字符串永远不会包括空白字符。因此,scanf 通常不会读入一整行输入。
- 调用时,scanf 会跳过空白字符,然后读入字符,并且把读入的字符存储到 str 中,直到遇到空白字符为止。scanf 始终会在字符串末尾存储一个空字符
- gets 函数可以读入一整行输入。类似 scanf,gets 函数把读到的字符存储到数组中,然后存储一个空字符。
gets(str);
- 两者区别:
- gets 函数不会在开始读字符之前跳过空白字符(scanf 函数会跳过)。
- gets 函数会持续读入直到找到换行符为止(scanf 会在任意空白符处停止)。
- gets 会忽略换行符,不会把它存储到数组里,而是用空字符代替换行符。
- scanf 和 gets 函数都无法检测何时填满数据,会有数组越界的可能。
- 使用 %ns 代替 %s 可以使 scanf 更安全,n 代表可以存储的最大字符数量。
- 由于 gets 和 puts 比 scanf 和 printf 简单,因此通常运行也更快。
13.8. 自定义逐个字符读字符串函数
- 在开始存储之前,不跳过空白字符。
- 在第一个换行符处停止读取(不存储换行符)。
- 忽略额外的字符。
#include <stdio.h>
#include <stdlib.h>
int read_line(char[] ,int);
int main(void)
{
char str[10];
int n = 10;
read_line(str, n);
printf("--- end ---");
return 0;
}
int read_line(char ch[], int n)
{
char tmp_str;
int i = 0;
while((tmp_str = getchar()) != '\n')
{
if(i<n)
{
ch[i++] = tmp_str;
}
}
ch[i] = '\0';
printf("message is:%s\n", ch);
return i;
}
13.9. 字符串处理函数
C 语言字符串库 string.h 的几个常见函数:
- strcpy 函数(字符串复制)
char* strcpy (char* s1, const char* s2);
- 把字符串 s2 赋值给 s1 直到(并且包括)s2 中遇到的一个空字符为止。
- 返回 s1。
- 如果 s2 长度大于 s1,那么会越过 s1 数组的边界继续复制,直到遇到空字符为止,会覆盖未知内存,结果无法预测。
- strcat 函数(字符串拼接)
char* strcat (char* s1, const char* s2);
- 把字符串 s2 的内容追加到 s1 的末尾
- 返回 s1。
- 如果 s1 长度不够 s2 的追加,导致 s2 覆盖 s1 数组末尾后面的内存,结果是不可预测的。
- strcmp 函数(字符串比较)
int strcmp (const char* s1, const char* s2)
- 比较 s1 和 s2,根据 s1 是否小于、等于、大于 s2,会返回小于、等于、大于 0 的值。
- strcmp 利用字典顺序进行字符串比较,比较规则如下:
-
abc
小于bcd
,abc
小于abd
,abc
小于abcd
。 - 比较两个字符串时,strcmp 会查看表示字符的数字码。以 ASCII 字符集为例:
- 所有大写字母(65 ~ 90)都小于小写字母(97 ~ 122)。
- 数字(48 ~ 57)小于字母。
- 空格符(32)小于所有打印字符。
-
- strlen 函数(求字符串长度)
size_t strlen (const char* s1)
- 返回 s1 中第一个空字符串前的字符个数,但不包括第一个空字符串。
- 当数组作为函数实际参数时,strlen 不会测量数组的长度,而是返回数组中的字符串长度。
13.10. 字符串惯用法
- strlen 的简单实现
size_t strlen(const char * str) {
const char *cp = str;
while (*cp++ )
;
return (cp - str - 1 );
}
- strcat 的简单实现
char* strcat ( char * dst , const char * src )
{
char * cp = dst;
while( *cp )
cp++; /* find end of dst */
while( *cp++ = *src++ ) ; /* Copy src to end of dst */
return( dst ); /* return dst */
}
13.11. 存储字符串数组的两种方式
- 二维字符数组
char planets[][8] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};
这种方式浪费空间,如下所示:
- 字符串指针数组
char *planets[] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};
推荐这种方式,如下所示:
13.12. read_line 检测读入字符是否失败
- 增加对 EOF 的判断。
int read_line(char ch[], int n)
{
char tmp_str;
int i = 0;
while((tmp_str = getchar()) != '\n' && ch != EOF)
{
if(i<n)
{
ch[i++] = tmp_str;
}
}
ch[i] = '\0';
printf("message is:%s\n", ch);
return i;
}
第14章 预处理器
14.1. #
运算符
宏定义中使用 #
运算符可以将宏的参数(调用宏时传递过来的实参)转化为字符串字面量,如下所示:
#define PRINT_INT(x) printf(#x " = %d\n", x)
宏展开后的结果是:
printf("x" " = %d\n", x) 等价于 printf("x = %d\n", x)
14.2. ##
运算符
宏定义中使用 ##
运算符可以将两个记号粘在一起,成为一个记号。如果其中有宏参数,则会在形式参数被实际参数替换后进行粘合。如下所示:
#define MK_ID(n) i##n
int MK_ID(1), MK_ID(2), MK_ID(3);
上述宏展开后结果为:
int i1, i2, i3;
14.3. 宏定义的作用范围
一个宏定义的作用范围通常到出现这个宏的文件末尾。
- 由于宏是由预处理器处理的,他们不遵从通常的范围规则。
- 一个定义在函数内的宏,并不仅在函数内起作用,而是作用的文件末尾。
14.4. 宏定义加圆括号
- 如果宏的替换列表有运算符,始终要将替换列表放在圆括号中。
- 如果宏有参数,参数每次在替换列表出现的时候,必须放在圆括号中。
14.5. 预定义宏
宏名字 | 宏类型 | 宏作用 |
---|---|---|
__LINE__ | 当前行的行数 | 整型常量 |
__FILE__ | 当前源文件的名字 | 字符串字面量 |
__DATE__ | 编译的日期(Mmm dd yyyy) | 字符串字面量 |
__TIME__ | 编译的时间(hh:mm:ss) | 字符串字面量 |
__STDC__ | 如果编译器接收标准 C,那么值为 1 | 整型常量 |
第15章 编写大规模程序
15.1. 共享宏定义和类型定义
15.2. 共享函数原型
- 文件 stack.c 中包含 stack.h,以便编译器检查 stack.h 中的函数原型是否与 stack.c 中的函数定义相匹配。
- 文件 calc.c 中包含 stack.h,以便编译器检查每个函数的返回类型,以及形式参数的数量和类型是否匹配。
15.3. 共享变量声明
在共享变量共享之前,不需要区分其定义和声明。
- 为了声明变量,写成如下形式,这样不仅声明了 i,也对 i 进行了定义,从而使编译器为 i 留出了空间:
int i; /* declares i and defines it as well */
- 在其他文件中,想要共享这个变量的话,需要声明没有定义的变量 i,使用关键字 extern,extern 提示编译器变量 i 是在其他程序中定义的,因此不需要为 i 分配空间。
extern int i; /* declares i without defining it */
15.4. 保护头文件
为了防止头文件多次包含,将用 #ifndef 和 #endif 两个指令把文件的内容闭合起来。例如,如下的方式保护 boolean.h:
#ifndef BOOLEAN_H
#define BOOLEAN
#define TRUE 1
#define FALSE 0
typedef int Bool;
#endif
第16章 结构、联合和枚举
16.1. 结构使用 = 运算符复制
数组不能使用 = 运算符复制,结构可以使用 = 运算符复制:
struct { int a[10]; } a1, a2;
a1 = a2; /* legal, since a1 and a2 are structures */
16.2. 表示结构的类型
C 语言提供了两种命名结构的方法:
- 声明 “结构标记”
struct part { int number; char name[NAME_LEN+1]; int on_hand; }; /* 结构标记的声明:右大括号的分号不能省略,表明声明的结束 */ struct part part1, part2; /* 结构变量的声明:不能漏掉单词 struct 来缩写这个声明 */ part part1, part2; /*** WRONG,part 不是类型名,没有 struct,part 没有任何意义 ***/ struct part { int number; char name[NAME_LEN+1]; int on_hand; } part1, part2; /* 结构标记的声明可以和结构变量的声明合在一起 */
- 使用 typedef 来定义类型名
typedef struct { int number; char name[NAME_LEN+1]; int on_hand; } Part; /* 类型 Part 的名字必须出现在定义的末尾,而不是在 struct 后面 */ Part part1, part2; /* 可以像内置类型一样使用 Part,由于 Part 是 typedef 定义的,所以不能写 Struct Part */
16.3. 联合的两个应用
- 使用联合来节省空间
- 使用联合来构造混乱的数据结构
-
实现数组的元素类型是 int 和 float 的混合
typedef union { int i; float f; } Number; Number number_array[1000]; number_array[0].i = 5; number_array[1].f = 8.396;
-
实现数组的元素类型是 int 和 float 的混合
16.4. 为联合添加 "标记字段"
为了判断联合中成员的类型,可以把联合嵌入一个结构中,且此结构还含有另一个成员 "标记字段",用来提示当前存储在联合中的内容的类型,如下所示:
#define INT_KIND 0
#define FLOAT_KIND 1
typedef struct {
int kind; /* tag field */
union {
int i;
float f;
} u;
} Number;
void print_number (Number n){
if (n.kind == INT_KIND){
printf("%d", n.u.i);
} else {
printf("%g", n.u.f);
}
}
16.5. 枚举
在一些程序中,我们可能需要变量只具有少量有意义的值,例如布尔类型应该只有两种可能,真值或假值。
C 语言为少量可能值的变量设计了枚举类型。
- 定义枚举标记和枚举变量。类似 struct 的定义,可以通过 "枚举标记" 或者 typedef 两者方法定义枚举类型。
enum bool { FALSE, TRUE }; /* 定义枚举标记 */ enum bool b1, b2; typedef enum { FALSE, TRUE } Bool; /* 使用 typedef 进行定义类型 Bool */ Bool b1, b2;
- 枚举作为整数
在系统内部,C 语言会把枚举变量和常量作为整数来处理。-
当没有为枚举常量指定值时,它的值是一个大于前一个常量的值(默认第一个枚举常量值为 0)。
enum colors { BLACK, WHITE, GRAY = 6, YELLOW, RED = 15 } ; /* BLACK = 0, WHITE = 1,YELLOW = 7 */
- 枚举的值和整数混用,编译器会把 c 当作整型处理,而
BLACK, WHITE, GRAY, YELLOW, RED
只是数0,1,2,3,4
的同义词。int i; enum { BLACK, WHITE, GRAY, YELLOW, RED } c; i = WHITE; /* i is now 1 */ c = 0; /* c is now 0 (BLACK)*/ c++; /* c is now 1 (WHITE)*/ i = c + 2; /* i is now 3 */
-
当没有为枚举常量指定值时,它的值是一个大于前一个常量的值(默认第一个枚举常量值为 0)。
- 用枚举声明联合的 "标记字段",这样做的优势不仅远离了宏 INT_KIND 和 FLOAT_KIND (他们现在是枚举常量),而且阐明了 kind 的含义。
typedef struct { enum { INT_KIND, FLOAT_KIND } kind; union { int i; float f; } u; } Number;
第17章 指针的高级应用
17.1. 内存分配函数
- malloc:分配内存块,但是不对内存块进行初始化。
- calloc:分配内存块,并且对内存块进行清除。
- realloc:调整先前分配的内存块。
17.2. 空指针
当调用内存分配函数时,无法定位满足我们需要的足够大的内存块时,函数会返回空指针。
- 空指针是 "指向为空的指针",这是区别与所有有效指针的特殊值。
- 程序员的责任是测试任意内存分配函数的返回值,并且在返回空指针时进行适当的操作。
对指针的处理惯用法如下:
p = malloc(1000);
if (p == NULL){
/* allocation failed; take appropriate action */
} /* 惯用法一 */
/***************************************************/
if ((p = malloc(1000)) == NULL){
/* allocation failed; take appropriate action */
} /* 惯用法二 */
/***************************************************/
p = malloc(1000);
if (!p){
/* allocation failed; take appropriate action */
} /* 惯用法三,C 语言中非空指针都为真,只有空指针为假 */
17.3. 使用 malloc 动态分配字符串
malloc 函数原型如下:
void *malloc ( size_t size );
- malloc 分配 size 字节的内存块,并且返回指向此内存块的指针。
- 通常情况下,可以把
void*
类型赋值给任何指针类型的变量。 - 为 n 个字符串分配空间,可以写成
p = malloc(n + 1);
。
返回指向 "新" 字符串的指针的函数,没有改变原来的两个字符串:
char *concat( const char *s1, const char *s2 ){
char *result;
result = malloc(strlen(s1) + strlen(s2) + 1);
if (result == NULL){
printf("Error: malloc failed in concat\n");
exit(EXIT_FAILURE);
}
strcpy(result, s1);
strcat(result, s2);
return result;
}
17.4. 使用 malloc 为数组分配内存空间
需要使用 sizeof 运算符来计算每个元素所需要的空间大小:
int *a;
a = malloc( n * sizeof(int) );
一旦 a 指向动态的内存块,就可以把 a 当作数组的名字。
17.5. 使用 calloc 为数组分配内存
calloc 函数原型如下:
void *calloc ( size_t nmemb, size_t size );
- calloc 函数为 nmemb 个元素的数组分配内存空间,其中每个元素的长度都是 size 个字节。
- 如果要求的空间无效,那么此函数返回空指针。
- 在分配了内存以后,calloc 会通过对所有位设为 0 的方式进行初始化。
通过调用以 1 为第一个实际参数的 calloc 函数,可以为任何类型的数据项分配空间。
struct point {
int x;
int y;
} *p;
p = calloc(1, sizeof(struct point)); /* p 执行结构,且此结构的成员 x,y 都会被设为 0 */
17.6. 使用 realloc 函数调整先前分配的内存块
一旦为数组分配完内存,后面 realloc 函数可以调整数组的大小使它更适合需要。
realloc 的原型如下:
void *realloc (void *ptr, size_t size);
当调用 realloc 时,ptr 必须指向内存块,且此内存块一定是先前通过 malloc 函数、calloc 函数或 realloc 函数的调用获得的。
size 表示内存块的新尺寸,可能会大于或小于原有尺寸。
-
C 标准中关于 realloc 函数的规则:
- 当扩展内存块时,realloc 函数不会对添加进内存块的函数进行初始化。
- 如果 realloc 函数不能按要求扩大内存块,那么它会返回空指针,并且原有内存块中的数据不会发生改变。
- 如果 realloc 函数调用时以空指针作为第一个实际参数,那么它的行为就像 malloc 函数一样。
- 如果 realloc 函数调用时以 0 作为第二个实际参数,那么它会释放掉内存块。
-
realloc 使用建议:
- 当要求减少内存块大小时,realloc 函数应该在 "适当位置" 缩减内存块,而不需要移动存储在内存块中的数据。
- 当要求扩大内存块大小时,realloc 函数应该始终试图扩大内存块而不需要对其进行移动。
- 如果无法扩大内存块(因为内存块后面的字节已经用于其他目的),realloc 函数会在别处分配新的内存块,并把旧块中的内容复制到新块中。
- 一旦 realloc 函数返回,一定要对指向内存块的所有指针更新,因为 realloc 函数可能移动了其他地方的内存块。
17.7. 释放内存 free 函数
free 函数原型如下:
void free(void* ptr);
使用 free 函数,只要把指向不再需要内存块的指针传递给 free 函数即可,如下所示:
p = malloc (...);
q = malloc (...);'
free (p);
p = q;
- free 函数的实际参数必须是指针,而且此指针一定是先前被内存分配函数返回的。
悬空指针(dangling point)的问题:
- 调用
free(p);
释放了 p 指向的内存块,但是 p 本身不会改变。如果忘记了 p 不再指向有效的内存块,可能会出现问题,如下所示:char *p = malloc(4); free(p); strcpy(p, "abc"); /*** WRONG。错误,修改 p 指向的内存是严重错误的,因为程序不再对此内存有控制权。***/
- 悬空指针很难发现,因为几个指针可能指向相同的内存块。在释放内存块时,全部指针可能都留有悬空。
第18章 声明
18.1. 声明的语法
在大多数通过格式中,声明格式如下:
[ 声明的格式 ] 声明说明符 声明符;
声明说明符(declaration specifier)描述声明的数据项的性质。声明符(declarator)给出了数据项的名字,并可以提供关于数据项的额外信息。
- 声明说明符分为以下三类:
- 存储类型:存储类型一共四种,auto、static、extern 和 register。声明中最多出现一种存储类型,如果出现存储类型,则需要放在声明的首要位置。
- 类型限定符:只有两种类型限定符,const 和 volatile。声明可以指定一个、两个限定符或者一个也没有。
- 类型说明符:关键字 void、char、short、int、long、float、double、signed 和 unsigned 都是
类型说明符。这些出现的顺序不是问题(int unsigned long 和 long unsigned int 完全一样)。类型说明符也包括结构、联合和枚举的说明。用 typedef 创建的类型名也是类型说明符。
18.2. 变量的存储类型
C 程序中每个变量都具有 3 个性质:
-
存储期限
- 变量的存储期限决定了为变量预留和释放内存的时间。
- 具有自动存储期限的变量在所属块被执行时获得内存单元,并在块终止时释放内存单元。
- 具有静态存储期限的变量在程序运行期间占有同样的存储单元,也就是可以允许变量无限期的保留它的值。
-
作用域
- 变量的作用域是指引用变量的那部分文本。
- 变量可以有块作用域(变量从声明的地方一直到闭合块的末尾都是可见的)。
- 变量可以有文件作用域(变量从声明的地方一直到闭合文件的末尾都是可见的)。
-
链接
- 变量的链接确定了程序的不同部分可以分享此程序的范围。
- 具有外部链接的变量可以被程序中几个(或许全部)文件共享。
- 具有内部链接的变量只能属于单独一个文件,但是此文件中的函数可以共享这个变量。
- 无链接的变量属于单独一个变量,而且根本不能被共享。
变量的默认存储期限、作用域和链接都依赖于变量声明的位置:
- 在块内部(包括函数体)声明的变量具有自动存储期限、块作用域,并且无链接。
- 在程序的最外层,任意块外部声明的变量具有静态存储类型、文件作用域和外部链接。
- 如下图所示:
对许多变量而言,默认的存储期限、作用域和链接是可以符合要求的。当这些性质无法满足要求时,可以通过指定明确的存储类型来改变变量的特性:auto、static、extern 和 register。
-
auto 存储类型
- auto 存储类型只对属于块的变量有效。
- auto 类型是自动存储期限、块作用域,并且无链接。
- 在块内部的变量,默认是 auto 类型,不需要明确指定。
-
static 存储类型
- static 存储类型可以用于全部变量,不需要考虑变量声明的位置。
- 块外部声明变量和块内部声明变量的效果不同。
- 在块外部时,static 说明变量具有内部链接。
- 在块内部时,static 把变量的存储期限从自动变成了静态。
- 如下所示:
-
extern 存储类型
- extern 存储类型可以使几个源文件共享同一个变量。
- extern 声明中的变量始终具有静态存储期限。
- 变量的作用域依赖于变量的位置。
- 变量在块内部,具有块作用域。
- 变量在块外部,具有文件作用域。
- extern 变量的链接不是确定的。
- 如果变量在文件中较早的位置(任何函数定义的外部)声明为 static,那么它具有内部链接。
- 否则(通常情况),变量具有外部链接。
- 如下所示:
-
register 存储类型
- register 存储类型要求编译器把变量存储在寄存器中,而不是内存中。
- 指明 register 类型是一种要求,而不是命令。
- register 只对声明在块内的变量有效,和 auto 类型一样是自动存储期限、块作用域,并且无链接。
- 跟 auto 相比,由于寄存器没有地址,所以 register 存储类型使用取地址符
&
是非法的,
18.3. 函数的存储类型
函数的声明(和定义)可以包含存储类型,但是选项只有 extern 和 static。
- 函数在默认情况(不指明存储类型)下,具有外部链接,允许其他文件调用此函数。
- extern 说明函数具有外部链接,函数默认是 extern 类型,不需要明确使用 extern。
- static 说明函数具有内部链接,只能在定义函数的文件内调用此函数。
- 函数的形式参数具有和 auto 变量相同的性质:自动存储期限、块作用域,和无链接。
- 唯一能用于说明形式参数存储类型的就是 register。
四种类型中最重要的就是 extern 和 static 了,auto 没有任何效果,而现代编译器已经使得 register 变得废弃无用了。
18.4. 类型限定符 const
- const 用来声明一些类似于变量的对象,但这些变量是 “只读” 的。程序可以访问 const 型对象的值,但无法改变它的值。
- 不同于宏,不可以把 const 型对象用于常量表达式。
const int n = 10; int a[n]; /*** WRONG ***/
- 我们使用 const 主要是为了保护存储在数组中的常量数据。
18.5. 解释复杂声明
两条简单的规则可以用来理解任何的声明:
- 始终从内往外读声明符。换句话说,定位用来声明的标识符,并且从此处的声明开始解释。
- 在作选择时,始终先是
[]
和()
后是*
。- 如果
*
在标识符前面,而在标识符后面有[]
,那么标识符表示数组而不是指针。 - 如果
*
在标识符前面,而在标识符后面有()
,那么标识符表示函数而不是指针。
- 如果
18.6. 初始化式
- 为了方便,C 语言允许在声明变量时为他们指定初始值。
- 为了初始化变量,可以在声明符后面写
=
,然后在其后再跟上初始化式(不要把声明中的符号 = 和赋值运算符混淆,初始化和赋值不一样)。 - 控制初始化式的额外规则:
- 具有静态存储期限的变量的初始化式必须是常量。
- 如果变量具有自动存储期限,那它的初始化式不需要常量。
- 用大括号封闭的数组、结构或联合的初始化式只能包含常量表达式,不能有变量或函数调用。
第20章 低级程序设计
20.1. 结构中的位域
C 语言可以声明结构中其成员表示位域的结构。
- 例如,使用 16 位来存储日期,其中 5 位用于日,4 位用于月,7 位用于年。
- 利用位域,可以如下定义:
struct file_data { unsigned int day : 5; unsigned int mouth : 4; unsigned int year : 7; }
控制位域存储的技巧:
- 忽略位域的名字,未命名的位域通常用来作为字段间的 "填充",以保证其他位域存储在适当的位置。
struct file_data { unsigned int : 5; /* not used */ unsigned int mouth : 4; unsigned int year : 7; }
- 指定未命名的字段长度为 0。长度为 0 的位域是给编译器一个信号,告诉编译器将下一个位域放在一个存储单元的起始位置。
struct file_data { unsigned int a : 4; unsigned int : 0; /* 如果存储单元是 8 位,编译器会给 a 分配 4 位,跳过余下的 4 位到下一个存储单元,给 b 分配 8 位。 */ unsigned int b : 8; }
20.2. volatile 类型限定符
使用 volatile 类型限定符,我们可以通知编译器程序使用了内存空间 "易变" 的数据(例如从键盘缓冲区读取的数据)。
第21章 标准库
21.1 标准库概述
以下是标准库中的 15 个头。
- <assert.h> 诊断
- 仅包含 assert 宏,可以在程序中插入该宏,从而检测程序状态。一旦任何检查失败,程序终止。
- <ctype.h> 字符处理
- 包括用于字符分类及大小写转换的函数。
- <errno.h> 错误
- 提供了 errno(error number)。errno 是一个左值,可以在调用特定函数后进行检测,来判断调用过程中是否有错误发生。
- <float.h> 浮点型的特性
- 提供了用于描述浮点类型特性的宏,包括值的范围及精度。
- <limits.h> 整形的大小
- 提供了用于描述整数类型和字符类型的宏,包括它们的最大值和最小值。
- <locale.h> 本地化
- 提供一些函数来帮助程序适应针对一个国家或地区的特定行为方式。
- <math.h> 数学计算
- 提供了大量用于数学计算的函数。
- <setjmp.h> 非本地跳转
- 提供了 setjmp 函数和 longjmp 函数。
- <signal.h> 信号处理
- 提供了用于异常情况(信号)处理的函数,包括中断和运行时错误。
- <stdarg.h> 可变实际参数
- 提供函数可以处理不定个数个参数的工具。
- <stddef.h> 常用定义
- 提供了经常使用的类型和宏。
- <stdio.h> 输入/输出
- 提供大量用于输入输出的函数。
- <stdlib.h> 常用使用程序
- 包含大量无法归类于其他头的函数。
- <string.h> 字符串处理
- 提供用于字符串操作的函数。
- <time.h> 日期和时间
- 提供相应的函数来获取日期和时间、操纵时间和以多种方式显示时间。
第22章 输入 / 输出
22.1. 标准流
22.2. fopen 函数打开文件的模式
- 打开文本文件的模式
- 打开二进制文件的模式
22.3. 从命令行打开文件
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *fp;
if (argc != 2)
{
printf("usage: canopen filename\n");
return 2;
}
if ((fp = fopen(argv[1], "r")) == NULL)
{
printf("%s can't be opened\n", argv[1]);
return 1;
}
printf("%s can be opened\n", argv[1]);
fclose(fp);
return 0;
}
22.4. ...printf 类函数
使用符号
*
填充格式串中的常量
22.5. ...scanf 类函数
22.6. 复制文件
/* Copies a file */
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *source_fp, *dest_fp;
int ch;
if (argc != 3)
{
fprintf(stderr, "usage: fcopy source dest\n");
exit(EXIT_FAILURE);
}
if ((source_fp = fopen(argv[1], "rb")) == NULL)
{
fprintf(stderr, "Can't open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
if ((dest_fp = fopen(argv[2], "wb")) == NULL)
{
fprintf(stderr, "Can't open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}
while ((ch = getc(source_fp)) != EOF)
{
putc(ch, dest_fp);
}
fclose(source_fp);
fclose(dest_fp);
return 0;
}