编程语言应用

首页 » 常识 » 常识 » C陷阱与缺陷要点总结
TUhjnbcbe - 2025/7/16 11:56:00
陷阱(网络)词法词法分析中的贪心法:每一个符号应该包含尽可能多的字符,也就是说编译器把程序分解为符号的方法是,从左到右一个字符一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分,如果可能重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号,这个策略被称为“贪心法”y/*x实际想表达的是y/(*x)但是/*会被理解为一段注释的开始,这样的准二义性问题会招致麻烦。习题:a+++++b整形常量如果一个整形常量的第一个字符是0,那么该常量将被视为8进制。字符与字符串单引号和双引号有时候会被弄混,编译器有时候不会报错,在运行时产生难以预料的错误。单引号引起的字符实际上代表一个整数。错误:printf(\n)会产生错误语法变量声明由两部分组成:类型和一组类似表达式的声明符。模拟开机时启动程序,(*(void(*)())0)()floatff()表达的含义是ff()求值的结果是一个float,也就是说ff表示一个返回结果为float的函数float*ptr表达的含义是*ptr的结果是一个float,也就是说ptr是一个float的指针float*g(),表达的含义是*g()求值的结果是float,但是g和右边()结合的优先级高于左边的*,因此g表示一个返回float指针的函数float(*h)(),表达的含义是(*h)()求值的结果是float,h表示一个返回float的函数的指针类型的转换符:把一个变量声明中的变量名和结尾的分号去掉,再将剩余部分用一个括号封装起来。比如前面的(float(*)())函数指针的调用,将设fp表示一个函数指针,那么(*fp)()表示调用该函数指针指向的函数,可以简写为fp(),但是*两边的()不能去掉,因为()运算符的优先级高于*,如果省掉括号表示对函数执行结果解引用。void(*sfp)(int)sfp表示一个函数指针void(*signal(something))(int)表达的意思是(*singnal(something))的结果是一个传入参数为int,返回结果为void的函数,那么signal的表达的意思就是传入somenthing返回一个函数指针。正常人的写法应该是typedefvoid(*HANDLER)(int);HANDLERsignal(int,HANDLER);关于运算符的优先级,靠谱的做法还是增加括号,明确的表达。表达式中的分号,if、while等语句如果后面加了分号则表示独立的语句。如果struct定义后面少加了分号,则可能出现下面类似的错误:struct{intxxx;intccc}main(){}表达式的意思可能就变成返回一个结构体。奇葩的联想。switch中的break也是一个比较容易出错的地方,如果真的需要省略break,可以在对应的位置增加注释case1:/*此处没有break*/case2:break;default:break;ifelse对括号的使用要一致,避免出现else悬挂的问题,看下面的错误例子if(x==0)if(y==0)error();else{z=x+y;f(z);}c语言允许初始化列表出现多余的逗号,这样在初始化列表很长的时候,需要分成多行,每行都是以逗号结尾,这种语法上的相似性可以方便代码编辑工具进行处理。语义数组c语言的数组需要注意两点,第一点,c语言只有一维数组,但是数组的成员可以是任何类型,包括数组类型,所以可以模拟出多维数组。第二点,c语言中对数组只能有两种操作,第一种指定数组大小,第二种获取下标为0的元素的地址。其他的元算,即使是下标的运算也是通过指针元算进行的。对于一个数组变量a,sizeof(a)表示这个数组的大小,而其他时候a都表示下标为0元素的地址。*(a+i)简记为a非数组的指针常见的:第一,不检查malloc的返回值第二,分配的内存没有及时释放第三,申请的内存长度不足。作为参数传递的数组名会被转换成指向数组第一个元素的地址。虽然指针和数组可以转换,但是如果一个指针变量不代表一个数组,那么不要把他声明为一个数组类型,避免出现误导。混淆指针和指针所指向的数据:复制指针并不同时复制指针指向的数据。空指针不是空字符串,可以把0赋值给一个指针变量,但是不能尝试访问该指针变量指向内存存储的内容,空指针不能解引用。边界计算与不对称边界栏杆错误或者说差一错误,英尺围栏,每10英尺需要一个栏杆,总共需要多少栏杆边界的计算两个通则:通则一,计算最简单的特例,在此结果上外推通则二,仔细计算边界,绝不掉以轻心避免出现栏杆错误的编程技巧,即不对称边界:采用第一个入界点和第一个出界点表示一个范围,这里的下界是入界点表示包含在取值范围之内,上界为出界点,不包含在取值范围之内。另一种考虑不对称边界的方式是,把上界作为某序列中第一个被占用的元素,把下界作为第一个被释放的元素。对于指针bufptr,是让它始终指向最后一个已占用的字符,还是让它指向第一个未被占用字符。根据不对称边界的原则,选择后一种更为合适。依据不对称边界的原则,判断指针到达缓冲区尾部的方法是if(bufpter==buf[N])而不是if(bufptrbuf[N-1])。尽管buf[N]的原色是不存在的,他的地址仍然可以用来比较,但是不能访问不存在的元素。求值顺序c语言中只有四个运算符(‘’,‘

’,‘?’,‘,’)存在规定的求值顺序,运算符和

都是先求左值,如果有需要再求右值,表达式a?b:c也是先求a的值如果有需要再计算b或c,而逗号运算符,先对左侧操作求值,然后该值被丢弃,再对右侧操作求值。f(x,y)中x和y的求值顺序时未定义的。g((x,y))中x和y的求值顺序是先计算x,丢弃之后再计算y,而函数g传入的参数也是只有一个。赋值操作符并不保证任何求值顺序,比如下面的例子:x=y[i++],并不能保证左侧先执行。不要使用位运算符(,

,~)来替代逻辑运算符(,

,!),有时候运行结果正常只是巧合,意义不同,并且逻辑运算符有求值顺序,能够避免错误访问。整数溢出,这里不是类型长度导致的溢出,而是指有符号整型的溢出,两个有符号整型计算,结果可能会溢出。而溢出的结果是未定义的,各编译器实现不同,因此不能使用类似下面的方式来检测溢出if(a+b0),而应该把他们都转换成无符号整型进行比较if((unsigned)a+(unsigned)bINT_MAX)main函数应该有返回值。连接4.1什么是连接器连接器不理解C语言,编译器的责任是把C语言翻译成连接器能够理解的形式。连接器的作用是把编译器或者汇编器生成的若干目标模块,整合成一个载入模块或者一个可执行文件的实体。程序中每个函数或者外部变量没有声明为static,就都是一个外部对象。连接器通常把一个目标模块看成是一组外部对象组成。工作过程:连接器的输入是目标模块,输出是载入模块,连接器读入目标模块和库文件,同时生成载入模块,对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象,如果没有就写入载入模块,如果有就进行同名冲突处理。一个目标模块中引用了其他目标模块的外部对象时,生成载入模块时需要记录这些引用,直到读入定义该外部对象的模块时,修改载入模块中的标记,标记该外部对象不再是未定义的。4.2声明与定义变量的定义如果出现在所有函数体之外,称为外部对象的定义。外部变量的定义如果没有指定初始值,那么多次定义可能能够编译通过,但是不应该这样做,每个外部变量只应该定义一次。4.3命名冲突与static修饰符为了避免可能出现的命名冲突,如果一个函数仅被同一文件中的其他函数调用,我们应该把它声明为static。4.4形参实参返回值任何一个函数,在被调用的每个文件中,都在第一次被调用之前进行了声明或者定义就不会出现参数或者返回值的错误。也就是说在某些时候,调用函数之前可以不声明或者定义该函数,此时返回值和参数有有一些默认的规则。这不是常规做法?4.5检查外部类型保证一个特定的名称的所有外部定义在每个目标模块中具有相同的类型,是程序员的责任,编译器可能检测不到这种错误。如:externintn;longn;charfilename[]=/ect/passwd;externchar*filename;4.6头文件避免上述问题的方法是,外部对象在一个地方声明,这个地方应该是一个头文件中。定义外部变量的地方也应高包含该头文件。库函数5.1getchar返回整形的getchar,看一个例子c=getchar,如果c被定义为char类型,结果是c无法容纳下所有字符,包括EOF,有时候编译器在这类情况下会把返回结果截断。5.2文件操作,为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作,不能随后紧跟着一个输出操作,如果需要同时进行输入和输出操作,需要在其中插入fseek函数5.3缓冲输出与内存分配程序输出的两种方式:第一种叫做及时处理,另外一种方式是先暂存起来,然后大块写入。前者旺旺造成较高的系统负担。通过调用库函数setbuf(stdout,buf)来实现。在使用setbuf时,需要注意buf变量的生命周期,在指针执行打印之前buf不能被释放。5.4使用errno检测错误在调用库函数时,应该先检查返回值,在确认函数执行失败的情况下,再查看errno查看错误原因。这么做的原因是在执行成功情况下errno也可能被上次库函数设置,而没有清零。5.5signal信号处理函数非常复杂棘手,而且具有一些从本质上而言的不可移植性,signal处理函数应该尽可能的简单,并把他们组织在一起。事件处理函数必须是不对全局变量产生影响的可重入的函数。常用的做法是打印错误信息,然后调用longjmp或者exit退出程序。预处理器宏只对程序文本起作用。宏提供了一种对组成程序字符进行变换的方式,而并不是作用于程序中的对象。6.1注意宏定义中的空格6.2宏不是函数,所以我们需要:宏定义中的每个参数都用括号括起来宏定义整个表达式也应该用括号括起来宏中的参数可能被求值多次,所以要确保宏的参数没有副作用,不如不能传递类似(a++)这类的参数宏可能产生非常庞大的表达式6.3宏不是语句,看下面的例子#defineassert(e)if(!e)assert_error(__FILE__,__LINE__)if(x0y0)assert(xy)elseassert(yx)最终展开之后else错误的宏定义中的if结合了。6.4宏不是类型定义#defineT1structfoo*T1a,b;展开表达式,b并不是我们想要的类型。因此我们应该使用typedef来定义类型。可移植性缺陷7.1c标准的变更7.2标示符名称的限制7.3整数的大小7.4字符是有符号整数还是无符号整数字符转换为较大的数时,如果字符的最高位是1,那么转换成无符号数还是有符号数?可以声明为unsignedchar类型,这样编译器在转换时,只是把多余的位补0注意,不能使用(unsigned)c的方式获得无符号整数,因为这个操作会先把c转换为int,而这个结果可能是非预期的。7.5移位运算向右移位时多出来的空位是由0来填充,还是由符号位副本来填充?无符号数进行移位,空位用0来填充,有符号数移位,可以用0也可以用符号位来填充,所以,如果

1
查看完整版本: C陷阱与缺陷要点总结