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中。缓冲区的大小由系统头文件
程序为了保证多线程处理,可以采取以下两种方法:第一种办法是让缓冲数组成为静态数组,既可以直接显式声明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函数,只使用负数(和零)来得到函数的结果,从而避免了溢出:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)