《C专家编程》读书笔记之第1~4章

一、C:穿越时空的迷雾

1. C标准中定义了描述编译器的特点的一些术语:

(1) 由编译器定义的(imprementation-defined)

    由编译器设计者决定如何处理。例如:整型数右移时要不要扩展符号位。(vc6.0中是扩展的)

(2) 未确定的(unspecified) 

    在某些正确情况下的做法,但标准并未规定应该怎样做。例如:参数求值的顺序。

(3) 未定义的(undefined)

    在某些不正确情况下的做法,但标准并未规定应该怎样做。例如:当一个有符号整数溢出时该如何处理。

(4) 约束条件(a constraint)

    必须遵守的限制或要求。例如:%操作符的操作数必须属于整型。

(5) 严格遵循标准的(strictly-confirming)

    一个严格遵循标准的程序应该是:a.只使用已确定的特性;b.不突破任何由编译器实现的限制;c.不产生任何依赖于由编译器定义的或未确定的或未定义的特性的输出。

(6) 遵循标准的(confirming)

    一个遵循标准的程序可以依赖一些某种编译器特有的不可移植的特性。

 

2. ANSI C标准对编译器制定了一些限制条件,比如函数定义中形参数量的上限至少可以达到31个、在表达式中至少可以支持32层嵌套的括号等。

 

3. 有时必须非常专注地阅读ANSI C标准才能找到某个问题的答案。作者举了一个例子:比如把char**变量的值赋给const char**时,编译器会发出错误警告,为什么呢?

char alpha = 'c';
char *pc = α
char **ppc = &pc;
const char **ppcc1 = ppc;  // error,ppcc1 points to a pointer whose type is const char*
char *const *ppcc2 = ppc;  // ok, ppcc2 points to a pointer whose type is char* const
char ** const ppcc3 = ppc; // ok, ppcc3 points to a pointer whose type is char*

    C标准中的描述:“要使指针的赋值形式合法,必须满足:两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符(指const或volatile)”。注意,const char类型有限定符const,而const char *类型并没有限定符,它的类型是“指向一个有const限定符的char类型的指针”。同理,const char **同样没有限定符。由于char **和const char **都是没有限定符的指针类型,但它们所指向的类型不一样(前者是char *,后者是const char *),所以它们是不相容的,不能相互转换。

 

4. const实际上表示read-only,在一个符号前加上const限定符只是表示这个符号不能被修改,但const并不能把变量变成常量,因此在C语言中不能用const int型整数来定义数组的长度,尽管在C++中这是允许的。

 

5. sizeof返回的是unsigned int型。

#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))
int array[] = {0,1,2,3,4};
int d = -1;
/* ... */
if (d <= TOTAL_ELEMENTS - 2)  // d被强制转换为一个大数
    x = array[d+1];

 

二、这不是Bug,而是语言特性

1. 多做之过

(1) switch语句缺省采用fall through(即如果case后面不加break就依次执行下去)在97%的情况下是失误,据统计缺省的fall through的使用频率只有3%。

    另外注意,break跳出的是最近的那层循环语句或switch语句。

switch (line) {
    case THING1: 
        doit1();
        break;
    case THING2:
        if (x == STUFF) {
            do_first_stuff();
            if (y == OTHER_STUFF) 
                break;
            do_later_stuff();       
        }
        initialize_modes_pointer(); // 代码的意图是跳到这里
        break;
    default:
        proccessing();
} // 事实上是跳到这里
use_modes_pointer();  //导致modes-pointer未初始化

(2) 自动合并字符串

    ANSI C规定相邻的字符串常量将被自动合并成一个字符串。但这也意味着字符串数组在初始化时如果不小心漏掉了一个逗号,编译器将不会发出错误信息,而是悄无声息地把两个字符串合并在一起。

//下面的指针数组只有两个元素,分别指向"Math"和"ChineseEnglish"
char *subject[] = {"Math", "Chinese"  "English"}; 

(3) 允许数组末尾出现拖尾的逗号

char *fruit[] = {"apple", "banana", "orange",};  // ok

(4) 太多的缺省可见性

    a. 定义C函数时,缺省情况下函数名是全局可见的,若想限制对该函数的访问,必须加个static。b.太大范围的全局可见性还影响到C语言的另一个特性:interpositioning,即用户编写与库函数同名的函数并取而代之的行为。c. C语言中对信息可见性的选择只有两种:all-or-nothing,一个符号要么全局可见,要么对其他文件都不可见。

 

2. 误做之过

(1) C语言中不少符号或关键字被重载而具有好几种意义,例如static,extern,void,*,&,()等等。

(2) "有些运算符的优先级是错误的",这些运算符是“当按照常规方式使用时,可能引起误会的任何运算符”。

*p.f  // .优先级高于*,故等价于*(p.f)
int *ap[] // []优先级高于*,故等价于int* (ap[])
int *fp()  // ()优先级高于*,故等价于int* (fp())
(ip & mask != 0) // 关系运算符高于逻辑运算符,故等价于ip & (mask != 0)
c = getchar() != EOF // 关系运算符高于赋值运算符,故等价于c = (getchar() != EOF)
msb << 4 + lsb // 算符运算符高于移位运算符,故等价于msb << (4+lsb)
i = 1,2 //逗号运算符优先级最低,故等价于(i=1), 2

(3) 早期gets()中的Bug导致了Internet蠕虫。gets()函数并不检查缓冲区的空间,如果函数的调用者提供了一个指向堆栈的指针,并且gets()函数读入的字符数量超过了缓冲区空间,gets()函数将会愉快地将多出来的字符继续写入到堆栈中,从而覆盖堆栈原先的内容。

 

3.少做之过

(1) 标准参数处理:C语言不能区分运行时选项和其他参数。

(2) 空格

    a. 当"\"转义为newline时,"\"后面有无空格无法看出来。

    b. 有无空格对自增操作符的影响

z = y+++++x;    // error
z = y++ + ++x;  // ok

    c. 当程序员有两个指向int的指针并想对两个int数据执行除法运算时,"/"后面要有空格。

ratio = *x / *y  // ok
ratio = *x/*y    // error, "/*" is the beginning of a comment

(3) 把lint程序从编译器中分离出来作为一个独立的程序是个严重的失误。应该“早用lint程序,勤用lint程序”。

 

三、分析C语言中的声明

1. C语言设计哲学:对象的声明形式与它的使用形式尽可能相似。

2. C语言声明的一个缺点:操作符的优先级设计不当、过于复杂,使人们无法以习惯的自然方式从左到右阅读一个声明。

 

3. C语言声明的名字

(1) 类型说明符:void, char, short, int, long, signed, unsigned, float, double, struct, enum

(2) 存储类型:extern, static, register, auto

(3) 类型限定符:const, volatile

 

4. 结构体

(1) 结构体的声明与结构体变量的定义应该分开写,以增加可读性。毕竟,我们只编写一次代码,但在以后的程序维护过程中将多次阅读它们。

(2) 参数传递时首先尽可能地存放在寄存器中。

(3) 若把数组放在结构体中,则对结构体赋值时,整个数组都会被赋值。

 

5. #define定义的名字一般在编译时就被丢弃,而枚举名字则通常在调试器中一直可见,可以在调试代码中使用它们。

 

6. C语言声明的优先级规则

(1) 声明从它的名字开始读取,然后按照优先级顺序依次读取

(2) 优先级从高到低依次是:

    a. 声明中被括号括起来的那部分

    b. 后缀操作符:()表示这是一个函数,[]表示这是一个数组

    c. 前缀操作符:*表示“指向...的指针”

(3) 如果const或volatile关键字的后面紧跟类型说明符(如int,double),那么它作用于类型说明符,否则作用于它左边紧邻的指针星号。

 

7. typedef

(1) typedef和#define的区别

    a. 可以用其他类型说明符对宏类型名进行扩展,但对typedef定义的类型名不能这样做。

#define peach int 
unsigned peach i;  // ok

typedef int banana;
unsigned banana i;  // error

    b. 在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证

#define int_ptr int *
int_ptr chalk, cheese;  // chalk is int *, while cheese is int

typedef char * char_ptr;
char_ptr duck, mouse;  // both of duck and mouse are char*

(2) 使用typedef的建议

    a. 不要为了方便起见对结构体使用typedef,这样做唯一的好处是使你不必书写struct关键字,但这个关键字可以向你提示一些信息,你不应将其省掉。

    b. typedef应该用在:数组、结构体、指针以及函数的组合类型;可移植类型。

    c. 应该始终在结构体的定义中使用结构标签,这样可使代码更清晰。

 

四、数组和指针并不相同

 1. 左值与右值

(1) 左值:出现在赋值符左边的符号,表示存储结果的地方,编译时可知。左值又分可修改与不可修改两种,数组名是不可修改的左值。

(2) 右值:出现在赋值符右边的符号,表示地址对应的内容,运行时可知。

2. 指针的外部声明与数组定义并不匹配

// file 1:
int mango[100];
// file 2:
extern int *mango;  // error, the type doesn't match

3. 指针与数组的区别

 

你可能感兴趣的:(《C专家编程》读书笔记之第1~4章)