《C陷阱与缺陷》笔记

更新时间:2023-08-17 17:54:01 阅读量: 资格考试认证 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

在初读《C陷阱与缺陷》时,前几章介绍的问题经常遇到,较容易掌握。又因懒

于动手未做笔记,越到后面越觉得自己记忆力有限才补做笔记。毕竟好记性不如烂笔

头。前四章时在别的笔记中粘贴过来。后面几章的学习中遇到似是而非的问题也参照

了其他人得笔记并整理出自己的东西。现在一并发出来,为更多的人提供参考。

让我们一起坚定的走下去!

第1章 词法“陷阱”

1.1 = 不同于 ==

== 为比较运算符, = 为赋值运算符

例: while( c = ' ' || c == '\t' || c == '\n' )

c = getc( f );

本意是c和 ' ' 比较,但错用成赋值符。这样的后果是将 ' ' || c == '\t'

|| c == '\n' 这个表达式的值给了c, 而使c = 1。

同样: if ( ( filedesc == open( argv[i], 0 ) ) < 0 ) error();

open的返回值和filedesc比较的结果只能是0或1,所以,error没有机会调用。

但是,此时filedesc的值于open返回值无关,编译器这里不会报错。容易被忽视,

达不到检查效果。

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

&和|均为按位运算符,而&& 和 || 均为逻辑运算符,不能混淆。

1.3 语法分析中的“贪心法” 当C编译器读入一个字符后又跟了一个字符,那么编译器就必须做出判断:是将其作为两个

分别的符号对待,还是合起来作为一个符号对待。C语言对这个问题的解决方案可以归纳为

一个很简单的规则:每一个符号应该包含尽可能多的字符。

a---b 与 a -- - b 的含义相同,而与 a - -- b 的含义不同。

1.4 整型常量

如果一个整形常量的第一个字符是数字0,那么该常量将被视作八进制数。因此,10

和010是完全不同的含义。此外书中还介绍了一些ANSI C不允许的做法,比如将8

和9也作为八进制数字处理。

1.5 字符和字符串

C语言中的单引号和双引号含义迥异,在某些情况下如果把两者弄混,编译器并不会检测报

错,从而在运行是产生难以预料的结果。

用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字

符集中的序列值。

用双引号引起的字符串,代表的却是一个指向无名数字起始字符的指针,该数组被双引

号之间的字符以及一个额外的二进制为零的字符''初始化。

然而,某些C编译器对函数参数并不进行类型检查,特别是对printf函数的参数。因此,

如果用

printf(' ');

来代替正确的

printf(" ");

则会在程序运行的时候产生难以预料的错误,而不会给出编译器诊断信息。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

整型数(一般为16位或32为)的存储空间可以容纳多个字符(一般为8位),因此有个C

编译器允许在一个字符常量(以及字符串常量)中包括多个字符。也就是说,用'yes'代替"yes"

不会被该编译器检测到。后者的含义是“一次包括'y''e''s'以及空字符''的4个连续内存单元的首

地址“。前者的含义并没有准确的进行定义,但大多数编译器理解为,“一个整数值,由'y''e''s'

所代表的整数值按照特定编译器实现中定义的方式组合得到“。

(注:在Borland C++ v5.5 和 LCC v3.6中采取的做法是,忽略多余的字符,最后的整数

值即第一个字符的整数值;而在Visual C++ 6.0 和 GCC v2.95中采取的做法是,依次用后一

个字符覆盖前一个字符,最后得到的整数值即最后一个字符的整数值。)

第2章:语法“陷阱”

2.1 理解函数声明

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

任何复杂表达式其实只有一条简单的规则:按照使用的方式来声明。

任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。声

明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。

因为声明符与表达式的相似,所以我们也可以在声明符中任意使用括号:

float ((f));

这个声明的含义是:当对其求值时,((f))的类型为浮点类型,由此可以推知,f也是浮

点类型。

各种形式的声明还可以组合起来,就像在表达式中进行组合一样。因此,

float *g(),(*h)()表示*g()与(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g()):g

是一个函数,该函数的返回值类型为指向浮点数的指针。同理,可以得出h是一个函数指针,

h所指向函数的返回值为浮点类型。

一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到

了:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封

装”起来即可。例如,因为下面的声明:

float (*h)();

表示h是一个指向返回值为浮点类型的函数的指针,因此,(float (*)())表示一个“指向返

回值为浮点类型的函数的指针”的类型转换符。

(*fp)(); -> (*0)(); -> (*(void (*)())0)();

2.2 运算符的优先级问题

优先级最高者其实并不是真正意义上的运算符,包括:数组下标,函数调用操作符各结

构成员选择操作符。他们都是自左于右结合,因此 a.b.c的含义是(a.b).c。

() [] -> .

单目运算符的优先级仅次于前述运算符。在所有的真正意义上的运算符中,它们的优先

级最高。单目运算符是自右至左结合。因此*p++会被编译器解释成*(p++)。

! ~ ++ == = (type) * & sizeof

优先级比单目运算符要低的,接下来就是双目运算符。在双目运算符中,算术运算符的

优先级最高,移位运算符次之,关系运算符再次之,接着是逻辑运算符,赋值运算符,最后

是条件运算符。

* / %

+ -

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

<< >>

< <= > >=

== !=

&

^

|

&&

||

?:

我们需要记住的最重要的两点是:

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

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

2.3 主义作为语句结束标志的分号

2.4 关于switch语句

case ' ':

linecount++;

case ' ':

case '':

.......

2.5 函数调用

f();是一个函数调用语句,而f; 计算函数f的地址,却并不调用该函数。

2.6 “悬挂”else引发的问题

if (x == 0)

if (y == 0) error();

else{

z = x + y;

f(&z);

}

else与最近的if配对。除非用括号进行划分区域。

第3章 “语义”陷阱

3.1 指针和数组

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

1.C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然

而,C语言中数组的元素可以是任何类型的对象,当然也就可以是另外一个数组。

(注:C99标准允许变长数组(VLA)。GCC编译器中实现了变长数组,但细节与C99标

准不完全一致。)

2.对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标

为0的元素的指针。其他有关数组的操作,哪怕他们看上去是以数组下标进行运算的,实际

上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,

因此我们完全可以依据指针行为定义数组下标的行为。

很多程序设计语言中都内建有索引运算,在C语言中索引运算是以指针算术的形式来定

义的。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

如果一个指针指向的是数组中的一个元素,那么我们只要给这个指针加1,就能够得到指向该数组中下一个元素的指针。同样地,如果我们给这个指针减1,得到就是指向该数组中前一个元素的指针。

int calendar[12][31];

int *p;

则p = calendar; 是非法的。因为calendar是一个二维数组,即“数组的数组”,在此处的上下文中使用calendar名称会将其转换为一个指向数组的指针;而p是一个指向整型变量的指针,这个语句试图将一种类型的指针赋值给另一种类型的指针。

要构造一个指向数组的指针的方法:

int calendar[12][31];

int (*monthp)[31];

monthp = calendar;

这样,monthp将指向数组calendar的第一个元素,也就是数组calendar的12个有着31个元素的数组类型元素之一。

3.2 非数组的指针

在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符('')的内存区域的地址。

假定我们有两个字符串s和t,我们希望将这两个字符串连接成单个字符串t。 考虑:

char *r,*malloc();

r = mallor(strlen(s) + strlen(t));

strcpy(r,s);

strcat(r,t);

这个例子的错误有3点:

1,malloc函数有可能无法提供请求的内存。

2,显式地分配了内存必须显式地释放内存。

3,malloc函数并未分配足够的内存。

正确是方法:

char *r,*malloc();

r = malloc(strlen(s) + strlen(t) + 1);

if(!r) {

complain();

exit(1);

}

strcpy(r,s);

strcat(r,t);

/*一段时间之后*/

free(r);

3.3 作为参数的数组声明

在C语言中,我们没有办法可以将一个数组作为函数参数直接传递。如果我们使用数组名作为参数,那么数组名会立刻被转换为指向该数组第1个元素的指针。

因此,将数组作为函数参数毫无意义。所以,C语言中会自动地将作为参数的数组声明转换为相应的指针声明。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

3.4 避免“举隅法”

需要记住的是,复制指针并不同时复制指针所指向的数据。

3.5 空指针并非空字符串

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

#define Null 0

需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用(dereference)。 换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。

3.6 边界计算与不对称边界

在所有常见的程序设计错误中,最难于察觉的一类是“栏杆错误”,也常被称为“差一错误”(off-by-one error)。

避免“栏杆错误”的两个通用原则:

(1) 首先考虑最简单情况下的特例,然后将得到的结果外推。

(2) 仔细计算边界,绝不掉以轻心。

用第一个入界点和第一个出界点来表示一个数值范围 能够降低这类错误发生的可能性。 比如整数x满足边界条件x>=16且x<=37我们可以说x>=16且x<38,这里下界是“入界点”,即包括在取值范围之中;而上界是“出界点”,即不包括在取值范围之中。

另一种考虑不对称边界的方式是,把上界视作某序列中第一个被占用的元素,而把下界视作序列中第一个被释放的元素。

3.7 求值顺序

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

3.8 运算符&&, || 和 !

运算符&和运算符&&不同,运算符&两侧的操作数必须被求值。

3.9 整数溢出

C语言中存在两类整数算术运算,有符号运算与无符号运算。在无符号算术运算中,没有所谓“溢出”一说:所有的无符号运算都是以2的n次方为模,这里n是结果中的位数。如果算术运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发生。当两个操作数都是有符号整数时,“溢出”有可能发生。当一个运算的结果发生“溢出”时,作出任何假设都是不安全的。

无符号数不会存在溢出,而有符号的数字则会出现,在进行if(a + b < MAX_SIZE )的时候,a + b 可能会出现溢出,为了避免这种情况,我们可以强制转换为无符号类型,或者换一种表达方式,如: a < MAX_SIZE – b;

3.10 为函数main提供返回值

一个返回值为整型的函数如果返回失败,实际上是隐含地返回了某个“垃圾”整数。只要该数值不被用到,就无关紧要。 现在main函数的标准是int _tmain(int argc, _TCHAR* argv[]) (参数个数(int argc )和一个char类型的指针表示参数的值(char *argv[]))

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

第4章 连接

4.1 什么事连接器

1-1 典型的连接器把有编译器或汇编器生成的若干个目标模块,整合成为一个载入模块或可执行文件的实体,该实体能被操作系统直接执行。其中某些目标模块是直接作为输入,某些事从库文件中取得提供给连接器的。

1-2连接器是独立于C实现的, C中要提供lint程序(捕获连接器中与C有关的错误),一定要使用。

1-3 程序中,每个外部变量未被声明为static就是一个外部对象。

Static 可以将变量,函数声明为static类型,这样子就保证相关变量只能够相同源文件的函数调用,防止了函数,变量名的冲突!

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

4.2 声明与定义

2-1 int a要在所有函数体之外,它被称为外部对象a的定义,并为a分配存储空间,初始化为0。extern int a,不是对a的定义,显式的说明a的存储空间是其他地方分配的。从连接器角度看,是对外部对象的显式引用,其他地方必须存在语句int a这个外部变量。严格的说每个外部变量只能够定义一次。如重复定义可能显式程序错误或几个源文件中共享a的一个实例。

4.3 命名冲突与static修饰符

3-1 static 把定义的变量和函数作用域限制在一个源文件中,对于其他源文件不可见。

4.4 形参、实参与返回值

4-1 任何C函数都有一个形参列表,列表中每个参数都是一个变量。调用函数时将实参列表传递给被调用函数。对于某些函数形参为空,被调用时实参列表也为空。

4-2 任何C函数都用返回类型,要么是void,要么是函数生成结果的类型。main函数返回值是来告诉操作系统该函数执行的是成功还是失败(典型 0 代表成功,1代表失败),如main函数并无返回值,那么有可能看上去执行失败或得到令人惊讶的结果。

4-3 一个函数在调用之前要进行定义或声明。否则它的返回值类型就默认为整形。

4-4 一个函数只有在没有float、short、char类型的参数时,才可以在函数声明中省略参数类型的说明。这样同样依赖于调用者能提供数目正确的实参。

4-5 printf 、scanf 在不同情况下可接受不同类型的参数,特别容易出错。

例:#include <stidio.h>

Main()

{

int i;

char c;

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

{

scanf(“%d”,&c);

printf(“%d”,i);

}

}

程序中scanf要求读入一个整数,传递给它一个指向整型的指针,而c却被声明成char

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

型,得到的却是一个指向字符的指针。Scanf不能分辨这种情况,它会在指向字符的指针所指位置存储一个整数,这将把字符c附近的内存(由编译器决定)被覆盖。

4.5 检查外部类型

5-1 保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型。

5-2 一个未声明的标识符后跟一个开括号,那它将被视为一个返回整型的函数。 例 main()

{

double s;

s=sqrt(2);

printf(”%g/n”,s) }

根据上下语句aqrt函数返回 双精度类型,但调用前无声明默认返回值为整形,程序的结果将不可预测。

4.6 头文件

6-1 为避免声明类型的冲突及忘记声明,可每个外部变量只在一个地方进行声明,这个地方就是头文件中。

6-2 BIG ENDIAN:最低位地址存放高位字节,可称高位优先,内存从最低地址开始按顺序存放(高数位数字先写)。最高位字节放最前面。

LITTLE ENDIAN:最低位地址存放低位字节,可称低位优先,内存从最低地址开始按顺序存放(低数位数字先写)。最低位字节放最前面。

第5 章 库函数

尽量使用系统头文件。如库文件的编写者已经提供了精确描述库函数的头文件,不去使用它们就真是愚不可及。

5.1 返回整数的getchar函数

1-1 EOF是在stdio.h中被定义的值,不同于任何一个字符。当标准输入文件中没有输入时,返回EOF,类型为整型,避免与某些合法输入字符被“截断”后的取值相同,导致程序在文件复制的中途终止及避免取不到EOF这个值导致程序陷入死循环。

5.2 更新顺序文件

'r' 开文件方式为只读,文件指针指到开始处。

'r+' 开文件方式为可读写,文件指针指到开始处。

'w' 开文件方式为写入,文件指针指到开始处,并将原文件的长度设为 0。若文件不存在,则建立新文件。

'w+' 开文件方式为可读写,文件指针指到开始处,并将原文件的长度设为 0。若文件不存在,则建立新文件。

'a' 开文件方式为写入,文件指针指到文件最后。若文件不存在,则建立新文件。 'a+' 开文件方式为可读写,文件指针指到文件最后。若文件不存在,则建立新文件。 'b' 若操作系统的文字及二进位文件不同,则可以用此参数,UNIX 系统不需要使用本参数。

1-1为了保持与过去不能通史进行写操作的程序的上下兼容性,一个输入操作不能随后直接紧跟一个输出操作。如果要同步就要用到fseek函数改变文件状态。即对于一个可写可读流,是不能在一次读之后马上进行一次写,或者进行一次写之后马上进行一次读的,这可能会发生问题,需要在两个操作之间进行至少一次刷新。

例:FILE *fp;

struct record rec;

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

……

while( fread( (char *)&rec, sizeof(rec), 1, fp) == 1 )

{

/* handle rec here*/

if(/*we have to write rec back to file*/)

{

fseek(fp, -(long)sizeof(rec), 1);

fwrite( (char*)&rec, sizeof(rec), 1, fp);

fseek(fp, 0L, 1);

}

}

5.3 缓冲输出与内存分配

1

错误例程:

#include <stdio.h>

main() {

int c;

char buf[BUFSIZ];

setbuf(stdout, buf);

while((c = getchar()) != EOF)

putchar(c);

}

main函数并不是进程的唯一要执行的代码,作为库在将控制交回到操作系统之前所执行的清理的一部分,main结束后进程仍会进行一些与系统有关的清理工作 但char buf[BUFSIZ],在main中声明时,是在栈上的,在main结束时会被收回 ,缓冲区已经被释放了!所以在进行清理时对buf的操作是违规的。

有两种方法可以避免这一问题。

1、用静态缓冲区,或者将其显式地声明为静态:

static char buf[BUFSIZ];

2、将整个声明移到主函数之外。

3、动态地分配缓冲区并且从不释放它:

char *malloc();

setbuf(stdout, malloc(BUFSIZ));

注意在后一种情况中,不必检查malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得stdout变成非缓冲的。这会运行得很慢,但它是可以运行的。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

5.4 使用 errno检查错误

4-1 错误示例:

/*调用库函数*/

If(errno)

/*处理错误*/

这里没有对errno进行重新设置, errno可能是前一个执行失败的库函数的值。

在调用库函数时,应首先检测作为错误指示的返回值,确定程序执行已经失败然后再检查errno来弄清出错原因。例:/*调用库函数*/

If(返回的错误值)

检查errno

5.5 库函数 signal

5-1使用signal库函数:捕获异步事件的一种方式

#include <signal.h>

signal( signal type, handler function);

Signal type:系统头文件signal.h中定义的某些常量。 Handler function:指定事件发生时,将调用的事件处理函数。

但一个信号可能在C程序执行期间的任何时刻上发生。甚至可能出现在某些复杂库函数(如malloc)的执行过程中。因此从安全角度考虑,信号函数不应该调用上述类型的库函数。信号非常复杂棘手,而且具有一些从本质上不可移植的特性。解决这个问题最好采取“守势”,让signal处理函数尽可能的简单,并将它们组织在一起。

5-2 有getchar putchar的函数要包含头文件#include<stdio.h>,否则函数中getchar putchar宏出现的地方将被替换成getchar putchar函数,降低系统效率。

第6章 预处理器

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

1-1 define f (x)((x)-1)表示 f代表(x)((x)-1)。

6.2 宏并不是函数

2-1 宏定义中的括号不是函数调用的意思,它们是预防引起与优先级有关的问题。因此,在宏定义中最好把每个参数都用括号括起来。

2-2 错误示例(效率低而且可能是错误的):

biggest=x[0];

i=1;

while(i<n)

biggest=max(biggest,x[i++]);

初始化数组x[0]=2; x[1]=3; x[2]=1;

若max被定义成宏则上式被预处理为 biggest=((biggest)>x[i++])?(biggest);(x[i++]) 这里执行后比较和取值将进行两次使x[i++]的值发生了变化。(max宏的每个参数值都可能使用两次,一次是在两个参数比较时,一次是作为结果返回时)

解决类似问题:

1、确保宏中的参数没有副作用

例:biggest=x[0];

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

biggest=max(biggest,x[i]);

2、在max宏之外的地方把每个参数存储在一个临时变量中,若max宏不止一个程序文件,

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

我们应把这些临时变量声明为 static,以避免命名冲突。

例:static int max_temp1,max_temp2;

#defin emax(p,q)

(max_temp1=(p),max_temp2=(q),max_temp1>max_temp2?max_temp1:max_temp2)

3、让max这类作为函数而不是宏。

4、直接编写比较两者大小的代码。

例:biggest=x[0];

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

if(x[i]>biggest)

biggest=x[i];

6.3 宏并不是语句

3-1考虑这样一个例子

#define assert(e) if(!(e)) assert_error(__FILE_,_LINE_)

则对以下代码

if(x>0 && y>0)

assert(x>y);

else

assert(y>x);

展开后得到,

if(x>0 && y>0)

if(!(x>y)) assert_error("foo.c",37);

else

if(!(y>x)) assert_error("foo.c",39);

注意到,else并不是与第一个if 匹配,这与我们的期望不符。解决办法是,将宏assert定义为一个表达式而不是一个语句:

#define assert(e) ((void)((e)||_assert_error(_FIL_,_LINE_)))

上述定义利用了对 || 两侧操作数依次顺序求值的性质。

6.4 宏并不是类型定义

4-1 考虑下面的代码

#define T1 struct foo *

typedef struct foo *T2;

从两个定义来看,T1和T2从概念上完全符同,都是指向结构foo的指针。但是 T1 a,b;

T2 a,b;

对于第一个声明展开后,struct foo *a,b; ,a为指向结构的指针,而b并定义成一个结构,与期望不符。

第二个声明中a b都是指向结构的指针。

第7章 可移植性缺陷

7.1 应对C语言标准变更

7.2 标识符名称的限制

2-1 某些C语言实现:

1、把一个标识符中出现的所有字符都作为有效字符处理,另一些实现会自动截断一个长标识符的尾部。

2、标识符区分大小写,另一种不区分。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

ANSI中 C实现必须区分前6个字符不同,且不区分大小写字母。

7.3 整数的大小

3-1 short型整数肯定能被int型整数容纳,int型整数肯定能被long int型整数容纳。特定C并不需要支持3中不同长度整数,但可能不会让short大于int型整数,不会让int大于long型整数。

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

3-3 字符长度由硬件特性决定。现大多数机器字符长度是8位,也有的是9位,越来越多的C语言字符长度是16位(处理诸如日语之类的大字符集)。

ANSI标准:long整型长度至少应该是32位,而short和int整型长度至少应该是16位。

可移植性最好的办法就是声明该变量为long型。但在这种情况下定义一个新的类型更清晰:typedef long tenmil;这样最坏的情况下只要改动类型定义即可。

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

4-1 大多数编译器把字符实现为8位,但并不是所有编译器都按照同样的方式来解释这8位数,在把一个字符转换成一个较大整数时这个问题需要重视。在其他情况下多余的位将被简单的“丢弃”。

在讲char类型转换成int类型时编译器:

1、将字符作为有符号数:编译器在将char扩展到int类型时,同时复制符号位。(8位字符的取值范围是-128到127)

2、将字符作为无符号数:编译器只需在多余位上填充0即可。(8位字符的取值范围是0到255)

4-2 如一字符的最高位是1,程序员可将这个字符声明为无符号字符(unsigned char),这样无论声明编译器将该字符转换为整数时都只需在多余位上填充0。

4-3 (unsigned)c :执行中要先将字符c转换int型整数,在把int型整数转换成无符号整数。这样可能得不到与c等价的无符号整数。

解决方式:使用(unsigned char)c 直接将字符c转换成无符号整数。

7.5 移位运算符

5-1 在向右移位时如果被移位对象是无符号数,那么由0填充;如果是有符号数,那么是0或符号位的副本填充。

如果被移位对象的长度是n位,那么移位计数必须大于或等于0,而严格小于n。 5-2 有符号整数的向右移位运算并不等同于除以2的某次幂。例(-1)>>1不等于0,而(-1)/2 等于0。

5-3 移位运算符一般比除法代替的移位运算符运行速度要快。

7.6 内存位置0

6-1 null指针并不指向任何对象,除用于赋值或比较运算外,任何情况使用null指针都是非法的。

6-2 在所有的C程序中,误用null指针的效果都是未定义的。然而这样的程序可能在某个C实现中能工作,只有转移到另一台机器上运行时才会暴露出问题来。所以检查这类问题最简单方法就是把程序移植到不允许读取内存位置0的机器上运行。

7.7 除法运算时发生的截断

7-1 假定a除以b,商为q,余数为r,那么我们认为的整数除法和余数操作应具备的a、b、q、r之间的关系:

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

1、q*b+r==a :定义余数关系

2、如果我们改变a的正负号,我们希望会改变q的符号,但这不会改变q的绝对值。

3、当b>0,我们希望r>0且r<b。

在c实际运算中,上述3条不能同时满足,至少放弃3条中的1条。大多数是放弃第三条,而改为余数与被除数正负号相同。

7-2 在程序设计时避免被除数n为负这样的情形,并且声明n为无符号数。

7.8 随机数的大小

8-1 随机数返回值:PDP-11和AT&T是 0~215-1 ;VAX-11是0~231-1这样我们在程序中用到rand函数,在移植时就必须根据特定的C语言实现做出“剪裁”。

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

7.9 大小写转换

9-1 大多数toupper和tolower的使用都需要首先进行检查以保证参数时合适的。所以最后AT&T重写的toupper和tolower函数大致如下例:

int topper(int c)

{

if(c>=’a’&&c<=’z’)

return c+’A’-’a’;

return c;

}

这样虽然避免了参数类型不合适的错误和宏定义中可能因为带副总用的表达式多次求值造成的错误,但这样的函数使开销增大,效率降低。

在AT&T的系统上使用toupper和tolower时不必担心传入大小写不合适的字母,但在其他一些C语言实现上,程序却可能无法运行。

9-2 引入新的宏,让使用者在速度与方便之间根据程序选择。

#define _toupper(c) ((c)+’A’-‘a’):小写转换大写

#define _tolower(c) ((c)+’a’-‘A’):大写转换小写

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

10-1 调用realloc函数时一般c语言实现需传入已分配内存区域的指针以及这块内存新的大小作为参数,这样就可以调整这块区域为新的大小。

10-2、UNIX参考手册第7版中,realloc的实现允许在某内存块被释放之后重新分配其大小,前提是内存重分配操作执行的必须足够早。

例:free(p);

p=realloc(p,newsize);

这在符合第7版参考手册描述中的系统就是合法的

7.11 可移植性问题的一个例子

输入一个long型整数和一个函数指针。程序把给出的long型整数转换为其10进制表示,并且对10进制表示中的每个字符都调用函数指针所指向的函数。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

void

printneg( long n, void (*p)() )

{

long q;

int r;

q = n / 10;

r = n % 10;

if( r > 0 ){

r - = 10;

q++;

}

if (n <= -10)

printneg( q, p);

(*p) ( "0123456789"[ -r ] );

}

void

printnum (long n, void (*p)() )

{

if ( n < 0) {

(*p)('-');

pirntneg(n, p);

}else

printneg(-n, p);

}

移植性需考虑的地方:

11-1 机器的字符表不同,则通过 +’0’把数字转换成字符不一定合适,这样只是假定机器的字符集是顺序排列没有间隔的。但除符合ANSI的C实现的机器上不一定正确。

解决办法:使用一张代表数字的字符表。因为一个字符串常量可以用来表示一个字符数组,所以在数组名出现得地方都可用字符串常量来替代。

11-2 基于2的补码的计算机一般允许表示负数的取值范围大于正数的取值范围。所以当n为负数时,我们不能对-n进行求值,这样将引起溢出。

无论是基于1的补码(one's complement:二进制的下限 -(2N-1))还是基于2的补码(two's complement:二进制的下限 -(2N))的机器,改变一个正整数符号都可以保证不发生溢出,麻烦来自于当改变一个负数的符号时。

所以这里函数先检查n是否为负如果为负就先打印一个符号,再以n的绝对值的相反数为参数进行运算。

11-3 当n是负数时,n%10完全有可能是一个正数,因此 -(n%10)就是一个负数,则“0123456789”[-(n%10)]就不在数字数组中。

解决方法:创建两个临时变量来分别保存商和余数。在除法运算完成之后,检查余数是否在合理的范围内。要不在,就如上例适当调整两个变量。

第8章 建议

不要说服自己相信“皇帝的新装”:有的错误极具伪装性和欺骗性。

C中的经典,经过半个月的时间,慢慢的看了,感觉挺好的!做了点小总结

将惯用的c == '\t' 写作 '\t' == c。一旦写错成=号,编译器就能检查出来。 直接了当的表明意图。

当有些代码可能被误解为另一种意思时,请用括号和其他方式让原意图尽可能的清楚。有助于日后程序的维护。

构思或测试程序时考查最简单的特例。

(输入数据为空或者只有一个元素时)

使用不对称边界。

注意潜伏在暗处的Bug。

坚持使用C中众所周知的部分,避免使用生僻的语言特性。你能增加移植性,减少编译器Bug。

防御性编程。

对程序用户和编译器实现的假设不要过多。

增加:C中文件的操作

向文件读写单个字符:fgetc(FILE *fp), fputc(char ch, FILE *fp)。

例如进行文件复制:while(!feof(fp1)){

char ch = fgetc(fp1);

fputc(ch,fp2);

}

向文件读写字符串: char *fgets(char *str, int n, FILE *fp), char *fputs(char*str,FILE *fp)

本文来源:https://www.bwwdw.com/article/9j8j.html

Top