C和指针[Kenneth A.Reek 著]

第一章:快速上手

1.使用scanf函数进行用户输入的时候,需要注意:
使用所有格式码(除了%c之外)时,输入值之前的空白(空格、制表符、换行符等)会被跳
过,值后面的空白表示该值的结束。也就是说只有%c还会响应用户的空白输入,其他都
会忽略重复的空白。

第二章:基本概念

1.不良的风格和不良的文档是软件生产和维护代价高昂的两个重要原因。
良好的编程风格能够大大提高程序的可读性。良好的编程风格的直接结果就是程序更容
易正确运行,间接结果就是他们更容易维护。

第三章:数据

1.typedef:自定义数据类型

//自定义字符型指针的类型名为:ptr_to_char
typedef char *ptr_to_char;
//使用自定义的数据类型声明变量a
ptr_to_char a;
等价于
char *a

应该使用typedef而不是#define来创建新的数据类型名,请看下例:

#define d_ptr_to_char char *
d_ptr_to_char a, b;
等价于
char *a, b;

2.使用define时需要常加括号,以免优先级的变化改变代码的初衷

第四章:语句

1.在使用if语句时,即使对应的代码块仅有一条语句也要保持使用括号的习惯

2.在每一个switch语句中都使用default子句

第六章:指针

1.理解变量名与内存地址
名字与内存位置至今的关联并不是硬件所提供的,它是由编译器为我们实现的。所有这
些变量给了我们一种更方便的方法”记住地址”
硬件仍然通过地址访问内存位置。

2.值的类型并非值本身所固有的一种特性,而是取决于它的使用方法。
所以下面这段代码,能够在使用ASCII码的系统中实现字符型转为整形

char a;
int b;
a = '3';
//将a作为一个整数使用,减去'0'(整形的48)。b的值为3
b = (int)a - '0';

3.变量的值就是分配给该变量的内存位置所存储的数值,即使是指针变量也不例外

4.对所有的指针变量进行显示的初始化是种好做法。如果已经知道指针将被初始化为什
么地址,就把它初始化为该地址,否则就把它初始化为NULL
风格良好的程序会在解引用之前对它进行检查,这种初始化策略可以节省大量的调试时间

5.关于指针表达式
通过一些例子,详细解释常见的指针表达式

//声明
char ch = 'a';
char *cp = &ch;
char tmp, *tmp_p;

//ch作为左值使用,表示变量ch的内存地址,语句的含义是将'b'写到ch所在的内存地址
ch = 'b';    
//ch作为右值使用,表示变量ch的值,即'a'
tmp = ch;    

//&ch仅能作为右值使用,表示变量ch的内存地址
tmp_p = &ch;    

//cp作为左值,表示cp的内存位置,语句含义是将&ch写到cp所在的内存地址
cp = &ch;        
//cp作为右值,表示cp的值,即ch的内存位置
tmp_p = cp;        

//*cp作为右值,表示'cp指向的内存位置的内容',即ch的值
tmp = *cp        
//*cp作为左值,表示'cp指向的内存位置',即ch的内存地址
*cp = 'b'        

*(cp + 1) 与 *cp + 1的区别:前者作为右值表示取cp所指向内存地址(即ch的内存
地址)的下一个地址中的内容,后者表示取ch中的值,然后加1
++cp:只能作为右值使用。将cp指向的地址后移1个单位,返回移动后cp的一个拷贝
cp++:只能作为右值使用。先返回cp的一份拷贝,然后增加cp的值

*++cp: 先对cp后移一个单位,然后解引用
*cp++: ++操作符产生cp的一份拷贝,然后cp后移一个单位,最后对得到的拷贝进行
解引用

注:后缀++操作符优先级高于解引用 操作符。所以会误认为 cp++ 是等同 *(cp + 1)的
,但实质上确实执行上述的操作。对于这种容易混淆的表达式,应减少使用

第七章:函数

1.在表达式内部调用一个无返回值得函数是一个严重的错误,因为这样一来在表达式的
求值过程中会使用一个不可预测的垃圾值。所幸的是现代编译器通常能捕捉到这类错误

2.函数声明的时候,必须指定参数的类型。但是在函数原型中加入描述性的参数名是明
智的,因为它可以给希望调用该函数的人员提供更多有用的信息

3.一个没有参数的函数的原型应该写成下面这个样子:

int func( void )

关键字void提示没有任何参数,而不是表示它有一个类型为void的参数

4.C函数的所有参数均以“传值调用”方式进行传递

5.stdarg宏的使用
可变参数列表是通过宏实现的,这些宏定义在头文件stdarg.h,它是标准库的一部分。
这个头文件声明了一个类型va_list和三个宏—-va_start, va_arg, va_end。
下面是书中的例子:

//包含相应头文件
#include<stdarg.h>    
//第一个参数是必须给出的,通常使用这个参数指定剩余的参数个数
float average ( int n_value, ... )    
{
    //用于访问参数列表的未确定部分
    va_list var_arg;    
    //使用va_start函数对va_list变量进行初始化,第二个参数是省略号前最后  
    //一个有名字的参数  
    va_start( var_arg, n_value );    
    //初始化过程把var_arg变量设置为指向可变参数部分的第一个参数
    for(count = 0; count < n_value; count++ ){
        //使用va_arg函数访问变量,接受两个参数:va_list变量和参数列表中  
        //下一个参数的类型
        sum += va_arg( var_arg, int );    
    }
    va_end( var_arg );
    return sum / n_value;
}

6.抽象数据类型可以减少程序对模块实现细节的依赖,从而提高程序的可靠性

第八章:数组

1.在C中,在几乎所有使用是组名的表达式中,数组名的值是一个指针常量,也就是
数组第1个元素的地址。
只有在两种场合下,数组名并不用指针常量来表示:
a.当数组名作为sizeof操作符:sizeof返回这个数组的长度,而不是指向数组的指针的
长度
b.单目操作符&的操作数:取一个数组名的地址所产生的是一个指向数组的指针,而不
是一个指向某个指针常量的指针

2.指针与下标:假设这两种方法都是正确的,下标绝对不会比指针更有效率,但指针有
时会比下标更有效率
试比较下面两段代码:

int array[10], a;
for ( a = 0; a < 10; a += 1 ){
    array[a] = 0;
}

int array[10], *ap;
for ( ap = array; ap < array + 10; ap++ ){
    *ap = 0;
}

第一段代码对于下标计算由于a是变量,所以求下标的时候都需要重复:取a的值,并把
它与整形的长度(4)相乘。
而第二段代码中对于ap++,每次都是加固定的值,且这个乘法(1*4)只在编译时执行一
次。运行时并不执行乘法。

3.当根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生效
率更高的代码。当这个增量是1并且机器具有地址自动增量模型时,这点表现的更为突出

第九章:字符串、字符和字节

1.strlen返回值类型为size_t,该类型是在头文件stddef.h中定义的,是一个无符号整
数类型。
所以下面两条语句,第一条效果符合预期,第二条语句结果则恒为真

if( strlen(x) >= strlen(y) ) ...
if( strlen(x) - strlen(y) >= 0 ) ...

2.不要试图自己编写功能相同的函数来取代库函数,同时使用字符分类和转换函数可以
提高函数的移植性

3.寻找一种更好的算法比改良一种差劲的算法更有效率,复用已经存在的软件比重新开
发一个效率更高

4.使用strcpy函数把一个长字符串复制到一个较短的数组中,容易导致溢出。因为strcpy
无法判断目标字符数据的长度

5.strtok函数将会修改它所处理的字符串,所以strtok是不可再入的。
不可再入是指函数在连续几次调用中,即使它们的参数相同,其结果也可能是不同的。

第十章:结构和联合

1.结构体的声明语法:

struct  tag { member-list } variable-list;

标签(tag)字段允许为成为列表提供一个名字,这样它就可以在后续的声明中使用。

2.警惕下面这个陷阱:

typedef struct {
    int a;
    SELF_REF3 *b;
    int c;
}SELF_REF3;

这个声明的目的是为了这个结构创建类型名SELF_REF3。但是,它失败了。类型名直到
声明的末尾才定义,所以在结构声明的内部它尚未定义。
解决方案是定义一个结构标签(tag)来声明b,如下所示:

typedef struct SELF_REF3_TAG {
    int a;
    struct SELF_REF3_TAG *b;
    int c;
}SELF_REF3;

3.箭头操作符接受两个操作数,但左操作数必须是一个指向结构的指针

4.关于结构体的内存分配
a.一般情况下,编译器按照成员列表的顺序一个接一个地给每个成员分配内存。只有当
存储成员时需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存
空间。sizeof返回值包含了结构中浪费的内存空间
b.系统禁止编译器在一个结构的其实位置跳过几个字节来满足边界对齐条件
c.使用offsetof宏能够获取某个成员实际的存储位置相对于结构体起始位置的偏移

offsetof (type, member)
offsetof (struct ALIGN, b)

d.使用下面的方法能够通知编译器不满足边界对齐的条件

#pragma pack(push)
#pragma pack(1)
typedef struct _BMP_HEAD{
    //some declarations
}BMP_HEAD;
#pragma pack(pop)

5.关于位段
a.位段成员必须声明为int, signed int, unsigned int类中
b.在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目
c.注重可移植性的程序应该尽量避免使用位段

第十一章:动态内存分配

1.通过malloc申请的内存空间并没有以任何方式进行初始化,需要程序猿手动优化。

2.动态内存分配最常见的错误就是忘记检查所请求的内存是否成功分配

3.动态内存分配第二大错误来源是操作内存时超出了分配内存的边界

第十三章:高级指针话题

1.函数指针用法

int f(int a);
int (*pf)(int) = &f;

初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函
数指针。&操作符只是显式地说明了编译器将隐式执行的任务。

int ans;
ans = f(25);
ans = (*pf)(25);
ans = pf(25);

第一条语句简单地使用名字调用函数f,但它的执行过程可能和你想象的不太一样。函
数名f首先被转换为一个函数指针,该指针指定函数在内存中的位置。然后,函数调用
操作符调用该函数,执行开始于这个地址的代码。

第二条语句效果与第一条完全一样。而且本质上间接访问操作是不需要的,因为编译器
在执行函数调用操作符之前又会把它转换回去。

第三条语句和前两条语句的效果是一样的。正如第二条解释中提到的,间接访问操作并
非必需。

2.函数指针用途:两个最常见的用途是把函数指针作为参数传递给函数以及用于转换表

第十五章:输入/输出函数

1.只有当一个库函数失败是,errno才会被设置。当函数成功运行时,errno的值不会被
修改。这意味着我们不能通过测试errno的值来判断是否有错误发生。反之,只有当被
调用的函数提示有错误发生时检查errno才是有意义的。

2.这章的总结部分很值得一看。

3.在任何scanf系列函数的每个非数组,非指针参数前需要加上&符号。

总结

C语言之所以被称之为”God’s programming Language”,我想一个很重要的原因就是指针
这部分的功能。啃完这本书,希望能提高自己对C语言的核心部分的理解吧。最重要的当
然是尽量避免书中提到的陷阱,同时谨记其对编程实践的警告提示。