C陷阱与缺陷(三)库函数、预处理器、可移植性

C陷阱与缺陷(三)库函数、预处理器、可移植性,第1张

第五章 :库函数 5.1 返回整数的getchar函数

                ​​​​​​​ 

        getchar 函数在一般情况下返回的是标准输入文件中的下一个字符,当没有输入时返回EOF(一个在头文件 stdio.h中被定义的值,不同于任何一个字符)。

        这个程序乍一看似乎是把标准输入复制到标准输出,实则不然。

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

        可能会导致三种结果:①某些合法的输入字符在被“截断”后使得c的取值与EOF相同;②另一种可能是,c根本不可能取到EOF这个值。对于前一种情况,程序将在文件复制的中途终止:对于后一种情况,程序将陷入一个死循环。③编译器可能会比较getchar函数的返回值与EOF,编译器如果采取的是这种做法,上面的例子程序看上去就能够“正常”运行了。

5.2 更新顺序文件

                ​​​​​​​ 

        ①&rec在传入fread 和 fwrite函数时被转换为字符指针类型,sizeof(rec)被转换为长整型。

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

        为了保持与过去不能同时进行读写 *** 作的程序的向下兼容性,一个输入 *** 作不能随后直接紧跟一个输出 *** 作,反之亦然。如果要同时进行输入和输出 *** 作,必须在其中插入fseek函数的调用

5.3 缓冲输出与内存分配

        程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。

        因此,C语言实现通常都允许程序员进行实际的写 *** 作之前控制产生的输出数据量。

        这种控制能力一般是通过库函数setbuf实现的。如果buf是一个大小适当的字符数组,那么,

setbuf (stdout,buf ) ;

        语句将通知输入/输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到 buf缓冲区被填满或者程序员直接调用fflush(译注:对于由写 *** 作打开的文件,调用fflush将导致输出缓冲区的内容被实际地写入该文件),buf缓冲区中的内容才实际写入到stdout中。缓冲区的大小由系统头文件中的BUFSIZ定义。

                ​​​​​​​ 

        程序为了保证多线程处理,可以采取以下两种方法:第一种办法是让缓冲数组成为静态数组,既可以直接显式声明buf 为静态;第二种办法是动态分配缓冲区(malloc)。

5.4 使用errno检测错误

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

        在使用errno进行判断之前应该先对库函数的返回值进行判断(确认出错),再对errno进行判断。

        因为在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置errno为0,这样errno的值就可能是前一个执行失败的库函数设置的值。

        ​​​​​​​ 

5.5 库函数signal

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

                #include

        要处理一个特定的signal(信号),可以这样调用signal函数:

                signal (signal type,handler function) ;

                signal type:系统头文件 signal.h中定义的某些常量,这些常量用来标识signal函数将要捕获的信号类型。

                handler function:当指定的事件发生时,将要加以调用的事件处理函数。

        在许多C语言实现中,信号是真正意义上的“异步”。从理论上说,一个信号可能在C程序执行期间的任何时刻上发生。需要特别强调的是,信号甚至可能出现在某些复杂库函数(如 malloc)的执行过程中。因此,从安全的角度考虑,得到的结论是:信号非常复杂棘手,而且具有一些从本质上而言不可移植的特性。解决这个问题我们最好采取“守势”,让 signal 处理函数尽可能地简单,并将它们组织在一起。这样,当需要适应一个新系统时,我们可以很容易地进行修改。

5.6 练习

        练习5-1 当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是什么?我们能够采取怎样的措施来解决这个问题?

        一个异常终止的程序可能没有机会来清空其输出缓冲区。因此,该程序生成的输出可能位于内存的某个位置,但却永远不会被写出了。在某些系统上,这些无法被写出的输出数据可能长达好几页。

        对于试图调试这类程序的编程者来说,这种丢失输出的情况经常会误导他们,因为它会造成这样一种印象,程序发生失败的时刻比实际上运行失败的真正时刻要早得多。解决方案就是在调试时强制不允许对输出进行缓冲。要做到这一点,不同的系统有不同的做法,这些做法虽然存在细微差别,但大致如下:

                setbuf (stdout,(char * ) 0 ) ;

        这个语句必须在任何输出被写入到stdout(包括任何对printf 函数的调用)之前执行。该语句最恰当的位置就是作为main函数的第一个语句。

练习5-2 下面程序的作用是把它的输入复制到输出:

                ​​​​​​​ 

        从这个程序中去掉#include 语句,将导致程序不能通过编译,因为这时EOF是未定义的。假定我们手工定义了EOF(当然,这是一种不好的做法)

                ​​​​​​​ 

        这个程序在许多系统中仍然能够运行,但是在某些系统运行起来却慢得多。这是为什么?

        函数调用需要花费较长的程序执行时间,因此getchar 经常被实现为宏。这个宏在 stdio.h头文件中定义,因此如果一个程序没有包含stdio.h头文件,编译器对getchar的定义就一无所知。在这种情况下,编译器会假定getchar是一个返回类型为整型的函数。

        实际上,很多C语言实现在库文件中都包括有getchar函数,原因部分是预防编程者粗心大意,部分是为了方便那些需要得到getchar地址的编程者。因此,程序中忘记包含stdio.h头文件的效果就是,在所有 getchar宏出现的地方,都用getchar函数调用来替换getchar宏。这个程序之所以运行变慢,就是因为函数调用所导致的开销增多。同样的依据也完全适用于putchar。

第六章:预处理器 6.1 不能忽视宏定义中的空格

                ​​​​​​​ 

        这一规则不适用于宏调用,而只对宏定义适用。在上面完成宏定义后,f(3)与f   (3)求值后都等于2。

6.2 宏不是函数

        请注意宏定义中出现的所有这些括号,它们的作用是预防引起与优先级有关的问题。

        注意混合了宏和递增运算所带来的副作用。

        使用宏的另一个危险是,宏展开可能产生非常庞大的表达式,占用的空间远远超过了编程者所期望的空间。

6.3 宏不是语句

                ​​​​​​​

6.4 宏并不是类型定义

        宏的一个常见用途是,使多个不同变量的类型可在一个地方说明。

        但是使用typedef的方式要更加通用一些。

例如,考虑下面的代码:

                 #define T1 struct foo *    

typedef struct foo *T2;

但是,当我们试图用它们来声明多个变量时,问题就来了

T1 a,b;   ===>  struct foo *a, b;

T2 a,b;   ===> struct foo *a ,*b;

6.5 练习

        练习6-1  请使用宏来实现max 的一个版本,其中 max 的参数都是整数,要求在宏max的定义中这些整型参数只被求值一次。

        max宏的每个参数的值都有可能使用两次:一次是在两个参数作比较时;一次是在把它作为结果返回时。因此,我们有必要把每个参数存储在一个临时变量中。

        遗憾的是,我们没有直接的办法可以在一个C表达式的内部声明一个临时变量。因此,如果我们要在一个表达式中使用max宏,那么我们就必须在其他地方声明这些临时变量,比如说可以在宏定义之后,但不是将这些变量作为宏定义的一部分进行声明。如果max宏用于不止一个程序文件,我们应该把这些临时变量声明为static,以避免命名冲突。不妨假定,这些定义将出现在某个头文件中;

练习6-2  本章第1节中提到的“表达式”

                (x)((x)-1)

        能否成为一个合法的C表达式?

                一种可能是,如果x是类型名,例如x被这样定义:typedef int x;

                在这种情况下,

                (x)((x)-1)等价于

                        ( int ) ( (int ) -1)

        这个式子的含义是把常数-1转换为int类型两次。我们也可以通过预处理指令来定义x为一种类型,以达到同样的效果:

                #define x int

        另一种可能是当x为函数指针时。回忆一下,如果某个上下文中本应需要函数而实际上却用了函数指针,那么该指针所指向的函数将会自动地被取得并替换这个函数指针。因此,本题中的表达式可以被解释为调用x所指向的函数,这个函数的参数是(x)-1。为了保证(x)-1是一个合法的表达式,x必须实际地指向一个函数指针数组中的某个元素。

        x的完整类型是什么呢?为了讨论问题方便起见,我们假定x的类型是T,因此可以如下声明x:

                T x ;

        显而易见,x必须是一个指针,所指向的函数的参数类型是T。这一点让T比较难以定义。下面是最容易想到的办法,但却没有用:

                typedef void (*T)(T);

        因为只有当T已经被声明之后,才能这样定义T!不过,x所指向的函数的参数类型并不一定要是T,而可以是任何T可以被转换成的类型。具体来说,void *类型就完全可以:

                typedef void (*T) (void * ) ;

        这个练习的用意在于说明,对于那些看上去无从着手、形式“怪异”的结构,我们不应该轻率地一律将其作为错误来处理。

第七章:可移植性 7.1 应对C语言标准的变更

        许多有关可移植性的决策都有类似的特点。一个程序员是否应该使用某个新的或特定的特性?使用该特性也许能给编程带来巨大的方便,但代价却是使程序失去了一部分潜在用户。

        这个问题确实难于回答。程序的生命期往往超过了编程者最初的预料,即使这个程序只是编程者出于自用的目的而编写的。因此,我们不能只看到当前的需要,而忽视未来可能的需要。然而,为了尽量增加程序的可移植性,让过去的工具能够继续工作,而放弃现在可能的收益,这种代价又未免过于昂贵。要解决这类有关决定的问题,最好的做法也许就是承认我们需要下定决心才能做出选择,因此必须慎重对待,不能等闲视之。

7.2 标识符名称的限制 7.3 整数的大小

        1、3种类型的整数其长度是非递减的。也就是说,short型整数容纳的值肯定能够被int型整数容纳,int型整数容纳的值也肯定能够被long 型整数容纳。对于一个特定的C语言实现来说,并不需要实际支持3种不同长度的整数,但可能不会让 short型整数大于int型整数,而int型整数大于long 型整数。

        2、一个普通( int类型)整数足够大以容纳任何数组下标。

        3、字符长度由硬件特性决定。

        ANSI标准要求long 型整数的长度至少应该是32,而 short型和 int型整数的长度至少应该是16。因为大多数机器中字符长度是8位,对这些机器而言最方便的整数长度是16位和32位,因此所有早期的C编译器也都能够满足这些限制条件。

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

        如果一个字符的最高位是1,编译器是将该字符当作有符号数,还是无符号数呢?对于任何一个需要处理该字符的程序员来说,上述选择的结果非常重要。

        如果编程者关注一个最高位是1的字符其数值究竟是正还是负,可以将这个字符声明为无符号字符(unsigned char)。这样,无论是什么编译器,在将该字符转换为整数时都只需将多余的位填充为0即可。而如果声明为一般的字符变量,

        一个常见错误认识是:如果c是一个字符变量,使用(unsigned) c就可得到与c等价的无符号整数。这是会失败的,因为在将字符c转换为无符号整数时,c将首先被转换为int型整数,而此时可能得到非预期的结果。

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

7.5 移位运算符

        1.在向右移位时,空出的位是由0填充,还是由符号位的副本填充?

        2.移位计数(即移位 *** 作的位数)允许的取值范围是什么?

                关于1:与具体的C语言实现有关。

        如果被移位的对象是无符号数,那么空出的位将被0填充。

        如果被移位的对象是有符号数,那么C语言实现既可以用0填充空出的位,也可以用符号位的副本填充空出的位。

        编程者如果关注向右移位时空出的位,那么可以将 *** 作的变量声明为无符号类型,那么空出的位都会被设置为0。

                关于2:

        如果被移位的对象长度是n位,那么移位计数必须大于或等于0,而严格小于n。因此,不可能做到在单次 *** 作中将某个数值中的所有位都移出。为什么要有这个限制呢?因为只要加上了这个限制条件,我们就能够在硬件上高效地实现移位运算。

        需要注意的是,即使C实现将符号位复制到空出的位中,有符号整数的向右移位运算也并不等同于除以2的某次幂。要证明这一点,让我们考虑(-1)>>1,以除法运算来代替移位运算,将可能导致程序运行速度大大减慢。

7.6 内存位置0

        在这种情况下究竟会得到什么结果呢?不同的编译器有不同的结果。

        某些C语言实现对内存位置0强加了硬件级的读保护,在其上工作的程序如果错误使用了一个null 指针,将立即终止执行。

        其他一些C语言实现对内存位置0只允许读,不允许写。在这种情况下,一个null 指针似乎指向的是某个字符串,但其内容通常不过是一堆“垃圾信息”。

        还有一些C语言实现对内存位置0既允许读,也允许写。在这种实现上面工作的程序如果错误使用了一个null 指针,则很可能覆盖了 *** 作系统的部分内容,造成彻底的灾难!

7.7 除法运算时发生截断

        假定我们让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

        C语言或者其他语言在实现整数除法截断运算时,必须放弃上述三条原则中的至少一条。大多数程序设计语言选择了放弃第3条,而改为要求余数与被除数的正负号相同。这样,性质1和性质2就可以得到满足。大多数C编译器在实践中也都是这样做的。

        程序在设计时就应该避免值为负这样的情形。

7.8 随机数的大小

        设计之初:认为rand函数的返回值范围应该包括该机器上所有可能的非负整数取值。

        ANSIC标准中定义了一个常数RAND_MAX,它的值等于随机数的最大取值,但是早期的C实现通常都没有包含这个常数。

7.9 大小写转换

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

        早期的realloc函数的实现要求待重新分配的内存区域必须首先被释放。因为这个原因,仍然还有一些较老的C程序是首先释放某块内存,然后再重新分配这块内存。当我们移植这样一个较老的C程序到一个新的实现中时,必须注意到这一点。

百科中总结的关于realloc的用法

        1. realloc失败的时候,返回NULL

        2. realloc失败的时候,原来的内存不改变,不会释放也不会移动

        3. 假如原来的内存后面还有足够多剩余内存的话,realloc的内存=原来的内存+剩余内存,realloc还是返回原来内存的地址; 假如原来的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被free掉,realloc返回新内存的地址

        4. 如果size为0,效果等同于free()。这里需要注意的是只对指针本身进行释放,例如对二维指针**a,对a调用realloc时只会释放一维,使用时谨防内存泄露

        5. 传递给realloc的指针必须是先前通过malloc(),calloc(),或realloc()分配的,或者是NULL。

        6.传递给realloc的指针可以为空,等同于malloc

7.11 练习

练习7-1  本章第3节中说,如果一个机器的字符长度为8位,那么其整数长度很可能是16位或32位。请问原因是什么?

        某些计算机为每个字符分配一个惟一的内存地址,而另一些机器却是按字来对内存寻址。按字寻址的机器通常都存在不能有效处理字符数据的问题,因为要从内存中取得一个字符,就必须读取整个字的内容,然后把不需要用到的部分都丢弃。

        由于按字符寻址的机型在字符处理方面的效率优势,它们相对于按字寻址的机型近年来要更为流行。然而,即使对于按字符寻址的机器,字的概念在进行整数运算的时候也仍然是重要的。因为字符在内存中的存储位置是连续的,所以一个字中包含的字符数,将决定在内存中连续存放的字的地址

        如果一个字中包含的字符数是2的某次幂,因为乘以2的某次幂的运算可以转换为移位运算,所以计算机硬件就能很容易地完成从字符地址到字地址的转换。因此,我们可以合理地预期,字的长度是字符长度的2的某次幂。

        那么整数的长度为什么不是64位呢?当然,某些时候这样做无疑是有用的。但是,对于那些支持浮点运算的硬件的机器,这样做的意义就不大了;而且考虑到我们并不经常需要用到64位整数这样的精度,实现64位整数的代价就过于昂贵。如果只是偶尔用到,我们完全可以用软件来仿真64位(或者更长)的整数,而且丝毫不影响效率。

练习7-2  函数atol的作用是,接受一个指向以null结尾的字符串的指针作为参数,返回一个对应的 long 型整数值。假定:

        作为输入参数的指针,指向的字符串总是代表一个合法的long 型整数值,因此atol函数无须检查该输入是否越界。

        惟一合法的输入字符是数字和正负号。输入字符串在遇到第一个非法字符时结束。

        请写出atol函数的一个可移植版本。

                我们不妨假定在机器的排序序列中,数字是连续排列的:任何一种现代计算机都是这样实现的,而且ANSI C标准中也是这样要求的。因此,我们面临的主要问题就是避免中间结果发生溢出,即使最终的结果在取值范围之内也是如此。

                正如printnum 函数中的情形,如果long 型负数的最小可能取值与正数的最大可能取值并不相匹配,问题就变得棘手了。特别地,如果我们首先把一个值作为正数处理,然后再使它为负,对于负数的最大可能取值的情况,在很多机器上都会发生溢出。

        下面这个版本的atol函数,只使用负数(和零)来得到函数的结果,从而避免了溢出:

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1330919.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存