《C陷阱与缺陷》摘要

来源:互联网 发布:unity3d 离散事件仿真 编辑:程序博客网 时间:2024/05/22 01:26

本博客属作者原创,转载需声明出处;

一、词法“陷阱”

1.1 =不同于==

注意:if(x = y) 这样写可能会使得编译器出现一些警告
而if((x = y) != 0) 这样写使得代码的意图一目了然,也显示的进行比较。

1.2 & 和 | 不同于 && 和 ||

同样的,将按位运算符 & 与逻辑运算符 &&,或者将按位运算符 | 和 逻辑预算符 || 调换,也是很容易犯的错误。

1.3 词法分析中的 “ 贪心法 ”

编译器将成语分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断两个字符是否可以组成一个字符。如果可以则继续读入下一个,往复直到组成的字符串不能组成一个有意义的字符。这种处理策略被称为“贪心法”。

注意:字符中间不能嵌有空白(空格符、指标符和换行符)
例:a—b 与表达式 a– -b的含义相同而与 a- –b的含义不同
同时页要特别小心×/ 或者 /×的使用 不经意间就有可能同注释关联导致程序错误。

1.4 整型常量

一个整型常量的第一个字符是数字0,那么该常量将被视为八进制数。
例:010与10的含义截然不同

1.5 字符与字符串

单引号和双引号含义迥异,在某些情况下弄混,编译器是不会检测报错的。
用单引号引起的一个字符实际上代表一个整数,用双引号引起的字符串代表的是一个指向无名数组起始字符的指针。

二、语法“陷阱”

2.·1 理解函数声明

例1:float(*h)(); 表示h是一个指向返回值为浮点类型的函数的指针。
例2:(*(void(*)())0)()
分析:第一步,调用函数指针所指向的函数,(*fp)(); //括号一定要加,加括号是函数指针,不加则变为了*((fp)()) 就是一个返回值是指针的函数。
第二步,指向返回值为void的类型的函数的指针,结果就成了对0的类型转换,(void (*) ()) 0,
然后用(void (*) ()) 0 来替换fp 从而得到(*(void(*)())0)()
不过使用typedef来解决这个问题无疑是一个更好的方式:
typedef void (*funcptr)();
(* (funcptr)0) ();
例3:void (* signal (int ,void(*)(int)))(int) ;(Linux中很经典的信号处理的函数)
typedef void (* HANDLER) (int);
HANDLER signal(int HANDLER);

2.2 运算符的优先级问题

C语言当中涉及到运算符优先级的问题太多了,记住优先级表是很重要的。这里写图片描述

我们需要记住两点是:
1.任何一个逻辑运算符的优先级低于任何一个关系运算符;
2.移位运算符的优先级比算术运算符要低,但是比关系运算符要高。

此外,所有的赋值运算符的优先级是一样的,他们的结合方式是从右到左。

例:if((t = BTRPE (pt -> aty) == STRTY) || t == UNIONTY){
该行代码的本意是t是否等于STRTY或者UNIONTY。但结果却是大相径庭:根据BTRPE (pt -> aty) 的值是否等于STRTY,t取0或1,如果为0则进一步与UNIONTY比较。

2.3 注意作为语句结束符标志的分号

经常有人在if或者for等条件语句之后加上一个 ; 号,导致程序大不相同;
同样少写一个 ; 号也会招致麻烦;在结构体定义的结束的}后面也是要加一个;号的哟。

2.4 switch 语句

用好switch语句的break;可以避免麻烦,同时也可以产生相较其他语言的优势。

2.5 函数调用

如果f是一个函数。
f();
就是一个函数调用。

f;
则是一个什么也不是的语句,更确切的说该语句是计算函数f的地址;

2.6 “悬挂”else引发的问题

else是与和它最近的一个if相匹配,千万不要忘记;
例: if (x == 0)
if (y == 0)
error();
else{
z = x + y;
f(&z);
}
可以用一个大括号封装来解决这个问题带来的不必要的麻烦。

三、语义“陷阱”

3.1 指针与数组

数组需要注意的两点:1.C语言只有一维数组,数组元素是可以多种多样,当然要仿真出一个多维数组就不是什么难事。
2.对于数组我们只能确定该数组大小,以及获得指向该数组下标为0的元素的指针。其他操作其实都是基于指针进行的。

例:*(a+i) = a[i]; //是正确的;

例:int calendar[12][13];
int *p;
p = canlendar ; //这句话是非法的;
因为calender是一个二维数组,即“数组的数组”,在此使用calender时会将其转化为一个指向数组的指针,而P是一个指向整型变量的指针。两者的类型不对。
很显然 int calendar[12][31]
int (*ap)[13];
ap = calendar;就是正确的了;

3.2 非数组的指针

在 char *r; 的时候,不仅要确定指针指向何处,我们应该还要让R有足够的空间。
malloc();//实现了这样的功能;
free();//再给r分配地址后,使用完毕需要释放内存;

3.3 作为参数的数组声明

多数情况下数组和指针作为参数声明会自动的转换
但是,在某些情况下并不是如此,比如:extern char *hello;与extern char hello[];却有着天壤之别;数组分配了空间,而指针只有一个指针的大小空间;

3.4 避免“举隅法”

char *p, *q;
p = “xyz”;
q = p;
在这个情况下只是把q的指针也指向了P也指向的地址;
若 p[1] = ‘Y’;
则 q所指向的内容就是 xYz;

3.5 空指针并非空指针字符串

if(p == (char *) 0)\是合法的
if(strcmp (p, (char *) 0) == 0)\是非法的
因为将0赋值给一个指针变量时,绝不能企图使用该指针所指向的内存中存储的内容。

3.6 边界计算与不对称边界

例:int a[10]; 他的下标是0~9;
在程序设计过程中会经常出现“栏杆错误”
解决这种边界的问题有两种方法:1.首先考虑最简单情况下的特例,然后将得到的结果外推,这是原则之一;
2.仔细计算边界,绝不掉以轻心,这是原则二;
在C语言中,不对称边界给程序设计带来便利:把上界视作某序列中第一个被占用的元素,而把下界视作序列中第一个被释放的元素。

3.7 求值顺序

if(count != 0 && sum/count < smallaverage)
即使当变量count为0时,也不会产生一个“用0作为除数”的错误

3.8 运算符&&、||和!

在某些情况下&、|和~与&&、||和!互换,程序还能正常工作;
while(i < tabsiize && tab[i] != x)
i++;
用&替换&&,循环语句仍能正常工作。
这都是存在侥幸的原因1、表达式两边都是比较运算。
2、对于数组结尾的下一个元素,程序只用其值而不改变他,一般不会有什么危害。

3.9 整数溢出

无符号中没有所谓的“溢出”一说;
在发生溢出的时候,作出任何假设都是不安全的,正确的方式是都强制转化为无符号数;

3.10 为函数main提供返回值

严格来说return 0 是最稳妥的或者最后写成exit(0);

四、连接

4.1 声明与定义

int a =7;
在定义a的同时也为a明确指定了初始值。这个语句不仅为a分配内存,而且也说明了该内存中应该存储的值。
extern int a;
extern关键字,说明了a的存储空间是在程序的其他地方分配的。从连接器的角度来看,上述声明是一个对外部变量a的引用,而不是定义。

每个外部对象都必须在程序的某个地方进行定义。可以在同一源文件中,也可以位于程序的不同源文件中。

如果一个程序对一个外部变量定义不止一次,大多数系统都会拒绝接受。

4.2 命名冲突与static修饰符

static int a ;
int a;
两者的含义是相同的,只不过用static修饰的a的作用域限制在一个源文件内,对其他源文件是不可见的。
static不仅适用于变量,也适用于函数。
用static就可以避免命名冲突;

4.4 形参,实参与返回值

任何一个函数在调用它之前进行声明或定义,那么就不会有任何与返回类型相关的麻烦。如果一个函数被调用而没有在之前进行声明或者定义,那么它的返回类型就默认是整型

4.5 检查外部类型

一个外部变量名在两个不同的文件中应被声明为相同的类型

4.6 头文件

有一个好的方法可以避免许多问题。每个外部变量只声明在头文件中,特别指出,定义该外部对象的模块也应该包括这个头文件。

五、库函数

5.1 返回整数的getchar函数

#include <stdio.h>main(){    char c;    while((c = getchar()) != EOF)        putchar(c);}

getchar在一般情况下返回的是标准输入的下一个字符,当没有输入时返回EOF。
乍一看程序是把标准输入复制到标准输出,其实不然。原因在于变量c被声明为char类型,而不是int类型。意味着可能无法容下所有可能字符.

5.2 更新顺序文件

在许多系统中的标准输入/输出库都允许程序打开一个文件,同时进行读写操作:
FILE *fp;
fp = fopen(file,”r+”);
但并不是执行上述操作就可以自由的交错进行读出和写入操作,为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入后不能紧跟着一个输出操作,反之亦然。如果要同时进行输入和输出操作,必须在其中插入sfeek函数的调用。

while (fread( (char *)%rec, sizeof(rec), 1, fp) == 1 ){    /* 对rec执行某些操作 */    if( /* rec必须被重新写入 */){        fseek(fp, -(long)sizeof(rec), 1);        fwrite( (char *)&rec, sizeof(rec), 1, fp);        fseek(fp, 0l, 1);    }}

第二个fseek函数虽然什么也没做,但是它改变了文件的状态,使得文件可以正常的读取。

5.3 缓冲输出与内存分配

程序输出有两种方式:一种是即时处理方式,另一种的先暂存起来,然后在大块写入的方式,前者往往造成较高的系统负担。
因此程序员要在进行实际的写操作之前控制产生的输出数据量
setbuf(stdout, buf);

#include <stdio.h>main(){    int c;    char buf[BUFSIZE];    setbuf(stdout, buf);    while((c = getchar()) != EOF)        putchar(c);}

遗憾的是,这个程序是错的。因为buf缓冲区最后一次清空是在main 函数结束之后,作为程序交回控制给操作系统之前c运行时库必须进行的清理工作的一部分。但是在此之前buf字符数组已经被释放!.避免这类型错误有两种方法:一、让缓冲数组称为静态数组,二、动态分配缓冲区。

5.4 使用errno检测错误

很多库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为errno的外部变量,通知程序该函数调用失败。

/* 调用库函数 */if(返回的错误值)    检查 errno

5,5 库函数signal

#include < signal.h >
要处理一个特定的singal(信号),可以调用signal函数:signal(signal type, handler function);
对于处理函数的唯一安全、可移植的操作就是打印一条出错消息,然后推出程序;

六、预处理器

6.1 不能无视宏定义中的空格

6.2 宏并不是函数

#define max(a,b) ((a) > (b) ? (a) : (b))
请注意宏定义中出现的所有这些括号,它们的作用是预防引起与优先级有关的问题。

6.3 宏并不是语句

6.4 宏并不是类型定义

对于类型的定义,我们最好还是用typedef

七、可移植性缺陷

7.1 应对C语言标准变更

7.2 标识符名称的限制

注意函数的大小写,有的编译器是不区分的.

7.3 整数大小

1.对于 short, int, long 三种类型的整数其长度是非递减的
2.一个普通(int)整数足够下可以容纳下任何数组下标
3.字符长度由硬件特性决定

7.4 字符是有符号整数还是无符号整数

视编译器而定。
一般转换,应使用(unsigned char)c,因为 unsigned char 类型的字符在转换为无符号整数时无需首先转换为int型整数,而是直接转换。

7.5 移位运算符

有两个问题:
1.在向右移位时,空出的位是由0填充,还是由符号位的副本填充?
2.移位计数允许的取值范围是什么?
第一个问题:被移对象时无符号数,那么空出的位将被0填充。如果是他有符号位,那么c语言既可以用0填充也可以用符号位副本填充。
第二个问题:如果被移位的对象长度是n,那么移位计数必须大于或等于0,严格小于n。
n>>1

n/2
完全等效,而且前者的执行速度也要快得多。

7.6 内存位置0

在所有的c程序中,无用NULL指针的效果都是未定义的。然而,这样的程序有可能在某个c语言实现上“似乎”能够工作,只有当改程序转移到另一台机器上运行时才会暴露出来。

7.7 除法运算时发生的截断

假定 a除以b ,商为q, 余数为r
c语言的定义值保证:q*b+r == a 以及 a>= 0 且 b>0 时,保证 |r|<|b|以及r>=0.

7.8 随机数的大小

ANSI C 标准中定义了一个常数RAND_MAX,它的值等于随机数的最大取值

7.9 大小写转换

库函数 toupper 和 tolower

7.10 首先释放,然后重新分配

malloc realloc free

完。

0 0
原创粉丝点击