第一部分 C++过程化语言基础
第3章 表达式和语句
-
表达式是操作符、操作数和标点符号组成的序列,其目的是说明一个计算过程。
-
表达式根据某些约定、求值次序、结合性和优先级规则来进行计算。
-
左值(left value,缩写为lvalue)是能出现在赋值表达式左边的表达式。左值表达式具有存放数据的空间,并且存放是允许的。
-
显然常量不是左值,因为C++规定常量的值一旦确定是不能更改的。
-
右值(right value,缩写为rvalue)只能出现在赋值表达式的右边。左值表达式也可以作为右值表达式。
-
由表达式组成的语句称为表达式语句,它由一个表达式后接一个分号“;”组成。
-
块(或称复合语句)是指括在一对大括号{}里的语句序列。从语法上来说,块可以被认为是单个语句。
-
对于整型数则为除法取整操作。例如,5/2得到结果为2。
对于浮点数则为通常意义的除法。例如,5.0/2.0得到结果为2.5。 -
如整数加法是将两个整型数相加,而浮点数加法是将两个浮点数相加,相加的具体操作(在机器指令级上)浮点和整数是不同的。
-
%如果对浮点数操作,则会引起编译错误。
-
赋值构成一个表达式,因而它具有值。赋值表达式的值为赋值符左边表达式的值。
-
进行算术运算时,很可能溢出结果。发生溢出是由于一个变量被赋予一个超出其数据类型表示范围的数值。数值溢出是不会引起编译错误的,只要分母不为0也不会引起除0运行故障,但会使运行结果发生偏差。
-
一个整数类型的变量,用任何一个超过表示范围的整数初始化,得到的值为用该整数范围作模运算后的值。
-
转换总是朝表达数据能力更强的方向,并且转换总是逐个运算符进行的
-
数据运算过程中自动进行的类型转换称为隐式类型转换。
-
如果让第1次乘法的结果以long型数保留下来,就能得到正确的结果。这就要求参加乘法运算的两个数至少有一个为long型数。
-
强制转换又称显式转换,其语法是在一个数值或变量前加上带括号的类型名。也可以类型名后跟带括号的数值或表达式。
-
前增量操作++a的意义为:先修改操作数使之增1,然后将增1过的a值作为表达式的值。而**后增量操作a++**的意义为:先将变量a的值作为表达式的值确定下来,再将a增1。
-
由于增量与减量操作修改内存实体,所以操作数不能是常量,它必须是一个左值表达式。
-
增量与减量操作符是两个+或两个-的一个整体,中间不能有空格。如果有多于两个+或两个-连写的情况,则编译首先识别前面两个+或-为增量或减量操作符。
-
a++是个非左值表达式
-
**真和假是逻辑值。**在C++中,假意味着0,真意味着非0。所以,任意一个非0数都是真,表示为逻辑值就是1。
-
如果多个表达式用&&连接,则一个假表达式将使整个连接都为假(此处需要数理逻辑知识)。
-
同理,如果多个表达式用||连接,则一个真表达式将使整个连接都为真。
-
else连接到上面第1个没有配对的且为可见的if上
-
它是C++中唯一一个三元运算符,它们之间用“?”和“:”隔开。上例中,把a和b中较小的值赋给x。该例是if…else语句的一个替代:
-
条件运算符的嵌套可读性不够好,应通过加括号将意义明确。
-
在一个条件运算符的表达式中,如果后面两个表达式的值类型相同,均为左值,则该条件运算符表达式的值为左值表达式。
-
任何被转换的变量都不是左值。
-
在C中,条件运算符是不能作左值的,所以“(x?a:b)=1;”将通不过编译。
-
逗号表达式是有值的,这一点是语句所不能代替的。逗号表达式的值为第n个子表达式的值,即表达式n的值
-
逗号表达式作为值的形式,可以用于几乎所有的地方。
-
C++中,如果逗号表达式的最后一个表达式为左值,则该逗号表达式为左值。
-
在C中,逗号表达式是不能作左值的,所以“(a=1,b,c+1,d)=5;”将通不过编译。
-
先求前操作数的值还是先求后操作数的值,C++并无明确规定。
-
表达式和语句的副作用说明编程者对程序思路还有不够完善、不够周密的地方。它导致可读性下降,也破坏了可移植性。所以编程时务必要避免副作用的产生。
-
解决表达式副作用的方法是分解表达式语句,即将复合表达式语句写成几个简单的表达式语句。
第4章 过程化语句
-
高级语言源程序的基本组成单位是语句。语句按功能可以分为两类:一类用于描述计算机执行的操作运算(如表达式语句),即操作运算语句;另一类是控制上述操作运算的执行顺序(如循环控制语句),即流程控制语句。后一类语句也称为过程化语句。
-
如果循环体包含一个以上的语句,应该用大括号括起来,以块语句形式出现。如果不加大括号,则while的范围只到while后面第一条语句。
-
C++有足够的能耐让代码最大限度的优化。该代码也显示了C++的灵活与技巧,但可读性较差,所以它不是现代程序设计所追求的。我们介绍的用意是让初学者见识这类代码,以达到更好地领会概念的目的。
-
当流程到达do后,立即执行循环体语句,然后再对条件表达式进行测试。若条件表达式的值为真(非0),则重复循环,否则退出。
该语句结构使循环至少执行一次。 -
为明显区分它们,do…while循环体即使是一个单语句,习惯上也使用大括号包围起来,并且while(表达式)直接写在大括号“}”的后面。这样的书写格式可以与while循环清楚地区分开来。例如:
-
switch后面括号中的表达式只能是整型、字符型或枚举型。
-
case语句起标号的作用。标号不能重名,所以每一个case常量表达式的值必须互不相同,否则就会出现编译错误。
-
case通常与break语句联用,以保证多路分支的正确实现。
-
多个case可以共用一组执行语句。
-
default语句是可选的。当default不出现时,则当表达式的值与所有常量表达式的值都不相等时,越过switch语句。
-
switch语句可以嵌套。case与default标号是与包含它的最小的switch相联系的。
-
因为switch语句只能对等式进行测试,如果测试值包含一个较大的范围,就需要关系表达式比较,这时候用if语句较好。
if…else语句的执行体等价于switch语句的case中含有break的语句组。 -
在switch语句中,break语句用来使流程跳出switch语句,而执行switch后的语句。
-
continue语句用在循环语句中,作用为结束本次循环,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定。
-
由于在C++中有块语句的支持,所以经常使用反条件的if语句,把continue后面的语句以块的形式包含在if语句之中,可以避免使用continue语句。
-
语句标号用标识符表示,它的命名规则与变量名相同。
-
用goto语句实现的循环完全可以用while或for循环来表示。现代程序设计方法主张限制使用goto语句,因为滥用goto语句将使程序流程无规则,可读性差。goto语句只在一个地方有使用价值:当要从多重循环深处直接跳转到循环之外时,如果用break语句,将要用多次,而且可读性并不好,这时goto可以发挥作用。
-
程序设计更多的是体现其艺术性,可读性是我们追求的重要目标。
第5章 函数
-
要编好程序,就要会合理地划分程序中的各个程序块,C++称之为函数。
-
领会函数调用的内部实现机制,区分函数声明与定义,掌握全局变量、静态局部变量和局部变量之间的区别,理解并运用递归、内联、重载和默认参数的函数。
-
标准库函数是C++提供的可以在任何程序中使用的公共函数。程序总是从main()函数开始启动。
-
函数可以被函数调用也可以调用函数。
-
C++不允许函数定义嵌套,即在函数定义中再定义一个函数是非法的。
-
函数原型是一条程序语句,即它必须以分号结束。它由函数返回类型、函数名和参数表构成
-
在C++中,函数声明就是函数原型。
函数原型和函数定义在返回类型、函数名和参数表上必须完全一致。如果它们不一致,就会发生编译错误。
函数原型不必包含参数的名字,而只要包含参数的类型。 -
该代码能够正确通过编译,因为函数声明的原型与函数调用相吻合。但在连接时,发现没有与函数声明相一致的函数定义,结果产生“不能确定的外部函数”的连接错误。
-
函数的返回值也称函数值。返回的不是函数本身,而是一个值。
-
编译器遇到一个函数调用时,需要判断该函数调用是否正确,该机制即函数原型。
-
一个程序将操作系统分配给其运行的内存块分为4个区域:
(1)代码区,存放程序的代码,即程序中的各个函数代码块。
(2)全局数据区,存放程序的全局数据和静态数据。
(3)堆区,存放程序的动态数据。
(4)栈区,存放程序的局部数据,即各个函数中的数据。 -
在函数外边访问的变量被认为是全局变量,并在程序的每个函数中是可见的。全局变量存放在内存的全局数据区。全局变量由编译器建立,并且初始化为0,在定义全局变量时,进行专门初始化的除外。
-
这样定义的全局变量n在所有函数中都可见。如果一个函数修改了n,则所有其他的函数都会看到修改后的变量。
-
全局变量通常在程序顶部定义。全局变量一旦定义后就在程序的任何地方可知。可以在程序中间的任何地方定义全局变量,但要在任何函数之外。全局变量定义之前的所有函数定义,不会知道该变量。
-
**在函数内部定义的变量仅在该函数内是可见的。**另外,局部变量的类型修饰是auto,表示该变量在栈中分配空间,但习惯上都省略auto。
-
一个函数可以为局部变量定义任何名字,而不用担心其他函数使用过同样的名字。
-
函数中的局部变量存放在栈区。在函数开始运行时,局部变量在栈区被分配空间;函数退出时,局部变量随之消失。
-
**局部变量没有默认初始化。**如果局部变量不被显式初始化,那么,其内容是不可预料的。
-
函数调用时,C++首先:
(1)建立被调函数的栈空间。
(2)保护调用函数的运行状态和返回地址。
(3)传递参数。
(4)将控制转交给被调函数。 -
funcA()可以修改其变量aa和bb,但始终不会影响main()中的a和b。这便是C++函数的参数传值特性。
-
可以看出,如果一层层地调用下去,或者过多地定义局部变量,特别是数组(将在第7章介绍),最后可能导致栈空间枯竭而引起程序运行出错。如果程序确实要占用相当大的栈空间,可以在连接前通过设置栈空间大小来改善。
-
函数在返回时,将把返回值保存在临时变量空间中(如果有返回值的话)。然后恢复调用函数的运行状态,释放栈空间,使其属于调用函数栈空间的一部分,最后根据返回地址,回到调用函数代码执行处。
-
在局部变量前加上“static”关键字,就成了静态局部变量。静态局部变量存放在内存的全局数据区。函数结束时,静态局部变量不会消失,每次调用该函数时,也不会为其重新分配空间。它始终驻留在全局数据区,直到程序运行结束。
-
静态局部变量与全局变量共享全局数据区,但静态局部变量只在定义它的函数中可见。
-
程序控制每次进入func()函数时,局部变量b都被初始化。而静态局部变量a仅在第一次调用时被初始化,第二次进入该函数时,不再进行初始化,这时它的值是第一次调用后的结果值4。
-
静态局部变量的用途有很多:可以使用它确定某函数是否被调用过;使用它保留多次调用的值。
-
**递归函数(recursive function)**即自调用函数,在函数体内部直接或间接地自己调用自己,即函数的嵌套调用是函数本身。
-
任何函数之间不能嵌套定义,调用函数与被调用函数之间相互独立,但彼此可以调用。
-
发生函数调用时,被调函数中保护了调用函数的运行环境和返回地址,使得调用函数的状态可以在被调函数运行返回后完全恢复,而且该状态与被调函数无关。
-
被调函数运行的代码虽是同一个函数的代码体,但由于调用点、调用时状态、返回点的不同,可以看作是函数的一个副本,与调用函数的代码无关,所以函数的代码是独立的。
被调函数运行的栈空间独立于调用函数的栈空间,所以与调用函数之间的数据也是无关的。函数之间靠参数传递和返回值来联系,函数看作为黑盒。 -
理论上已经证明,递归函数都能用非递归函数来代替。
-
递归增加了系统开销。时间上,执行调用与返回的额外工作要占用CPU时间。空间上,随着每递归一次,栈内存就多占用一截。
-
内联函数也称内嵌函数,它主要是解决程序的运行效率。
-
编译器看到inline后,为该函数创建一段代码,以便在后面每次碰到该函数的调用都用相应的一段代码来替换。内联函数可以在一开始仅声明一次。
-
内联函数必须在被调用之前声明或定义。因为内联函数的代码必须在被替换之前已经生成被替换的代码
-
**内联函数中不能含有复杂的结构控制语句,如switch和while。**如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码。
另外,递归函数(自己调用自己的函数)是不能被用来做内联函数的。 -
通过一个内联函数可以得到所有宏的替换效能和所有可预见的状态以及常规函数的类型检查:
-
对于在不同类型上作不同运算而又用同样的名字的情况,则称之为重载。
-
(1)寻找一个严格的匹配,如果找到了,就用那个函数。
(2)通过内部转换寻求一个匹配,只要找到了,就用那个函数。
(3)通过用户定义的转换寻求一个匹配,若能查出有唯一的一组转换,就用那个函数(见18.5节中的后增量函数type operator++(type&,int))。 -
重载函数至少在参数个数、参数类型或参数顺序上有所不同。
-
typedef定义的类型只能使之相同于一个已存在的类型,而不能建立新的类型,所以不能用typedef定义的类型名来区分重载函数声明中的参数。
-
让重载执行不同的功能,是不好的编程风格。同名函数应该具有相同的功能。
-
C++用名字粉碎(name mangling)的方法来改变函数名,以区分参数不同的同名函数。名字粉碎是十分简单的过程,一系列代码被附加到函数名上以指示参数类型以及它们出现的次序。
-
默认参数在函数声明中提供,当又有声明又有定义时,定义中不允许默认参数。如果函数只有定义,则默认参数才可出现在函数定义中。
-
如果一个函数中有多个默认参数,则形参分布中,默认参数应从右至左逐渐定义。当调用函数时,只能向左匹配参数。
-
默认值可以是全局变量、全局常量,甚至是一个函数
-
默认值不可以是局部变量,因为默认参数的函数调用是在编译时确定的,而局部变量的位置与值在编译时均无法确定。
第6章 程序结构
-
要求掌握外部存储类型和静态存储类型在多文件程序中的联络作用,理解作用域、可见性与生命期的概念,学会使用头文件,理解多文件结构,理解编译预处理的概念。
-
一般具有应用价值的程序由多个源文件组成。根据C++程序的定义,其中只有一个源文件具有主函数main(),而其他的文件不能含有main(),否则程序不知道该从何处开始执行了。
-
构成一个程序的多个源文件之间,通过声明数据或函数为外部的(extern)来进行沟通。它们告诉连接程序,在所有组成该程序的文件(这里是ch6_1.cpp和ch6_1_1.cpp)中搜索该函数的定义。其中,fn1()在本文件中定义,fn2()在ch6_1_1.cpp中定义。
-
虽然在包含main()函数的源文件中分配变量是最合理的,但哪个文件真正分配该变量(全局变量定义)是无关紧要的。
带extern的变量说明是变量声明,不是变量定义。 -
**在全局变量前加一个static,使该变量只在这个源文件中可用,称之为全局静态变量。**全局静态变量就是静态全局变量。
-
但是在多文件组成的程序里,全局变量与全局静态变量是不同的。全局静态变量使得该变量成为由定义该变量的源文件所独享。
-
静态全局变量对组成该程序的其他源文件是无效的。
-
使一个变量只在一个源文件中全局使用有时是必要的。第一,不必担心另外源文件使用它的名字,该名字在源文件中是唯一的;第二,源文件的全局变量不能被其他源文件所用,不能被其他源文件所修改,保证变量的值是可靠的。
-
函数的声明和定义默认情况下在整个程序中是外部(extern)的。有时候,你可能需要使某个函数只在一个源文件中有效,不能被其他源文件所用,这时在函数前面加上static。
-
在文件作用域下声明的inline函数默认为static存储类型。在文件作用域下声明的const的常量也默认为static存储类型。它们如果加上extern,则为外部存储类型。
-
作用域是标识符在程序中有效的范围,标识符的引入与声明有关,作用域开始于标识符的声明处。C++的作用域范围分为局部作用域(块作用域)、函数作用域、函数原型作用域、文件作用域和类作用域
-
局部变量不具有函数作用域。
-
函数原型声明(不是函数定义)中所作的参数声明在该作用域中。这个作用域开始于函数原型声明的左括号,结束于函数原型声明的右括号。
-
参数中有了标识符可以增强可读性。上面参数中带标识符的函数原型声明使人一看就明白一个参数是宽度值,另一个参数是长度值。所以,习惯上,在函数原型声明中,都为参数指定一个有说明意义的标识符,而且一般总是与该函数定义中参数的标识符一致。
-
文件作用域是在所有函数定义之外说明的,其作用域从说明点开始,一直延伸到源文件结束
-
在头文件的文件作用域中所进行的声明,若该头文件被一个源文件嵌入,则声明的作用域也扩展到该源文件中,直到源文件结束。
例如,cout和cin都是在头文件iostream.h的文件作用域中声明的标识符,这两个标识符的作用域延伸到嵌入iostream.h的源文件中。 -
可见性从另一角度表现标识符的有效性,标识符在某个位置可见,表示该标识符可以被引用。可见性与作用域是一致的。作用域指的是标识符有效的范围,而可见性是分析在某一位置标识符的有效性。
-
可见性在分析两个同名标识符作用域嵌套的特殊情况时,非常有用。在内层作用域中,外层作用域中声明的同名标识符是不可见的,当在内层作用域中引用这个标识符时,表示的是对内层作用域中声明的标识符的引用。
-
标识符的可见性范围不超过作用域,作用域则包含可见范围。
-
在内部块中,double i的作用域和可见性是一致的,int i的作用域存在,但不可见。在外部块中,ch的作用域与可见性是一致的,因为ch的可见性渗透至内部块中。
-
如果被隐藏的是全局变量,则可用符号::来引用该全局变量。
-
变量在固定的数据区中分配空间的,具有静态生命期。所以,全局变量、静态全局变量、静态局部变量都具有静态生命期。具有文件作用域的变量具有静态生命期。
-
被调用函数不能享有使用调用函数中数据的权利。
静态生命期的变量,若无显式初始化,则自动初始化为0。 -
**具有局部生命期的变量也具有局部作用域。但反之不然,具有局部作用域的变量若为局部变量,则具有局部生命期;若为静态局部变量,则具有静态生命期。**静态局部变量的生命期是从定义它的函数第一次被调用时开始存在,直到程序运行结束。
-
具有这种生命期的变量驻在内存的堆中。当用函数malloc()或new为变量分配空间时,生命期开始;当用free()或delete释放该变量的空间或程序结束时,生命期结束。
-
同一名字的声明可以多次,具有外部存储类型的声明可以在多个源文件中引用,因此方便的方法是将它们放在头文件中。头文件起着源文件之间接口的作用。
-
工程文件中的3个文件都含有myarea.h的头文件,这样可以使信息共用,保证程序的一致,在大型程序开发中尤为必要。
-
预处理程序也称预处理器,它包含在编译器中。预处理程序首先读源文件。预处理的输出是“翻译单元”,它是存放在内存中的临时文件。
-
预处理器遇到这种格式的包含指令后,首先在当前文件所在目录中进行搜索,如果找不到,再按标准方式进行搜索。这种方式适合于规定用户自己建立的头文件
-
条件编译的指令有#if、#else、#elif、#endif、#ifdef、#ifndef和#undef。
条件编译的一个有效使用是协调多个头文件。
第7章 数组
-
求理解数组下标,掌握初始化数组的方法,学会把数组用作函数参数,学会二维数组的使用,并学习数组应用的技术。
-
**下标是数组元素到数组开始的偏移量。**第1个元素的偏移量是0,第2个元素的偏移量是1,以此类推。由此,数组是一系列大小相同的连续项,每项到公共基点的偏移量是固定的。
-
一个数组定义是具有确定含义的操作,它分配固定大小的空间。如果方括号的值不能在编译时确定,那就只能在运行时确定,即在函数调用时即兴分配数组空间。这使得为局部作用域的数组分配数据空间的语句具有不同的意义,它随每次函数调用的不同而不同,这是不允许的。
-
编程时,如果要定义一个很大的数组,可以通过将其定义为静态或全局来解决,也可以将其在堆内存中分配(见8.4节)。
-
管变量size已经赋有值50,紧接着就是数组的定义,阅读上都能理解,但是size是变量这一性质是编译不能原谅的。对于常量,编译可以用一个值直接代替,但对于变量,编译不能用值来代替,而是编译为取该变量的值。取值是一个操作,不是值本身,不能决定数组下标。程序运行中,通常通过常量来决定数组大小。
-
如果循环条件误写成“i<=10”,那么程序将会执行到包括“iArray[10]=90;”的语句,改变不属于数组空间的内存单元(见图7-2)。这个代码的失误不会在程序的编译与连接中反映出来,而是可能一直运行下去,直到出现结果不正确,或严重时导致死机。
-
字符数组若用来存储字符串,则要考虑字符串末尾的'\0'结束符。
-
cin.get()在用户输入的最后字符后面加上'\0'。
-
初始化数组的值的个数不能多于数组元素个数,初始化数组的值也不能通过跳过逗号的方式来省略,这在C中是允许的,但在C++中不允许。
-
其中,全局数组和全局静态数组的初始化是在主函数运行之前完成的,而局部数组和局部静态数组的初始化是在进入主函数后完成的。
-
第一种方法用途较广,初始化时,系统自动在数组没有填值的位置用'\0'补上。另外,这种方法中的大括号可以省略
-
这里不要忘记为最后的'\0'分配空间。如果要初始化一个字符串"hello",那为它定义的数组至少有6个数组元素。
-
**编译时必须知道数组的大小。**通常,声明数组时方括号内的数字决定了数组的大小。有初始化的数组定义又省略方括号中的数组大小时,编译器统计大括号之间的元素个数,以求出数组的大小。
-
sizeof操作使for循环自动调整次数。如果要从初始化a数组的集合中增删元素,只需重新编译即可,其他内容无须改动。
-
对于字符串的初始化,要注意数组实际分配的空间大小是字符串中字符个数加上末尾的'\0'结束符。
-
省略数组大小只能在有初始化的数组定义中。
-
无论何时,将数组作为参数传给函数,实际上只是把数组的地址传给函数
-
C++中有一个memset()函数,它可以一字节一字节地把整个内存区块设置为一个指定的值。memset()函数在string.h头文件中声明,它的第一个参数是内存区块的起始地址,第二个参数是设置每个字节的值,第三个参数是内存区块的长度(字节数,不是元素个数)
-
数组名作参数即数组作参数,它仅仅只是一个数组的起始地址而已。
-
在函数memset()栈区,从返回地址往上依次为第1、2、3个参数。第1个参数中的内容是main()函数中定义的数组ia1的起始地址;第2个参数是给数组设置的值(0);第3个参数是数组的长度(50×2)。
-
数组大小参数是需要的,因为从传递的数组参数(地址)中,没有数组大小的信息。
-
数组形参的空方括号只是告诉函数,该参数是个数组的起始地址。由于数组参数是地址,对数组参数不能通过sizeof求得数组大小,所以sum()函数必须要有第二个参数:数组的大小,即数组的元素个数。
-
二维数组是按先行后列的顺序在内存中线性排列的。
-
如果某行没有足够的初始化值,那么该行中的剩余元素都被初始化为0。初始化还可以将多个大括号简化为一个大括号。
-
如果对全部元素赋初值,则定义数组时对第一维的大小可以忽略,但第二维的大小不能省。
-
编译器会根据数据总个数分配空间,每行4列,所以确定该数组为3行。
-
在定义时,也可以只对部分元素赋初值而省略第一维的大小,但应分行赋初值。
-
作为参数传递一个二维数组给函数,其意义也为内存地址,所以原型中,声明整数数组参数的形式只能省略左边的方括号。要注意被传递的数组地址不要用数组名表示,要用第一个元素的地址表示,因为数组名表示的是二维数组的首地址,尽管地址值相同,但操作不同
-
函数调用时,数组参数的实参为整型变量的地址,函数原型中,数组参数的形参为整型数组的首地址
第8章 指针
-
这种用来操纵地址的特殊类型变量就是指针。指针用于数组,作为函数参数,用于内存访问和堆内存操作。
-
指向整型数的指针是包含该整型数地址的变量或常量
-
指向字符的指针是包含字符地址的变量或常量
-
**指针是一个内存实体,具有值。**要使用指针,就必须定义指针。**指针有指针常量和指针变量之分,定义指针通常定义的是指针变量,即可以随时改变指针的指向。**所以,指针与指针变量经常划等号。
-
建立指针包括定义指针和给指针赋初值。
变量存在于内存中的某位置(地址)。例如,一旦有了变量,则放置该变量的地方就用内存地址描述。
用&操作符可以获取变量的地址,指针用于存放地址。 -
间接引用指针时,可获得由该指针指向的变量内容。
-
*放在可执行语句中的指针之前,为间接引用操作符;*放在指针定义中时,为指针定义符。
非指针是不能用间接引用操作符的,因为*只能作用于地址。 -
指针也是变量,是变量就具有内存地址。所以指针也有地址。
-
不要将“int* iPtr=&iCount;”与“*iPtr=&iCount;(error)”混淆。前者是定义语句,*是指针定义符,C++为iPtr指针分配一个指针空间,并用iCount的地址值初始化;后者是赋值语句,左右两边类型不匹配。
-
*操作符在指针上的两种用途要区分开:定义或声明时,建立一指针;执行时,间接引用一指针。
-
指针忘了赋值比整型变量忘了赋值危险得多。
iPtr当前指向什么地方?该代码能通过编译,但没有赋初值的指针iPtr是一个随机地址。“*iPtr=58;”是把58赋到内存中的随机位置,因此将改写另一存储位置的数值,甚至修改了栈中的函数返回地址,计算机将死机或进入死循环。 -
指针是有类型的,给指针赋值,不但必须是一个地址,而且应该是一个与该指针类型相符的变量或常量的地址。
-
iPtr是整型指针,它总是访问该地址中的整型数;而fPtr是浮点指针,总是访问该地址的浮点数。
-
指针是具有某个类型的地址。
-
加减运算的结果,指针挪移到了邻近的内存单元,于是指针的运算与数组扯上了关系。
-
**数组名本身,没有方括号和下标,它实际上是地址,表示数组起始地址。**整型数组的数组名本身得到一整数地址,字符数组的数组名得到一字符型地址。
-
只有加法和减法可用于指针运算。
-
指针减法的原理与加法相同。只是,不论指针的加法还是减法,其访问操作都必须是有意义的,否则是危险的。
-
下标操作是针对地址而不仅仅是针对数组名的,所以iPtr[i]也表示第i个元素的值。
-
数组名本身是一指针,它的类型是指向数组元素的指针。&a[i]表示数组第i个元素的地址。
-
数组名是指针常量,区别于指针变量,所以,给数组名赋值是错误的。
-
对于编译器来说,数组名表示内存中分配了数组的固定位置,修改了这个数组名,就会丢失数组空间,所以数组名所代表的地址不能被修改。
-
堆(heap)是内存空间。堆是区别于栈区、全局数据区和代码区的另一个内存区域。
-
堆允许程序在运行时(而不是在编译时)申请某个大小的内存空间。
-
运行中申请的内存就是堆内存,所以堆内存是动态的。堆内存也称动态内存。
-
因为malloc()函数并不知道用这些内存干什么,所以它返回一个没有类型的指针(见8.6节)。但对整数指针ap来说,malloc()函数的返回值必须显式转换成整数类型指针才能被接受(ANSI C++标准)。
-
上例中并没有保证一定可以从堆中获得所需内存。有时,系统能提供的堆空间不够分配,这时系统会返回一个空指针值NULL。这时所有对该指针的访问都是破坏性的,因此调用malloc()函数更完善的代码应该如下
-
函数free()返还由malloc()函数分配的堆内存
-
new返回一个具有操作数的数据类型的指针。
-
delete的操作数是new返回的指针,当返还的是new分配的数组时,应该带[]。
-
可以看到,一个指针涉及两个变量:指针本身pi和指向的变量a。修改这两个变量的对应操作为“pi=&a;”和“*pi=58;”。
-
在指针定义语句的类型前加const,表示指向的对象是常量。
-
定义指向常量的指针只限定指针的间接访问只能读而不能写,而没有限定指针值的读写访问性。
-
常量指针定义“const int* pi=&a;”告诉编译,*pi是常量,不能将*pi作为左值进行操作。
-
在指针定义语句的指针名前加const,表示指针本身是常量。
-
pc是指针常量,在定义指针常量时必须初始化,就像常量初始化一样。
-
指针常量定义“int* const pc=&b;”告诉编译,pc是常量,不能作为左值进行操作,但是允许修改间接访问值,即*pc可以修改。
-
可以定义一个指向常量的指针常量,它必须在定义时进行初始化。
-
cpc和cpi都是指向常量的指针常量,它们既不允许修改指针值,也不允许修改*cpc的值
-
常量指针常量定义“const int* const cpc=&b;”告诉编译,cpc和*cpc都是常量,它们都不能作为左值进行操作。
-
考虑到安全性,指针一旦初始化或者赋了初值后,不轻易改动,于是指针访问数组中的元素便多用下标而不是频繁修改指针的指向。绝大多数指针应用是指针常量和常量指针常量。
-
一旦把数组作为参数传递到函数中,则在栈上定义了指针,可以对该指针进行递增、递减操作。
-
传递的数组参数在Sum()中,实质上是一个指针,所以声明Sum(int array[],int n)与Sum(int*array,int n)是等价的。
-
由于形参array是指针而不是数组,所以它所占的空间是指针大小而不是数组空间大小。不能用sizeof(array)/sizeof(*array)来求取数组元素个数,这就是第二参数n(表示数组元素个数)必须要给的原因。
-
传递指针的函数调用实现过程为:
(1)函数声明中指明指针参数,即本例中的void swap(intx,inty)。
(2)函数调用中传递变量的地址,即本例中的swap(&a,&b)。
(3)函数定义中对形参进行间接访问。 -
指针的灵活是以破坏函数的黑盒特性为代价的。它使函数可以访问本函数的栈空间以外的内存区域(函数的副作用初露端倪),以致引起了以下问题。
(1)可读性问题:因为间接访问比直接访问相对难理解,传递地址比传递值的直观性要差,函数声明与定义也相对比较复杂。
(2)重用性问题:函数调用依赖于调用函数或整个外部内存空间的环境,丧失了黑盒的特性,所以无法作为公共的函数模块来使用。
(3)调试复杂性问题:跟踪错误的区域从函数的局部栈空间扩大到整个内存空间。不但要跟踪变量,还要跟踪地址。错误现象从简单的不能得到相应的返回结果,扩展到系统环境遭破坏甚至死机。 -
**返回指针的函数称为指针函数。**指针函数不能把在它内部说明的具有局部作用域的数据地址作为返回值。
-
该程序中的getInt()函数返回一个局部作用域变量的地址是不妥的。因为getInt()函数结束时,其栈中的变量value随之消失。
-
可以返回堆地址,可以返回全局或静态变量的地址,但不要返回局部变量的地址。
-
**void指针是空类型指针,它不指向任何类型,即void指针仅仅只是一个地址。**所以空类型指针不能进行指针运算,也不能进行间接引用,因为指针运算和间接引用都需要指针的类型信息。
-
由于其他指针都包含地址信息,所以将其他指针的值赋给空类型指针是合法的;反之,将空类型指针赋给其他指针则不被允许,除非进行显式转换。
-
字符串用于字符数组初始化时,其在完成将内容填写到所创建的字符数组中之后,随即消失,不再另辟存储空间;而当字符串用于表达式,或输出,或赋值,或作参数传递,则其在运行中有它自己的存储空间,可以寻址访问。
-
字符串的类型是指向字符的指针(字符指针char*),它与字符数组名同属于一种类型。字符串在内存中以'\0'结尾。这种类型的字符串称为C字符串,或ASCIIZ字符串(ASCII序列后跟Zero之意)。
-
当编译器遇到一字符串时,就把它放到字符串池(data区的const区)中,以'\0'作结束符,记下其起始地址,在所构成的代码中使用该地址。这样,字符串就“变成”了地址。
-
由于字符串的地址属性,所以两个同样字符组成的字符串的地址是不相等的。
-
程序中两个字符串的比较实质上是两个地址的比较。在编译时,给了这两个字符串不同的存放地点,所以两个“join”字符串的地址是不同的。
-
字符串、字符数组名、字符指针均属于同一种数据类型。
-
输出字符指针就是输出字符串。
-
输出字符指针的间接引用,就是输出单个字符
-
当pc指向buffer后,字符串字面值“hello”仍逗留在内存的data区,但是再也访问不到该字符串(数据丢失)了。所以对于字符串赋给字符指针的情形,指针一般不再重新赋值。
-
字符串比较应该是逐个字符一一比较,通常使用标准库函数strcmp(),它在string.h或cstring头文件中声明,其原型为:其返回值如下:
(1)当str1串等于str2串时,返回值0;
(2)当str1串大于str2串时,返回一个正值;
(3)当str1串小于str2串时,返回一个负值。 -
C++中可以用字符串去初始化字符数组,但是不能对字符数组赋予一个字符串,原因是数组名是常量指针,不是左值。
-
函数strcpy()仅能对以'\0'作结束符的字符数组进行操作。若要对其他类型的数组赋值,可调用函数memcpy():
#include<iostream> #include<string.h> using namespace std; int main() { char src[10] = "*********"; char dst[10]; char* pc = (char*) mempcpy(dst, src, 10); cout<<pc<<endl; //输出字符串 cout<<src<< endl; }
-
一个数组中若每个元素都是一个指针,则为指针数组。
-
在二维数组情况下,每一列的大小必须是一样的,因此只能将数组中所要存储的字符串中最长列的大小作为数组的列的大小。这样,共需3×8=24字节。如果被赋值的各字符串长短相差悬殊,则二维数组空间就浪费多一些。二维数组是作为一个整体存储在某一个区域中。
-
指针数组是数组,则其名字便为指针常量。指针具有类型,那么指针数组名是什么类型呢?
指针数组名是指向指针的指针(即二级指针)。 -
这里的初始化值"a""b""c"必须为双引号,如果是单引号则表示是字符而不是字符串,而且字符没有'\0'的结束标记。字符总是占一个字节。
-
传递数组给函数就是传递指针给函数。传递指针数组给函数就是传递二级指针给函数。
-
NULL是空指针值,它不指向任何地方。
-
NULL与void*是不同的概念,NULL是一个值,一个指针值,任何类型的指针都可赋予该值;而void*是一种类型,它定义无类型指针。
-
C++程序只不过是操作系统调用的函数。
-
argc表示参数个数,argv表示参数数组。
-
尽管main()函数返回类型为void,即无返回值,但仍可使用exit()给操作系统返回一个值。任何其他的函数使用exit(),即意味着程序终止,返回到操作系统中。
-
函数代码是程序的算法指令部分,它们同样也占有内存空间,存放在代码(code)区。每个函数都有地址。指向函数地址的指针称为函数指针。函数指针指向代码区中的某个函数,通过函数指针可以调用相应的函数。
-
FUN是一个函数指针类型,该指针类型中的指针指向一个函数,它有两个整数参数,返回一个整型数。FUN不是指针,只是一个指针类型名。通过第二个语句的函数指针定义,才确定一个函数指针funp。
-
程序中,sigma()函数的第一个参数为函数指针,该指针指向的函数有一个double参数并返回double类型数。sin和cos就是这样的函数,它们作为实参赋给函数指针func。
-
最后一行声明一个函数,该函数返回一个函数指针,且有一个整型参数和一个函数指针参数。返回的函数指针是指向返回整型且无参数的函数。作为函数指针参数的指针指向无返回且无参数的函数。
-
指针不仅仅是地址,还有对数据类型的操作性规定。这是理解指针的关键。
-
堆允许程序在运行时,而不是在编译时,确定所申请的内存大小。在C++中,堆分配一般用new和delete两个操作符,malloc()和free()函数则相对过时,只在阅读用C编制的程序时才需要这些知识。
第9章 引用
-
引用是个别名,当建立引用时,程序用另一个变量或对象(目标)的名字初始化它。从那时起,引用作为目标的别名而使用,对引用的改动实际就是对目标的改动。
-
引用不是值,不占存储空间,声明引用时,目标的存储状态不会改变。所以,既然定义的概念有具体分配空间的含义,那么引用只有声明,没有定义。
#include<iostream> using namespace std; int main(){ int intOne = 5; int& rInt = intOne; cout << "intOne: " << intOne << endl; cout << "rInt: " << rInt << endl; rInt = 7; cout << "intOne: " << intOne << endl; cout << "rInt: " << rInt << endl; cout << "&intOne: " << &intOne << endl; cout << "&rInt: " << &rInt << endl; } /* intOne: 5 rInt: 5 intOne: 7 rInt: 7 &intOne: 0x61fe14 &rInt: 0x61fe14 */
-
引用在声明时必须被初始化,否则会产生编译错误。
-
引用运算符与地址操作符使用相同的符号。尽管它们显然是彼此相关的,但它们不一样。
引用运算符只在声明的时候使用,它放在类型名后面 -
为了提高可读性,不应在同一行上同时声明引用、指针和变量。
-
如果程序寻找引用的地址,它只能找到所引用的目标的地址。
-
C++没有提供访问引用本身地址的方法,因为它与指针或其他变量的地址不同,它没有任何意义。引用在建立时就初始化,而且总是作为目标的别名使用,即使在应用地址操作符时也是如此。
-
引用一旦初始化,它就维系在一定的目标上,再也不分开。任何对该引用的赋值,都是对引用所维系的目标赋值,而不是将引用维系到另一个目标上。
-
引用与指针有很大的差别,指针是个变量,可以把它再赋值成指向别处的地址;然而建立引用时必须进行初始化并且绝不会再指向其他不同的变量。
-
指针也是内存实体,所以可以有指针的引用:
-
对void进行引用是不允许的。
-
引用虽在语法上代表一种类型,但在概念上只是其他实体的附体,所以对同一实体可以定义多个引用,但对不存在的引用实体,就没有引用的引用,也没有指向引用实体的指针(所谓引用的指针)。
-
因为引用是变量或对象的引用,而不是类型的引用。所以有空指针,无空引用。
-
C++的目标之一就是让使用函数的用户无须考虑函数是如何工作的。传递指针给使用函数的用户增加了编程和理解的负担,这些负担本应属于被调用函数。
#include<iostream> using namespace std; void swap(int& x, int& y); int main(){ int x = 5, y = 6; cout << "Before swap, x: " << x << " y: " << y << endl; swap(x, y); cout << "After swap, x: " << x << " y: " << y << endl; } void swap(int& rx, int& ry){ int tmp = rx; rx = ry; ry = tmp; } /* Before swap, x: 5 y: 6 After swap, x: 6 y: 5 */
引用隐藏了函数所使用的参数传递的类型,所以无法从所看到的函数调用判断其是值传递还是引用传递。
-
函数只能返回一个值。如果程序需要从函数返回两个值怎么办?解决这一问题的办法之一是用引用给函数传递两个参数,然后由函数往目标中填入正确的值。因为用引用传递允许函数改变原来的目标,这一方法实际上让函数返回两个信息。这一策略绕过了函数的返回值,使得可以把返回值保留给函数,作报告运行成败或错误原因用。
#include<iostream> using namespace std; bool Factor(int, int&, int&); int main(){ int number, squared, cubed; cout << "Enter a number between 0 and 20 : "; cin >> number; bool error = Factor(number, squared, cubed); if(error) cout << "Error encoutered!\n"; else{ cout << "Number: " << number << endl; cout << "Squared: " << squared << endl; cout << "Cubed: " << cubed << endl; } } bool Factor(int n, int& rSquared, int& rCubed){ if(n > 20 | n < 0) return true; rSquared = n*n; rCubed = n*n*n; return false; } /* Enter a number between 0 and 20 : 10 Number: 10 Squared: 100 Cubed: 1000 */
-
函数返回值时,要生成一个值的副本。而用引用返回值时,不生成值的副本。
-
这种情况,函数fn2()的返回值不产生副本,所以直接将变量temp返回给主函数。主函数的赋值语句中的左值c直接从变量temp中得到复制,这样避免了临时变量的产生。当变量temp是一个用户自定义的类型时,这种方式直接带来了程序执行效率和空间利用的利益。
-
如果返回的引用是作为一个左值进行运算,也是程序员最忌讳的。
-
由于返回的是引用,所以可以作为左值直接进行增量操作。该函数调用代表typeA还是typeB的左值视具体的学生成绩统计结果而定。
本例说明:返回引用的函数,可以使函数成为左值 -
引用变量概念上是不占空间的,引用变量被理解为粘附在初始化的实体上,它的实现对用户来说不可见。但并不等于具体实现的时候,非得不占任何空间。
-
保护实参不被修改的办法是传递const指针和引用。
-
C++不区分变量的const引用和const变量的引用**。程序绝不能给引用本身重新赋值,使它指向另一个变量,因此引用总是const的**。如果对引用应用关键词const,其作用就是使目标成为const变量
-
返还堆空间有两种方式,一种是delete pd,另一种是delete&rd,因为&rd和pd都指向同一个堆空间地址
-
函数的副作用是良性还是恶性,各人自有评说,对于函数潜在的破坏性,是放任自流,还是适当地抑制,专家们动尽了脑筋,直到C++类机制的实现,才使函数的恶性作用得以控制。
-
引用是C++独有的特性。指针存在种种问题,间接引用指针会使代码可读性差,易编程出错。而引用正好扬弃了指针
-
引用具有表达清晰的优点。引用将对传递的参数的责任附给了编写函数的程序员,而不是使用它们的各个用户。
第10章 结构
-
学习本章后,应能掌握结构声明、结构变量定义与访问结构成员的方法,掌握结构作为参数传递与返回结构的函数方法,掌握链表结构的各项基本操作。
-
用结构变量就可以有组织地把这些不同类型的数据信息存放在一起。
-
**结构是用户自定义的新数据类型,除此之外,它可与int、float等基本数据类型同等看待。**声明结构类型时,首先指定关键字struct和结构名,然后用一对大括号将若干个结构成员数据类型说明括起来。
-
通常情况下,结构声明在所有函数之外,位于main()函数之前。这使新声明的数据类型在程序的任何地方都可以被使用。
-
声明一个结构并不分配内存,内存分配发生在定义这个新数据类型的变量中。
结构中包含的数据变量称为该结构的成员,如code、salary是结构Employee的成员。 -
一旦通过定义相应结构变量,分配了空间,就可以使用点操作符“.”(或称结构成员操作符)来访问结构中的成员。左操作数为结构类型变量,右操作数为结构中的成员。点操作符运算的结果可以是左值,也可以是右值。
-
两个不同结构名的变量是不允许相互赋值的,即使二者包含同样的成员。
-
根据结构类型可以定义一个变量,是变量就有地址。结构不像数组,结构变量不是指针。通过取地址“&”操作,可以得到结构变量的地址,这个地址就是结构的第一个成员地址。
-
可以将结构变量的地址赋给结构指针,结构指针通过箭头操作符“->”(也是一种结构成员操作符)来访问结构成员。
-
但必须清楚,当用点操作符时,它的左边应是一个结构变量;当用箭头操作符时,它的左边应是一个结构指针。
-
指针是有类型的,(间接)引用一个整型指针得到一个整数,引用一个结构指针得到一个结构。即*prPtr的值就是结构Person的变量pr1的值,而不会是其他类型的值。
-
结构数组中,每个元素都是结构变量,访问结构数组元素中的成员,方法与前类似。
-
发生交换时,并不是两个结构变量值交换,而是两个结构指针交换,所以交换的临时变量是一个结构指针。
-
结构可以按值传递,这种情况下整个结构值都将被复制到形参中去。
-
结构也可以引用传递,这种情况下仅仅把结构地址传递给形参
-
由于结构的大小是随用户定义而定的,所以有时候会很大。当结构很大时,引用传递的优越性才真正开始体现。引用传递的进一步介绍在18.3节。在编程经验上,除非是小结构,一般很少按值传递。
-
由于结构返回时,要复制结构值给一个临时结构变量(见9.6节),当结构很大时,运行效率会受影响。可以用一种效率更高的结构参数引用传递的方法来代替。结构参数引用传递时无须复制结构值,连赋值操作都不需要了。
-
结构可以嵌套,即结构中可以包含结构成员。
-
结构不可以递归嵌套,即结构成员不能是自身的结构变量,但可以用自身结构指针作为成员。
-
name成员含有结构中的实际信息,pN成员是指向另一个List的指针。这种结点(结构的实例)通过每个List的pN成员链接起来,能用于构造任意长的结构链,这样的结构链称为链表。链表中的每个List结构变量称为结点。
第二部分 面向对象程序设计
第11章 类
-
学习本章后,要求掌握声明和定义类和成员函数的方法,掌握访问成员函数的方法,理解保护数据屏蔽外部访问的原理,使得对类的封装有更好的认识。
-
C的结构不含成员函数。C++的类既能包含数据成员(data member),又能包含函数成员或称成员函数(member function)。
-
关键字class表示类,Savings是类名,一般首字符用大写字母表示,以示与对象名的区别。关键字public和protected(或private)表示访问控制。
-
在类中说明的,要么是数据成员,要么是成员函数。它们或者说明为public的,或者说明为protected的,或者说明为private的。
-
而C++中,默认情况下类(class)定义中的成员是private的。
-
结构在C中不允许有成员函数,而在C++中可以有成员函数。
-
在面向对象中,算法与数据结构被捆绑成一个类,从这样的角度看问题,就不用为如何实现通盘的程序功能而费尽心机了。现实世界本身就是一个对象的世界,任何对象都具有一定的属性与操作,也就总能用数据结构与算法二者合一地来描述。
-
类名Tdate的作用是指出Set(int,int,int)是Tdate的一个成员函数,而不是其他类的成员函数(如Teacher∷Set(int)),也不是普通函数。没有类名的函数也称为非成员函数
-
成员函数也叫方法(mathod),它多出现于面向对象方法论的叙述中。
-
∷叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。∷可以不跟类名,表示全局数据或全局函数(即非成员函数)。
-
类定义和其成员函数定义分开,是目前开发程序的通常做法。我们把类定义(头文件)看成类的外部接口,类的成员函数定义看成类的内部实现。
-
在类定义的外部定义成员函数,比在类内部定义时,成员函数名前多加上一个类名。如果该函数的前面没有用“类名∷”表达形式把它与该类紧紧连在一起,编译器就会认为该函数是一个普通函数,它只是与类中的成员函数有相同的名字罢了。以后在连接时,会查出缺少与成员函数相对应的定义而报错。
-
类名加在成员函数名之前而不是加在函数的返回类型前。
-
由于类名是成员函数名的一部分,所以一个类的成员函数与另一个类的成员函数即使同名,也不能认为是重载。
-
一个对象要表现其行为,就要调用它的成员函数。调用成员函数的形式类似于访问一个结构对象的分量,先指明对象,再指明分量。它必须指定对象和成员名,否则无意义。
-
对象可以由指针来引导。
-
用对象的引用来调用成员函数,看上去和使用对象自身的形式一样。
-
成员函数必须用对象来调用。另一方面,在成员函数内部,访问数据成员或成员函数无须如此。
-
原来,在对象调用s.Set(2,15,1998)时,成员函数除了接受3个实参外,还接受了一个对象s的地址。这个地址被一个隐含的形参this指针所获取,它等同于执行this=&s。所有对数据成员的访问都隐含地被加上前缀this->
-
因为成员函数是所有对象共享的代码,不是某一个对象所独占的,所以不能在成员函数内使用某个特定的对象。在编译期间,s对象由于没有在成员函数内部或文件作用域中声明而导致失败。
-
一个类对象所占据的内存空间由它的数据成员所占据的空间总和所决定。类的成员函数不占据对象的内存空间。
-
可以把类的成员声明作为私有的(private),使外部不能访问它们而起到保护作用。一个类定义,如果不写访问控制说明符(public、protected、private),那么它就默认为private。
-
protected和private的区别,在类的继承中才表现出来
-
编制应用程序,想要使用某个类,所要了解的全部内容是它的公共成员,它们有什么用,参数是什么
-
math.h是C库函数头文件,专门描述数学函数,而cmath则是对应C++的数学函数头文件,其为math.h的改造版,使之适应C++编程,例如允许函数重载等。此处包含cmath和math.h等价。
-
由于类很好地屏蔽了内部数据表示,所以由类负责的内部实现上的维护不影响应用程序的开发,类编程与应用编程作了分工,职责明确,使得编程工作可以模块化运作,这大大减轻了开发应用程序的强度。
-
一个类的所有成员位于这个类的作用域内,一个类的任何成员都能访问同一类的任一其他成员。C++认为一个类的全部成员都是一个整体的相关部分。
-
类作用域是指类定义和相应的成员函数定义范围。在该范围内,一个类的成员函数对同一类的数据成员具有无限制的访问权。
-
类X的数据成员m的作用域尽管在类X中,但是成员函数中定义了同名的局部作用域变量后,就把数据成员m给隐藏了。
-
一个类名被在后面的函数中的形参所覆盖,在该函数内,要定义一个类对象,则加上class即可
-
在函数中定义的类称为局部类,局部类在面向对象程序设计中并不多见。类作为类型也有作用域,局部类的作用域在定义该类的函数块中。
-
局部类的成员函数必须在类定义内部定义,因为若在类外部和包含该类的函数内部中定义,则导致在函数内部定义函数的矛盾。如果在包含类的函数外部定义,则该局部类无法与其取得联系。
-
非类型名(变量名、常量名、函数名、对象名或枚举成员)不能重名
-
在一个作用域中,一个名字可以声明为一个类型,也可以声明为一个非类型。当二者同时登场时,类型名要加前缀,以区别非类型名。
-
数据与算法(操作)结合,构成一个不可分割的整体(对象)。其次是,在这个整体中一些成员是保护的,它们被有效地屏蔽,以防外界的干扰和误操作;另一些成员是公共的,它们作为接口提供给外界使用。
-
将类的描述分成类的外部接口和类的内部实现就是下面两个文件的代码
-
在point.h头文件中,是一个类定义。其中保护数据成员并不是外部接口部分,但作为类定义的整体(从开括号起到闭括号止)描述,只好把它放在头文件中。对于面向对象程序设计之严格的内外界面描述来说,这是C++类机制中无可奈何的一面。但它不妨碍类的外部接口的有效性。
-
其中,class.cpp's表示多个类成员函数定义的源文件;function.cpp's表示多个函数定义的源文件。
-
类定义以头文件的方式提供,成员函数定义则以一定的计算机硬件或操作系统为背景而编译实现的内部代码方式提供。
-
自定义类库头文件称为类库设计。而在main()函数开始之后的面向对象程序设计,则显得相对自然和简洁。往往是先定义若干对象,然后调用其成员函数,由成员函数来完成程序员所规定的操作(即让对象表现自己)。
第12章 构造函数
-
C++的构造函数和析构函数使类对象能够轻灵地被创建和撤销。构造函数创建类对象,初始化其成员;析构函数撤销类对象。
-
学习本章后,要求理解类与对象的区别,掌握定义构造函数和析构函数的方法,把握默认构造函数的意义,了解类成员初始化的问题,掌握构造类成员的方法。
-
一个类描述一类事物,描述这些事物所应共同具有的属性,如人有身高、体重、文化程度、性别、年龄、民族等。
-
一个对象是类的一个实例,它具有确定的属性,如张三(人的实例)身高180cm,体重70kg,大学本科,男,21岁,汉族。
-
人类只有一个,人类的实例可以有无数个。对象可以被创建和销毁,但类是无所不在的。
-
全局对象在主函数开始执行前先被建立,局部对象在程序执行遇到它们的对象定义时才被建立。与定义变量类似,定义对象时,C++为其分配内存空间。
-
对象的意义表达了现实世界的实体,因此,一旦建立对象,需要有一个有意义的初始值。C++建立和初始化对象的过程专门由该类的构造函数来完成。这个构造函数很特殊,只要对象建立,它马上被调用,给对象分配内存空间和初始化。
-
C++另有一种析构函数,它也是类的成员函数,当对象撤销时,就会马上被调用,其作用是释放与对象捆绑的资源,做一些善后处理
-
我们要求建立对象的同时自动调用构造函数,省去人为调用的麻烦。
-
**类只有一个名字,但可以有多个对象名,每个对象创建时,都要调用该类的构造函数。**类的唯一性和对象的多样性,使我们马上想到用类名而不是对象名来作为构造函数名是比较合适的。
-
与成员函数相同,构造函数可以放在类的外部定义。
-
构造函数另一个特殊之处是它没有返回类型,函数体中也不允许返回值,但可以有无值返回语句“return;”。因为构造函数专门用于创建对象并为其初始化,所以它不能随意被调用。没有返回类型,正显得它与众不同。
-
如果一个类对象是另一个类的数据成员,则在创建那个类的对象所调用的构造函数中,对该成员(对象)自动调用其构造函数。
#include<iostream> using namespace std; class Student { private: int someHours; float gpa; public: Student(){ cout << "Constructing student.\n"; someHours = 100; gpa = 3.5; } ~Student(){ cout << "Destroying student.\n"; } }; class Teacher{ public: Teacher(){ cout << "Constructing teacher.\n"; } ~Teacher(){ cout << "Destroying teacher.\n"; } }; class TutorPair{ public: TutorPair(){ cout << "Constructing tutorPair.\n"; } ~TutorPair(){ cout << "Destroying tutorPair.\n"; } private: Student student; Teacher teacher; int noMeetings; }; int main(){ TutorPair tp; cout << "Back to Main!\n"; } /* Constructing student. Constructing teacher. Constructing tutorPair. Back to Main! Destroying tutorPair. Destroying teacher. Destroying student. */
该构造启动时,首先分配对象空间(包含一个Student对象、一个Teacher对象和一个int型数据),然后根据在类中声明的对象成员的次序依次调用其构造函数。这里先调用Student()构造函数,再调用Teacher()构造函数,最后才执行它自己的构造函数的函数体。按照这个顺序,分别产生运行结果的第一、第二、第三行输出。当执行完TutorPair()构造函数后,控制权回到主函数中,产生第四行输出。
-
这个例子告诉我们,类在工作过程中分工十分明确,每个类只负责初始化它自己的对象。当TutorPair类要初始化成员Student类对象的时候,马上调用Student构造函数,而不是由自己去包办
-
一个类可能在构造函数里分配资源,这些资源需要在对象不复存在前被释放。例如,如果构造函数打开了一个文件,文件就需要被关闭;或者,如果构造函数从堆中分配了内存,这块内存在对象消失之前必须被释放。析构函数允许类自动完成这些清理工作,不必调用其他成员函数。
-
**析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载。**它只是在类对象生命期结束的时候,由系统自动调用。在12.5节和12.6节将会看到,构造函数不同于析构函数,它可以有参数,可以重载。
-
析构函数名就在构造函数名前加上一个逻辑非运算符“~”,表示“逆构造函数”。
-
析构函数以调用构造函数相反的顺序被调用。
-
当主函数运行到结束的大括号处时,析构函数依次被调用,其调用顺序正好与构造函数相反。
-
构造函数可以被重载,C++根据声明中的参数选择合适的构造函数。
-
无参的构造函数被称为默认构造函数。
-
由于构造函数用于创建对象,所以调用它来给对象赋值是错误的。
-
显式调用构造函数将创建一个无名对象
-
要想共享初始化的过程,可以先定义一个共享成员函数,然后每个构造函数都调用之
-
还可以通过给最后一个构造函数以参数默认值,例如设定最后一个构造函数的3个参数的默认值为12、31、2003,那么参数不同的4种调用都能匹配该函数,从而使这4个构造函数结合成一个。
-
只要一个类定义了一个构造函数(不一定是无参构造函数),C++就不再提供默认的构造函数。也就是说,如果为类定义了一个带参数的构造函数,还想要无参构造函数,则必须自己定义。
-
与变量定义类似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的位模式全为0,否则,对象值是随机的。
-
“Student s("Randy");”语句执行步骤如下:
(1)分配s对象空间,调用Student构造函数。
(2)建立s对象空间中的结构,第一为name[20],第二为id。因id属于StudentID类,于是创建过程启动了调用StudentID的构造函数,这时,id的保护数据value得到了赋值,全局变量nextStudentID也得到了赋值,并且输出第一行信息。之后,返回到Student构造函数。
(3)执行Student构造函数体。输出第二行信息,数据成员name得到了赋值。之后返回到主函数main()。 -
类是一个抽象的概念,并不是一个实体,并不含有属性值,而只有对象才占有一定的空间,含有明确的属性值。
-
我们需要一个机制来表示“构造已分配了空间的对象成员,而不是创建一个新对象成员”。该机制应在建立对象空间的结构时反映出来,即需要出现在函数调用刚刚转入之时,函数体执行(开括号)之前。
#include<iostream> #include<cstring> using namespace std; class StudentID{ public: StudentID(int id = 0){ value = id; cout << "Assigning student id " << value << endl; } ~StudentID(){ cout << "Destroying id " << value << endl; } private: int value; }; class Student{ public: Student(char* pName = "no name", int ssID = 0) :id(ssID){ cout << "Constructing student " << pName << endl; strncpy(name, pName, sizeof(name)); name[sizeof(name)-1] = '\0'; } private: char name[20]; StudentID id; }; int main(){ Student s("Randy", 9818); Student t("Jenny"); }
**在Student构造函数头的后面,冒号表示后面要对类的数据成员的构造函数进行调用。**id(ssID)表示调用以ssID为实参的StudentID构造函数。
-
冒号语法使得常量数据成员和引用数据成员的初始化成为可能。
-
在SillyClass类的构造函数进入之后(开始执行大括号后的函数体语句时),对象结构已经建立,数据成员ten和refI已经存在,所以再在构造函数体内对常量赋值或对引用变量指派就不是初始化了
-
常量和引用变量的初始化必须放在构造函数正在建立数据成员结构空间的时候,也就是放在构造函数的冒号后面。
-
对于类的数据成员是一般变量的情况,则放在冒号后面与放在函数体中初始化都一样。
-
创建对象的唯一途径是调用构造函数。构造函数是一段程序,所以构造对象的先后顺序不同,直接影响程序执行的先后顺序,导致不同的运行结果。
-
局部和静态对象是指块作用域和文件作用域中的对象。它们声明的顺序与它们在程序中出现的顺序是一致的。
-
静态对象和静态变量一样,文件作用域中的静态对象在主函数开始运行前全部构造完毕。块作用域中的静态对象,则在首次进入定义该静态对象的函数时,进行构造。
-
和全局变量一样,所有全局对象在主函数开始运行之前,全部已被构造。
-
有两种方法可以解决这个问题:一是将全局对象先作为局部对象来调试;二是在所有怀疑有错的构造函数的开头增加输出语句,这样在程序开始调试时,可以看到来自这些对象的输出信息。
-
对于简单应用的单文件程序来说,全局对象可以按照它们出现的顺序依次进行构造。但是,单文件程序只是出现在实验室或教室里,真正有用的程序都是由多个文件组成的,这些文件被分别编译、连接。因为编译器不能控制文件的连接顺序,所以它不能决定不同文件中全局对象之间的构造顺序。
-
不要允许一个全局对象访问另一个全局对象。
-
由于按成员在类定义中的声明顺序进行构造,而不是按构造函数说明中冒号后面的顺序,所以num成员被赋的是一个随机值,并不是想赋的16,因为这时候,成员age还没有被赋值,age的内存空间中是一个随机值(此次运行,其值为0)。
第13章 面向对象程序设计
-
C++区别于C的特征是C++支持面向对象程序设计。在知道了C++中如何创建类后,必须搞清什么是面向对象程序设计,类适用于现实世界中的哪些问题,才能真正进行面向对象的思考和编程。
-
层层分类,使概念逐渐细化,即具体化。相反,归类的结果,便是逐步抽象的过程。
-
在面向对象的计算机世界中,我们把一辆实实在在的桑塔纳小轿车称作类(class)桑塔纳的一个实例(instance)或者说是对象(object)。
-
在面向对象的程序设计中,对象被分成类。类又是层层分解的,这些类与子类的关系可以被规格化地描述。描述了类,再描述其子类,就可以只描述其增加的部分。所有子类层次上的编程,都只需在已有的类的基础上进行。
-
在面向对象的分析和设计中,我们执行下面的步骤:
(1)找出类;
(2)描述类和类之间的联系;
(3)用类来定义程序结构。 -
一般来说,有一个数据属性,就有该数据属性的访问操作。每个数据成员和成员函数的取名也是一个环节,应该使用易懂的组合单词,没有用的名字应删掉。
-
面向对象程序设计使用户既不需要懂计算机太多,也不需要懂业务太多。
-
面向对象程序设计可以将程序员分成两类:
一类是面向对象应用程序设计。他(她)们无须了解类的实现细节,就像使用微波炉那样,这要比结构化程序设计简单得多。
另一类是类库设计。他(她)们为面向对象程序设计提供“素材”。这些素材涉及各个领域,由各领域的专业人员来设计完成。他(她)们需要了解特定类的知识,如Josephus问题的解答。 -
**类定义是面向对象程序设计中的基础问题,对象定义是面向对象程序设计的一般操作。**定义类的过程要求了解问题中的组织思想和弄清本质。
类是很容易想象的,如果选择了一个错误的类,则描述起来很困难;如果是正确的类,则将很容易理解它,并清楚地描述出其成员函数和数据成员
第14章 堆与拷贝构造函数
-
在C++中,堆分配的概念得到了扩展,不仅C++的关键字new和delete可以分配和释放堆空间,而且通过new建立的对象要调用构造函数,通过delete删除对象也要调用析构函数。
-
应该掌握new和delete这两个操作符的使用,并能把握从堆中分配和释放对象以及使用对象数组的时机;领会拷贝构造函数的实质,区别浅拷贝和深拷贝,在程序中适当地运用拷贝构造函数。
-
C++程序的内存格局通常分为4个区:
(1)全局数据区(data area);
(2)代码区(code area);
(3)堆区(即自由存储区)(heap area);
(4)栈区(stack area)。 -
全局变量、静态数据、常量及字面量存放在全局数据区,所有类成员函数和非成员函数代码存放在代码区,为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区,余下的空间都被作为堆区。
-
作符new和delete是C++语言的一部分,无须包含头文件。它们都从堆中分配和释放内存块
-
**操作堆内存时,如果分配了内存,就有责任回收它,否则运行的程序将会造成内存泄漏。**这与函数在栈区分配局部变量有本质的不同。
-
从C++的立场上看,不能用malloc()函数的一个原因是,它在分配空间的时候不能调用构造函数。类对象的建立是分配空间、构造结构以及初始化的三位一体,它们统一由构造函数来完成。
-
然而malloc()仅仅只是一个函数调用,它没有足够的信息来调用一个构造函数,它所接受的参数是一个unsigned long类型
-
不必显式指出从new返回的指针类型,因为new知道要分配对象的类型是Tdate。而且new还必须知道对象的类型,因为它要藉此调用构造函数。
-
“pD=new Tdate(1,1,1998);”这一语句,使new去调用了构造函数Tdate(int,int,int),new是根据参数匹配的原则来调用构造函数的。
-
分配过程将激发noOfObjects次构造函数的调用,从0至noOfObjects-1。调用构造函数的顺序依次为pS[0],pS[1],pS[2],…pS[noOfObjects-1]。由于分配数组时,new的格式是类型后面跟[元素个数],不能再跟构造函数参数,所以,从堆上分配对象数组,只能调用默认的构造函数,不能调用其他任何构造函数。
-
delete[]pS中的[]是要告诉C++,该指针指向的是一个数组。如果在[]中填上了数组的长度信息,C++编译系统将忽略,并把它作为[]对待。但如果忘了写[],则程序将会产生运行错误。
-
堆空间相对其他内存空间比较空闲,随要随拿,给程序运行带来了较大的自由度。使用堆空间往往由于:
(1)直到运行时才能知道需要多少对象空间;
(2)不知道对象的生存期到底有多长;
(3)直到运行时才知道一个对象需要多少内存空间。 -
可用一个对象去构造另一个对象,或者说,用另一个对象值初始化一个新构造的对象
-
对象作为函数参数传递时,也要涉及对象的拷贝。函数fn()的参数传递的方式是传值,参数类型是Student,调用时,实参ms传给了形参fs,ms在传递的过程中是不会改变的,形参fs是ms的一个拷贝。这一切是在调用的开始完成的,也就是说,形参fs用ms的值进行构造。
-
类定义中,如果未提供自己的拷贝构造函数,则C++提供一个默认拷贝构造函数,就像没有提供构造函数时,C++提供默认构造函数一样。
-
在默认拷贝构造函数中,拷贝的策略是逐个成员依次拷贝。但是,一个类可能会拥有资源,当其构造函数分配了一个资源(例如堆内存)的时候,会发生什么呢?如果拷贝构造函数简单地制作了一个该对象的拷贝,而不对它本身进行资源分配和复制,就得面临一个麻烦的局面:两个对象都拥有同一个资源。当对象析构时,该资源将经历两次资源返还。
-
创建p2时,对象p1被复制给了p2,但资源并未复制,因此,p1和p2指向同一个资源,这称为浅拷贝。
-
当一个对象创建时,分配了资源,这时,就需要定义自己的拷贝构造函数,使之不但拷贝成员,也分配和拷贝资源。
-
创建p2时,对象p1被复制给了p2,同时资源也作了复制,因此,p1和p2指向不同的资源,这称为深拷贝。
-
**堆内存并不是唯一需要拷贝构造函数的资源,但它是最常用的一个。**打开文件,占有硬设备(例如打印机)服务等也需要深拷贝。它们也是析构函数必须返还的资源类型。因此,一个很好的经验是:如果你的类需要析构函数来析构资源,则它也需要一个拷贝构造函数。
-
一般规定,创建的临时对象,在整个创建它们的外部表达式范围内有效,否则无效。也就是说,“s=fn();”这个外部表达式,当fn()返回时产生的临时对象拷贝给s后,临时对象就析构了。
-
因为外部表达式“Student&refs=fn();”到分号处结束,以后从fn()返回的临时对象便不再有效,这就意味着引用refs的实体已不存在,所以接下去的任何对refs的引用都是错的。
-
可以直接调用构造函数产生无名对象。
-
无名对象可以作为实参传递给函数,还可以拿来拷贝构造一个新对象,也可以初始化一个引用的声明。
-
因为有Student(char*)的构造函数,又有fn(Student&s)函数,于是,fn("Jenny")便被认为是fn(Student("Jenny")),最终予以匹配。把构造函数用来从一种类型转换为另一种类型,这是C++从类机制中获得的附加性能
第15章 静态成员与友元
-
将所要共享的数据声明为static便能在类范围中共享,声明为static的类成员称为静态成员。友元函数完全是普通的C++函数,不同的是,它可以访问类的保护成员或私有成员,这样既方便编程,也提高了效率,但破坏了类的封装性。
-
类成员有数据成员和成员函数之分,静态成员也有静态数据成员和静态成员函数之分。静态成员用static声明。
-
但无论对象有多少,甚至没有,静态成员noOfStudents也只有一个。所有Student对象都能共享它,并且能够访问它。
-
静态数据成员是类的一部分,静态数据成员的定义是类定义的一部分,将其放在类的内部实现部分中定义是再合适不过了。定义时要用类名引导。重用该类时,简单地包含其头文件便可。
-
在类的外部,访问静态数据成员的形式可以是s1.noOfStudents,它等价于s2.noOfStudents,更通常的用法是Student∷noOfStudents(不能用Student.noOfStudents)。其意义是,静态数据成员是属于Student类的,而不是属于哪个特定对象的,它也不需要依赖某个特定对象的数据。
-
静态数据成员用得比较多的场合一般为:
(1)用来保存流动变化的对象个数(如noOfStudents);
(2)作为一个标志,指示一个特定的动作是否发生(如可能创建几个对象,每个对象要对某个磁盘文件进行写操作,但显然在同一时间里只允许一个对象写文件,在这种情况下,用户希望说明一个静态数据成员指出文件何时正在使用,何时处于空闲状态);
(3)一个指向链表第一个成员或最后一个成员的指针(如pFirst)。 -
**静态成员函数定义在类的内部实现,属于类定义的一部分。**它的定义位置与一般成员函数一样。
-
一个静态成员函数不与任何对象相联系,故不能对非静态成员进行默认访问。
-
它们的根本区别在于静态成员函数没有this指针,而非静态成员函数有一个指向当前对象的指针this。
-
在函数内部,Sc∷nsfn()对非静态成员的访问将自动地把this参数作为指向当前对象的指针,而当Sc∷sfn()被调用时,没有任何对象的地址被传递。因此,当访问非静态成员时,无this指针(出错)。这就是为什么一个静态成员函数与任何当前对象都无联系的原因。
-
在类里声明一个普通函数,标上关键字friend,就成了该类的友元,可以访问该类的一切成员。
-
友元函数不是成员函数,它是类的朋友,因而能够访问类的全部成员。在类的内部,只能声明它的函数原型,加上friend关键字。友元声明的位置可在类内部的任何位置,既可在public区,也可在protected区,意义完全一样。友元函数定义则在类的外部,一般与类的成员函数定义放在一起。
-
一个类的成员函数可以是另一个类的友元。
-
不能在类名声明“class Student;”后声明Student类之前,出现定义类对象语句:Student ss;)。
-
**整个类可以是另一个类的友元,该友元称为友类。**友类的每个成员函数都可访问另一个类中的保护或私有数据成员。
-
使用静态数据成员,实际上可以消灭全局变量。全局变量给面向对象程序带来的问题就是违背封装原则。使用静态数据成员必须在main()程序运行之前分配空间和初始化。
第16章 继承
-
我们也可以为一个派生类指定多个基类,这样的继承结构被称为多重继承或多继承。应能理解多继承的工作原理,了解多继承要解决的问题,认识虚拟继承的实质,把握多继承的方法,并能简单地从多个基类中派生出新类。
-
面向对象程序设计可以让你声明一个新类作为另一个类的派生。**派生类(也称子类)继承它父类的属性和操作。子类也声明了新的属性和新的操作,剔除了那些不适合于其用途的继承下来的操作。**这样,继承可让你重用父类的代码,专注于为子类编写新代码。
-
继承的方式是在类定义中类名后跟:public Student。一个研究生是一个大学生,当然,研究生类GraduateStudent也包含有自己特有的成员。
-
在布局上gs对象的最初部分是Student数据成员,fn(gs)等于做了一个Student(gs)的隐式转换,指向gs的this指针也就是指向Student对象的指针。由此看出,gs对象中包含有Student对象空间部分,gs用this指针访问Student成员与访问自己增加的成员没有差别。
-
根据类的实现机制,派生类对象创建时,将执行其默认的构造函数。该默认构造函数会首先调用基类的默认构造函数,而基类没有默认构造函数,但正好匹配默认参数的构造函数。所以在运行结果中,gs对象的name值为“no name”。
-
在构造一个子类时,完成其基类部分的构造由基类的构造函数去做,C++类的继承机制满意地提供了这种支持。
-
在构造函数原型的后面是Student(pName),advisor(adv),表示对数据成员初始化的方式。
-
基类初始化由Student(pName)去完成,派生类的构造总是由基类的初始化开始的。
-
派生类的析构函数以构造函数相反的顺序被调用,所以函数fn()返回时,系统开始处理gs对象的析构,先调用GraduateStudent的析构函数,执行析构函数体的代码,然后调用Advisor析构函数,最后调用Student析构函数。
-
参数advisor与gs对象中的数据成员advisor处在两个不同的对象空间,所以当fn()函数返回时,析构gs之后,并没有把函数fn()的形参advisor析构掉,由于该形参是传引用方式,所以fn()返回时只是解除引用关系,对其不做其他处理。
-
advisor形参就是main()函数中的Advisor对象da。一直到main()函数结束时,才由Advisor析构函数将da析构。
-
一个类作为基类时,对于使用基类的应用编程,其在类作用域之外,只能访问基类的公有成员,不能访问基类的保护成员和私有成员;对于公有继承基类的类编程,其在派生类作用域中,只能访问基类的公有成员和保护成员,不能访问基类的私有成员。这便是保护成员与私有成员的区别。
-
保护成员专为继承而设。
-
如果是公共继承,将使基类作为开源代码不断让类程序员派生代码从而迅速传播。
如果是保护继承,那么将使基类作为公司内部技术不断接续和派生。
如果是私有继承,那么将使技术世代单传,开发者在其派生类中也只能通过调用基类的成员函数来访问基类。 -
一个私有的或保护的派生类不是子类,因为非公共的派生类不能做基类能做的所有事。
-
Giraffe类私有继承了Animal类,意味着对象gir不能直接访问Animal类的成员。其实,在gir对象空间中,包含Animal类的对象,只是无法让其公开访问。
#include<iostream> using namespace std; class Animal{ public: Animal() {} void eat(){cout << "Eat.\n";} }; class Giraffe : private Animal { public: Giraffe(){} void StretchNeck(){ cout << "stretch neck.\n";} void take() { eat(); } }; void Func(Giraffe& an){ an.take(); } int main() { Giraffe gir; gir.StretchNeck(); // gir.eat(); // error: 'Animal' is not an accessible base of 'Giraffe' Func(gir); } /* stretch neck. Eat. */
-
私有继承就像是离家出走的小孩,一个人在外面飘泊。他(她)不能拥有父母的住房和财产(如gir.eat()是非法的),在外面自然也就不能代表其父母,甚至他(她)不算是其父母的小孩(如Func(gir)调用被禁)。但是在他(她)的身体中,毕竟流淌着父母的血液(在gir对象空间中,含有Animal对象),所以,在小孩自己的行为中又有着与父母相似的成分(可以通过自身成员函数访问父类私有成员)。
-
保护继承与私有继承类似,继承之后的类相对于基类来说是独立的,保护继承的类对象,在公开场合同样不能使用基类的成员
-
公有继承将基类的保护成员和公有成员视为自己的保护和公有成员。
保护继承将基类的保护成员和公有成员全变为自己的保护成员。
私有继承将基类的保护成员和公有成员全变为自己的私有成员。 -
基类的私有成员在派生类采用任何继承方式下都是隔离的,也就是视派生类为“外人”,必须通过基类的保护成员或公有成员函数去访问它们。
基类的公有成员在派生类中的访问属性随继承方式而定。即公有继承下为公有成员,保护继承下为保护成员,私有继承下为私有成员。 -
调整访问控制属性的前提是在派生类中该成员必须是可见的。
-
类以另一个类对象作数据成员,称为组合。
-
GraduateStudent有一个Advisor;而在继承的场合,称GraduateStudent是一个Student。
-
**采用继承方式还是组合方式,要看类层次的描述,需要分析对象之间的关系。**例如,汽车与马达的关系,更多的是包含关系,所以应该设计成汽车组合马达更合适。而小轿车作为车辆的一种,更多的是分类关系,所以应该设计成车辆派生小轿车更合适。而在技术上,无论组合设计还是继承设计,都能实现所需功能。
-
成员构造和基类构造的不同在于,前者是以成员的名义去初始化,后者是调用基类构造函数,完成基类对象构造。
-
但是一些类却代表两个类的合成。例如,两用沙发,它是一个沙发,也是一张床,两用沙发应允许同时继承沙发和床的特征,即SleeperSofa继承Bed和Sofa两个类,
-
在编写应用程序时,程序员还得额外知道类的层次信息,加大了复杂度。这些在单继承中是不会出现的。
// ch16_5.cpp 多继承 #include<iostream> using namespace std; class Bed{ public: Bed(): weight(0){} void Sleep(){ cout<<"Sleeping...\n";} void SetWeight(int i){ weight = i;} protected: int weight; }; class Sofa{ public: Sofa() : weight(0){} void WatchTV(){ cout<<"Watching TV.\n";} void SetWeight(int i){ weight=i; } protected: int weight; }; class SleeperSofa : public Bed, public Sofa{ public: SleeperSofa(){} void FoldOut(){ cout<<"Fold out the sofa.\n"; } }; int main() { SleeperSofa ss; ss.WatchTV(); ss.Sleep(); ss.FoldOut(); ss.Sofa::SetWeight(10); }
-
SleeperSofa只需一个Furniture,所以我们希望它只含一个Furniture拷贝,同时又要共享Bed和Sofa的成员函数与数据成员,C++实现这种继承结构的方法称为虚拟继承(virtual inheritance)。
-
在Bed和Sofa继承Furniture中加上virtual关键字,这相当于说,“如果还没有Furniture类,则加入一个Furniture拷贝,否则就用已有的那一个。”此时一个SleeperSofa在内存中的布局见图16-7。
// ch16_6.cpp 虚拟继承 #include<iostream> using namespace std; class Furniture{ public: Furniture(){} void SetWeight(int i) { weight=i; } int GetWeight() const { cout<<weight<<endl;return weight; } protected: int weight; }; class Bed : virtual public Furniture{ public: Bed(){} void Sleep(){ cout<<"Sleeping..."<<endl;} }; class Sofa : virtual public Furniture{ public: Sofa(){} void WatchTV(){ cout<<"Watching TV..."<<endl;} }; class SleeperSofa : public Bed, public Sofa{ public: SleeperSofa() : Sofa(), Bed(){} void FoldOut(){ cout<<"Fold out the sofa\n"; } }; int main() { SleeperSofa ss; ss.SetWeight(20); ss.GetWeight(); ss.Sleep(); ss.WatchTV(); }
-
在虚拟继承的情况下,应用程序main()中引用GetWeight()不再模糊,我们得到了真正的图16-4所示的继承关系。虚拟继承的虚拟和虚拟函数的虚拟没有任何关系。
-
构造对象的规则需要扩展以控制多重继承。构造函数按下列顺序被调用:
// ch16_7.cpp 多继承的构造顺序 /* 构造函数按照下列顺序被调用: (1)任何虚拟基类的构造函数按照它们被继承的顺序构造; (2)任何非虚拟积累的构造函数按照它们被继承的顺序构造; (3)任何成员对象的构造函数按照它们声明的顺序调用; (4)类自己的构造函数。 */ #include<iostream> using namespace std; class OBJ1{ public: OBJ1(){ cout<<"OBJ1\n"; } }; class OBJ2{ public: OBJ2(){ cout<<"OBJ2\n"; } }; class B1{ public: B1(){ cout<<"B1\n"; } }; class B2{ public: B2(){ cout<<"B2\n"; } }; class B3{ public: B3(){ cout<<"B3\n"; } }; class B4{ public: B4(){ cout<<"B4\n"; } }; class Derived : public B1, virtual public B2, public B3, virtual public B4 { public: Derived() : B4(), B3(), B1(), obj2(), obj1() { cout<<"Derived ok.\n"; } protected: OBJ1 obj1; OBJ2 obj2; }; int main(){ Derived d; cout<<"This is ok!\n"; } /* B2 B4 B1 B3 OBJ1 OBJ2 Derived ok. This is ok! */
-
Derived的虚基类Base2和Base4最先构造,尽管它在Derived类中出现的顺序不在最前面;Derived的非虚基类其次构造,不管它在Derived构造函数中出现的顺序如何;Derived的组合对象obj1和obj2随后构造,它以类定义时数据成员排列顺序为准,不管在Derived构造函数中出现顺序怎样;最后是Derived类构造函数本身。
-
**单个继承提供了足够强大的功能,不一定非用多重继承不可。**我们应先学会阅读一些商品化的类库源程序中有关多重继承的部分,因为那些都是经过测试的安全代码。
第17章 多态
-
理解多态性对于继承的意义,掌握多态的工作原理,理解真正的多态是要维护类编程的独立性,不干扰应用编程;理解抽象类和具体类的区别,特别是看到多态支持抽象类时,对抽象编程的理解,学会运用纯虚函数。
-
C++允许子类的成员函数重载基类的成员函数
-
研究生对象gs调用其学费计算函数时,两个重载函数都在它自己可使用范围内,C++编译规定:gs.calcTuition()指的是GraduateStudent∷calcTuition()。若派生类没有定义其calcTuition(),则gs.calcTuition()才指的是基类Student∷calcTuition()。
-
C++的继承机制中用一种称为多态性(polymorphism)的技术来解决上面的问题。这种在运行时,能依据其类型确认调用哪个函数的能力,称为多态性,或称迟后联编(late binding),也有的译为滞后联编。
-
编译时就能确定哪个重载函数被调用的,称为先期联编(early binding)
-
例如,一个人对另一个人吆喝“开门”,彼此都处于同一场景中,明白是开机舱门还是开冰箱门等等。
-
为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。
-
由于fn()标志为虚函数,编译看见b.fn()后,将其作为迟后联编来处理,以保证在运行时确定调用哪个fn()虚函数。
编译通常是在先期联编状态下工作的,只有看见虚函数,才把它作为迟后联编来实现。 -
fn()在基类中声明为virtual,该虚函数的性质自动地向下带给其子类,所以SubClass子类中virtual可以省略。
-
多态性让类的设计者更多地去考虑工作的细节,而且这个细节简单,就是在成员函数前加一个virtual关键字。多态性使应用程序代码极大地简化,它是开启继承能力的钥匙。
-
如果虚函数在基类与子类中仅仅是名字相同,但参数类型不同,或返回类型不同,这样即使写上了virtual关键字,系统也不进行迟后联编。
-
而对于传自SubClass类对象的引用b,编译看见b.fn(i)的调用,分析到fn(i)虽然是多态函数,但是SubClass却没有自己的版本,对于首要的精准匹配性,自然只能调用Base::fn(int)了,所以也就无须迟后联编了。
-
上述程序的编译分析与运行结果,关键是因为不恰当的虚函数没有构成多态,使编译看作为先期联编。有一种例外,如果基类中的虚函数返回一个基类指针或返回一个基类的引用,子类中的虚函数返回一个子类的指针或子类的引用,则C++将其视为同名虚函数并进行迟后联编。
-
一个类中将所有的成员函数都尽可能地设置为虚函数对编程固然方便,Java语言中正是这样做的,但是会增加一些时空上的开销。C++是在性能上有偏激追求的编程语言,只选择设置个别成员函数为虚函数。
-
只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象,所以普通函数不能说明为虚函数。
-
静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
-
内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的
-
构造函数不能是虚函数,因为构造时,对象还是一片未定型的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。
-
析构函数可以是虚函数,而且通常声明为虚函数。
-
pHeapObject是传递过来的一个对象指针,它或者指向基类对象,或者指向子类对象。在执行delete pHeapObject时,要调用析构函数,但是执行基类的析构函数?还是执行子类的析构函数?将析构函数声明为虚的,就可以解决这个问题。
-
继承给程序员在一个过程中从不同类里组合共有特征的能力,这个过程称为分解(factoring)。也就是把共同的特征分解到基类中。
-
一个程序员努力工作,写出比较巧妙的代码,以达到减少一些程序行的目的,是不值得的。这种巧妙常常会弄巧成拙。但是通过继承而分解出多余部分,可以合理地减少编程的工作量。
-
C++允许程序员声明一个不能有实例对象的类,这样的类唯一的用途是被继承。这种类称为抽象类(abstract class)。一个抽象类至少具有一个纯虚函数。所谓纯虚函数(pure virtual function)是指被标明为不具体实现的虚成员函数。
-
**在Withdrawal()的声明之后的“=0”表明程序员将不定义该函数。**该声明是为派生类而保留的位置。Account的派生类被期待用一个具体的函数来重载该函数。
-
一个抽象类不能有实例对象,即不能由该抽象类来制造一个对象。
-
抽象类是作为基类为其他类服务的。一个Account类包含一个银行账户的所有特征。可以通过继承Account来创建其他类型的银行账户类,但是Account自身不能有实例对象。
-
所有纯虚函数被重载之前,抽象类的子类也一直保持抽象状态。
-
不能创建一个抽象类的对象,但是可以声明一个抽象类的指针或引用。
-
在这里,函数func()的参数pA是一个Account指针。从otherFunc()函数调用func()时,传递的实参都是具有实际地址的子类对象。它们要么是Savings类对象,要么是Checking类对象,但绝不可能是Account类对象,因为在otherFunc()函数中,绝不可能创建Account对象。
-
C++是强类型语言。当访问一个成员函数时,C++坚持要证明该成员函数在类中存在,否则拒绝接受。
-
如果Withdrawal()是Account的成员函数,即使是纯虚函数,也能顺利通过编译。这正是纯虚函数为什么非要不可的原因所在。
纯虚函数是在基类中为子类保留的一个位置,以便子类用自己的实在函数定义来覆盖之。如果在基类中没有保留位置,则无法覆盖。 -
因为继承,所以有了类族对象,有了批量处理类族对象的需求。而多态便是专为处理类族对象而设。
-
显然,一个指针链表承载着一叠类族中的异类对象。但是它们的结点又同属一种Account*类型,使得循环(批量)处理异类对象成为可能。
而带有指针Account*参数的函数doBusiness()接应着来自同一类族的异类对象的指针传递,通过调用Withdrawal()和Display()函数,展现了多态。
第18章 运算符重载
-
运算符重载的目的是:使C++代码更直观,更易读,由简单的运算符构成的表达式常常比函数调用更简洁、易懂。学习本章后,应该理解怎样重定义与类有关的运算符,学会怎样把一个类对象转换为另一个类对象,能把握重载运算符的时机。
-
如果不定义运算符重载,则expense2()中principle*rate和principle+interest是非法的。因为参加运算的操作数是类对象而不是浮点值。
-
重载运算符时,要注意该重载运算符的运算顺序和优先级不变
-
运算符的操作数是规定好了的,例如,乘法和加法是双目运算符,++是单目运算符,等等。如果改变运算符的操作数个数,将带来编译器错误。
-
运算符是函数,除了运算顺序和优先级不能更改外,参数和返回类型是可以重新说明的,即可以重载
-
C++规定,运算符中参数说明都是内部类型时,不能重载。
-
C++还规定了点操作符(.)、域操作符(∷)、成员间访操作符(.)、成员指针操作符(->)、条件操作符(?:),这五个运算符不能重载,也不能创造新运算符。
-
不是必须要让operator+()执行加法,可以让它做任何事。但是不让它做加法,而做其他操作是一个很糟的想法。如果重载+运算符,向一个文件写10次“I like C++”,语法上可以,但与语义相差悬殊,不利于可读性,背离了允许运算符重载的初衷。当别人读这个程序时,发现s1+s2的操作,想象是某种加法操作,怎么也想不到会是这样的写操作。所以在使重载运算符脱离原义之前,必须保证有充分的理由。
-
如果只给出一个operator++()定义,那么它一般可用于前缀、后缀两种形式。即d3++与++d3不作区别。
-
对于operator+(),两个对象相加,不改变其中任一个对象。而且它必须生成一个结果对象来存放加法的结果,并将该结果对象以值的方式返回给调用者。
-
虽然它无编译问题,可以运行,但是该堆空间无法回收,因为没有指向该堆空间的指针,会导致内存泄漏,程序不断做加法时,堆空间也在不断流失。
如果坚持结果对象从堆中分配,而返回一个指针,那样在应用程序中就要付出代价: -
与operator+()不一样,operator++()确实修改了它的参数,而且其返回值要求是左值,这个条件决定了它不能以值返回。
-
可见函数体中内容几乎相同,只是非成员形式加s1和s2,成员形式s加当前对象,当前对象的成员隐含着由this指向。即yuan意味着this->yuan。
一个运算符成员形式,将比非成员形式少一个参数,左边参数是隐含的。 -
该变换可以是显式的,如s=RMB(1.5)+s那样,也可以是隐含的。此时,由于其中的一个操作数是RMB对象,而且参数个数相同,所以它首先假定operator+(RMB&,RMB&)可以匹配,然后寻找能够使用的转换。发现构造函数RMB(double)可作为转换的依据。在完成转换后,真正匹配operator+(RMB&,RMB&)运算符。所以程序员可以通过定义转换函数,来减少定义的运算符个数。
-
C++规定:=、()、[]、->这4种运算符必须为成员形式。
-
使用前增量时,对对象(操作数)进行增量修改,然后再返回该对象。所以前增量运算符操作时,参数与返回的是同一个对象。这与基本数据类型的前增量操作类似,返回的也是左值。
使用后增量时,必须在增量之前返回原有的对象值。为此,需要创建一个临时对象,存放原有的对象,以便对操作数(对象)进行增量修改时,保存最初的值。后增量操作返回的是原有对象值,不是原有对象,原有对象已经被增量修改,所以,返回的应该是存放原有对象值的临时对象。 -
前后增量操作的意义,决定了其不同的返回方式。前增量运算符返回引用,后增量运算符返回值。
-
后增量运算符中的参数int只是为了区别前增量与后增量,除此之外没有任何作用。因为定义中无须使用该参数,所以形参名在声明与定义中均省略
-
转换运算符将对象转换成类型名规定的类型
-
转换运算符与转换构造函数(简称转换函数)互逆。例如,RMB(double)转换构造函数将double转换为RMB,而RMB∷operator double()将RMB转换成double。
-
还要防止同一类型提供多个转换路径(转换的二义性),它会导致编译出错。
-
通常赋值运算符有两部分,第一部分与析构函数类似,在其中取消对象已经占用的资源。第二部分与拷贝构造函数类似,在其中分配新的资源。
-
对象t创建时,具有名字“temporary”,它在堆中存放。在t=s赋值过程中,通过调用deleteName(),原先名字占用的空间还给堆,再另外调用copyName()从堆中分配新存储区去存储新名字“claudette”。
-
如果赋值运算符说明为保护或私有的,则可以将赋值操作限定在类的作用域范围,防止应用程序中使用赋值操作:
-
因为Name类对象newN使得=匹配为Name类的赋值运算符,但是protected限定符使之不能在普通函数中被调用,从而防止了对象非法赋值操作。
-
如果在类中没有说明本身的拷贝构造函数和赋值运算符,编译程序将会提供,但它们都只是对对象进行成员浅拷贝。在那些数据成员是指向堆空间指针的类中,必须避免使用浅拷贝,而要为类定义自己的赋值运算符,以给对象分配堆内存。
-
this指针指向当前的对象,它是所有成员函数的不可见的参数,在重载运算符时,经常返回this指针的间接引用。
-
拷贝构造函数用已存在的对象创建一个相同的新对象。而赋值运算符用已存在的对象赋予一个已存在的同类对象。
第19章 I/O
-
学习了本章后,应该理解怎样使用C++面向对象的I/O流,能够格式化输入和输出,理解I/O流类的层次结构,理解怎样输入和输出用户自定义类型的对象,能够建立用户自定义的流操作符,能够确定流操作的成败,能够把输出流系到输入流上。
-
上面这些语句,用错了数据类型,而编译都能通过。为此,程序员将花更多的代价在程序运行中出现的错误诊断上。特别对于scanf()中的错误,往往是致命的。
-
printf()和scanf()却无能为力,它们既不能识别,也不能学会如何识别用户定义的对象
-
iostream是I/O流的标准头文件。
-
当程序测试并处理关键错误时,不希望程序的错误信息从屏幕显示重定向到其他地方,这时使用cerr流显示信息。
#include<iostream> using namespace std; void fn(int a, int b){ if(b==0) cerr << "Zero encoutered." << "The message cannot be redirected\n"; else cout << a/b << endl; } int main() { fn(20, 2); fn(20, 0); }
主函数第一次调用fn()函数时,没有碰到除0运算,得到文件的写内容10,第二次调用fn()函数时,碰到除0运算,于是在屏幕上输出错误信息。写到cerr上的信息是不能被重定向的,它只能输出到屏幕。
-
此处的文件名要说明其路径,斜杠要双写,因为编译器理解下的斜杠是转义字符。这与包含头文件时的路径不一样,因为包含头文件是由编译预处理器处理的。
-
假定程序中原来的有效位数设置不知道,“cout.precision(4)”可以返回原来设置的有效位数,保存该值在prePrecision变量中,使得最后用该值恢复原来的设置。
-
当程序使用cin输入时,cin用空白符和行结束符将各个值分开。但根据所需输入的值,可能需要读取一整行文本包括空白符。为了读取整行文本,可以使用getline成员函数。
-
程序中的X为大小写敏感的。一个小写X不会结束第一个cin.getline()的输入,而且,在输入X之前,可以按一到多次回车键,而并不结束第一个cin.getline()的输入。第一个cin.getline()的输入操作将以键入X后的第一个回车结束。
-
根据程序的输入要求,有时需要执行每次输入一个字符。这时,可以使用get()成员函数。
#include<iostream> #include<cctype> using namespace std; int main(){ char letter; while(!cin.eof()){ letter = cin.get(); letter = toupper(letter); if(letter == 'Y'){ cout << "'Y' be met."; break; } cout << letter; } }
-
使用流成员函数的输入操作不只限于键盘,上例程序可从重定向输入中每次读入一个字符。
-
cin≫letter将跳过在文件中发现的任何空白字符(空白字符指空格、tab符、backspace符和回车符)。而cin.get()则不跳过空白字符。
-
用get()成员函数的第二种形式可以输入一系列字符,直到输入流中出现结束符或所读字符个数已达到要求。
-
getline()和get()第二种形式相同。唯一的不同是getline()从输入流中输入一系列字符时包括分隔符,而get()不包括分隔符。
-
cout<<letter;与cout.put(letter);有一个区别,前者显示以该数据类型表示的形式,后者将参数值以字符方式显示。所以,若letter是char型,那么这两种方法都可以用来显示字母,若letter为int型,那么前者将在屏幕上显示65到90的数字,而不是字母A到Z。
#include<iostream> using namespace std; int main(){ for(char letter = 'A'; letter <= 'Z'; letter++){ cout.put(letter); } cout << "\n"; for(char letter = 'A'; letter <= 'Z'; letter++){ cout << letter; } }
-
左移运算符也称插入运算符,它比较形象,执行“cout≪x;”输出时,好像x被插入到输出设备上。重载插入运算符的特性使得流I/O可扩展,这与printf()是重要的区别。
#include<iostream> #include<iomanip> using namespace std; class RMB{ unsigned int yuan; unsigned int jf; public: RMB(double v = 0.0){ yuan = v; cout << "yuan: " << yuan << endl; jf = (v-yuan)*100.0+0.5; cout << "jf: " << jf << endl; } operator double(){ return yuan+jf/100.0; } void display(ostream& out){ out << yuan << "." << setfill('0') << setw(2) << jf << setfill(' '); } }; ostream& operator <<(ostream& oo, RMB& d){ d.display(oo); return oo; } int main() { RMB rmb(1.5); // cout << double(rmb) << endl; cout << "Initially rmb=" << rmb << "\n"; rmb = 2.0 * rmb; cout << "then rmb=" << rmb << endl; }
这时候,重载插入运算符应为RMB类的友元,因为它要直接访问RMB类的保护数据。但是它不能是成员,因为首先,插入运算符跟在ostream对象的后面,显然它不能是RMB类的成员;其次,ostream类在iostream头文件中定义,是标准类库,用户只能继承,不能修改标准类库,所以它更不能是ostream类的成员。
-
重载插入运算符中最后一条语句是“return oo;”,为什么要返回传递给它的ostream对象?
这样允许该运算符在单个表达式中与其他插入运算符联结在一起。≪的运算顺序是从左到右,下面的表达式这个运算符返回它的ostream对象,这一点很重要,这样做,对象才能被传递给下一个插入运算符。
-
插入运算符不能是成员函数,也就不能成为虚函数
-
cout≪a;能匹配重载的插入运算符,但是执行的结果是输出a的人民币值而不能输出a作为派生的附加信息a.c。
解决方法:在重载插入运算符中,不直接实现输出,而是调用display()成员,再将display()定义为虚函数。这样,重载插入运算符的行为便可随display()的不同而不同。这就是上一节为什么要间接实现重载插入运算符的用意。 -
类Currency有三个子类RMB、EUR和USD。Currency中display()成员定义为纯虚函数。在每一个子类中,display()成员被重载,从而可以适当的格式输出相应对象。重载插入运算符函数对display()的调用是一个虚调用。因此当它被传递以RMB类对象时,则像人民币那样输出;当它被传递以EUR对象时,则像欧元那样输出。因而,尽管重载的插入运算符不是虚拟的,因为它调用了一个虚函数,结果令人满意。
-
因为Currency是抽象类,不能构造该类的对象。
-
如果要打开一个文件用于输入,可以用ifstream类。
第20章 模板
-
本章介绍了模板的概念、定义和使用模板的方法,通过这些介绍,使读者有效地把握模板,以便能正确使用C++系统中日渐庞大的标准模板类库。
-
若一个程序的功能是对某种特定的数据类型进行处理,则若将所处理的数据类型说明为参数,就可把这个程序改写为模板。模板可以让程序对任何其他数据类型进行同样方式的处理。
-
其中的类型形式参数表可以包含基本数据类型,也可以包含类类型。如果是类类型,则须加前缀class。
-
函数模板是模板的定义,定义中使用通用类型参数。
模板函数是实实在在的函数定义,它是由函数模板生成的。编译系统在发现具体的函数调用时,匹配类型参数,生成函数代码。 -
这样的一个说明(包括成员函数模板定义),不是一个实实在在的类,只是对类的描述,称为类模板(class template)。
-
class_name<类型实在参数表>是模板类(template class),object是该模板类的一个对象。
-
类模板是模板的定义,定义中使用通用类型参数。
模板类是实实在在的类定义,是由类模板生成的。编译系统在发现以类模板方式创建其实例(对象)时,匹配类型参数,生成模板类定义。该模板类创建的对象即类模板的实例。 -
对于具有各种参数类型,相同个数、相同顺序的同一函数(重载函数),如果用宏定义来写:
则它不能检查其数据类型,损害了类型安全性。这也是为什么要使用函数模板的一个原因。 -
函数模板可将许多重载函数简单地归为一个
#include<iostream> using namespace std; template<class T> T Max(T a, T b){ return a > b ? a : b; } int main() { cout << "Max(3, 5) is " << Max(3, 5) << endl; cout << "Max('3', '5') is " << Max('3', '5') << endl; }
#include<iostream> #include<cstring> using namespace std; template<class T> T max(T a, T b){ return a > b ? a : b; } char* max(char* a, char* b){ return strcmp(a, b) >= 0 ? a : b; } int main(){ cout << "Max(\"Hello\", \"Gold\") is " << max("Hello", "Gold") << endl; }
-
编译程序在处理这种情况时,首先匹配重载函数,然后再寻求模板的匹配。
-
用类模板来定义一个通用链表,此时该通用链表还不是一个类定义,只是类定义的一个框架,即类模板。
/* listtmp.h */ # ifndef LIST # define LIST # include<iostream> using namespace std; template<class T> struct Node{ Node* pNext; T tValue; }; template<class T> class List{ Node<T> * pFirst, * pivot; public: List(){ pFirst=0; } void Add(T&); void Remove(T&); Node<T> * Find(T&); void PrintList(); ~List(); }; template<class T> void List<T>::Add(T& t){ Node<T> * tmp = new Node<T>; tmp->tValue = t; tmp->pNext = pFirst; pFirst = tmp; } template<class T> void List<T>::Remove(T& t){ Node<T> * q = Find(t); if(!q) return; if(q==pFirst) pFirst = pFirst->pNext; else pivot->pNext = q->pNext; delete q; } template<class T> Node<T> * List<T>::Find(T& t){ if(pFirst->tValue==t) return pFirst; for(Node<T> * p = pFirst->pNext; p; pivot=p, p = p->pNext) if(p->tValue==t) return p; return 0; } template<class T> void List<T>::PrintList(){ for(Node<T> * p=pFirst; p; p = p->pNext) cout << p->tValue << " " << endl; } template<class T> List<T>::~List(){ for(Node<T> * p; p=pFirst; delete p) pFirst = pFirst->pNext; } # endif
#include "listtmp.h" int main(){ List<float> floatList; for(int i = 1; i < 7; i++) floatList.Add(* new float(i + 0.6)); floatList.PrintList(); float b = 3.6; floatList.Remove(b); floatList.PrintList(); }
-
标准模板类库STL(Standard Template Library)是一个基于模板的容器类库,它包括向量、链表、队列和栈,还包括了一些通用的算法,如排序和查找等。它已经成为C++标准的组成部分。使用标准模板类库的好处是:可以避免自己在开发模板类库时,不同模板类之间的功能重复;最大限度的类库重用;作为C++标准,可移植性强是不言而喻的。
-
在C++中,一个发展趋势是使用标准模板类库(STL),VC和BC都把它作为编译器的一部分。STL是一个基于模板的包容类库,包括向量、链表和队列,还包括一些通用的排序和查找算法等。
第21章 异常处理
-
在大型软件开发中,最大的问题就是错误连篇的、不稳定的代码。而在设计与实现中,最大的开销是花在测试、查找和修改错误上。
-
程序的错误,一种是编译错误,即语法错误。如果使用了错误的语法、函数、结构和类,程序就无法被生成运行代码;另一种是在运行时发生的错误,它分为不可预料的逻辑错误和可以预料的运行异常
-
逻辑错误是由于不当的设计造成的,如,某个排序算法不合适,导致在边界条件下,不能正常完成排序任务。
-
运行异常,可以预料,但不能避免,它是由系统运行环境造成的。
-
异常是一种程序定义的错误,它对程序的逻辑错误进行设防,对运行异常加以控制。C++中,异常是对所能预料的运行错误进行处理的一套实现机制。
-
恢复的过程就是把产生异常所造成的恶劣影响去掉,中间可能要涉及一系列的函数调用链的退栈,对象的析构,资源的释放等
-
在C++中,异常是指从发生问题的代码区域传递到处理问题的代码区域的一个对象,
-
异常的基本思想是:
(1)实际的资源分配(如内存申请或文件打开)通常在程序的低层进行,如图21-1中的k()。
(2)当操作失败、无法分配内存或无法打开一个文件时,在逻辑上如何进行处理通常是在程序的高层,如图21-1中的f(),处理中间还可能有与用户的对话。
(3)异常为从分配资源的代码转向处理错误状态的代码提供了一种表达方式。如果还存在中间层次的函数,如图21-1中的g(),则为它们释放所分配的内存提供了机会,但这并不包括用于传递错误状态信息的代码。 -
使用异常的步骤是:
(1)定义异常范围(try语句块)。将那些有可能产生错误的语句框定在try块中。
(2)定义异常处理(catch语句块)。将异常处理的语句放在catch块中,以便异常被传递过来时就处理它。
(3)抛掷异常(throw语句)。检测是否产生异常,若是,则抛掷异常。 -
当打开文件失败时,就执行“throw argv[1];”语句,throw后面的表达式argv[1]的类型被称为所引发的异常之类型。
-
在try块之后必须紧跟一个或多个catch()语句,目的是对发生的异常进行处理。catch()括号中的声明只能容纳一个形参,当类型与抛掷异常的类型匹配时,该catch()块便称捕获了一个异常而转到其块中进行异常处理。
-
可以将抛掷异常与处理异常放在不同的函数中。
-
应把异常处理catch块看作是函数分程序。跟在catch之后的圆括号中必须含有数据类型,捕获是利用数据类型匹配实现的。在数据类型之后放参数名是可选的。参数名使得被捕获的对象在异常处理程序中被引用。
-
捕获的原因是抛掷的数据类型与异常处理程序的数据类型相匹配。
-
如果一个函数抛掷一个异常,但在通往异常处理函数的调用链中找不到与之匹配的catch,则该程序通常以abort()函数调用终止。
-
g('w')将能顺利地匹配函数g(int b),但是抛掷异常与异常处理程序之间,是按数据类型的严格匹配来捕获的。不允许类型转换
-
如果catch语句块执行完毕,则跟随最后catch语句块的代码(如果有的话)就被执行。
-
→在一个类定义的内部定义一个类,称为嵌套类。
嵌套类的成员函数和静态成员可以在包含该类的外部定义,但嵌套类的作用域在包含该类定义的内部。 -
值得注意的是,C++自带标准异常类定义及默认异常处理。它在头文件exception中。其中当申请内存空间的new操作失败时,将抛掷bad_alloc异常,它是except异常类的子类。
-
函数f()中的catch(...)块,参数为省略号,定义一个“默认”的异常处理程序。通常这个处理程序应在所有异常处理块的最后,因为它与任何throw都匹配,目的是为避免定义的异常处理程序没能捕获抛掷的异常而使程序运行终止。
-
可以把多个异常组成族系。构成异常族系的一些示例有数学错误异常族系和文件处理错误异常族系。在C++代码中把异常组在一起有两种方式:异常枚举族系和异常派生层次结构。
-
异常捕获的规则除了前面所说的,必须严格匹配数据类型外,对于类的派生,下列情况可以捕获异常:
(1)异常处理的数据类型是公有基类,抛掷异常的数据类型是其派生类;
(2)异常处理的数据类型是指向公有基类的指针或引用,抛掷异常的数据类型是指向派生类的指针或引用。
→对于派生层次结构的异常处理,catch块组中的顺序是重要的。因为“catch(基类)”总能够捕获“throw派生类对象”,所以“catch(基类)”块总是放在“catch(派生类)”块的后面,以避免“catch(派生类)”永远不能捕获异常。
个人点评
点评:★★★★☆
有一些代码错误,总体介绍挺全面。