第三章 语义陷阱
3.1 指针与数组
C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来 。然而,C语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要“仿真”出一个多维数组就不是一件难事。
对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下表为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此,我们完全可以依据指针行为定义数组下标的行为。
int calendar[12][31];
该数组拥有12个数组类型的元素,其中每个元素都是一个拥有31个整型元素的数组。(而不是一个拥有31个数组类型的元素的数组,其中每个元素又是一个拥有12个整型元素的数组。)
sizeof ( calendar )的值是372(31×12)与sizeof ( int )的乘积。
如果calendar不是用于sizeof的操作数,而是用于其他的场合,那么calendar总是被转换成为一个指向calendar数组的起始元素的指针。
任何指针都是指向某种类型的变量
给一个指针加上一个整数,与给该指针的二进制表示加上同样的整数,两者的含义截然不同。如果ip指向一个整数,那么ip+1指向的是计算机内存中的下一个整数,在大多数现代计算机中,它都不同于ip所指向的下一个内存位置。
如果两个指针指向的是同一个数组中的元素,可以将两个指针相减,这样做是有意义的,例如:
int *q = p + i;
那么可以通过q - p 而得到i 的值。值得注意的是,如果p与q指向的不是同一个数组中的元素,即使它们所指向的地址在内存中的位置正好间隔一个数组元素的整数倍,所得的结果仍然是无法保证其正确性的。
int a[3];
如果在应该出现指针的地方,却采用了数组名来替换,那么数组名就被当作指向该数组下标为0的元素的指针。因此如果这样写,
p = a;
就是把数组a中下标为0的元素的地址赋值给p。注意,并没有写成:
p = &a;
这种写法在ANSI C中是非法的,因为&a是一个指向数组的指针,而p是一个指向整型变量的指针,它们的类型不匹配。
p指向数组a中下标为1的元素,可写成:
p = p +1;
等同于:
p++;
除了a被用作sizeof的参数这一情形,在其他所有的情形中数组名a代表了指向数组a中下标为0的元素的指针。
*a即数组a下标为0的元素的引用。
*a = 84;
就是将数组a中下标为0的元素的值设置为84。同样道理,*(a+1)是数组a中下标为1的元素的引用。概而言之,*(a+i)即数组a中下标为i的元素的引用,简记为a[i]。
由于a+i与i+a的含义一样,因此a[i]与i[a]也具有同样的含义。
int calendar[12][31];
int *p;
int i;
calendar[4]是calendar数组的第5个元素,是calendar数组中12个有着31个整型元素的数组之一。sizeof(calendar[4])的结果是31与sizeof(int)的乘积。
i = calendar[4][7];
i = *(calendar[4]+7);
i = *( *( calendar +4) + 7);
p=calendar;
这个语句是非法的。因为calendar是一个二维数组,即“数组的数组”,在此处的上下文中使用calendar名称将会将其转换为一个指向数组的指针;而p是一个指向整型变量的指针。
int (*monthp)[31];
monthp=calendar;
*monthp是一个拥有31个整型元素的数组。monthp就是一个指向这样的数组的指针。
monthp将指向数组calendar的第一个元素。
int month;
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; day < & (*monthp)[31]; dayp++)
*dayp = 0;
}
3.2 非数组的指针
字符串常量代表了一块包括字符串中所有字符以及一个空字符(’\0’)的内存区域的地址。
将两个字符串s和t连接成单个字符串r。
char *r;
strcpy (r, s);
strcat (r, t);
上面的方法并不能满足目标,不行的原因在于不能确定r指向何处。不仅要让r指向一个地址,而且r所指向的地址处还应该有内存空间可供容纳字符串,这个内存空间应该是以某种方式已经分配了的。
char *r, *malloc();
r = malloc ( strlen(s) + strlen( t ) + 1); //还需要一个空字符(’\0’)的空间
if ( !r) {
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
/*使用完后*/
free(r);
3.3 作为参数的数组声明
在C语音中,没有办法可以将一个数组作为函数参数直接传递。如果使用数组名作为参数,那么数组名会立刻被转换为指向该数组第一个元素的指针。
char hello[] = “hello”;
printf ( “%s\n”, hello);
等价于printf ( “%s\n”, &hello[0]);
int strlen ( char s[]){}
等价于 int strlen ( char *s ){}
3.4 避免“举隅法”
char *p, *q;
p = “xyz”;
p的值是一个指向由’x’、’y’、’z’和’\0’4个字符组成的数组的起始元素的指针。
q = p;
p和q现在是两个指向内存中同一地址的指针。
q[1] = ‘Y’;
q所指向的内存现在存储的是字符串’xYz’。
3.5 空指针并非空字符串
#define NULL 0
当常数0被转换为指针使用时,这个指针绝对不能被解除引用。换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。厦门的写法是完全合法的:
if ( p == (char *)0 )
但如果写成这样
if (strcmp (p, (char *)0 ) == 0)
就是非法了,原因在于库函数strcmp的实现中会包括查看它的指针参数所指向的内存中的内容的操作。
如果p是一个空指针,即使printf (p); 和 printf (“%s”, p); 的行为也是未定义的。
3.6 边界计算与不对称边界
int i, a[10];
for ( i=1; i<=10; i++)
a[i]=0;
实际上并不存在的a[10]被设置为0。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数组a之后的一个字(word)实际上是分配给了整型变量i。i被设置为0,所以陷入死循环。
3.7 求值顺序
C语言中只有4个运算符(&&、||、?:和, )存在规定的求值顺序。&&和||首先对左侧操作数求值,只有在需要时才对右侧操作数求值。a?b:c中,a先被求值,再求b或c。逗号运算符,首先对左侧操作数求值,然后再对右侧操作数求值。
分隔函数参数的逗号并不是逗号运算符。x和y在函数F(x,y)中的求值顺序是未定义的,而在函数g( (x,y))中却是先x后y。函数g只有一个参数。
C语言中其他所有运算符对其操作数求值的顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序。
运算符&&和||对于保证检查操作按照正确的顺序执行至关重要。例如,
if ( y != 0 && x/y > tolerance)
complain();
中,必须保证仅当y非0时才对x/y求值。
i = 0;
while (i < n)
y[i] = x[i++]; //或者y[i++]=x[i];
上面的做法是不正确的。上面的代码假设y[i]的地址将在i的自增操作执行之前被求值,这一点并没有任何保证!
3.8 运算符&&、||和!
3.9 整数溢出
无符号算术运算中,没有所谓的“溢出”一说:所有的无符号运算都是以2的n次方为模。如果算术运算中一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发生。但是,当两个操作数都是由符号整数时,“溢出”就有可能发生,而且“溢出”的结果是未定义的。当一个运算的结果发生“溢出”时,作出任何假设都是不安全的。
if ( a + b < 0)
complain();
以上检查a + b是否会“溢出”并不能正常运行。当a + b确实发生溢出时,所有关于结果如何的假设都不再可靠。
一种正确的方式是将a和b都强制转换为无符号整数:
if ( (unsigned)a + (unsigned)b > INT_MAX)
complain();
INT_MAX是一个已定义常量,代表可能的最大整数值。
不需要用到无符号算术运算的另一种可行方法是:
if ( a > INT_MAX – b)
complain();
3.10 为函数mian提供返回值
函数main与其他任何函数一样,如果未明显声明返回类型,那么函数返回类型就默认为整形。
一个返回值为整形的函数如果返回识别,实际上是隐含着返回了某个“垃圾”整数。