Never save something for a special occasion. Every day in your life is a special occasion.
分类: C/C++
2010-07-08 23:44:14
一个C程序可能由多个分别编译的部分组成,这些不同部分通过一个通常称为连接器的程序合并成一个整体。
Separate Compilation
{ 目标模块、库 } -连接→载入模块(可执行文件)
external object、命名冲突、外部对象的引用
声明与定义、命名冲突与static修饰符
静态变量、本地全局变量、本地函数
检查外部类型
保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型 一般来说 是程序员的责任。而且,“相同的类型”应该是严格意义上的相同。
例如,考虑下面,在一个文件中包含定义:
char filename[] = "/etc/passwd";
而在另一文件中包含声明:
extern char *filename;
尽管在某些上下文环境中,数组与指针非常相似,但它们毕竟不同。
这两个filename的声明使用存储空间的方式是不同的;它们无法以一种合理的方式共存。
头文件
一个好方法可以避免编译连接报告重复定义、未定义等问题——每个外部对象只在一个地方声明,这个声明的地方就是头文件。每个引用该外部对象的模块需要包含该头文件;定义该外部对象的模块也需要包含该头文件。
file.h
extern char filename[];
file.c
#include "file.h"
char filename[]="/etc/passwd";
int getchar();
EOF
宏只是对程序的文件起作用。要小心宏替换带来的副作用,如 你很可能将toupper实现为宏:
#define toupper(c) \
((c)>='a" && (c)<='z' ? (c)+('A'-'a'):(c))
如果这样,你千万不要这样使用
toupper(*p++)
定义宏 assert
尝试:
#define assert(e) if(!e) _assert_error(__FILE__, __LINE__)
当你看到if 你应该相当可能是bug——“悬挂if”
if(x>0 && y>0)
assert(x>y);
else
assert(y>x);
解决if悬挂问题通常的方法是用{}将语句块括起来,但这里这样又带来新问题,展开后
if(x>0 && y>0)
{ ... };
else
{ ... };
不能用if,有没有其它方法实现相同的功能? 有,利用运算符求值顺序。
#define assert(e) \
( (void)( (e) || _assert_error(__FILE__, __LINE__) ))
得到的这个定义虽然很不直观,但它并不是故弄玄虚,这是一个朴实的过程——brain、pain、gain
你可能经常(常见)这样使用 #define
#define uchar unsigned char
这是不正确的,正确的方法是使用typedef
理由如下:
#define pchar char *
pchar p1, p2; // attempt to declare two pointers
// p1 is pointer to char.
// p2 is char !
随着世界各地越来越多的人们开始在不同的C语言实现上工作,某些库函数的性质几乎是注定要发生分化。今天,一个C程序员如果希望自己写的程序在另一个编程环境中也能够工作,他就必须掌握许多这类细小的判别。
标识符名称的限制
ANSI标准所能保证的只是,C实现必须能够区别出前6个字符不同的外部名称。而且这个定义中并没有区分大小写字母。
因此,为了保证程序的可移植性,谨慎地选择外部标识符是名称是重要的。比如两个函数分别命名为print_fields与print_float这样的命名方式就不恰当。
某些C语言实现把一个标识符中所有字符作有效字符处理。使用(这种)某种特性也许能给编程带来巨大的方便,但代价却是使程序失去了一部分潜在用户。
整数的大小
C语言的定义中对各种不同类型整数的相对长度作了一些规定:
1、3种类型的整数其长度的非递减的
long >=32bit >= int >= short >=16bit
非正式情况下可以说,short和int(普通整数)是16位,long是32位
2、一个普通(int类型)整数足够大以容纳任何数组下标
3、字符长度由硬件特性决定
字符是有符号整数还是无符号整数
只有在需要把一个字符值转换为一个较大的整数时,这个问题才变得重要起来。
如果编程者关注一个最高位是1的字符其数值究竟是正还是负,可以将这个字符声明为无符号字符(unsigned char)。
移位运算
如果关注整数右移时最高位补0还是1,则应将变量声明为无符号类型。
有符号整数右移并不等同于除以2的某次幂:
(-1) >> 1 != 0 而 (-1)/2 在大多数C实现上求值结果为0.
内存位置0
null指针并不指向任何对象。除非是用于赋值或者比较运算,出于其他任何目的使用null指针都是非法的。
除法运算时发生的截断
q = a/b;
r = a%b;
我们希望a、b、q、r 之间维护怎样的关系呢?
① q*b +r = a,这是必然的
② 如果 a改变符号,则q也改变符号,但绝对值不变
③ 如果 b> 0,则 0<=r
这3条性质是我们认为整数除法和余数操作所应该具备的。但不幸的是它们不同时成立。
考虑一个简单的例子:
1=3/2;
1=3%2;
如果改变被除数的符号,此时 (-3)/2 = ?
如果满足①② 则 q = -1,r = -1,此时③无法满足;
如果满足①③ 则 q = -2,r = 1,此时②无法满足。
大多数C实现选择放弃期望性质③,改为 sign(r) = sign(b)。
然而,C语言的定义只保证了①,以及当a>=0且b>0时,保证0<=r。
大多数时候只要程序员清楚地知道要做什么,该做什么,这个定义已经足够。
随机数的大小
ANSI C标准中定义了一个常数 RAND_MAX
大小写转换
考虑到效率和程序健壮性,大小写转换函数分别有宏和函数两种实现。
_toupper()、toupper()
最令人生厌的问题来自那些看似能工作,其实潜藏bug的程序。如果拿到程序问题就不加思索,动手就做,使之能运行就万事大吉,可以肯定 人得到的只是一个几乎能工作的程序。也许最重要是规避技巧是 知道自己在做什么,并且事前周密思考。在面临时间压力的情况下,对程序组合方式(架构)的理解尤为重要。
我已经有一个很好的编译器。但我知道任何C语言实现都无法捕捉到所有的程序错误(语法~ 逻辑~ 移植性~)。你认为再怎么不可能发生的事情,某些时候还是有可能发生的。一个健壮的程序应该预先考虑到这种异常情况。
Endian 的意思是“数据在内存中的字节排列顺序”,表示一个字在内存中或传送过程中的字节顺序。
BigEndian:最低地址存放最高字节,可称为高位优先。
与printf同族的2个函数 fprintf、sprintf
因为格式字符串决定了其余参数的类型,而且可以到运行时九建立格式字符串,所以C语言实现要检查printf函数的参数类型是否正确是异常困难的。你可能写出下面的句子而编译通过
printf("%d", 0.1); // error result
fprintf("error"); // forgot parammete "stderr"
简单格式类型
%c 字符;
%s 字符串;
%d 符号十进制数;
%u 无符号十进制数;
特别地,char、short类型的参数会被自动扩展为int类型。
%o、%x 用于打印8、16进制整数(不打印前缀);
%g、%f、%e 浮点数;
其中
%g 去掉尾缀0,保留6位有效数字。对于很小的数值(<10^-5)采用科学记数法表示。
%e 强制以科学记数法表示(指数形式)。
%f 与%e相反,强制以小数形式打印——即使打印出来会很长,如10000000000.000000
%% 打印一个%。
修饰符
在%与格式码之间。
整数的修饰符:
整数有3种类型,short、long 和一般整数。short作为参数时自动扩展为正常长度(int);我们仍然需要l通知printf某参数是long型整数,将l与其它整数格式符组合产生新的格式码 %ld、%lo、%lx、%lu。l修饰符只对用于整数的格式码有意义。
宽度修饰符:
指定最小输出宽度(不会截断输出域),对所有的格式码都有效(包括%%)。
精度修饰符:
由小数点和数字组成。其确切含意与格式码有关:
l 对于整数格式项 %d、%o%x、%u 精度修饰符指定了打印数字的最少位数(不足时前面补0)
l 对于 %e、%f,精度修饰符指定小数点后应出现的数字位数。特别地,.0时不打印小数点。
l 对于 %g 精度修饰符用于指定有效数字位数。
l 对于 %s 精度修饰符限制(截取)串长。
l 对于 %c、%%,精度修饰符被忽略。
标志
在%与域宽修饰符之间,以微调格式项的效果。
l 左对齐 -,如%-14s。提示:+不表示右对齐,只是普通符号。
l 号空白 如果打印正数和负数时希望数值部分对齐,而不想在正数前用'+',可使用空格。如% d。 如果希望在固定栏内按科学计数法打印数值 "%+e"(或"% e")将比"%e"更好,因为这时出现在非负数前面的正号(或空格)保证同列数值的小数点都会对齐。
-2.000000e+000 -2.000000e+000 -2.000000e+000
-1.000000e+000 -1.000000e+000 -1.000000e+000
0.000000e+000 0.000000e+000 0.000000e+000
+1.000000e+000 +1.000000e+000 +1.000000e+000
l # (略)
除了'+'和空格,其余的标志字符都是各自独立的。
精度与可变宽度
有时你可能需要像宏定义那样指定打印的宽度、精度,但预处理器的作用不能达到字符串内部,为此printf提供 * 允许间接指定域宽和精度。
printf("... %.*s ...", ..., NAMESIZE, name, ...);
例如 printf("%*.*s", 12, 5, str) 等效于 printf("%12.5s", str); 意思是 “限制12字符宽度内打印5个字符”
新增的格式码
%p 以某种形式打印指针。
%n 用于指出已打印的字符数,这个值被存储在一个地址处。
示例
int n, m;
printf("Add of m %p.\n%n", &m, &n);
printf("上一行打印字符数 %d\n", n);
结果
Add of m 0012FF78。
上一行打印字符数 18
有两种方式实现“可变参数列表”。如果你觉得难懂,可以路过第一种方法
第一种方式
老方式,使用varargs.h。varargs.h定义一组宏实现可变参数机制
typedef char * va_list;
#define va_dcl int va_alist
#define va_start(list) list=(char *)&va_alist
#define va_end(list)
#define va_arg(list, mode) \
((mode *)( list+=sizeof(mode)))[-1]
简单说明下
va_alist 参数是整个可变参数列表,包括 格式串 和 变量表;
va_del 函参声明。这属于老版本的C函数定义,它允许将函参的类型说明放在括号外面。
va_start 取得整个可变参数列表 的起始地址;
va_arg 实现2个功能,取得 list的第一个参数,使 list指向下一个参数。
这个实现兵基于事实:底层的C实现要求函参在内存中连续存储。这样我们只要知道当前参数的地址,就能访问参数列表中其它参数,并且通过检查第一个参数(字符串)来得到其它参数的数目与类型。
printf的实现
int printf(va_alist) va_del
{
va_list ap; // 定义一个char *指针,用于指向参数列表(中的参数)
char * format; // 定义一个char *指针,用于指向参数列表中的格式串
int n;
va_start(ap); // ap 指向 va_alist
format = va_arg(ap, char *); // format 指向格式串, ap指向剩余的参数(真正的参数列表)
n = vprintf(format, ap); // 按格式串format指定的格式,打印参数列表 ap
return n; // 返回成功打印的项目数
}
第二种方式
ANSI 版本实现
stdarg.h 是ANSI版的varargs.h以固定参数作为可变参数的基础——varargs.h 也是基于这样的事实,只是stdarg.h 将格式串从“可变参数列表”中分离出来, 这样显得明了。
#include
#include
void error(char * format, ...)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, "err: ");
vprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
老版本“可变参数函数”的实现把我们引入机器深层,指针是如此巧妙。
如果你的看完上面的讲述还不清楚事情是怎么回事,我再补充一下
何时需要“可变参数函数”?
我需要一个像printf一样的输出函数,它接受可变参数,并能将输出定向到文件,或者 打印前输出一些其它信息,如时间、信息来源(IP)—— 要实现这个 需要将vfprintf、printf封装起来 如将错误信息 和 警告信息 输出到 文件 error.log、alarm.log,同时打印在屏幕上,这在调试程序时是很有用的。
总结
varargs.h、stdarg.h 可变参数函数的新旧版本实现工具,它们都基于这样的事实:以固定参数(格式串)作为可变参数的基础。区别在于:varargs.h 将格式串视为可变参数表的一部分,需要程序员分离;而stdarg.h版将格式串视为固定参数,不需要程序员对可变参数做任何处理。
作者语录
糟糕的手艺人常常责怪自己的工具。
当你手里拿着锤子时,整个世界都成了钉子。
如果你选择了某种工具,那你就必须负责选择合适的设计方案。
所谓面向对象编程,就是使用继承和动态绑定机制编程。