C陷阱与缺陷:第三,四章
第三章 “语义”陷阱
1-1. 一维数组
数组名a 除了用作运算符sizeof的参数这一情形外,在其他所有的情形中数组名a都代表指向数组a中下标为0的元素的指针
a[i] 与 i[a] 的含义相等
1-2. 二维数组:
int calendar [12][31];
calendar[4] 是calendar的第五个元素,是calendar数组中12个有关31个整形元素的数组之一。因此,calendar是有着31个整形元素的数组的行为。
sizeof(calendar[4])的结果是31与sizeof(int)和乘积。
calendar[4][7] == *(calendar[4]+7) == *(*(calendar+4)+7)
int *p;
p = calendar; // 非法,
这是因为calendar是二维数组,calendar是一个指向数组的指针,而p是一个指针整形变量的指针,类型不符,非法。
解决办法是:让指针指向一个数组
int (*monthp)[31];
monthp = calendar; // 这样monthp指向calendar的第一个元素,也就是calendar的12个有着31个元素的数组类型元素之一
清空数组:
for (month = 0; month < 12; month++)
{
int day;
for (day = 0; day < 31; day++)
calendar[month][day] = 0;
}
上面这个应该算是最好理解的了。
用指针表示可以是:
int (*monthp)[31];
for (monthp = calendar; monthp < &calendar[12]; monthp++)
{
int *dayp;
for (dayp = *monthp; dayp < &(*monthp)[31]; dayp++)
*dayp = 0;
}
2. 两个字符串的连接
cahr *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1); // 为r申请内存空间,并且要多分配一个字符
if (!r) // 如果申请失败
{
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
free(r); // 释放malloc申请的内存空间
3. 作参数的数组声明
int strlen (char s[]) {… …} 与 int strlen (char *s) {… …}等价
main (int argc, char* argv[]) {… …} 与 main(int argc, char** argv) 等价
前者重点强调argv是一个指向某数组的起始元素的指针,该数组的元素为字符指针类型。
4. 避免“举偶法”
所谓“举偶法”是以整体代表部分或者以部分代表整体。
char *p, *q;
p = “xyz”;
q = p; // p, q两个指针指向同一地址,但没有复制内存中的字符
复制指针并不同时复制指针所指向的数据。
5. 空指针并非空字符串
当常数0被转换为指针使用时,这个指针绝对不能被解除引用。换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。
6. 边界计算与不对称边界
“栏杆错误”与常被称为“差一错误”,如果在条件判断表达式时,i 的值是开区间(<, >)还是闭区间(<=, >=)。这里的编程技巧是:用第一个入界点和第一个出界点来表示一个数值的范围。具体而言,我们不应说整数x满足边界条件x >= 16 且 x <= 37,而是说整数x满足边界条件x >= 16 且 x < 38。注意,这里下界是“入界点”,即包括在取值范围之中,而上界是“出界点”,即不包括在取值范围之中。因为如此,我们这样写:
int a[10] = i;
for (i = 0; i < 10; i++)
a[i] = 0;
而不是写成:
int a [10] = i;
for (i = 0; i <= 9; i++)
a[i] = 0;
7. 求值顺序
7-1. && 运算符引发的“短路法则”,即:
条件表达式1 && 条件表达式2
即只有条件表达式1为真时,条件表达式2才被执行,否则丢弃不予以计算。
7-2
i = 0;
while ( i < n)
y[i] = x[i++];
上面这种从数组x中复制前n个元素到数组y中的做法是不正确的,原因是它对求值顺序做了太多的假设。代码可以写成:
i = 0;
while (i < n)
{
y[i] = x[i];
i++;
}
或
for (i = 0; i < n; i++)
y[i] = x[i];
8. 逻辑运算符(&&, ||, !)与位运算符(&, |, ~)的颠倒
如果将某个逻辑运算符替换成对应的另一个位运算符,程序还能正常运行,这纯属是巧合。
如将下面的&&换成&,程序仍能运行,但这里存在两个“侥幸”:
i = 0;
while(i < tabsize && tab[i] != x)
i++;
第一个“侥幸”是,while中表达式&运算符的两侧都是比较运算,而比较去处的结果在为真时为1,为假时是0.只要x和y的取值都限制在0或1,那么x & y与 x && y总是得出相同的结果;第二个“侥幸”是,对于数组结尾之后的下一个元素(实际上不存在的),只要程序不去改变该元素的值,而仅仅读取它的值,一般情况下是不会有什么危险的。
9. 整数溢出
无符号运算不会“溢出”。
第四章 链接
编译器的责任是把C源程序“翻译”成对连接器有意义的形式,这样接连器就能够“读懂”C源程序了。
连接器的一个重要工作就是处理命名冲突。
1. extern int a; // 显示声明一个外部整形变量,即使这个变量是在一个函数内部声明的,也仍然有同样的含义。
2. 每个外部变量只能够定义一次。
3. static 修饰符是一个能够减少此类命名冲突的有效工具。
static int a;
其含义与下面的语句相同:
int a;
只不过,a的作用域限制在一个源文件内,对于其他源文件,a是不可见的。因此,如果若干个函数需要共享一组外部对象,可以将这些函数放到一个源文件中,把它们需要用到的对象也都在同一个源文件中以static修饰符声明。static不仅适用于修饰变量,也适用于函数。
4. 函数在被调用前都要声明或定义(声明一般只写一行用来告诉编译器,定义需要将函数功能的实现写出来)
5. 如果一个函数没有float, short或者char类型的参数,在函数声明中完全可以省略参数类型的说明,但在函数定义中不能省略参数类型的说明。
6. scanf() 函数格式类型修饰符与变量类型不符,将导致不可预知的错误。如:
char c;
scanf(“%d”, &c);
7. 保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型,是程序员的责任。如:
char filename[] = “/ect/passwd”;
而在另一个文件中包含声明:
extern char* filename;
尽管在某些上下文中,数组与指针非常类似,但它们毕竟不同。它们声明所使用的存储空间不同,无法以一种合乎情理的方式共存。
解决这一问题的策略是,使两个声明类型一致,可以是:
char filename[] = “/ect/passwd”;
extern char filename[];
也可以是:
char* filename = “/ect/passwd”;
extern char* filename;