《C 陷阱与缺陷》笔记
2015-06-07
孙耀珠
编程语言
- 词法陷阱(Lexical pitfalls)
- 语法陷阱(Syntactic pitfalls)
- 语义陷阱(Semantic pitfalls)
- 连接(Linkage)
- 库函数(Library functions)
- 预处理器(Preprocessor)
- 可移植性缺陷(Portability pitfalls)
词法陷阱(Lexical pitfalls)
- 由 ALGOL 派生的编程语言如 Pascal 和 Ada,使用
:=
作为赋值运算符,而=
作为比较运算符。C 语言则使用了另一种表示法,以=
为赋值运算符,以==
为比较运算符。 - 在 C 语言中,
&
和|
是按位运算符,而&&
和||
是逻辑运算符。另外,^
表示按位异或,而不是乘方。 - C 编译器在词法分析时遵从贪心法,比如
y = x/*p
的/*
会被理解为注释的开始,而不是y = x / *p
。 0
开头的整型字面量将被视为八进制,因此切忌用0
来占位对齐。- 单引号引起的一个字符实际上代表一个整数,而用双引号引起的字符串代表一个无名的字符数组。如果误用单引号引起一个字符串,使用 Clang 等编译器会得到最后一个字符的整数值,而其他编译器也可能得到第一个字符的整数值。
语法陷阱(Syntactic pitfalls)
- 变量声明的含义为,右边表达式的值为左边所述的类型。同样的逻辑对于指针和函数声明也适用,如
const char *(*f)()
表示f
指向的函数返回值也是一个指针,它指向字符串常量。同时只需要将变量名去掉并在最外面加上圆括号,便可以得到该类型的类型转换符。 - 关于运算符优先级的常见错误:
if (flags & FLAG != 0) ...
// <=> if (flags & (FLAGS != 0)) ...
r = hi<<4 + low
// <=> r = hi << (4 + low)
// SHOULD BE: r = hi<<4 | low
while (c=getc(in) != EOF) putc(c, out);
// <=> c = (getc(in) != EOF)
运算符 | 结合性 | 分类 |
---|---|---|
() [] -> . |
→ | |
! ~ ++ -- - (type) * & sizeof |
← | 单目运算符 |
* / % |
→ | 算术运算符 |
+ - |
→ | |
<< >> |
→ | 移位运算符 |
< <= > >= |
→ | 关系运算符 |
== != |
→ | |
& |
→ | 按位运算符 |
^ |
→ | |
| |
→ | |
&& |
→ | 逻辑运算符 |
|| |
→ | |
?: |
← | 三目运算符 |
assignments | ← | 赋值运算符 |
, |
→ |
switch
语句的case
只是一个标号,分支结束不加break
控制流程会穿过下一个case
标号。- 调用无参函数时仍需要括号,否则单独的函数名只是计算函数的地址,而不会调用它。
else
始终跟最近的if
匹配,即使这两句没有被外层花括号包围。
if (x == 0)
if (y == 0) f();
else
g();
/* 等价于 */
if (x == 0) {
if (y == 0) f();
else g();
}
语义陷阱(Semantic pitfalls)
- 数组的所有操作都是通过指针实现的,如
a[i]
等价于*(a + i)
,因此该表达式也可以写成i[a]
。 - 除了进行
&
和sizeof
运算,数组名都会被转换为一个指向其起始元素的指针。而&array
会返回一个指向数组的指针类型,其值仍为起始元素的地址。 - 对于二维数组,它实际上相当于以数组为元素的数组,其下标和数组名的行为也是类似的。
- 如果使用数组名作为函数参数,那么它会被立即转换为指针。因此 C 语言会自动把作为参数的数组声明转换为相应的指针声明。如
size_t strlen(char s[])
等价于size_t strlen(char *s)
。 - 在 C 语言中,字符串字面量是一个编译时便初始化好的字符数组,对其做出修改可能会触发 bus error(OS X)。
&&
和||
遵循短路求值的原则,只有当左操作数无法确定逻辑运算的结果时,才对右操作数求值。- 如果没有为函数声明返回类型,那么返回类型默认为
int
。如果在主函数中没有写return
语句,Clang 等编译器会自动加上return 0
。
连接(Linkage)
- 通常 C 编译器(cc)等组件只负责独立地将每个源文件(.c)编译为目标文件(.o),因此利用目标文件和库文件生成可执行文件的工作都交给与 C 语言不相关的连接器(ld),包括处理命名冲突和外部引用。
extern
关键字可以声明外部变量,该变量的定义既可以在同一源文件内,也可以在不同源文件中。- 同一工程中不允许出现同名的全局变量或函数,这时使用
static
关键字可以将其作用域限制在源文件内,以解决命名冲突的问题。 - 如果一个函数在被定义或声明前被调用,那么它的返回值默认为
int
。 - 如果没有对函数形参类型进行声明,则调用时
float
类型参数会自动转换为double
类型,char
和short
会自动转换为int
类型。scanf
和printf
函数对参数的处理便是如此。 - 由于无法得知 C 语言的实现细节,连接器不检查不同源文件中的外部变量和函数声明和定义是否一致。
- 为避免上述问题,所有的外部声明应集中在头文件中;且实现这些定义的源文件也应包含此头文件,编译成功即可确保声明的正确性。
库函数(Library functions)
getchar
函数的返回值为int
类型,如果读取成功会将unsigned char
转换为int
返回,否则返回EOF
(-1)。若将getchar
返回值赋给char
类型,可能会导致 255 与 -1 混淆。- ANSI C 可通过
stdarg.h
实现可变参数列表,譬如:
#include <stdarg.h>
int printf(char *format, ...)
{
va_list ap; int n;
va_start(ap, format);
n = vprintf(format, ap);
va_end(ap);
return n;
}
预处理器(Preprocessor)
- 在宏定义中,宏名和形参列表之间不可以有空格。
- 尽量将宏定义的各个参数以及整个结果表达式用括号括起来,以免引起与优先级相关的问题。
- 要确保宏中的参数没有副作用,譬如:
#define max(a, b) ((a) > (b) ? (a) : (b))
int i = 1, biggest = x[0];
while (i < n)
biggest = max(biggest, x[i++]);
// <=> biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]))
// i++ 可能执行两次
- 尽量不要将宏定义为语句,否则会出现难以意料的结果,譬如:
if (x > 0 && y > 0)
assert(x > y);
else
assert(y > x);
#define assert(e) if (!(e)) assert_error(__FILE__, __LINE__)
// 如果这样定义,会出现 if-else 的嵌套问题
#define assert(e) { if (!(e)) assert_error(__FILE__, __LINE__); }
// 如果这样定义,花括号后会多出一个分号
#define assert(e) do { if (!(e)) assert_error(__FILE__, __LINE__); } while (0)
// 这是一个可行的定义
#define assert(e) ((e) || assert_error(__FILE__, __LINE__))
// 这是另一个可行的定义
- 尽量不要用宏代替
typedef
,如果#define IP int *
,则IP p1, p2;
中的p2
将是整型而不是整型指针。
可移植性缺陷(Portability pitfalls)
- ANSI 标准要求
short
和int
至少是 16 位,long
至少是 32 位,C99 要求long long
至少 64 位,但没有规定确切的大小。
Data model | short | int | long | long long | pointers / size_t | OS |
---|---|---|---|---|---|---|
ILP32 | 16 | 32 | 32 | 64 | 32 | Most 32-bit |
LLP64 | 16 | 32 | 32 | 64 | 64 | Windows 64-bit |
LP64 | 16 | 32 | 64 | 64 | 64 | Most Unix and Unix-like 64-bit |
char
默认是signed
还是unsigned
因环境而异,如 Android NDK 中的 GCC 默认是unsigned char
。long double
的实现也因编译器而异,可能是双精度的同义词、扩展精度(extended precision, 80-bit)、四倍精度(quadruple precision, 128-bit)、一对双精度浮点数(double-double arithmetic, 64-bit + 64-bit)。为了字节对齐,扩展精度(10 字节)可能会被存储为 12 / 16 字节。- C99 规定求余结果与被除数同号,相应地,整数除法向零取整;而 C99 以前对此没有明确的定义。
See also
C 语言应试笔记 | 梦断代码