《C专家编程》学习笔记
第一篇没有找到原文,所以不能附上出处,还望谅解。
http://blog.csdn.net/pengbojiutian/article/details/6569868
?
第一章:
一:尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性,尤其是不要仅仅因为无符号数不存在负值(如年龄,国债)而用它来表示数量。只有在使用位段和二进制掩码时,才可以使用无符号数,应该在表达式中使用强制类型转换,使操作数均为有符号数或无符号数。这样就不必由编译器来选择结果的类型。
二:当执行算术运算时:操作数的类型如果不同,就会发生转换,数据类型一般朝着浮点精度更高,长度更长的方向转变。整型数如果转换为signed 不会丢失信息,就转换为signed,否则转换为unsigned。
一个L 的NUL 用于结束一个ASCALL字符串。ASCALL 字符中零的位模式被称为NUL。
两个L的NULL 用于表示什么也不指向,(空指针)
?
第二章:这不是BUG,而是语言特性
2.1 多做之过:
这些特性包括容易出错的switch 语句,相邻字符串常量的自动连接和缺省全局范围。C 语言中,几乎从来不进行运行时错误检查---对进行解除引用操作的指针有效性检查大概是唯一的例外。无效的指针可能成为程序员的噩梦,人们很容易用一个无效的指针来引用内存。在所有的虚拟内存体系结构里,一旦一个指针进行解除引用操作时,所引用的内存超出内存地址的虚拟地址空间,操作系统就会结束这个进程。但是MS—DOS 并不支持虚拟内存,即使内存访问失败,它也无法立即捕获这种情况。
C 语言的理念,程序员应该知道自己正在干什么,而且保证自己的所做所为是正确的。各个case 和default 的顺序是可以任意的,但习惯总是把default 放在最后。switch 存在的一些问题是,其中之一是它对case 可能出现的值太过放纵了,例如可以在switch 的做好括号后声明一些变量,从而进行一些局部存储的分配,在最初的编译器中,这是一个技巧--绝大多数用于处理复杂复合语句的代码都可以被复用。switch 语句缺省采用”fall through“,在97%的情况下都是错误的。break 语句事实上跳出的是最近的那层循环语句或switch 语句。
字符串常量的自动合并意味着字符串数组在初始化时,如果不小心漏掉了一个逗号,编译器不会发出错误信息,而是悄无声息的合并在一起。在最后一个字符串末尾的逗号并不是打错字,而是从早期的C 语法中继承下的东西,不管存在是否有意义,ANSIC对它的解释是是C语言自动生成容易些。太多的缺省可见性定义C函数时,在缺省情况下名字是全局可见的,可以再名字前面加一个冗余的extern 关键字,也可以不加,效果一样的。如果想限制这个函数的访问,就必须加个static 关键字。
2.2 误做之过:
C 语言中属于“误做之过”的特性,就是语言中有误导性质或是不适当的特性,这些特性有些跟C 语言的简介有关,有些则更操作符的优先级有关。
C 语言存在的一个问题就是它太简洁了,仅增加,修改或删除一个字符就会使程序成另外一个仍然有效却全然不同的程序。更糟糕的是,许多符号是被”重载的“在不同的上下文环境有不同的意思。当sizeof 的操作数是个类型名时,两边必须加上括号(这常常使人认为他是一个函数),但操作数如果是一个变量则不必加括号。你让一个符号所表达的意思越多,编译器就越难检测到这个符号在你的使用中所存在的异常情况。
2.3 少做之过
属于少做之过的特性就是语言应该提供但未提供的特性。
C 语言有最大一口策略,这种策略表示如果下个标记有超过一种的解释方案,编译器将选取最长的字符序列方案。
?
第三章声明
参数按照从右到左的次序压倒堆栈中,这种说法过于简单了,参数在传递时首先尽可能地存放到寄存器中(追求速度)。
一个int 型变量跟只包含一个Int 型成员的结构变量S 在参数传递时可能完全不同,一个int型参数一般会被传递到寄存器中,而结构变量s 在参数则很可能被传递到堆栈中。
结构体:
在结构中放置数组,如struct s_tag{int a[100];};现在可以把数组当做第一等级的类型,用赋值语句拷贝整个数组,一传值的方式传递到函数,或者把它作为函数的返回类型。在典型的
情况下并不需要频繁的对整个数组进行赋值操作。但是如果需要这样做,可以通过放入结构中实现。
C 语言声明的优先级
A 声明从它的名字开始读取,然后按照优先级一次读取。
B 优先级从高到低一次是:
1 声明中被括号括起来的
2 后缀符号()[]
3 前缀符号*
不要在一个typedef 中放入几个声明器,千万不要把typedef 嵌到声明的中间部分。
不要为了方便起见对结构使用typedef,这样做唯一的好处是能使你不必书写struct 关键字,但这个关键字可以向你提示一些信息,你不应该把它省掉。
typedef int x[10],#define x int[10]的区别
正确思考这个问题的方法是把typedef 看成是一种彻底的“封装”类型--在声明它之后不能再往里面增加别的东西。它和宏的区别体现在两个方面。
首先,可以用其他类型说明符对宏类型进行扩展,但对typedef 所定义的类型名却不能这样做。如下所示:
#define peach int
unsigned peach i;/没问题
typedef int peach
unsigned peach i;//错误非法。
其次:
在连续几个变量的声明中,用typedef 定义的类型能够保证声明中所有的变量均为同一种类型,而用#define 定义的类型则无法保证。
extern 对象声明告诉编译器对戏那个的类型和名字,对象的内存分配在别处进行。由于并未
在声明中为数组分配内存,所以并不需要提供关于数组长度的信息。
并且extern int * i;
extern int i[];
是不一样的。
第四章数组和指针
出现在赋值符号左边的符号有时被称为左值(由于它位于“左手边”或“表示地点”),出现在
赋值符号右边的符号有时则被称为右值,编译器为每个变量分配一个地址(左值),这个地
址在编译时可知,而且该变量在运行时一直保存于这个地址,相反存储于该变量中的值只有
在运行时才可知。如果需要用到变量中存储的值,编译器发出指令从指定地址读入变量的值,
并将它存于寄存器。
char *p="abcdef";
char p[]="abcdef';
前者编译器告知p 是一个纸箱字符的指针(相反数组的定义告诉编译器p 是一个字符序列)
p[i]表示"从p 所指向的地址开始,前进一步,每步都是一个字符",既然把p 声明为指针,
那么不管p 原先是定义为指针还是数组,都会按照上面所示的三个步骤进行操作;。
前者间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。如果
指针有一个小标[1]就把指针的内容更加上一作为地址,从中去数据
后者直接访问数据,a[1]指示简单地一a+1 为地址取数据。
第五章;链接器
收集模块准备执行的三个阶段的规范名称是连接-编辑, 载入, 动态链接。
如果函数库的一份拷贝时可执行文件的物理组成部分,那么我们称之为静态链接;如果可执
行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的库函数,那么我们称之
为动态链接。静态链接的模块被连接编辑并载入运行,动态链接的模块被连接编辑后载入,
并在运行时进行连接以便运行。程序执行时,在main()函数被调用前,运行时载入器把共享
的数据对象载然入到进程的地址空间。外部函数被真正调用之前,运行时载入器不解析它们,
所以即使链接了函数库,如果没有实际调用,也不会带来而外开销。即使是在静态链接中,
整个文件也没有被全部载如到可执行文件中,所装入的只是所需要的函数。动态链接时是一
种动态链接,这意味着程序在运行时必须能够找到它们所需要的库函数,连接器吧库文件名
或路径名植入可执行文件做到这一点,这意味着,库函数的路径不能够随意移动,
动态链接可以从两个方面提高性能:
一:动态链接可执行文件比功能相同的链接的体积小。
二:所有动态库连接到某个特定的函数库的可执行文件在运行时共享该函数库的一个单独拷
贝。操作系统内核保证映射到内存中的库函数可以被所有使用它的进程共享。这就提供了更
好的I/O 和交换空间利用率,节省了物理内存,提高了系统的整体性能。
静态库被称为archive,它们通过ar(用于archive 的实用工具)来创建和更新。
动态链接库由链接编辑器ld 创建根据约定,动态库的文件扩展名为“.so”,表示“shared object
共享对象”。
涉及到UNIX 的部分没有看***********************
第六章:运行时数据结构
a.out--汇编程序和链接编辑输出格式它不是胡编程序输出而是连接器输出。
编译器设计者通过不存储为使用的信息来提高速度,其他的优化措施包括把信息保存在寄存
器而不是堆栈中,尽管我们谈到了将过程活动记录压到堆栈中,但是过程活动记录并不一定
要存在于堆栈中。事实上,尽可能地把过程活动记录的内容更放到寄存器中会使函数调用的
速度更快,效果更好。
第七章对内存的思考
就像堆栈能够根据需要自动增长一样,数据段也包含了一个对象,用于完成这项工作,这就
是堆,它用于动态分配的存储。用malloc 函数,callvoc 函数与malloc 函数类似,但它在返
回指针之前先把分配好的内存的内容清零,realloc 函数把内存拷贝到别的地方然后将指向新
地址的指针返回给你。这在动态增长的表很有用。alloca()分配在栈上的动态内存,它分配的
内存会自动释放。这并不适合那些比创建它们的函数生命周期更长的结构。
数据对齐的意思是数据项只能存储在地址是数据项大小的整数倍的内存位置上。
编译器通过自动分配和填充数据(在内存中)来对齐。当然在磁盘或磁带上并没有这样的对
齐要求,所以程序员对它们可以很愉快地不必关心数据对齐,但是当他们把一个char 指针
转换为Int 指针时,就会出现神秘的总线错误。
段错误或段违规是由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,该异常则通
常是由于解除引用一个未初始化或非法值的指针引起的。如果指针引用一个并不位于你的地
址空间的地址,操作系统便会进行干涉。如int * p=0; *p=17;
一个微妙之处是,导致指针具有非法的值通常不是由于编程错误引起的,和总线错误不同,
段错误更像是一个间接的症状而不是错误的原因。一个更糟糕的微妙之处是,如果未初始化
的指针恰好具有未对其的值(对于指针所要访问的数据而言),它将会产生总线错误,而不
是段错误。
通常导致段错误的几个直接原因:
解除引用一个包含非法值的指针。
解除引用一个空指针
在未得到正确的权限时进行访问。
用完了堆栈或堆空间
下面的说法过于简单,但在绝大多数框架的绝大多数情况下,总线错误意味着Cpu 对进程
引用内存的一些做法不满,而段错误则是MMU 对进程引用内存的一席情况发出抱怨。
第八章为什么程序员无法分清楚万圣节和圣诞节
ANSI C 函数原型的目的是使C 语言成为一种更加可靠的语言。建立原型就是为了消除一种
普通(但很难发现)的错误,就是形参和实参类型不匹配。ANSI C 的函数原型就是才用一
种新的函数声明形式,把参数的类型也包含在声明之中。函数的定义也作为相应的改变以匹
配声明。在ANSI C 中如果使用了新风格的函数定义,编译器就不会假定参数是准确声明的,
于是便不进行类型提升,并据此产生代码。不要在函数的声明和定义中混用新旧两种风格。
复杂的类型转换可以按下面的3 个步骤编写
(1)一个对象的声明,它的类型就是想要转换的结果类型
(2)删除标示符(以及任何如extern 之类的存储限定符),并把剩余的内容放在一对括号里。
(3)把第二步产生的内容放在需要进行类型转换的对象的左边。
第九章;再论数组
对编译器而言,一个数组就是一个地址,一个指针就是一个地址的地址。
什么时候数组和指针时相同的
规则一: 表达式中的数组名(与声明不同)被编译为一个指向该数组的第一个元素的指针
规则二: 下标总是与指针的偏移量相同
规则三: 在函数参数的声明中,数组名被编译成当做指向该数组的第一个元素的指针。
规则一个规则二合起来理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地
址的指针加偏移量”。你只要记住在表达式中,指针和数组时可以互换的,因为它们在编译
器里最终形式都是指针,并且都可以进行取下标操作,就像加法一样,取下标操作的操作数
是可以交换的这就是为什么a[6]=6[a]都是正确的。
在处理一维数组时,指针并不见得比数组块,C 语言吧数组下标改写为指针偏移量的根本原
因是指针和偏移量是底层硬件所使用的基本模型。
之所以要把传递给函数的数组参数转换为指针时出于效率的考虑,要拷贝整个数组的效率时
太低。
不管程序员实际缩写的是哪种形式,函数并自动知道指针所指向的数组的范围,所以要有个
约定,如数组一NUL 结尾或另一个附加参数表示结束。注意有一样操作只能在指针里进行
二无法再数组中进行,那就是修改它的值。数组名是不可以修改的左值,它的值不能改变的。
多维数组:
定义和引用多维数组唯一的方法就是使用数组的数组。尽管C 语言把数组的数组当作多维
数组,在思维模式中,吧数组当做是一种向量(即某种对象的一维数组,它的元素可以使另
一个数组),就能极大的简化编程语言中这个复杂的领域。
在C 语言的多维数组,最右边的下标是最先变换的,这个约定被称为“行主序”。C语言中,
多维数组的最大用途就是存储多个字符串。
第十章再论数组
数组名被改写成一个指针参数规则并不是递归定义的。数组的数组会被改写为“数组的指针”
而不是指针的指针。
实参所匹配的形式参数
数组的数组char c[8][10]; char(*)[10] 数组的指针
指针数组char* c[15] char **c 指针的指针
数组指针char(*c)[64] char(*)[64] 不改变
指针的指针char **C char **C 不改变
二维数组要复杂一些,数组被改写为指向数组第一行的指针。现在需要两个约定,其中一个
用于提示每行的结束,另一个用于提示所有行的结束。提示单行的结束可以使用一维数组的
两种方法,增加一个额外的参数表示大小,赋予数组最后一个元素一个特殊的值,这个特殊
的值不会作为数组元素出现。提示所有行结束也可以这样。
使用指针向函数传递一个多维数组
C语言没有办法表达“这个数组的边界在不同的调用中可以变化”,我们能够采用的最后的方
法是把array[x][y]这样的形式改写为一个一维数组array[x+1],它的元素类型是指向array[y]
的指针。这样就改变了问题的性质,而改变后的问题是我们已经解决了的。在数组最后的那
个元素array[x+1]里存储一个null 指针,提示数组的结束。
二维或更多维的数组无法再C 语言中用作一般形式的参数。你无法向函数传递一个普通的
多维数组。可以系那个函数传递预先确定长度的特殊数组。
方法一f(int array[10][20])
尽管这是最简单的方法,但同时也是作用最小的,因为它迫使函数只处理10 行20 列的int
型数组,注意多维数组最主要的一维的长度不必显示写明,所有函数都必须知道数组其他维
的确切长度和数组的基地址。
方法二f(int array[][20])
方法三我们可以采用的这种方法是放弃二维数组,把它的结构改为以个Hiffe 向量。也就
是说,创建一个一维数组,数组中的元素是指向其他东西的指针。注意:::只有把二维数组
改为以指向向量的指针数组的前提下才可以这样做。Hiffe 向量的这种数据结构美感在于,
它允许任意的字符串指针数组传递给函数,但必须是指针数组,而且必须是指向字符串的指
针数组。
不要在一行代码里实现太多的功能。
?
?
第一章《C:穿越时空的迷雾》
C诡异离奇,缺陷重重,缺获得了巨大的成功。----Dennis Ritchie
1,C语言的基本数据类型直接与底层硬件相对应,不容许嵌套函数。
2,在C语言中,绝大多数库函数或辅助程序都需要显式调用。如:必要时,程序员必须管理动态内存的使用,创建各种大小的数组,测试数组边界,并自己进行范围检测。
3,预处理器大约在1972年被加入。它所实现的主要功能,字符串替换,头文件包含,通用代码模块的扩展。(宏定义中,注意空格)
4,对于宏这样的预处理器,只应适量使用,C++在这方面引入了一些新的方法,使得预处理器几乎无用武之地。
5,宏最好只用于命名常量,并为一些适当的结构提供简洁的记法,宏名应该大些,这样便于和函数调用区分开。
6,const并不能把变量变成常量,只能表示该符号不能被赋值,const最有用之处在于用它来限定函数的形参,这样该函数将不会修改实参指针所指的数据,但其他的函数却可以修改它。
7,const int *limitp = &limit;limitp这个指针不能用于修改这个整型数,但本身的值却可以变。
第二章《这不是Bug,而是语言特性》
Bug是迄今为止地球上最庞大最成功的实体类型,有近百万种已知的品种。在这个方面,它比其他任何已知的生物种类的总和还要多,而且至少多出4倍。----摘自Snope教授的Encyclopedia of Animal Life
1,const不是真正的常量,switch case语句要加入break。
2,break事实上跳出的是最近的那层循环语句或switch语句。
3,相邻的字符串常量将被自动合并成一个字符串,换行无须’/‘符号。
4,定义C函数时,在缺省情况下函数的名字是全局可见的。可以再函数的名字前加个冗余的extern关键字,也可以不加。如果想限制对这个函数的访问,必须加个static关键字。
5,C语言关键字重载引发的问题。如:static在函数内部,表示该变量的值在各个调用间一直保持延续性,而static在函数这一级,表示该函数只对本文件可见。extern用于函数定义,表示全局可见(属于冗余的),而extern用于变量,表示它在其它地方定义。void作为函数的返回类型,表示不返回任何值,而在指针声明中,表示通用指针的类型,而位于参数列表中,表示没有参数。类似的,还有*,&,=,==,()等。
6,在C语言中,自动变量在堆栈中分配内存。当包含自动变量的函数或代码块退出时,它们所占的内存便被回收,它们的内容肯定会被下一个所调用的函数覆盖。
第三章《分析C语言的声明》
1,C语言的声明比较复杂,如:const int *grape;int const *grape;表示指针指向的对象是只读的;而int *const grape_ptr则表示指针是只读的。
2,理解C语言声明的优先级规则
? ? A??声明从它的名字开始读取,然后按照优先级顺序依次读取;
? ? B??优先级从高到低依次是:
? ?? ???B1??声明中被括号括起来的那部分
? ?? ???B2??后缀操作符:
? ?? ?? ?? ?括号()表示这是一个函数,而方括号[]表示这是一个数组。
? ?? ???B3??前缀操作符:星号*表示 “指向……的指针”
? ? C??如果const和(或)volatile关键字的后面紧跟类型说明符(如int,long等),那么它作? ?? ? 用于子类型说明符,在其他情况下,const和(或)volatile关键字关键字作用于它左边? ?? ? 紧邻的指针星号。
? ? 例如:char * const *(*next)();这个声明表示“next十一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为char的常量指针”。
3,typedef是一种有趣的声明形式:它为一种类型引入新的名字,而不是为变量分配空间。
4,typedef 与#define宏文本定义之间存在一个关键性的区别。正确思考这个问题的方法就是把typedef看成是一种彻底的“封装”类型——在声明它之后不能再往里面增加别的东西。首先,可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型名却不能这样做。其次,在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证。
第四章《令人震惊的事实:数组和指针并不相同》
1,声明和定义的区别,声明相当于普通的声明:它所说明的并非自身,而是描述其他地方的创建的对象。定义相当于特殊的声明:它为对象分配内存。
2,extern char a[]与extern char a[100]等价,这俩个声明都提示a是个数组,也就是一个内存地址,数组内的字符可以从这个地址找到。编译器并不需要知道数组总共有多长,因为它只产生偏离起始地址的偏移地址。相反,如果声明extern char *p;它将告诉编译器p是一个指针,它指向的对象是一个字符。
3,文件1:int mango[100];文件2 extern int *mango;编译器已被告知p是一个指向字符的指针(相反,数组定义告诉编译器p是一个字符序列)。
4,数组和指针都可以在它们的定义中用字符串常量进行初始化。尽管看上去一样,底层机制却不相同。定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非在定义时同时赋给指针一个字符串常量进行初始化。注意只有对字符串常量才是如此。不能指望为浮点数之类的常量分配空间。在ANSI C中,初始化指针时创建的字符串常量被定义为只读。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。在有些编译器中,字符串常量被存放在只允许读取的文本段中,以防止它被修改。数组也可以用字符串常量进行初始化:char a[] = “gooseberry”;与指针相反,由字符串常量初始化的数组是可以修改的。其中单个字符在以后可以改变。
5,专业C程序员必须熟练掌握malloc()函数,并且学会用指针操作匿名内存。
?
第五章《对链接的思考》
1,编译器创建一个输出文件,这个文件包含了可重定位的对象。这些对象就是与源程序对应的数据和机器指令。
2,绝大多数编译器并不是一个单一的庞大程序。它们通常由多达六七个稍小的程序组成,这些程序由一个叫做“编译器驱动器(compiler driver)“的控制程序来调用。这些可以方便地从编译器中分离出来的单独程序包括:预处理器(preprocessor),语法和语义检查器(syntactic and?semantic checker),代码生成器(code generator),汇编程序(assembler),优化器(optimizer),链接器(linker),当然还包括一个调用所有这些程序并向各个程序传递正确选项的驱动器程序(driver program)。
3,如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;如果可执行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么我们称之为动态链接。
4,收集模块准备执行的三个阶段的规范名称是链接-编辑(link-editing),载入(loading)和运行时链接(runtime linking)。动态链接的模块被链接编辑后载入,并在运行时进行链接以便运行。程序执行时,在main()函数被调用前,运行时载入器把共享的数据对象载入到进程的地址空间。外部函数被真正调用之前,运行时载入器并不解析它们。所以即使链接了函数库,如果没有实际调用,也不会带来额外开销。即使是在静态链接中,整个libc.a文件也没有全部装入到可执行文件中,所装入的只是所需要的函数。
5,动态链接可执行文件体积小。运行速度稍慢,但更加有效利用磁盘空间,而且链接-编辑阶段的时间也会缩短(因为链接器的有些工作被推迟到载入时)。
6,动态链接可执行文件比功能相同的静态链接可执行文件的体积小。它能够节省磁盘空间和虚拟内存,因为函数库只有在需要时才被映射到进程中。以前,避免把函数库的拷贝绑定到每个可执行文件的惟一方法就是把服务器置于内核而不是函数库中,这就带来了可怕的”内核膨胀“问题。
7,所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝。操作系统内核保证映射到内存中的函数库可以被所有使用它们的进程共享。这就提供了更好的I/O和交换空间利用率,节省了物理内存,从而提高了系统的整体性能。
8,动态库文件的扩展名是”.so“,而静态库文件的扩展名是”.a“,通过-lthread选项,告诉编译链接连接到libthread.so(”lib“替换为”l“)。编译器期望在确定的目录找到库。(-Lpathname和-Rpathname)指定目录。
9,头文件的名字通常并不与它对应的函数库名相似。函数库所包含的某个函数的原型可能与其头文件中所声明的函数的原型一样。
第六章《运动的诗章:运行时数据结构》
1,代码和数据的区别也可以认为是编译时和运行时的分界线。编译器的绝大部分工作都跟翻译代码有关;必要的数据存储管理的绝大部分都在运行时进行。
2,学习运行时系统,有助于优化代码,获得最佳的效率。有助于理解更高级的材料。当陷入麻烦时,它可以使分析问题更加容易。
3,a.out及其传说,它是assembler output(汇编程序输出)的缩写形式。
4,目标文件和可执行文件可以有几种不同的格式。在绝大多数SVr4实现中都采用了一种称作ELF(原意为”Externsible Linker Format,可扩展链接器格式“、现在代表”Executable and Linking Format,可执行文件和链接格式“)的格式。在BSD UNIX中,a.out文件具有a.out格式。可以通过键入man a.out在主文档中查看更多有关UNIX系统所使用的格式的信息。
5,所有这些不同格式具有一个共同的概念,那就是段(segments)。就目标文件而言,它们是二进制文件中简单的区域,里面保存了和某种特定类型(如符号表条目)相关的所有信息。术语section也被广泛使用,section是ELF文件中的最小组织单位。一个段一般包含几个section。
6,UNIX中,段表示一个二进制文件相关的内容块。当在一个可执行文件中运行size命令时,它会告诉你这个文件中的三个段(文本段,数据段和bss段)的大小。BSS段这个名字是”Block Started by Symbol(有符号开始的块)“的缩写,BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BBS段的大小记录在目标文件中,但BBS段(不像其他段)并不占据目标文件的任何空间。
7,a.out要以段的形式组织。段可以方便地映射到链接器在运行时可以直接载入的对象中!载入器只是取文件中每个段的映像,并直接将它们放入内存中。从本质上说,段是正在执行的程序中一块内存区域,每个区域都有特定的目的。
8,文本段包含程序的指令。链接器把指令直接从文件拷贝到内存中(一般使用mmap()系统调用),以后便再也不用管它。
9,数据段包含经过初始化的全局和静态变量以及它们的值。BSS段的大小可以从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段之后。当这个内存区进入程序的地址空间后全部清零。包括数据段和BSS段的整个区段此时通常统称为数据区。这是因为在操作系统的内存管理术语中,段就是一片连续的虚拟地址,所以相邻的端被接合。一般情况下,在任何进程中数据段是最大的段。
10,堆栈段(stack segment)用于保存局部变量,临时数据,传送到函数的参数等。堆(heap)空间,用于动态分配的内存。只要调用malloc()函数,就可以根据需要在堆上分配内存。
11,虚拟地址空间的最低部分未被映射。也就是说,它位于进程的地址空间内,但并未赋予物理地址,所以任何对它的引用都是非法的。在典型情况下,它是从地址零开始的几K字节。它用于捕捉使用空指针和小整型值的指针引用内存的情况。
12,运行时数据结构有:堆栈,活动记录(activation record),数据,堆等。
13,堆栈段包含一种单一的数据结构--堆栈。堆栈是一块动态内存区域,后进先出。编译器设计者采用了一种稍微灵活一些的方法,我们从顶部增加或拿掉盘子,但我们也可以修改位于堆栈中部的盘子的值。函数可以通过参数或全局指针访问它所调用的函数的局部变量。运行时系统维护一个指针(常位于寄存器中),通常称为sp。用于提示堆栈当前的顶部位置。
14,堆栈段的三个主要用途,其中俩个跟函数有关,另一个跟表达式计算有关。堆栈为函数内部声明的局部变量提供存储空间,进行函数调用时,堆栈存储与此有关的一些维护信息。这些信息被称为堆栈结构(stack frame),另外一个更常用的名字是过程活动记录(precedure activation recored)。它包括函数调用地址(即当所调用的函数结束后跳回的地方),任何不适合装入寄存器的参数以及一些寄存器值的保存。堆栈也可以被用作暂时存储区。alloca()函数分配的内存就是位于堆栈中。如果想让内存在函数调用结束之后仍然有效,就不要使用alloca()来分配(它将被下一个函数调用所覆盖)。
15,为每个控制线程分配不同的堆栈可在进程中支持不同的控制线程。
16,在UNIX中,当进程需要更多空间时,堆栈会自动生长。程序员可以想象堆栈是无限大的。在UNIX的实现中一般使用某种形式的虚拟内存。当试图访问当前系统分配给堆栈的空间之外时,它将产生一个硬件中断,称为页错误(page fault)。
17,在正常情况下,内核通过向违规的进程发送合适的信号(可能是段错误)来处理对无效地址的引用。内存映射硬件确保你无法访问操作系统分配给你的进程之外的内存。
?
?
?
第7章《对内存的思考》
1,所有进程共享机器的物理内存,当内存用完时就用磁盘保存数据。在进程运行时,数据在磁盘和内存之间来回移动。内存管理硬件负责把虚拟地址翻译为物理地址,并让一个进程始终运行于系统的真正内存中。应用程序程序员只看到虚拟地址,并不知道自己的进程在磁盘和内存之间来回切换。
2,虚拟内存通过“页”的形式组织。页就是操作系统在磁盘和内存之间移来移去或进行保护的单位。当内从的映象在磁盘和物理内存间来回移动时,称它们是page in(移入内存)或page out(移到磁盘)。
3,从潜在的可能性上说,与进程有关的所有内存都将被系统所使用。如果该进程可能不会马上运行(可能它的优先级低,也可能是它处于睡眠状态),操作系统可以暂时取回所有分配给它的物理内存资源,将该进程的所有相关信息都备份到磁盘上。这样,这个进程就被“换出”。在磁盘中有一个特殊的“交换区”,用于保存从内存中换出的进程。在一台机器中,交换区的大小一般是物理内存的几倍。只有用户进程才会被换进换出,SunOS内核常驻于内存中。
4,进程只能操作位于物理内存中的页面。当进程引用一个不在物理内存中的页面时,MMU就会产生一个页错误。内核对此事件做出响应,并判断该引用是否有效。如果无效,内核向进程发出一个“segmentation violation(段违规)”的信号。如果有效,内核从磁盘取回该页,换入到内存中。一旦页面进入内存,进程便被解锁,可以重新运行--进程本身并不知道它曾经因为页面换入事件等待了一会。
5,Cache存储器是多层存储器概念的更深扩展。它的特点是容量小,价格高,速度快。Cache位于CPU和内存之间,是一种极快的存储缓冲区。从内存管理单元(MMU)的角度看,有些机器的Cache是属于CPU一侧的,在这种情况下,Cache使用的是虚拟地址,在每次进程切换时,它的内容必须进行刷新。也有些机器的Cache从MMU的角度看是属于物理内存一侧的,在这种情况下,Cache使用的是物理地址,这就容易使多处理器CPU共享同一个Cache。
6,所有的现代处理器都使用了Cache存储器。当数据从内存读入时,整“行”(一般16或32个字节)的数据被装入Cache。如果程序具有良好的地址引用局部性(如:它顺序浏览一个字符串),那么CPU以后对邻近数据的引用就可以从快速的Cache读取,而不用从缓慢的内存中读取。Cache操作的速度与系统时间相同,所以一个50MHz的处理器,其Cache的存取周期为20ns。在典型情况下,主存的存取速度可能只有它的四分之一。与常规的内存相比,Cache要贵得多,单位体积也更大,消耗的能量也更多。所以,在系统中我们把它作为存储系统的附加部分,而不是把它作为唯一的存储形式。
7,Cache包含一个地址的列表以及它们的内容。随着处理器不断引入新的内存地址,Cache的地址列表也一直处于变化中。所有对内存的读取和写入操作都要经过Cache。当处理器需要从一个特定的地址提取数据时,这个请求首先递交给Cache。如果数据已经存在于Cache中,它就可以立即被提取。否则,Cache向内存传递这个请求,于是就要进行较缓慢的访问内存操作。内存读取的数据以行为单位,在读取的同时也装入到Cache中。
8,如果你的程序行为颇为怪异,以致每次都无法命中Cache,那么程序的性能比不采用Cache还要差。因为每次判断Cache是否命中的额外逻辑并不是免费的午餐。
9,Sun当前使用俩种类型的Cache:全写法(write-through)Cache----每次写入Cache时总是同时写入到内存中,使内存和Cache始终保持一致。写回法(write-back)Cache--当第一次写入时,只对Cache进行写入。如果已经写入过的Cache行再次需要写入时,此时第一次写入的结果尚未保存,所以要把它写入到内存中。当内核切换进程时,Cache中的所有数据也都要写入到内存中。在俩种情况下,一旦对Cache的访问结束,指令流都将继续执行,不用等待缓慢的内存操作全部完成。
10,行(line) 行就是对Cache进行访问的单位。每行由俩部分组成,一个数据部分以及一个标签,用于指定它所代表的地址。块(block)一个Cache行内的数据被称作块。块保存来回移动于Cache行和内存之间的字节数据。一个典型的块为32字节。一个Cache行的内容代表特定的内存块,如果处理器试图访问属于该块地址范围的内存,它就会作出反应,速度自然要比访问内存快得多。Cache,一个Cache(一般为64K到1M之间,也可能更多)由许多行组成。有时也使用相关的硬件来加速对标签的访问。为了提高速度,Cache的位置离CPU很近,而且内存系统和总线经过高度优化,尽可能地提高大小等于Cache块的数据块的移动速度。
11,就像堆栈段能够根据需要自动增长一样,数据段也包含了一个对象,用于完成这项工作,这就是堆(heap),堆区域用于动态分配的存储,也就是通过malloc(内存分配)函数获得内从,并通过指针访问。堆中的所有东西都是匿名的----不能按名字直接访问,只能通过指针间接访问。从堆中获取内存的惟一办法就是通过调用malloc(以及同类的calloc,realloc等)库函数。calloc函数与malloc类似,但它在返回指针之前先把分配好的内存的内容都清空为零。calloc函数中的c的意思是“分配清零后的内存”。realloc函数改变一个指针所指向的内存的大小,既可以将其扩大,也可以把它缩小,它经常把内存拷贝到别的地方然后将指向新地址的指针返回给你。
12,堆内存的回收不必与它所分配的顺序一致,所以无序的malloc/free最终会产生堆碎片,堆对它区域管理的一种策略是建立一个可用块(“自由存储区”)的链表,每块由malloc分配的内存块都在自己的前面表明自己的大小。有些人用arema这个术语描述由内存分配器(memory allocator)管理的内存块的集合。
13,被分配的内存总是经过对齐,以适合机器上最大尺寸的原子访问,一个malloc请求申请的内存大小为方便起见一般被圆整为2的乘方。回收的内存可供重新使用,但并没有(方便的)办法把它从你的进程移出交还给操作系统。
14,堆的末端由一个称为break的指针来标识。当堆管理器需要更多内存时,它可以通过系统调用brk和sbrk来移动break指针。一般情况下,不必由自己显式地调用brk,如果分配的内存容量很大,brk最终会被自动调用。用于管理内存的调用是:malloc和free--从堆中获得内存以及把内存返回给堆。brk和sbrk--调整数据段的大小至一个绝对值(通过某个增量)。
15,有些程序并不需要管理它们的动态内存的使用。当需要内存时,它们简单地通过分配来获得,从来不用担心如何释放它们。这类程序包括编译器和其他一些运行一段固定的时间然后终止的程序。当这类程序终止时,所有内存会被自动回收。
16,生存时间长的程序,如日历管理器,邮件工具以及操作系统本身,需要管理动态内存的分配和回收。由于C语言通常并不使用垃圾收集器(自动确认并回收不再使用的内存块),这些C程序在使用amlloc()和free()时不得不非常慎重。
17,堆经常会出现俩种类型的问题。释放或改写仍在使用的内存(称为“内存损坏”)。未释放不再使用的内存(称为“内存泄露”)。
18,我们使用“内存泄露”这个词是因为一种稀有的资源被一个进程榨干。内存泄露的主要可见症状就是罪魁进程的速度会减慢。原因是体积大的进程更有可能被系统换出,让别的进程运行,而且打得进程在换进换出时花费的时间也更多。
19,操作系统内核同时动态管理它的内存使用。内核中的许多数据表是动态分配的,所以预先没有固定的限制。如果一个内核程序错误引起内存泄露,机器的速度便会慢下来,有时机器干脆挂起或不知所措。当内核程序请求内存时,它们通常会进行等待,直到有足够的内存可以分配为止,如果出现内存泄露,最终可能导致可以分配的内存无法满足内核的需求如果每个内核程序都无限制的等待--于是机器便被挂起。
20,UNIX编程,常遇到的俩个错误。bus error(core dumped)总线错误(信息已转储)和segmentation fault(core dumped) 段错误(信息已转储)。当硬件告诉操作系统一个有用的内存引用时,就会出现这俩种错误。操作系统通过向出错的进程发送一个信号与之交流。信号就是一种事件通知或一个软件中断,在UNIX系统编程中使用很广,但在引用程序编程中几乎不使用。在缺省情况下,进程在收到“总线错误”或“段错误”信号后将进行信息转储并终止。
21,信号是由于硬件中断而产生的。对中断的编程是非常困难的,因为它们是异步发生的(其发生时间是不可预测的),因此,信号编程和调试也是很困难的。可以通过阅读信号的主文档和头文件usr/include/sys/signal.h了解更多相关信息。
22,这条信息的“core dump”部分则来源于很早的过去,那时所有的内存都是由铁氧化物圆环(也就是core,指磁心)制造的。所以“core”这个词用作“内存”的同义词。
23,总线错误,事实上,总线错误几乎都是由于未对齐的读或写引起的。它之所以称为总线错误,是因为出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。对齐(alignment)的意思就是数据项只能存储在地址是数据项大小的整数倍的内存位置上。在现代的计算机架构中,尤其是RISC架构,都需要数据对齐,因为与任意的对齐有关的额外逻辑会使整个内存系统更大且更慢。通过迫使每个内存访问局限在一个Cache行或一个单独的页面内,可以极大地简化(并加速)如Cache控制器和内存管理单元这样的硬件。
24,我们表达“数据项不能跨越页面或Cache边界”规则的方法多少有些间接,因为我们用地址对齐这个术语来陈诉这个问题,而不是直截了当说是禁止内存跨页访问,但它们说的是同一回事。例如,访问一个8字节的double数据时,地址只允许是8的整数倍。所以一个double数据可以存储于地址24,8008或32768,但不能存储于地址1006(因为它无法被8整除),页和Cache的大小是经过精心设计的,这样只要遵守对齐规则就可以保证一个原子数据项不会跨越一个页或Cache块的边界。编译器通过自动分配和填充数据(在内存中)来进行对齐。
25,段错误或段违规(segmentation violation)是由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常则通常是由于解除引用一个未初始化或非法值的指针引起的。如果指针引用一个并不位于你的地址空间中的地址,操作系统便会对此进行干涉。
26,通常导致段错误的几个直接原因:解除引用一个包含非法值的指针。解除应用一个空指针(常常由于从系统程序中返回空指针,并未经检查就使用)。在未得到正确的权限时进行访问。例如,试图往一个只读的文本段存储值就会引起段错误。用完了堆栈或对空间(虚拟内存虽然巨大但绝非无限)。
27,总线错误意味着CPU对进程引用内存的一些做法不满,而段错误则是MMU对进程引用内存的一些情况发生抱怨。
28,导致段错误的常见编程错误:
1)坏指针值错误:在指针赋值之前就用它来引用内存,或者向库函数传送一个坏指针(不要上当!如果调试器显示系统程序中出现了段错误,并不能因为系统程序引起了段错误,问题可能还存在于自己的代码中)。第三种可能导致坏指针的原因是对指针进行释放之后再访问它的内容。可以修改free语句,在指针释放之后再将它置为空值。free(p);p = NULL;这样,如果在指针释放之后继续使用该指针,至少程序能在终止之前进行信息转储。
2)改写(overwrite)错误:越过数组边界写入数据,在动态分配的内存俩端之外写入数据,或改写一些堆管理数据结构(在动态分配的内存之前的区域写入数据就很容易发生这种情况)。
3)指针释放引起的错误:释放同一内存块俩次,或释放一块未曾使用malloc分配的内存,或释放仍在使用中的内存,或释放一个无效的指针。一个极为常见的与释放内存有关的错误就是在for(p = start;p;p = p -> next)这样的循环中迭代一个链表,并在循环体中使用free(p)语句。这样,在下一次循环迭代时,程序就会对已经释放的指针进行解除引用操作,从而导致不可预料的结果。
29,当程序出现坏指针时,什么样的结果都有可能发生。如果“你走运”,指针将指向你的地址空间之外,这样第一次使用该指针的时候就会使程序进行信息转储后终止。如果你“不走运”,指针将指向你的地址空间之内,并损坏(改写)它所指向的内存软件的任何信息。
?
第8章《为什么程序员无法分清万圣节和圣诞节》
1,八进制31等于十进制25,也就是说万圣节10月31日等于圣诞节12月25日。
2,整型提升就是char,short int和位段类型(无论signed或unsigned)以及枚举类型将被提升为int,前提是int能够完整地容纳原先的数据,否则将被转换为unsigned int。ANSI C表示如果编译器能够保证运算结果一致,也可以省略类型提升--这通常出现在表达式中存在常量操作的时候。
3,C语言中的类型转换远比其他语言更为常见,其他语言往往将类型转换只用于操作数上,使操作符俩端数据类型一致。C语言也执行这项任务,但它同时也提升比规范类型int或double更小的数据类型。
4,隐式类型转换是语言中的一种临机手段,起源于简化最初的编译器的想法。把所有的操作数转换为统一的长度极大地简化了代码的生成,这样,压到堆栈中的参数都是同一长度的,所以运行时系统只需要知道参数的数目,而不需要知道它们的长度。把所有的浮点数运算都以double精度进行就意味着PDP-11能够简单地设置为double运算模型,它只管按double精度执行运算,无需顾及操作数的精度。即使不理睬缺省的类型转换,也可以用C语言进行大量的编程工作,许多C程序员就是这样做的。隐式类型转换在涉及原型的上下文中显得非常重要。
5,ANSI C函数原型的目的是使C语言成为一种更加可靠的语言。建立原型就是为了消除一种普通(但很难发现)的错误,就是形参和实参之间类型不匹配。ANSI C的函数原型就是采取一种新的函数声明形式,把参数的类型也包含在声明之中。函数的定义也作了相应的改变以匹配声明。这样,编译器就可以在函数的声明和使用之间进行检查。
6,”强制类型转换(cast)“这个术语从C语言一诞生就开始使用,既用于”类型转换“,也用于”消除类型歧义“。
?
第9章《再论数组》
1,声明本身可以进一步分为3种情况:外部数组(external array)的声明。数组的定义(记住,定义是声明的一种特殊情况,它分配内存空间,并可能提供一个初始值)。函数参数的声明。 2,所有作为函数参数的数组名总是可以通过编译器转换为指针。在其他所有情况下,数组的声明就是数组,指针的声明就是指针,俩者不能混淆。但在使用数组时,数组总是可以写成指针的形式,俩者可以互换。 3,数组和指针在编译器处理时是不同的,在运行时的表示形式也是不一样的,并可能产生不同的代码。对编译器而言,一个数组就是一个地址,一个指针就是一个地址的地址。 4,”表达式中的数组名“就是指针;C语言把数组下标作为指针的偏移量;”作为函数参数的数组名“等同于指针。 5,数组和指针可交换性的总结:1)用a[i]这样的形式对数组进行访问总是被编译器”改写“或解释为像*(a+1)这样的指针访问。2)指针始终就是指针,它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。3)在特定的上下文中,也就是它作为函数的参数(也只有这种情况),一个数组的声明可以看作是一个指针。作为函数参数的数组(就是在一个函数调用中)始终会被编译器修改成为指向数组第一个元素的指针。4)因此,当把一个数组定义为函数的参数时,可以选择把它定义为数组,也可以定义指针。不管选择哪种方法,在函数内部事实上获得的都是一个指针。5)在其他所有情况中,定义和声明必须匹配,如果定义了一个数组,在其他文件对它进行声明时页必须把它声明为数组,指针也是如此。
?
第10章《再论指针》
1,多维数组在系统编程中并不常用。所以,毫不奇怪的是,C语言并未像其他语言所要求的那样定义了详细了详细的运动时程序来支持这个特性。对于某些结构如动态数组,程序员必须使用指针显式地分配和操纵内存,而不是由编译器自动完成。例外还有一些结构(作为参数的多维数组),在C语言中并没有一般的形式来表达。
2,可以通过声明一个一维指针数组,其中每个指针指向一个字符串来取得类似二维字符数组的效果。
3,在C语言中,任何一维数组均可以作为函数的实参。形参被改写为指向数组第一个元素的指针,所以需要一个约定来提示数组的长度。一般有俩个基本方法:增加一个额外的参数,表示元素的数目(argc就是起这个作用)。赋予数组最后一个元素一个特殊的值,提示它是数组的尾部(字符串结尾的‘/0’字符就起这个作用),这个特殊值必须不会作为正常的元素在数组中出现。
?
第11章《你懂得C,所以C++不在话下》
1,软件对象的定义有很多种,其中绝大多数定义都同意面向对象的关键就是把一些数据和对这些数据进行操作的代码组合在一起,并用某种时髦手法将它们做成一个单元。许多编程语言把这种类型的单元称为“class(类)”。
2,抽象(abstraction)它是一个去除对象中不重要的细节的过程,只有那些描述了对象的本质特征的关键点才被保留。抽象是一种设计活动,其他的概念都是提供抽象的OOP特性。
3,类(class)类是一种用户定义类型,就好像是int这样的内置类型一样。内置类型已经有了一套完善的针对它的操作(如算术运算等),类机制也必须允许程序员规定他所定义的类能够进行的操作。类里面的任何东西被称为类的成员。
4,对象(object)某个类的一个特定变量,就像j可能是int类型的一个变量一样。对象也可以被称作类的实例(instance)。
5,封装(encapsulation)把类型,数据和函数组合在一起,组成一个类。在C语言中,头文件就是一个非常脆弱的封装实例。它之所以是一个微不足道的封装例子,是因为它的组合形式是纯词法意义上的,编译器并不知道头文件是一个语义单位。
6,继承(inheritance)这是一个很大的概念--允许类从一个更简单的基类中接收数据结构和函数。派生类获得基类的数据和操作,并可以根据需要对它们进行改写,也可以在派生类中增加新的数据和函数成员。在C语言里不存在继承的概念,没有任何东西可以模拟这个特性。
7,面向对象编程从面向对象设计开始,而面向对象设计从抽象开始。抽象的概念就是观察一群“事物”(如汽车,发票或正在执行的计算机程序),并认识到它们具有一些共同的主题,你可以忽略不重要的区别,只记录能表现事物特征的关键数据项(如许可证号码,预订数量或地址空间边界等)。当你这样做的时候,就是在进行“抽象”,所存储的数据类型就是“抽象数据类型”。抽象听上去像是一个艰深的数学概念,但不要被它糊弄--它只不过是对事物的简化而已。
8,在软件中,抽象是非常有用的,因为它允许程序员实现下列目标:隐藏不相关的细节,不注意力集中在本质特征上。向外部世界提供一个“黑盒子”接口。接口确定了了施加在对象之上的有效操作的集合,但它并不提示对象在内部是怎样实现它们的。把一个复杂的系统分解成几个相互独立的组成部分。这可以做到分工明确,避免组件之间不符合规则的相互作用。重用和共享代码。
9,C语言通过允许用户定义新的类型(struct,enum)来支持抽象。用户定义类型几乎和预定义类型(int,char等)一样方便,使用形式也几乎一样。我们说“几乎一样方便”是因为C语言并不允许在用户定义类型中重新定义*,<<,[],+等预定义操作符。C++则消除了这个障碍。C++同时提供自动和受控制的初始化,数据在生命周期结束后自动清除以及隐式类型转换。这些特征有些是C语言所不支持的,有些在C语言里不是很方便。
10,抽象建立了一种抽象数据类型,C++使用类(class)这个特性来实现它。它提供了一种自上而下的,观察数据类型属性的方法来看待封装:把用户定义类型中的各种数据和方法组合在一起。它同时也提供了一种自底向上的观点来看待封装:把各种数据和方法组合在一起实现一种用户定义类型。
11,封装--把相关的类型,数据和函数组合在一起。当你把抽象数据类型和它们的操作捆绑在一起的时候,就是在进行“封装”。非OOP语言没有完备的机制来实现封装。我们没有办法告诉C编译器“这3个函数只对这个特定的结构类型才有效”,也没有办法防止程序定义一个新的函数,以未经检查的和不一致的方式访问这个结构。
12,在程序设计演化的最初阶段,汇编程序只能在位和字上进行操作。随着高级语言的出现,程序员可以很容易地访问各种日益增长的硬件操作数:float,double,long,char等,有些高级语言使用了强类型,确保只有在某种类型的变量上才能有效地进行某种类型的操作,这是类的启蒙形式,因为它把数据项和可能施加载它们上面的操作固定在一起。这些操作通常对应每条单独的硬件指令上,如“浮点数乘法”。
13,随着程序设计语言的进一步发展,它们允许程序员将各种数据类型整合在一起形成用户定义的记录(在C语言中是结构)。但没有办法对函数进行限制。是它们不能随心所欲地操作数据以及对用户定义类型的私有字段进行访问。如果一个结构是完全可见的。它的任何部分都可能以任何方式被修改。人们无法把函数固定到数据类型上,是它们清晰地成为一体。
14,程序设计语言的当前状态是面向对象语言,它们通过把用户定义的数据结构和用户定义的能够在这些数据结构上进行操作的函数捆绑在一起实现了数据的完整性。别的函数无法访问用户定义类型的内部数据。这样,强类型就从预定义类型扩展到用户定义类型。
15,C++的类机制实现了OOP的封装要求,类就是封装的软件实现。类也是一种类型,就像char,int,double和struct rec*都是类型一样。因此,你必须声明该类的变量以便进行游泳的工作。类和类型一样,可以对它进行很多操作,如取得它的地址,把它作为参数传递,把它作为函数的返回值,是它成为常量值等。一个对象(一个类的变量)可以像声明其他任何变量一样被声明。
16,C++类允许用户定义类型:把用户定义类型和施加在它们上面的操作组合在一起。具有和内置类型一样的特权和外观。可以用更基本的类型创建更复杂的类型。
17,类就是用户定义数据加上所有对该类型进行的操作。类经常被定义的形式是:一个包含多个数据的结构,加上对这些数据进行操作的函数的指针。编译器施行强类型——确保这些函数只会被该类的对象调用,而且该类的对象无法调用出它们之外的其它函数。
18,访问控制是一个关键字,它说明了谁可以访问接下来的声明的数据或函数。
? 1)public:属于public的声明在类的外部可见,并可按需要进行设置,调用和操纵。一般的原则是不要把类的数据做成public,因为让数据保持私有才符合面向对象编程理论之一,只有类本身才能改变自己的数据,外部函数只能调用类的成员函数,这就保证了雷的数据只会以呵护规则的方式被更新。
? 2)protected:属protected的声明的内容只能由该类本身的函数以及从该类派生的类的函数使用。
? 3)private: 属private的声明只能被该类的成员函数使用。private声明在类外部是可见的(名字是已知的),但却不能访问。
19,另外还有俩个关键字也会影响访问控制,它们是friend和virtual。这俩个关键字每次只能用于一条声明,而上述3个关键字每个后面可以跟一大串声明。另外一点不同的是,friend和virtual这俩个关键字后面不跟冒号。属于friend的函数不属于类的成员函数,但可以像成员函数一样访问类的private和protected成员。friend可以是一个函数,也可以是一个类。
20,C++的声明就是正常的C声明,内容包括函数,类型(包括其它类)或数据,类把它们捆在一起,类中的每个函数声明都需要一个实现,它可以在类里面实现,也可以在类外部实现(这是通常的做法)。
21,必须在需要调用的成员函数前面附上类的实例名(或称类的变量,也就是对象)。
22,绝大多数类至少具有一个构造函数。当类的一个对象被创建时,构造函数被隐式地调用,它负责对象的初始化。与之相对应,类也存在一个清理函数,称为析构函数。与对象被销毁(超出其生存范围或进行delete操作,回收它所使用的堆内存)时,析构函数被自动调用。析构函数不如构造函数常用,它里面的代码一般用于处理一些特殊的终止要求以及垃圾收集等。
23,有些人把析构函数当做一种保险方法来确保当对象离开适当的范围时,同步锁总能够被释放。所以他们不仅清除对象,还清理对象所持有的锁。构造函数和析构函数是非常需要的,因为类外部的任何函数都不能访问类的private数据成员。因此,你需要类内部有一个特权函数来创建一个对象并对其进行初始化。
24,相对于C语言而言,这是一个小小的飞跃。在C语言中,只能使用赋值符号在变量定义时对它进行初始化,或干脆使它保持为初始化状态。可以在C++的类中声明多个构造函数,通过参数来区分它们。构造函数的名字总是和类的名字一样。
25,构造函数是必要的,因为类通常包含一些结构,而结构又可能包含许多字段。这就需要复杂的初始化。当类的一个对象被创建时,构造函数会被自动调用,程序员永远不应该显示地调用构造函数。至于全局和静态对象,它们的构造函数会在程序开始时被自动调用,而当程序终止时,它们的析构函数会被自动调用。
26,构造函数和析构函数违反了C语言中“一切工作自己负责”的原则。它们可以使大量的工作在程序运行时被隐式地完成,减轻程序员的负担,这也违背了C语言的哲学,也就是语言中的任何部分都不应该通过隐藏的运行时程序来实现。
27,当一个类沿用或定制它的惟一基类的数据结构和成员函数时,它就是用了单继承。这就建立了一个类体系,类似一种科学分类法。每一层都是对上一层的细化。类型继承是OOP的精华之一,这个概念在C语言中确实不存在。你要做好思想准备,迎接这个“概念上的飞跃”。
28,继承通常在概念上提供越来越多的细化。一般从较为简单的基类(如交通工具)派生出更为明确的派生类(如载客小汽车,救火车或运货车等)。它既可裁剪也可以增加可用的操作。
29,继承的语法:派生类的名字后面跟一个冒号,然后是基类的名字。不要把一个类内部嵌套另一个类与继承混淆。嵌套只是把一个类嵌入另一个类的内部,它并不具有特殊的权限,跟被嵌套的类也没有什么特殊的关系。嵌套通常被用于实现容器类(就是实现一些数据结构的类,如链表,散链表,队列等)。现在C++增加了模板(template)这个特性,它页被用于实现容器类。
30,继承表示派生类是基类的一个变型,它们之间如何相互访问,则是有许多详细的语义来决定。与嵌套类(一个较小的对象是一个较大对象的许多组成部分之一)不同,继承表示一个对象是一个更为普通的父对象的特型。我们不会认为不如动物内嵌了一条狗,而会认为狗继承了哺乳动物的特征。
31,多重继承允许把俩个类组合成一个,这样结果类对象的行为类似于这俩个类的对象中的任何一个。它把树形类体系变成格形。
32,重载(overload)就是简单地复用一个现存的名字,但使它操作一个不同的类型。它可以是函数的名字,也可以是一个操作符。操作符重载在C语言中已经以一种初步的方式存在。事实上,所有的语言都为内置类型进行了操作符重载。
33,重载(按照它的定义)总是在编译时进行解析。编译器查看操作数的类型,并核查它是否是该操作符所声明的类型之一。
34,像C语言具有自己的标准I/O函数库一样,C++的特性之一就是它自身拥有一套新的I/O程序和概念。C++有一个iostream.h头文件,提供了I/O接口,使I/O操作更为方便,也更符合OOP的理念。
35,多态(polymorphism)源于希腊语,意思是“多种形状”。在C++中,它的意思是支持相关的对象具有不同的成员函数(但原型相同),并允许对象与适当的成员函数进行运行时绑定。C++通过覆盖(override)支持这种机制——所有的多态成员函数具有相同的名字,由运行时系统判断哪一个最为合适。当使用继承时就要用到这种机制:有时你无法在编译时分辨所拥有的对象到底是积累对象还是派生类对象。这个判断并调用正确的函数的过程称为“后期绑定(late bingding)”。在成员函数前面加上virtual关键字告诉编译器该成员函数是多态的(也就是虚拟函数)。
36,在寻常的编译时重载中,函数的原型必须显著的不同,这样编译器才能通过查看参数的类型判断需要调用哪个函数。但在虚拟函数中,函数的原型必须相同,由运行时系统进行解析调用哪一个函数。
37,多态是指一个函数或操作符只有一个名字,但它可以用于几个不同的派生类型的能力。每个对象都实现该操作的一种变型,表现一种最适合自身的行为。它始于覆盖一个名字——对同一个名字进行复用,代表不同对象中的相同概念。多态非常有用,因为它意味着可以给类似的东西取相同的名字。运行时系统在几个名字相同的函数中选择了正确的一个进行调用,这就是多态。
38,当想用派生类的成员函数取代积累的同名函数时,C++要求你必须预先通知编译器。通知的方法就是在可能会被取代的基类成员函数前面加上virtual关键字。
39,从当前这个上下文角度来说,virtual(虚拟)这个词多少显得有些用词不当。在计算机科学的其它领域中,virtual的意思是用户所看到的东西事实上并不存在,它只是用某种方法支撑的幻觉罢了。这里,它的意思是不让用户看到事实上存在的东西(基类的成员函数)。换用一个更有意义的关键字(虽然长得不切实际):在运行时根据对象的类型选择合适的成员函数。
40,多态是一种运行时效果,它是指C++对象在运行时决定应该调用哪个函数来实现某个特定操作的过程。运行时系统查看调用虚拟函数的对象,并选择适合该类型对象的成员函数。如果它是一个派生类对象,我们就不希望它调用基类版本的成员函数,而是希望它调用派生类的成员函数。但是当积累被编译时,编译器可能看不到这种情况。因此,这个效果必须在运行时动态实现,用C++的术语就是“虚拟实现”。?
41,单继承通常通过在每个对象内包含一个vptr指针来实现虚拟函数。vptr指针指向一个叫做vtbl的函数指针向量(称为虚拟函数表,也称V表)。每个类都有一个这样的向量,每个虚拟函数在该向量中都有一条记录。使用这种方法,该类的所有对象共享实现代码。虚拟函数表的布局就是预先设置好的,某个成员函数的函数指针在该类所有子类的虚拟函数和表中的偏移地址都是一样的。在运行时,对虚拟成员函数的调用是通过vptr指针根据适当的偏移量调用虚拟函数表中适合的函数指针来实现的,它是一种间接的调用。多重继承的情况更为复杂,需要另外一层的间接形式。
42,在多态中,你可以发掘出许多新奇的玩意,但有时候它们又是极为本质的东西。它可使派生类的成员函数优先于积累的同名函数获得调用,但如果派生类对虚拟函数未曾定制,它页可以调用基类的成员函数。有时候,成员函数在编译时并不知道它是作用于本类的对象还是派生于本类的子类对象。多态必须保证这种情况能够正确地工作。
43,异常(exception):它的关键思想是“cluster,集群”。它用于在错误处理时改变程序的控制流。异常通过发生错误时把处理自动切换到程序中用于处理错误的那部分代码。从而简化错误处理。
44,模板(template):这个特性支持参数化类型。同类/对象的关系一样,模板/函数的关系也可以看作时为算法提供一种“甜点刀具”的方法。一旦确定了基本的算法,你可以把它应用于不同的类型。template<class T> T min(T a,T b)(return(a<b)? a :b)允许你对min函数和变量a,b赋予任意类型T(该类型必须能接受<操作符)。有些人称模板为编译时的多态。这是一个优点,但它也以为着一个通过模板声明的操作可以由许多不同的类型来进行,所以你必须在编译时决定使用哪个类型。
45,内联(inline)函数:程序员可以规定某个特定的函数在行内以指令流的形式展开(就像宏一一样),而不是产生一个函数调用。
46,new和delete操作符,用于取代malloc()和free()函数。这俩个操作符用起来更方便一些(如能够自动完成sizeof的计算工作,并会自动调用合适的构造函数和析构函数)。new能够真正地建立一个对象,malloc()函数只能分配内存。
47,传引用调用(call-by-rererence,相当于传值调用):C语言只使用传址调用(call-by-value)。C++在语言中引入了传引用调用,可以把对象的引用作为参数传递。
?