《C陷阱和缺陷》总结

《C陷阱和缺陷》

第一章

1.1 =不同于==

在C语言中,=作为赋值运算,符号==作为比较,一般而言,赋值运算相对于比较运算出现的更加频繁。

同时,在C语言中赋值运算符号被作为一种操作符对待,因而重复进行赋值操作(如a=b=c)可以很容易的写出来。

这种使用上的便利性很可能导致一个潜在的问题,当程序员的本意是要做比较运算时,却可能无意中写成了赋值运算符,如:

例(1)

if(x=y)
  break;

上述的语句本意是要检查x是否等于y,而实际上是将y的值赋值给了x,然后检查该值是否为0.

例(2)
本里中循环语句的本意是跳过文件中的空格符,制表符和换行号

while(c=' '||c=='\t'||c=='\n')
c=getc(f);

但是由于程序员在比较字符’'和变量C时,错误的将==写成了赋值运算符=,由于赋值运算符的优先级低于逻辑运算符的优先级||因此实际上是将以下的表达式赋值给了c

' '||c=='\t'||c=='\n'

因为’ '值不等于0,那么无论变量c此前为何值,上述表达式的结果都为1,因此循环将一直进行下去直到整个文件结束。

当然了,有时候也可能把赋值运算符错误的写成比较运算符,这些都是可能的,所以要在使用的时候谨慎一些。

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

按位运算符和逻辑运算符
按位操作符:
是按照所输入数字的二进制位来进行逻辑与,逻辑或和逻辑异或运算的。
通过二进制来判断最终的结果到底是什么的,所以说按位操作符主要关注的是数字的二进制位,与数字表面的值没有什么太大的关系。
逻辑操作符,逻辑与和逻辑或运算符关心的是数字表面的值到底是不是0
若为0则为假值,若为非0,则为真值,仅此而已
pascal语言中按位与和按位或运算符分别为and和or

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

C语言中有多字符符号和的单字符符号

只有一个字符长的称为单字符符号,例如/ * +

包括多个字符的符号称为多字符符号 如/* 和== 以及标识符

C语言对于判断一个符号为单字符符号还是多字符符号归纳成了一个简单的规则:每一个符号应该包含尽可能多的字符,如果一直能组成在一起,就把他们看成是一起的。这个处理策略有时候称为“贪心法”或者"大嘴法"

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

总的来说,就是要注意空格的使用,不然的话,有可能造成歧义和编译器的混乱

1.4整形常量

如果一个整形常量的第一个字符是数字0,那么该常量将被视为八进制数,因此,10与010含义截然不同

此外,许多C编译器会把8和9也作为8进制数字处理,,这种多少有点奇怪的处理方式来自八进制数的定义。例如:0195的含义是181+9*82+58^0,结果是141(十进制)。但是大多数情况下,不建议使用这种用法,ANSIC也禁止这种算法。

八进制,Octal,缩写OCT或O,一种以8为基数的计数法,采用0,1,2,3,4,5,6,7八个数字,逢八进1。一些编程语言中常常以数字0开始表明该数字是八进制。八进制的数和二进制数可以按位对应(八进制一位对应二进制三位),因此常应用在计算机语言中。

1.5 字符与字符串

用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。所以说a的含义与’97’严格一致。

用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制值为0的字符’\0’初始化。对于字符串来说,’\0’对于字符串是非常重要的,他是字符串的结束标识,如果一个字符串没有’\0’,那么他就不算结束,直到在内存中碰到\0的存储位置为止结束。

用单引号括起的一个字符代表一个整数,而用双引号括起的一个字符代表一个指针,如果两者混用,那么编译器的类型检查功能将会检查到错误。(现在的编译器一般能够检测到在函数调用时混用单引号和双引号的情形。)

整形数(一般为16位或者32位)的存储空间可以容纳多个字符(一般为8位),因此有的C编译器允许在一个字符常量(以及字符串常量)中包含多个字符。也就是说用’yes’代替"yes"不会被编译器检测到的。"yes"后面自动存放有一个\0的位置,而’yes’时没有\0的存储的,这两个之间还是有本质差别的,如果值有什么相似之处,那也完全都是巧合。

第二章

2.1 理解函数声明

“有一次一个程序员与我交谈一个问题,他当时正在编写一个独立运行与某种微处理器上的C程序。当计算机启动时,硬件调用首地址位0位置的子例程。为了模拟开机启动时的情形,我们必须涉及出来一个C语句,以显示调用孩子例程。最终得到的语句如下”:

(*(void(*)()0)();
//把0强制类型转换,转换成了函数指针类型

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

float f,g;   
float *pf;
//这个声明的含义时*pf时一个浮点数,也就是说pf是一个指向浮点数的指针。
//我们可以在声明符号中使用任意括号
folat ((f));
//这个声明的含义是:对其进行求值时,((f))的类型为浮点型类型,由此可以推知,f也是浮点型类型的。同样的逻辑也适用于函数和指针类型的声明。
float *g()//因为()的结合优先级高于*,*g()也就是*(g());,g为一个函数,该函数的返回值类型时指向浮点型的指针
2.2 运算符的优先级问题

假设存在一个一定义的常量FLAG,FLAG是一个整数,且该整数的二进制位中只有某一位是1,其余各位均为0,所以这个整数肯定是2的某次幂。如果对于整型变量flags,我们需要判断他在常量FLAG为1的那一位上是否也为1,通常可以这样来写

if(flags&FLAG);

上式的含义对于大多是C程序员来说是显而易见的,if语句判断括号内表达式的值是否为0,考虑到可读性,如果这么写的话:

if(flags&FLAG!=0);

那么这个语句就会更好懂一些,但是这个代码又出现了别的问题,运算符!=的优先级高于&的优先级,所以这个语句就出现一定的错误了。

所以这么看来的话,在C语言中涉及到运算符的问题买最好还是带上括号,不仅容易理解,反而不容易出现意想不到的错误。

运算符包括运算符的优先级和结合性

优先级最高的其实并不是真正意义上的运算符,包括:数组下标,函数调用操作符,各结构成员选择操作符,这些是优先级最高的.
他们的结合性是自左向右,因此a.b.c的含义是(a.b).c而不是a.(b.c)
单目运算符的优先级仅次于前述运算符,在所有真正意义上的运算符中,他们的优先级最高
因为函数调用的优先级高于单目运算符的优先级,所以,如果p是一个函数指针,要调用p所指向的函数,要写成 (*p) () 注意 () 和 * 的优先级问题。

单目运算符的结合性是自右向左结合,因此p++会被编译器解释称(p++),即取指针p所指向的对象,然后将p增加1.

优先级比单目运算符还要低的,接下来就是双目运算符了。在双目运算符中,算数运算符的优先级最高,移位运算符次之,关系运算符次之,接着是逻辑运算符,赋值操作符,条件操作符((三目运算符),也就是说,在双目运算符中:

算数运算符>移位运算符>关系运算符>逻辑运算符>赋值运算符>条件操作符
最重要的两点
  • 任何一个逻辑运算符的优先级低于任何一个关系运算符
  • 移位运算符的优先级比算数运算符的优先级要低,但是比关系运算符要高

在所有的运算符中,逗号运算符的优先级最低,这一点很容易记住,因为逗号运算符常用于在需要一个表达式而不是一条语句的情形下替换作为语句的结束标志的分号。

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

一般来说,在C程序中多写一个分号不会造成什么恶劣的影响,编译器也可能会查出来这个分号的错误之处。但是有一个重要的例外情形是在if或者while语句之后需要紧跟一条语句时,如果此时多了一个分号,那么原来紧跟在if或者while字句之后的语句就是一条单独的语句,与判断条件没有任何的关系,举个例子:

if(x[i]>big);
big=x[i];

此时,编译器会正常接收第一行代码中的分号,而且不会给提示信息,因为比那一起对上述代码的处理和下述代码的处理完全不相同

if(x[i]>big)
big=x[i];
if(x[i]>big);
big=x[i];

相当于

if(x[i]>big)
{
    
}
big=x[i];

所以说,多写一个分号或者少写一个分号都会引起不必要的麻烦。

2.4 switch语句

C语言的switch语句的控制流程能够依次通过并执行各个case部分,这一点是C语言的与众不同之处。C语言中switch语句的这种特性,既是他的优势所在,也是他的一大弱点。说是弱点,因为有的时候可能会遗忘case之后的break语句。

2.5 函数调用

与其他程序涉及语言不同,C语言要求,在函数调用时即使函数不带参数,也应该包括参数列表,因此,如果f是一个函数,f();是一个函数调用语句,而f是在计算函数的地址,却并没有调用这个函数,函数名单独存在,以及&函数名都表示函数的地址。

2.6 "悬挂"else引发的问题

其实就是if else相互对应的问题,C语言中有规则规定,else始终与最近的未匹配的if结合。找好对应关系,就可以很容易的解决悬挂的问题

第三章

一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写着希望表达的意思。程序也有可能表面看上去一个意思,而实际意思却相去甚远。本章考察了若干种可能引起上述歧义的程序书写方式。

3.1 指针与数组

C语言中数组值得注意的地方有以下两点:

  • C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而,C语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组,所以多维数组就出来了
  • 对于一个数组,我们能做的只有两件事情,确定该数组的大小,以及获得指向该数组下标为0元素的指针,其他有关数组的操作,实际上都是通过指针进行的。换句话说,任何一个数组的下标运算都等同于一个对应的指针运算。
struct
{
    int p[4];
    double x;
}b[17];
//声明了b是一个拥有了17个元素的数组,其中每个元素都是一个结构
//该结构中包括了一个拥有4个整形元素的数组和一个双精度的浮点类型的变量(命名为x)

如果一个指针指向的是数组中的一个元素,那么我们只要给这个指针+1,就能够得到指向该数组中下一个元素的指针。同样的,给这个指针-1,得到的就是指向该数组中前一个元素的指针。给一个指针加上一个整数,与该指针的二进制表示加上同样的整数,两者的含义截然不同。

现在p指向数组中下标为0的元素,p+1指向数组中下标为1的元素,p+2指向数组中下标为2的元素,以此类推。

3.2 非数组的指针

在C语言中,字符串常量代表了一块包含字符串中所有字符以及一个空字符\0的内存区域地址。因为C语言要求字符串常量以空字符作为结束的标志,对于其他字符串,C程序员也沿用了这一个惯例。

假设我们现在有两个字符串s和t,我们希望将这两个字符串连接成单个字符串r。要做到这一点,我们可以借助常用的库函数strcpy和strcat函数,下面的代码看似可以完成任务,但是实际上是不可以的:

char *r;
strcpy(r,s);
strcat(r,t);

之所以不能完成任务,是因为我们并不知道r指向何处,所以出现了野指针的问题,而且,我们也并没有确保r有足够的空间来容纳两个字符串,这个内存空间应当是以某种方式被分配了的。看下面这个代码:

char r[100];
strcpy(r,s);
strcat(r,t);

上述代码还是不能完成我们希望的功能,原因是C语言强制要求我们必须声明数组大小为一个常量,所以我们还是不能确保r是足够大的,那么再尝试用malloc和strlen函数

char *r,*malloc;
r=malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);

但是这个例子还是错的,malloc函数有可能申请内存失败,当malloc函数申请内存失败的话,它会返回一个空指针;还有就是给r分配的内存要在使用完成之后释放掉。还有就是再调用malloc函数的时候还是没有分配足够的内存,忘记了\0的存在,还是放不下的。

这些点都考虑到,才算是一个正确的代码。

3.3 作为参数的数组声明

在C语言中,我们没有办法可以将一个数组作为参数直接进行传递,如果我们使用数组名进行参数的传递,那么我们传递的也是数组首元素的地址,所以,在C语言中,会自动将作为参数的数组声明转换为相应的指针声明。

3.4 避免"举隅法"

举隅法在牛津词典中是这样解释的:以含义更宽泛的词语来代替含义相对较窄的词语,或者相反:例如:以整体代表部分,或者以部分代表整体等。

C语言中一个常见的陷阱:混淆指针与指针所指向的数据,比如如下的错误:

char *p,*q;
p="xyz";

实际上p的值是一个指向’x’,‘y’,‘z’和’\0’4个字符组成的数组的起始元素的指针,因此我们执行下面的语句

q=p;

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

3.5 空指针并非空字符串

除了一个重要的例外情况,在C语言将一个整数转换成指针,最后得到的结果取决于具体的C编译器实现。这个特殊情况就是常数0.编译器保证0转换来的指针不等于任何有效指针

#define NULL 0

当然,无论使用常数0还是用符号NULL,效果都是相同的,需要记住的重要的一点是:当常数0转换成指针使用时,这个指针绝对不能被解引用的,也不能使用该指针所指向内存中的存储内容。

3.6 边界计算与不对称边界

C语言中拥有10个元素的数组中,他的元素的下标为0-9,所以建议在for循环中,采用的范围最好是左闭右开区间,当然也不是死规定,最好是这样。C语言中,元素下标范围为0-n-1;

3.7 求值顺序

本书2.2节讨论了运算符优先级的问题,求值顺序则完全是另一码事情,运算符的优先级是:

a+b*c 应该被理解成a+(b*c),而不是(a+b)*c;

求值顺序是另一类规则,可以保证像下面的语句

if(count!=0&&sum/count<smallaverage)
  printf("average<%d\n",smallaverage);

即使count为0时,也不会产生一个"用0作除数"的错误。

C语言中某些运算符总是以一种已知的,规定的顺寻来对其操作数进行求解,而另外一些则不是这样的。如:

a<b&&c<d

如果a>=b,则无需对c< d 进行求解了,说明了&&运算符的短路特性.

C语言中只有四个运算符(&&,||,?:和,)存在规定的求值顺序,运算符&&和||首先对左侧的操作数求值,只在需要时才对右侧操作数求值,三目运算符也是要分情况进行运算的操作。C语言中其他的运算符对其操作数的求值顺序是未定义的。特别的,赋值运算符并不保证任何求值顺序

3.8 运算符&&,||和!

按位运算符&,|和~对操作数的处理方式是将其视为一个二进制的的序列,然后进行相应的运算,另一方面,逻辑运算符&&和||和!对操作数的处理方式是将其视作要么是真,要么是假的数字,通常规定0为假,非0为真,而且运算符&&和||还存在有短路特性。

3.9 整数溢出

C语言中存在两类整数算数运算,有符号运算和无符号运算。在无符号算数运算中,没有所谓的溢出一说,所有的无符号运算都是以2的n次方为模,这里n是结果中的位数。如果算数运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换成无符号整数,溢出也不可能发生,但是当两个操作数都是有符号整数的时候,那么溢出就可能会发生了。而且溢出的结果是未被定义的。那么如何来解决呢?一种正确的方式是将a和b都强制类型转化位无符号的整数

3.10 为函数main提供返回值

最简单的C程序也许是这样

main()
{
    
}

但是这个main函数并未显式声明返回值类型,那么函数的返回值类型就默认为是整形。通常来说,这不会造成什么危害,一个返回值为整形的函数如果返回失败,实际上是隐含的返回了某个垃圾整数,只要这个整数没有被用到,就无关紧要。但是,在某些情况下,main函数的返回值并不是无关紧要的,大多数C语言程序都通过main函数的返回值来告知操作系统该程序执行成功还是失败,典型的是,返回0代表程序执行成功,返回非0代表程序执行失败。

第四章

一个C程序可能是由很多个分别编译的部分组成,这些不同部分通过一个通常叫做连接器(也叫连接编辑器或载入器)的程序合并为一个整体。因为编译器一般每次只处理一个文件,所以他不能检测出哪些需要一次了解多个源程序文件才能察觉的错误。

某些C语言实现提供了一个称为lint的程序,可以捕获大量的此类错误,但遗憾的是并非全部C语言实现都提供了该程序。如果能够找到注入lint的程序,就一定要善加利用,这一点无论怎么强调都不为过。

本章介绍如何使用 lint 程序检查 C 代码中可能导致编译失败或运行时出现意外结果的错误。在很多情况下,lint 会警告您存在编译器未对其作必要标志的不正确、有错误倾向或非标准的代码。
lint 程序会发出 C 编译器生成的每条错误消息和警告消息。它还发出关于潜在错误和可移植性问题的警告。由 lint发出的许多消息有助于您提高程序的效率,其中包括减小其大小和减少必需的内存。

lint 程序使用与编译器相同的语言环境,并且lint 的输出会定向到 stderr。

在本章中,我们将考察一个典型的连接器,注意它是如何对C程序进行处理的,从而归纳出一些由于连接器的特点而可能导致的错误。

4.1 什么是连接器

C语言中的一个重要思想就是分别编译,即若干个源程序可以在不同的时候单独进行编译,然后再恰当的时候整合到一起。但是连接器一般是与C编译器分离的,那么,连接器如何做到把若干个C源程序合并成一个整体呢? 连接器能够理解机器语言和内存布局。编译器的责任是把C源程序翻译成对连接器有意义的形式,这样连接器就能够读懂C源程序了。

典型的连接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。

连接器通常把目标模块看成是由一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别,因此,程序中每个函数和每个外部变量,如果没有被声明为static,就是一个外部对象。

4.2 声明与定义

下面的声明语句:

int a;

如果其位置出现在了所有函数体之外,那么他就被称为外部对象a的定义。这个语句说明了a是一个外部整形变量,同时为a分配了存储空间,如果全局变量没有初始化的话,那么默认他的值为0,局部变量如果没有初始化的话,那么默认他的值为随机值。

下面的声明语句:

int a=7;

在定义a的同时也为a明确了初始值,这个语句不仅为a分配了内存,而且也说明了在该内存中存储的值。

下面的声明语句:

extern int a;

并不是对a的定义,这个语句仍然说明了a是一个外部整形变量,但是因为它包括了extern关键字,这就显示的说明了a的存储空间是在程序的其他地方分配的。从连接器的角度来看,上述声明是一个对外部变量a的引用,而不是对a的定义。每个外部对象都必须在程序某个地方进行定义。因此如果一个程序中包含了语句 extern int a;那么,这个程序就必须在别的地方包括语句 int a;

4.3 命名冲突与static修饰符

两个具有相同名称的外部对象实际上代表的是同一个对象,即使编程者的本意并非如此,但系统会如此处理。因此,如果在两个不同的源文件中都包括了定义 int a;那么它或者表示程序错误,或者在两个源文件中共享a的同一个实例。

ANSIC定义了C标准库,列出了经常用到的因而可能会引发命名冲突的所有函数。这样,我们就容易避免与库文件中的外部对象名称发生冲突。

static修饰符是一个能够减少此类命名冲突有用工具。例如,以下声明语句:

static int a;

只不过a的作用域限制在一个源文件内,对于其他源文件,a是不可见的。static适用于全局变量,局部变量,以及函数。

static在C语言中的作用有3个:

  • 修饰局部变量
  • 修饰全局变量
  • 修饰函数
    其含义与下面的语句相同:
int a;
4.4 形参,实参与返回值

任何一个C函数都有一个形参列表,列表中的每一个参数都是一个变量,该变量在函数调用过程中被初始化。任何一个C函数都有返回值,要么是void,要么是函数生成的结果类型,先讨论函数的返回值。

如果一个函数在被定义或者声明之前被调用,那么他的返回值类型默认为整形

如果任何一个函数在调用他的每个文件中,都在第一次被调用之前进行了声明或者定义,那么就不会又热和与返回值类型相关的麻烦。如果想要相关的功能可以正确的实现,它必须要在主函数之前进行声明或者定义才可以。

4.5 检查外部类型

假设我们有一个C程序,他由两个源文件组成。一个文件中包含变量n的声明:

extern int n;

另一个文件中包含外部变量n的定义:

long n;

这里假定两个语句都不在任何一个函数体内,因此n是外部变量。但是,这是一个无效的C程序,因为同一个外部变量在两个不同的文件中被声明成不同的类型,但是可能C语言不能检测出这样的错误,因为编译器对不同的文件是分别进行处理的。所以,当这个程序运行的时候,究竟会发生什么情况呢?存在着很多种可能的情况:

  • C编译器能够检测到这样的错误存在
  • 但也有可能我们使用的C语言对int 和long类型的数值在内部表示上是一样的,尤其是在32位的机器上,所以,也可能是不会检测出任何错误的。
  • 变量n的两个实例虽然要求的存储空间大小不同,但是他们共享存储空间的方式却恰好能够满足这样的条件:赋给其中一个的值,对于另一个也是有效的。
  • 当然了,也有可能不会正常工作
4.6头文件

有一个好的方法可以避免大部分此类的问题,这个方法只需要我们接受一个简单的规则:每个外部对象只在一个地方进行声明。这个声明的地方一般就在一个头文件中,需要用到该外部对象的所有模块都应该包括这个头文件。特别需要指出的是,定义该外部对象的模块也应该包括这个头文件。

第五章 — 库函数

C语言中是没有定义输入/输出语句的,任何一个有用的C程序(起码必须接收零个或多个输入,生成一个或者多个输出)都必须调用库函数来完成最基本的输入/输出操作,ANSIC意识到了这一点,因而定义了一个包含大量标准库的集合。从理论上来说,任何一个C语言的实现都应该提供这些标准库函数。但是也不是都全部都在ANSIC中存在着。

有关库函数的使用,我们能给出的最好建议就是尽量使用系统的头文件,如果库文件的编写者已经提供好了精确描述库函数的头文件,不去使用他们未免也太可惜了。事实上,某些情况下为了得到正确的结果,ANSIC标准甚至强制要求使用系统头文件。

本章剩下的部分将探讨某些常见的库函数,以及在使用过程中可能出错的地方。

5.1 返回整数的getchar函数

考虑下面这个例子:

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

getchar函数在一般情况下返回的是标准输入文件中的下一个字符,当没有输入时,返回EOF(一个在头文件stdio.h中被定义的值,不同于任何一个字符)。这个程序乍一看你似乎是把标准输入复制到标准输出,实际上不是的。

原因在于,程序中的变量c被声明为char类型,而不是int类型,这意味着c无法容下所有可能的字符,特别是,可能无法容下EOF。

因此结果最终存在两种可能,一种是,某些合法的输入字符在被截断后,使得c的取值与EOF相同;另一种可能是,c根本不可能取到EOF这个值。对于前一种情况,程序将在文件复制的中途终止;对于后一种情况,程序可能会死循环。实际上,还有可能存在第三种情况,程序表面上似乎能够正常工作,但完全是因为巧合。编译器会对getchar函数的返回值做截断处理,并把低端字节部分赋值给了变量c

5.2 更新顺序文件

许多系统中的标准输入/输出库都允许程序打开一个文件,同时写入和读出的操作

FILE *fp;
fp=fopen(file,"r+");

上面的例子代码打开了文件名由变量file指定的文件,对于存取权限的设定表明程序希望对这个文件进行输入和输出操作。但是,一个输入操作不能紧随其后直接跟紧一个输出操作,反之亦然。如果要同时进行输入输出操作,必须在其中插入fseek函数的调用。

5.3缓冲输出和内存分配

当一个程序生成输出时,是否有必要将输出立即展示给用户?这个问题的答案根据不同的程序而定。

程序的输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。因此,C语言实现同行都允许程序员进行实际的写入操作之前控制产生的输出数据量。这种控制能力一般都是库函数setbuf实现的,如果buf是一个大小适当的字符数组,那么

setbuf(stdout,buf);

语句将通知输入/输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满或者程序员直接调用fflush。缓冲区的大小由系统头文件<stdio.h>中的BUFSIZ定义。

5.4 使用errno检测错误

很多库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为errno的外部变量,通知程序该函数调用失败,下面的代码利用这一特性进行错误处理,似乎再清楚明白不过了,然而却是错误的代码

/*调用库函数*/
if(errno)
   /*处理错误*/

出错原因在于:在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置errno为0,这样errno的值就可以是前一个执行失败的库函数设置的值。下面的代码做了改正,但是还是错的

errno=0;
/*调用库函数*/
if(errno)
   /*处理错误*/

库函数在调用成功的是时候,既没有强制要求对errno清零,但同时也没有禁止设置errno。既然库函数已经调用成功,为什么还有可能设置errno呢?因此,在调用库函数时,我们应该先检测作为错误指示的返回值,确定程序执行已经失败,然后再检查errno,来搞清楚错误的原因

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

实际上,所有的C语言实现中都包括有signal库函数,作为捕获异步时间的一种方式,要使用该库函数,需要再源文件加上

#include<signal.h>

以引入相关的声明。要处理一个特定的signal(信号),可以这样调用signal函数:

signal(signal type,handler function)

这里signal type代表系统头文件signal.h中定义的某些常量,这些常量用来标识signal函数将要捕获的信号类型。这里的handlerfunction是当指定的事件发生时,将要加以调用的事件处理函数。

第六章 — 预处理器

在严格的意义上的编译过程开始之前,C语言预处理器首先对程序代码做了必要的转换处理。因此,我们运行的程序实际上并不是我们所写的程序。预处理器的重要性:

  • 我们也许会与遇到这样的情况,需要将某个特定数量在程序中出现的所有实例通通加以修改。我们希望能够通过在程序中之改动一处数值,然后重新编译就可以实现我们想要一改全改的想法。预处理器对此是非常的轻而易举的。我们只需要将这个数值定义为一个显示常量就可以了。而且,预处理器还能够很容易的把所有常量都集中在一起,这样,想要找到那个常量也是非常容易的。
  • 大多数C语言实现在函数调用的时候都会带来重大的系统开销。因此,我门也许希望有这样一个程序块,他看上去像是一个函数,但却没有函数调用的的开销。举例来说,putchar函数和getchar函数经常被实现为宏,以避免每次执行输入或者输出一个字符这样简单的操作时,都要调用相应的函数从而造成系统效率的下降。

也就是说,宏提供了一种对组成C程序的字符进行变换的方式,而并不作用与程序中的对象。

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

一个函数如果不带参数,在调用是只需要在函数名后面加一对括号就可以了。而一个宏如果不带参数的话,则只需要使用宏名就可以了,括号不重要。只要宏已经定义了,就不会带来什么问题。下面宏定义中f是否带了一个参数呢?

#define f (x) ((x)-1)

答案可能有两种:f(x)或者代表 ((x)-1) 或者代表(x) ((x)-1)。在上述宏定义中,答案是第二种,因为在f和后面的(x)之间多了一个空格。所以,如果希望定义f(x)为((x)-1),必须要写成下面这个样子

#define f(x) ((x)-1)
6.2 宏并不是函数

因为宏从表面上看其行为与函数十分相似,程序员有时会禁不住把两者视为完全等同。但是注意了:宏定义中所出现的括号,他们的作用是预防引起与优先级有关的问题。因此,我们最好在宏定义中把每个参数都用括号括起来。

即使宏定义的各个参数与整个表达式都被括号括起来,也仍然可能有其他的问题存在,意思就是还是要慎重使用宏,以免造成不必要的麻烦。

6.3 宏并不是语句
6.4 宏并不是类型定义

第七章 可移植性缺陷

C语言在许多不同的系统平台上都有实现,的确,使用C语言编写程序的一个首要原因就是,C语言能够方便的在不同的编程环境中进行移植。

7.1 应对C语言标准变更
7.2 标识符名称的限制

某些C语言实现把一个标识符中出现的所有字符都作为有效字符进行处理,而另一些C实现却会自动的阶段一个长标识符名称的尾部。连接器也会对他们能够处理的名称强加限制。其实也就是想说在C语言中还是要区分标识符的大小写问题的。

7.3 整数的大小

C语言提供了三种不同长度的整数:short,int,long,C语言中的字符行为方式与小整数类似:

  • 3种类型的整数长度是非递减的,也就是说,short类型整数容纳的值肯定能够被int型整数所容纳,int类型的也能被long类型的所容纳
  • 一个普通(int类型)整数足够大以容纳那任何数组下标
  • 字符长度由硬件决定
7.4 字符是有符号整数还是无符号整数

本节讲述的主要是整形提升到底是补0还是补1的问题,涉及char到int的整形提升的问题

7.5 移位操作符
7.6 内存位置0

NULL指针并不指向任何对象,因此,除非是用于赋值比较运算,出于其他任何目的使用NULL指针都是非法的。例如,如果p或者q是一个NULL指针,那么strcmp(p,q)的值就是未定义的。

在这种情况下,究竟会得到什么结果呢?不同的编译器会有不同的结果。某些C语言实现对内存位置0强加了硬件级的保护,在其上工作的程序如果错误使用了一个NULL指针,将立即终止执行。其他一些C语言程序实现对内存位置0只允许度,不允许写。在这种情况下,一个NULL指针似乎指向的是某个字符串,但其内容通常不过是一堆垃圾信息。还有一些C语言程序实现对内存位置0既可以读,也可以写。在这种是线上面工作的程序如果错误的使用了一个NULL指针,则可能覆盖了操作系统的部分内容,造成很严重的后果。

发布了80 篇原创文章 · 获赞 84 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43831728/article/details/104338946