“拷贝”的重要性
在C语言程序开发中,函数的参数有时候需要拷贝一份才能安全的执行功能,下面是一个经常在面试题中出现的典型例子,请看:
double pow2(double *x)
{
return (*x)*(*x);
}
上面这段C语言代码很简单,无非就是定义了 pow2() 函数接收参数 x,并计算其指向的值的平方值。但是读者应该明白的是,这样的写法是不安全的。上述C语言代码在某种程度上等价于下面这种写法,请看:
double pow2(double *x)
{
double a = *x;
double b = *x;
return a*b;
}
从等价代码(或者汇编代码)可以看出,C语言函数 pow2 读取 x 指向的值实际上是分为两步的,所以下面这种情况就有可能发生:
x 指向的值为 3.14,程序员调用 pow2() 函数,原本应该得到 3.14 * 3.14 的结果。不过,x 指向的值随时可能改变,也就是说可能 a = 3.14,但是在执行 b = * x 之前,x 指向的值改变了,这就会导致 b 不等于 3.14,最终得到奇怪的结果。
这种“奇怪的结果”很难被发现,被称为“幽灵般”的 bug。
所以,实现 pow2() 函数时,更安全的做法是拷贝一份 x 指向的值,相关C语言代码如下,请看:
double pow2(double *x)
{
// return (*x)*(*x); // 不安全
double a = *x;
return a*a;
}
虽然多了一行“拷贝代码”,但是 pow2() 的功能却安全和稳定许多。事实上,在 Linux 操作系统内核源码中,有相当多的这种“拷贝代码”。
“拷贝代码”的麻烦之处
从上例可以看出,C语言函数在运行之前,先拷贝参数还是很有必要的。不过应该明白,要拷贝和使用C语言函数的参数,需要实现知道其数据类型,例如 pow2() 函数关心的参数是 double 型,所以用于拷贝参数 x 指向值的变量 a 是 double 型的。
如果 pow2() 函数的参数数据类型被修改了,那函数内部拷贝参数的变量数据类型也需要做同步修改,这样就略显繁琐了。一个小技巧是使用 typeof 关键字,请看下面的C语言代码示例:
double pow2(double *x)
{
typeof(*x) a = *x;
return a*a;
}
顾名思义,typeof() 可以将传递给它的变量的数据类型取出,并且以该数据类型定义变量。上面的C语言代码中,因为 * x 是 double 型的,所以:
typeof(*x) a;
// 等价于
double a;
这样一来,即使后来 pow2() 的参数数据类型被修改,函数内部的逻辑也无需再做改动了,因为 typeof() 可以自动的取初修改后的数据类型。
typeof 的妙用
事实上,typeof 在C语言程序开发中相当好用,Linux 操作系统内核中有大量使用 typeof 的地方。typeof 常常和 ({...}) 符号联合使用,例如:
#define max(a,b) \
({ typeof (a) _a = (a); \
typeof (b) _b = (b); \
_a > _b ? _a : _b; })
上面这段C语言代码定义了一个安全求最大值的宏,typeof 可以自动的取出传入变量的数据类型,因此该宏可以求任意数据类型的最大值。
按照我们前面的讨论,使用函数式宏定义,可能会产生多次“副作用”引起的不预期结果,而这里的 max 宏显然可以避免这种情况出现。
想想为什么?
关于 typeof 关键字,下面是一些比较常用的使用实例,相关C语言代码如下,请看:
定义一个 x 指向的变量 y:
typeof(*x) y;
定义一个 x 指向的变量数组:
typeof(*x) y[4];
定义一个 char 型的指针数组 y:
typeof(typeof(char *)[4]) y;
它和下面这行C语言代码是等价的:
char *y[4];
如果读者理解了上面几条实例,写出下面这样看着更加直白的宏就简单了:
#define pointer(T) typeof(T *)
#define array(T, N) typeof(T [N])
现在我们再定义 char 型的指针数组就更加清晰明了了:
array (pointer (char), 4) y;
array 说明 y 是一个数组,它的类型是 char pointer(指针)型的,一共有 4 个元素。
小结
本节先讨论了在C语言函数中,拷贝值可能会改变的参数的重要性,并在此基础上讨论了C语言新特性关键字 typeof,可以看出,合理的使用它,我们能够写出更加清晰易懂的C语言代码,这对于后期维护,以及分享给他人,都是大有裨益的。