少做之过的特性就是语言应该提供但未能提供的特性,如标准参数处理以及把lint程序错误从编译器中分离出来。
2.4.1 用户名中若有字母f,便不能收到邮件
用户名的第二个字母是f,邮件确实无法发送到他们那里
许多人对ANSI C采用argc、argv的约定向C程序传递参数感到惊奇,但事实就是如此。
UNIX的约定有所提升,达到了一个标准的层次,但此时却成了这个邮件Bug的原因之一。
/*分析方法像试探法。*/
if (argv[argc - 1][0] == '-' || (argv[argc - 2][1] == 'f')) {
readmail(argc, argv);
} else {
sendmail(argc, argv);
}
/*能正确读取邮件*/
mail -h -d -f -/usr/linden/mymailbox
/*读取邮件,而不是发送邮件*/
mail effie Robert
/*修改方案*/
if (argv[argc - 1][0] == '-' || argv[argc - 2][0] == '-' && (argv[argc - 2][1] == 'f')) {
readmail(argc, argv);
} else {
sendmail(argc, argv);
}
/*能正确读取邮件*/
mail -h -d -f -/usr/linden/mymailbox
/*能正确发送邮件*/
mail effie Robert
许多操作系统(如VAX/VMS)能够在程序中区分运行时选项和其他参数(如文件名),但UNIX却不能,ANSI C也不能。
软件信条
Shell参数解析
不充分的参数解析问题出现在UNIX的许多地方。找出目录中的那些文件是链接文件,你可能输入下面的命令:
ls -l | grep ->
缺少重定向的名字,->被shell翻译成重定向符。
ls -l | grep "->"
grep先看到减号,然后把整个参数翻译成大于号的一种未知组合形式,然后退出,要解决问题,必须放弃使用ls命令。
file -h * | grep link
创建一个文件,文件名以连字符开头,然后去发现无法用rm命令把连字符去掉。
一种解决方法是给出文件的完整路径名,这样rm就不会把连字符当做选项开关,并依次翻译文件名。
有些C程序员采用了一种约定,即带“--”的参数表示“从这里开始,没有参数是选项开关,即使它是以连字符开头”。
一种更好的解决方法是把包袱扔给系统而不是用户,使用参数处理器把参数分成选项开关和非选项开关两种。目前这种简单的argv机制由于使用得太广,因而不可能对它作任何修改。
2.4.2 空格---最后的领域
这里的几个例子,空格从根本上改变了程序的意思或程序的有效性。“\”字符用于对一些字符进行“转义”,包括newline(这里指回车键)。被转义的newline在逻辑上把下一行当做当前行的延续,它可用于连接长字符串。如果在“\”和回车键之间不小心留上一两个空格就会出现问题,\ newline \newline 效果是不一样的。
因为你是在寻找无形的东西(在应该是newline的地方出现了一个空格,注意newline并不是一个有形的字符,所以“\”后面有没有空格在实际代码中根本看不出来)。newline在典型情况下用于转义连续多行的宏定义。转义newline的另一种用处是延续一个字符串常量,如下:
char a[] = "Hi! How are you? I am quite a \
long string, folded onto 2 lines";
这种多行字符串常量的问题被ANSI C通过引入相邻字符串常量自动连接的约定得以解决。
如果所有的空格都弃之不用,也会陷入麻烦
z = y+++x;
//correct
z = y++ + x;
//error
z = y+ ++x
ANSI C规定了一种逐渐为人熟知的“maximal munch strategy”(最大一口策略)这种策略表示,如果下一个标记有超过一种的解释方案,编译器将选取能组成最长字符序列的方案。
z = y+++++x;
唯一有效的编排方式是:
z = y++ + ++x;
它还是会出现编译错误。
/*
** space.
*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
int x;
int y;
int z;
x = 1;
y = 2;
printf( "1:x = %d, y = %d\n", x, y );
z = x+++y;
printf( "1:x = %d, y = %d, z = %d\n", x, y, z );
x = 1;
y = 2;
printf( "2:x = %d, y = %d\n", x, y );
z = x++ +y;
printf( "2:x = %d, y = %d, z = %d\n", x, y, z );
x = 1;
y = 2;
printf( "3:x = %d, y = %d\n", x, y );
z = x+ ++y;
printf( "3:x = %d, y = %d, z = %d\n", x, y, z );
x = 1;
y = 2;
printf( "4:x = %d, y = %d\n", x, y );
/* z = y+++++x;
** can't pass compilation:
** because of [Error] lvalue required as increment operand
*/
z = y++ + ++x;
printf( "4:x = %d, y = %d, z = %d\n", x, y, z );
return EXIT_SUCCESS;
}
输出:
有两个指向int的指针并想对int数据进行除法运算时,代码如下。
除法运算符“/”与“*”操作符之间缺少空格。它们紧贴在一起,被编译器理解成注释的开始部分,并把它与下一个“*/”之间的所有代码都变成注释的内容。
int ratio = *x / *y;
int ratio = *x/*y;
/*打算结束注释时却由于意外未能结束*/
int hashval = 0;
/*PJW hash function from "Compilers: Principles, Techniques, and Tools"
*by Aho, Sethi, and Ullman, Second Edition.(*/)
while (cp < bound) {
unsigned long overflow;
hashval = (hashval << 4) + *cp++;
if ((overflow = hashval & (((unsigned long)OxF) << 28)) != 0) {
hashval ^= overflow | (overflow >> 24);
}
hashval %= ST_HASHSIZE; /*选择起始桶*/
/*
* 搜索每个表,这次搜索名字。如果失败,保存该字符串?
* 进入字符串的指针,然后返回它
*/
for (hp = &st_ihash; ;hp = hp->st_hnext) {
int probeval = hashval; /*下一个探测值*/
}
}
2.4.3 C++的另一种注释形式
把该符号以后直至行末的内容均作为注释内容。
a //*
//*/ b
上面的代码在C语言中表示a/b,但在C++语言中表示a。C风格的注释在C++语言中依然有效。
2.4.4 编译器日期被破坏
将源文件的timestamp转换为表示当地格式日期的字符串。
调用stat()得到UNIX格式的源文件修正时间。
调用localtime()将其转换成tm结构。
最后调用strftime()函数,把tm结构转换成以当地日期。
格式表示的ASCII字符串。
症状就是表示日期的字符串被破坏
#include <stdio.h>
#include <time.h>
char *localized_time(char *filename) {
struct tm *tm_ptr;
struct stat stat_block;
char buffer[120];
/*获得源文件的timestamp,格式为time_t*/
stat(filename, &stat_block);
/*把UNIX的time_t转换为tm结构,里面保存当地时间*/
tm_ptr = localtime(&stat_block.st_mtime);
/*把tm结构转换为以当地日期格式表示的字符串*/
strftime(buffer, sizeof(buffer), "%a %b %e %T %Y", tm_ptr);
return buffer; //program takes place
}
int main() {
char *p = localized_time("test.txt");
printf("date = %s\n", p);
return 0;
}
buffer是一个自动分配内存的数组,是该函数的局部变量。当控制流离开声明自动变量(即局部变量)的范围时,由于该变量已被销毁,谁也不知道指针所指向的地址的内容是什么。
在C语言中,自动变量是在堆栈中分配内存。当包含自动变量的函数或代码退出时,它们所占用的内存便被回收,它们的内容肯定会被下一个所调用的函数覆盖。这一切取决于堆栈中先前的自动变量位于何处,活动函数声明了什么变量,以及写入了什么内容。原先的变量地址的内容既可能被立即覆盖,也可能稍后才能覆盖,这就是日期破坏问题难以发现的原因。
//1.返回一个指向字符串常量的指针
char *func() {
return "Only works for simple strings";
/*只适用于简单的字符串*/
}
如果字符串常量存储于只读内存区但以后需要改写它时,你也会有麻烦。
//2.使用全局声明的数组
char *func() {
...
my_global_array[i] =
...
return my_global_array;
}
它的缺点在于任何人都有可能在任何时候修改这个全局数组,而且该函数的下一次调用也会覆盖该数组的内容。
//3.使用静态数组
char *func() {
static char buffer[20];
...
return buffer;
}
这就可以防止任何人修改这个数组。只有拥有指向该数组的指针的函数(通过参数传递给它)才能修改这个静态数组。但是,该函数的下一次调用将覆盖这个数组的内容,所以调用者必须在此之前使用或备份数组的内容。与全局数组一样,大型缓冲区如果闲置不用,是非常浪费内存空间的。
//4.显式分配一些内存,保存返回的值。
char *func() {
char *s = malloc(120);
...
return s;
}
这个方法具有静态数组的优点,而且在每次调用时都创建一个新的缓冲区,所以该函数以后的
调用不会覆盖以前的返回值。它适用于多线程的代码(在某一时刻具有一个以上的活动线程的程序)。它的缺点在于程序员必须承担内存管理的责任。
//5.也许最好的解决方法就是要求调用者分配内存来保存函数的返回值。为了提高安全性,调用者应该通知指定缓冲区的大小(就像标准库中fgets()函数所要求的那样)
void func(char *result, int size) {
...
strncpy(result, "That's be in the data segment, Bob", size);
}
buffer = (char*)malloc(size);
func(buffer, size);
...
free(buffer);
如果程序员可以在同一代码块同时进行malloc和free操作,内存管理是最为轻松的。这个解决方案可以实现这一点。
return local_array;
/*return local_array
*"function returns pointer to automatic"(函数返回一个指向自动变量的指针)。*/
2.4.5 lint程序不应该被分离出来
lint程序能够检测到问题,并向你发出警告。
把编译器中所有的语义检查措施都分离出来。错误检查由一个单独的程序完成,这个程序被称为lint。这样编译器可以做得更小、更快而且更简单。
小启发
早用lint程序,勤用lint程序
lint程序是软件的道德准则,当你做错事,他会告诉你那里不对。应该始终使用lint程序,按照它的道德标准办事。
几个真正严重的bug
实参的类型在函数和调用之间发生了转变
一个期望接受3个参数的函数实际上只传递它一个参数,该函数从堆栈中再抓两个参数。
变量在设置(初始化或赋值)前使用。
经验不断证明,把lint程序作为一个独立的工具通常意味着把lint程序束之高阁。
2.5 轻松一下---有些特性确实就是Bug
当然,任何人都知道从相对论的角度讲,信息也是有质量的。