《C和指针》 -- Kenneth A.Reek著,徐波译
相关笔记:《The C Programming Language》阅读笔记
《Pointers On C,C和指针》一书是Kenneth A. Reek所著。我花了一两天时间将此本书通看了一遍,观后感为:《C和指针》是一本既系统的叙述了C语言基础知识又深入的讲解了C语言具有深度(或叫进阶,以我的水平为参考线)的知识。如果好好的读此本书,可以达到浅(基础)深(进阶内容)双收的效果。还记得大一上C语言课程时,课本中就基本上只介绍了《C和指针》中的部分内容,并未进一步的讲C更深入的知识(当然,学习C不得依靠教材)。
快速地浏览了一遍《C和指针》一书,觉得书中每个部分都写的挺好的,如“递归调用的传参过程”、“变量存储类型”、“左右值”、“二维数组”、“指针”等部分,所以将部分内容摘录下来形成笔记,相信此书中还有大量的精华甚至是核心内容并未被我发觉,有缘会再读此本书。此篇笔记可以和之前阅读《C语言深度剖析》的笔记(每个知识点以独立的笔记呈现了)形成照应关系,与此笔记格式相同的是阅读C圣经《The C Programming Language》阅读笔记。
《Pointers On C,C和指针》by Kenneth A. Reek
1. C程序的翻译和执行
翻译:将组成一个程序的每个源文件通过编译过程(…)分别转换为目标代码(object code)。然后,各个目标文件由链接器(linker)捆绑在一起,形成一个可执行程序。链接器同时会引入标准C函数库中任何被该程序所用到的函数,而且它也可以搜索程序员的个人程序库,将其中需要使用的函数也链接到程序中。编译过程也由几个阶段组成,首先是预处理,然后是源代码解析,然后产生目标代码,如果在编译程序的命令或设置中包含了优化代码的选项则会对目标代码进行优化。
执行:程序的执行过程也需要经历几个阶段。首先,程序必须载入到内存中,在具有操作系统的宿主环境中,这个过程由操作系统完成。那些不是堆栈中的尚未初始化的变量将在这个时候得到初始值。然后,程序的执行便开始。在宿主环境中,通常一个小型的启动程序与程序链接在一起。它负责处理一些日常事务,如收集命令行参数传递给程序以让程序可以访问到它们。接着,便调用main函数。现在,便开始执行代码。在绝大多数的机器里,程序将使用一个运行时的堆栈,用于保存函数的局部变量和返回地址(每次调用到子函数时,这些变量的堆栈内存地址很有可能不同)。程序同时也可以使用静态内存,存储在静态内存中的变量在程序整个执行过程中的值将会被一直保存。程序执行的最后一个阶段就是程序的终止,它可以由许多不同的原因引起,“正常”的终止就是从main函数返回。有些执行环境允许程序返回个代码,提示程序为什么停止运行。在宿主环境中,程序将再次获取得控制权,并可能执行各种不同的日常任务。除此之外,程序也可能由于用户按下break键或者电话的挂起而终止,另外也可能使由于在执行过程中出现错误而自行中断。
2. 数据类型牵涉的程序移植性
尽管设计char类型变量的目的是为了让它们容纳字符型值,但字符在本质上是小整型值。缺省的char要么是signed char(-127 ~ 127),要么是unsigned char(0 ~ 255),如果在程序中声明一个char类型变量,在使用char类型变量时,只有其值在signed char与unsigned char范围交集时程序才是可移植的。所以在声明变量时,变量的具体类型(signed or unsigned)要声明清楚。
3. 整型常量
八、十、十六进制的整型常量如20可能是int, long, 或unsigned long。在缺省情况下(20,非20L等),它是最短类型但能够完整容纳这个值得类型。int类型变量所占内存字节不得少于short int,long类型不得少于int。没有规定“sizeof(long) > sizeof(int) > sizeof(short)”,视编译器而定,读到这里忽然觉得编译器至高无上的地位:一方面根据C语言的特性来编写编译器,另一方面自定义的编译器的一些特性也限制着C语言程序的编写。
4.存储类型
变量的存储类型是指存变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁及它的值将保持多久。有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
变量的缺省类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态变量。无法为这类变量指定其它的存储类型(这句话很有道理呀,哈哈)。静态变量在程序运行之前(结合前面的摘录,看来程序运行是指main函数的开始运行)创建,在程序的整个执行期间始终存在。疑问:都说编译器为未初始化的静态变量自动赋值,程序载入内存后且未调用程序main函数之前这个所谓的编译器是如何为静态变量赋值的?编译器在哪里?
在代码块内部声明的变量的缺省存储类型是自动的,也就是说存储于堆栈中,称为自动变量。当程序执行到声明自动变量的代码块时,自动变量才被创建,当程序离开该代码块时,这些自动变量自动销毁。如果该代码块被数次执行,例如一个函数被反复调用,这些自动变量每次都将重新创建。在代码块再次执行时,这些自动变量在堆栈中所占据的内存位置有可能和原来的位置相同也有可能不同。即使他们所占的位置相同,也不能保证这块内存没有被用作过其它的用途或即将用作其它的用途,故而自动变量在使用完后便自动消失是合理的。如此推理,静态变量的地址在程序真个运行过程中都是不变的。
5. 初始化
自动变量和静态变量的初始化存在一个重要的差别。在静态变量的初始化中,我们可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置(显示初始化:赋值),当可执行文件载入到内存时,这个已经保存了正确初始值的位置将赋值给对应的静态变量(连显式初始化静态变量都是在程序执行前,如全局变量 int i = 2;i被保存在静态内存的某个区域,常量2也被保存在静态内存的某个区域中 )。如果不显式的初始化静态变量,则静态变量的值将会是0。
6. 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作无法进行。
int a = 5000;
int b = 25;
long c = a* b; //应该是long c = (long)a * b,不然在如16位机之上a*b就会超过int范围从而产生溢出,得不到想要的值。
7. 指针中的地址值
指针变量被分配4个字节的内存,故而指针可以容纳的无符号范围是 0 ~ 2^32 -1,;可以容纳的有符号范围是-2^31 ~ -2^31 -1。一个地址有4个字节表示,至于被指对象的地址是最左边那个字节表示还是由最右边那个字节表示,不同的机器有给不同的规定。不过这个都是由系统完成的,我们不必太关心,因为我们只是使用指针。
8. NULL指针
标准定义了NULL指针,它作为特殊的指针变量,表示不指向任何东西。之所以选择0这个值是因为一种源代码约定,就机器而言,NULL指针的实际值可能与此不同,在这种情况下,编译器将负责0值和内部值之间的翻译转换。NULL指针十分有用,因为他给了程序员一种方法,表示某个特定的指针目前并未指向任何东西(这就是咱目前需要知道的)。
9. 指针常量
非*100 = 25而是*(int *)100 = 25。在某些机器上,与设备控制器的通信是通过某个特定的内存地址读取和写入值来实现的,此时就可以用指针常量来访问地址。
10. 变量名的左右值
有的变量既可以当左值(赋值符号左边)也可以当右值(赋值符号右边)。
对于一般的变量如char ch = ‘a’;变量与内存之间的关联由编译器完成。左值标识了一块内存,ch作为左值被赋值时,表示与ch相关联的内存的内容将要为多少,它标识存储‘a’(变量作为右值时的值)的那块内存。当ch位于赋值符号右边时ch代表(地址内的容)’a’。指针的左右值还可能联合”*”、”++”等符号,此时需要明确各符号的优先级及几何形然后合理的判断当指针作为左值或右值的含义。《C与指针》中讲得很明白。
11. 指针运算
通常指针的运算发生在同一块内存即同时即指向一个数据结构(如同一个数组)中时的运算才算有意义。不然两个指针的运算操作很可能是没有意义的。
12. 值被引用
值的类型并不是值的内在本质,而是取决于它被使用的方式。如果编译器认定函数返回一个整型值,它将产生整数指令去操纵这个值,如果这个值是非整型值,如是一个浮点数,则结果通常是不正确的。
在未看到函数的原型之前,编译器便认定这个函数返回一个整型值。如此而来,若未看到函数的原型加上函数返回一个非整型如浮点(3.14)时,返回值就会被程序弄错误。
13. 用C实现类封装
可以用C语言封装一个模块,指定类中的私有成员不被除接口之外的函数访问。这个过程通过static关键字来实现:
- 将这个模块单独编写在一个文件file1中,将此文件中的数据声明为static不让其它的文件访问。
- 将函数定义在file1中,并不将这些函数声明为非static以让其它的文件可以访问到接口。
14. 递归调用过程
《C和指针》将函数递归调用过程用图示表示得很清楚,这些图描述了函数递归调用过程中的传参过程,这能成为理解递归调用的关键点。我以前的关于递归分析的过程倒是显得不清楚和笨了许多。
Figure1:《C和指针》图解递归参数传递过程
15. 一维数组名
数组名是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型,如果数组是int型的,那么数组名就是指向int的常量指针;如果数组是其它类型的则数组名就是指向其它类型的常量指针。(《C语言深度剖析》中提到,“&数组名”表示整个数组的首地址)吗,sizeof(数组名)和sizeof(&数组名)的值都是整个数组占有的内存字节数,这个时候想起陈正冲前辈在《C语言深度剖析》中说到的sizeof(数组名)应该等于4的结论。说这是编译器的一个BUG。
16. 指针的效率
除了只需要复制4字节内存外,其它的还有等鄙人功力提升后再笔记。
17. 堆中的字符串和在常量区的字符串
18. 多维数组
int c[6][10];
将c看作是一个包含6个元素的向量,只不过它的每个元素本身是一个包含10个整型元素的向量。用这个层次理解多维数组对通过多维数组用指针形式访问元素及理解多维数组名有极大的好处。《C与指针》将这个也讲得很好呀。想想那个过程。c指向第一个元素(第一个元素包含10个元素即为一维数组,故而c是一个指向数组的指针,则c实际就成为了一个二级指针)相当于一维数组中的“&数组名”,*c表示的是第一个元素的首地址即10个元素的第一个元素的地址,*c + 2就表示第一行的第三个元素的地址了,*(*c + 2)就就取其内容了。c + 1就成了第二行的地址了,*(c + 1)是第二行的10个元素第一个元素的首地址……..依此类推。(c +1就相当于c + sizeof(&数组名[10]),其中数组名表示一维的 ),可以参看一下《C语言深度剖析》里的说法。19. 数组指针维数不能为空
int (*p)[] = matrix; //matrix为二维数组
p是一个指向整型数组的指针,但数组的长度不见了会导致指针执行运算时得不到预想的值。20. 指针数组和静态数组存储字符串
两个区别:
- 指针数组所指向的每一个字符串独自存储在常量区,而数组存储的字符串就存储在数组名代表的那块内存中。
- 所以,指针数组元素指向的内存长度刚好是字符串的长度。而数组开辟的内存是下标那么大,哈哈。
20. 运算
21. 字符串操作函数
22. 结构体内存对齐
23. 位段。
24. 动态内存分配
25. #define的泛型编程
26. 流 缓冲
绝大多数流是完全缓冲的,这意味着“读取”和“写入”实际上是从一块被成为缓冲区的内存区域来回赋值数据。所以,当缓冲区的数据未满时,数据是不会被输出的。在缓冲区未满情况下输出数据需要使用fflush(stream)函数。
缺省下,标准输入流被定义为键盘,标准输出流被定义为屏幕或终端。