C陷阱与C缺陷笔记

词法陷阱

程序是由符号token序列所组成的。将程序分解成符号的过程称为“词法分析”。

C语言对符号的判断规则是:每一个符号应该包含尽可能多的字符。也就是,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。

这个处理策略有时被称为“贪心法”或者“大嘴法”。

注意:除了字符串与字符常量,符号中间不能嵌有空白(空格符、制表符和换行符)。

语法陷阱

(*(void(*)())0)()

构造这类表达式有一条简单的规则:按照使用的方式来声明。

任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。

float *g()   g是一个函数,返回值类型为指向浮点数的指针。

对一个常数进行类型转换,将其转换为该变量的类型:只需在变量声明中将变量名去掉即可。

float (*h)();

(float (*)())表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。

将常数0转型为“指向返回值为void的函数的指针”类型  (void (*)())0,替换h,从而得到float (*(void (*)())0)();

void (*fp)();

(*fp)();

typedef kkk xxx这里意思就是xxx代表kkk

typedef void(*int_handler) () 这里void (*) ()等同于kkk,int_handler等同于xxx

void (*signal(int,void(*)(int)))(int);

typedef void (*HANDLER)(int);

HANDLER signal(int,HANDLER);

!=运算符的优先级高于&运算符

加法运算符优先级高于移位运算符<<


任何一个逻辑运算符的优先级低于任何一个关系运算符;

移位运算符的优先级比算术运算符要低,但是比关系运算符要高。

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

struct a

{

int b;

...

};

C语言规则:else始终与同一对括号内最近的未匹配的if结合。

语义陷阱

指针与数组

C99标准允许变长数组VLA。GCC编译器中实现了变长数组,但细节与C99标准不完全一致。

对于一个数组:确定该数组大小,以及获得指向该数组下标为0的元素的指针。任何一个数组下标运算都等同于一个对应的指针运算。

calendar[month][day]

*(*(calendar+month)+day)

calendar具有12个数组类型元素的数组,每个数组类型元素又是一个有着31个整型元素的数组。

ANSI C   C89标准

strlen(字符串)返回字符串所包括的字符数目n,而作为结束标志的空字符并未计算在内。字符串实际需要n+1个字符空间。

malloc()返回空指针来作为内存分配失败事件的信号。

return是函数的退出(返回);exit是进程的退出。

return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。exit是一个库函数,exit(1)表示发生错误后退出程序,exit(0)表示正常退出。

除了a被用作运算符sizeof的参数这一情形,在其他所有的情形中,数组名a都代表指向数组a中下标为0的元素的指针。sizeof(a)是整个数组a的大小。

*a即数组a中下标为0的元素的引用。简记为a[0]。

避免举隅法:以含义更宽泛的词语来代替含义相对较窄的词语,或者相反。

char *p ="xyz";

char *q;

q=p;

p和q是两个指向内存中同一地址的指针,这个赋值语句并没有同时复制内存中的字符。

复制指针并不同时复制指针所指向的数据。

P的值是一个指向‘x' 'y' 'z'和'\0'  4个字符组成的数组的起始元素的指针。

p[1]等于'y';


编译器保证由0转换而来的指针并不等于任何有效的指针。处于代码文档化的考虑,常数0这个值经常用NULL来替代。

#define NULL 0

空指针并非空字符串

当常数转换为指针使用时,这个指针决不能被解除引用。即绝对不能企图使用该指针所指向的内存中的存储内容


差一错误:

整数x满足边界条件x>=16且x<38.

下界为“入界点”,即包括在取值范围之中;上界是“出界点”,即不包括在取值范围之中。

1、取值范围的大小就是上界与下界之差。38-16=22,恰恰是不对称边界16和38之间所包括的元素数目。

2、如果取值范围为空,那么上界等于下界。

3、即使取值范围为空,上界页永远不可能小于下界。

含有10个元素数组,下标范围是0<=i; i<10;

#define N 1024

static char buffer[N];

static char *bufptr;

*bufptr++=c;

bufptr = buffer;或者bufptr = &buffer[0];

任何时候缓冲区中已存放的字符数都是bufptr-buffer;

缓冲区中未占用的字符数是N-(bufptr-buffer);  等价于(buffer+N)-bufptr  出界点减去入界点;

if(bufptr == &buffer[N])等价于if(bufptr>&buffer[N-1])

buffer[N]并不存在,但是其地址&buffer[N]存在,位于数组所占内存之后。该地址可以进行赋值和比较。但是,引用该元素,是非法的。

缓冲区指针始终指向缓冲区中第一个未占用的字符。


z=x++,y++,++y;等价于这样的结合:(z=x++),y++,++y;赋值运算符的优先级是14,而逗号表达式的优先级是15。

Z=X,x=x+1;

C语言中只有四个运算符&&、||、?:和,存在规定的求值顺序。前两个运算符先对左侧操作数求值,只在需要时才对右侧操作数求值,例如a||f(),f()不会被求值。?:有三个操作数,a?b:c,操作数a首先被求值,根据a的值再求操作数b或c的值。逗号运算符,先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值。

C语言对其他所有运算符对其操作数求值顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序。

以下复制做法是不正确的,因为它对求值顺序做了太多假设:

i=0;

while(i<n)

y[i]=x[i++];//在C语言的某些实现上,不能保证顺序

改写为

for(i=0;i<n;i++)

y[i]=x[i];


整数溢出

在无符号运算中,没有溢出的说法。算术运算符的一个操作数是有符号数,另一个是无符号数,那么有符号数会被转换为无符号数,也不会发生溢出。

当两个操作数都是有符号数,就可能发生溢出。强制转换为无符号数来判断。

(unsigned) a +(unsigned)b>INT_MAX

INT_MAX代表可能的最大整数值。

或者a>INT_MAX-b

连接

PC-Lint是一种静态代码检测工具,是一种更加严格的编译器,不仅可以检查出一般的语法错误,还可以检查出虽然完全符合要求,但是却可能存在潜在的,不易发现的错误。

连接器对C语言知之甚少,所以有很多错误不能被检查出来。

static修饰能减少命名冲突。限制在一个源文件中,对于其他源文件不可见。

如果一个函数在被定义或声明前被调用,那么它的返回类型就是默认为整型。


库函数

有关库函数的使用,最好建议是尽量使用系统头文件。

头文件中包括了库函数的参数类型以及返回类型的声明。

假设malloc函数的执行过程被一个信号中断,此时malloc函数用来跟踪可用内存的数据结构可能只有部分被更新。如果signal处理函数再调用Malloc函数,结果可能是malloc函数用到的数据结构完全崩溃,后果不堪设想。


预处理器

在严格意义上的编译开始前,C语言预处理器首先对程序代码坐了必要的转换处理。

宏提供了一种对组成C程序的字符进行变换的方式,而并不是作用于程序中的对象。因而,宏可以使一段看上去完全不符合语法的代码称为一个有效的C程序,也能使一段看上去无害的代码成为可怕的怪物。

#define T1 struct foo *

typedef struct foo  *T2

T1 A,B;被扩展为struct foo *A,struct foo B;


T2 A,B;被扩展为struct foo *A,struct foo* B;

可移植性缺陷

ANSI C标准所能保证的是,C实现必须能够区别出前6个字符不同的外部名称,而且这个定义中并没有区分大写字母与其对应的小写字母。

可移植性最好的办法就是声明该变量为Long型,可以定义新的,typedef long tenmil,只需要改动类型定义,所有这些变量的类型就自动更改。

如果c是一个字符变量,使用(unsigned) c会首先被转换为int型整数,可能得到非预期的结果;

正确方式是使用(unsigned char)c,因为unsigned char类型的字符在转换为无符号整数时无需首先转换为int型整数,而是直接进行转换。

移位运算符

1、向右移位时,无符号数,空出的位将被0填充;如果是有符号数,既可以用0填充,也可以用符号位的副本填充空出的位。可以先将变量声明为无符号类型。

 2、移位操作的位数限制

被移位的对象长度是n位,那么移位计数必须大于或等于0,而严格的小于n.

即使 C实现将符号位复制到空出的位中,有符号整数的向右移也不等同于除以2的某次幂。

null指针并不指向任何对象。因此,除非是用于赋值或比较运算,出于其他任何目的的使用Null指针都是非法的。

除法运算时发生的截断:

假定a除以b,商为q,余数为r:
        q = a / b ;
        r = a % b ;
       假定b大于0。
希望a、b、q、r之间维持怎么样的关系呢?
        1. 最重要的一点,希望q*b+r = a要成立,这是定义余数的关系。
        2. 如果我们改变a的正负号,我们希望这会改变q的符号,但这不会改变q的绝对值。

        3. 当b>0时,我们希望保证r>=0且r<b。例如,如果余数用于哈希表的索引,确保它是一个有效地索引值很重要。
很不幸,它们不可能同时成立。简单例子:

3/2,商为1,余数为1。此时,第1条性质得到满足。(-3)/2如果满足第2条性质,答案应该是商为-1,但是这样余数也为-1,这样性质3就不成立了。如果优先满足性质3,即余数为1,这种情况下商为-2,那么性质2不成立。
        因此:C语言在实现整数除法截断运算时,必须放弃上述三条原则中的一条。大多数程序设计语言选择放弃第3条,而改为要求余数与被除数的正负号相同。这样,性质1和2就可以得到满足。大多数C编译器在实践中也是这样做的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值