《C专家编程》写的较为口语化,作者以一种很轻松的方式向我们展现了C语言一些容易忽略的知识点,这些知识点对我们的编程很有价值。
第一章
c预处理器功能:
- 字符串替换。把常量字符串替换到程序中
- 头文件包含
- 通用代码模版的扩展,即替换宏定义
不可移植的代码:依赖于某一个具体编译器的代码是不可移植的代码。因为c语言标准对某些情况下的做法并没有做出明确的规定,因此在不同编译器中面对这些情况有不同的处理方法,但这些做法都是正确的。
可移植的代码:严格遵循c语言标准。因此,在不同平台上都会产生相同的输出。
const的用法:在一个符号前面加上const限定符只是表示这个符号不能被赋值,意味着它的值对于这个符号来说是只读的,但它并不能防止通过程序的其它方法来修改这个值。const最有用的地方是用它来限定函数的形参,这样该函数就无法修改实参指针指向的数据。如
const int limit = 10;
const int *limitp = &limit;
int i = 27;
limitp = &i;
无法通过limitp这个指针修改它指向的数据的值,但这个指针的值却可以改变。
当执行算术运算时,操作数的类型如果不同,就会发生转换。数据类型一般朝着浮点精度更高、长度更长的方向转换,整型数如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned。
但这有时会产生bug
#define size_int sizeof(int)
int main()
{
int d = -1;
if(d < size_int)
{
}
return 0;
}
这段代码中的if语句判断是不通过,这是因为sizeof()返回的值是无符号类型的,if语句测试int和unsigned int时,int d自动升级为unsigned int,此时d变为一个巨大的正整数,导致表达式为假。要修正,只需要将size_int强制转换为int就可以
if(d < (int)size_int)
第二章
容易出错的switch语句
switch语句默认行为是“fall through”,它表示如果case语句后不加break,就依次执行下去。一般认为,这种默认行为是设计c语言时的一种瑕疵,大多数程序并不需要这种“fall through”
相邻字符串自动合并
ANSI C引入的一个新特性是相邻的字符串常量将被自动合并为一个字符串,这省掉了过去在书写多行信息时必须在末尾添加“\”的做法。但这种新特性有时会由于程序员的不小心而造成bug。在初始化字符串时,如果漏掉一个逗号,编译器将不会发出警告,而是自动地将两个字符串合并起来。
函数的默认全局性
定义c函数时,默认情况该函数是全局可见的。也就是定义函数时,在前面添加“extern”关键字和不添加该关键字的效果是一样的。根据经验,c函数的这种默认的全局可见性是一种瑕疵,软件对象在大多数情况下都应该默认采用有限可见性,当需要全局可见时采用显式的手段。这种默认全局可见的特性有时是会造成bug的,因此,程序员在定义函数时,如果不是故意要该函数全局可见,就最好在函数前加上“static”关键字使该函数在这个文件之外不可见。
第三章
结构体
参数在传递时首先尽可能地存放到寄存器中(追求速度)。一个int型变量i跟只包含一个int型成员i的结构变量s在参数传递时的方式可能完全不同。一个int型参数一般会传递到寄存器中,而结构体参数则很可能被传递到堆栈中。
联合
联合的外表与结构相似,但在内存布局上存在关键性差别。结构体中,每个成员依次存储,而在联合中,所有成员都从偏移地址零开始存储。
typedef
typedef为一种类型引入了新的名字。一般情况下,typedef用来简洁地表示指向其它东西的指针。如signal函数
void (*signal(int sig,void(*func)(int)))(int);
signal函数返回一个函数指针,该指针所指向的函数接受一个int参数并返回void。
上面代码等效于下面代码:
typedef void(*ptr_to_func)(int);
/*原本void(*ptr_to_func)(int)表示定义了一个函数指针ptr_to_func,该指针指向的函数接受一个int参数,返回值为void,现在加上typedef后表示给这类型的指针取了一个别名ptr_to_func*/
ptr_to_func signal(int,ptr_to_func);
/*表明signal是一个函数,返回值为ptr_to_func*/
typedef与宏定义的区别体现在两个方面
- 可以用其它类型说明符对宏类型进行扩展,但不能对typedef所定义的类型名进行扩展
#define peach int
unsigned peach i;/*没问题*/
typedef int peach;
unsigned peach i;/*错误,有问题*/
- 在连续的几个变量声明中,用typedef的类型能保证声明的所有变量为同一类型,但宏定义无法保证。
#define int_ptr int *
int_ptr a,b;/*a是指针,b是整型*/
typedef int * int_ptr;
int_ptr a,b;/*a,b都是指针*/
不要为了方便对结构使用typedef。这样做的唯一好处是不必书写“struct”,但这个关键字能提高程序的可读性。typedef应该用于:
- 数组、结构、指针以及函数的组合类型
- 可移植类型(如typedef int int32_t;)
第四章
C语言中的对象必须有且只有一个定义,但可以有多个声明。声明是描述其它地方创建的对象,而定义为对象分配内存。
数组与指针有相似的地方,比如数组名和指针都表示一个地址,但它们并不一样。
访问数组的数据与访问指针的数据的方式是不一样的
- 数组是直接访问数据,a[i]只是简单地以a+i为地址取得数据;
- 指针是间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。如果指针有一个下标[i],就把指针的内容加上i作为地址,从中取得数据。
即比如
char *p = "abcdefg";
char a[] = "abcdefg";
p[3]和a[3]仍然能取得字符‘d’,但两者途径不一样。
定义指针时,编译器不为指针指向的对象分配空间,他只分配指针本身的空间,除非在定义时同时赋给指针一个字符串常量进行初始化。如
char *p = "abc";
只有字符串常量才能这样,其它的如整型都不可以。
int *ptr = 5;/*错误,编译不通过*/
赋给指针一个字符串常量,则该字符串常量并不能通过指针修改它的值;由字符串初始化的数组则可通过数组修改字符串的值。
第五章
大多数编译器并不是一个单一的大程序,而是由几个小程序组成,这些小程序由“编译器驱动器”的程序控制调用。这些小程序有:预处理器、语法和语义检查器、代码生成器、汇编程序、优化器、链接器,调用这些小程序的顺序和上面的顺序一致。
链接器确认main函数为函数入口,把符号引用绑定到内存地址,把所有的目标文件集中在一起,再加上库文件(静态编译),从而产生可执行文件。
使用动态编译,程序在运行时必须能够找到所需要的函数库。链接器通过把库文件名或路径名植入可执行文件中来达到这一点。这也意味着函数库的路径不能随意改变,除非在链接器中进行特殊说明。当在一台机器上编译完程序后,把它拿到另一台机器上可能会运行错误,原因可能就出在函数库的路径上。静态编译已经过时,现在都是默认采用动态编译。
interpositioning就是通过编写与库函数同名的函数来取代该库函数的行为。这是一种危险的行为,应该尽量避免。
比如mktemp()和getwd()函数都是库函数,如果程序员在程序中自己编写了一个mktemp()函数,则调用mktemp()函数时就会调用程序员编写的mktemp()函数,而不是库函数中的mktemp()函数。这看起来似乎没什么问题,调用自己编写的mktemp()函数就是程序员的用意。但假如程序还需要调用库函数getwd(),则程序运行会出错,这是因为getwd()函数需要使用库函数中的mktemp()函数,而现在程序员定义了mktemp()函数,则getwd()函数会调用程序员编写的mktemp()函数而不是库函数的mktemp()函数,从而运行结果出错。
第六章
段
编译程序时,默认情况下,所有程序编译生成的输出文件都是a.out。a.out以段的形式进行组织,如初始化后的全局和静态变量保存到数据段,未初始化的全局和静态变量保存到bss段,程序指令保存到文本段,局部变量并不进入a.out,它们在运行时创建。除了文本段、数据段、bss段,a.out还包括有a.out神奇数字、a.out的其它内容。
运行a.out时,系统取文件中每个段的映射直接放入内存,并为程序开辟堆栈段。文本段、数据段、bss段、堆栈段就成了进程在内存中的地址布局。
栈是一种“后进先出”的结构,对它的经典操作就是往栈顶存放数据或者从栈顶取出数据。编译器设计者采用了较为灵活的方式,既能往栈顶存放数据或者从栈顶取出数据,也可以修改栈中部的数据的值。
堆栈段作用有三点:
- 堆栈为函数内部声明的局部变量提供存储空间
- 进行函数调用时,存储一些与此有关的一些维护信息,如函数调用地址(即所调用的函数结束后跳回的地址)
- 用作暂时存储区
setjmp和longjmp
setjmp和longjmp是C语言所独有的,它们部分弥补了C语言有限的转移能力。
int setjmp(jmp_buf env)
必须首先使用。它表示使用env记录现在的位置。如果是从setjmp直接调用返回,setjmp返回值为0。如果是从longjmp恢复的程序调用环境返回,setjmp返回值由longjmp决定。void longjmp(jmp_buf env, int value)
跳回到env所记录的位置,让它看起来像是从setjmp返回。如果value传递给longjmp零值,setjmp的返回值为1;否则,setjmp的返回值为value。
#include <stdio.h>
#include <setjmp.h>
jmp_buf buf;
void banana(void)
{
printf("banana() \n");
longjmp(buf, 1);
/*以下代码不会被执行*/
printf("never see\n");
}
void main(void)
{
if(setjmp(buf))
printf("back in main");
else
{
printf("first time through\n");
banana();
}
}
setjmp/longjmp的最大的用途是错误处理,只要还没有从函数中返回,一旦发现一个不可恢复的错误,可以把控制转移到主输入循环,并从那里重新开始执行。这与C++中的异常处理机制”catch”和”throw”类似。
使用setjmp/longjmp使得程序难以理解,如果不是处于特殊需求,尽量避免使用。
第七章
虚拟内存
虚拟内存的基本思路是使用廉价但缓慢的磁盘来扩充快速却昂贵的内存。
SunOS中进程执行于32位地址空间,操作系统负责具体细节,使每个进程都以为自己拥有整个地址空间的独家访问权。这是通过虚拟内存实现的。所有进程共享机器的物理内存,当内存用完时就用硬盘保存数据。内存管理硬件MMU把虚拟地址翻译成物理地址,并让一个进程始终运行于系统的真正内存中。
内存泄漏
- 内存损坏:释放或改写仍在使用的内存
- 内存泄漏:未释放不再使用的内存
总线错误
总线错误几乎都是由于未对齐的读或写引起的。之所以称为总线错误,是因为出现未对齐的读或写时,被阻塞的组件就是地址总线。对齐的意思就是数据项只能存储在地址是数据项大小的整数倍的内存位置上。
一个会引起总线错误的小程序如下:
union
{
char a[10];
int i;
}u;
int *p = (int *)&(u.a[1]);
*p = 17;/*p中未对齐的地址引起总线错误*/
段错误
段错误是由于内存管理单元的异常所致,该异常通常是由于解引用一个未初始化或非法值的指针所致。比如指针引用一个并不位于你的地址空间的地址,就会引起段错误。比如下面这个程序:
int *p = 0;
*p = 17;/*解引用一个非法值的指针,因为这个指针指向的地址不属于程序的地址空间*/
通常导致段错误的几个直接原因:
- 解引用一个包含非法值的指针
- 解引用一个空指针
- 指针还没有初始化就对其进行解引用
- 使用free函数对指针进行释放后再访问它的内容
- 改写错误:如越过数组边界写入数据
- 指针释放引起的错误:释放同一块内存块两次,释放一块未曾使用malloc分配的内存块,释放仍在使用的内存,释放一个无效的指针
- 在未得到正确的权限时进行访问,如往只读的文本段写入数据
- 用完了堆栈或堆空间
在遍历链表时正确释放元素的方式是使用临时变量存储下一个元素的地址。