C语言在许多不同的系统平台上都有实现;因此我们应该能够预料到,机器不同则其上的C语言实现也有细微差别;
7.1 应对C语言标准变更
作者写这本书的时候,还是1988年;对于2024年的我们,应该不需要考虑C语言标准变更的问题;对于标准版本变更而言,我们必须在当前需要产生的工作量和未来的收益之间做出选择;
7.2 标识符名称的限制
为了保证程序的可移植性,谨慎的选择外部标识符的名称是重要的;
// 错误示例,使用 含义相同的变量名称
int print_fields;
int print_float;
// 错误示例,变量名称仅仅知识大小写存在区别
char state;
char STATE;
定义的函数名称和库函数,仅仅只是大小写不一致;
ps: 但是现在的编译器都已经区分大小写了,可能作者那个时候还不支持;
// 错误示例, 如果编译器不区分大小写,则变为递归调用了
char *Malloc(unsigned n)
{
char *p,*malloc(unsigned);
p = malloc(n);
if (p == NULL)
panic("out of memory);
return p;
}
7.3 整数的大小
C语言为编程者提供了 3种不同长度的整数:short, int, long 型;但是在不同的硬件设备上(8位,13位,32位,64位),同一个类型对应的数据长度,可能是不一样的;
C语言的定义中,对各种不同类型整数的相对长度作了一些规定
1: 3种类型的整数其长度是非递减的;也就是说,short 型整数容纳的值肯定能够被 int型整数容纳,int型整数容纳的值也肯定能够被 long型整数容纳;
2:一个普通int型整数足够大,可以容纳任何数组的下标;
3:字符长度由硬件特性决定;
ANSI标准要求long型整数的长度至少应该是32位,而short和int型整数的长度至少应该是16位;
为了适配不同设备上的数据长度,我们最好使用的使用重新定义一个参数类型
// 当我们在不同的设备上的时候,只需要修改 Int16 的定义就行
#define short Int16
#define unsigned short UInt16
// 改动上面的#define 之后,这里的长度就会自动适配了
Int16 a;
UInt16 b;
7.4 字符是有符号整数还是无符号整数
有符号类型的最高bit位为1,当我们把 一个字符值转换为较大的整数时,可能会发现数据的符号位发生非预期的变化;
void func5()
{
char a=10;
char b=20;
char c = 10*20; // 溢出了
printf("c= %d \n",c ); // c= -56
}
当数据类型发生变化时候,也应该注意符号位是否发生变化;
void func6()
{
char a=-10;
int b=0;
memcpy(&b,&a,1); // b的符号位没有发生变化
printf("a=%d b=%d \n",a, b ); // a=-10 b=246
}
7.5 移位运算符
1:向右移位时,空出的位是由 0 填充,还是由符号位的副本填充呢?
如果操作对象是无符号数,空出的位被0填充;如果操作对象是有符号数,那么不同的编译器可能填充的结果是不一致的;尽量不要对有符号数,进行移位运算;
2:移位计数(即移位操作的位数)允许的取值范围是什么;
根据不对称边界计算,
int a=9; // 认为 int 型长度是32位
if ((n>=0) && (n<31))
a = a>>n;
以除法运算来代替移位运算,将可能导致程序运行速度大大减慢;
unsigned int low;
unsigned int high;
// something
mid = (low+high) >> 1; //执行速度更快
// 等价于
mid = (low+high) / 2;
7.6 内存位置0
null指针并不指向任何对象。因此,除非是用于赋值或比较运算,出于其它任何目的使用null指针都是非法的;
在所有的C程序中,误用 null 指针的效果都是未定义的,可能会造成程序崩溃问题;
#include <stdio.h>
main()
{
char *p;
p = NULL;
// 在禁止读取内存地址0的机器上,这个程序将会执行失败。在可以读取的机器上,会打印内存位置0中存储的字符内容
printf("location 0 contains %d \n", *p);
}
7.7 除法运算时发生的截断
// TODO 没看明白这里想说的重点是什么
7.8 随机数的大小
在不同的系统上,rand函数中随机数的取值范围是不一致的;
在16位系统上,rand 函数将返回一个介于 0 ~ (
2
15
2^{15}
215-1) 之间的整数
在32位系统上,rand 函数将返回一个介于 0 ~ (
2
31
2^{31}
231-1) 之间的整数
为了处理这个问题,ANSI C标准中定义了一个常数 RAND_MAX, 它的值等于岁数的最大取值;
7.9 大小写转换
库函数 toupper和tolower 也有与随机数类似的历史;
不同的库函数,可能具体的实现方式不一致;使用者需要特别注意
// 函数会判断传进来的参数是否是有效的
int toupper(int c)
{
if (c >= 'a' && c <= 'z')
return c+'A'-'a';
return c;
}
// 宏定义需要用户自己判断参数,不然可能出错
#define _toupper(c) ((c)+'A'-'a')
7.10 首先释放,然后重新分配
大多数C语言实现都为使用者提供了三个内存分配函数: malloc, realloc, free。
不同版本的C语言,可能会realloc的使用要求不一致;有的需要先释放,在realloc,使用时需要特别注意;
// 申请一块n大小的内存空间
int *p = malloc(n);
// 重新申请一块 m 大小的内存空间,可能涉及到内存的拷贝; min(n,m)部分存储的内容将保持不变
int *rea_p = realloc(p,m);
if (p != NULL)
free(p); // 将申请的内容空间释放掉
if (rea_p != NULL)
free(rea_p );
在某些版本的代码中,下面的代码是合法的
free(p);
p = realloc(p, new_size);
那么在如上系统中,我们可以使用下面的方法来释放 一个链表中的所有元素; 同时不必担心调用free之后,会是 p->next 变的无效;不过这种技巧不值得推荐,因为并非所有的C实现在某块内存被释放后还能较长时间的保留之;
for (p = head; p != NULL; p = p->next)
free(p);