第一周课程
第一部分 绪论
1.程序:(1)指程序员编写的源代码;(2)指可执行软件。
2.过程化编程:过程(函数或方法)是指一组依次执行的指令,数据与过程分离,编程技巧是跟踪函数的调用方向和函数对数据的修改动向。
3.结构化编程:其主要思想是分而治之,即是将过于复杂、无法简单解决的任务分解为不可再分的小任务,从而完成编程。
4.面向对象编程(OOP):其实质是模拟对象(东西或概念)而不是数据,从而实现将数据和操作结合起来。
5.C++支持的面向对象编程包括三要素:封装、继承和多态。
5.1封装:实现对数据和属性的隐藏,成为自包容单元的特性称为封装,C++中,利用“类(class)”来实现。
5.2继承:从已有类继承一部分属性成为新类,这类新类被称为派生类。
5.3多态:指同一名称有多种形式。
6.ANSI标准:隶属于美国国家标准协会的授权委员会(Accredited Standards Cpmmittee)制定了C++国际标准,该标准也被称为ISO(国际标准化组织)标准、NCITS(国家信息技术标准化委员会)标准、X3(NCITS的前身)标准和ANSI/ISO标准。
7.创建可执行文件的步骤是:1、创建扩展名为.cpp的源代码;2、将源代码文件编译成扩展名为.obj或.o的目标文件;3、将目标文件与所需的各种库链接起来,生成可执行程序。
程序清单1.1 HELLO.CPP
#include
int mian()
{
std::cout <<"Hello Wrold!\n";
return 0;
}
第二部分 C++程序的组成部分
1.mian函数的返回值被设置void是不合法的,习惯性让main函数返回int。
程序清单 2.1 使用cout
//Listing 2.2 using std::cout
#include
int main()
{
std::cout<<"Hallo there.\n";
std::cout<<"Here is 5:"<<5<<"\n";
std::cout<<"The manipulator std::endl";
std::cout<<"writes a new line to the screen.";
std::cout<
注:endl是指end line。
程序清单2.2 使用关键字using
//Listing 2.3 - using the using keyword
#include
int main()
{
using std::cout;
using std::endl;
cout<<"Hello there.\n";
cout<<"Here is 5:"<<5<<"\n";
cout<
注:这里的using是提前告诉编译器将使用标准库的两个对象的语句,所以就不需要限定cout和endl了。另一个用法就是using namespace std;,但是使用using namespace的缺点是可能不小心使用了错误库中的对象。所以较为推荐使用第一种做法。
2.注释的类型:1、单行注释;2、多行注释。其中单行注释使用双斜杠(//)来表示,即双斜杠告诉编译器,忽略之后到行尾的所有内容。而多行注释则是以单斜杠和星(/*)来打头的。这中注释是告诉编译器,忽略之后到星和单斜杠(*/)之间的所有内容,这两种注释标记可以位于同一行,之间也可以有一行或多行,但是每个/*必须要有*/来配对。
3.注释的使用:1、在函数的开头使用,说明函数的功能和返回值;2、在晦涩难懂的地方,加入注释以帮助理解。3、注释应该说明为什么会发生这样的事情,而不是说明发生了什么。
4.函数(简介):系统调用,当程序中没有函数时,程序依然可以运行,当程序遇到函数时,将会执行函数,当函数执行完后,将会继续执行下一条语句。
程序清单2.3 演示函数调用
#include
//function Demonstration Function
//prints out a useful message
void DemonstrationFunction()
{
std::cout<<"In Demonstration Function\n";
}
//function main - prints out a message,then
//calls DemonstrationFunction,then prints out
//a second message
int main()
{
std::cout<<"In main\n";
DemonstrationFunction();
std::cout<<"Back in main\n";
return 0;
}
5.函数的使用:函数由函数头和函数体组成,而函数头又由返回类型、函数名和参数组成,函数参数让你能够将值传递给函数。参数用于声明要传入的值的类型(调用函数实际传入的值被称为实参,而在函数头上标明的参数为形参)函数主体由左大括号、零条或者更多的语句以及右大括号组成,函数的功能由语句实现。函数可能用return语句来返回一个值,如果函数不包括return语句则函数将在自动返回void。
程序清单2.4 函数返回值
#include
int Add(int first,int second)
{
std::cout<<"In Add(),received"<>a>>b;
cout<
第三部分 使用变量和常量
1.变量:指存储信息的空间,变量是计算机内存的一个位置,可以在其中存储值或检索其中的值,其变量用于临时存储,退出程序或关机后,变量中的信息将丢失。
2.变量的大小:任何变量在内存中都将占据一定空间,而对任何机器来说,其占用空间的大小是固定的。单个字符用char变量存储(大小通常为一个字节),而整型变量则用int来存储(可能2字节,也可能是4字节),对于较小的整数,则用short变量来存储(大多数机器上,short通常为2字节,而long通常为4字节)。C++规定,short的长度不可以超过int,而int不可以超过long。
程序清单3.1
#include
int main()
{
using namespace std;
cout<<"The size of an int is:\t\t"<
注:sizeof可以求出给定类型的长度。
3.signed和unsigned:其中signed可以让变量带有正负号,而unsigned则只可以让变量为正。
4.基本变量类型:C++内置浮点变量和字符变量。其中浮点变量为实数,字符变量为字符(通常为1位),通常用来表示ASCII字符集和扩展ASCII字符集中的256个字母和符号。
5.定义变量:要创建或定义变量,可声明其类型,然后指出变量名称,再加分号结尾。变量名可为任意字母组合,但不能有空格,不能以数字开头,也不可使用C++预定义的符号名。同时要注意,C++是区分大小写的。比较流行的命名方法是:1、my_cat、2、myCat(第二种被叫做驼峰法)、3、匈牙利表示法:即变量名以一组表示其类型的字符打头,例如指针就用p打头。但是注意:匈牙利表示法不推荐使用。
6.C++系统关键字
ISO C++98/03关键字共63个,此处严格按标准原文排版:
|
其中auto意义改变为:表示由编译器静态判断其应有的类型;register 被视为过时;export 因为实现支持太少(仅Edison Design Group的前端支持),编译效率低下,取消原有意义(仍是关键字,但使用它的程序是错误的),改为保留给未来标准使用。
7.同时创建多个变量:在创建变量时,用逗号将变量名分开即可。
8.给变量赋值:使用赋值运算符(=)给变量赋值。
程序清单:3.2 演示变量的用法
#include
int main()
{
using std::cout;
using std::endl;
unsigned short Width = 5,Length;
Length = 10;
//create an unsigned short and initialize with result
//of multiplying Width by Length
unsigned short Area = (Width * Length);
cout<<"Width:"<
注:long是long int的缩写,short是short int的缩写。
9.用typedef来创建别名:C++允许使用关键字typedef(表示类型定义)来给一个短语创建一个别名。例:typedef int INT;
10.变量类型使用原则:当需要存储的数值足够大时,应将变量声明为更大类型的变量。
11.unsigned和singned:这两个关键字分别定义了无符号类型和有符号类型的变量。
12.字符变量(char):通常占一个字节,char变量通常被解释为0~256的数或ASCII码字符集中的成员,其中ASCII码97表示小写字母a,ASCII码65表示大写字母A,ASCII码48表示数字0。
程序清单3.3 利用数字打印字符
#include
int main()
{
for(int i =0;i<128;i++)
std::cout<<(char)i;
return 0;
}
13.特殊打印字符:一些常用的特殊格式化字符,用反斜杠(/,转义字符)和相应的字符输入。
常用的转义字符表:
转义字符 |
意义 |
ASCII码值(十进制) |
\a |
响铃(BEL) |
007 |
\b |
退格(BS) ,将当前位置移到前一列 |
008 |
\f |
换页(FF),将当前位置移到下页开头 |
012 |
\n |
换行(LF) ,将当前位置移到下一行开头 |
010 |
\r |
回车(CR) ,将当前位置移到本行开头 |
013 |
\t |
水平制表(HT) (跳到下一个TAB位置) |
009 |
\v |
垂直制表(VT) |
011 |
\\ |
代表一个反斜线字符''\' |
092 |
\' |
代表一个单引号(撇号)字符 |
039 |
\" |
代表一个双引号字符 |
034 |
\? |
代表一个问号 |
063 |
\0 |
空字符(NULL) |
000 |
\ddd |
1到3位八进制数所代表的任意字符 |
三位八进制 |
\xhh |
1到2位十六进制所代表的任意字符 |
二位十六进制 |
注意:区分,斜杠:"/" 与 反斜杠:"\" ,此处不可互换
14.常量:不能修改的值,创建常量时,必须要赋值,且在初始化之后就无法赋值了。C++有两种常量:字面常量和符号常量。
14.1.字面常量:指直接输入到程序中的值,例如:int a = 19; 其中19就是字面常量
14.2.符号常量:指用名称表示的常量,传统方法定义常量是用#define,现在使用的是关键字const来创建。例如const int a =10;
15.枚举常量:可利用枚举常量创建新的类型,并定义新类型变量,同时将这些变量的取值限定为一定范围的可能值,创建枚举常量的方法是:关键字enum,新类型名,左大括号,合法值(用逗号隔开) ,右大括号。每个枚举常量都有一个整数值,如不特殊指定,则第一个常量的值为0,其余常量的值依次递增。
程序清单3.4
#include
int main()
{
enum Days{Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday};
Days Today;
Today = Monday;
const int a = 3;
if (a == Today || Today == Saturday)
std::cout<<"\nGotta'love the weekends!\n";
else
std::cout<<"\nBack to work.\n";
return 0;
}
第四部分 创建表达式和语句
1.语句:在C++中,语句控制程序的执行顺序、计算表达式的值或者什么都不做(空语句)。所有C++语句都已分号(;)结尾。
2.语句块和复合语句:在任何可以使用单条语句的地方都可以使用复合语句(也叫语句块),语句块以“{”开始,以“}”结尾。注意:语句块不需要用”;“结尾。
3.表达式:表达式总是返回一个值,而当表达式带上分号后,就称为语句。
4.运算符:指能让编译器执行某种操作的符号,常用于操作数,分为多种,先学习两种:赋值运算符和数学运算符。
4.1.赋值运算符(=):将表达式左边的值赋给右边表达式的值。注:放在赋值运算符左侧的操作数叫左值,放在右侧的操作数叫右值,所有的左值都是右值,但所有的右值并非都是左值。其中,常量是右值,因为他们不可以被修改,所以常量不能是左值。
4.2数学运算符:有五个,分别是+、-、*、/和%(求模)。求模也叫取余,即求得余数。
5.赋值运算符和求值运算符结合:+=、-=、*=、/=、%=等都是赋值运算符和求值运算符的结合。例:a+=3 <=> a = a+3;
6.自增和自减运算符:++为自增运算符,--为自减运算符。这两种运算符有前缀和后缀之分:其中前缀时,先修改变量的值,再求表达式的值。例: i = 1;++i; 后,i的值为2,表达式的值也为2。而后缀时,先求表达式的值,再修改变量的值。例:i = 1; i++;后,表达式的值为2,而i的值任然为1。
程序清单4.1 前缀和后缀表达式的使用
#include
int main()
{
using std::cout;
int myAge = 39;
int yourAge = 39;
cout<<"I am:"<
7.算数运算符的优先级
C++中的算数运算符的优先级为:
Operator(运算符)
|
Description(描述)
|
Example(例子)
|
Overloadable(替换)
|
Group 1(no associativity)
|
|||
::
|
Scope resolution operator
|
Class::age = 2;
|
NO
|
Group 2
|
|||
()
|
Function call
|
isdigit('1')
|
YES
|
()
|
Member initalization
|
c_tor(int x, int y) : _x(x), _y(y*10){};
|
YES
|
[]
|
Array access
|
array[4] = 2;
|
YES
|
->
|
Member access from a pointer
|
ptr->age = 34;
|
YES
|
.
|
Member access from an object
|
obj.age = 34;
|
NO
|
++
|
Post-increment
|
for( int i = 0; i < 10; i++ ) cout << i;
|
YES
|
--
|
Post-decrement
|
for( int i = 10; i > 0; i-- ) cout << i;
|
YES
|
const_cast
|
Special cast
|
const_cast |
NO
|
dynamic_cast
|
Special cast
|
dynamic_cast |
NO
|
static_cast
|
Special cast
|
static_cast |
NO
|
reinterpret_cast
|
Special cast
|
reinterpret_cast |
NO
|
typeid
|
Runtime type information
|
cout « typeid(var).name();
cout « typeid(type).name();
|
NO
|
Group 3(right-to-left associativity)
|
|||
!
|
Logical negation
|
if( !done ) …
|
YES
|
not
|
Alternate spelling for !
|
||
~
|
Bitwise complement
|
flags = ~flags;
|
YES
|
compl
|
Alternate spelling for ~
|
||
++
|
Pre-increment
|
for( i = 0; i < 10; ++i ) cout << i;
|
YES
|
--
|
Pre-decrement
|
for( i = 10; i > 0; --i ) cout << i;
|
YES
|
-
|
Unary minus
|
int i = -1;
|
YES
|
+
|
Unary plus
|
int i = +1;
|
YES
|
*
|
Dereference
|
int data = *intPtr;
|
YES
|
&
|
Address of
|
int *intPtr = &data;
|
YES
|
new
|
Dynamic memory allocation
|
long *pVar = new long;
MyClass *ptr = new MyClass(args);
|
YES
|
new []
|
Dynamic memory allocation of array
|
long *array = new long[n];
|
YES
|
delete
|
Deallocating the memory
|
delete pVar;
|
YES
|
delete []
|
Deallocating the memory of array
|
delete [] array;
|
YES
|
(type)
|
Cast to a given type
|
int i = (int) floatNum;
|
YES
|
sizeof
|
Return size of an object or type
|
int size = sizeof floatNum;
int size = sizeof(float);
|
NO
|
Group 4
|
|||
->*
|
Member pointer selector
|
ptr->*var = 24;
|
YES
|
.*
|
Member object selector
|
obj.*var = 24;
|
NO
|
Group 5
|
|||
*
|
Multiplication
|
int i = 2 * 4;
|
YES
|
/
|
Division
|
float f = 10.0 / 3.0;
|
YES
|
%
|
Modulus
|
int rem = 4 % 3;
|
YES
|
Group 6
|
|||
+
|
Addition
|
int i = 2 + 3;
|
YES
|
-
|
Subtraction
|
int i = 5 - 1;
|
YES
|
Group 7
|
|||
<<
|
Bitwise shift left
|
int flags = 33 << 1;
|
YES
|
>>
|
Bitwise shift right
|
int flags = 33 >> 1;
|
YES
|
Group 8
|
|||
<
|
Comparison less-than
|
if( i < 42 ) …
|
YES
|
<=
|
Comparison less-than-or-equal-to
|
if( i <= 42 ) ...
|
YES
|
>
|
Comparison greater-than
|
if( i > 42 ) …
|
YES
|
>=
|
Comparison greater-than-or-equal-to
|
if( i >= 42 ) ...
|
YES
|
Group 9
|
|||
==
|
Comparison equal-to
|
if( i == 42 ) ...
|
YES
|
eq
|
Alternate spelling for ==
|
||
!=
|
Comparison not-equal-to
|
if( i != 42 ) …
|
YES
|
not_eq
|
Alternate spelling for !=
|
||
Group 10
|
|||
&
|
Bitwise AND
|
flags = flags & 42;
|
YES
|
bitand
|
Alternate spelling for &
|
||
Group 11
|
|||
^
|
Bitwise exclusive OR (XOR)
|
flags = flags ^ 42;
|
YES
|
xor
|
Alternate spelling for ^
|
||
Group 12
|
|||
|
|
Bitwise inclusive (normal) OR
|
flags = flags | 42;
|
YES
|
bitor
|
Alternate spelling for |
|
||
Group 13
|
|||
&&
|
Logical AND
|
if( conditionA && conditionB ) …
|
YES
|
and
|
Alternate spelling for &&
|
||
Group 14
|
|||
||
|
Logical OR
|
if( conditionA || conditionB ) ...
|
YES
|
or
|
Alternate spelling for ||
|
||
Group 15(right-to-left associativity)
|
|||
? :
|
Ternary conditional (if-then-else)
|
int i = (a > b) ? a : b;
|
NO
|
Group 16(right-to-left associativity)
|
|||
=
|
Assignment operator
|
int a = b;
|
YES
|
+=
|
Increment and assign
|
a += 3;
|
YES
|
-=
|
Decrement and assign
|
b -= 4;
|
YES
|
*=
|
Multiply and assign
|
a *= 5;
|
YES
|
/=
|
Divide and assign
|
a /= 2;
|
YES
|
%=
|
Modulo and assign
|
a %= 3;
|
YES
|
&=
|
Bitwise AND and assign
|
flags &= new_flags;
|
YES
|
and_eq
|
Alternate spelling for &=
|
||
^=
|
Bitwise exclusive or (XOR) and assign
|
flags ^= new_flags;
|
YES
|
xor_eq
|
Alternate spelling for ^=
|
||
|=
|
Bitwise normal OR and assign
|
flags |= new_flags;
|
YES
|
or_eq
|
Alternate spelling for |=
|
||
<<=
|
Bitwise shift left and assign
|
flags <<= 2;
|
YES
|
>>=
|
Bitwise shift right and assign
|
flags >>= 2;
|
YES
|
Group 17
|
|||
throw
|
throw exception
|
throw EClass(“Message”);
|
NO
|
Group 18
|
|||
,
|
Sequential evaluation operator
|
for( i = 0, j = 0; i < 10; i++, j++ ) …
|
YES
|
9.真值的本质:表达式都有值,可以将每个表达式的值视为真或假,如果表达式的值为零,则返回false,否则返回true。再新的ANSI中引入了bool型,这种类型只可以取两种值:false和ture。
10.关系运算符:C++中的6个关系运算符分别是:==(等于)、<(小于)、>(大于)、<=(小于等于)、>=(大于等于)、!=(不等)。有这些运算符组成的表达式只会返回ture和false。
11.if语句:测试某个条件是否成立,如果成立则执行语句,反之则不执行。
程序清单4.2 基于关系运算符进行分支
#include
int main()
{
using std::cout;
using std::cin;
using std::endl;
int MetScore,YankeeScore;
cout<<"Enter the score for the Mets:";
cin>>MetScore;
cout<>YankeeScore;
cout< YankeeScore)
cout<<"Let's Go Mets!"<>YankeeScore;
if(MetScore > YankeeScore)
cout<<"Knew it!Go Yanks!";
if(YankeeScore > MetScore)
cout<<"Knew it!GO Yanks!";
if(YankeeScore == MetScore)
cout<<"Wow,it really was a tie!";
}
cout<
注:在if判断条件后不需要加分号(;),加入分号则代表执行一条空语句。所以为了减少错误,请在if后使用大括号。
12.缩进风格:大括号的对齐风格现在比较流行的缩进风格有三种:
(1)if(expression){
statements
}
(2)if(expression)
{
statements
}
(3)if(expression)
{
statements
}
C++程序员比较常用是第二种风格。13.else语句:程序常常在if判断后不执行某个分支,而需要执行另外一分支,所以可以使用关键字else。
if(expression)
{
statements
}
else{
statements
}
14.if和else语句的嵌套使用:多个分支均则可以使用if和else组合程序清单4.3
#include
int main()
{
using namespace std;
int firstNumber,secondNumber;
cout<<"Enter two numbers."<>firstNumber;
cout<>secondNumber;
cout<= secondNumber)
{
if((firstNumber & secondNumber) == 0)
{
if(firstNumber == secondNumber)
cout<<"They are the same!"<
15.逻辑运算符:&&(AND)、||(OR)、!(NOT)。AND运算符:运算符两侧的运算数同时为真时,表达式的结果才会为真。OR运算符:运算符两侧的运算数同时为假时,表达式的结果才会为假,NOT运算符:使表达式的值与表达式的值相反。
16.0的使用:在C++中,0被解释为假,其他非0数都被解释为真。所以在如下例子中,推荐使用第二种。
(1)if(x = 0)
x = 0;
(2)if(x != 0)
x = 0;
注:良好的编程习惯就是:用表达式来判断逻辑真假,而不使用它本身来判断逻辑真假。
16.三目运算符:条件运算符(?:)是C++\中唯一一个三目运算符,即它是唯一一个需要三个操作数的运算符。条件运算符接受单个表达式并返回一个值:(experssion1)?(expression2):(expression3)这个表达式的含义是:如果expression1为真,则返回expression2的值,否则返回expression3的值。
程序清单4.4
#include
int main()
{
int a,b,c;
std::cin>>a>>b;
c = (a > b)? a : b;
std::cout<
第五部分 组织函数
1.函数:指能够对数据进行处理并返回一个值的子程序,每个C++程序必须要有一个函数:main函数。每个函数都有自己的函数名,当程序遇到函数名时候,会执行函数(调用函数),当函数执行完时(遇到return语句或左大括号“}”),程序返回到函数调用的下一行继续执行。函数主要有两种类型:1、用户定义函数,2、内置函数。其中内置函数是由开发商提供给用户的。
2.返回值、参数和实参:函数可以接受值,还可以返回一个值。调用函数完成工作,返回一个值或者工作结果。这个值被称为返回值,返回值的类型必须被声明。另外,参数描述了函数被调用时,传递给它的值的类型,传递给函数的实际值被称为实参。
3.声明和定义函数:在程序中使用函数,需要先声明函数后在使用。声明将函数的名称、返回值和参数告诉编译器;定义将函数的工作原理告诉编译器。函数的声明又被称为原型。有3中声明函数的方法:1、将函数原型放在文件中,然后使用#include将该文件包含在程序内;2、将函数原型放在其中使用它的文件中;3、在函数被其他函数调用前定义它(这样做,函数定义将作为原型)。
4.函数原型:指一条语句,以分号结尾,由函数的返回值类型和特征标(signature)组成,函数特征标包含函数名和参数列表,参数列表是所有参数及其类型的列表,参数之间用逗号分开。注:函数原型及其定义再返回类型和特征标方面必须完全相同,否则编译时会出错。同时,函数原型可以不包含参数名,而只有参数类型。同时,所有函数都必须要有返回值,如果没有明确声明,则默认为int。如果函数不需要返回值,则可以将函数返回类型设置为void。
程序清单5.1 函数的声明、定义和用法
#include
int Area(int,int);
int main()
{
using namespace std;
int lengthofYard;
int widthofYard;
int areaofYard;
cout<>widthofYard;
cout<>lengthofYard;
areaofYard = Area(lengthofYard,widthofYard);
cout<
5.变量的作用域:每个变量都有作用域,作用域决定了变量在程序中的存活时间以及在什么地方可以使用它。其中,在语句块中声明的变量的作用域为该语句块;全局变量则在程序任何地方都可以使用它。
6.局部变量:在函数体内声明的变量被称为局部变量,当函数返回时,这些变量将不再存在,编译器将其标记并销毁。另:传入的参数也可看作局部变量。
程序清单5.2 局部变量和参数的用法
#include
float Convert(float);
int main()
{
using namespace std;
float TempFer;
float TempCel;
cout<<"Please enter the temperature in Fahrenheit:";
cin>>TempFer;
TempCel = Convert(TempFer);
cout<
7.作用域为语句块的局部变量:在函数的任何地方定义变量,且不仅限于函数开头,变量的作用域为定义它的语句所在的语句块。
8.参数作为局部变量:传入函数的参数为函数的局部变量,修改这些参数不会影响调用函数中的值,这被称为按值传递,也就是在函数中会创建参数的局部拷贝。
程序清单 5.3
#include
using namespace std;
void swap(int,int);
int main()
{
int x = 5,y = 10;
cout<<"Main.Before swap,x:"<
9.全部变量:在函数外面定义的变量的作用域为全局,在程序的任何地方都可以使用。与全局变量同名的局部变量不会修改全局变量,但会隐藏全局变量,即在函数中有一个与全局变量同名的局部变量,则在函数中使用该名称时,指的是局部变量而不是全局变量。注:全局变量在C++中很少使用,因为全局变量可以被修改,导致不可预计的错误,因此常用静态成员变量替代全局变量。
10.再谈函数实参:任意合法的C++表达式中都可以作为函数实参,包括常量、数字和逻辑表达式以及返回的一个值的函数,需要注意的是表达式的结果必须与函数期望的实参类型匹配。
11.再谈返回值:返回值可以返回一个值或者不返回值,也可以返回逻辑真值,并且在遇到关键字return时,其后的表达式将作为函数的返回值,并立即返回到调用的喊出,return后的所有语句都不会被执行。
程序清单 5.4 包含多条返回的语句
#include
int Doubler(int);
int main()
{
using std::cout;
using std::endl;
int input;
int result = 0;
cout<<"Enter a number between 0 and 10,000 to double:";
std::cin>>input;
cout<
注:int main()和void main()两者都被大多数编译器支持,但是就ANSI标准来说,只有int main()符合ANSI标准。
12.,默认参数:每个函数必须接受参数,当不传递参数时,会引起错误。但是可以有例外就是使用默认参数。即在函数参数中给参数定义一个预定义的值,例如:int Area (int a = 10);这个时候,当没有参数传入时,编译器将把x设置为默认值10。注:可以给任何函数参数指定默认值,但是如果某个参数没有设置默认值时,他前面的所有参数都不可以有默认值。
程序清单 5.5 默认参数值
#include
int AreaCube(int length,int width = 25,int height = 1);
int main()
{
int length = 100;
int width = 50;
int height = 2;
int area;
area = AreaCube(length,width,height);
std::cout<<"First area equals:"<
注:如果第二个参数没有默认值的时,不要为第一个参数设置默认值,按值传递参数不会修改函数中的变量。
13.重载函数:C++允许创建多个名称相同的函数,该函数被称为函数重载。在这些同名的函数中的参数列表中,必须有不同的参数类型、参数个数或兼而有之。注:如果两个函数的函数名和参数列表相同,但返回类型不同,将导致编译错误,要修改返回类型,必须同时修改特征标(名次和/或参数列表)。
14.函数多态:函数重载也叫函数多态(polymorphism)。多态指的是多种形态。即可以对函数进行重载,使之含有多种含义。通过修改参数的个数或类型,可以让多个函数使用相同的名称。进而根据指定的参数,调用与之匹配的函数。
函数清单 5.6 函数多态
#include
int Double(int);
long Double(long);
float Double(float);
double Double(double);
using namespace std;
int main()
{
int myInt = 6500;
long myLong = 6500;
float myFloat = 6.5F;
double myDouble = 6.5e20;
int doubleInt;
long doubleLong;
float doubleFloat;
double doubleDouble;
cout<<"myInt:"<
15.内联函数:当函数非常小,但是有需要频繁调用时,为了提高效率,可使用内联函数。内联函数是在声明函数时候,使用关键字inline,让编译器不会创建函数,而直接将内联函数的代码复制到调用函数中,以减少不必要的开销。注:为了避免因为内联函数过大,在编译器复制后,导致可执行程序变大,而降低速度。故推荐当函数只有一两句话时,再使用内联函数。
程序清单 5.7 使用Fibonacci数列演示递归
#include
int fib(int);
int main()
{
int n,answer;
std::cout<<"Enter number to find:";
std::cin>>n;
std::cout<
17.内存分区:C++里会把内存分为全局名称区、自由存储区、寄存器、代码空间和堆栈。其中全局名称区是存放全局变量的。寄存器是在CPU中的特殊存储区域,可将寄存器统称为指令指针,代码空间是存放代码的地方,堆栈是用来存放程序中每个函数所需的数据。堆栈是先进后出。
18.堆栈与函数:程序执行过程中发生的大致情况(具体细节随操作系统和编译器差别而差别)。
(1)指令指针中的地址增加1,指向函数调用后的下一条指令,这个地址随后被放入堆栈,它是函数返回时的返回地址;
(2)在堆栈中为声明的返回值类型分配空间,在int变量占两个字节的系统上,如果返回类型被声明为int,堆栈再增加两个字节,但这两个字节中不存放任何值(这就意味着这两个值中的数据(垃圾数据)保持不变,直到本地变量被初始化);
(3)被调用函数的地址存储在为此而分配的一块特殊区域中,这个地址被加载到指令指针中,这样将执行的下一条指令为被调用的函数;
(4)当前的栈顶被记录下来,并存入一个称为栈帧(stack frame)的特殊指针中,从现在开始到函数返回被加入到堆栈中任何数据都将被视为函数的局部数据;
(5)函数的所有参数都被放入堆栈;
(6)现在执行指令指针中的指令,即执行函数的第一条指令;
(7)局部变量在被定义时被压入堆栈。
当函数准备好返回时,返回值被放入第2步预留的堆栈区域中,随后不断对堆栈执行弹出操作,直接遇到栈帧指针,这相当于丢弃函数的所有局部变量和参数。
返回值被弹出堆栈,将其作为函数调用本身的值,然后检索第1步存储的地址,将其放入指令指针。程序回到函数调用后执行,并检索函数调用的返回值。
第六部分 面向对象编程
1.面向对象编程:将数据和操作数据的方法结合在一起,从而更加容易理解。面向对象是C++和C之间的桥梁。
2.使用结构创建新类型的缺点:通过将相关变量组合成结构的方法,为C语言增加了新类型的功能,通过使用typedef可以让结构称为新的类型。但是缺点有:1、结构和操作不是一个整体,只能通过阅读库的头文件,并使用新类型作为参数进行查找,才能找到函数。2、针对结构的相关函数组的行为进行协调时,任何数据都有可能在任何时候被修改,无法防止数结构数据被修改不被干扰,从而造成错误。3、内置的运算符不适用于结构:不能使用(+)将两个结构变量相加,即使这可能是一种最自然的表示问题解决方案的方式。
3.类:在C++中,可通过声明一个类来创建一个新类型,类将一组变量和一组相关操作函数组合在一起。类可以将一个事物的数据和属性以及相关操作封装在一个集合中,这个集合就被称为对象。成员变量也叫数据成员,类内的函数被称为成员函数或者类函数。类函数通常操作成员变量。
4.声明类:使用关键字class,后跟类名,左大括号,数据成员列表和方法,右大括号和分号。例:class Cat { unsigned itsAge; unsigned itsWeight; void Meow();};注:上述声明并没有分配内存,只是告诉编译器Cat的相关属性和数据,但是声明让编译器知道Cat多大,即编译器必须为你创建的每个Cat对象预留多少内存。
5.程序命名:软件开发公司通常在风格方面有内部标准,这可确保所有开发人员都轻松读懂其他开发人员的代码,但是这种趋势蔓延到了开发操作系统和可重用类库的公司,这也就意味着C++程序必须处理多种不同的命名规则。
6.定义对象:当创建好类时,就可以将其作为新类型来声明这种类型的变量。其声明方法与普通类型相同。例:Cat mimi。C++将类和对象区分,对象是类的实例。
7.访问类成员:定义类对象后,就可以使用句点运算符(.)来访问该对象的成员。例:mimi.Meow();调用对象mimi的成员函数Meow。
8.给对象赋值:例:mimi.itsAge = 2;这个操作就给对象mimi的itsAge赋值2。注:类内没有的成员或操作是不可以使用的,即对象没有这个属性。例如mimi.Bark(); 这个操作是错误的,因为类Cat没有Back这个操作。不应该把声明和定义混为一谈,声明是指出类是什么,定义是为对象分配内存。
9.类声明中的关键字:public(公开的)和private(私有的)用于类成员,其中私有成员只能在类方法中访问;共有成员可以通过类的任何对象进行访问。默认情况下,类成员都是私有的。
程序清单 6.1 访问一个简单类的公有成员
#include
class Cat
{
public:
int itsAge;
int itsWeight;
};
int main()
{
Cat Frisk;
Frisk.itsAge = 5;
std::cout<<"Frisky is a cat who is ";
std::cout<
return 0;
}
10.类的数据:通用的设计规则是,应该让类的数据成员为私有的。通过创建被存取方法(accessor method)的公有函数,用这些方法来设置和获取私有成员变量。在程序的其他地方调用它们来获取和设置私有成员变量。注:存取器函数将数据和存储细节和数据的使用细节分开,通过使用获取器函数,以后修改数据的存储方式时,不必重新编写使用这些数据的函数。
程序清单 6.2 包含存取器方法的类
#include
class Cat
{
public:
unsigned int GetAge();
void SetAge(unsigned int Weight);
unsigned int GetWeight();
void SetWeight(unsigned int Weight);
void Meow();
private:
unsigned int itsAge;
unsigned int itsWeight;
};
注:关键字class用于声明新类型,类是类成员数据的集合,类成员数据可以是各种类型的变量,包括其他类。类还包含类函数(方法),即用来操作类中的数据或为类提供其他服务的函数。定义类对象的方法与定义变量一样,首先指出类型(类)、然后是变量名(对象)、使用句点运算符(.)可访问类的成员和函数。
11.实现类方法:存取器函数提供了到类的私有成员数据的共有接口,每个存取器函数以及声明的其他类方法都必须有实现,实现被称为函数的定义。成员函数的定义类似于常规函数:首先指出函数的返回类型,如果函数不返回任何值,则使用void。然后是类名、两个冒号、函数名和参数。
程序清单 6.3 实现了一个简单类的方法
#include
class Cat
{
public:
int GetAge();
void SetAge(int age);
void Meow();
private:
int itsAge;
};
int Cat::GetAge()
{
return itsAge;
}
void Cat::SetAge(int age)
{
itsAge = age;
}
void Cat::Meow()
{
std::cout << "Meow" << std::endl;
}
int main()
{
Cat Frisky;
Frisky.SetAge(5);
Frisky.Meow();
std::cout << "Frisky is a cat who is ";
std::cout << Frisky.GetAge() << " years old." << std::endl;
Frisky.Meow();
return 0;
}
13.默认构造函数和析构函数:没有参数的构造函数称为默认构造函数。析构函数只能有一个,不带参数也没有返回值,可在析构函数中执行一些操作。当没有声明构造函数和析构函数时,编译器会提供一个默认构造函数和析构函数,但这个构造函数和析构函数什么都不做。所以尽量创建自己的默认构造函数和析构函数。
14.使用默认构造函数:不接受任何参数的构造函数,也可以给自己的默认构造函数提供函数体,在其中对对象进行初始化。出于格式方面的考虑,建议至少定义一个构造函数,将成员变量指定合适的默认值,以确保对象总是能正确的运行。
程序清单 6.4 使用构造函数和析构函数
#include
class Cat
{
public:
Cat(int initialAge);
~Cat();
int GetAge();
void SetAge(int age);
void Meow();
private:
int itsAge;
};
Cat::Cat(int initialAge)
{
itsAge = initialAge;
}
Cat::~Cat()
{
}
int Cat::GetAge()
{
return itsAge;
}
void Cat::SetAge(int age)
{
itsAge = age;
}
void Cat::Meow()
{
std::cout << "Meow."<
注:使用构造函数来初始化对象。在添加了构造函数后一定要有析构函数。
15..const成员函数:关键字const可以用来声明不可以被修改的变量。也可以使用const来声明不会修改任何类成员值的成员函数。要将类方法声明为const,可在方法声明中将所有参数括号和分号之间放置关键字const。例:void SomeFunction() const;注:如果将一个函数声明为const的,而该函数的实现通过修改某个成员变量的值而修改了对象,编译器将其视为错误。良好的编程习惯是,尽可能将方法声明为const的,这样可以让编译器捕获错误,减少bug。
16.接口与实现:客户是程序中创建和使用类对象的部分,可以将类的公有接口(类声明)视为类的行为方式。注:C++是强类型化(strongly typed)语言,当违反条款时,编译器将通过显示编译器错误强制执行合同。
程序清单 6.5 违反接口合同的例子
#include
class Cat
{
public:
Cat(int initialAge);
~Cat();
int GetAge() const;
void SetAge(int Age);
void Meow();
private:
int itsAge;
};
Cat::Cat(int initialAge)
{
itsAge = initialAge;
std::cout << "Cat Constructor." << std::endl;
}
Cat::~Cat()
{
std::cout << "Cat Constructor" << std::endl;
}
int Cat::GetAge() const
{
return (itsAge++);
}
void Cat::SetAge(int age)
{
itsAge = age;
}
void Cat::Meow()
{
std::cout << "Meow." << std::endl;
}
int main()
{
Cat Frisky;
Frisky.Meow();
Frisky.Bark();
Frisky.itsAge = 7;
return 0;
}
注:这个程序不可以编译,需要注意的是,编译错误比运行错误要很多,因为运行错误很可能藏得很深,很难发现,从而给调试带来很大的困难,为了减少不可预知的错误,
最好的是利用编译器发现错误。
17.类声明和方法定义的位置:为类声明的每个函数都必须有定义,这种定义被称为函数实现,类方法的定义也由函数头和函数题组成。定义必须位于编译器能够找到的文件中,大多数C++编译器希望这种文件的扩展名为.c或.cpp。也可以将声明放在实现文件中,但这不是一种恨好的习惯,大多数程序员采用的约定是,将声明放在头文件中,该头文件的名称与现实文件相同,但扩展名是.h、.hp或.hpp。
18.内联实现:类的方法也可以作为内联的。为此只需要在返回类型前面加上关键字inline。或者将函数的定义放在类声明中,这是函数将自动成为内联函数。注:内联函数的函数体紧跟在类方法声明之后,圆括号后面没有分号。
程序清单 6.6 位于Cat.hpp中的Cat类声明 (程序清单6.6和6.7重新编写了Cat类,将类声明放在文件Cat。hpp中,函数实现在Cat.cpp中)
#include
class Cat
{
public:
Cat(int initiaiAge);
~Cat();
int GetAge()const
{
return itsAge;
};
void SetAge(int age)
{
itsAge = age;
}
void Meow() const
{
std::cout << "Meow." << std::endl;
}
private:
int itsAge;
};
程序清单 6.7 位于Cat.cpp中的Cat类实现
#include "Cat.hpp"
Cat::Cat(int initialAge)
{
itsAge = initialAge;
}
Cat::~Cat()
{
}
int main()
{
Cat Frisky(5);
Frisky.Meow();
std::cout << "Frisky is a cat who is ";
std::cout << Frisky.GetAge() << " years old." << std::endl;
Frisky.Meow();
Frisky.SetAge(7);
std::cout << "Now Frisky is ";
std::cout << Frisky.GetAge() << " years old." << std::endl;
return 0;
}
19.将他类用作成员数据的类:常见的创建复杂类的方式是,首先声明较简单的类,然后将其包含到较复杂类的声明中。
程序清单 6.8 声明一个完整的类
//Begin Rectangle.hpp
#include
class Point //holds x,y coordinates
{
//no constructor,use default
public:
void SetX(int x) { itsX = x; }
void SetY(int y) { itsY = y; }
int GetX()const { return itsX; }
int GetY()const { return itsY; }
private:
int itsX;
int itsY;
};//end of point class declaration
class Rectangle
{
public:
Rectangle(int top, int left, int bottom, int right);
~Rectangle() {}
int GetTop() const { return itsTop; }
int GetLeft() const { return itsLeft; }
int GetBottom() const { return itsBottom; }
int GetRight() const { return itsRight; }
Point GetUpperLeft() const { return itsUpperLeft; }
Point GetLowerLeft() const { return itsLowerLeft; }
Point GetUpperRight() const { return itsUpperRight; }
Point GetLowerRight() const { return itsLowerRight; }
void SetUpperLeft(Point Location) { itsUpperLeft = Location; }
void SetLowerLeft(Point Location) { itsLowerLeft = Location; }
void SetUpperRight(Point Location) { itsUpperRight = Location; }
void SetLowerRight(Point Location) { itsLowerRight = Location; }
void SetTop(int top) { itsTop = top; }
void SetLeft(int left) { itsLeft = left; }
void SetBottom(int bottom) { itsBottom = bottom; }
void SetRight(int right) { itsRight = right; }
int GetArea() const;
private:
Point itsUpperLeft;
Point itsUpperRight;
Point itsLowerLeft;
Point itsLowerRight;
int itsTop;
int itsLeft;
int itsBottom;
int itsRight;
};
//end Rectangle.hpp
程序清单 6.9 RECT.cpp
//Begin Rect.cpp
#include "Rectangle.hpp"
Rectangle::Rectangle (int top,int left,int bottom,int right)
{
itsTop = top;
itsLeft = left;
itsBottom = bottom;
itsRight = right;
itsUpperLeft.SetX(left);
itsUpperLeft.SetY(top);
itsUpperRight.SetX(right);
itsUpperRight.SetY(top);
itsLowerLeft.SetX(left);
itsLowerLeft.SetY(bottom);
itsLowerRight.SetX(right);
itsLowerRight.SetY(bottom);
}
//compute area of rectangle by finding sides,
//establish width and height and then multiply
int Rectangle::GetArea()const
{
int Width = itsRight - itsLeft;
int Height = itsTop - itsBottom;
return (Width * Height);
}
int main()
{
//initialize a local Rectangle variable
Rectangle MyRectangle(100, 20, 50, 80);
int Area = MyRectangle.GetArea();
std::cout << "Area: " << Area << "\n";
std::cout << "Upper Left x Coordinate: ";
std::cout << MyRectangle.GetUpperLeft().GetX();
getchar();
return 0;
}
注:在main函数中,MyRectangle.GetUpperLeft().GetX();这句话看起来很奇怪,但是这就是将GetUpperLeft()和GetX()组合在一起。声明只引入一个名称而不为其分配内存,而定义分配内存。
20.结构:与关键字class类型的关键字是struct。用来声明结构,在C++中,结构和类相同,只是将成员默认为公有的。可以像类一样声明结构,并给他声明数据成员和函数。事实上,如果遵循显示地声明类的公有和私有部分种良好的编程习惯,那么结构和类的声明方法没有什么区别。
第七部分 再谈程序流程
1.循环的鼻祖:goto。在计算机科学发展的早期,循环由标签、语句和跳转组成。在C++中,标签是后面跟冒号(:)的名称。标签放在合法C++语句的左边,跳转是通过关键字goto后面加上标签的名称实现的。
程序清单 7.1 使用关键字goto实现循环
#include
int main()
{
using namespace std;
int counter = 0;
loop:
counter++;
cout << "counter:" << counter << endl;
if (counter < 5)
goto loop;
cout << "Comolete.Counter:" << counter << endl;
return 0;
}
注:作为一条原则,程序员应避免使用goto语句。因为goto语句可以向前或向后跳转到源代码的任何位置,不加选择的使用goto语句会导致程序可读性和维护性很差。
2.while循环:只要开始条件为真,则执行while循环体,不断重复直到条件为假时。所以循环必须要有终止条件。while循环测试的条件可以与任何合法C++表达式一样复杂。
3.continue和break简介:有时候,需要在执行while循环体总所有语句之前返回到while循环开头。continue语句用于跳转到循环开头。而在另一些时候,需要在满足循环退出条件之前跳出循环。break语句立即跳出while循环,继续执行右括号后的语句。
程序清单 7.2 break和continue
#include
int main()
{
using namespace std;
unsigned short small;
unsigned long large;
unsigned long skip;
unsigned long target;
const unsigned short MaxSmall = 65535;
cout << "Enter a small number:";
cin >> small;
cout << "Enter a large number:";
cin >> large;
cout << "Enter a skip number:";
cin >> skip;
cout << "Enter a target number:";
cin >> target;
cout << endl;
while (small < large && small < MaxSmall)
{
small++;
if (small % large == 0)
{
cout << "skipping on" << small << endl;
continue;
}
if (large == target)
{
cout << "Target reached!";
break;
}
large -= 2;
}
cout << endl << "Small:" << small << "Large:" << large << endl;
return 0;
}
注:应慎用continue和break语句,因为他们的危险性仅次于goto语句。原因与goto语句相同。
4.while(true)循环:while循环测试条件可以是任何合法的C++表达式,只要条件为真,while循环将继续执行,可以将ture用作测试条件来创建永不结束的循环。
程序清单 7.5 条件永远成立的while循环
#include
int main()
{
int counter = 0;
while(true)
{
counter++;
if (counter > 10)
break;
}
std::cout << "Counter:" << counter << std::endl;
return 0;
}
5.实现do...while循环:while循环体可能永远都不会执行。while语句在执行循环体之前检查条件,如果条件为假,将跳出整个while循环体。
程序清单 7.6
#include
int main()
{
using namespace std;
int counter;
cout << "How many hellos?";
cin >> counter;
do
{
cout << "Hello" << endl;
counter--;
} while (counter > 0);
cout << "Counter is:" << counter << endl;
return 0;
}
注:如果期望循环体至少执行一次,请使用do..while。如果期望初始条件为假时不执行循环体,使用while循环。
6.for循环:使用while循环进行编程时,通常需要3步:设置初始条件、测试条件真假、在每次循环中修改条件变量。for循环将这三个步骤合并在一条语句中。for语句由关键字for和一对括号组成。括号中是3条用分号分隔的语句:for(initialization;test;action){statement}。第一个表达式initialization为初始条件,可以是任何合法的C++语句,但通常用于创建并初始化一个计数变量。第二个表达式test进行测试,可以是任何合法的C++表达式,其功能与while循环中的条件相同。第三个表达式action是要执行的操作,虽然可以是任何合法的C++语句,但是通常是将计数变量递增或递减。
程序清单 7.7 for循环
#include
int main()
{
int counter;
for (counter = 0; counter < 5; counter++)
std::cout << "Looping!";
std::cout << std::endl << "Counter:" << counter << std::endl;
return 0;
}
7.高级for循环:for语句功能强大且灵活,三条独立语句有多种变体。(1)可对多个变量进行初始化和递增(用逗号分开);(2)在for语句中使用空语句(三个独立语句都可以为空)。
8.空for循环:当不需要循环体做任何事情时,必须放一条空语句( ;)。分号可以与循环头位于同一行。注:不要给三个条件语句加太多东西,避免循环条件过于复杂难于理解。
9.嵌套循环:任何循环都可以嵌套到另一个循环中。外层循环每执行一次,内层循环将完整执行一遍。
程序清单 7.8 嵌套for循环
#include
int main()
{
using namespace std;
int rows, columns;
char theChar;
cout << "How many rows?";
cin >> rows;
cout << "How many columns?";
cin >> columns;
cout << "What character?";
cin >> theChar;
for (int i = 0; i < rows;i++)
{
for (int j = 0; j < columns; j++)
cout << theChar;
cout << endl;
}
getchar();
return 0;
}
10.for循环中声明的变量的作用域:ANSI标准做了规定,这些变量的作用域为for循环本身的语句块,但并非所有的编译器都支持这种改变。
程序清单 7.9 使用循环计算第N个Fibonacci数
#include
unsigned int fib(unsigned int position);
int main()
{
using namespace std;
unsigned int answer, position;
cout << "Which position?";
cin >> position;
answer = fib(position);
cout << answer << "is the ";
cout << position << "th Fibonacci number." << endl;
getchar();
return 0;
}
unsigned int fib(unsigned int n)
{
unsigned int minusTwo = 1, minusOne = 1, answer = 2;
if (n < 3)
return 1;
for (n -= 3; n != 0; n--)
{
minusTwo = minusOne;
minusOne = answer;
answer = minusOne + minusTwo;
}
return 0;
}
11.switch语句控制程序流程:switch可以为多个不同发分支。
switch语句的通用格式如下:
switch (expression)
{
case valueOne:statement;
break;
case valueTwo:statement;
break;
......
case valueN: statement;
break;
default: statement;
}
其中expression可以是任何合法的C++表达式,statement可以是任何合法的C++语句或语句块。需要注意的是case值必须为整数或为整数的表达式,但在这种表达式中不能使用任何关系运算符或布尔运算符。当某个case值与表达式匹配,程序跳转到该case后面的语句处执行。知道遇到break语句或到达switch语句块末尾为止。如果没有匹配的case值时,程序跳转到默认(default)分支处执行。如果没有默认分支,程序将跳出switch语句。需要注意的是:如果case语句后没有break语句,程序将继续执行下一条case语句。当需要这样做的时候,请用注释说明不用break的原因。
程序清单 7.10 switch语句的用法
#include
int main()
{
using namespace std;
unsigned short int number;
cout << "Enter a number between 1 and 5:";
cin >> number;
switch (number)
{
case 0:
cout << "Too small,sorry!";
break;
case 5:
cout << "Good job!" << endl;//fall through
case 4:
cout << "Nice Pick!" << endl;//fall through
case 3:
cout << "Excellent!" << endl;//fall through
case 2:
cout << "Matsterful!" << endl;//fall through
case 1:
cout << "Incredible!" << endl;
break;
default:
cout << "Too large!" << endl;
break;
}
cout << endl << endl;
getchar();
return 0;
}
12.使用switch语句来处理菜单:死循环被用来提供菜单,要求用户进行选择,然后根据用户的选择执行相应的操作并返回到菜单。
程序清单 7.11 死循环提供菜单
#include
int menu();
void DoTaskOne();
void DoTaskMany(int);
using namespace std;
int main()
{
bool exit = false;
for (;;)
{
int choice = menu();
switch (choice)
{
case 1:
DoTaskOne();
break;
case 2:
DoTaskMany(2);
break;
case 3:
DoTaskMany(3);
case 4:
continue;//redundant!
break;
case 5:
exit = true;
break;
default:
cout << "Please select again! " << endl;
break;
}
if (exit == true)
break;
}
return 0;
}
int menu()
{
int choice;
cout << "*** Menu *** " << endl << endl;
cout << "(1) Choice one. " << endl;
cout << "(2) Choice two. " << endl;
cout << "(3) Choice three. " << endl;
cout << "(4) Redisplay menu. " << endl;
cout << "(5) Quit. " << endl << endl;
cout << ":";
cin >> choice;
return choice;
}
void DoTaskOne()
{
cout << "Task one! " << endl;
}
void DoTaskMany(int which)
{
if (which == 2)
cout << "Task Two!" << endl;
else
cout << "Task There!" << endl;
}
注:main函数中的case4中的continue语句是多余的。如果删除这条语句,从而结束了switch语句,且exit的值为假,因此重新执行循环,进而打印菜单,然而,continue确实避免了对exit的测试。
第一周复习:
程序清单 R1.1 第一周课程复习
/*
*Listing :WR01.cpp
*Description:week in Review listing for week 1
*=============================================*/
#include
using namespace std;
enum CHOICE
{
DrawRect = 1,
GetArea ,
GetPerim ,
ChangDimensions,
Quit
};
//Rectangle class declaration
class Rectangle
{
public:
//constructors
Rectangle(int width, int height);
~Rectangle();
//accessors
int GetHeight() const { return itsHeight; }
int GetWidth() const { return itsWidth; }
int GetArea() const { return itsHeight * itsWidth; }
int GetPerim() const { return 2 * itsHeight + 2 * itsWidth; }
void SetSize(int newWidth, int newHeight);
// Misc.methods
private:
int itsHeight;
int itsWidth;
};
//Class method implementations
void Rectangle::SetSize(int newWidth, int newHeight)
{
itsHeight = newHeight;
itsWidth = newWidth;
}
Rectangle::Rectangle(int width, int height)
{
itsHeight = height;
itsWidth = width;
}
Rectangle::~Rectangle() { }
int DoMenu();
void DoDrawRect(Rectangle);
void DoGetArea(Rectangle);
void DoGetPerim(Rectangle);
/*==============================================*/
int main()
{
//initialize a rectangle to 30,5
Rectangle theRect(30, 5);
int choice = DrawRect;
int fQuit = false;
while (!fQuit)
{
choice = DoMenu();
if (choice < DrawRect || choice > Quit)
{
cout << endl << "Invalid Choice,try again.";
cout << endl << endl;
continue;
}
switch (choice)
{
case DrawRect:
DoDrawRect(theRect);
break;
case GetArea:
DoGetArea(theRect);
break;
case GetPerim:
DoGetPerim(theRect);
break;
case ChangDimensions :
int newLength, newWidth;
cout << endl << "New width:";
cin >> newWidth;
cout << "New height:";
cin >> newLength;
theRect.SetSize(newWidth, newLength);
DoDrawRect(theRect);
break;
case Quit:
fQuit = true;
cout << endl << "Exiting..." << endl << endl;
break;
default:
cout << "Error in choice!" << endl;
fQuit = true;
break;
} //end switch
} //end while
return 0;
} //end main
int DoMenu()
{
int choice;
cout << endl << endl;//create two new lines
cout << "*** Menu ***" <> choice;
return choice;
}
void DoDrawRect(Rectangle theRect)
{
int height = theRect.GetHeight();
int width = theRect.GetWidth();
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
cout << "*";
cout << endl;
}
}
void DoGetArea(Rectangle theRect)
{
cout << "Area :" << theRect.GetArea() << endl;
}
void DoGetPerim(Rectangle theRect)
{
cout << "Perimeter :" << theRect.GetPerim() << endl;
}
//================== End of Listing ===================
第二周课程
第八部分 理解指针
8.1 .指针:指针是存储内存的地址的变量。注:能够使用指针以及在低层操纵内存,是C++被选择用于编写嵌入式和实时应用程序的原因之一。
8.2.内存简介:计算机内存被划分成按顺序编号的内存单元。每个变量都位于某个独特的内存单元,这被称为地址。
8.3.获取变量的内存地址:不同的计算机使用不同的复杂方案对内存进行编号。要获取变量的内存地址,可以使用地址运算符&,它返回对象在内存中的地址。
程序清单 8.1 地址运算符
#include
int main()
{
using namespace std;
unsigned short shortVar = 5;
unsigned long longVar = 65535;
long sVar = -65535;
cout << "shortVar:\t" << shortVar;
cout << "\tAddress of shortVar:\t";
cout << &shortVar << endl;
cout << "longVar:\t" << longVar;
cout << "\tAddress of longVar:\t";
cout << &longVar << endl;
cout << "sVar:\t\t" << sVar;
cout << "\tAddress of sVar:\t";
cout << &sVar << endl;
return 0;
}
8.4.将变量的地址存储的指针中:每个变量都有地址,即使不知道变量的具体地址,也可以将其地址存储到指针中。
8.5.指针名:指针是变量,所以可以使用任何合法的变量名,大多程序员采用这样的命名规则:所有指针均已p开头。例如:int * pAge = 0,其中pAge被初始化为0,值为0的指针被称为空指针。所有指针在创建时都应该初始化,当不知道初始化为什么时候,将0赋给它。没有被初始化的指针被称为失控指针(wild poiner),因为不知道它指向何处,所以对其操作时,可能带来不可预料的后果。
8.6.指针初始化:要指针存储一个地址,必须给其赋值地址。例如:unsigned short int howOld = 50; unsigned short int * pAge = &howOld;
8.7.获取指针指向的变量的值:通过指针访问被指针指向的变量的值被称为间接访问,间接访问意味着访问位于指针存储的地址处的值。注:常规变量告诉编译器,需要多少内存来存储该变量的值,但是指针没有这样的功能,所有的指针长度都相同:在32位处理器的机器上为4个字节,在64位处理器的计算机上为8字节。指针的类型告诉编译器,它存储的地址处的对象需要多少内存。
8.8.使用间接运算符解除引用:间接运算符(*)也被称为解除引用(dereference)运算符。对指针解除引用时,将得到指针存储的地址处的值。例如:unsigned short int yourAge;yourAge = * pAge;
8.9.使用指针来操作数据:除使用间接运算符来查看存储在指针指向的位置处的数据外,还可以对数据进行操纵。将变量的地址赋给指针后,可以使用该指针来访问它指向的变量中的数据。
程序清单 8.2 使用指针来操作数据
#include
typedef unsigned short int USHORT;
int main()
{
using namespace std;
USHORT myAge;
USHORT * pAge = 0;
myAge = 5;
cout << "myAge :" << myAge << endl;
pAge = &myAge;
cout << "pAge:" << *pAge << endl;
cout << "Setting *pAge = 7..." << endl;
*pAge = 7;
cout << "pAge:" << *pAge << endl;
cout << "myAge :" << myAge << endl;
cout << "Setting *pAge = 9..." << endl;
*pAge = 9;
cout << "pAge:" << *pAge << endl;
cout << "myAge :" << myAge << endl;
return 0;
}
8.10.查看地址:指针可以操作地址,而无需知道其实际值,将变量的地址赋给指针时,指针的值将为变量的地址
程序清单 8.3 确定指针中存储的内容
#include
int main()
{
using namespace std;
unsigned short int myAge = 5, yourAge = 10;
unsigned short int *pAge = &myAge;
cout << "myAge:\t" << myAge
<< "\t\tyourAge:\t" << yourAge << endl;
cout << "&myAge:\t" << &myAge
<< "\t&yourAge:\t" << &yourAge << endl;
cout << "pAge:\t" << pAge << endl;
cout << "*pAge:\t" << *pAge << endl;
cout << "\nReassigning:pAge = &yourAge..." << endl << endl;
pAge = &yourAge;
cout << "myAge:\t" << myAge <<
"\t\tyourAge:\t" << yourAge << endl;
cout << "&myAge:\t" << &myAge
<< "\t&yourAge:\t" << &yourAge << endl;
cout << "pAge:\t" << pAge << endl;
cout << "*pAge:\t" << *pAge << endl;
cout << "\n&pAge:\t" << &pAge << endl;
return 0;
}
8.11. 使用指针的原因:1、管理自由存储区中的数据;2、访问类的成员数据和函数;3、按引用传递参数。
8.12.栈和自由存储区(堆):五个内存区域中有:1、全局名称空间;2、自由存储区;3、寄存器;4、代码空间;5、堆栈。局部变量和函数参数位于堆栈中,代码位于代码空间,全局变量位于全局名称区,寄存器用于内存管理工作。余下的所有内存全部被当作自由存储区,被称为堆。注:自由存储区的优点:从中分配的内存将一直使用,直到不在需要时,将其释放,且如果在函数中分配自由存储区中的内存,在函数释返回后该内存还可使用。缺点:如果没有释放内存,将会导致内存被占据而导致内存崩溃。
8.13.用new来分配内存:C++中,使用关键字new来分配自由存储区中的内存。在new后跟上要为其分配内存的对象的类型。让编译器知道需要多少内存,例如:new unsigned short int在自由存储区中分配2字节内存。new的返回值是一个地址,因此应将new的返回值赋给一个指针。
8.14.使用delete释放内存:使用完内存后,必须将其释放。因此将delete对指针应用,delete将内存释放。注:new分配的内存不会自动被释放,如果指针变量指向自由存储区中的内存块,离开该指针的作用域时,该内存不会被自动释放。相反,当该内存被视为分配出去后,该指针将不再可用。这被称为内存泄漏。但是当使用过delete后,只是释放了内存,该指针仍然可用,可重新赋值。
8.15.delete的使用注意点:当delete用于指针时,它指向的内存被释放,当再次对该指针使用delete时,将会引起不可预知的后果,所以当指针被释放后,将指针指向0,即将其设置为空指针。对空指针使用deletd是安全的。
程序清单 8.4 分配、使用和删除指针
#include
int main()
{
using namespace std;
int localVariable = 5;
int * pLocal = &localVariable;
int * pHeap = new int;
*pHeap = 7;
cout << "localVariable:" << localVariable << endl;
cout << "*pLocal:" << *pLocal << endl;
cout << "pHeap:" << *pHeap << endl;
delete pHeap;
pHeap = new int;
*pHeap = 9;
cout << "pHeap:" << *pHeap << endl;
delete pHeap;
return 0;
}
8.16.内存泄漏的两种原因:1、当程序返回前,指针指向的内存没有被释放;2、在指针被重新赋值之前没有被释放。
8.17.在自由存储区上创建对象:在自由存储区中,可以创建任何数据类型的指针,当创建类对象时,将调用构造函数。
8.18.在自由存储区上删除对象:将delete用于指向自由存储区中的对象的指针时,在释放内存前将调用对象的析构函数。这给类提供了一个执行清理工作的机会。
程序清单 8.5 在自由存储区中创建和删除对象
#include
using namespace std;
class SimpleCat
{
public:
SimpleCat();
~SimpleCat();
private:
int itsAge;
};
SimpleCat::SimpleCat()
{
cout << "Constructor called." << endl;
itsAge = 1;
}
SimpleCat::~SimpleCat()
{
cout << "Destructor called." << endl;
}
int main()
{
cout << "SimpleCat Frisky..." << endl;
SimpleCat Frisky;
cout << "SimpleCat *pRags = new SimpleCat..." << endl;
SimpleCat * pRags = new SimpleCat;
cout << "delete pRag..." << endl;
delete pRags;
cout << "Exiting, watch Frisky go..." << endl;
return 0;
}
8.19.访问数据成员:访问类的数据成员和函数可以使用句号运算符(.)来。这种方法适用于堆栈中创建的类。使用指针来访问对象的成员要先对指针解除引用,然后将句点运算符用于指针指向的对象,例如:(*pCat).GetAge();由于这样使用比较麻烦,所以C++为间接访问提供了一个简捷运算符:类成员访问运算符(->)。该运算符又短线和大于号组成。
8.20.在自由存储区中创建成员数据:可在自由存储区中创建对象的数据成员。类的数据成员可以是指向自由存储区中对象的指针。
程序清单 8.6 将指针用作成员数据
#include
class SimpleCat
{
public:
SimpleCat();
~SimpleCat();
int GetAge() const { return *itsAge; }
void SetAge(int age) { *itsAge = age; }
int GetWeight() const { return *itsWeight; }
void setWeight(int weight) { *itsWeight = weight; }
private:
int * itsAge;
int * itsWeight;
};
SimpleCat::SimpleCat()
{
itsAge = new int(2);
itsWeight = new int(5);
}
SimpleCat::~SimpleCat()
{
delete itsAge;
delete itsWeight;
}
int main()
{
using namespace std;
SimpleCat * Frisky = new SimpleCat;
cout << "Frisky is " << Frisky->GetAge()
<< " years old" << endl;
Frisky->SetAge(5);
cout << "Frisky is " << Frisky->GetAge()
<< " years old" << endl;
delete Frisky;
return 0;
}
注:本程序的中使用指针的方法极其愚蠢,除非有充分理由要求Cat对象通过引用来存储成员。即,将数据成员声明为指针想达到的目的是什么?假如要设计的是另一个对象作为数据成员的对象,但前者被创建之前就已存在,且在后者消失后仍将存在,则后者必须通过引用来包含前者。例如:后者是一个窗口,前者是一个文档,窗口需要访问文档,但不能控制文档的生命周期,因此窗口需要通过引用来包含文档。
8.21.this指针:每个类成员函数都有一个隐藏的参数:this指针。this指针指向当前对象,因此,在每次调用函数时,函数都通过一个隐藏参数受到了一个this指针,该指针指向通过它调用的函数对象。
程序清单 8.7 使用this指针
#include
class Rectangle
{
public:
Rectangle();
~Rectangle();
void SetLength(int length) { this->itsLength = length; }
int GetLength() const { return this->itsLength; }
void SetWidth(int width) { itsWidth = width; }
int GetWidth() const { return itsWidth; }
private:
int itsLength;
int itsWidth;
};
Rectangle::Rectangle()
{
itsWidth = 5;
itsLength = 10;
}
Rectangle::~Rectangle(){}
int main()
{
using namespace std;
Rectangle theRect;
cout << "theRect is " << theRect.GetLength()
<< " feet long." << endl;
cout << "theRect is " << theRect.GetWidth()
<< " feet wide." << endl;
theRect.SetLength(20);
theRect.SetWidth(10);
cout << "theRect is " << theRect.GetLength()
<< " feet long." << endl;
cout << "theRect is " << theRect.GetWidth()
<< " feet wide." << endl;
return 0;
}
8.22.迷途指针:在C++中,导致难以发现和解决的错误的罪魁祸首之一迷途(stray)指针。迷途指针也称为失控(wild)指针或悬浮(dangling)指针。是将delete用于指针,但没有将它设置为空时引发的。如果在没有重新赋值的情况下使用该类指针,会导致不可预计的错误。
程序清单 8.8 创建迷途指针
typedef unsigned short int USHORT;
#include
int main()
{
USHORT *pInt = new USHORT;
*pInt = 10;
std::cout << "*pInt:" << *pInt << std::endl;
delete pInt;
long * pLong = new long;
*pLong = 90000;
std::cout << "*pLong:" << *pLong << std::endl;
*pInt = 20;
std::cout << "*pInt:" << *pInt << std::endl;
std::cout << "*pLong:" << *pLong << std::endl;
delete pLong;
return 0;
}
注:不要尝试生成该程序,因为可能导致计算机死锁。使用迷途指针或空指针是非法的,因为可能会导致程序崩溃。如果是空指针,程序将崩溃,这也是空指针相对于迷途之中呢的另一个有点:可预测的崩溃更可取,因为更容易调试。
8.23.使用const指针:声明指针时,在类型前或后使用关键字const,也可以在两个位置都使用。例如:const int * pOne;int * const pTwo;const int * const pThree;但是,这些声明的含义是不同的。pOne是一个指向整型常量的指针,它指向的值不可以被修改。pTwo是一个指向整型的常量指针,它指向的值可以修改,但pTwo不可以指向其他变量。pThree是一个指向整型常量的常量指针,它指向的值不可以被修改,且这个指针也不能指向其他变量。
8.24.const指针和const成员函数:可以将关键字const用于成员函数,当函数被声明为const时,不可以修改对象的数据。如果声明了一个指向const对象的指针,则通过该指针只能调用const方法。
程序清单 8.9 使用指向const对象的指针
#include
using namespace std;
class Rectangle
{
public:
Rectangle();
~Rectangle();
void SetLength(int length) { itsLength = length; }
int GetLength() const { return itsLength; }
void SetWidth(int width) { itsWidth = width; }
int GetWdith() const { return itsWidth; }
private:
int itsWidth;
int itsLength;
};
Rectangle::Rectangle()
{
itsWidth = 5;
itsLength = 10;
}
Rectangle::~Rectangle() {}
int main()
{
Rectangle * pRect = new Rectangle;
const Rectangle * pConstRect = new Rectangle;
Rectangle * const pConstPtr = new Rectangle;
cout << "pRect width: " << pRect->GetWdith()
<< " feet " << endl;
cout << "pConstRect width: " << pConstRect->GetWdith()
<< " feet " << endl;
cout << "pConstPtr width: " << pConstPtr->GetWdith()
<< " feet " << endl;
pRect->SetWidth(10);
pConstPtr->SetWidth(10);
cout << "pRect width: " << pRect->GetWdith()
<< " feet " << endl;
cout << "pConstRect width: " << pConstRect->GetWdith()
<< " feet " << endl;
cout << "pConstPtr width: " << pConstPtr->GetWdith()
<< " feet " << endl;
return 0;
}
8.25. 使用const this指针:将对象声明为const时,相当于将该对象的this指针声明为一个指向const对象的指针,const this指针只能用来调用const成员函数。注:当对象不需要被修改,则按引用传递它时应该使用const进行保护。
第九部分 使用引用
9.1.引用:引用就是别名,创建引用时,你将其初始化为另一个对象(即目标)的名称。然后,引用将成为目标的另一个名称,对引用执行的任何操作实际上都是针对目标的。
9.2.创建引用:首先给出目标对象的类型,然后加上引用运算符(&)、引用名称、等号和目标对象名称。引用名可以是任意合法的变量名,但很多程序员喜欢在引用名前加r。例如:int & rSomeRef = someInt;注:引用与其他变量的区别是:引用声明的同时必须对其进行初始化,如果创建时不给其赋值,将出现编译错误。引用运算符之前的空格必不可少。
程序清单 9.1 创建和使用引用
#include
int main()
{
using namespace std;
int intOne;
int &rSomeRef = intOne;
intOne = 5;
cout << "intOne: " << intOne << endl;
cout << "rSomeRef:" << rSomeRef << endl;
rSomeRef = 7;
cout << "intOne: " << intOne << endl;
cout << "rSomeRef:" << rSomeRef << endl;
return 0;
}
9.3.将地址运算符用于引用:符号&可以获取变量的地址以及声明引用,如果将地址运算符用于引用变量将返回其指向的目标的地址。这就是引用的特征,引用是目标的别名。
程序清单 9.2 获取引用的地址
#include
int main()
{
using namespace std;
int intOne;
int &rSomeRef = intOne;
intOne = 5;
cout << "intOne: " << intOne << endl;
cout << "rSomeRef:" << rSomeRef << endl;
cout << "&intOne: " << &intOne << endl;
cout << "&rSomeRef:" << &rSomeRef << endl;
return 0;
}
注:在C++中,没有提供获取引用本身的地址的方法,因为与指针或其他变量不同,获取引用本身的地址毫无意义。
9.4.不能给引用重新赋值:引用不能重新赋值,引用变量总是目标的别名,给引用重新赋值相当于给目标重新赋值。
9.5.引用对象:任何对象都可以被引用,包括用户定义的对象。指向对象的引用就像使用对象本身一样,要通过对象的引用来访问成员数据和方法,可使用类成员访问运算符(.);和内置类型的引用一样,对象的引用也是对象的别名。
程序清单 9. 3 指向对象的引用
#include
class SimpleCat
{
public:
SimpleCat(int age, int weight);
SimpleCat() {}
int GetAge() { return itsAge; }
int GetWeight() { return itsWeight; }
private:
int itsAge;
int itsWeight;
};
SimpleCat::SimpleCat(int age, int weight)
{
itsAge = age;
itsWeight = weight;
}
int main()
{
SimpleCat Frisky(5, 8);
SimpleCat & rCat = Frisky;
std::cout << "Frisky is: ";
std::cout << Frisky.GetAge() << " years old." << std::endl;
std::cout << "And Frisky weighs: ";
std::cout << rCat.GetWeight() << " pounds." << std::endl;
return 0;
}
9.6.空指针和空引用:在指针没有被初始化或删除指针时,应将它们赋值为空(0),但引用并非如此,因为必须在创建的同时将引用初始化。然而,为了让C++可用于编写能够直接访问硬件的设备驱动程序、嵌入式系统和实时系统,引用特定地址的能力很有用,因此大多数编译器允许将引用初始化为空或数值,仅当在引用非法时使用对象的时候,才会导致错误。然而在常规编程中,使用这种支持可能会导致难以预计的错误。
9.7.按引用传递函数参数:在C++中,按引用传递参数的方式有两种:使用指针和使用引用。二者虽然语法不同,但使用效果相同:不是创建一个作用域为整个函数的拷贝,而是让函数能访问原始对象。
程序清单 9.4 按值传递、使用指针传递和使用引用传递
#include
using namespace std;
void swap1(int x, int y);
void swap2(int *px, int *py);
void swap3(int &x, int &y);
int main()
{
int x = 5, y = 10;
cout << "Main.Before swap1 x:" << x << "y:" << y << endl;
swap1(x, y);
cout << "Main.Before swap1 x:" << x << "y:" << y << endl << endl;
cout << "Main.Before swap2 x: "<< x << "y:" << y << endl;
swap2(&x, &y);
cout << "Main.Before swap2 x: "<< x << "y:" << y << endl << endl;
cout << "Main.Before swap3 x:" << x << "y:" << y << endl;
swap3(x, y);
cout << "Main.Before swap3 x:" << x << "y:" << y << endl << endl;
return 0;
}
void swap1(int x, int y)
{
int temp;
cout << "Swap1.Before swap x:" << x << "y:" << y << endl;
temp = x;
x = y;
y = temp;
cout << "Swap1.Before swap x:" << x << "y:" << y << endl;
}
void swap2(int *px, int *py)
{
int temp;
cout << "Swap2.Before swap x:" << *px << "y:" << *py << endl;
temp = *px;
*px = *py;
*py = temp;
cout << "Swap2.Before swap x:" << *px << "y:" << *py << endl;
}
void swap3(int &x, int &y)
{
int temp;
cout << "Swap3.Before swapt x:" << x << "y:" << y << endl;
temp = x;
x = y;
y = temp;
cout << "Swap3.Before swap x:" << x << "y:" << y << endl;
}
9.8. 返回多个值:函数只能返回一个值,但是需要从函数那里获得多个值。解决方法之一就是:按引用将两个对象传递给函数。然后函数就可以将正确的值赋给这两个对象了。同样,指针也可以实现。并且由于可以直接修改原始对象,所以函数的返回值可保留用来报告错误。
程序清单 9.5 通过指针和引用来返回值
#include
using namespace std;
short Factor1(int n, int * pSquared, int * pCubed);
short Factor2(int n, int & pSquared, int & pCubed);
int main()
{
int number, squared, cubed;
short error;
cout << "Enter a number (0 - 20):";
cin >> number;
error = Factor1(number, &squared, &cubed);
if (!error)
{
cout << "number:" << number << endl;
cout << "square:" << squared << endl;
cout << "cubed:" << cubed << endl;
}
else
cout << "Error encountered!!" << endl;
cout << "Enter a number (0 - 20):";
cin >> number;
error = Factor2(number, squared, cubed);
if (!error)
{
cout << "number:" << number << endl;
cout << "square:" << squared << endl;
cout << "cubed:" << cubed << endl;
}
else
cout << "Error encountered!!" << endl;
return 0;
}
short Factor1(int n, int * pSquared, int * pCubed)
{
short value = 0;
if (n > 20)
value = 1;
else
{
*pSquared = n * n;
*pCubed = n * n * n;
value = 0;
}
return value;
}
short Factor2(int n, int & pSquared, int & pCubed)
{
short value = 0;
if (n > 20)
value = 1;
else
{
pSquared = n * n;
pCubed = n * n * n;
value = 0;
}
return value;
}
注:由于按引用传递时不能控制对对象属性和方法的访问,因此应在让函数能完成其工作的情况下。尽可能少的访问权限提供给函数,这有助于保护数据。
9.9.按引用传递的高效性:通过传值时,将生成拷贝,但这样既费时又占用内存。并且当创建临时拷贝时,都要调用:复制构造函数。函数返回时,临时对象将被销毁,这将调用析构函数。按引用访问时,将会减少很多开销,减少内存和时间的消耗。
程序清单 9.6 按引用传递对象
#include
using namespace std;
class SimpleCat
{
public:
SimpleCat();
SimpleCat(SimpleCat &);
~SimpleCat();
};
SimpleCat::SimpleCat()
{
cout << "调用默认构造函数!" << endl;
}
SimpleCat::SimpleCat(SimpleCat &)
{
cout << "调用复制构造函数!" << endl;
}
SimpleCat::~SimpleCat()
{
cout << "调用析构函数!" << endl;
}
SimpleCat FunctionOne(SimpleCat theCat);
SimpleCat * FunctionTwo(SimpleCat *theCat);
int main()
{
cout << "生成一个cat对象..." << endl;
SimpleCat Frisky;
cout << "调用函数FunctionOne..." << endl;
FunctionOne(Frisky);
cout << "调用函数FunctionTwo..." << endl;
FunctionTwo(&Frisky);
return 0;
}
SimpleCat FunctionOne(SimpleCat theCat)
{
cout << "Function One正在运行..." << endl;
return theCat;
}
SimpleCat * FunctionTwo(SimpleCat *theCat)
{
cout << "Function Two正在运行..." << endl;
return theCat;
}
9.10.传递const指针:当按引用传递参数时,又不希望修改原始数值,则可传递const指针,防止被修改
程序清单 9.7 传递指向const对象的指针
#include
using namespace std;
class SimpleCat
{
public:
SimpleCat();
SimpleCat(SimpleCat&);
~SimpleCat();
int GetAge() const { return itsAge; }
void SetAge(int age) { itsAge = age; }
private:
int itsAge;
};
SimpleCat::SimpleCat()
{
cout << "调用构造函数..." << endl;
itsAge = 1;
}
SimpleCat::SimpleCat(SimpleCat&)
{
cout << "调用复制构造函数..." << endl;
}
SimpleCat::~SimpleCat()
{
cout << "调用析构函数..." << endl;
}
const SimpleCat * const FunctionTwo(const SimpleCat * const theCat);
int main()
{
cout << "生成一个Cat..." << endl;
SimpleCat Frisky;
cout << "Frisky is ";
cout << Frisky.GetAge();
cout << " years old." << endl;
int age = 5;
Frisky.SetAge(5);
cout << "Frisky is ";
cout << Frisky.GetAge();
cout << " years old." << endl;
cout << "Calling FunctionTwo... " << endl;
FunctionTwo(&Frisky);
cout << "Frisky is ";
cout << Frisky.GetAge();
cout << " years old." << endl;
return 0;
}
const SimpleCat * const FunctionTwo(const SimpleCat * const theCat)
{
cout << "Function Two正在运行..." << endl;
cout << "Frisky is now" << theCat->GetAge();
cout << " years old" << endl;
return theCat;
}
注:当传递的不是指针而是引用将更加方便。
9.11.使用引用和指针的时机:相对指针来说,C++程序员更加喜欢使用引用,引用不但清晰而且更加容易使用,也更好的隐藏信息,然而引用不可以被重新赋值,如果需要首先指向一个对象,然后再指向下一个,则必须使用指针。并且如果对象可能为空,必须使用指针,因为引用不可以为空。
9.12.&和*的放置位置:有三种放置方法:1、int * a;2、int* a;3、int *a;很多程序员喜欢将&和*放置在中间,两边各加一个空格。需要注意的是:不要在同一行中同时声明引用、指针和变量。
9.13.返回指向不再作用域中的对象的引用:引用不可过度使用,当使用时,需要考虑其是否还在作用域中。
程序清单 9.8 返回指向不存在的对象的引用
#include
class SimpleCat
{
public:
SimpleCat(int age, int weight);
SimpleCat(){}
int GetAge() { return itsAge; }
int GetWeight() { return itsWeight; }
private:
int itsAge;
int itsWeight;
};
SimpleCat::SimpleCat(int age, int weight)
{
itsAge = age;
itsWeight = weight;
}
SimpleCat & TheFunction();
int main()
{
SimpleCat & rCat = TheFunction();
int age = rCat.GetAge();
std::cout << "rCat is " << age << " years old! " << std::endl;
return 0;
}
SimpleCat & TheFunction()
{
SimpleCat Frisky(5, 9);
return Frisky;
}
注:该程序不能被编译通过(Microsoft C++编译器可以),这是一种很糟糕的编程习惯。
9.14.指针的归所:程序在自由存储区中分配内存时,返回一个指针,必须让一个指针指向该内存,如果不这样做,会导致内存泄漏。然而在一个函数中申请了内存,在另外一个函数里释放是非常危险的。所以当需要在一个函数中分配内存,并将它传递给调用它的函数,应考虑修改接口,让调用函数分配内存,然后按引用将其传递给被调用的函数。这将把所有的内存管理工作转移到程序外,由释放内测的函数负责分配。
第十部分 有关函数的高级主题
10.1. 重载成员函数:第五部分介绍了如何通过多个名称相同参数不同的函数来实现多态(函数重载)。同理,成员函数也是可以重载的。
程序清单 10.1 重载成员函数
#include
class Rectangle
{
public:
Rectangle(int width, int height);
~Rectangle() {}
void DrawShape() const;
void DrawShape(int aWdith, int aHeight) const;
private:
int itsWidth;
int itsHeight;
};
Rectangle::Rectangle(int width, int height)
{
itsWidth = width;
itsHeight = height;
}
void Rectangle::DrawShape() const
{
DrawShape(itsWidth, itsHeight);
}
void Rectangle::DrawShape(int width, int height) const
{
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
std::cout << "*";
std::cout << std::endl;
}
}
int main()
{
Rectangle theRect(30, 5);
std::cout << "DrawShap():" << std::endl;
theRect.DrawShape();
std::cout << "\nDrawShap(40,2):" << std::endl;
theRect.DrawShape(40, 2);
return 0;
}
10.2.使用默认值:全局函数可以有一个或多个默认值,类成员函数也可以有。声明默认值的规则也相同。
10.3.选择默认值还是重载函数:以下情况下,需要使用函数重载:1、没有合理的默认值;2、需要不同的算法;3、需要在参数列表中支持不同的类型。
10.4.默认构造函数:构造函数用于创建对象,而构造函数的存在是面向对象编程的优点之一:调用程序无需做任何工作来确保对象始于自相容状态。可以自己创建默认构造函数,不接受任何参数,但根据需要对对象设置。注:当创建了任何构造函数后,编译器将不会提供默认构造函数,因此,如果需要一个不接受任何参数的构造函数,并且已经创建了其他构造函数,则必须自己添加默认构造函数。
10.5.重载构造函数:和所有成员函数一样,构造函数也可以被重载。并且重载构造函数非常强大和灵活。
程序清单 10.2. 重载构造函数
#include
using namespace std;
class Rectangle
{
public:
Rectangle();
Rectangle(int width, int length);
~Rectangle() {}
int GetWdith() const { return itsWidth ;}
int GetLength() const { return itsLength; }
private:
int itsWidth;
int itsLength;
};
Rectangle::Rectangle()
{
itsWidth = 5;
itsLength = 10;
}
Rectangle::Rectangle(int width, int length)
{
itsWidth = width;
itsLength = length;
}
int main()
{
Rectangle Rect1;
cout << "Rect1 width: " << Rect1.GetWdith() << endl;
cout << "Rect1 length: " << Rect1.GetLength() << endl;
int aWdith, aLength;
cout << "Enter a width: ";
cin >> aWdith;
cout << "\nEnter a length: ";
cin >> aLength;
Rectangle Rect2(aWdith, aLength);
cout << "\nRect2 width: " << Rect2.GetWdith() << endl;
cout << "Rect2 length: " << Rect2.GetLength() << endl;
return 0;
}
10.6.初始化对对象:构造函数分两个阶段调用的:初始化阶段和函数体执行阶段。大多数变量可以在任何一个阶段设置;在初始化阶段进行初始化或者在构造函数中进行赋值。在初始化阶段初始化更加清晰且效率更高。在构造函数的右括号后面加一个冒号(:),然后是成员变量的名称和一对圆括号。圆括号内是用来初始化成员变量的表达式。如果需要初始化多个变量,用逗号将它们分开。例如:Rectangle::Rectangle() : itsWidth(5), itsLength(10) { }。注:有些变量必须初始化而不能赋值,如常量和引用。构造函数体中通常包含其他赋值语句或操作语句,然而尽可能使用初始化。
10.7.复制构造函数:除了提供默认构造函数和析构函数外,编译器还提供一个默认复制函数。每当创建对象的拷贝时,都将调用复制构造函数。且所有的复制构造函数都接受一个参数:指向其所属类的对象的引用。应将该引用声明为const。默认复制构造函数将作为参数传入的对象的每个成员变量复制到新对象的成员变量中,这被称为成员复制(浅复制),这虽然对大多数成员变量来说是可行的,但对于指向自由存储区中的对象的指针成员变量不可行。浅复制只是将对象成员变量的值复制到另外的对象中,两个对象中的指针最后将指向同一个内存。当其中一个对象不存在后,释放了该内存,将会产生迷途指针。解决方法是:创建自己的复制构造函数,并根据需要分配内存。分配内存后,便可以将原来的值复制到新的内存中。这被称为深层复制。
程序清单 10.3 复制构造函数
#include
using namespace std;
class Cat
{
public:
Cat();
Cat(const Cat &);
~Cat();
int GetAge() const { return *itsAge; }
int GetWeight() const { return *itsWeight; }
void SetAge(int age) { *itsAge = age; }
private:
int *itsAge;
int *itsWeight;
};
Cat::Cat()
{
itsAge = new int;
itsWeight = new int;
*itsAge = 5;
*itsWeight = 9;
}
Cat::Cat(const Cat & rhs)
{
itsAge = new int;
itsWeight = new int;
*itsAge = rhs.GetAge();
*itsWeight = *(rhs.itsWeight);
}
Cat::~Cat()
{
delete itsAge;
itsAge = 0;
delete itsWeight;
itsWeight = 0;
}
int main()
{
Cat Frisky;
cout << "Firsky's age: " << Frisky.GetAge() << endl;
cout << "Setting Frisky to 6...\n";
Frisky.SetAge(6);
cout << "Creating Boots from Frisky\n";
Cat Boots(Frisky);
cout << "Frisky's age: " << Frisky.GetAge() << endl;
cout << "Boots'age: " << Boots.GetAge() << endl;
cout << "setting Frisky to 7...\n";
Frisky.SetAge(7);
cout << "Frisky's age: " << Frisky.GetAge() << endl;
cout << "Boot's age: " << Boots.GetAge() << endl;
return 0;
}
10.8. 运算符重载:C++允许内置的运算符可以被重载以支持被创建的类。重载通过声明下述的形式的函数:returnType operator op() 其中op是要被重载的运算符。例如:void operator ++()。operator是关键字。
程序清单 10.4 重载运算符++
#include
using namespace std;
class Counter
{
public:
Counter();
~Counter() {}
int GetIstVal() const { return itsVal; }
int SetIstVal(int x) { itsVal = x; }
void Increment() { ++itsVal; }
void operator ++() { ++itsVal; }
private:
int itsVal;
};
Counter::Counter():itsVal(0) { }
int main()
{
Counter i;
cout << "The value of i is " << i.GetIstVal() << endl;
i.Increment();
cout << "The value of i is " << i.GetIstVal() << endl;
++i;
cout << "The value of i is " << i.GetIstVal() << endl;
return 0;
}
10.9.运算符重载函数的返回类型:需要返回一个对象,以便可以将其赋给另外以一个对象。一种方法是创建一个临时对象将其返回;一种方法是返回无名临时对象;一种方法是使用this指针。其中创建临时对象变量的值与当前对象的相同,返回的临时变量立刻赋值。而返回无名临时对象时,创建了对象,然后直接返回值,虽然更加精简,但是意义不大。而this指针被传递给所有成员函数,在创建的对象中,this指针指向对象,如果将其解引用,将返回原对象,而不用创建临时对象。
程序清单 10.5 返回this指针
#include
using namespace std;
class Counter
{
public:
Counter();
~Counter() {};
int GetItsVal() const { return itsVal; }
void SetItsVal(int x ) { itsVal = x; }
void Increment() { ++itsVal; }
const Counter & operator ++();
private:
int itsVal;
};
Counter::Counter() :itsVal(0) { };
const Counter & Counter :: operator++()
{
++itsVal;
return *this;
}
int main()
{
Counter i;
cout << "The value of i is " << i.GetItsVal() << endl;
i.Increment();
cout << "The value of i is " << i.GetItsVal() << endl;
++i;
cout << "The value of i is " << i.GetItsVal() << endl;
Counter a = ++i;
cout << "The value of a: " << a.GetItsVal();
cout << " and i:" << i.GetItsVal() << endl;
return 0;
}
10.10.重载后缀运算符:根据约定,在声明后缀运算符函数时,提供一个int 参数。该参数的值被忽略。它只是一个信号,指出这是后缀运算符。
10.11.前缀和后缀运算符的区别:前缀运算符的的意思是先递增后使用,而后缀则是先使用后递增。所以当重载时,前缀只需要递增然后返回对象本身,而后缀则必须创建一个临时对象来存储原来的值,然后将原始对象的值递增,再返回临时对象。
10.12.重载双目数学运算符:双目数学运算符可通过重载是类的双目运算更为自然。即:returnType operator op()。
程序清单 10.6 operator+
#include
using namespace std;
class Counter
{
public:
Counter();
Counter(int initialValue);
~Counter() {}
int GetItsVal() const { return itsVal; }
void SetItsVal(int x) { itsVal = x; }
Counter operator+ (const Counter &);
private:
int itsVal;
};
Counter::Counter(int initiaValue):itsVal (initiaValue)
{
}
Counter::Counter() : itsVal(0)
{
}
Counter Counter::operator+ (const Counter & rhs)
{
return Counter(itsVal + rhs.GetItsVal());
}
int main()
{
Counter varOne(2), varTwo(4), varThree;
varThree = varOne + varTwo;
cout << "varOne: " << varOne.GetItsVal() << endl;
cout << "varTwo: " << varTwo.GetItsVal() << endl;
cout << "varThree: " << varThree.GetItsVal() << endl;
return 0;
}
10.13.对运算符重载的限制:用于内置类型的运算符不能重载(如 int),运算符的优先级和目数(单目还是双目)不能改变。不能创建新的运算符。
10.14.重载运算符准则:运算符重载后应该使程序更加清晰,且务必从重载的运算符返回一个其所属的类的对象。不要创建不直观的运算符行为。
10.15.赋值运算符:如果没有创建,编译器会提供的第四个(最后一个)函数是赋值运算符(operator=())。
程序清单 10.7 赋值运算符
#include
using namespace std;
class Cat
{
public:
Cat();
int GetAge() const { return *itsAge; }
int GetWeight() const { return *itsWeight; }
void SetAge(int age) { *itsAge = age; }
Cat & operator= (const Cat &);
private:
int *itsAge;
int *itsWeight;
};
Cat::Cat()
{
itsAge = new int;
itsWeight = new int;
*itsAge = 5;
*itsWeight = 9;
}
Cat & Cat::operator= (const Cat & rhs)
{
if (this == &rhs)
return *this;
*itsAge = rhs.GetAge();
*itsWeight = rhs.GetWeight();
return *this;
}
int main()
{
Cat Frisky;
cout << "Frisky's age: " << Frisky.GetAge() << endl;
cout << "Setting Frisky to 6...\n";
Frisky.SetAge(6);
Cat Whiskers;
cout << "Whiskers's age: " << Whiskers.GetAge() << endl;
cout << "copying Frisky to Whiskers...\n";
Whiskers = Frisky;
cout << "Whiskers's age: " << Whiskers.GetAge() << endl;
return 0;
}
10.16.数据类型转换:当定义了一个数据类型后,希望把内置类型的变量的值赋值给对象。则可以用一个参数的构造函数来实现。而将类对象的值赋值给内置变量,则需要转换运算符,声明时候没有返回值,且以operator开头。例如 operator unsigned int();
程序清单 10.8 类型转换
#include
class Counter
{
public:
Counter();
Counter(int val);
~Counter() {}
int GetItsVal() const { return itsVal; }
void SetItsVal(int x) { itsVal = x; }
operator unsigned int();
private:
int itsVal;
};
Counter::Counter():itsVal(0) {}
Counter::Counter(int val):itsVal(val) {}
Counter::operator unsigned int()
{
return (int(itsVal));
}
int main()
{
Counter ctr(5);
int theInt = ctr;
std::cout << "theInt: " << theInt << " ctr: " << ctr.GetItsVal() << std::endl;
theInt = 7;
ctr = theInt;
std::cout << "theInt: " << theInt << " ctr: " << ctr.GetItsVal() << std::endl;
return 0;
}
第十一部分 面向对象分析及设计
11.1.建立模型:要控制复杂程度,必须创建域模型。模型旨在创建有意义的现实世界抽象。这种抽象比现实世界简单但又精确地反映现实世界,以便可以使用这个模型来预测现实世界中事物的行为。它主要由两个重要部分组成:建模语言和过程。
11.2.软件设计-建模语言:建模语言是一种有关如何在媒介上,使用某种形式表示模型的约定。软件分析和设计的世界语是UML:统一建模语言。UML规范的任务是回答诸如“应该如何表示继承”等问题。在UML中,类用矩形表示,继承用带箭头线段表示。
11.3.软件设计-过程:面向对象分析和设计的过程比建模语言重要和复杂的多。方法是一种建模语言和一个过程。方法常常被误解为方法学,方法学是研究方法的。
11.4.方法学:方法学家开发和发表的方法中,有三位著名的方法学家及其方法是:Grady Booch及其开发的Booch方法、Ivar Jacobson及其开发的面向对象软件工程和James Rumbaugh及其开发的对象建模技术(OMT)。这三位在一起创建了Rational Unified Process(以前叫Objectory)。(本章大概地遵循他们的方法)。
11.5.软件设计:软件设计是有迭代性的。开发软件时,需要反复地完成整个过程以努力加深对需求的认识。设计指导着实现,但在现实中发现的细节将反馈到设计中,采用这种方法时,不要试图以有序、线性的方式一次性开发出项目,而是迭代性地完成项目的各个部分,不断地该进设计和优化实现。
11.6.迭代式开发和瀑布式开发:不同于迭代式开发,瀑布式开发中,一个阶段的输出将成为下一阶段的输入,采用这种开发方法时,不存在回到前一个阶段的问题。在瀑布式开发过程中,对需求进行细化,并由客户同意,然后将确定的需求提供给设计人员。设计人员制定设计方案,并将其提供程序员,由他们来实现设计方案,程序员再将代码提交给质量保证(QA)人员,后者对代码进行测试后,将其交付给客户。(虽然这个模式看起来很不错,但是实际使用时将是个灾难)
11.7.迭代式开发过程:在迭代式开发中,从概念开始,随着对细节进行分析,想法将成熟和演变。对需求有大致了解后,开始设计;对设计阶段出现的问题有深入认识后,可能回过头来修改需求,在设计阶段,也可能开始建立原型并实现产品。开发阶段出现的问题将反馈到设计中,甚至影响你对需求的认识,最重要的是,只参与设计的一部分,并不断地在设计和实现阶段之间反复。
11.8.迭代开发的过程步骤:1、概念化(期望和目标描述);2、分析(认识需求的过程);3、设计(创建类模型的过程,将根据设计来编写代码);4、实现(完成设计的完整实现);5、测试(测试以保证代码正确);6、交付(rollout 交付是将程序交付给客户)。以上的步骤buy同于Rational Unified Process的过程:开始(inception);精制(elaboration);构建(construction);迁移(Transition)也不同于Rational Unified Process中的如下工作流程中的如下流程:业务建模;需求;分析和设计;实现;测试;部署;配置和变量管理;项目管理;环境。注意:命名不重要,几乎所有面向对象过程的基本步骤都相同:确定要创建什么样的程序,设计解决方案并实现该设计方案。
11.9.第一步:概念化开始-从愿景开始:很所软件一开始都是愿景(vision)。面向对象分析和设计的第一个阶段就是用一句话或一段话来陈述这个愿景。
11.10.第二步:分析阶段-收集需求:为进入分析阶段,必须知道产品将如何使用以及它必须如何完成任务。分析阶段旨在阐明和收集需求。分析阶段的最终结果是制作一份需求文档。需求文档的第一节是用例分析。
11.10.1.用例:分析、设计和实现的驱动力是用例。用例是对产品被如何使用的高级描述。用例不关心用户界面的细节,也不关心要构建的系统的内部细节,而将重点放在需求发生的交互以及协同工作以得到所需结果的人和系统(被称为参与则,actor)上。创建一组健壮、全面的用例可能是分析阶段最重要的任务。需要依赖域专家。
11.10.1.1.1.用例定义:描述软件如何被使用。
11.10.1.1.2.域专家:在要开发的产品针对的业务领域具备专业知识的人员。
11.10.1.1.3.参与者:与要开发的系统进行交互的人和系统。
注:要收集所有的需求,仅仅关注用例还不够,其他部分包括业务规则、数据原素、性能方面的技术需求、安全性等。
11.10.1.2.确定参与者:并非所有的参与者都是人:与要创建的系统交互的系统也是参与者。参与者的基本特征为:1、位于系统外面;2、与系统交互。
11.10.1.3.确定第一批用例:找到合适的参与者切入点。选择的合适的操作。
11.10.1.4.创建域模型:确定初略的用例后,则使用细化的域模型来编制需求文档。域模型是一份文档,记录了相关域的全部信息。作为域模型的一部分,创建域对象,对用例中提到的所有对象进行描述。注:描述时使用的类不是设计时的类,而是需求域中对象的类。可以使用UML通过图示来描述域中对象之间的关系。UML在分析阶段,收集的主要关系是:泛化;包含;关联。
11.10.1.4.1.泛化:常常相当于继承,但二者存区别:泛化描述的是关系,而继承是泛化的编程实现。泛化的对立面是具体化。
11.10.1.4.2.包含:对象通常由多个子对象组成。包含模拟了has a(有一个)关系。UML通过绘制一条从包含对象到被包含对象且带菱形的线段来表示包含。
11.10.1.4.3.关联:关联意味着两个对象以某种方式交互,但具体的交互方式不清楚,在设计阶段更准确的阐述这种交互,但就分析而言,建议指出对象之间存在交互。至于包含的是泛化关系还是包含关系不用说明。在UML中,用一条直线表示两个对象之间关联。
11.10.1.5.建立场景:有了初步的用例和图示域之间关系的工具后,可以将这些用例规范化,每个用例都可以被划分成一系列的场景(scenario)。场景是对一组具体情形的描述,将用例的各元素区分开。每个场景都是原始用例的变体,这些变体都是考虑特殊情景。并找出那些可以确定系统需求或与参与者交互细节的场景。
11.10.1.6.指定指导原则:指导原则收集在需求文档中,通常,需要确保每个场景都包括如下内容:1、前置条件(场景开始前,哪些条件必须满足);2、触发器(什么导致场景开始);3、参与者执行什么操作;4、系统导致了什么样的结果和变化;5、参与者接受了什么样的反馈;6、是否有重复性活动的发生,什么导致它们结束;7、场景的逻辑流程描述;8、什么导致场景结束;9、后置条件:场景结束时,哪些条件必须满足。另外,需求为每个用例和场景命名。
11.10.1.6.1.交互图:虽然用例本身的价值有限,但是可以结合图和用例,加深对交互的理解和记录。在次过程中,可以利用交互图(协同图)来说明这种交互。
11.10.1.6.2.创建包:对任何非常复杂的问题,需要编写很多用例,UML允许将用例创建组合成包。包是一组建模对象(类、参与者等)。可以按任何对问题来说有意义的特征将用例组合成包。并且同一个用例可以出现在不同的包中。
11.10.2.应用分析:除创建用例外,需求文档还必须包含客户的假设、约束(有关硬件和操作系统的需求)、安全性和性能等方面的信息。应用需求(技术性需求)通常是由要与现有系统交互决定的。在这种情况下,理解现有系统的功能和工作原理是分析中不可或缺的一部分。
11.10.3.系统分析:系统分析是收集要与之交互的系统的所有细节的过程。还需要尽量收集与其他系统交互隐含的约束和限制。
11.10.4.规划文档:在以上所有步骤都完成了后,就该着手创建有关时间和预算的文档了。通常时间表是由客户规定的。理想情况下,根据需求设计和实现解决方案所需的时间。现实情况下,大多数系统都有时间和成本方面的限制。
11.10.4.1.确定项目预算和时间的指导原则:1、在知道区间的情况下,上限可能是乐观的;2、Liberty定律指出:完成任何工作的时间都比预期要长,即使你已经考虑的该定律。
11.10.4.2.规划文档:通常它们都是错误的,在过程的早起,几乎不可能给出对项目周期的可靠估计,有了需求后,可以准确的确定设计需要多长时间,准确地估计实现需要多长时间,合理猜测需要多长时间,然后必须加上20%~50%的“活动空间”。
11.10.5.可视化:需求文档的最后一部分是可视化,即一个虚构的名称、指的是图表、图片、屏幕截图、原型及其他任何可视化表示。用于帮助思考和设计产品的图形用户界面。
11.10.6.可交付品:在分析和设计阶段的结尾,创建一系列文档(deliverable,可交付品)。
在项目开发的分析阶段创建的可交付产品 | ||||||||
可交付品 | 描述 | |||||||
用例报告 | 详细说明用例、场景、构造型、前置条件、后置条件和可视化文档 | |||||||
域分析 | 描述域对象间关系的文档 | |||||||
分析协调图 | 描述问题域中对象间交互的协同图 | |||||||
分析活动图 | 描述问题域中对象间交互的活动图 | |||||||
系统分析 | 描述将在其上创建项目的低级和硬件系统的报告和图 | |||||||
应用分析和文档 | 描述客户对该项目的特定需求的报告和图 | |||||||
运行型约束报告 | 描述性能特征和客户加入的约束的报告 | |||||||
成本和规划文档 | 包含指示项目进度、里程碑和成本的图表的报告 |
11.11.1.设计文档:设计文档分两部分:类设计和体系结构机制(Architectural Mechanism)。类设计部分又称为静态设计(详细描述个各种类以及它们的关系和特征)和动态设计(详细描述类之间是如何交互的)。设计文档的体系结构机制部分提供有关如何实现对象持久性、并行性、分布式对象系统等的细节。
11.11.2.什么是类:正规的设计方法要求将概念C++类和设计类分开。使用代码编写的C++类是设计的类的实现。且存在一一对应的关系:设计方案中的每个类对应于代码中的一个类。大多数时候,这两种类不刻意区分,因为差别是高度抽象的。找出一组初始类以及理解什么是设计良好的类的简单技巧是:写出所有的用例,然后为每个名词创建一个类。然后合并同义词得到新名词,然后为这些名次创建类。然后使用图来描述某些类之间的一些显而易见的关系。
11.11.3.转换:从所有场景中提取名词,即将域分析中对象转化为设计中的对象,转换域对象后,可以开始确定其有用的设计阶段对象。通常,每个参与者都是一个类,合适的切入点是新系统和现有系统之间的接口。这种接口被封装到接口类中,通常最好让每个类负责管理自己的永久性问题,这些接口类让你能够将你的系统与其他系统的交互封装起来,从而不会因为系统的变化而影响代码。
11.11.3.1.数据操纵:如果必须将数据从一种格式转化到另一种格式,或想将这些转换封装到一个特殊类中,或需要将数据转换为其他系统要求的格式或通过Internet进行传输所要求的格式时,可以使用这种技术。总之,当需要将数据转换为指定的格式时,都可以将转换协议封装到数据操纵类中。
11.11.3.2.视图和报告:系统生成一个“试图”和“报表”都是一个候选类。报表采用的规则可封装在视图类中。
11.11.3.3.设备:如果系统与设备交互或操纵设备,应将设备协议的细节封装到一个类中,同样,通过为设备接口创建类,可以添加使用新协议的新设备,而不会影响其他代码,即只需创建一个支持相同接口的新类接口即可。
11.11.4.建立静态模型:确定初步类集后,需要建立它们的关系和交互模型。为了更加直观,首先解释静态模型,然后解释动态模型。在实际设计中,在动态和静态之间切换,加入两种模型的细节,实际上是随着认识的深入,添加新类并进行补充。静态模型主要关注三个方面:职责、属性和关系。其中最重要且首先需要关注的是每个类的职责,最终的指导原则是,每个类应负责一件事。为控制类的职责,首先使用CRC卡来进行设计工作。
11.11.4.1.CRC卡:CRC表示类(Class)、职责(Responsibility)和协同(Collaboration)。CRC卡是一张4*6的索引卡。这种技术含量低的工具,可以帮助理解初始类集。可以拿4*6的空索引卡,召开 CRC卡会议。
11.11.4.1.1.如何召开CRC会议:对于大型项目,参加每次CRC会议的理想人数为3~6人。应有一名协调员,其职责是确保会议顺利进行。至少有一名高级软件设计,至少有一两位域专家。最重要的是不可以有管理人员参与,因为这是具有创造性的会议,在会议召开后,每张CRC卡的开头写下类名,在卡片中间画一条竖线,左边写职责,右边写协同。在每张已确定的重要卡的背面写一两句定义,还可以写是哪个类的具体化。或者写出超类,在类名下面填写派生出当前类的类名。
11.11.4.1.2.将重点放在职责上:CRC会议的重点是确定每个类的职责,只确定最基本和明显的属性,在完成职责时,类必须将工作指定给另一类,在“协同”下方记录这些信息。同时,应该注意如果4*6的空间不够,则需要考虑这个类所做的工作是不是太多?此时,不需要考虑类的接口,也不要关注关系,重点关注类是做什么的。
11.11.4.1.3.拟人化和用例驱动:CRC卡的重要功能是将类拟人化,即赋予每个类以人一样的品性。其工作原理为:有了初步的类集成后,回到CRCR场景,将CRC随意分散在桌面上,将其与场景一起分析。
11.11.4.1.4.CRC卡的局限性:虽然CRC卡是一个启动设计的强有力工具,但它们有固有的局限性:1、扩展性不强;2、无法记录类之间的关系;3、CRC卡没有记录属性;4、CRC卡是静态的。注:当进入到一定阶段时,可将CRC卡过渡到UML图,但是需要注意,一旦过渡到UML图之后,就无法退回到CRC卡,因为二者同步很困难。
11.11.4.1.5.将CRC卡转换为UML图:每张CRC卡都可直接转换为用UML模拟的类。职业转换为类方法,同时将已确定的所有属性加入到类中。卡片的背面的类定义转换为类文档。
11.11.4.2.类之间的关系:用UML表示类后,可以将注意力转向各个类之间的关系。需要对其建模的主要关系如下:泛化、关联、聚集、组合。在C++中,泛化关系是通过公有继承来实现的。在这一阶段的任务是将相关类共有的功能提取出来,放到能够封装共有职责的基类中。C++允许的多重继承可以允许类继承多个基类,从而获得多个类的成员和方法。
11.11.4.2.1.多重继承和包含:公有继承总是模拟泛化关系,即:继承应模拟is-a关系。要模拟has-a关系,应使用聚集。简单来说,当各个部件相互之间的生命周期不彼此约束的时候,则称为聚集。当各个部件彼此依存,无法单独存在时,则称为组合
11.11.4.2.2.鉴别器和Powertype(强类型):在UML中,可以使用鉴别器构造型来区分和组合不同的构型。当性能特征相当复杂时,简单的值不足以模拟某些鉴别器,可以使用一个类来模拟鉴别器,鉴别信息可封装到这个类的实例中。powertype能够在不用继承的情况下创建各种逻辑类型,从而管理大量的复杂类型而不会遇到使用继承时可能出现的组合激增。通常在C++中,使用指针来实现powertype。
11.11.5.动态模型:在模拟类之间的关系外,还需要模拟它们之间如何交互。常用的是交互图。交互图有两种:1、时序图;2、协同图。时序图强调事件的发生顺序;协同图强调类之间的交互,不涉及先后顺序。可以根据时序图直接生成协同图。
11.11.5.1.状态切换图:要了解对象间的交互,必须知道每个对象的各种可能状态。可以在状态图(状态切换图)中模拟各种状态之间的切换。每个状态图都是以一个“开始”状态开始,从零或更多“结束”状态结束。每个状态都有名称,切换可能被标记。“保护措施”(guard)表示对象在状态间切换时必须满足的条件。
11.11.5.2.超状态:当使用者改变主意时,都会产生中断,导致状态切换图混乱。因此可以使用一个“超级状态”来简化这个图。即把所有可能的情况考虑进去,然后集合成一个状态。
11.12.第4~6步:实现、测试和交付:虽然这三个步骤很重要,但是这本书没有详细介绍的意思。但在后续章节还是会针对C++的实现介绍其中的细节。
11.13.迭代:以上所有步骤都被称为工作流程,将开始、精制、构建和迁移阶段以不同的程度完成。每个阶段都可能发生多次迭代。直至系统完善。
第十二部分 实现继承
12.1.继承和派生:这种泛化/具体化层次结构建立了一种is-a关系。C++允许定义从一个类派生出另一个类来表示这种关系。派生是一种表示is-a关系的方式。在已有类的基础上添加了新功能的类被称为从原来的类派生而来,原来的类被称为是新类的基类。派生类是基类的超集。通常基类有多个派生类。
12.2.派生的语法:声明类时,可在类名后加上冒号(:)、派生类型(公有或其他)和基类名来指出它们是从哪个类派生而来的。格式如下:class derivedclass:accessType baseClass。例如:class Dog:public Mammal。其中,派生类型的英文为accessType。并且从中派生的类必须已经声明,否则将出现编译错误。
程序清单 12.1 简单继承
#include
using namespace std;
enum BREED//品种
{
GOLDEN,CAIRN,DANDIE,SHETLAND,DOBEMAN,LAB
};
class Mammal
{
public:
Mammal();
~Mammal();
int GetAge() const;
void SetAge(int);
int GetWeight() const;
void SetWeight();
void Speak() const;
void Sleep() const;
protected:
int itsAge;
int itsWeight;
};
class Dog :public Mammal
{
public:
Dog();
~Dog();
BREED GetBreed() const;
void SetBreed(BREED);
void WagTail();//摇尾巴
void BegForFood();//乞讨食物
};
12.3.私有和保护:新的关键字protected是将数据设置为受保护的。C++创始人认为应将所有数据声明为私有的,而不是受保护的。但是当想要派生类型时,是这些数据对当前及其派生类来说是可见的,这种派生类型为保护(protected)。保护型数据成员和函数对派生类来说是完全可见的。但对其他类来说是私有的。总共有三种限定访问符:公有(public);保护(protected)和私有(private)。如果在函数中声明一个对象,则它可以访问所有的公有成员数据和函数,而成员函数可以访问其所属类的所有私有数据成员和函数,也可以访问任何从其所属类派生而来的类的保护数据和函数。
程序清单 12.2 使用派生对象
#include
using std::cout;
using std::endl;
enum BREED
{
GOLDEN,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB
};
class Mammal
{
public:
Mammal():itsAge(2),itsWeight(5) { }
~Mammal() { }
int GetAge() const { return itsAge; }
void SetAge(int age) { itsAge = age; }
int GetWeight() const { return itsWeight; }
void SetWeight(int weight) { itsWeight = weight; }
void Speak() const { cout << "Mammal sound!" << endl; }
void Sleep() const { cout << "shhh.I'm sleeping." << endl; }
protected:
int itsAge;
int itsWeight;
};
class Dog : public Mammal
{
public:
Dog() :itsBreed(GOLDEN){ }
~Dog() { }
BREED GetBreed() const { return itsBreed; }
void SetBreed(BREED breed) { itsBreed = breed; }
void WagTail() const { cout << "Tail wagging..." << endl; }
void BegForFood() const { cout << "Begging for food..." << endl; }
private:
BREED itsBreed;
};
int main()
{
Dog Fido;
Fido.Speak();
Fido.WagTail();
cout << "Fido is " << Fido.GetAge() << " years old" << endl;
return 0;
}
12.4.构造函数和析构函数的继承性:派生类和基类是is-a关系。创建派生类对象时,首先调用基类构造函数,然后调用派生类构造函数,当派生类对象被销毁时,首先调用派生类的析构函数,然后调用基类的析构函数。每个析构函数都提供了在其对应部分应被销毁后执行清理工作的机会。
12.5.向基类的构造函数传递参数:当要在基类构造函数中初始化某些值时,可能需要重载基类的构造函数,那么需要在派生类的构造函数名后加上冒号、基类名和基类构造函数需要的参数,可以在初始化期间对基类进行初始化。
程序清单 12.3 重载派生类的构造函数
#include
using namespace std;
enum BREED
{
GOLDEN,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB
};
class Mammal
{
public:
Mammal();
Mammal(int age);
~Mammal();
int GetAge() const { return itsAge; }
void SetAge(int age) { itsAge = age; }
int GetWeight() const { return itsWeight; }
void SteWeight(int weight) { itsWeight = weight; }
void Speak() const { cout << "Mammal sound!" << endl; }
void Sleep() const { cout << "shhh.I'm sleeping." << endl; }
protected:
int itsAge;
int itsWeight;
};
class Dog : public Mammal
{
public:
Dog();
Dog(int age);
Dog(int age, int weight);
Dog(int age, BREED breed);
Dog(int age, int weight, BREED breed);
~Dog();
BREED GetBreed() const { return itsBreed; }
void SteBreed(BREED breed) { itsBreed = breed; }
void WagTail() const { cout << "Tail wagging..." << endl; }
void BegForFood() const{ cout << "Begging for food..." << endl; }
private:
BREED itsBreed;
};
Mammal::Mammal():itsAge(1),itsWeight(5)
{
cout << "Mammal constructor..." << endl;
}
Mammal::Mammal(int age) : itsAge(age), itsWeight(5)
{
cout << "Mammal(int) constructor..." << endl;
}
Mammal::~Mammal()
{
cout << "Mammal destructor..." << endl;
}
Dog::Dog() :Mammal(), itsBreed(GOLDEN)
{
cout << "Dog constructor..." << endl;
}
Dog::Dog(int age) : Mammal(age), itsBreed(GOLDEN)
{
cout << "Dog(int) constructor..." << endl;
}
Dog::Dog(int age, int weight) : Mammal(age), itsBreed(GOLDEN)
{
itsWeight = weight;
cout << "Dog(int,int) constructor..." << endl;
}
Dog::Dog(int age, int weight, BREED breed) :Mammal(age), itsBreed(breed)
{
itsWeight = weight;
cout << "Dog(int int BREED) constructor..." << endl;
}
Dog::Dog(int age,BREED breed):Mammal(age),itsBreed(breed)
{
cout << "Dog(int,BREED) constructor..." << endl;
}
Dog::~Dog()
{
cout << "Dog destructor..." << endl;
}
int main()
{
Dog Fido;
Dog rover(5);
Dog buster(6, 8);
Dog yorkie(3, GOLDEN);
Dog dobbie(4, 20, DOBERMAN);
Fido.Speak();
rover.WagTail();
cout << "Yorkie is " << yorkie.GetAge() << " years old" << endl;
cout << "Dobbie weighs " << dobbie.GetWeight() << " pounds" << endl;
return 0;
}
12.6.覆盖基类函数:派生类可以访问基类的公有和受保护成员,派生类也可以覆盖基类函数,覆盖基类函数意味着在派生类中修改其实现。在派生类中创建一个返回值和特征标与基类成员函数相同但实现不同的函数时,被称为覆盖该函数。在这种情况下,创建一个派生类对象后,便可以调用正确的函数。覆盖函数时、特征标必须与基类中的被覆盖的函数相同。特征标指的是函数原型中除返回值类型外的内容。即函数名、参数列表和可能用到的关键字const。
程序清单 12.4 在派生类中覆盖基类方法
#include
using std::cout;
using std::endl;
enum BREED
{
GOLDEN,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB
};
class Mammal
{
public:
Mammal() { cout << "Mammal constructor..." << endl; }
~Mammal() { cout << "Mammal destructor..." << endl; }
void Speak() const { cout << "Mammal sound!" << endl; }
void Sleep() const { cout << "shhh.I'm sleeping." << endl; }
protected:
int itsAge;
int itsWeight;
};
class Dog : public Mammal
{
public:
Dog() { cout << "Dog constructor..." << endl; }
~Dog() { cout << "Dog destructor..." << endl; }
void WagTail() const { cout << "Tail wagging..." << endl; }
void BegForFood() const { cout << "Begging for food..." << endl; }
void Speak() const { cout << "Woof!" << endl; }
private:
BREED itsBreed;
};
int main()
{
Mammal bigAnimal;
Dog Fido;
bigAnimal.Speak();
Fido.Speak();
return 0;
}
注:重载和覆盖两个术语类似,功能也相似,重载方法时,创建多个名称相同但特征标不同的多个方法;覆盖方法时,在派生类中创建一个名称和特征标与基类方法相同的方法。
12.7.隐藏基类方法:覆盖会隐藏基类中的方法,但是覆盖一个方法则会把基类中所有同名的方法全部隐藏。所以当派生类想要使用基类的重载方法时,需要把基类的重载方法全部覆盖。注:一种常见的错误是,本想覆盖一种基类方法时,由于忘记包含关键字const,而将其隐藏了。const是特征标的一部分,省略它将改变特征标,导致方法被隐藏而不是覆盖。
12.8.调用基方法:覆盖基方法后,仍然可以通过限定方法名来调用它——在方法名前加上基类名和两个冒号:baseClass::Method()。
程序清单 12.5 调用被覆盖的基类方法
#include
using namespace std;
class Mammal
{
public:
void Move() const { cout << "Mammal move one step" << endl; }
void Move(int distance) const
{
cout << "Mammal move " << distance << " steps." << endl;
}
protected:
int itsAge;
int itsWeight;
};
class Dog : public Mammal
{
public:
void Move() const;
};
void Dog::Move() const
{
cout << "In dog move..." << endl;
Mammal::Move(3);
}
int main()
{
Mammal bigAnimal;
Dog Fido;
bigAnimal.Move(2);
Fido.Mammal::Move(6);
return 0;
}
注:通过使用::来调用祖先的类方法,如果在继承层次结构中,祖先和后代之间插入了新的类,后代将跳过这些中间类,从而遗漏对中间类实现的重要功能的调用。应该通过派生类来扩展经过测试的类的功能,通过覆盖基类方法来改变派生类中某些函数的行为。别忘了返回类型不是特征标的组成部分。
12.9.虚方法:C++扩展了多态性,允许将派生类对象赋给指向基类的指针。例如:Mammal * pMammal = new Dog;即创建了一个新的Dog对象,并返回一个指向该对象的指针,然后将该指针赋给一个Mammal指针。注:这是多态的本质,然后可以使用该指针调用Mammal类的任何方法。虚函数可以做到调用正确的函数,要创建虚函数,前面需要加关键字virtual。
程序清单 12.6 使用虚方法
#include
using namespace std;
class Mammal
{
public:
Mammal() :itsAge(1) { cout << "Mammal constructor..." << endl; }
virtual ~Mammal() { cout << "Mammal destructor..." << endl; }
void Move() const { cout << "Mammal move one step" << endl; }
virtual void Speak() const { cout << "Mammal speak!" << endl; }
protected:
int itsAge;
};
class Dog : public Mammal
{
public:
Dog() { cout << "Dog Constructor..." << endl; }
virtual ~Dog() { cout << "Dog destructor..." << endl; }
void WagTail() { cout << "Wagging Tail..." << endl; }
void Speak() const { cout << "Woof!" << endl; }
void Move() const { cout << "Dog moves 5 steps..." << endl; }
};
int main()
{
Mammal * pDog = new Dog;
pDog->Move();
pDog->Speak();
return 0;
}
注:事实上,如果有一个Mammal指针数组,其中每个指针都指向不同的Mammal子类对象,则可以依次通过这些指针来调用虚函数。
程序清单 12.7. 依次调用多个虚函数
#include
using namespace std;
class Mammal
{
public:
Mammal():itsAge(1) { }
virtual ~Mammal() { }
virtual void Speak() const { cout << "Mammal speak!" << endl; }
protected:
int itsAge;
};
class Dog : public Mammal
{
public:
void Speak() const { cout << "Woof!" << endl; }
};
class Cat : public Mammal
{
public:
void Speak() const { cout << "Meow!" << endl; }
};
class Horse : public Mammal
{
public:
void Speak() const { cout << "Winnie!" << endl; }
};
class Pig : public Mammal
{
public:
void Speak() const { cout << "Qink!" << endl; }
};
int main()
{
Mammal * theArray[5];
Mammal * ptr;
int choice, i;
for (i = 0; i < 5; i++)
{
cout << "(1)dog (2)cat (3)horse (4)pig:";
cin >> choice;
switch (choice)
{
case 1:
ptr = new Dog;
break;
case 2:
ptr = new Cat;
break;
case 3:
ptr = new Horse;
break;
case 4:
ptr = new Pig;
break;
default:
ptr = new Mammal;
break;
}
theArray[i] = ptr;
}
for (i = 0; i < 5; i++)
theArray[i]->Speak();
return 0;
}
注:在该程序编译过程时,无法知道将创建什么类型的对象,因此也无法知道将调用哪个speak()方法,ptr指向的对象时在运行阶段确定的,这被称为动态联编或运行阶段联编,与此相对的是静态联编或编译阶段联编。
12.10.虚函数的工作原理:创建派生对象时,首先调用基类的构造函数,然后调用派生类的构造函数。在类中创建虚函数后,这个类对象必须跟踪虚函数,很多编译器创建了虚函数表(v-table)。每个类都有一个虚函数表,每个类对象都有一个指向虚函数表的指针(vptr或v-pointer),虽然实现方式不同,但是编译器都需要完成这项工作,
12.11.通过基类指针访问派生类的方法:通过基类指针可以访问派生类的虚方法,如果派生类有一个基类没有的方法,不可以像访问虚方法那样通过基类指针来访问。虽然可以通过强制类型转换将基类指针转换为派生类指针,但C++不赞成这样做,因为这样做不安全。
12.12.切除:仅当通过指针和引用进行调用时,才能展现虚方法的强大。按值传递无法展示。当按值传递时,编译器会将派生类的部分切除至和基类相同。导致调用基类对应虚函数。
程序清单 12.8. 安置传递时数据切除(slicing)
#include
using namespace std;
class Mammal
{
public:
Mammal():itsAge(1) { }
virtual ~Mammal() { }
virtual void Speak() const { cout << "Mammal speak!" << endl; }
protected:
int itsAge;
};
class Dog : public Mammal
{
public:
void Speak() const { cout << "Woof!" << endl; }
};
class Cat : public Mammal
{
void Speak() const { cout << "Meow!" << endl; }
};
void ValueFunction(Mammal);
void PtrFunction(Mammal *);
void RefFunction(Mammal &);
int main()
{
Mammal * ptr = 0;
int choice;
while (1)
{
bool fQuit = false;
cout << "(1)dog (2)Cat (0)Quit:";
cin >> choice;
switch (choice)
{
case 0:
fQuit = true;
break;
case 1:
ptr = new Dog;
break;
case 2:
ptr = new Cat;
break;
}
if (fQuit == true)
break;
PtrFunction(ptr);
RefFunction(*ptr);
ValueFunction(*ptr);
}
return 0;
}
void ValueFunction(Mammal MammalValue)
{
MammalValue.Speak();
}
void PtrFunction(Mammal * pMammal)
{
pMammal->Speak();
}
void RefFunction(Mammal & rMammal)
{
rMammal.Speak();
}
12.13.创建虚析构函数:在需要的地方使用指向派生类对象的指针时一种合法和常见的做法。如果析构函数时虚函数,当指向派生类对象的指针被删除时,将调用派生类的析构函数,由于派生类的析构函数会自动调用基类的析构函数,因此整个对象将被正确的销毁。经验规则是:如果类中任何一个函数时虚函数,析构函数也应该时虚函数。
12.14.虚复制构造函数:构造函数不能是虚函数,因此不存在虚复制构造函数。但是当程序需要通过传递一个指向基类对象的指针,创建一个派生类对象的拷贝,对于这种问题,常见的解决方法时:在基类中创建一个Clone()方法,并将其设置为虚方法。Clone()方法创建当前对象的一个拷贝,并返回拷贝。由于每个派生类都覆盖了Clone()方法,因此它将创建派生类对象的一个拷贝。
程序清单 12.12 虚复制构造函数
#include
using namespace std;
class Mammal
{
public:
Mammal() : itsAge(1) { cout << "Mammal constructor..." << endl; }
virtual ~Mammal() { cout << "Mammal destructor..." << endl; }
Mammal(const Mammal & rhs);
virtual void Speak() const { cout << "Mammal speak!" << endl; }
virtual Mammal * Clone() { return new Mammal(*this); }
int GetAge() const { return itsAge; }
protected:
int itsAge;
};
Mammal::Mammal(const Mammal & rhs) : itsAge(rhs.GetAge())
{
cout << "Mammal Copy Constructor..." << endl;
}
class Dog : public Mammal
{
public:
Dog() { cout << "Dog constructor..." << endl; }
virtual ~Dog() { cout << "Dog destuctor..." << endl; }
Dog(const Dog & rhs);
void Speak() const { cout << "Woof!" << endl; }
virtual Mammal * Clone() { return new Dog(*this); }
};
Dog::Dog(const Dog & rhs) : Mammal(rhs)
{
cout << "Dog copy constructor..." << endl;
}
class Cat : public Mammal
{
public:
Cat() { cout << "Cat constructor..." << endl; }
~Cat() { cout << "Cat destructor..." << endl; }
Cat(const Cat &);
void Speak() const { cout << "Meow!" << endl; }
virtual Mammal * Clone() { return new Cat(*this); }
};
Cat::Cat(const Cat & rhs) :Mammal(rhs)
{
cout << "Cat copy constructor..." << endl;
}
enum AINMALS
{
MAMMAL,DOG,CAT
};
const int NumAnimalTypes = 3;
int main()
{
Mammal * theArray[NumAnimalTypes];
Mammal * ptr;
int choice, i;
for (i = 0; i < NumAnimalTypes; i++)
{
cout << "(1)dog (2)cat (3)Mammal:";
cin >> choice;
switch (choice)
{
case DOG:
ptr = new Dog;
break;
case CAT:
ptr = new Cat;
break;
default:
ptr = new Mammal;
break;
}
theArray[i] = ptr;
}
Mammal * OtherArray[NumAnimalTypes];
for (i = 0; i < NumAnimalTypes; i++)
{
theArray[i]->Speak();
OtherArray[i] = theArray[i]->Clone();
}
for (i = 0; i < NumAnimalTypes; i++)
OtherArray[i]->Speak();
return 0;
}
12.15.使用虚方法的代价:由于包含虚方法的类必须维护一个v-table,因此使用虚方法会带来一些开销。当类很小时,且不打算从它派生出其他类时,则不需要使用虚方法。将任何方法声明为虚方法后,便付出了创建v-table的大部分代价(每增加一个表项都会增加一些内存开销)。在这种情况下,应将析构函数声明为虚函数,并假设其他所有方法也可能是虚方法,因理解他们那些不是虚函数的原因。
第十三部分 管理数组和字符串
13.1.数组:数组是一个顺序数据存储单元集合,其中每个存储单元存储相同类型的数据,存储单元被称为数组元素。要声明数组,可以指定类型数组名和下标,下标用方括号括起,指定了数组包含多少个元素。例:int num[5];编译器看到该声明后,将会分配足够的内存来存储这些数据。
13.2.访问数组元素:通过使用离数组开头的偏移量,可以访问数组元素。数组偏移量时从0开始的。因此第一个元素是num[0].即,SomeArray[n]有n个元素,分别是SomeArray[0]到SomeArray[n-1]。这是因为索引是偏移量。
程序清单 13.1 使用int数组
#include
int main()
{
int myArray[5];
int i;
for (i = 0; i < 5; i++)
{
std::cout << "Value for myArray[" << i << "]";
std::cin >> myArray[i];
}
for (i = 0; i < 5; i++)
std::cout << i << ":" << myArray[i] << std::endl;
return 0;
}
13.3.在数组末尾后写入数据:在超出数组的地方写入数据,将会导致未知错误。即:在myArray[50] = 6;这超出而来数组长度,但编译器仍会在距数组头偏移量为50的地方写入数据6.从而导致未知错误。
13.4.护栏柱错误:由于越过数组末尾进行写操作的错误很常见,因此它被称为护栏柱错误(fence post error)。
13.5.初始化数组:可以在声明内置类型的简单数组时对其进行初始化。为此,在数组名后加上等号(=)以及一组大括号括起,用逗号分隔的值。例:int IntegerArray[5] = {10,20,30,40,50};如果省略数组大小,将会创建一个刚好能够存储初始值的数组。即:int a[ ] = {1,2,3};并且初始化的元素数不能超过数组声明的元素数。但是当初始化数组时,仅仅初始化一部分是可以的。
13.5.声明数组:数组可以使用任意合法的变量名,但不能与其他作用域中的其他变量或数组同名。另外,声明数组大小时,除使用字母量外,还可以使用常量或枚举量。注:要声明数组,可指定其存储的对象的类型以及数组名和决定数组能存储多少个对象的下标。
13.6.使用对象数组:任何对象(内置或用户定义的)都可以存储在数组中,声明对象数组时,需要告诉编译器要存储的对象的类以及为多少个对象分配内存。根据类声明,编译器知道每个对象需要多大内存,类必须有一个不接受任何参数的默认构造函数,以便在定义数组时创建对象。
程序清单 13.2. 创建对象数组
#include
using namespace std;
class Cat
{
public:
Cat() { itsAge = 1, itsWeight = 5; }
~Cat() { }
int GetAge() const { return itsAge; }
int GetWeight() const { return itsWeight; }
void SetAge(int age) { itsAge = age; }
private:
int itsAge;
int itsWeight;
};
int main()
{
Cat Litter[5];
int i;
for (i = 0; i < 5; i++)
Litter[i].SetAge(2 * i + 1);
for (i = 0; i < 5; i++)
{
cout << "Cat # " << i + 1 << ":";
cout << Litter[i].GetAge() << endl;
}
return 0;
}
13.7.声明多维数组:数组可以有多维,每维用一个下标表示,因为二维数组有两个下标,三维数组有三个下标,依次类推。但是更多是创建二维数组。例:int a[3][3];其中第一个下标为行,第二个下标为列。
13.8.初始化多维数组:可以初始化多维数组,指定的初始值按如下顺序赋给数组元素。最后(最右边)的数组下标从0递增,其他下标保持不变。因此,如果有如下的数组:int b[5][3];则前3个初始值将存储在b[0]中,接下来3个值存储在b[1]中,并依次类推。
程序清单 13.3. 创建多维数组
#include
using namespace std;
int main()
{
int SomeArray[2][5] = { {0,1,2,3,4},{0,2,4,6,8} };
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 5; j++)
{
cout << "SomeArray[" << i << "]" << "[" << j << "]:";
cout << SomeArray[i][j] << endl;
}
}
return 0;
}
注:当不知道需要多少个对象时,需要使用更高级的数据结构
13.9.指针数组:通常因为堆栈有限,而自由存储区大得多,可以在自由存储区声明每个对象,然后仅将指向对象的指针存储到数组中。这可极大地减少占用的堆栈内存量。
程序清单 13.4. 在自由存储区存储数组#include
using namespace std;
class Cat
{
public:
Cat() { itsAge = 1, itsWeight = 5; }
~Cat() { }
int GetAge() const { return itsAge; }
int GetWeight() const { return itsWeight; }
void SetAge(int age) { itsAge = age; }
private:
int itsAge;
int itsWeight;
};
int main()
{
Cat * Family[500];
int i;
Cat * pCat;
for (i = 0; i < 500; i++)
{
pCat = new Cat;
pCat->SetAge(2 * i + 1);
Family[i] = pCat;
}
for (i = 0; i < 500; i++)
{
cout << "Cat #" << i + 1 << ":";
cout << Family[i]->GetAge() << endl;
}
return 0;
}
13.10.指针算术:可以对指针做一些数学运算,可以将两个指针相减。即两个指针指向同一个数组中不同元素,然后将它们相减来确定它们之间相隔几个元素。这在分析字符数组方面很有用。
程序清单 13.5 提取字符串中的单词
#include
#include
#include
bool GetWord(char * theString, char * word, int & wordOffset);
int main()
{
const int bufferSize = 255;
char buffer[bufferSize + 1];
char word[bufferSize + 1];
int wordOffset = 0;
std::cout << "Enter a string: ";
std::cin.getline(buffer, bufferSize);
while (GetWord(buffer, word, wordOffset))
{
std:: cout << "Got this word: "<
注:改程序让用户输入一个句子,然后提取该句子中的每个单词。最好的理解方法是用步进的方法调试。在VS上运行时,会提示将strncpy替换为strcpy_s才会安全。
13.11.在自由存储区声明数组:可以将整个数组放入堆中。因此可以创建一个数组指针,方法是使用new 和下标运算符。这样将得到一个指向自由存储区中存储了数组的区域的指针。
13.12.指针数组和数组指针:数组指针(也称为行指针)的定义 int (*p)[n];其中()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。指针数组:定义 int *p[n];其中[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。
13.13.指针和数组名:数组名是一个常量指针,指向数组的第一个元素。因此int a[30];中,a是一个指向数组a的第一个元素a[0]的指针。将数组名用作常量指针是合法的。反之亦然。
程序清单 13.6. 使用new创建数组
#include
class Cat
{
public:
Cat() { itsAge = 1, itsWeight = 5; }
~Cat();
int GetAge() const { return itsAge; }
int GetWeight() const { return itsWeight; }
void SetAge(int age) { itsAge = age; }
private:
int itsAge;
int itsWeight;
};
Cat::~Cat()
{
//std::cout<<"Destructor called!"<
13.14.删除自由存储区中的数组:当需要删除数组时,使用delete[ ]来删除数组。使用方括号时,编译器会识别并且将数组的每个内存空间都归还给系统。为弄清这点,可以将上述程序中数组的大小改为20,并将析构函数的注释符去掉。再次运行即可。如果遗忘了[ ],将会只删除第一个元素而造成内存泄漏。
13.15.在运行阶段调整数组大小:在堆上创建数组的最大优点是,可以在运行阶段确定数组的大小,然后为其分配内存。其优点是在运行阶段当数组空间不足时,可使用类似的技巧来调整数组的大小。
程序清单 13.7. 在运行阶段给数组重新分配内存
#include
using namespace std;
int main()
{
int AllocationSize = 5;
int *pArrayofNumbers = new int[AllocationSize];
int ElementsUsedSoFar = 0;
int MaximumElementsAllowed = AllocationSize;
int InputNumber = -1;
cout << endl << "Next number = ";
cin >> InputNumber;
while(InputNumber > 0)
{
pArrayofNumbers[ElementsUsedSoFar++] = InputNumber;
if (ElementsUsedSoFar == MaximumElementsAllowed)
{
int * pLargerArray = new int[MaximumElementsAllowed + AllocationSize];
for (int CopyIndex = 0; CopyIndex < MaximumElementsAllowed; CopyIndex++)
{
pLargerArray[CopyIndex] = pArrayofNumbers[CopyIndex];
}
delete[] pArrayofNumbers;
pArrayofNumbers = pLargerArray;
MaximumElementsAllowed += AllocationSize;
}
cout << endl << "Next number =";
cin >> InputNumber;
}
for (int Index = 0; Index < ElementsUsedSoFar; Index++)
{
cout << pArrayofNumbers[Index] << endl;
}
return 0;
}
13.16. 字符数组和字符串:以空字符结尾的字符数组。被称为“C-风格字符串”。即“\0“是空字符,很多C++函数都将其视为C-风格字符串的结束标记。为了简洁,C++提供了简洁方法来穿件字符串数组:char a [ ] = "Hello world ";关于这种语法需要注意:使用双引号将C-风格字符串括起,没有逗号和大括号;也不是由逗号和单引号分隔并用大括号括起来。并且不需要添加空字符。编译器会动添加。
程序清单 13.8. 填充数组
#include
int main()
{
char buffer[20];
std::cout << "Enter the string: ";
std::cin >> buffer;
std::cout << "Here's the buffer: " << buffer << std::endl;
return 0;
}
注:改程序存在两个问题。1、如果输入超过20个,cin将在超出缓冲区末尾的地方写入;2、如果用户输入了空格,cin将认为这是字符串的结尾,从而停止向缓冲区写入。所以为了改善可用cin.get( )。cin.get( )接受三个参数:待填充的缓冲区;要读取的最大字符数;终止输入的限定符。
13.16.使用方法strcpy( )和strncpy( ):C++库中有很多用于处理C-风格字符串的函数,其中的很多是从C语言那里继承来的。在提供的很函数中,有两个用于将一个字符串复制到另一个字符串中:strcpy( )和strncpy( )。其中strcpy( )将整个字符串复制到指定的缓冲区中;strncpy( )将指定数目的字符从一个字符串复制到另一个字符串中。
程序清单 13.9. 使用strcpy()
#include
#include
using namespace std;
int main()
{
char String1[] = "No man is an island";
char String2[80];
const int MaxLength = 80;
char String3[] = "No man is an island";
char String4[MaxLength+1];
strcpy(String2, String1);
strncpy(String4, String3,MaxLength);
cout << "String1: " << String1 << endl;
cout << "String2: " << String2 << endl;
cout << "String3: " << String3 << endl;
cout << "String4: " << String4 << endl;
return 0;
}
13.17.string类:C++继承了C语言中以空字符结尾的C-风格字符串以及包括strcpy( )函数的函数库。但这些函数并没有集成到面向对象的框架中。标准库中包括一个string类。它提供了一套封装好的数据以及处理这些数据的函数,还提供了存取器函数以便对string类的客户隐藏数据本身。
程序清单 13.10 自定义的string类(模仿库内的string类,以助理解)
#include
#include
using namespace std;
class String
{
public:
//constructors
String();
String(const char * const);
String(const String &);
~String();
//overloaded operators
char & operator[] (unsigned short offset);
char operator[] (unsigned short offset) const;
String operator+ (const String &);
void operator+= (const String&);
String & operator= (const String &);
//General accessors
unsigned short GetLen() const { return itsLen; }
const char * GetString() const { return itsString; }
private:
String(unsigned short);
char * itsString;
unsigned short itsLen;
};
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen = 0;
}
//private (helper) constructor,use only by
//class methods for creating a new string of
//required size,Null filled.
String::String(unsigned short len)
{
itsString = new char[len + 1];
for (unsigned short i = 0; i <= len; i++)
itsString[i] = '\0';
itsLen = len;
}
//Converts a character array to a String
String::String(const char * const cString)
{
itsLen = strlen(cString);
itsString = new char[itsLen + 1];
for (unsigned short i = 0; i < itsLen; i++)
itsString[i] = cString[i];
itsString[itsLen] = '\0';
}
//copy constructor
String::String(const String & rhs)
{
itsLen = rhs.GetLen();
itsString = new char[itsLen + 1];
for (unsigned short i = 0; i < itsLen; i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
}
//destructor,frees allocated memory
String::~String()
{
delete[] itsString;
itsLen = 0;
}
//operator equals,frees existing memory
//then copies string and size
String & String::operator=(const String & rhs)
{
if (this == &rhs)
return *this;
delete[] itsString;
itsLen = rhs.GetLen();
itsString = new char[itsLen + 1];
for (unsigned short i = 0; i < itsLen; i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
return *this;
}
//nonconstant offset operator,returns
//referece to character so it can be
//changed!
char & String::operator[] (unsigned short offset)
{
if (offset > itsLen)
return itsString[itsLen - 1];
else
return itsString[offset];
}
//constant offset operator for use
//on const objects (see copy constructor!)
char String::operator[](unsigned short offset) const
{
if (offset > itsLen)
return itsString[itsLen - 1];
else
return itsString[offset];
}
//creates a new string by adding current
//string to rhs
String String::operator+(const String & rhs)
{
unsigned short totalLen = itsLen + rhs.GetLen();
String temp(totalLen);
unsigned short i;
for (i = 0; i < itsLen; i++)
temp[i] = itsString[i];
for (unsigned short j = 0; j < rhs.GetLen(); j++)
temp[i] = rhs[j];
temp[totalLen] = '\0';
return temp;
}
//changes current string,return noting
void String::operator+=(const String & rhs)
{
unsigned short rhsLen = rhs.GetLen();
unsigned short totalLen = itsLen + rhsLen;
String temp(totalLen);
unsigned short i;
for (i = 0; i < itsLen; i++)
temp[i] = itsString[i];
for (unsigned short j = 0; j < rhs.GetLen(); j++, i++)
temp[i] = rhs[i - itsLen];
temp[totalLen] = '\0';
*this = temp;
}
int main()
{
String s1("initial test");
cout << "S1:\t" << s1.GetString() << endl;
char * temp = "Hello World";
s1 = temp;
cout << "S1:\t" << s1.GetString() << endl;
char tempTwo[20];
strcpy(tempTwo, "; nice to be here!");
s1 += tempTwo;
cout << "tempTwo:\t" << tempTwo << endl;
cout << "S1:\t" << s1.GetString() << endl;
cout << "S1[4]:\t" << s1[4] << endl;
s1[4] = 'x';
cout << "S1:\t" << s1.GetString() << endl;
cout << "S1[999];\t" << s1[999] << endl;
String s2("Another string");
String s3;
s3 = s1 + s2;
cout << "S3:\t" << s3.GetString() << endl;
String s4;
s4 = "Why does this work?";
cout << "S4:\t" << s4.GetString() << endl;
return 0;
}
13.18 链表和其他结构:选择大型数组或需要移动、删除或插入数组元素时,分配和释放内存的开销非常高。解决这一问题的方法是:链表。链表是一种数据结构。由小型容器组成,这些容器被设计成必要时能够链接起来。
13.19.创建数组类:相对于内置数组,可以编写自己的数组类。在编写时,可能用到这些功能强大的数组变体:有序集合(所有成员按顺序排列);集(set,每个成员最多出现一次);词典(使用成对值,其中一个值充当关键字,用于检索另一个值);稀疏数组(下标的取值范围很大,但只有被添加到数组中的值才占用内存;布袋(无序集合,按随机顺序添加和检索)。其中,通过重载下边运算符([ ]),可将链表变成有序集合。通过排除重复内容,可将集合变成一个集(set)。如果链表中的每个对象都有一对匹配的值,可使用链表来构建词典或稀疏数组。
第十四部分 多态
14.1.单继承存在的问题:使用单继承时,只能从一个类派生出来,当所需要的类的属性比较复杂时,就无法满足需求,从而给代码的维护和使用带来很大的麻烦。
14.2.提升:将所需的函数放到类层次结构中较高的位置,是一种常见解决这种问题的方法。其结果是很多函数被提升(percolating up)到基类中。这样基类会变成派生类可能使用的所有函数的全局名称空间。这将严重破坏C++类的类型化(class typing),创建出庞杂的基类。需要避免:仅仅为了能够在某些派生类中调用它,而将函数提升到不属于它的地方。
14.3.向下转移:采用单继承时,另一种方法是将方法留在派生类内,且仅当指针指向派生类对象时才调用它。为此,需要知道指针实际指向的类型。这被称为运行阶段识别(Run Time Type Identification,RTTI)。RTTI是C++规范中的一项新功能,并非所有的编译器都支持。如果编译器不支持RTTI,可在每个类中放置一个返回枚举类型的方法来模拟RTTI。然后可以在运行阶段检查类型。如果返回的是派生类型,则调用派生类型的专有方法。注:在程序中应慎用RTTI,需要使用RTTI可能意味着继承层次结构设计的很糟糕。应考虑使用虚函数、模板或多重继承来代替它。
14.4.向下转移的工作原理:如果有一个指向基类的指针,并将指向派生类的指针赋给它,便可以以多态方式使用基类指针。如果需要访问派生类对象,可创建派生类指针并使用dynamic_cast运算符来进行转换。在运行阶段,将检查基类指针,如果转化正确,新的派生类指针也是正确的,如果转换不正确或根本没有派生类对象,新指针将为空。
程序清单 14.1 向下转移
#include
using namespace std;
enum TYPE
{
HORSE,PEGASUS
};
class Horse
{
public:
virtual void Gallop() { cout << "Galloping..." << endl; }
private:
int itsAge;
};
class Pegasus : public Horse
{
public:
virtual void Fly()
{
cout << "I can Fly! I can Fly!" << endl;
}
};
const int NumberHorses = 5;
int main()
{
Horse * Ranch[NumberHorses];
Horse * pHorse;
int choice, i;
for (i = 0; i < NumberHorses; i++)
{
cout << "(1)Horse (2)Pegasus:";
cin >> choice;
if (choice == 2)
pHorse = new Pegasus;
else
pHorse = new Horse;
Ranch[i] = pHorse;
}
cout << endl;
for (i = 0; i < NumberHorses; i++)
{
Pegasus * pPeg = dynamic_cast(Ranch[i]);
if (pPeg != NULL)
pPeg->Fly();
else
cout << "Just a horse" << endl;
delete Ranch[i];
}
return 0;
}
14.5.将对象添加到链表中:最后一种单继承解决方案是:将派生类、基类的方法都提升到新基类中,统一使用新基类的链表。这种解决方案可行,但导致将派生类的所有特征都放在基类中。不应该:不要将为支持多态而本应在派生类中添加的功能放在基类中。不要将基类指针向下转换为派生类指针。
14.6.多重继承:可以从多个基类派生出新类,这被称为多重继承。要从多个基类派生,在声明中将每个基类用逗号分开。例:class DerivedClass : public BaseClass1,public BaseClass2 { };
程序清单 14.2 多重继承
#include
using std::cout;
using std::cin;
using std::endl;
class Horse
{
public:
Horse() { cout << "Horse constructor..."; }
virtual ~Horse() { cout << "Horse destructor..."; }
virtual void Whinny() const { cout << "Whinny!..."; }
private:
int itsAge;
};
class Bird
{
public:
Bird() { cout << "Bird constructor..."; }
virtual ~Bird() { cout << "Brid destructor..."; }
virtual void Chirp() const { cout << "Chirp..."; }
virtual void Fly() const
{
cout << "I can fly! I can fly! I can fly!";
}
private:
int itsWeight;
};
class Pegasus : public Horse, public Bird
{
public:
void Chirp() const { Whinny(); }
Pegasus() { cout << "Pegasus constructor..."; }
~Pegasus() { cout << "Pegasus destructor..."; }
};
const int MagicNumber = 2;
int main()
{
Horse * Ranch[MagicNumber];
Bird * Aviary[MagicNumber];
Horse * pHorse;
Bird * pBird;
int choice, i;
for (i = 0; i < MagicNumber; i++)
{
cout << endl << "(1)Horse (2)Pegasus:";
cin >> choice;
if (choice == 2)
pHorse = new Pegasus;
else
pHorse = new Horse;
Ranch[i] = pHorse;
}
for (i = 0; i < MagicNumber; i++)
{
cout << endl << "(1)Bird (2)Pegasus:";
cin >> choice;
if (choice == 2)
pBird = new Pegasus;
else
pBird = new Bird;
Aviary[i] = pBird;
}
cout << endl;
for (i = 0; i < MagicNumber; i++)
{
cout << endl << "Ranch[" << i << "]:";
Ranch[i]->Whinny();
delete Ranch[i];
}
cout << endl;
for (i = 0; i < MagicNumber; i++)
{
cout << endl << "Aviary[" << i << "]:";
Aviary[i]->Chirp();
Aviary[i]->Fly();
delete Aviary[i];
}
return 0;
}
14.7.多重继承对象的组成部分:多重继承后,在内存中创建派生类对象时,多个基类都将成都将成为派生类的组成部分。
14.8.多重继承对象中的构造函数:多重继承后,如果派生类从各个基类派生而来且每个基类都有接受参数的构造函数。派生类将依次调用这些构造函数。
程序清单 14.3 调用多个构造函数
#include
using namespace std;
typedef int HANDS;
enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown };
class Horse
{
public:
Horse(COLOR color, HANDS height);
virtual ~Horse() { cout << "Horse destructor..." << endl; }
virtual void Whinny() const { cout << "Whinny!..."; }
virtual HANDS GetHeight() const { return itsHeight; }
virtual COLOR GetColor() const { return itsColor; }
private:
HANDS itsHeight;
COLOR itsColor;
};
Horse::Horse(COLOR color, HANDS height) : itsColor(color), itsHeight(height)
{
cout << "Horse constructor..." << endl;
}
class Bird
{
public:
Bird(COLOR color, bool migrates);
virtual ~Bird() { cout << "Bird destructor..." << endl; }
virtual void Chirp() const { cout << "Chirp..."; }
virtual void Fly() const
{
cout << "I can fly! I can fly! I can fly!";
}
virtual COLOR GetColor() const { return itsColor; }
virtual bool GetMigration() const { return itsMigration; }
private:
COLOR itsColor;
bool itsMigration;
};
Bird::Bird(COLOR color, bool migrates) : itsColor(color),itsMigration(migrates)
{
cout << "Brid constructor..." << endl;
}
class Pegasus : public Horse, public Bird
{
public:
void Chirp() const { Whinny(); }
Pegasus(COLOR, HANDS, bool, long);
~Pegasus() { cout << "Pegasus destructor..." << endl; }
virtual long GetNumberBelievers() const
{
return itsNumberBelievers;
}
private:
long itsNumberBelievers;
};
Pegasus::Pegasus(COLOR aColor, HANDS height, bool migrates, long NumBelieve) :
Horse(aColor, height), Bird(aColor, migrates), itsNumberBelievers(NumBelieve)
{
cout << "Pegasus constructor..." << endl;
}
int main()
{
Pegasus * pPeg = new Pegasus(Red, 5, true, 10);
pPeg->Fly();
pPeg->Whinny();
cout << endl << "Your Pegasus is " << pPeg->GetHeight();
cout << " hands tall and ";
if (pPeg->GetMigration())
cout << "it does migrate.";
else
cout << "it does not migrate.";
cout << endl << "A total of " << pPeg->GetNumberBelievers();
cout << " people believe it exists." << endl;
delete pPeg;
return 0;
}
14.9.歧义解析:在多重继承后,基类都有同名且同操作的方法时,派生类调用这些方法将出现错误。因此如果需要调用时候,必须显示地调用要调用的函数。即:COLOR currentColor = pPeg->Horse::GetColor( );要指出从哪个类继承而来的成员函数或成员数据时,都可以通过在数据或函数前加上基类名来全限定调用。
14.10.从共同基类继承:当两个派生类从一个基类继承而来,有两个基类对象,调用共同基类的函数或数据成员时,将出现歧义。
程序清单 14.4 共同基类
#include
using namespace std;
typedef int HANDS;
enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown };
class Animal
{
public:
Animal(int);
virtual ~Animal() { cout << "Animal destructor..." << endl; }
virtual HANDS GetAge() const { return itsAge; }
virtual void SetAge(int age) { itsAge = age; }
private:
HANDS itsAge;
};
Animal::Animal(int age) : itsAge(age)
{
cout << "Animal constructor..." << endl;
}
class Horse : public Animal
{
public:
Horse(COLOR color, HANDS height,int age);
virtual ~Horse() { cout << "Horse destructor..." << endl; }
virtual void Whinny() const { cout << "Whinny!..."; }
virtual HANDS GetHeight() const { return itsHeight; }
virtual COLOR GetColor() const { return itsColor; }
protected:
HANDS itsHeight;
COLOR itsColor;
};
Horse::Horse(COLOR color, HANDS height,int age) : Animal(age),itsColor(color), itsHeight(height)
{
cout << "Horse constructor..." << endl;
}
class Bird : public Animal
{
public:
Bird(COLOR color, bool migrates,int age);
virtual ~Bird() { cout << "Bird destructor..." << endl; }
virtual void Chirp() const { cout << "Chirp..."; }
virtual void Fly() const
{
cout << "I can fly! I can fly! I can fly!";
}
virtual COLOR GetColor() const { return itsColor; }
virtual bool GetMigration() const { return itsMigration; }
protected:
COLOR itsColor;
bool itsMigration;
};
Bird::Bird(COLOR color, bool migrates,int age) : Animal(age),itsColor(color),itsMigration(migrates)
{
cout << "Brid constructor..." << endl;
}
class Pegasus : public Horse, public Bird
{
public:
void Chirp() const { Whinny(); }
Pegasus(COLOR, HANDS, bool, long,int);
virtual ~Pegasus() { cout << "Pegasus destructor..." << endl; }
virtual long GetNumberBelievers() const
{return itsNumberBelievers; }
virtual COLOR GetColor() const { return Horse::itsColor; }
virtual HANDS GetAge() const { return Horse::GetAge(); }
private:
long itsNumberBelievers;
};
Pegasus::Pegasus(COLOR aColor, HANDS height, bool migrates, long NumBelieve,int age) :
Horse(aColor, height,age), Bird(aColor, migrates,age), itsNumberBelievers(NumBelieve)
{
cout << "Pegasus constructor..." << endl;
}
int main()
{
Pegasus * pPeg = new Pegasus(Red, 5, true, 10,2);
int age = pPeg->GetAge();
cout << "This pegasus is " << age << " years old." << endl;
delete pPeg;
return 0;
}
注:每当显示地指定祖先类时,都将带来风险:如果以后再在当前类及其祖先类之间插入一个新类,将导致调用新的祖先类时,直接进入原来的祖先类。这可能导致意外的后果。
14.11.虚继承:当多个派生类继承一个共同的基类时,且有新的派生类从这些派生类继承而来,很难明确新派生类调用原始派生类中的共有方法的来源。为解决这一问题,可使用虚继承的方法。即在继承时使用关键字virtual。但是需要注意的是,虚继承的基类由最后的派生类进行初始化。
程序清单 14.5 使用虚继承
#include
using namespace std;
typedef int HANDS;
enum COLOR
{
Red,Green,Blue,Yellow,White,Black,Brown
};
class Animal
{
public:
Animal(int);
virtual ~Animal() { cout << "Animal destructor...." << endl; }
virtual int GetAge() const { return itsAge; }
virtual void SetAge(int age) { itsAge = age; }
private:
int itsAge;
};
Animal::Animal(int age) : itsAge(age)
{
cout << "Animal constructor..." << endl;
}
class Horse : virtual public Animal
{
public:
Horse(COLOR color, HANDS herght, int age);
virtual ~Horse() { cout << "Horse destructor..." << endl; }
virtual void Whinny() const { cout << "Whinny!..."; }
virtual HANDS GetHeight() const { return itsHeight; }
virtual COLOR GetColor() const { return itsColor; }
protected:
HANDS itsHeight;
COLOR itsColor;
};
Horse::Horse(COLOR color, HANDS height, int age) : Animal(age), itsColor(color), itsHeight(height)
{
cout << "Horse constructor..." << endl;
}
class Bird : virtual public Animal
{
public:
Bird(COLOR color, bool migrates, int age);
virtual ~Bird() { cout << "Bird destructor..." << endl; }
virtual void Chirp() const { cout << "Chirp..."; }
virtual void Fly() const { cout << "I can fly!"; }
virtual COLOR GetColor() const { return itsColor; }
virtual bool GetMigation() const { return itsMigration; }
protected:
COLOR itsColor;
bool itsMigration;
};
Bird::Bird(COLOR color,bool migrates,int age) : Animal(age),itsColor(color),itsMigration(migrates)
{
cout << "Bird constructor..." << endl;
}
class Pegasus : public Horse, public Bird
{
public:
void Chirp() const { Whinny(); }
Pegasus(COLOR, HANDS, bool, long, int);
virtual ~Pegasus() { cout << "Pegasus destructor..." << endl; }
virtual long GetNumberBelievers() const { return itsNumberBelievers; }
virtual COLOR GetColor() const { return Horse::itsColor; }
private:
long itsNumberBelievers;
};
Pegasus::Pegasus(COLOR aColor, HANDS height, bool migrates, long NumBelieve, int age) :
Horse(aColor, height, age), Bird(aColor, height, age), Animal(age * 2), itsNumberBelievers(NumBelieve)
{
cout << "Pegasus constructor..." << endl;
}
int main()
{
Pegasus *pPeg = new Pegasus(Red, 5, true, 10, 2);
int age = pPeg->GetAge();
cout << "This pegasus is " << age << " years old." << endl;
delete pPeg;
return 0;
}
注:Horse和Bird必须在其构造函数中初始化Animal。但创建Pegasus对象时,这些初始化将被忽略。为确保派生了只有一个共同基类实例,可将中间类声明为从共同基类虚继承。
14.12.多重继承存在的问题:很多程序员不愿意使用多重继承,原因是多重继承增加调试难度,开发多重继承类层次结构比开发单继承类层次结构更困难且风险更大。几乎所有用多重继承完成的工作,不用多重继承也可以完成。所以应该:1、当新类需要多个基类和特性时使用多重继承;2、当最后的派生类只能有一个共同基类的实例时必须使用虚继承;3、使用虚基类时需要在最后的派生类中初始化共同基类。不应该在单继承可行的情况下不要使用多重继承。
14.13.混合(功能)类:在多重继承和单继承之间的一种折衷方案是使用混合类(mixin)。混合(功能)类增加专用功能而不会增加大量方法或数据的类。将功能类混合到派生类中的方法与其他类相同:将派生类声明为以公有方式继承功能类,功能类和其他类的唯一区别是:功能类没有或只有很少的数据。这样做会在访问其他基类的数据时出现歧义的可能性更低。
14.14.抽象数据类型:只是为派生类提供接口,是一种抽象数据类型(ADT),在抽象类中,接口表示一种概念而不是具体的对象。在C++中,抽象类只能用作其他类的基类,不能创建抽象类的实例。
程序清单 14.6.Shape类
#include
using namespace std;
class Shape
{
public:
Shape() { }
virtual ~Shape() { }
virtual long GetArea() { return -1; }//error
virtual long GetPerim() { return -1; }
virtual void Draw() {}
private:
};
class Circle : public Shape
{
public:
Circle(int radius) : itsRadius(radius) { }
~Circle() { }
long GetArea() { return 3 * itsRadius * itsRadius; }
long GetPerim() { return 6 * itsRadius; }
void Draw();
private:
int itsRadius;
int itsCircumfernce;
};
void Circle::Draw()
{
cout << "Circle drawing routine here!" << endl;
}
class Rectangle : public Shape
{
public:
Rectangle(int len, int width) : itsLength(len),itsWidth(width) {}
virtual ~Rectangle() { }
virtual long GetArea() { return itsLength * itsWidth; }
virtual long GetPerim() { return 2 * itsLength + 2 * itsWidth; }
virtual int GetLength() { return itsLength; }
virtual int GetWidth() { return itsWidth; }
virtual void Draw();
private:
int itsWidth;
int itsLength;
};
void Rectangle::Draw()
{
for (int i = 0; i < itsWidth; i++)
{
for (int j = 0; j < itsWidth; j++)
cout << "x";
cout << endl;
}
}
class Square : public Rectangle
{
public:
Square(int len);
Square(int len, int width);
~Square() { }
long GetPerim() { return 4 * GetLength(); }
};
Square::Square(int len) : Rectangle(len,len) { }
Square::Square(int len, int width) : Rectangle(len, width)
{
if (GetLength() != GetWidth())
cout << "Error, not a squre...a Rectangle??" << endl;
}
int main()
{
int choice;
bool fQuit = false;
Shape * sp = 0;
while (!fQuit)
{
cout << "(1)Circle (2)Rectangle (3)Square (0)Quit:";
cin >> choice;
switch (choice)
{
case 0:
fQuit = true;
break;
case 1:
sp = new Circle(5);
break;
case 2:
sp = new Rectangle(4, 6);
break;
case 3:
sp = new Square(5);
break;
default:
cout << "Please enter a number between 0 and 3" << endl;
continue;
break;
}
if (!fQuit)
sp->Draw();
delete sp;
sp = 0;
cout << endl;
}
return 0;
}
14.15.纯虚函数:C++提供纯虚函数来支持创建抽象类。通过将虚函数初始化为0来将其声明为纯虚的。例:virtual void Draw() = 0;任何包含一个或多个纯虚函数的类都是抽象类,因此不能对其实例化。事实上,试图对抽象类或从抽象类派生而来但没有实现其所有纯虚函数的类进行实例化都是非法的。将纯虚函数放在类中向其客户指出两点:1、不要创建这个类的对象,应从其派生;2、务必覆盖从这个类继承的纯虚函数。在从抽象类派生而来的类中,继承的纯虚函数仍是纯虚的。要实例化这个类的对象,务必覆盖每个纯虚函数。
程序清单 14.7 抽象类(在程序清单14.6的基础上修改的)
#include
using namespace std;
class Shape
{
public:
Shape() { }
~Shape() { }
virtual long GetArea() = 0;
virtual long GetPerim() = 0;
virtual void Draw() = 0;
private:
};
class Circle : public Shape
{
public:
Circle(int radius) : itsRadius(radius) { }
~Circle() { }
long GetArea() { return 3 * itsRadius * itsRadius; }
long GetPerim() { return 6 * itsRadius; }
void Draw();
private:
int itsRadius;
int itsCircumfernce;
};
void Circle::Draw()
{
cout << "Circle drawing routine here!" << endl;
}
class Rectangle : public Shape
{
public:
Rectangle(int len, int width) : itsLength(len),itsWidth(width) {}
virtual ~Rectangle() { }
virtual long GetArea() { return itsLength * itsWidth; }
virtual long GetPerim() { return 2 * itsLength + 2 * itsWidth; }
virtual int GetLength() { return itsLength; }
virtual int GetWidth() { return itsWidth; }
virtual void Draw();
private:
int itsWidth;
int itsLength;
};
void Rectangle::Draw()
{
for (int i = 0; i < itsWidth; i++)
{
for (int j = 0; j < itsWidth; j++)
cout << "x";
cout << endl;
}
}
class Square : public Rectangle
{
public:
Square(int len);
Square(int len, int width);
~Square() { }
long GetPerim() { return 4 * GetLength(); }
};
Square::Square(int len) : Rectangle(len,len) { }
Square::Square(int len, int width) : Rectangle(len, width)
{
if (GetLength() != GetWidth())
cout << "Error, not a squre...a Rectangle??" << endl;
}
int main()
{
int choice;
bool fQuit = false;
Shape * sp = 0;
while (!fQuit)
{
cout << "(1)Circle (2)Rectangle (3)Square (0)Quit:";
cin >> choice;
switch (choice)
{
case 0:
fQuit = true;
break;
case 1:
sp = new Circle(5);
break;
case 2:
sp = new Rectangle(4, 6);
break;
case 3:
sp = new Square(5);
break;
default:
cout << "Please enter a number between 0 and 3" << endl;
continue;
break;
}
if (!fQuit)
sp->Draw();
delete sp;
sp = 0;
cout << endl;
}
return 0;
}
注:只修改了Shape类的声明,运行结果不变,但是现在不能创建Shape类对象了。
14.16.实现纯虚函数:通常不实现抽象基类中的纯虚函数,由于不能创建抽象类的对象,因此没有理由提供实现。另外,抽象类用作从其派生类而来的类的接口定义。然而,可以给纯虚函数提供实现,这样就可以通过从抽象类派生而来的对象调用该函数,该函数可能旨在给所有覆盖函数提供通用功能。
程序清单 14.8 实现纯虚函数(重写程序清单14.7)
#include
using namespace std;
class Shape
{
public:
Shape() { }
~Shape() { }
virtual long GetArea() = 0;
virtual long GetPerim() = 0;
virtual void Draw() = 0;
private:
};
void Shape::Draw()
{
cout << "Abstract drawing mechanism!" << endl;
}
class Circle : public Shape
{
public:
Circle(int radius) : itsRadius(radius) { }
virtual ~Circle() { }
long GetArea() { return 3.14 * itsRadius * itsRadius; }
long GetPerim() { return 2 * 3.14 * itsRadius; }
void Draw();
private:
int itsRadius;
int itsCircumfernce;
};
void Circle::Draw()
{
cout << "Circle drawing routine here!" << endl;
Shape::Draw();
}
class Rectangle : public Shape
{
public:
Rectangle(int len, int width) : itsLength(len),itsWidth(width) {}
virtual ~Rectangle() { }
long GetArea() { return itsLength * itsWidth; }
long GetPerim() { return 2 * itsLength + 2 * itsWidth; }
virtual int GetLength() { return itsLength; }
virtual int GetWidth() { return itsWidth; }
virtual void Draw();
private:
int itsWidth;
int itsLength;
};
void Rectangle::Draw()
{
for (int i = 0; i < itsWidth; i++)
{
for (int j = 0; j < itsWidth; j++)
cout << "x";
cout << endl;
}
Shape::Draw();
}
class Square : public Rectangle
{
public:
Square(int len);
Square(int len, int width);
virtual ~Square() { }
long GetPerim() { return 4 * GetLength(); }
};
Square::Square(int len) : Rectangle(len,len) { }
Square::Square(int len, int width) : Rectangle(len, width)
{
if (GetLength() != GetWidth())
cout << "Error, not a squre...a Rectangle??" << endl;
}
int main()
{
int choice;
bool fQuit = false;
Shape * sp = 0;
while (!fQuit)
{
cout << "(1)Circle (2)Rectangle (3)Square (0)Quit:";
cin >> choice;
switch (choice)
{
case 1:
sp = new Circle(5);
break;
case 2:
sp = new Rectangle(4, 6);
break;
case 3:
sp = new Square(5);
break;
default:
fQuit = true;
break;
}
if (fQuit == false)
{
sp->Draw();
delete sp;
sp = 0;
cout << endl;
}
}
return 0;
}
注:不必把Shape中的3个存取器函数都是声明为纯虚函数。但这是一种不错的习惯。
14.17.复杂的抽象层次结构:当从抽象类派生出其他抽象类,可将一些继承而来的纯虚函数变成非纯虚函数,而保留其他的不变。
程序清单 14.9 从抽象类派生出其他抽象类
#include
using namespace std;
enum COLOR
{
Red,Green,Blue,Yellow,White,Black,Brown
};
class Animal
{
public:
Animal(int);
virtual ~Animal() { cout << "Animal destructor..." << endl; }
virtual int GetAge() const { return itsAge; }
virtual void SetAge(int age) { itsAge = age; }
virtual void Sleep() const = 0;
virtual void Eat() const = 0;
virtual void Reproduce() const = 0;
virtual void Move() const = 0;
virtual void Speak() const = 0;
private:
int itsAge;
};
Animal::Animal(int age): itsAge(age)
{
cout << "Animal constructor..." << endl;
}
class Mammal : public Animal
{
public:
Mammal(int age) : Animal(age) { cout << "Mammal constructor..." << endl; }
virtual ~Mammal() { cout << "Mammal destructor..." << endl; }
virtual void Reproduce() const { cout << "Mammal reproduction depicted..." << endl; }
};
class Fish : public Animal
{
public:
Fish(int age) : Animal(age) { cout << "Fish constructor..." << endl; }
virtual ~Fish() { cout << "Fish destructor..." << endl; }
virtual void Sleep() const { cout << "fish snoring..." << endl; }
virtual void Eat() const { cout << "fish feeding..." << endl; }
virtual void Reproduce() const { cout << "fish laying eggs..." << endl; }
virtual void Move() const { cout << "fish swimming..." << endl; }
virtual void Speak() const { }
};
class Horse : public Mammal
{
public:
Horse(int age, COLOR color) : Mammal(age), itsColor(color)
{
cout << "Horse constructor..." << endl;
}
virtual ~Horse() { cout << "Horse destructor..." << endl; }
virtual void Speak() const { cout << "Whinny!..." << endl; }
virtual COLOR GetItsColor() const { return itsColor; }
virtual void Sleep() const { cout << "Horse snoring..." << endl; }
virtual void Eat() const { cout << "Horse feeding..." << endl; }
virtual void Move() const { cout << "Horse running..." << endl; }
protected:
COLOR itsColor;
};
class Dog : public Mammal
{
public:
Dog(int age, COLOR color) : Mammal(age), itsColor(color)
{
cout << "Dog constructor..." << endl;
}
virtual ~Dog() { cout << "Dog destructor..." << endl; }
virtual void Speak() const { cout << "Whoof!..." << endl; }
virtual void Sleep() const { cout << "Dog snoring..." << endl; }
virtual void Eat() const { cout << "Dog eating..." << endl; }
virtual void Move() const { cout << "Dog running..." << endl; }
virtual void Reproduce() const { cout << "Dogs reproducing..." << endl; }
protected:
COLOR itsColor;
};
int main()
{
Animal * pAnimal = 0;
int choice;
bool fQuit = false;
while (fQuit == false)
{
cout << "(1)Dog (2)Horse (3)Fish (0)Quit:";
cin >> choice;
switch (choice)
{
case 1:
pAnimal = new Dog(5, Brown);
break;
case 2:
pAnimal = new Horse(4, Black);
break;
case 3:
pAnimal = new Fish(5);
break;
default:
fQuit = true;
break;
}
if (fQuit == false)
{
pAnimal->Speak();
pAnimal->Eat();
pAnimal->Reproduce();
pAnimal->Move();
pAnimal->Sleep();
delete pAnimal;
pAnimal = 0;
cout << endl;
}
}
return 0;
}
14.18.哪些类时抽象的:哪些类要被声明为抽象的不取决现实世界的固有因素,而是有程序如何合理而决定的。应该:1、使用抽象类时给系列相关类通用的功能提供描述;2、将所有必须覆盖的函数声明为纯虚函数。且不要试图实例化抽象类对象。
第二周复习:
程序清单 2.1 第二周课程复习
//*****************************************************************
//
//Title: Week 2 in Review
//
//File: Week2
//
//Description: Provide a linked list demonstration program
//
//Classes: PART - acts as a node in a PartsList
//
// PartsList - provides the mechanisms for a
// linked list of parts
//
//******************************************************************
#include
using namespace std;
//**************************** Part *********************************
//Abstract base class of parts
class Part
{
public:
Part() : itsPartNumber(1) { }
Part(int PartNumber) : itsPartNumber(PartNumber) { }
virtual ~Part() { };
int GetPartNumber() const { return itsPartNumber; }
virtual void Display() const = 0; //must be overridden
private:
int itsPartNumber;
};
//implementation of purs virtual function so that
//derived classes can chain up
void Part::Display() const
{
cout << endl << "Part Number: " << itsPartNumber << endl;
}
//********************** Car Part *********************************
class CarPart : public Part
{
public:
CarPart() : itsModelYear(94) { }
CarPart(int year, int partNumber);
virtual void Display() const
{
Part::Display();
cout << "Mode Year: ";
cout << itsModelYear << endl;
}
private:
int itsModelYear;
};
CarPart::CarPart(int year, int partNumber) : itsModelYear(year), Part(partNumber) { }
//************************ AirPlane Part *********************************
class AirPlanePart : public Part
{
public:
AirPlanePart() : itsEngineNumber(1) { };
AirPlanePart(int EngineNumber, int PartNumber);
virtual void Display() const
{
Part::Display();
cout << "Engine No.: ";
cout << itsEngineNumber << endl;
}
private:
int itsEngineNumber;
};
AirPlanePart::AirPlanePart(int EngineNumber,int PartNumber) : itsEngineNumber(EngineNumber),Part(PartNumber) { }
//**************************** Part Node ****************************
class PartNode
{
public:
PartNode(Part *);
~PartNode();
void SetNext(PartNode * node)
{
itsNext = node;
}
PartNode * GetNext() const;
Part * GetPart() const;
private:
Part * itsPart;
PartNode * itsNext;
};
//PartNode Implementations...
PartNode::PartNode(Part * pPart) : itsPart(pPart), itsNext(0) { }
PartNode::~PartNode()
{
delete itsPart;
itsPart = 0;
delete itsNext;
itsNext = 0;
}
//Returns NULL if no next PartNode
PartNode * PartNode::GetNext() const
{
return itsNext;
}
Part * PartNode::GetPart() const
{
if (itsPart)
return itsPart;
else
return NULL;//error
}
//**************************** Part List ****************************
class PartsList
{
public:
PartsList();
~PartsList();
//needs copy constructor and operator equals~
Part * Find(int & position, int PartNumber) const;
int GetCount() const { return itsCount; }
Part * GetFirst() const;
void Insert(Part *);
void Iterate() const;
Part * operator[](int) const;
private:
PartNode * pHead;
int itsCount;
};
//Implementions for Lists...
PartsList::PartsList() : pHead(0),itsCount(0) { }
PartsList::~PartsList() { delete pHead; }
Part * PartsList::GetFirst() const
{
if (pHead)
return pHead->GetPart();
else
return NULL;//error catch here
}
Part * PartsList::operator[](int offset) const
{
PartNode * pNode = pHead;
if (!pHead)
return NULL;//error catch here
if (offset > itsCount)
return NULL;//error
for (int i = 0; i < offset; i++)
pNode = pNode->GetNext();
return pNode->GetPart();
}
Part * PartsList::Find(int & position, int PartNumber) const
{
PartNode * pNode = 0;
for (pNode = pHead, position = 0; pNode != NULL; pNode = pNode->GetNext(), position++)
{
if (pNode->GetPart()->GetPartNumber() == PartNumber)
break;
}
if (pNode == NULL)
return NULL;
else
return pNode->GetPart();
}
void PartsList::Iterate() const
{
if (!pHead)
return;
PartNode * pNode = pHead;
do
pNode->GetPart()->Display();
while (pNode = pNode->GetNext());
}
void PartsList::Insert(Part * pPart)
{
PartNode * pNode = new PartNode(pPart);
PartNode * pCurrent = pHead;
PartNode * pNext = 0;
int New = pPart->GetPartNumber();
int Next = 0;
itsCount++;
if (!pHead)
{
pHead = pNode;
return;
}
//if this one smaller than head
//this one is new head
if (pHead->GetPart()->GetPartNumber() > New)
{
pNode->SetNext(pHead);
pHead = pNode;
return;
}
for (;;)
{
//if there is no next,append this new one
if (!pCurrent->GetNext())
{
pCurrent->SetNext(pNode);
return;
}
//if this goes after this one and before the next
//then intsert it here,otherwise get the next
pNext = pCurrent->GetNext();
Next = pNext->GetPart()->GetPartNumber();
if (Next > New)
{
pCurrent->SetNext(pNode);
pNode->SetNext(pNext);
return;
}
pCurrent = pNext;
}
}
int main()
{
PartsList p1;
Part * pPart = 0;
int PartNumber;
int value;
int choice = 99;
while (choice != 0)
{
cout << "(0)Quit (1)Car (2)Plane: ";
cin >> choice;
if (choice != 0)
{
cout << "New PartNumebr?:";
cin >> PartNumber;
if (choice == 1)
{
cout << "Model Year?: ";
cin >> value;
pPart = new CarPart(value, PartNumber);
}
else
{
cout << "Engine Number?: ";
cin >> value;
pPart = new AirPlanePart(value, PartNumber);
}
p1.Insert(pPart);
}
}
p1.Iterate();
return 0;
}
注:该程序提供了一个Part对象的链表实现。
第十五部分 特殊类和函数
15.1.静态成员数据:当需要记录用于同一种类型的所有对象的数据时,可使用静态成员变量。其是同一类的所有实例共享的变量,它们是全局数据和成员数据的折衷。可以将静态成员视为属于类的而不是对象。简单来说,在同一类型的对象之间可以共享一个数据。
程序清单 15.1 静态成员数据
#include
using namespace std;
class Cat
{
public:
Cat(int age) : itsAge(age) { HowManyCats++; }
virtual ~Cat() { HowManyCats--; }
virtual int GetAge() { return itsAge; }
virtual void SetAge(int age) { itsAge = age; }
static int HowManyCats;
private:
int itsAge;
};
int Cat::HowManyCats = 0;
int main()
{
const int MaxCats = 5;
int i;
Cat * CatHouse[MaxCats];
for (i = 0; i < MaxCats; i++)
CatHouse[i] = new Cat(i);
for (i = 0; i < MaxCats; i++)
{
cout << "There are: ";
cout << Cat::HowManyCats;
cout << " cats left!" << endl;
cout << "Deleting the one that is ";
cout << CatHouse[i]->GetAge();
cout << " years old" << endl;
delete CatHouse[i];
CatHouse[i] = 0;
}
return 0;
}
注:HowManyCats的声明并未定义一个int变量,没有分配存储空间,不同于非静态成员变量,实例化Cat对象不会为成员变量HowManyCats分配存储空间,因为它不再对象中。因此要在声明之外初始化这个变量。
程序清单 15.2 在不通过对象的情况下访问静态成员
#include
using namespace std;
class Cat
{
public:
Cat(int age) : itsAge(age) { HowManyCats++; }
virtual ~Cat() { HowManyCats--; }
virtual int GetAge() { return itsAge; }
virtual void SetAge(int age) { itsAge = age; }
static int HowManyCats;
private:
int itsAge;
};
int Cat::HowManyCats = 0;
void TelepathicFunction();
int main()
{
const int MaxCats = 5;
int i;
Cat * CatHouse[MaxCats];
for (i = 0; i < MaxCats; i++)
{
CatHouse[i] = new Cat(i);
TelepathicFunction();
}
for (i = 0; i < MaxCats; i++)
{
delete CatHouse[i];
TelepathicFunction();
}
return 0;
}
void TelepathicFunction()
{
cout << "There are ";
cout << Cat::HowManyCats << " Cats alive! " << endl;
}
注:该成员变量不属于任何对象,而属于类。通过类可以访问任何成员函数。如果该变量是公有的,在程序中的任何函数都能够访问它,即使函数没有类的实例。也可以将该类成员变量声明为私有而不是公有的,如果这样做,可通过成员函数来访问它,但这种情况下必须有这个类的对象。
15.2.静态成员函数:静态成员函数类似静态成员变量,它们不属于某个对象而属于整个类。因此,不通过对象也能调用它们。
程序清单 15.3 静态成员函数
#include
class Cat
{
public:
Cat(int age) : itsAge(age) { HowManyCats++; }
virtual ~Cat() { HowManyCats--; }
virtual int GetAge() { return itsAge; }
virtual void SetAge(int age) { itsAge = age; }
static int GetHowMany() { return HowManyCats; }
private:
int itsAge;
static int HowManyCats;
};
int Cat::HowManyCats = 0;
void TelepathicFunction();
int main()
{
const int MaxCats = 5;
Cat * CatHouse[MaxCats];
int i;
for (i = 0; i < MaxCats; i++)
{
CatHouse[i] = new Cat(i);
TelepathicFunction();
}
for (i = 0; i < MaxCats; i++)
{
delete CatHouse[i];
TelepathicFunction();
}
return 0;
}
void TelepathicFunction()
{
std::cout << "There are " << Cat::GetHowMany()
<< " cats alive!" << std::endl;
}
注:静态成员函数没有this指针,因此,不能将它们声明为const。另外,由于在成员函数中是通过this指针来访问成员数据变量,因此静态成员函数不能访问非静态成员变量。
可以像调用其他成员函数一样,通过对象来调用静态成员函数;也可以不通过对象使用类名进行全限定来调用它们。
15.3.函数指针:函数名是指向函数的常量指针。可以声明指向函数的指针,并使用该指针来调用相应的函数。这可以让你能够根据用户输入来决定调用哪个函数的程序。
需要注意的是函数指针必须指向有合适返回类型和特征标的函数。例:long (* funcPtr)(int)。funcPtr被声明为一个指针,它指向接受一个int参数且返回类型为long的函数。*funcPtr两侧的括号必不可少。函数指针的声明总是包括返回类型和指定参数类型的括号。
程序清单 15.4 函数指针
#include
using namespace std;
void Square(int &, int &);
void Cube(int &, int &);
void Swap(int &, int &);
void GetVals(int &, int &);
void PrintVals(int, int);
int main()
{
void(*pFunc) (int &, int &) = 0;
bool fQuit = false;
int valOne = 1,valTwo = 2;
int choice;
while (fQuit == false)
{
cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: ";
cin >> choice;
switch (choice)
{
case 1:
pFunc = GetVals;
break;
case 2:
pFunc = Square;
break;
case 3:
pFunc = Cube;
break;
case 4:
pFunc = Swap;
break;
default:
fQuit = true;
break;
}
if (fQuit == false)
{
PrintVals(valOne, valTwo);
pFunc(valOne, valTwo);
PrintVals(valOne, valTwo);
}
}
return 0;
}
void PrintVals(int x, int y)
{
cout << "x: " << x << "y: " << y << endl;
}
void Square(int & rX, int & rY)
{
rX *= rX;
rY *= rY;
}
void Cube(int & rX, int & rY)
{
int tmp;
tmp = rX;
rX *= rX;
rX = rX * tmp;
tmp = rY;
rY *= rY;
rY = rY * tmp;
}
void Swap(int & rX,int & rY)
{
int temp;
temp = rX;
rX = rY;
rY = temp;
}
void GetVals(int & rValOne, int & rValTwo)
{
cout << "New value for ValOne: ";
cin >> rValOne;
cout << "New value for ValTwo: ";
cin >> rValTwo;
}
注:给函数指针赋值的方法是,将不带括号的函数名的函数名赋给它,函数名是指向函数的常量指针。函数指针的用法与函数名一样。函数指针必须在返回类型和特征标方面与赋给它的函数一样。
15.4.使用函数指针的原因:一般来说,不推荐使用函数指针。函数指针可以追溯到C语言时代。提供它们旨在支持具备面向对象的某些优点的编程风格。当然,如果编写的是高度动态的、需要在运行阶段的决策执行不同操作的程序,函数指针可提供可行的解决方案。
15.5.使用函数指针的优点:函数指针将会使程序代码的清晰度提升,还可以根据运行阶段的情况决定调用哪个函数。而面向对象编程通常是尽量避免创建或传递函数指针,且不需要对函数指针解除引用。pFunc(x);和(*pFunc)(x);是一样的。但前者是简化版。
15.6.函数指针数组:可以声明函数指针数组,其中的指针指向返回特定类型和具有特定特征标的函数。
程序清单 15.5. 使用指针数组
#include
using namespace std;
void Square(int &, int &);
void Cube(int &, int &);
void Swap(int &, int &);
void GetVals(int &, int &);
void PrintVals(int, int);
int main()
{
int valOne = 1, valTwo = 2;
int choice, i;
const int MaxArray = 5;
void (* pFuncArray[MaxArray])(int &, int &);
for (i = 0; i < MaxArray; i++)
{
cout << "(0)Change Values (2)Square (3)Cube (4)Swap: ";
cin >> choice;
switch (choice)
{
case 1:
pFuncArray[i] = GetVals;
break;
case 2:
pFuncArray[i] = Square;
break;
case 3:
pFuncArray[i] = Cube;
break;
case 4:
pFuncArray[i] = Swap;
break;
default:
pFuncArray[i] = 0;
}
}
for (i = 0; i < MaxArray; i++)
{
if (pFuncArray[i] == 0)
continue;
pFuncArray[i](valOne, valTwo);
PrintVals(valOne, valTwo);
}
return 0;
}
void PrintVals(int x, int y)
{
cout << "x: " << x << "y: " << y << endl;
}
void Square(int & rX, int & rY)
{
rX *= rX;
rY *= rY;
}
void Cube(int & rX, int & rY)
{
int tmp;
tmp = rX;
rX *= rX;
rX = rX * tmp;
tmp = rY;
rY *= rY;
rY = rY * tmp;
}
void Swap(int & rX, int & rY)
{
int temp;
temp = rX;
rX = rY;
rY = temp;
}
void GetVals(int & rValOne, int & rValTwo)
{
cout << "New value for ValOne: ";
cin >> rValOne;
cout << "New value for ValTwo: ";
cin >> rValTwo;
}
15.7.将函数指针传递给其他函数:函数指针(和函数指针数组)可被传递给其他函数,后者可执行操作,然后使用传入的指针调用相应的函数。
程序清单 15.6 将函数指针作为参数传递给函数
#include
using namespace std;
void Square(int &, int &);
void Cube(int &, int &);
void Swap(int &, int &);
void GetVals(int &, int &);
void PrintVals(void (*)(int &,int &),int &,int &);
int main()
{
int valOne = 1, valTwo = 2;
int choice;
bool fQuit = false;
void (* pFunc)(int &, int &) = 0;
while (fQuit == false)
{
cout << "(0)Change Values (2)Square (3)Cube (4)Swap: ";
cin >> choice;
switch (choice)
{
case 1:
pFunc = GetVals;
break;
case 2:
pFunc = Square;
break;
case 3:
pFunc = Cube;
break;
case 4:
pFunc = Swap;
break;
default:
fQuit = true;
break;
}
if (fQuit == false)
PrintVals(pFunc, valOne, valTwo);
}
return 0;
}
void PrintVals(void (*pFunc)(int&, int&),int & x,int & y)
{
cout << "x: " << x << "y: " << y << endl;
pFunc(x, y);
cout << "x: " << x << "y: " << y << endl;
}
void Square(int & rX, int & rY)
{
rX *= rX;
rY *= rY;
}
void Cube(int & rX, int & rY)
{
int tmp;
tmp = rX;
rX *= rX;
rX = rX * tmp;
tmp = rY;
rY *= rY;
rY = rY * tmp;
}
void Swap(int & rX, int & rY)
{
int temp;
temp = rX;
rX = rY;
rY = temp;
}
void GetVals(int & rValOne, int & rValTwo)
{
cout << "New value for ValOne: ";
cin >> rValOne;
cout << "New value for ValTwo: ";
cin >> rValTwo;
}
15.8.typedef用于指针:结构void(*)(int &,int &)有点繁琐,可用typedef来简化。
程序清单 15.7 使用typedef提高函数指针的可读性
#include
using namespace std;
void Square(int &, int &);
void Cube(int &, int &);
void Swap(int &, int &);
void GetVals(int &, int &);
typedef void(*VPF)(int &, int &);
void PrintVals(VPF,int &,int &);
int main()
{
int valOne = 1, valTwo = 2;
int choice;
bool fQuit = false;
VPF pFunc = 0;
while (fQuit == false)
{
cout << "(0)Change Values (2)Square (3)Cube (4)Swap: ";
cin >> choice;
switch (choice)
{
case 1:
pFunc = GetVals;
break;
case 2:
pFunc = Square;
break;
case 3:
pFunc = Cube;
break;
case 4:
pFunc = Swap;
break;
default:
fQuit = true;
break;
}
if (fQuit == false)
PrintVals(pFunc, valOne, valTwo);
}
return 0;
}
void PrintVals(VPF pFunc,int & x,int & y)
{
cout << "x: " << x << "y: " << y << endl;
pFunc(x, y);
cout << "x: " << x << "y: " << y << endl;
}
void Square(int & rX, int & rY)
{
rX *= rX;
rY *= rY;
}
void Cube(int & rX, int & rY)
{
int tmp;
tmp = rX;
rX *= rX;
rX = rX * tmp;
tmp = rY;
rY *= rY;
rY = rY * tmp;
}
void Swap(int & rX, int & rY)
{
int temp;
temp = rX;
rX = rY;
rY = temp;
}
void GetVals(int & rValOne, int & rValTwo)
{
cout << "New value for ValOne: ";
cin >> rValOne;
cout << "New value for ValTwo: ";
cin >> rValTwo;
}
注:程序中使用typedef将VPF声明为类型:指向返回类型为void 且接受两个int引用参数的函数指针。
15.9.成员函数指针:可以创建指向类成员函数的指针。这是非常高级且不常用的技巧。要创建成员函数指针,可使用与创建函数指针相同的语法,但需要包含类名和作用域运算符(::)。例:void (Shape :: *pFunc) (int,int);。成员函数指针的用法和函数指针相同,只是需要通过相应类的对象来调用。
程序清单 15.8 成员函数指针
#include
using namespace std;
class Mammal
{
public:
Mammal() : itsAge(1) { }
virtual ~Mammal() { }
virtual void Speak() const = 0;
virtual void Move() const = 0;
protected:
int itsAge;
};
class Dog : public Mammal
{
public:
void Speak() const { cout << "Woof!" << endl; }
void Move() const { cout << "Walking to heel..." << endl; }
};
class Cat : public Mammal
{
public:
void Speak() const { cout << "Meow!" << endl; }
void Move() const { cout << "slinking..." << endl; }
};
class Horse : public Mammal
{
public:
void Speak() const { cout << "Winnie" << endl; }
void Move() const { cout << "Galloping..." << endl; }
};
int main()
{
void(Mammal::*pFunc)() const = 0;
Mammal * ptr = 0;
int Animal;
int Method;
bool fQuit = false;
while (fQuit == false)
{
cout << "(0)Quit (1)dog (2)cat (3)horse: ";
cin >> Animal;
switch (Animal)
{
case 1:
ptr = new Dog;
break;
case 2:
ptr = new Cat;
break;
case 3:
ptr = new Horse;
break;
default:
fQuit = true;
break;
}
if (fQuit == false)
{
cout << "(1)Speak (2)Move: ";
cin >> Method;
switch (Method)
{
case 1:
pFunc = &Mammal::Speak;//有些老的编译器可以通过没有&号的赋值方式,但标准C++强制要求加上&号
break;
default:
pFunc = &Mammal::Move;//同上
break;
}
(ptr->*pFunc)();
delete ptr;
}
}
return 0;
}
注:没有理由对pFunc调用delete,因为它是一个指向代码的指针,而不是指向自由存储区中对象的指针。当试图这样做时,将导致编译错误。
15.10.成员函数指针数组:像函数指针一样,成员函数指针也可以存储在数组中,可以用不同成员函数的地址来初始化数组,且用下标表示法来调用这些函数。
程序清单 15.9 成员函数指针数组
#include
using std::cout;
using std::endl;
using std::cin;
class Dog
{
public:
void Speak() const { cout << "Woof!" << endl; }
void Move() const { cout << "Walking to heel..." << endl; }
void Eat() const { cout << "Gobbling food..." << endl; }
void Growl() const { cout << "Grrrrr" << endl; }
void Whimper() const { cout << "Whining noises..." << endl; }
void RollOver() const { cout << "Rolling over..." << endl; }
void PlayDead() const { cout << "The end of Little Caesar?" << endl; }
};
typedef void (Dog::*PDF)() const;
int main()
{
const int MaxFuncs = 7;
PDF DogFunctions[MaxFuncs] =
{
&Dog::Speak,
&Dog::Move,
&Dog::Eat,
&Dog::Growl,
&Dog::Whimper,
&Dog::RollOver,
&Dog::PlayDead
};
Dog * pDog = 0;
int Method;
bool fQuit = false;
while (!fQuit)
{
cout << "(0)Quit (2)Speak (2)Move (3)Eat (4)Growl ";
cout << "(5)Whimper (6)Roll Over (7)Play Dead:";
cin >> Method;
if (Method <= 0 || Method >= 8)
{
fQuit = true;
}
else
{
pDog = new Dog;
(pDog->*DogFunctions[Method - 1])();
delete pDog;
}
}
return 0;
}
注:一定要理解(pDog->DogFunctions[Method-1])();且不要在简单的解决方案可行的情况下不要使用成员函数指针。声明函数指针时别忘了括号,否则声明的将是返回指针的函数。
第十六部分 高级继承
16.1.聚合(aggregation):类的成员数据可以是其他类的对象,这被称为聚合或has-a(有一个)关系。
程序清单 16.1 String类(这是一个更为复杂的String类,没有任何输出,只提供了后面的程序清单使用的代码)
//String.hpp
#include
#include
using namespace std;
class String
{
public:
//constructors
String();
String(const char * const);
String(const String &);
~String();
//overloaded operators
char & operator[] (int offset);
char operator[] (int offset) const;
String operator+ (const String&);
void operator+=(const String&);
String & operator= (const String &);
//General accessors
int GetLen() const { return itsLen; }
const char * GetString() const { return itsString; }
//static int ConstructorCount;
private:
String(int); //private constructor
char * itsString;
unsigned short itsLen;
};
//default constructor creates string of 0 bytes
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen = 0;
//cout<<"\tDefault string constructor"<#include "String.hpp"
class Employee
{
public:
Employee();
Employee(char *, char *, char * ,long);
~Employee();
Employee(const Employee&);
Employee & operator= (const Employee &);
const String & GetFirstName() const { return itsFirstName; }
const String & GetLastName() const { return itsLastName; }
const String & GetAddress() const { return itsAddress; }
long GetSalary() const { return itsSalary; }
void SetFirstName(const String & fName) { itsFirstName = fName; }
void SetLastName(const String & lName) { itsLastName = lName; }
void SetAddress(const String & address) { itsAddress = address; }
void SetSalary(long salary) { itsSalary = salary; }
private:
String itsFirstName;
String itsLastName;
String itsAddress;
long itsSalary;
};
Employee::Employee() :
itsFirstName(""),itsLastName(""),itsAddress(""),itsSalary(0)
{ }
Employee::Employee(char * firstName, char * lastName, char * address, long salary) :
itsFirstName(firstName), itsLastName(lastName), itsAddress(address), itsSalary(salary)
{ }
Employee::Employee(const Employee & rhs):
itsFirstName(rhs.GetFirstName()),itsLastName(rhs.GetLastName()),itsAddress(rhs.GetAddress()),itsSalary(rhs.GetSalary())
{ }
Employee::~Employee() { }
Employee & Employee::operator= (const Employee & rhs)
{
if (this == &rhs)
return *this;
itsFirstName = rhs.GetFirstName();
itsLastName = rhs.GetLastName();
itsAddress = rhs.GetAddress();
itsSalary = rhs.GetSalary();
return *this;
}
int main()
{
Employee Edie("Jane", "Doe", "1461 Shore Parkway", 2000);
Edie.SetSalary(50000);
String LastName("Levine");
Edie.SetLastName(LastName);
Edie.SetFirstName("Edythe");
cout << "Name: ";
cout << Edie.GetFirstName().GetString();
cout << " " << Edie.GetLastName().GetString();
cout << ".\nAddress: ";
cout << Edie.GetAddress().GetString();
cout << ".\nSalary: ";
cout << Edie.GetSalary();
return 0;
}
16.2.访问被聚合类的成员:聚合了其他对象的类在访问这些对象成员数据和成员函数方面没有特权,而只有与其他常规类一样的访问权限。它们唯一的特备之处是,在创建时或创建后,将传给它们一个指向其所属对象的this指针,除此之外,访问权限与其他函数完全相同。
16.3.聚合的代价:采用聚合时,在性能方面会付出代价。每次创建目标类时,都需要创建其包含的类对象。
程序清单 16.3 被聚合的类的构造函数的调用情况
#include "String.hpp"
class Employee
{
public:
Employee();
Employee(char *, char *, char * ,long);
~Employee();
Employee(const Employee&);
Employee & operator= (const Employee &);
const String & GetFirstName() const { return itsFirstName; }
const String & GetLastName() const { return itsLastName; }
const String & GetAddress() const { return itsAddress; }
long GetSalary() const { return itsSalary; }
void SetFirstName(const String & fName) { itsFirstName = fName; }
void SetLastName(const String & lName) { itsLastName = lName; }
void SetAddress(const String & address) { itsAddress = address; }
void SetSalary(long salary) { itsSalary = salary; }
private:
String itsFirstName;
String itsLastName;
String itsAddress;
long itsSalary;
};
Employee::Employee() :
itsFirstName(""),itsLastName(""),itsAddress(""),itsSalary(0)
{ }
Employee::Employee(char * firstName, char * lastName, char * address, long salary) :
itsFirstName(firstName), itsLastName(lastName), itsAddress(address), itsSalary(salary)
{ }
Employee::Employee(const Employee & rhs):
itsFirstName(rhs.GetFirstName()),itsLastName(rhs.GetLastName()),itsAddress(rhs.GetAddress()),itsSalary(rhs.GetSalary())
{ }
Employee::~Employee() { }
Employee & Employee::operator= (const Employee & rhs)
{
if (this == &rhs)
return *this;
itsFirstName = rhs.GetFirstName();
itsLastName = rhs.GetLastName();
itsAddress = rhs.GetAddress();
itsSalary = rhs.GetSalary();
return *this;
}
int main()
{
cout << "Craeting Edie..." << endl;
Employee Edie("Jane", "Doe", "1461 Shore Parkway", 2000);
Edie.SetSalary(20000);
cout << "Calling SetFirstName with char * ..." << endl;
Edie.SetFirstName("Edythe");
cout << "Creating temporary string LastName..." << endl;
String LastName("Levine");
Edie.SetLastName(LastName);
cout << "Name: ";
cout << Edie.GetFirstName().GetString();
cout << " " << Edie.GetLastName().GetString();
cout << ".\nAddress: ";
cout << Edie.GetAddress().GetString();
cout << ".\nSalary: ";
cout << Edie.GetSalary();
cout << endl;
return 0;
}
注:在编译程序之前,需要取消对16.1中的cout语句的注释。可显示构造函数被调用的频繁程度。
16.4.按值传递导致复制:程序清单16,3表明,创建Employee对象时将导致String构造函数被调用3次,然后给两个String成员赋值时导致String构造函数被调用两次。且程序清单16.4重写main函数,并取消16.1中对ConstructorCount变量的注释。可通过观察ConstructorCount的变化来记录创建了多少个String对象。
程序清单 16.4 按值传递导致复制
#include "String.hpp"
class Employee
{
public:
Employee();
Employee(char *, char *, char * ,long);
~Employee();
Employee(const Employee&);
Employee & operator= (const Employee &);
const String & GetFirstName() const { return itsFirstName; }
const String & GetLastName() const { return itsLastName; }
const String & GetAddress() const { return itsAddress; }
long GetSalary() const { return itsSalary; }
void SetFirstName(const String & fName) { itsFirstName = fName; }
void SetLastName(const String & lName) { itsLastName = lName; }
void SetAddress(const String & address) { itsAddress = address; }
void SetSalary(long salary) { itsSalary = salary; }
private:
String itsFirstName;
String itsLastName;
String itsAddress;
long itsSalary;
};
Employee::Employee() : itsFirstName(""),itsLastName(""),itsAddress(""),itsSalary(0)
{ }
Employee::Employee(char * firstName, char * lastName, char * address, long salary) :
itsFirstName(firstName), itsLastName(lastName), itsAddress(address), itsSalary(salary)
{ }
Employee::Employee(const Employee & rhs):
itsFirstName(rhs.GetFirstName()),itsLastName(rhs.GetLastName()),itsAddress(rhs.GetAddress()),itsSalary(rhs.GetSalary())
{ }
Employee::~Employee() { }
Employee & Employee::operator= (const Employee & rhs)
{
if (this == &rhs)
return *this;
itsFirstName = rhs.GetFirstName();
itsLastName = rhs.GetLastName();
itsAddress = rhs.GetAddress();
itsSalary = rhs.GetSalary();
return *this;
}
void PrintFunc(Employee);
void rPrintFunc(const Employee&);
int main()
{
Employee Edie("Jane", "Doe", "1461 Shore Parkway", 2000);
Edie.SetSalary(20000);
Edie.SetFirstName("Edythe");
String LastName("Levine");
Edie.SetLastName(LastName);
cout << "Constructor conunt: ";
cout << String::ConstructorCount << endl;
rPrintFunc(Edie);
cout << "Constructor conunt: ";
cout << String::ConstructorCount << endl;
PrintFunc(Edie);
cout << "Constructor conunt: ";
cout << String::ConstructorCount << endl;
return 0;
}
void PrintFunc(Employee Edie)
{
cout << "Name: ";
cout << Edie.GetFirstName().GetString();
cout << " " << Edie.GetLastName().GetString();
cout << ".\nAddress: ";
cout << Edie.GetAddress().GetString();
cout << ".\nSalary: ";
cout << Edie.GetSalary();
cout << endl;
}
void rPrintFunc(const Employee & Edie)
{
cout << "Name: ";
cout << Edie.GetFirstName().GetString();
cout << " " << Edie.GetLastName().GetString();
cout << ".\nAddress: ";
cout << Edie.GetAddress().GetString();
cout << ".\nSalary: ";
cout << Edie.GetSalary();
cout << endl;
}
16.5.代理:是指使用被聚合类的成员执行包含类的功能。
16.6.聚合:将一个类对象声明为另一个类的成员,这也被称为has-a关系。聚合是一种“部分-整体”的关系。如:class A{ }; class B { A objA; //A的对象在B类中};
16.7.以继承的方式来实现聚合:当一个类使用另一个类的部分功能时,可以使用聚合。将类的操作工作交给包含在内部的另一个类来进行操作。
程序清单 16.5 将链表管理工作交给被聚合的PartList(在第二周复习的程序基础上进行修改)
#include
using namespace std;
//**************************** Part *********************************
//Abstract base class of parts
class Part
{
public:
Part() : itsPartNumber(1) { }
Part(int PartNumber) : itsPartNumber(PartNumber) { }
virtual ~Part() { };
int GetPartNumber() const { return itsPartNumber; }
virtual void Display() const = 0; //must be overridden
private:
int itsPartNumber;
};
//implementation of purs virtual function so that
//derived classes can chain up
void Part::Display() const
{
cout << endl << "Part Number: " << itsPartNumber << endl;
}
//********************** Car Part *********************************
class CarPart : public Part
{
public:
CarPart() : itsModelYear(94) { }
CarPart(int year, int partNumber);
virtual void Display() const
{
Part::Display();
cout << "Mode Year: ";
cout << itsModelYear << endl;
}
private:
int itsModelYear;
};
CarPart::CarPart(int year, int partNumber) : itsModelYear(year), Part(partNumber) { }
//************************ AirPlane Part *********************************
class AirPlanePart : public Part
{
public:
AirPlanePart() : itsEngineNumber(1) { };
AirPlanePart(int EngineNumber, int PartNumber);
virtual void Display() const
{
Part::Display();
cout << "Engine No.: ";
cout << itsEngineNumber << endl;
}
private:
int itsEngineNumber;
};
AirPlanePart::AirPlanePart(int EngineNumber, int PartNumber) : itsEngineNumber(EngineNumber), Part(PartNumber) { }
//**************************** Part Node ****************************
class PartNode
{
public:
PartNode(Part *);
~PartNode();
void SetNext(PartNode * node)
{
itsNext = node;
}
PartNode * GetNext() const;
Part * GetPart() const;
private:
Part * itsPart;
PartNode * itsNext;
};
//PartNode Implementations...
PartNode::PartNode(Part * pPart) : itsPart(pPart), itsNext(0) { }
PartNode::~PartNode()
{
delete itsPart;
itsPart = 0;
delete itsNext;
itsNext = 0;
}
//Returns NULL if no next PartNode
PartNode * PartNode::GetNext() const
{
return itsNext;
}
Part * PartNode::GetPart() const
{
if (itsPart)
return itsPart;
else
return NULL;//error
}
//**************************** Part List ****************************
class PartsList
{
public:
PartsList();
~PartsList();
//needs copy constructor and operator equals!
void Iterate(void (Part::*f)()const) const;
Part * Find(int & position, int PartNumber) const;
Part * GetFirst() const;
void Insert(Part *);
Part * operator[](int) const;
int GetCount() const { return itsCount; }
static PartsList & GetGlobalPartsList()
{
return GlobalPartsList;
}
private:
PartNode * pHead;
int itsCount;
static PartsList GlobalPartsList;
};
PartsList PartsList::GlobalPartsList;
PartsList::PartsList() : pHead(0), itsCount(0) { }
PartsList::~PartsList() { delete pHead; }
Part * PartsList::GetFirst() const
{
if (pHead)
return pHead->GetPart();
else
return NULL;//error catch here
}
Part * PartsList::operator[](int offset) const
{
PartNode * pNode = pHead;
if (!pHead)
return NULL;//error catch here
if (offset > itsCount)
return NULL;//error
for (int i = 0; i < offset; i++)
pNode = pNode->GetNext();
return pNode->GetPart();
}
Part * PartsList::Find(int & position, int PartNumber) const
{
PartNode * pNode = 0;
for (pNode = pHead, position = 0; pNode != NULL; pNode = pNode->GetNext(), position++)
{
if (pNode->GetPart()->GetPartNumber() == PartNumber)
break;
}
if (pNode == NULL)
return NULL;
else
return pNode->GetPart();
}
void PartsList::Iterate(void(Part::*func)()const) const
{
if (!pHead)
return;
PartNode * pNode = pHead;
do
(pNode->GetPart()->*func)();
while ((pNode = pNode->GetNext()) != 0);
}
void PartsList::Insert(Part * pPart)
{
PartNode * pNode = new PartNode(pPart);
PartNode * pCurrent = pHead;
PartNode * pNext = 0;
int New = pPart->GetPartNumber();
int Next = 0;
itsCount++;
if (!pHead)
{
pHead = pNode;
return;
}
//if this one smaller than head
//this one is new head
if (pHead->GetPart()->GetPartNumber() > New)
{
pNode->SetNext(pHead);
pHead = pNode;
return;
}
for (;;)
{
//if there is no next,append this new one
if (!pCurrent->GetNext())
{
pCurrent->SetNext(pNode);
return;
}
//if this goes after this one and before the next
//then intsert it here,otherwise get the next
pNext = pCurrent->GetNext();
Next = pNext->GetPart()->GetPartNumber();
if (Next > New)
{
pCurrent->SetNext(pNode);
pNode->SetNext(pNext);
return;
}
pCurrent = pNext;
}
}
class PartsCatalog
{
public:
void Insert(Part*);
int Exists(int PartNumber);
Part * Get(int PartNumber);
void ShowAll() { thePartsList.Iterate(&Part::Display); }//不加&也是正确的取值方式,但是有的编译器必须要加&才可以通过编译
private:
PartsList thePartsList;
};
void PartsCatalog::Insert(Part * newPart)
{
int partNumber = newPart->GetPartNumber();
int offset;
if (!thePartsList.Find(offset, partNumber))
{
thePartsList.Insert(newPart);
}
else
{
cout << partNumber << " was the ";
switch (offset)
{
case 0:
cout << "first ";
break;
case 1:
cout << "seconde";
break;
case 2:
cout << "third";
break;
default:
cout << offset + 1 << "th";
break;
}
cout << "entry.Rejected!" << endl;
}
}
int PartsCatalog::Exists(int PartNumber)
{
int offset;
thePartsList.Find(offset, PartNumber);
return offset;
}
Part * PartsCatalog::Get(int PartNumber)
{
int offset;
Part * thePart = thePartsList.Find(offset, PartNumber);
return thePart;
}
int main()
{
PartsCatalog pc;
Part * pPart = 0;
int PartNumber;
int value;
int choice = 99;
while (choice != 0)
{
cout << "(0)Quit (1)Car (2)Plane: ";
cin >> choice;
if (choice != 0)
{
cout << "New PartNumebr?:";
cin >> PartNumber;
if (choice == 1)
{
cout << "Model Year?: ";
cin >> value;
pPart = new CarPart(value, PartNumber);
}
else
{
cout << "Engine Number?: ";
cin >> value;
pPart = new AirPlanePart(value, PartNumber);
}
pc.Insert(pPart);
}
}
pc.ShowAll();
return 0;
}
16.8.私有继承:如果一个类需要访问另一个类的保护成员或者覆盖其中的部分方法时,必须使用继承。当不想将类的全部信息暴露给另一个类,可使用私有继承。私有继承从一个类派生出另一个类,同事使基类的成员对派生类来说是私有的。且需要注意的是:所有基类成员变量和函数都被视为私有的。不管它们在基类中的实际访问限制级别。并且私有继承不涉及继承接口,只涉及继承实现。
程序清单 16.6 私有继承
#include
using namespace std;
//**************************** Part *********************************
//Abstract base class of parts
class Part
{
public:
Part() : itsPartNumber(1) { }
Part(int PartNumber) : itsPartNumber(PartNumber) { }
virtual ~Part() { };
int GetPartNumber() const { return itsPartNumber; }
virtual void Display() const = 0; //must be overridden
private:
int itsPartNumber;
};
//implementation of purs virtual function so that
//derived classes can chain up
void Part::Display() const
{
cout << endl << "Part Number: " << itsPartNumber << endl;
}
//********************** Car Part *********************************
class CarPart : public Part
{
public:
CarPart() : itsModelYear(94) { }
CarPart(int year, int partNumber);
virtual void Display() const
{
Part::Display();
cout << "Mode Year: ";
cout << itsModelYear << endl;
}
private:
int itsModelYear;
};
CarPart::CarPart(int year, int partNumber) : itsModelYear(year), Part(partNumber) { }
//************************ AirPlane Part *********************************
class AirPlanePart : public Part
{
public:
AirPlanePart() : itsEngineNumber(1) { };
AirPlanePart(int EngineNumber, int PartNumber);
virtual void Display() const
{
Part::Display();
cout << "Engine No.: ";
cout << itsEngineNumber << endl;
}
private:
int itsEngineNumber;
};
AirPlanePart::AirPlanePart(int EngineNumber, int PartNumber) : itsEngineNumber(EngineNumber), Part(PartNumber) { }
//**************************** Part Node ****************************
class PartNode
{
public:
PartNode(Part *);
~PartNode();
void SetNext(PartNode * node)
{
itsNext = node;
}
PartNode * GetNext() const;
Part * GetPart() const;
private:
Part * itsPart;
PartNode * itsNext;
};
//PartNode Implementations...
PartNode::PartNode(Part * pPart) : itsPart(pPart), itsNext(0) { }
PartNode::~PartNode()
{
delete itsPart;
itsPart = 0;
delete itsNext;
itsNext = 0;
}
//Returns NULL if no next PartNode
PartNode * PartNode::GetNext() const
{
return itsNext;
}
Part * PartNode::GetPart() const
{
if (itsPart)
return itsPart;
else
return NULL;//error
}
//**************************** Part List ****************************
class PartsList
{
public:
PartsList();
~PartsList();
//needs copy constructor and operator equals!
void Iterate(void (Part::*f)()const) const;
Part * Find(int & position, int PartNumber) const;
Part * GetFirst() const;
void Insert(Part *);
Part * operator[](int) const;
int GetCount() const { return itsCount; }
static PartsList & GetGlobalPartsList()
{
return GlobalPartsList;
}
private:
PartNode * pHead;
int itsCount;
static PartsList GlobalPartsList;
};
PartsList PartsList::GlobalPartsList;
PartsList::PartsList() : pHead(0), itsCount(0) { }
PartsList::~PartsList() { delete pHead; }
Part * PartsList::GetFirst() const
{
if (pHead)
return pHead->GetPart();
else
return NULL;//error catch here
}
Part * PartsList::operator[](int offset) const
{
PartNode * pNode = pHead;
if (!pHead)
return NULL;//error catch here
if (offset > itsCount)
return NULL;//error
for (int i = 0; i < offset; i++)
pNode = pNode->GetNext();
return pNode->GetPart();
}
Part * PartsList::Find(int & position, int PartNumber) const
{
PartNode * pNode = 0;
for (pNode = pHead, position = 0; pNode != NULL; pNode = pNode->GetNext(), position++)
{
if (pNode->GetPart()->GetPartNumber() == PartNumber)
break;
}
if (pNode == NULL)
return NULL;
else
return pNode->GetPart();
}
void PartsList::Iterate(void(Part::*func)()const) const
{
if (!pHead)
return;
PartNode * pNode = pHead;
do
(pNode->GetPart()->*func)();
while ((pNode = pNode->GetNext()) != 0);
}
void PartsList::Insert(Part * pPart)
{
PartNode * pNode = new PartNode(pPart);
PartNode * pCurrent = pHead;
PartNode * pNext = 0;
int New = pPart->GetPartNumber();
int Next = 0;
itsCount++;
if (!pHead)
{
pHead = pNode;
return;
}
//if this one smaller than head
//this one is new head
if (pHead->GetPart()->GetPartNumber() > New)
{
pNode->SetNext(pHead);
pHead = pNode;
return;
}
for (;;)
{
//if there is no next,append this new one
if (!pCurrent->GetNext())
{
pCurrent->SetNext(pNode);
return;
}
//if this goes after this one and before the next
//then intsert it here,otherwise get the next
pNext = pCurrent->GetNext();
Next = pNext->GetPart()->GetPartNumber();
if (Next > New)
{
pCurrent->SetNext(pNode);
pNode->SetNext(pNext);
return;
}
pCurrent = pNext;
}
}
class PartsCatalog : private PartsList
{
public:
void Insert(Part*);
int Exists(int PartNumber);
Part * Get(int PartNumber);
void ShowAll() { thePartsList.Iterate(&Part::Display); }
private:
PartsList thePartsList;
};
void PartsCatalog::Insert(Part * newPart)
{
int partNumber = newPart->GetPartNumber();
int offset;
if (!thePartsList.Find(offset, partNumber))
{
thePartsList.Insert(newPart);
}
else
{
cout << partNumber << " was the ";
switch (offset)
{
case 0:
cout << "first ";
break;
case 1:
cout << "seconde";
break;
case 2:
cout << "third";
break;
default:
cout << offset + 1 << "th";
break;
}
cout << "entry.Rejected!" << endl;
}
}
int PartsCatalog::Exists(int PartNumber)
{
int offset;
thePartsList.Find(offset, PartNumber);
return offset;
}
Part * PartsCatalog::Get(int PartNumber)
{
int offset;
Part * thePart = thePartsList.Find(offset, PartNumber);
return thePart;
}
int main()
{
PartsCatalog pc;
Part * pPart = 0;
int PartNumber;
int value;
int choice = 99;
while (choice != 0)
{
cout << "(0)Quit (1)Car (2)Plane: ";
cin >> choice;
if (choice != 0)
{
cout << "New PartNumebr?:";
cin >> PartNumber;
if (choice == 1)
{
cout << "Model Year?: ";
cin >> value;
pPart = new CarPart(value, PartNumber);
}
else
{
cout << "Engine Number?: ";
cin >> value;
pPart = new AirPlanePart(value, PartNumber);
}
pc.Insert(pPart);
}
}
pc.ShowAll();
return 0;
}
注:当使PartsCatalog从PartsList私有继承而来时,对于PartsCatalog类的客户而言,PartsList类是不可见的。它们不能直接使用PartsList类的接口,不能调用PartsList的任何方法,但可以调用PartsCatalog的方法,而PartsCatalog的方法可以访问PartsList的任何方法。且PartsCatalog的方法可以直接调用PartsList的方法,唯一的例外时,当PartsCatalog覆盖了某个方法,而又需要调用该方法的PartsList版本时必须对方法名进行完全限定。
16.8.总结:1、当派生类是一种基类时应采用公有继承;2、需要将功能交给另一个类去实现但无需访问其保护成员时应使用聚合;3、需要以一个类的方式实现另一个类且需要访问基类的保护成员时,应使用私有继承。4、当需要使用多个基类实例时,不要使用私有继承,而必须使用聚合。不想让基类的成员对派生类的客户可用时,不用应该使用公有继承。
16.9.添加友元类:当需要同时创建多个类,将其作为一组,且需要将某个类的私有成员暴露给另一个类时,必须将后者声明为友元类。这样就将类的接口扩展为包括友元类。一个类将另一个类声明为友元后,前者所有的成员数和函数对后者来说都是公有的。且友元不可传递。友元关系不是互通的。类1声明为类2的友元,并不能使类2成为类1的友元。需要声明友元时,使用C++关键字friend:。
程序清单 16.7. 演示友元类(为说明友元关系,重写程序清单16.6)
#include
using namespace std;
//**************************** Part *********************************
//Abstract base class of parts
class Part
{
public:
Part() : itsPartNumber(1) { }
Part(int PartNumber) : itsPartNumber(PartNumber) { }
virtual ~Part() { };
int GetPartNumber() const { return itsPartNumber; }
virtual void Display() const = 0; //must be overridden
private:
int itsPartNumber;
};
//implementation of purs virtual function so that
//derived classes can chain up
void Part::Display() const
{
cout << endl << "Part Number: " << itsPartNumber << endl;
}
//********************** Car Part *********************************
class CarPart : public Part
{
public:
CarPart() : itsModelYear(94) { }
CarPart(int year, int partNumber);
virtual void Display() const
{
Part::Display();
cout << "Mode Year: ";
cout << itsModelYear << endl;
}
private:
int itsModelYear;
};
CarPart::CarPart(int year, int partNumber) : itsModelYear(year), Part(partNumber) { }
//************************ AirPlane Part *********************************
class AirPlanePart : public Part
{
public:
AirPlanePart() : itsEngineNumber(1) { };
AirPlanePart(int EngineNumber, int PartNumber);
virtual void Display() const
{
Part::Display();
cout << "Engine No.: ";
cout << itsEngineNumber << endl;
}
private:
int itsEngineNumber;
};
AirPlanePart::AirPlanePart(int EngineNumber, int PartNumber) : itsEngineNumber(EngineNumber), Part(PartNumber) { }
//**************************** Part Node ****************************
class PartNode
{
public:
friend class PartsList;
PartNode(Part *);
~PartNode();
void SetNext(PartNode * node)
{
itsNext = node;
}
PartNode * GetNext() const;
Part * GetPart() const;
private:
Part * itsPart;
PartNode * itsNext;
};
//PartNode Implementations...
PartNode::PartNode(Part * pPart) : itsPart(pPart), itsNext(0) { }
PartNode::~PartNode()
{
delete itsPart;
itsPart = 0;
delete itsNext;
itsNext = 0;
}
//Returns NULL if no next PartNode
PartNode * PartNode::GetNext() const
{
return itsNext;
}
Part * PartNode::GetPart() const
{
if (itsPart)
return itsPart;
else
return NULL;//error
}
//**************************** Part List ****************************
class PartsList
{
public:
PartsList();
~PartsList();
//needs copy constructor and operator equals!
void Iterate(void (Part::*f)()const) const;
Part * Find(int & position, int PartNumber) const;
Part * GetFirst() const;
void Insert(Part *);
Part * operator[](int) const;
int GetCount() const { return itsCount; }
static PartsList & GetGlobalPartsList()
{
return GlobalPartsList;
}
private:
PartNode * pHead;
int itsCount;
static PartsList GlobalPartsList;
};
PartsList PartsList::GlobalPartsList;
PartsList::PartsList() : pHead(0), itsCount(0) { }
PartsList::~PartsList() { delete pHead; }
Part * PartsList::GetFirst() const
{
if (pHead)
return pHead->GetPart();
else
return NULL;//error catch here
}
Part * PartsList::operator[](int offset) const
{
PartNode * pNode = pHead;
if (!pHead)
return NULL;//error catch here
if (offset > itsCount)
return NULL;//error
for (int i = 0; i < offset; i++)
pNode = pNode->itsNext;
return pNode->itsPart;
}
Part * PartsList::Find(int & position, int PartNumber) const
{
PartNode * pNode = 0;
for (pNode = pHead, position = 0; pNode != NULL; pNode = pNode->GetNext(), position++)
{
if (pNode->itsPart->GetPartNumber() == PartNumber)
break;
}
if (pNode == NULL)
return NULL;
else
return pNode->itsPart;
}
void PartsList::Iterate(void(Part::*func)()const) const
{
if (!pHead)
return;
PartNode * pNode = pHead;
do
(pNode->itsPart->*func)();
while (pNode = pNode->itsNext);
}
void PartsList::Insert(Part * pPart)
{
PartNode * pNode = new PartNode(pPart);
PartNode * pCurrent = pHead;
PartNode * pNext = 0;
int New = pPart->GetPartNumber();
int Next = 0;
itsCount++;
if (!pHead)
{
pHead = pNode;
return;
}
//if this one smaller than head
//this one is new head
if (pHead->itsPart->GetPartNumber() > New)
{
pNode->itsNext = pHead;
pHead = pNode;
return;
}
for (;;)
{
//if there is no next,append this new one
if (!pCurrent->itsNext)
{
pCurrent->itsNext = pNode;
return;
}
//if this goes after this one and before the next
//then intsert it here,otherwise get the next
pNext = pCurrent->itsNext;
Next = pNext->itsPart->GetPartNumber();
if (Next > New)
{
pCurrent->itsNext = pNode;
pNode->itsNext = pNext;
return;
}
pCurrent = pNext;
}
}
class PartsCatalog : private PartsList
{
public:
void Insert(Part*);
int Exists(int PartNumber);
Part * Get(int PartNumber);
void ShowAll() { Iterate(&Part::Display); }
private:
};
void PartsCatalog::Insert(Part * newPart)
{
int partNumber = newPart->GetPartNumber();
int offset;
if (!Find(offset, partNumber))
PartsList::Insert(newPart);
else
{
cout << partNumber << " was the ";
switch (offset)
{
case 0:
cout << "first ";
break;
case 1:
cout << "seconde";
break;
case 2:
cout << "third";
break;
default:
cout << offset + 1 << "th";
break;
}
cout << "entry.Rejected!" << endl;
}
}
int PartsCatalog::Exists(int PartNumber)
{
int offset;
Find(offset, PartNumber);
return offset;
}
Part * PartsCatalog::Get(int PartNumber)
{
int offset;
return (Find(offset, PartNumber));
}
int main()
{
PartsCatalog pc;
Part * pPart = 0;
int PartNumber;
int value;
int choice = 99;
while (choice != 0)
{
cout << "(0)Quit (1)Car (2)Plane: ";
cin >> choice;
if (choice != 0)
{
cout << "New PartNumebr?:";
cin >> PartNumber;
if (choice == 1)
{
cout << "Model Year?: ";
cin >> value;
pPart = new CarPart(value, PartNumber);
}
else
{
cout << "Engine Number?: ";
cin >> value;
pPart = new AirPlanePart(value, PartNumber);
}
pc.Insert(pPart);
}
}
pc.ShowAll();
return 0;
}
注:该程序清单中,友元声明放在公有部分,但并非必须这样做:可以将该语句放在类声明的任何地方而不会改变其含义。需要注意的是,使用友元类要非常小心,如果两个类联系非常紧密,其中一个类必须频繁访问另一个类的数据,则可能有充分的理由使用这种声明,否则,尽量少使用这种声明。通常使用公有存取器函数更简单,这样做后,当需要修改一个类时,不必修改另外一个类。
16.10.友元函数:将另一个类声明为友元,给它提供了所有访问权限。有时候可能不需要将这种级别权限授予整个类,而只授予这个类的一两个函数,则可以将另一个类的成员函数声明为友元,而不是将整个类声明为友元。事实上,可以将任何函数声明为友元函数,而不管它是否是另一个类的成员函数。
16.11.友元函数和运算符重载:程序清单16.1中的String类重载了operator+,还提供了一个接受常量字符指针作为参数的构造函数,以便可以使用C-风格字符串来创建String对象。C-风格字符串没有重载operator+。为解决这一问题,可在String中声明友元函数,它将operator+重载为接受两个String对象作为参数。这样,将通过合适的构造函数将C-风格字符串转换为String对象,然后使用两个String对象为参数来调用operator+。
程序清单 16.8 友元函数operator+
#include
#include
using namespace std;
class String
{
public:
//constructors
String();
String(const char * const);
String(const String &);
~String();
//overloaded operators
char & operator[] (int offset);
char operator[] (int offset) const;
String operator+ (const String &);
friend String operator+(const String &, const String &);
void operator+=(const String &);
String & operator= (const String &);
//General accessors
int GetLen() const { return itsLen; }
const char * GetString() const { return itsString; }
private:
String(int); //private constructor
char * itsString;
unsigned short itsLen;
};
//default constructor creates string of 0 bytes
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen = 0;
//cout<<"\tDefault string constructor"<
注:要将函数声明为友元,可使用关键字friend以及该函数的权限定名。将函数声明为友元并没有给它提供访问this指针的权限,但确实提供了访问所有私有和保护成员数据和函数的权限。
16.12.重载插入运算符:上述程序清单可以重载operator<< ( ).以提供更加便利的输出。
程序清单 16.9 重载operator<<
#include
#include
using namespace std;
class String
{
public:
//constructors
String();
String(const char * const);
String(const String & );
~String();
//overloaded operators
char & operator[] (int offset);
char operator[] (int offset) const;
String operator+ (const String &);
void operator+=(const String &);
String & operator= (const String &);
friend ostream & operator<<
(ostream & theStream, String & theString);
//General accessors
int GetLen() const { return itsLen; }
const char * GetString() const { return itsString; }
private:
String(int); //private constructor
char * itsString;
unsigned short itsLen;
};
//default constructor creates string of 0 bytes
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen = 0;
//cout<<"\tDefault string constructor"<
第十七部分 处理流
17.1.流概述:C++没有定义如何将数据写入屏幕或文件,也没有定义如何将数据读入程序。标准C++库包含用来方便输入和输出(I/O)的iostream库。将输入和输出同语言分开并使用库来处理输入喝输出的优点是,更容易使语言独立于平台。注:库是一组扩展名.obj的文件,可将它们链接到程序以提供额外的功能。这是最基本的代码重用形式。
17.2.其他库:除对平面文件输入外,流对C++编程来说不那么重要。C++程序已经发展到使用操作系统或编译器厂商提供的图形用户界面(GUI)来同屏幕、文件和用户交互。这包括Windows库、X Windows库以及Borland的Windows和X Windows用户界面抽象。这些库时专门针对操作系统的,并非C++标准的组成部分。
17.3.数据流的封装:文件输入和输出可使用iostream类完成。Iostream将数据流视为一个字节接一个字节的流。如果流的目标是文件或控制台屏幕,数据源通常是程序的一部分。如果流的方向与此相反,数据可以来自键盘或磁盘文件并流入到数据变量中。流的主要目标是将磁盘读取文件或将输入写入控制台屏幕的问题封装起来。
17.4.缓冲技术:将数据写入磁盘(和屏幕)是非常“昂贵”的。相对而言,将数据写入磁盘或从磁盘读取数据都要花很长时间,读写磁盘可能妨碍程序的执行。为解决这个问题,流提供了缓冲技术。使用缓冲技术时,数据被写入到流中,而不立刻写入到磁盘中。不断填充流的缓冲区,当缓冲区填满后,一次性将其中所有的数据写入磁盘。
17.5.刷新缓冲区:当缓冲区内部的数据还没有填满时,就使缓冲区中数据流出,这被称为刷新缓冲区。使用缓冲技术的危险之一是:如果数据下次输入/输出之前,没有刷新缓冲区,可能导致程序崩溃。这种情况下,可能丢失数据。
17.6.流和缓冲区:C++从面向对象的角度来实现流和缓冲区。它使用一系列的类和对象来完成这些任务:1、streambuf类管理缓冲区,其成员函数提供了填充、清空、刷新和处理缓冲区的其他功能;2、ios类是输入和输出流类的基类,它有一个成员变量为streambuf对象;3、istream和ostream类时从ios类派生而来的,用来专门管理输入和输出行为;4、iostream时从istream和ostream类派生而来的,提供了向屏幕写入数据和输入和输出方法;5、fstream类提供了文件输入和输出功能。
17.7.标准I/O对象:包含iostream类的C++程序启动时,将创建并初始化4个对象:1、cin:处理来自标准输入设备(键盘)的输入;2、cout:处理到标准输入设备(控制台屏幕)的输入;3、cerr:处理到标准错误设备(控制台屏幕)的非缓冲输出。由于这是非缓冲的,因此发送到cerr的任何数据都将立即写入标准错误设备,而不等到缓冲区填满或收到刷新命令;4、clog:处理输出到标准错误设备(控制台屏幕)的缓冲错误信息。这种输出通常被重定向到日志文件。
17.8.重定向标准流:每种标准流(输入、输出和错误)都可以重定向到其他设备。标准错误流(ceer)经常被重定向到文件,而标准输入流(cin)和输出流(cout)可使用操作系统命令重定向到文件。重定向指的是输出(或输入)发送到不同于默认设备的地方。重定向是一种操作系统功能,而不是iostream库的功能。C++只提供了访问4种标准设备的途径,要重定向到其他设备,需要用户去完成。注:管道技术(piping)指的是将一个程序的输出用作另一个程序的输入。
17.9.使用cin进行输入:全局对象cin负责输入,在程序中包含iostream后便可使用它。cin为重载了接受各种参数的提供的运算符、包括int & ,short & ,long & , double & ,float & ,char & ,char *等。编译器将判断输入数据的类型,调用相应的函数。由于参数是按引用传递的,因此提取运算符能够对原始变量进行操作。
程序清单 17.1 cin处理不同的数据类型
#include
using namespace std;
int main()
{
int myInt;
long myLong;
double myDouble;
float myFloat;
unsigned int myUnsigned;
cout << "Int: ";
cin >> myInt;
cout << "Long: ";
cin >> myLong;
cout << "Double: ";
cin >> myDouble;
cout << "Float: ";
cin >> myFloat;
cout << "Unsigned: ";
cin >> myUnsigned;
cout << endl << endl << "Int:\t" << myInt << endl;
cout << "Long:\t" << myLong << endl;
cout << "Double:\t" << myDouble << endl;
cout << "Float:\t" << myFloat << endl;
cout << "Unsigned:\t" << myUnsigned << endl;
return 0;
}
17.10.输出字符串:cin还能够处理字符串指针(char *)参数;因此,可以创建一个字符缓冲区,并用cin来填充它。缓冲区必须有足够的空间容纳整个字符串和空字符。对cin对象来说,空字符表示字符串结束。
程序清单 17.2 输入多个变量
#include
using namespace std;
int main()
{
int myInt;
long myLong;
double myDouble;
float myFloat;
unsigned int myUnsigned;
char myWord[50];
cout << "Int: ";
cin >> myInt;
cout << "Long: ";
cin >> myLong;
cout << "Double: ";
cin >> myDouble;
cout << "Float: ";
cin >> myFloat;
cout << "Word: ";
cin >> myWord;
cout << "Unsigned: ";
cin >> myUnsigned;
cout << endl << endl << "Int:\t" << myInt << endl;
cout << "Long:\t" << myLong << endl;
cout << "Double:\t" << myDouble << endl;
cout << "Float:\t" << myFloat << endl;
cout << "Unsigned:\t" << myUnsigned << endl;
cout << endl << endl << "Int,Long,Double,Float,Word,Unsigned: ";
cin >> myInt >> myLong >> myDouble;
cin >> myFloat >> myWord >> myUnsigned;
cout << endl << endl;
cout << "Int:\t" << myInt << endl;
cout << "Long: \t" << myLong<
17.11.>>的返回值:>>的返回值是一个istream对象引用。由于cin本身是istream对象,因此提取操作的返回值可用作下一次提取操作的输入。例:int one,two,three; cout<<"Enter three number: ";cin>>one>>two>>three;对于上述代码,首先执行的提取操作为cin>>one,其返回值是另一个istream对象。该对象的提取运算符将变量two作为参数,就像代码((cin>>one)>>two)>>three;
17.12.cin的其他成员函数:除了重载运算符>>外,cin还有很多其他成员函数。它们用于对输入进行细致的控制。以便:1、读取单个字符;2、读取字符串;3、忽略输入;4、查看缓冲区中的下一个字符;5、将数据放回缓冲区。
17.12.1.单字符输入:运算符>>接受一个字符引用作为参数,可用于标准输入中读取单个字符。成员函数get()也可用于获取单个字符,且有两种调用方式,不提供任何参数并使用其返回值;使用一个字符引用参数来调用。
17.12.1.1.使用不接受任何参数的get():get()的第一种形式不接受任何参数,它返回找到的字符值,如果达到文件末尾则返回EOF(文件末尾)。不接受任何参数的get()不常使用。这种形式的get()不能像cin那样用于级联多次输入,因为其返回值不是iostream对象。
程序清单 17.3. 使用不接受参数的get()
#include
int main()
{
char ch;
while ((ch = std::cin.get()) != EOF)
{
std::cout << "ch: " << ch << std::endl;
}
std::cout << std::endl << "Done!" << std::endl;
return 0;
}
注:要退出程序,必须通过键盘输入文件末尾标记。在DOS计算机上按Ctrl+Z,在UNIX上按Ctrl+D。并非每种istream实现都支持这种版本的get()。虽然它是ANSI/ISO标准的组成部分。
17.12.1.2.使用接受字符引用参数的get():用字符变量作为参数来调用get()时,把输入流中的下一个字符赋给该字符变量,返回值是一个iostream对象,因此这种形式的get()可以级联。
程序清单 17.4. 使用接受参数是get()
#include
int main()
{
char a,b,c;
std::cout << "Enter three lettern: ";
std::cin.get(a).get(b).get(c);
std::cout << "a: " << a << std::endl << "b: ";
std::cout << b << std::endl << "c: " << c << std::endl;
return 0;
}
注:需要跳过空白时,应使用提取运算符(>>)。需要检查包括空白在内的每个字符时应使用接受一个字符参数的get();尽量使用多条cin语句进行输入,它比较容易理解。
17.12.1.3.从标准输入读取字符串:要将输入存储到字符数组中,可使用提取运算符(>>),成员函数get()的第三个版本或getline()。
17.12.1.4get()的第三个版本:get()的第三个版本接受3个参数。第一个参数(pCharArray)时字符数组指针,第二个参数(StreamSize)是要读取的最大字符数加1,第三个参数(TermChar)是结束字符。如果第二个参数为20.get()将读取19个字符,然后在存储第一个参数的字符串末尾加上空字符。第三个参数(结束字符)默认为换行符(‘\n’)。如果get()在读取最大字符数之前遇到结束字符,将添加空字符,并将结束字符留在缓冲区。
程序清单 17.5 使用接受字符数组作为参数的get()
#include
using namespace std;
int main()
{
char stringOne[256];
char stringTwo[256];
cout << "Enter string one: ";
cin.get(stringOne, 256);
cout << "stringOne: " << stringOne << endl;
cout << "Enter string two: ";
cin >> stringTwo;
cout << "stringTwo: " << stringTwo << endl;
return 0;
}
注:接受3个参数的get()非常适合用来读取字符串,然而这并非唯一的解决方案。解决这种问题的另一个方法是使用getline()。如程序清单17.6所示。
程序清单 17.6. 使用getline()
#include
using namespace std;
int main()
{
char stringOne[256];
char stringTwo[256];
char stringThree[256];
cout << "Enter string one: ";
cin.getline(stringOne, 256);
cout << "stringOne: " << stringOne << endl;
cout << "Enter string two: ";
cin >> stringTwo;
cout << "stringTwo: " << stringTwo << endl;
cout << "Enter string Three: ";
cin.getline(stringThree, 256);
cout << "stringThree: " << stringThree << endl;
return 0;
}
注:getline()接受一个缓冲区和最大字符数作为参数;但与get()不同的是,它读取换行符并将其丢弃。get()不丢弃换行符,将其留在输入缓冲区中。因为在读取stringTwo时,提取运算符会把空格当做结束,所以当输入过长的句子后,只有在第一个空格前的数据被存入stringTwo中,剩余部分留在缓冲区中,当再次调用getline()时,缓冲区中剩余的字符变量将自动被getline()读取,所以用户没有第三次输入的机会。
17.12.2.使用cin.ignore():有时可能需要忽略行尾(EOL)或文件尾(EOF)之前的剩余字符,成员函数ignore()提供了这样的功能。它接受两个参数:要忽略的最大字符数和结束字符。代码ignore(80,‘\n’);将最多忽略80个字符,直到遇到换行符。然后丢去换行符,ignore()语句结束。
程序清单 17.7 使用ignore()
#include
using namespace std;
int main()
{
char stringOne[256];
char stringTwo[256];
cout << "Enter string one: ";
cin.get(stringOne, 256);
cout << "String One: " << stringOne << endl;
cout << "Enter string Two: ";
cin.getline(stringTwo, 256);
cout << "String Two: " << stringTwo << endl;
cout << endl << endl << "Now try again..." << endl;
cout << "Enter string one: ";
cin.get(stringOne, 256);
cout << "String One: " << stringOne << endl;
cin.ignore(256, '\n');
cout << "Enter string Two: ";
cin.getline(stringTwo, 256);
cout << "String Two: " << stringTwo << endl;
return 0;
}
注:第一次使用get()来读取字符串,get()将换行符之前的内容存储到stringOne中,并将换行符留在输入缓冲区中,随后使用getline()来读取缓冲区中换行符之前的内容。由于之前调用get()时将一个换行符留在了缓冲区中,因此用户还没有输入任何内容之前,getlien()以及结束了。而之后的ignore()忽略的换行符,当再次调用getline()时,缓冲区是空的。所以,用户可以进行第二次输入。
17.12.3.查看和插入字符:peek()h和putback():输入对象cin有两使用起来非常方便的方法:peek()和putback()。peek()查看但不提取下一字符,putback()将一个字符插入输入流中。
程序清单 17.8 使用peel()和putback()
#include
using namespace std;
int main()
{
char ch;
cout << "Enter a phrase: ";
while (cin.get(ch))
{
if (ch == '!')
cin.putback('$');
else
cout << ch;
while (cin.peek() == '#')
cin.ignore(1, '#');
}
return 0;
}
注:输入Now!is#the!time#for!fun#!。peek()和putback()常常用于分析字符串和其他数据,如编写编译器时。
17.13.使用cout进行输出:cout和重载的插入运算符(<<)将字符串、整数和其他数值写入到目的外置。还可以对数据进行格式化;对齐列以及十进制和十六进制书写数值数据。
17.13.1.刷新输出:endl写入一个换行符并刷新输出缓冲区,endl调用cout成员函数flush(),后者输出被缓冲的所有数据。也可以直接调用flush();需要清空输出缓冲区并将其中的内容写入屏幕时,这种方法非常方便。
17.13.2.执行输出的函数:就像get()和getline()时提取运算符的补充一样,put()和write()是插入运算符的补充。
17.13.2.1.使用put()写入字符:函数put()用于将单个字符写入输入设备。put()返回一个ostream引用,而cout时一个ostream对象,因此可以像级联插入运算符一样级联put().
程序清单 17.9 使用put()
#include
int main()
{
std::cout.put('H').put('e').put('l').put('l').put('o').put('\n');
return 0;
}
注:部分编译器不支持上述代码,如果不支持编译,忽略上述代码
17.13.2.2.使用write()写入更多字符:函数write()的工作原理和插入运算符(<<)相同,只是它接受一个指出最多写入多少个字符的参数:cout.write(Text,Size);write()的第一个参数是要打印的文本,第二个参数(Size)指定要打印Text中的多少个字符。Size可能大于或小于Text实际长度。如果大于,将输出内存中Text后面的值。
程序清单 17.10 使用write()
#include
using namespace std;
int main()
{
char One[] = "One if by land";
int fullLength = strlen(One);
int tooShort = fullLength - 4;
int tooLong = fullLength + 6;
cout.write(One, fullLength) << endl;
cout.write(One, tooShort) << endl;
cout.write(One, tooLong) << endl;
return 0;
}
注:最后一行的输出可能会出现乱码。因为访问了未被初始化的变量占用的内存。
17.13.3.控制符、标记和格式化指令:输出流包含很多状态标记,用于指定计数方式(十进制或十六进制)、字段宽度和字段填充字符。状态表标记长1字节,其中的每一位都有特殊含义。每个ostream标记都可以用成员函数和控制符来设置。
17.13.3.1.使用cout.width():输出的默认宽度为刚好能够容纳输出缓冲区中的数字、字符或字符串。可以使用width()来修改默认宽度设置。width()是成员函数,必须通过cout对象来调用。它只修改下一个输出字段的宽度,然后字段宽度设置立刻恢复到默认值。
程序清单 17.11 调整输出的宽度
#include
using namespace std;
int main()
{
cout << "Strat >";
cout.width(25);
cout << 123 << "< End" << endl;
cout << "Strat >";
cout.width(25);
cout << 123 << "< Next >";
cout << 456 << "< End" << endl;
cout << "Strat >";
cout.width(4);
cout << 123456 << "< End" << endl;
return 0;
}
注:将宽度设置为小于输出内容与设置为刚刚好等效,当宽度被设置为比输出内容小时,不会导致输出被截短。
17.13.3.2 设置填充字符:通常情况下,cout用于空格填充字段,当需要使用其他符号填充时,可以使用fill(),并将要用来填充的字符作为参数传递给它。
程序清单 17.12 使用fill()
#include
using namespace std;
int main()
{
cout << "Start >";
cout.width(25);
cout << 123 << "End <" << endl;
cout << "Start >";
cout.width(25);
cout.fill('*');
cout << 123 << "End <" << endl;
cout << "Start >";
cout.width(25);
cout << 456 << "End <" << endl;
return 0;
}
注:不同于函数width()那样只对下一次输出有效,fill()设置的填充字符将一直有效,直到修改为止。
17.13.3.3.管理输出状态-设置标记:当对象的部分或全部数据成员表示可在程序执行期间修改的条件时,该对象被认为是有状态的。例如可以设置是否显示末尾额零。iostream对象使用标记来记录其状态,可以调用setf()并传递一个预定义的枚举常量来设置这些标记。这些枚举常量的作用域为iostream类(ios),因此将其作为setf()的参数时,需要采用全限定方式ios::flagname。当使用那些需要参数的标记,还必须包含iomanip。
一些iostream设置标记 | |||||
标记 | 用途 | ||||
showpoint | 根据精度设置显示小数点和末尾的零 | ||||
showpos | 在正数前面加上正号(+) | ||||
left | 让输出左对齐 | ||||
right | 让输出右对齐 | ||||
internal | 让符号左对齐并让数值右对齐 | ||||
scientific | 用科学表示法显示浮点数 | ||||
fixed | 以小数点表示法显示浮点数 | ||||
showbase | 在十六进制前加上0x,指出这是十六进制值 | ||||
Uppercase | 在十六进制和科学表示法中使用大写字母 | ||||
dec | 以十进制方式表示 | ||||
oct | 以八进制方式表示 | ||||
hex | 以十六进制方式表示 |
程序清单 17.13 使用setf
#include
#include
using namespace std;
int main()
{
const int number = 185;
cout << "The number is " << number << endl;
cout << "The number is " << hex << number << endl;
cout.setf(ios::showbase);
cout << "The number is " << hex << number << endl;
cout << "The number is ";
cout.width(10);
cout << hex << number << endl;
cout << "The number is ";
cout.width(10);
cout.setf(ios::left);
cout << hex << number << endl;
cout << "The number is ";
cout.width(10);
cout.setf(ios::internal);
cout << hex << number << endl;
cout << "The number is " << setw(10) << hex << number << endl;
return 0;
}
注:与插入运算符一起使用时,无需对标记进行全限定;要采用十六进制表示只需使用hex即可;而用作函数setf()参数时,必须对标记进行全限定;要采用十六进制表示必须传递ios::hex。
17.14.流和printf()函数比较:大多数C++程序还提供了标准C语言I/O库,其中包括printf()。虽然printf()在某些方面比cout使用起来更容易,但应尽量避免使用它。printf()没有提供类型安全性,很容易无意间命令它将整数当做字符显示或反过来。printf()也不支持类,因此无法告诉它如何打印类数据,而必须将类成员逐个传递给printf()。printf()将一个格式化字符串作为第一个参数,并将一系列的值作为其他参数。格式化字符串使用引号括起的文本和转换说明符。所有转换说明符都必须以百分号(%)打头。
常见的转换说明符 | |
说明符 | 用于 |
%s | 字符串 |
%d | 整型 |
%l | 长整型 |
%ld | 双精度 |
%f | 浮点数 |
程序清单 17.14 使用printf()进行打印
#include
int main()
{
printf("%s", "Hello world\n");
char * phrase = "Hello again!\n";
printf("%s", phrase);
int x = 5;
printf("%d\n", x);
char * phraseTwo = "Here's some value: ";
char * phraseThree = " and also these: ";
int y = 7, z = 35;
long longVar = 98456;
float floatVar = 8.8f;
printf("%s %d %d", phraseTwo, y, z);
printf("%s %ld %f\n", phraseThree, longVar, floatVar);
char * phraseFour = "Formatted: ";
printf("%s %5d %10d %10.5f\n", phraseFour, y, z, floatVar);
return 0;
}
注:没有进行严格的类型检查,同时printf()也不能被声明为类的友元或成员函数,因此要打印类的各个成员数据,必须在参数列中将每个存取器方法传递给printf()函数。
17.15.总结控制输出:在C++中,要格式化输出,需要结合使用特殊字符、输出控制符和标记:可使用插入运算符将包含下述特殊字符的输出字符串传递给cout:\n:换行符;\r:回车;\t:制表符;\\:反斜杠;\ddd(八进制数):ASCII字符;\a:响铃;(控制符和cout运算符一起使用,要使用接受参数的控制符,必须在程序中包含iomanip)。以下是不需要包含头文件iomanip就可以使用的控制符:flush:刷新输出缓冲区;endl:换行并刷新输出缓冲区;cot:采用八进制;dec:采用十进制;hex:采用十六进制;下面是需要包含头文件iomanip才能使用的控制符:setbase(base):设置计数方式(0为十进制,8为八进制,10为十进制,16为十六进制);setw(width):设置最小输出字段宽度;setfill(ch):制定了字段宽度时使用的填充字符;setprecision(p):设置浮点数的精度;setiosflags(f):设置一个或多个ios标记;resetiosflags(f):重置一个或多个ios标记。除了flush、endl和setw外,所有控制符在被修改或程序结束前一直有效。setw在当前cout结束后恢复默认值。很多标记可用作setiosflags和resetiosflags控制符的参数。
17.16.文件输入和输出:在处理来自键盘或硬盘的数据以及输出到屏幕或硬盘的数据方面,流提供了统一的方法。不管在哪种情况下,都可以使用插入和提取运算符以及其他相关函数和控制符。要打开和关闭文件,需要创建ifstream和ofstream对象。
17.16.1.使用ofstream:用于读写文件的对象被称为ofstream对象。这些对象是从iostream对象派生出来。要开始写文件,首先必须创建一个ofstream对象,然后将其与磁盘上的文件关联起来。要使用ofstream对象,必须在程序中包含头文件fstream。注:由于fstream包含了iostream,因此不需要再显示地包含iostream。
17.16.2.条件状态:iostream对象包含报告输入和输出状态的标记,可以使用布尔函数eof()、bad()、fail()和good()来检查这些标记。iostream对象遇到文件末尾(EOF)时,函数eof()返回true;当试图进行非法操作时,函数bad()将返回true;在bad()返回true或操作失败时,函数fail()将返回true;最后,在其他3个函数都返回false时,函数good()返回true。
17.16.3.打开文件进行输入和输出:要使用文件,必须首先打开它。要使用ofstream打开文件。声明一个ofstream实例,并将该文件名作为参数传递给它。
程序清单 17.15 打开文件以便进行读写
#include
#include
using namespace std;
int main()
{
char fileName[80];
char buffer[255];
cout << "File name: ";
cin >> fileName;
ofstream fout(fileName);
fout << "This line written directly to the file..." << endl;
cout << "Enter text for the file: ";
cin.ignore(1, '\n');
cin.getline(buffer, 255);
fout << buffer << endl;
fout.close();
ifstream fin(fileName);
cout << "Here's the contents of the file: " << endl;
char ch;
while (fin.get(ch))
cout << ch;
cout << endl << "***End of file contents.***" << endl;
fin.close();
return 0;
}
17.16.4.修改ofstream打开文件时的默认行为:打开文件时的默认行为是,如果文件不存在则创建它,如果文件已经存在则删除其内容。如果不采用打开文件时的默认行为,可以给ofstream类的构造函数提供第二个参数。该参数的合法取值包括:ios::app:附加到已有文件的末尾,而不是删除其内容;ios::ate:跳到文件末尾,但可以在文件的任何地方写入数据;ios::trunc:默认值。删除已有文件的内容;ios::nocreate:如果文件不存在,打开操作失败;ios::nocreplace:如果文件已经存在,打开操作失败。注:app时append的缩写;ate时at end的缩写;而trunc是truncate的缩写。
程序清单 17.16 在文件末尾添加内容
#include
#include
using namespace std;
int main()
{
char fileName[80];
char buffer[255];
cout << "Please reenter the file name: ";
cin >> fileName;
ifstream fin(fileName);
if (fin)
{
cout << "Current file contents: " << endl;
char ch;
while (fin.get(ch))
cout << ch;
cout << endl << "***End of file contents.***" << endl;
}
fin.close();
cout << endl << "Opening " << fileName << " in append mode..." << endl;
ofstream fout(fileName, ios::app);
if (!fout)
{
cout << "Unable to opern " << fileName << " for appending." << endl;
return (1);
}
cout << endl << "Enter text for the file: ";
cin.ignore(1, '\n');
cin.getline(buffer, 255);
fout << buffer << endl;
fout.close();
fin.open(fileName);
if(!fin)
{
cout << "Unable to open " << fileName << " for reading." << endl;
return (1);
}
cout << endl << "Here's the contents of the file: " << endl;
char ch;
while (fin.get(ch))
cout << ch;
cout << endl << "***End of file contents.***" << endl;
fin.close();
return 0;
}
注:if(fout)和测试if(fout.fail())等效。每次打开文件时应该进行测试以核实文件被成功打开;应重用已有的ifstream和ofstream对象。使用完fstream对象后应该关闭它们。不要试图关闭cin和cont或给它们重新赋值。
17.17.二进制文件和文本文件:有些操作系统区分二进制文件和文本文件。文本文件将所有内容都保存为文本。这样做虽然效率很低,但优点是可以用简单程序等来阅读文本。为了帮助文件系统区分二进制文件和文本文件,C++提供了标记ios::binary。在很多系统上该标记都被忽略,因为所有数据都以二进制方式存储。在一些非常严谨的系统上,ios::binary标记是非法的,甚至不能通过编译。二进制文件不仅能够存储整数和字符串,还能够存储整个数据结构,可以使用fstream的write()方法一次性写入所有的数据。如果使用write()写入,可以用read()来读取。然而,这两个函数都接受一个字符指针作为参数,因此必须将类的地址强制转换为字符指针。这些函数的第二个参数是要读写的字符数,这可以使用sizeof()来确定。注:写入的只是类的数据,而不包括方法;读取的也只有数据。
程序清单 17.17 将对象的内容写入文件
#include
#include
using namespace std;
class Animal
{
public:
Animal(int weight,long days) : itsWeight(weight),DaysAlive(days) { }
~Animal() { }
int GetWeight() const { return itsWeight; }
void SetWeight(int weight) { itsWeight = weight; }
long GetDaysAlive() const { return DaysAlive; }
void SetDaysAlive(long days) { DaysAlive = days; }
private:
int itsWeight;
long DaysAlive;
};
int main()
{
char fileName[80];
cout << "Please enter the file name: ";
cin >> fileName;
ofstream fout(fileName, ios::binary);
if (!fout)
{
cout << "Unable to open " << fileName << " for writing." << endl;
return (1);
}
Animal Bear(50, 100);
fout.write((char *)& Bear, sizeof Bear);
fout.close();
ifstream fin(fileName, ios::binary);
if (!fin)
{
cout << "Unable to open " << fileName << " for writing." << endl;
return (1);
}
Animal BearTwo(1, 1);
cout << "BearTwo weight: " << BearTwo.GetWeight() << endl;
cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl;
fin.read((char*)&BearTwo, sizeof BearTwo);
cout << "BearTwo weight: " << BearTwo.GetWeight() << endl;
cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl;
fin.close();
return 0;
}
17.18.命令行处理:很多操作系统都允许用户在启动程序时给它传递参数,这些参数被称为命令行选项,通常在命令行上用空格将它们分开。例:SomeProgram Param1 Param2 Param3.这些参数并不直接传递给main()函数。相反每个main()函数都被传入两个参数。第一个参数是一个整数,它指定了命令行参数数目,其中包括程序名本身,因此每个程序至少有一个参数。传递给函数main()的第二个参数是一个字符串指针数组,由于数组名是一个指向数组第一个元素的常量指针,因此可以将参数声明为指向字符指针的指针、指向字符数组的指针或字符数组的数组。第一个参数通常名为argc(参数数目),但也可以给它指定其他名称;第二个参数通常名为argv(参数向量),这通常是一种约定。通常应检测argc以核实main()函数接受到预期的参数数目,并且使用argv来访问字符串本身。注意argv[0]是程序名,argv[1]是以字符串表示的程序的第一个参数。如果程序接受两个数字作为参数,需要将这些数字转换为字符串。
#include
int main(int argc, char ** argv)
{
std::cout << "Received " << argc << " arguments..." << std::endl;
for (int i = 0; i < argc; i++)
std::cout << "argument " << i << "i: " << argv[i] << std::endl;
return 0;
}
注:必须在命令行运行上述代码,或者在编译器中设置命令行参数。
程序清单 17.18 使用命令行参数来获取文件名
#include
#include
using namespace std;
class Animal
{
public:
Animal(int weight,long days) : itsWeight(weight),DaysAlive(days) { }
~Animal() { }
int GetWeight() const { return itsWeight; }
void SetWeight(int weight) { itsWeight = weight; }
long GetDaysAlive() const { return DaysAlive; }
void SetDaysAlive(long days) { DaysAlive = days; }
private:
int itsWeight;
long DaysAlive;
};
int main(int argc, char * argv[])
{
if (argc != 2)
{
cout << "Usage: " << argv[0] << "filename>" << endl;
return (1);
}
ofstream fout(argv[1], ios::binary);
if (!fout)
{
cout << "Unable to open " << argv[1] << " for writing." << endl;
return (1);
}
Animal Bear(50, 100);
fout.write((char*)&Bear, sizeof Bear);
fout.close();
ifstream fin(argv[1], ios::binary);
if (!fout)
{
cout << "Unable to open " << argv[1] << " for writing." << endl;
return (1);
}
Animal BearTwo(1, 1);
cout << "BearTwo weight: " << BearTwo.GetWeight() << endl;
cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl;
fin.read((char*)&BearTwo, sizeof BearTwo);
cout << "BearTwo weight: " << BearTwo.GetWeight() << endl;
cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl;
fin.close();
return 0;
}
第十八部分 创建和使用名称空间
18.1.名称空间:当程序的两个部分中出现作用域相同的同名变量时。将发生名称冲突。为了减少冲突,可使用名称空间。名称空间在某些方面与类相似,去语法与类极其相似。在名称空间中声明的东西归名称空间所有。名称空间中的所有内容都是公有的,名称空间还可以嵌套。函数可在名称空间体内定义,也可在名称空间体外定义。如果函数是在名称空间体外定义的,必须在程序中将名称空间导入其全局名称空间中,否则调用函数时,必须用名称空间进行限定。
18.2.根据名称解析函数和类:在编译器分析源代码并建立函数和变量名列表时,也检查名称冲突。那些不能由编译器解决的冲突将留给链接器去解决。编译器不能检查跨越目标目标文件或编译单元之间的名称冲突,这是链接器的工作。对于这类情况,编译器甚至不会发出警告。链接器连接失败并显示下述消息的情况屡见不鲜:identifier multiply defined。其中,identifier是一种被命名的类型。如果在不同的编译单元中定义了作用域相同的相同名称,将看到这个链接器消息。如果在同一个文件重复定义了作用域相同的名称,将导致编译错误。
注:以下两个程序在一起编译,链接器将会显示错误消息。
18.1.a 第一个定义integerValue的程序清单
.//file first.cpp
int integerValue = 0;
int main()
{
int integerValue = 0;
//...
return 0;
}
18.1.b 第二个定义integerValue的程序清单
//file second.cpp
int integerValue = 0;
//end of second.cpp
注:如果这些名称的作用域不同,编译器和链接器将不会显示错误消息。
18.3.隐藏:编译器还可能发出有关标识符隐藏的警告。编译器应该发出警告,在程序里,可能隐藏了同名的全局变量。要使用在main()外面声明的同名变量,必须显示地指定要使用的作用域为全局的哪个变量。
程序清单 18.2.a 演示标识符隐藏的第一个程序清单
//file first.cpp
int integerValue = 0;
int main()
{
int integerValue = 0;
::integerValue = 10;//assign to global "integerValue"
//...
return 0;
}
程序清单 18.2.b 演示标识符隐藏的第二个程序清单
//file second.cpp
int integerValue = 0;
//end of second.cpp
注:请注意作用域解析运算符::的用法,它表明使用的是全局(而不是局部)变量integerValue。在函数外部定义的两个int变量的问题在于,它们的名称和可见性相同,因此将导致链接错误。
18.4.变量的可见性:可见性(visibility)指的是对象(无论它是变量、类还是函数)的作用域。函数外部声明和定义的变量的作用域为整个文件(全局),该变量的可见性为其定义位置到文件末尾。在代码块中定义的变量的作用域为代码块(局部)。
程序清单 18.3.变量的作用域
int lobalScopeInt = 5;//1
void f()
{
int localScopeInt = 10;//2
}
int main()
{
int localScopeInt = 15;//3
{
int localScopeInt = 30;//4
}
return 0;
}
注:1f()中和main()中均可见。但在f()内部的2将隐藏1;而main()中的3将隐藏1,且无法使用2;而4将隐藏3,1;且无法使用1.
18.5.链接性:名称的链接性可以为内部(internal)或外部(external)。这两个术语指的是名称在多个编译单元还是一个编译单元中可用。链接性为内部的名称只在定义它的编译单元中可用。链接性为外部的名称对其他编译器单元来说也是可用的。
程序清单 18.4.a 内部链接性和外部链接性
//file: first.cpp
int externalInt = 5;
const int j = 10;
int main()
{
return 0;
}
程序清单 18.4.b 内部链接性和外部连接性
//file: seconde.cpp
extern int externalInt;
int anExternalInt = 10;
const int j = 10;
注:在first.cpp中,第1行定义的externalInt变量的链接性外为外部的。虽然它在first.cpp中定义的,但在second.cpp中也可以访问它。两个文件中定义的了两个j为const,const变量的链接性默认为内部。可以显示声明const变量的链接性以覆盖其默认链接性。如程序清单18.5a和18.5b
程序清单 18.5.a 覆盖const变量的默认链接性
//file:first.cpp
extern const int j = 10;
程序清单 18.5.b 覆盖const变量的默认链接性
//file: second.cpp
#include
extern const int j;
int main()
{
std::cout << "j is " << j << std::endl;
return 0;
}
18.6.静态全局变量:标准委员会建议不要使用静态全局变量。声明静态全局变量的方法为:static int staticInt = 10; 建议不使用static来限制外部变量的作用域,将来这样做可能是非法的。应使用名称空间而不是static。
18.7.创建名称空间:名称空间的声明语法类似于结构和类:首先使用关键字 namespace,然后是可选的名称空间名和左大括号。名称空间声明以及右大括号。需要注意的是:没有分号。对于已命名的名称空间、可以有多个实例,这些实例可以位于同一个文件中,也可以位于多个编译单元中,编译器将它们合并成一个名称空间。C++标准库名称空间std是具有这种特质的典范。因为标准库是一个逻辑功能组,但它过于庞大和复杂,无法再单个文件中。名称空间的基本思想是,将相关的项目组合到一个特定的(已命名)区域。
程序清单 18.6.a 将相关的项目组合到一起
//header1.h
namespace Widow
{
void move(int x, int y);
}
程序清单 18.6.b 将相关的项目组合到一起
//header2.h
namespace Window
{
void resize(int x, int y);
}
注:名称空间Window分布在两个头文件中,编译器将函数move()和resize()都视为名称空间Window的组成部分。
18.8.声明和定义类型:在名称空间中,可以声明和定义类型和函数。当然这是一个设计和维护的问题。优秀的设计要求将接口和实现分开。不仅对于类应遵循这条原则,对于名称空间也应该如此。所以应该在名称空间体外定义名称空间中的函数。这样做可以将函数的声明和定义分开,并避免名称空间体凌乱不堪。通过将函数定义同名称空间分离,可以将名称空间及包含的声明放在头文件中,而将定义放在实现文件中。
程序清单 18.7.a 在名称空间中声明函数头
//file header.h
namespace Window
{
void move(int x, int y);
//other declarations...
}
程序清单 18.7.b 将实现放在源代码文件中
//file impl.cpp
void Window::move(int x, int y)
{
//code to move the window
}
18.9.添加新成员:要添加新成员,只能在名称空间体内进行;不能使用限定符语句来定义新成员。采用这种方式将导致编译错误。添加新成员时,不应该指定访问限定符,名称空间中所有的成员都是公有的。
18.10.嵌套名称空间:可以将名称空间嵌套在另一个名称空间中,原因是名称空间的定义也是声明。要访问被嵌套的名称空间,必须使用包含它的名称空间来限定。如果嵌套了名称空间,必须依次限定每个名称空间。例:namespace Window { namespace Pane{void size (int x, int y);} }。要在名称空间Window外部访问函数size(),必须用这两个名称空间的名称限定这个函数。如:Window::Pane::size(10,20);
18.11.使用名称空间:程序清单18.8中,可以看到使用名称空间的例子以及作用域解析运算符的相关用法。这个程序中,首先在名称空间Window中声明了所有需要的类型和函数,然后定义了已声明的成员函数。这些成员函数都是在名称空间外定义的,并使用作用域解析运算符显式地标志了函数名。
程序清单 18.8 使用名称空间
#include
namespace Window
{
const int MAX_X = 30;
const int MAX_Y = 40;
class Pane
{
public:
Pane();
~Pane();
void size(int x, int y);
void move(int x, int y);
void show();
private:
static int count;
int x;
int y;
};
}
int Window::Pane::count = 0;
Window::Pane::Pane() : x(0), y(0) { }
Window::Pane::~Pane() { }
void Window::Pane::size(int x, int y)
{
if (x < Window::MAX_X && x > 0)
Pane::x = x;
if (y < Window::MAX_Y && y > 0)
Pane::y = y;
}
void Window::Pane::move(int x, int y)
{
if (x < Window::MAX_X && x > 0)
Pane::x = x;
if (y < Window::MAX_Y && y > 0)
Pane::y = y;
}
void Window::Pane::show()
{
std::cout << " x " << Pane::x;
std::cout << " y " << Pane::y;
std::cout << std::endl;
}
int main()
{
Window::Pane pane;
pane.move(20, 20);
pane.show();
return 0;
}
18.12.关键字using:关键字using用于using编译指令和using声明。关键字using的语法决定是编译指令还是声明。
18.12.1.using编译指令:using编译指令将名称空间中声明的所有的名称导入到当前作用域中,这样引用这些名称时无需用其所属的名称空间进行限定。using编译指令的作用域从其声明之处开始到当前作用域结束,using编译指令可用于任何作用域级别中,也就是说,可以在代码块作用域中使用它,离开该代码块后,名称空间中所有的名称将不在作用域中。在局部作用域中声明的变量将隐藏被导入到该作用域的名称空间中的同名变量。即使在局部变量后导入名称空间,该局部变量也将隐藏名称空间中的同名变量。这时,需要使用名称空间中变量时,需要用名称空间名来全限定它。使用被定义为全局又在名称空间中定义了的名称可能导致歧义。这种歧义只在使用了该名称时才出现。仅仅导入名称空间不会导致歧义。
18.12.2.using声明:using声明类似于using编译指令,只是using声明提供更细致的控制,具体地说,using声明用于将名称空间的一个名称导入到当前作用域中,然后便可以只使用其名称来引用该对象。这种声明不影响名称空间中的其他名称,使用using声明可以更细致地控制将名称空间中的哪些名称导入到当前作用域中,而using编译指令将名称空间中所有名称都导入到当前作用域中。将名称导入作用域中后,该名称一直可见,直到到达当前作用域末尾。这与其他声明相同。using声明可用于全局名称空间中或任何局部作用域中。将名称空间中的名称导入到已经声明了同名变量的局部作用域中将导致错误,反之亦然。使用using声明导入到局部作用域中的名称将隐藏该该作用域外声明的其他同名变量。using声明优于using编译指令。因为using编译指令违背名称空间机制的初衷。声明更准确,因为它明确地制定了要导入到作用域中的名称。using声明不会污染全局名称空间,而using编译指令会(当然,除非要导入名称空间中所有的名称),使用using声明可将名称隐藏、全局名称空间污染和歧义降低到易于控制水平。
18.13.名称空间别名:名称空间别名用于给已命名的名称空间提供另一个名称。别名提供了引用明曾空间简捷方式。尤其是当名称空名很长时,创建别名可避免冗长的重复输入。例:namespace the_software_company { //... } namespace TSC = the_software_company; 这时就可以用TSC来声明名称空间内的内容了。
18.14.未命名的名称空间:未命名的名称空间是没有名称的名称空间。未命名空间的一种常见用途是:防止目标文件或其他编译单元中的全局数据发生名称冲突。每个编译单元都有独一无二的未命名名称空间。在未命名的名称空间(每个编译单元一个)中定义的所有名称都可以不加显示限定的情况下进行引用。
程序清单 18.9.a 一个未命名的名称空间
//file: one.cpp
namespace
{
int value;
char p(char * p);
//...
}
程序清单 18.9.b 另一个未命名的名称空间
//file: two.cpp
namespace
{
int value;
char p(char * p);
//...
}
int main()
{
char c = p(char * ptr);
return 0;
}
注:每个文件中的名称、值和函数p()都是不同的。在编译单元中引用(未命名的名称空间中)名称时,不用进行限定。在上面的程序中,通过在每个文件中调用函数p()演示了这种用法。这种用法隐含着包含一条导入未命名的名称空间的using编译指令。因此,不能访问另一个编译单元中未命名的名称空间中的成员。未命名的名称空间的行为与链接性为外部的静态对象相同。所以常用名称空间来替换静态声明代码。另一种看待未命名名称空间的方法是,将其视为链接性为内部的全局变量。
18.15.标准名称空间std:名称空间的典范时C++标准库。标准库全部位于std的名称空间中。所有的函数、类、对象和模板都是在名称空间std中声明的。所有头文件都使用名称空间功能,如果包含多个标准头文件并指定using编译指令,则头文件中声明的所有东西将都被导入全局名称空间中。所以在此之前很多的程序清单中,都违反了这条准则。
程序清单 18.10 使用std名称空间的正确方式
#include
using std::cin;
using std::cout;
using std::endl;
int main()
{
int value = 0;
cout << "So, how many eggs did you say you wanted?" << endl;
cin >> value;
cout << value << " eggs, sunny-side up!" << endl;
return (0);
}
注:也可以对使用的名称空间进行全限定。
程序清单 18.11 对名称进行全限定
#include
int main()
{
int value = 0;
std::cout << "So, how many eggs did you say you wanted?" << std::endl;
std::cin >> value;
std::cout << value << " eggs, sunny-side up!" << std::endl;
return (0);
}
注:对名称进行全限定的方法可能适合于小程序,对于代码量很大的程序可能非常繁琐。
注:本章内容旨在理解模板的强大和灵活。模板可以创建一个类:可以更改其处理的东西的类型。虽然标准模板库非常强大,提供了一组标准化的容器类,其中包括数组、链表等,但学习模板的最佳方法是创建自己的模板。当然,商业程序中,几乎肯定使用标准模板库类而不是创建自己的模板类。
19.1.模板:C++程序员可使用的一种功能强大的工具是参数化类型(模板)。模板定义:模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。模版可以分为两类,一个是函数模版,另外一个是类模版。(这个定义源自:http://www.cppblog.com/abilitytao/archive/2009/04/03/78795.html)。实例化时根据模板创建一个具体的类型,根据模板创建的类被称为模板的实例。注:模板实例不同于使用模板创建的类的实例。“实例化”通常指定是根据类创建一个实例(对象)。参数化模板提供的功能是:创建一个通用类,然后通过将类型作为参数传递给这个类来创建特定的实例。实例化模板前,必须定义它。
19.2.创建模板定义:模板的基本生命由关键字template和表示类型的参数组成,格式如下:template
程序清单 19.1 类模板Array
#include
using namespace std;
const int DefaultSize = 10;
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Arrays &);
T & operator[] (int offset) { return pType[offSet]; }
int getSize() { return itsSize; }
private:
T * pType;
int itsSize;
};
注:可以使用typename代替class。应使用相对来说更为清晰的关键字。在类型为类时使用关键字class,在类型不是类时使用关键字typename。在某种程度上说,创建模板旨在为了C++对宏的需求。
19.3.使用名称:在类声明中使用创建的模板类时,无需对其进行限定。在程序的其他地方,将使用 classname
19.4.模板实现的缺陷:一些较老的编译器不支持模板,然而模板是ANSI C++标准的组成部分,所有主要编译器厂商都在最新的编译器版本中支持模板。
程序清单 19.2. 模板类Array的实现
#include
const int DefaultSize = 10;
class Animal
{
public:
Animal(int);
Animal();
~Animal() { }
int GetWeight() const { return itsWeight; }
void Display() const { std::cout << itsWeight; }
private:
int itsWeight;
};
Animal::Animal(int weight) : itsWeight(weight)
{ }
Animal::Animal() : itsWeight(0)
{ }
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete [] pType; }
Array & operator= (const Array &);
T & operator[] (int offset) { return pType[offset]; }
const T & operator[] (int offset) const
{
return pType[offset];
}
int GetSize() const { return itsSize; }
private:
T * pType;
int itsSize;
};
template
Array::Array(int size) : itsSize(size)
{
pType = new T[size];
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
template
Array & Array :: operator= (const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
int main()
{
Array theArray;
Array theZoo;
Animal * pAnimal;
for (int i = 0; i < theArray.GetSize(); i++)
{
theArray[i] = i * 2;
pAnimal = new Animal(i * 3);
theZoo[i] = *pAnimal;
delete pAnimal;
}
for (int j = 0; j < theArray.GetSize(); j++)
{
std::cout << "theArray[" << j << "]: \t";
std::cout << theArray[j] << "\t\t";
std::cout << "theZoo[" << j << "]: \t";
theZoo[j].Display();
std::cout << std::endl;
}
return 0;
}
19.4.将实例化的模板对象传递给函数:要将Array对象传递给常规函数,必须传递Array的一个实例,而不是模板。因此要创建一个接受一个Array实例作为参数的函数,其参数类型的声明为:void SomeFunction(Array19.5.模板和友元:模板类可以声明3种友元:1、非模板友元类或函数;2、通过模板友元类或函数;3、特定类型的模板友元类或函数。
19.6.非模板友元类和函数:可以将任何类或函数声明为模板类的友元。模板的每个实例都将正确处理友元关系,就像友元关系实在该实例中声明的一样。
程序清单 19.3 非模板友元函数
#include
using std::cout;
using std::endl;
const int DefultSize = 10;
class Animal
{
public :
Animal(int);
Animal();
~Animal() { }
int GetWeight() const { return itsWeight; }
void Display() const { cout << itsWeight; }
private:
int itsWeight;
};
Animal :: Animal(int weight) : itsWeight(weight) { }
Animal :: Animal() : itsWeight(0) { }
template
class Array
{
public:
Array(int itsSize = DefultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array &);
T & operator[] (int offset) { return pType[offset]; }
const T & operator[] (int offset) const
{return pType[offset];}
int GetSize() const { return itsSize; }
friend void Intrude(Array);
private:
T * pType;
int itsSize;
};
void Intrude(Array theArray)
{
cout << endl << "*** Intrude ***" << endl;
for (int i = 0; i < theArray.itsSize; i++)
cout << "i: " << theArray.pType[i] << endl;
cout << endl;
}
template
Array::Array(int size) : itsSize(size)
{
pType = new T[size];
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
template
Array & Array ::operator=(const Array & rhs)
{
if (this = &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
int main()
{
Array theArray;
Array theZoo;
Animal * pAnimal;
for (int i = 0; i < theArray.GetSize(); i++)
{
theArray[i] = i * 2;
pAnimal = new Animal(i * 3);
theZoo[i] = *pAnimal;
}
int j;
for (j = 0; j < theArray.GetSize(); j++)
{
cout << "theZoo[" << j << "]:\t";
theZoo[j].Display();
cout << endl;
}
cout << "Now use the friend function to ";
cout << "find the members of Array ";
Intrude(theArray);
cout << endl << "Done." << endl;
return 0;
}
19.7.通用模板友元类和函数:给上述例子添加一个显示运算符将很有帮助,这样可以将值发送给输出流,进而基于类型被正确处理。一种方法是,为每种可能的Array类型声明一个显示运算符,但这样有悖于将Array作为模板的初衷。因此需要的是一个插入运算符,它用于任何类型的Array:ostream & operator << (ostream & , Arrat
程序清单 19.4 使用运算符<<
#include
using std::cout;
using std::endl;
using std::cin;
using std::ostream;
const int DefaultSize = 10;
class Animal
{
public:
Animal(int);
Animal();
~Animal() { }
int GetWeight() const { return itsWeight; }
void Display() const { cout << itsWeight; }
private:
int itsWeight;
};
Animal::Animal(int weight) : itsWeight(weight)
{ }
Animal::Animal() : itsWeight(0)
{ }
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array&);
T & operator[] (int offset) { return pType[offset]; }
const T & operator[] (int offset) const { return pType[offset]; }
int GetSize() const { return itsSize; }
template//标记
friend ostream& operator<< (ostream&, Array&);
private:
T * pType;
int itsSize;
};
template
ostream & operator<< (ostream & output, Array & theArray)
{
for (int i = 0; i < theArray.itsSize; i++)
output << "[" << i << "] " << theArray[i] << endl;
return output;
}
template
Array::Array(int size) : itsSize(size)
{
pType = new T[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
template
Array & Array::operator= (const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
int main()
{
bool Stop = false;
int offset, value;
Array theArray;
while (Stop == false)
{
cout << "Enter an offset (0 -- 9) ";
cout << "and a value.(-1 to stop): ";
cin >> offset >> value;
if (offset < 0)
break;
if (offset > 9)
{
cout << "*** Please use values between 0 and 9 ***" << endl;
continue;
}
theArray[offset] = value;
}
cout << endl << "Here's the entire array: " << endl;
cout << theArray << endl;
return 0;
}
注:如果使用的是Microsoft编译器,注释有“标记”的那一行必须要存在,虽然不写也是符合C++标准的。然而使用Microsoft C++编译器,这一行必不可少。
19.8.使用模板对象:可以像使用其他类型那样使用模板:可以按值或引用将它们作为参数传递给函数,特可以按值或引用从函数返回它们。
程序清单 19.5 将模板对象传递给函数和从函数返回模板对象
#include
using std::cout;
using std::endl;
using std::cin;
using std::ostream;
const int DefaultSize = 10;
class Animal
{
public:
Animal(int);
Animal();
~Animal() { }
int GetWeight() const { return itsWeight; }
void SetWeight(int theWeight) { itsWeight = theWeight; }
friend ostream & operator<< (ostream &, const Animal &);
private:
int itsWeight;
};
ostream & operator<< (ostream & theStream, const Animal & theAnimal)
{
theStream << theAnimal.GetWeight();
return theStream;
}
Animal::Animal(int weight) : itsWeight(weight)
{ }
Animal::Animal() : itsWeight(0)
{ }
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array&);
T & operator[] (int offset) { return pType[offset]; }
const T & operator[] (int offset) const { return pType[offset]; }
int GetSize() const { return itsSize; }
template
friend ostream& operator<< (ostream&, Array&);
private:
T * pType;
int itsSize;
};
template
ostream & operator<< (ostream & output, Array & theArray)
{
for (int i = 0; i < theArray.itsSize; i++)
output << "[" << i << "] " << theArray[i] << endl;
return output;
}
template
Array::Array(int size) : itsSize(size)
{
pType = new T[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
template
Array & Array::operator= (const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
void IntFillFunction(Array& theArray);
void AnimalFillFunction(Array & theArray);
int main()
{
Array intArray;
Array animalArray;
IntFillFunction(intArray);
AnimalFillFunction(animalArray);
cout << "itnArray..." << endl << intArray;
cout << endl;
cout << "animalArray..." << endl << animalArray << endl;
return 0;
}
void IntFillFunction(Array & theArray)
{
bool Stop = false;
int offset, value;
while (Stop == false)
{
cout << "Enter an offset (0 -- 9) ";
cout << "and a value.(-1 to stop): ";
cin >> offset >> value;
if (offset < 0)
break;
if (offset > 9)
{
cout << "*** Please use values between 0 and 9 ***" << endl;
continue;
}
theArray[offset] = value;
}
}
void AnimalFillFunction(Array& theArray)
{
Animal * pAnimal;
for (int i = 0; i < theArray.GetSize(); i++)
{
pAnimal = new Animal;
pAnimal->SetWeight(i * 100);
theArray[i] = *pAnimal;
delete pAnimal;
}
}
19.9.使用具体化函数:在程序清单19.5中,如果给Animal的构造函数和析构函数中添加打印语句的话,将发现一些没有预料到的Animal的构造和析构。将对象添加到数组中,将调用该对象的默认构造函数,然而Array构造函数仍将0赋给数组每个元素。为了减少浪费,而专门使用针对相应类的构造函数时,可显式地为其提供一个构造函数实现,这种具体化被称为模板具体化。
程序清单 19.6 具体化模板实现
#include
using std::cout;
using std::endl;
using std::cin;
using std::ostream;
const int DefaultSize = 3;
class Animal
{
public:
Animal(int);
Animal();
~Animal();
int GetWeight() const { return itsWeight; }
void SetWeight(int theWeight) { itsWeight = theWeight; }
friend ostream & operator<< (ostream &, const Animal &);
private:
int itsWeight;
};
ostream & operator<< (ostream & theStream, const Animal & theAnimal)
{
theStream << theAnimal.GetWeight();
return theStream;
}
Animal::Animal(int weight) : itsWeight(weight)
{
cout << "animal(int)" << endl;
}
Animal::Animal() : itsWeight(0)
{
cout << "animal()" << endl;
}
Animal::~Animal()
{
cout << "Desttroyed an animal..." << endl;
}
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array&);
T & operator[] (int offset) { return pType[offset]; }
const T & operator[] (int offset) const { return pType[offset]; }
int GetSize() const { return itsSize; }
template
friend ostream& operator<< (ostream&, Array&);
private:
T * pType;
int itsSize;
};
//定义了Array构造函数的模板行为。
template
Array::Array(int size = DefaultSize) : itsSize(size)
{
pType = new T[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
template
Array & Array::operator= (const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
template
ostream & operator<< (ostream & output, Array & theArray)
{
for (int i = 0; i < theArray.itsSize; i++)
output << "[" << i << "] " << theArray[i] << endl;
return output;
}
//专门用于Animal类型的Array的构造函数
Array::Array(int AnimalArraySize) : itsSize(AnimalArraySize)
{
pType = new Animal[AnimalArraySize];
}
void IntFillFunction(Array& theArray);
void AnimalFillFunction(Array& theArray);
int main()
{
Array intArray;
Array animalArray;
IntFillFunction(intArray);
AnimalFillFunction(animalArray);
cout << "itnArray..." << endl << intArray;
cout << endl;
cout << "animalArray..." << endl << animalArray << endl;
return 0;
}
void IntFillFunction(Array & theArray)
{
bool Stop = false;
int offset, value;
while (Stop == false)
{
cout << "Enter an offset (0 -- 2) and a value. ";
cout << "(-1 to stop): ";
cin >> offset >> value;
if (offset < 0)
break;
if (offset > 2)
{
cout << "*** Please use values between 0 and 2 ***" << endl;
continue;
}
theArray[offset] = value;
}
}
void AnimalFillFunction(Array& theArray)
{
Animal * pAnimal;
for (int i = 0; i < theArray.GetSize(); i++)
{
pAnimal = new Animal(i * 10);
theArray[i] = *pAnimal;
delete pAnimal;
}
}
注:专门用于Animal类型的Array构造函数中,使用Animal默认构造函数为每个Aniaml设置初始值,但没有显示赋值。
19.10.静态成员和模板:模板可以声明静态数据成员,在这种情况下,将为根据模板可创建的每个类创建一组静态数据,即,如果在模板中添加了一个静态成员,则每种类型的类都有一个这样的成员。
程序清单 19.7 在模板中使用静态成员数据和函数
#include
using std::cout;
using std::endl;
using std::cin;
using std::ostream;
const int DefaultSize = 3;
class Animal
{
public:
Animal(int);
Animal();
~Animal();
int GetWeight() const { return itsWeight; }
void SetWeight(int theWeight) { itsWeight = theWeight; }
friend ostream & operator<< (ostream &, const Animal &);
private:
int itsWeight;
};
ostream & operator<< (ostream & theStream, const Animal & theAnimal)
{
theStream << theAnimal.GetWeight();
return theStream;
}
Animal::Animal(int weight) : itsWeight(weight)
{
//cout << "animal(int)" << endl;
}
Animal::Animal() : itsWeight(0)
{
//cout << "animal()" << endl;
}
Animal::~Animal()
{
//cout << "Desttroyed an animal..." << endl;
}
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; itsNumberArrays--; }
Array & operator= (const Array&);
T & operator[] (int offset) { return pType[offset]; }
const T & operator[] (int offset) const { return pType[offset]; }
int GetSize() const { return itsSize; }
static int GetNumberArrays() { return itsNumberArrays; }
template
friend ostream& operator<< (ostream&, Array&);
private:
T * pType;
int itsSize;
static int itsNumberArrays;
};
template
int Array::itsNumberArrays = 0;
template
Array::Array(int size = DefaultSize) : itsSize(size)
{
pType = new T[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
itsNumberArrays++;
}
template
Array & Array::operator= (const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetSize();
pType = new T[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
itsNumberArrays++;
}
template
ostream & operator<< (ostream & output, Array & theArray)
{
for (int i = 0; i < theArray.itsSize; i++)
output << "[" << i << "] " << theArray[i] << endl;
return output;
}
int main()
{
cout << Array::GetNumberArrays() << " integer arrays" << endl;
cout << Array::GetNumberArrays();
cout << " animal arrays " << endl << endl;
Array intArray;
Array animalArray;
cout << Array::GetNumberArrays() << " integer arrays" << endl;
cout << Array::GetNumberArrays();
cout << " animal arrays " << endl << endl;
Array * pIntArray = new Array;
cout << Array::GetNumberArrays() << " integer arrays" << endl;
cout << Array::GetNumberArrays();
cout << " animal arrays " << endl << endl;
delete pIntArray;
cout << Array::GetNumberArrays() << " integer arrays" << endl;
cout << Array::GetNumberArrays();
cout << " animal arrays " << endl << endl;
return 0;
}
注:对静态数据进行初始化,用模板对其进行全限定。访问模板静态成员的方法与访问其他类的静态成员相同。
19.11.总结:1、当一个概念适用于不同类(或基本数据类型)的对象时,应该使用模板来实现它;2、应使用模板函数参数将其实例限制为类型为安全的;3、必要时应在模板中使用静态成员;4、应根据类型覆盖模板的函数、以具体化模板的行为。
19.12.标准模板库:标准模板库(STL)和标准C++库的其他组成部分一样,STL也是可以在各种操作系统之间移植的。基本所有的主流编译器厂商都在其编译器中提供了STL。STL是一个基于模板的容器类库,其中包括向量、链表、队列和堆栈。STL还包含大量的算法。STL旨在避免编程时大量重复的工作。STL是可重用的,知道如何使用某个STL容器后,便可以在所有的程序中使用它而无需重新开发。
19.12.1.使用容器:容器是用于存储其他对象的对象。标准C++库提供了一系列的容器类,它们是功能强大的工具,标准模板库容器类分两种:顺序容器和关联容器。顺序容器支持对其成员(元素)的顺序和随机访问。关联容器经过了优化,可根据键值访问其中元素。所有的STL容器类都是在名称空间 std中定义的。
19.12.2.理解顺序容器:标准模板库中的顺序容器提供了对一系列对象进行顺序访问的高效途径。标准C++库提供了5种顺序容器:向量、链表、堆栈、双端队列和队列。
19.12.2.1.向量容器:STL提供了一个vector容器类,其行为类似于数组,但相对于标准C++数组功能更强大,使用起来更安全。vector是经过优化的容器,提供了通过索引快速访问其元素的功能。容器类vector是位于名称空间std中的头文件
程序清单 19.8 创建向量及访问其中的元素
#include
#include
#include
using std::cout;
using std::cin;
using std::endl;
using std::string;
using std::ostream;
using std::vector;
class Student
{
public:
Student();
Student(const string & name, const int age);
Student(const Student & rhs);
~Student();
void SetName(const string & name);
string GetName() const;
void SetAge(const int age);
int GetAge() const;
Student & operator= (const Student & rhs);
private:
string itsName;
int itsAge;
};
Student::Student() : itsName("New Student"),itsAge(16)
{ }
Student::Student(const string & name,const int age) : itsName(name),itsAge(age)
{ }
Student::Student(const Student & rhs) : itsName(rhs.GetName()),itsAge(rhs.GetAge())
{ }
Student::~Student() { }
void Student::SetName(const string & name)
{
itsName = name;
}
string Student::GetName() const
{
return itsName;
}
void Student::SetAge(const int age)
{
itsAge = age;
}
int Student::GetAge() const
{
return itsAge;
}
Student & Student::operator= (const Student & rhs)
{
itsName = rhs.GetName();
itsAge = rhs.GetAge();
return *this;
}
ostream & operator<< (ostream & os, const Student & rhs)
{
os << rhs.GetName() << " is " << rhs.GetAge() << " years old";
return os;
}
template
void ShowVector(const vector& v);
typedef vector SchoolClass;
int main()
{
Student Harry;
Student Sally("Sally", 15);
Student Bill("Bill", 17);
Student Peter("Peter", 16);
SchoolClass EmptyClass;
cout << "EmptyClass: " << endl;
ShowVector(EmptyClass);
SchoolClass GrowingClass(3);
cout << "GrowingClass(3): " << endl;
ShowVector(GrowingClass);
GrowingClass[0] = Harry;
GrowingClass[1] = Sally;
GrowingClass[2] = Bill;
cout << "GrowingClass(3) after assiging students: " << endl;
ShowVector(GrowingClass);
GrowingClass.push_back(Peter);
cout << "GrowingClass() after added 4th student: " << endl;
ShowVector(GrowingClass);
GrowingClass[0].SetName("Harry");
GrowingClass[0].SetAge(18);
cout << "GrowingClass() after Set:" << endl;
ShowVector(GrowingClass);
return 0;
}
template
void ShowVector(const vector & v)
{
cout << "max_size()= " << v.max_size();
cout << "\tsize() = " << v.size();
cout << "\tcapacity() =" << v.capacity();
cout << "\t" << (v.empty() ? " empty " : " not empty ");
cout << endl;
for (int i = 0; i < v.size(); ++i)
cout << v[i] << endl;
cout << endl;
}
注:1、如果类的实例可能被存储到向量中,应为其定义默认构造函数;2、还应为前面所说的类定义一个复制构造函数;3、还应为前面所说的类定义一个重载的赋值运算符。尽量不要创建自己的向量类,可以使用STL中的vector类。由于它是C++标准的组成部分,因此遵循该标准的编译器应该提供了vector类。
19.12.2.2.容器vector的其他成员函数:1、函数front()返回对向量第一个元素的引用;2、函数back()返回对向量中最后一个元素的引用;3、函数at()的作用与下标运算符[ ]相同,但比向量的[ ]实现更安全,因为它检查传递给它的下标是否在有效的范围内(当然,也可以修改[ ]运算符的实现,让其也进行这样的检查)。如果不在有效范围内,它将引发outofrangge异常。4、函数insert()在向量的指定位置插入一个或多个节点;5、函数popback()删除向量中的最后一个元素;6、函数remove()从向量中删除一个或多个成员。
19.13.链表向量:链表是需要频繁插入或删除元素时最适合使用的容器。STL容器类list是位于名称空间std中的头文件中定义的。list类是以双向链表的方式实现的,其中每个节点都有指向链表中前一个节点和下一个节点的链接。vector类有的成员函数在list类中都有。链表的链接通常是使用指针实现的,使用它旨在避免指针带来的一些危险,可以对迭代器解除引用来获取它指向的节点。
程序清单 19.9 使用迭代器来遍历链表
#include
#include
using std::list;
using std::endl;
using std::cout;
using std::cin;
typedef list IntegerList;
int main()
{
IntegerList intList;
for (int i = 1; i <= 10; i++)
intList.push_back(i * 2);
for (IntegerList::const_iterator ci = intList.begin(); ci != intList.end(); ++ci)
cout << *ci << " ";
return 0;
}
注:list类还提供了push_front()和pop_front()函数,它们的功能与push_back和pop_back()相同,只是它们不是在链表的末尾而是在开头添加和删除元素。
19.14.堆栈容器:在计算机编程中,最常用的数据结构之一是堆栈,然而,堆栈不是作为独立的容器类来实现的,而是作为容器包装器(也就是模板),模板类stack是在名称空间std中的文件
19.15.双端队列容器:双端队列类似于双向向量,它继承了容器类vector在顺序读写方面的效率,然而容器类deque还提供了优化的前端和后端操作。这些操作的实现类似于容器类list:只为新元素分配内存。deque类的这种特性避免了像list类那样重新为整个容器分配内存。因此,双端队列非常适合在一端或两端进行插入和删除以及顺序访问元素至关重要的应用。
19.16.队列容器:队列是计算机编程中另一种常用的数据结构。在队列的一端添加元素,从另一端取出元素。队列是FIFO(先入先出)结构。和stack一样,queue也是以容器包装器的方式实现的。容器必须支持front()、back()、push_back()和pop_front()操作。
19.17.理解关联容器:向量类似于改进的数组,它具备数组的所有特征,还新增了其他特性。但是,向量也有数组的弱点之一:不能根据除下标之外的其他索引来查找元素。关联容器提供了根据关键字来快速存取值的功能。顺序容器被设计为使用索引或迭代器来顺序和随机存取元素,而关联器被设计为使用关键字来快速随机存取元素。标准C++库提供了5种关联容器:map(映射)、multimap(多重映射)、set(集合)、multiset(多重集)和bitset(位组)
19.17.1.映射容器:该名称源自它们包含映射,从关键字到相关值的映射。
程序清单 19.10 容器类map
#include
#include
#include
注:在映射容器中,元素的关键字必须是独一无二的,即任何两个元素的关键字都不能相同。在STL中,关键字-值是用包含两个成员的结构来实现的:first和second。可通过这些成员来访问节点的关键字和值。也可以使用push_back()和insert()函数将关键字-值对添加到映射容器中。
19.17.2.其他关联容器:容器类multimap是一个不要求关键字独一无二的map类,多个元素可以有相同的关键字。容器类set也类似于map类,唯一差别是,其元素不是由关键字-值对组成,而只有关键字。容器类multiset是允许关键字重复的set类。容器类bitset是用于存储一系列位的模板。
19.18.使用算法类:容器非常适合存储一系列元素。所有标准容器都定义了操纵容器及其元素的操作,一组通用的算法可避免为每个新容器编写操作。标准库提供了大约60种标准算法,它们执行最基本、最常见的容器操作。标准算法在名称空间std中的头文件
程序清单 19.11. 函数对象
#include
using std::cout;
using std::endl;
template
class Print
{
public:
void operator() (const T & t)
{
cout << t << " ";
}
};
int main()
{
Print DoPrint;
for (int i = 0; i < 5; ++i)
DoPrint(i);
return 0;
}
19.18.1.非修改型操作:非修改型操作来自算法库,它们执行的不修改元素的操作。这些操作包括for_each()、find()、search()和count()等。
程序清单 19.12 使用for_each()算法
#include
#include
#include
using std::cin;
using std::cout;
using std::endl;
using std::vector;
template
class Print
{
public:
void operator() (const T & t)
{
cout << t << " ";
}
};
int main()
{
Print DoPrint;
vector vInt(5);
for (int i = 0; i < 5; i++)
vInt[i] = i * 3;
cout << "for_each()" << endl;
for_each(vInt.begin(), vInt.end(), DoPrint);
cout << endl;
return 0;
}
注:所有C++标准算法都是在19.18.2.修改型算法:修改型操作执行修改容器中元素的操作哦,其中包括填充和重新排序。
程序清单 19.13 修改型算法
#include
#include
#include
using namespace std;
template
class Print
{
public:
void operator() (const T & t)
{
cout << t << " ";
}
};
int main()
{
Print DoPrint;
vector vInt(10);
fill(vInt.begin(), vInt.begin() + 5, 1);
fill(vInt.begin() + 5, vInt.end(), 2);
for_each(vInt.begin(), vInt.end(), DoPrint);
cout << endl;
return 0;
}
19.19.排序及相关操作:第三类算法是排序及相关操作,这包括合并、部分排序、进行复制的部分排序、二分法搜索、下限和上限检车、计算交集、计算差集、计算最小值、计算最大值、置换等。
19.20.什么时候使用模板:除类处理的元素类型外,所有行为或几乎所有行为都不变时使用模板。如果需要复制类代码,且只修改一个或多个成员类型,应考虑使用模板。另外,在试图修改类,使之将祖先类对象作为操作对象时(降低类型安全性)或使两个不相关的类有共同祖先以便类能够与它们协同工作(也会降低类型安全性)时,应使用模板。注:当每个实例(无论其类型是什么)都应该为当前类的友元时,应该使用通用模板友元类。当两个类之间建立一对一的关系时,使用特定类型的模板友元类或函数。
19.21.存储到标准容器的要求:这个类必须创建默认构造函数、复制构造函数和重载的赋值运算符。
第二十部分 处理错误和异常
20.1.程序中的各种错误:实际的程序中很少没有bug,程序越大,有bug的可能性越大。实际上,有些bug会隐藏在发布版本中,造成不可预知的损失。软件业中最大的问题就是有错误、不稳定的代码。很多重要的编程工作中,花费最大的是测试和修改。造成bug的主要原因有:1、逻辑性差;2、语法错误。在开发过程中,发现逻辑问题的时间越晚,修复它的代价越高。与只是偶尔导致程序崩溃的错误相比,发现和修复让程序能够通过编译器但首次测试时就会出现的错误(导致程序每次运行都崩溃的错误)的代价更低些。比逻辑和语法错误更常见的运行阶段问题是脆弱性;另一些程序在内存不足、软盘不在软驱中或Internet连接断开时崩溃。为避免这种脆弱性,程序员致力于提高程序的健壮性(bulletproof)。区分bug、逻辑错误和异常至关重要。bug时由于程序员犯错引发的,逻辑错误是由于程序员对解决问题的方式不了解引起的;异常时由于不常见、但可预见的问题引起的。
20.2.异常情况:无法消除异常情况,只能为异常情况做好准备。当出现异常时,程序选择包括:1、崩溃;2、通知用户并妥善退出;3、采取正确的措施,在不影响用户的情况下继续运行。
程序清单 20.1. 导致异常情形
#include
using std::cin;
using std::cout;
using std::endl;
const int DefaultSize = 10;
int main()
{
int top = 90;
int bottom = 0;
cout << "top/2 = " << (top / 2) << endl;
cout << "top divided by bottom = ";
cout << (top / bottom) << endl;
cout << "top/3 = " << (top / 3) << endl;
cout << "Done." << endl;
return 0;
}
注:该程序被有意设计成导致崩溃;然而,如果程序要求用户输入两个数,并将它们相除,也可能出现这样的问题。C++的异常处理提供了一种类型安全的集成方法,来应对程序运行时出现的可预见到但不常发生的情况。
20.3.异常的基本思想:1、计算机试图执行一段代码。这段代码可能要求分配资源、锁定文件或执行其他各种任务;2、包含应对代码由于异常原因而执行失败的逻辑(代码);3、部分代码被其他代码使用时,也需要一种机制来将有关问题的信息传递到下一级。应该有一条从问题发生的代码到处理错误状态代码的路径,如果函数之间存在中间层,应该给它们提供解决问题的机会,但不应要求它们包含只是为了传递错误状态的代码。异常处理将这三点结合在一起。
20.4.异常处理的组成部分:要处理异常,必须首先确定需要监视哪段代码可能发生的异常。这可以使用try块来完成。应创建try块来包围可能导致问题的代码块。try块的基本格式如下:try { SomeDangerousFunction(); } catch(...) { ... }。这个例子中,当SomeDangerousFunction()执行时,如果发生任何异常,都将被发现并被捕获。要监视异常,只需要加上关键字try和大括号,当然,如果异常发红色呢过,需要采取措施来处理它。当try块中的代码执行时,如果发生异常,则被称为引发异常。然后可以捕获引发的异常,转移到try块后面合适的catch块执行。故,try块后面可使用一个或多个catch块。且,为了避免意料之外的异常,最后一个catch块使用省略号作为参数。
20.5.异常处理的基本步骤:1、确定程序中执行某种操作且可能引发异常的代码,并将它们放到try块中;2、创建catch块,在异常被引发时捕获它们,可以创建捕获特定类型的异常的catch块(通过制定catch块的类型参数),也可以创建捕获所有异常的catch块(使用省略号作为参数)
注:有些古老的编译器不支持异常,然而异常时ANSI C++标准的一部分,最新的编译器版本都会全面支持异常。
程序清单 20.2 捕获异常
#include
using std::endl;
using std::cout;
const int DefaultSize = 10;
int main()
{
int top = 90;
int bottom = 0;
try
{
cout << "top/2 = " << (top / 2) << endl;
cout << "top divided by bottom = ";
cout << (top / bottom) << endl;
cout << "top / 3 = " << (top / 3) << endl;
}
catch (...)
{
cout << "something has gone wrong!" << endl;
}
cout << "Done." << endl;
return 0;
}
20.6.手工引发异常:异常处理存在三方面:1、标记要监控的代码;2、指定要如何处理异常;3、自定义异常处理程序(catch块)。要创建导致try对其作出反应的异常,可使用关键字throw。实际上,引发异常,处理程序(catch块)可能捕获它。throw语句的基本格式为:throw exception;该语句引发异常exception。这将导致程序跳到一个处理程序处执行,如果没有找到匹配的处理程序,程序将终止。引发异常时,可以使用任何类型的值。
程序清单 20.3 引发异常
#include
using namespace std;
const int DefaultSize = 10;
int main()
{
int top = 90;
int bottom = 0;
try
{
cout << "top/2 = " << (top / 2) << endl;
cout << "top divided by bottom = ";
if (bottom == 0)
throw "Division by zero!";
cout << (top / bottom) << endl;
cout << "top / 3 = " << (top / 3) << endl;
}
catch (const char * ex)
{
cout << " *** " << ex << " *** " << endl;
}
cout << "Done." << endl;
return 0;
}
注:如果在函数中引发异常,而该函数被其他函数调用时,可以将异常向上传递,要将异常向上传递,只需调用throw命令且不用提供任何参数,这将在当前位置重新引发现有的异常。
20.7.创建异常类:可以创建更复杂的类,供引发异常时使用,
程序清单 20.4. 引发异常
#include
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
const int DefaultSize = 10;
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array &);
int & operator[] (int offset);
const int & operator[] (int offset) const;
int GetitsSize() const { return itsSize; }
friend ostream & operator<< (ostream &, const Array &);
class xBoundary { };
private:
int * pType;
int itsSize;
};
Array::Array(int size) : itsSize(size)
{
pType = new int[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
Array & Array::operator=(const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetitsSize();
pType = new int[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
Array::Array(const Array & rhs)
{
itsSize = rhs.GetitsSize();
pType = new int[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
int & Array::operator[] (int offset)
{
int size = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
const int & Array::operator[] (int offset) const
{
int mysize = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
ostream & operator << (ostream & output, const Array & theArray)
{
for (int i = 0; i < theArray.GetitsSize(); i++)
output << "[" << i << "]" << theArray[i] << endl;
return output;
}
int main()
{
Array intArray[20];
try
{
for (int j = 0; j < 100; j++)
{
intArray[j] = j;
cout << "intArray[" << j << "] okay..." << endl;
}
}
catch (Array::xBoundary)
{
cout << "Unable to process your input!" << endl;
}
cout << "Done." << endl;
return 0;
}
20.8.使用try块和catch块:确定在什么地方放try块比较困难:可能引发异常的操作并非总是那么明显,下一个问题是什么地方捕获异常。在确定try块的位置时,考虑在何处分配内存或资源。要监视的其他异常包括越过边界,无效输入等。至少应该在main()函数中所有代码的周围放置try/catch块。try/catch块通常放在高级函数中,尤其是那些知道程序用户界面中。
20.9.捕获异常的工作原理:捕获异常的工作原理如下:异常被引发后,将检查调用栈。调用栈时在程序的一部分调用另一个函数时创建额函数调用表。调用栈记录执行路径。异常沿调用向上传给每个封闭块(enclosing block),这被称为堆栈解退(unwinding the stack)。当堆栈解退时,将对堆栈中的局部对象调用析构函数,将对象销毁。每个try块后都有一个或多个catch语句,如果异常与某个catch语句匹配,将执行该catch语句并认为已得到处理。如果没有匹配的catch语句,将继续解退堆栈。如果异常知道程序开始位置还没有被捕获,将调用内置的处理程序来终止程序。需要注意的是,异常沿堆栈向上传递时单行线,在异常向上传递的过程中,堆栈被退解,堆栈中的对象被销毁。没有回头路可走,异常被捕获后,程序将继续执行捕获异常后的catch语句后面的语句。
20.10.使用多条catch语句:可能引发多种异常,在这种情况下,可以依次使用多条catch语句,就像switch语句中的条件一样。
程序清单 20.5. 多种异常
#include
using namespace std;
const int DefaultSize = 10;
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array &);
int & operator[] (int offset);
const int & operator[] (int offset) const;
int GetitsSize() const { return itsSize; }
friend ostream & operator<< (ostream &, const Array &);
class xBoundary { };
class xTooBig { };
class xTooSmall { };
class xZero { };
class xNegative { };
private:
int * pType;
int itsSize;
};
Array::Array(int size) : itsSize(size)
{
if (size == 0)
throw xZero();
if (size < 10)
throw xTooSmall();
if (size > 30000)
throw xTooBig();
if (size < 1)
throw xNegative();
pType = new int[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
Array & Array::operator=(const Array & rhs)
{
if (this == &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetitsSize();
pType = new int[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
return *this;
}
Array::Array(const Array & rhs)
{
itsSize = rhs.GetitsSize();
pType = new int[itsSize];
for (int i = 0; i < itsSize; i++)
pType[i] = rhs[i];
}
int & Array::operator[] (int offset)
{
int size = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
const int & Array::operator[] (int offset) const
{
int mysize = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
ostream & operator << (ostream & output, const Array & theArray)
{
for (int i = 0; i < theArray.GetitsSize(); i++)
output << "[" << i << "]" << theArray[i] << endl;
return output;
}
int main()
{
try
{
Array intArray(0);
for (int j = 0; j < 100; j++)
{
intArray[j] = j;
cout << "intArray[" << j << "]okay..." << endl;
}
}
catch (Array::xBoundary)
{
cout << "Unable to process your input!" << endl;
}
catch (Array::xTooBig)
{
cout << "This array is too big..." << endl;
}
catch (Array::xTooSmall)
{
cout << "This array is too small..." << endl;
}
catch (Array::xZero)
{
cout << "You asked of an array";
cout << " of zero objects!" << endl;
}
catch (...)
{
cout << "Something went wrong!" << endl;
}
cout << "Done." << endl;
return 0;
}
注:构造函数被调用后,便为对象分配了内存。因此,在构造函数中引发异常可能为对象分配了内存,但该对象不可用,通常应该将构造函数放在try/catch块中,并在发生异常时将对象标记为不可用。每个成员函数都应该检查“有效”标记“,以避免使用初始化被中断的对象进而导致其他错误。
20.11.异常层次结构:异常是类,因此也可以从它们派生出其他类,可以创建一个异常基类,然后从其中派生出其他派生异常类。
程序清单 20.6 类层次结构和异常
#include
using namespace std;
const int DefaultSize = 10;
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array);
int & operator[] (int offset);
const int & operator[] (int offset) const;
int GetitsSize() const { return itsSize; }
friend ostream & operator<< (ostream &, const Array &);
class xBoundary { };
class xSize { };
class xTooBig : public xSize { };
class xTooSmall : public xSize { };
class xZero : public xSize { };
class xNegative : public xSize { };
private:
int * pType;
int itsSize;
};
Array::Array(int size) : itsSize(size)
{
if (size == 0)
throw xZero();
if (size > 30000)
throw xTooBig();
if (size < 1)
throw xNegative();
if (size < 10)
throw xTooSmall();
pType = new int[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
int & Array::operator[] (int offset)
{
int size = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
const int & Array::operator[] (int offset) const
{
int mySize = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
int main()
{
try
{
Array intArray(0);
for (int j = 0; j < 100; j++)
{
intArray[j] = j;
cout << "intArray[" << j << "] okay..." << endl;
}
}
catch (Array::xBoundary)
{
cout << "Unable to process your input!" << endl;
}
catch (Array::xTooBig)
{
cout << "This array is too big..." << endl;
}
catch (Array::xTooSmall)
{
cout << "This array is too small..." << endl;
}
catch (Array::xZero)
{
cout << "You asked for an array of zero objects!" << endl;
}
catch (...)
{
cout << "Something went wrong!" << endl;
}
cout << "Done." << endl;
return 0;
}
20.12.异常中的数据及给异常对象命名:对被引发的异常,通常需要知道除其类型之外的其他信息,以便能够正确地应对错误。和其它类一样,异常类也可以包含数据成员、在构造函数中初始化数据成员、在任何时候读取数据成员。
程序清单 20.7 读取异常对象中数据
#include
using namespace std;
const int DefaultSize = 10;
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array);
int & operator[] (int offset);
const int & operator[] (int offset) const;
int GetitsSize() const { return itsSize; }
friend ostream & operator<< (ostream &, const Array &);
class xBoundary { };
class xSize
{
public:
xSize(int size) : itsSize(size) { }
~xSize() { }
int GetSize() { return itsSize; }
private:
int itsSize;
};
class xTooBig : public xSize
{
public:
xTooBig(int size ) : xSize(size) { }
};
class xTooSmall : public xSize
{
public:
xTooSmall(int size) : xSize(size) { }
};
class xZero : public xTooSmall
{
public:
xZero(int size) : xTooSmall(size) { }
};
class xNegative : public xSize
{
public:
xNegative(int size) : xSize(size) { }
};
private:
int * pType;
int itsSize;
};
Array::Array(int size) : itsSize(size)
{
if (size == 0)
throw xZero(size);
if (size > 30000)
throw xTooBig(size);
if (size < 1)
throw xNegative(size);
if (size < 10)
throw xTooSmall(size);
pType = new int[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
int & Array::operator[] (int offset)
{
int size = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
const int & Array::operator[] (int offset) const
{
int mySize = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
int main()
{
try
{
Array intArray(9);
for (int j = 0; j < 100; j++)
{
intArray[j] = j;
cout << "intArray[" << j << "] okay..." << endl;
}
}
catch (Array::xBoundary)//标记头
{
cout << "Unable to process your input!" << endl;
}
catch (Array::xTooBig theException)
{
cout << "This array is too big..." << endl;
cout << "Received " << theException.GetSize() << endl;
}
catch (Array::xTooSmall theException)
{
cout << "This array is too small..." << endl;
cout << "Received " << theException.GetSize() << endl;
}
catch (Array::xZero theException)
{
cout << "You asked for an array of zero objects!" << endl;
cout << "Received " << theException.GetSize() << endl;
}
catch (...)//标记尾
{
cout << "Something went wrong,but I've no idea what!" << endl;
}
cout << "Done." << endl;
return 0;
}
注:从标记头到标记尾的catch语句将它们捕获的异常命名为theException,并使用这个对象来访问存储在itsSize中的数据。在发生异常(出现某种错误)后,如果要创建异常对象,应避免在创建它时引发同样的问题。
20.13.在异常中按引用传递:让每条catch语句分别打印相应的消息很繁琐,也容易出错,这种工作应该由对象来完成,它知道自己的类型和存储的值。可使用虚函数让每个异常都能做对应的输出。
程序清单 20.8 按引用传递及在异常中使用虚函数
#include
using namespace std;
const int DefaultSize = 10;
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array);
int & operator[] (int offset);
const int & operator[] (int offset) const;
int GetitsSize() const { return itsSize; }
friend ostream & operator<< (ostream &, const Array &);
class xBoundary { };
class xSize
{
public:
xSize(int size) : itsSize(size) { }
~xSize() { }
int GetSize() { return itsSize; }
virtual void PrintError()
{
cout << "Size error.Received: ";
cout << itsSize << endl;
}
protected:
int itsSize;
};
class xTooBig : public xSize
{
public:
xTooBig(int size ) : xSize(size) { }
virtual void PrintError()
{
cout << "Too big.Received: ";
cout << xSize::itsSize << endl;
}
};
class xTooSmall : public xSize
{
public:
xTooSmall(int size) : xSize(size) { }
virtual void PrintError()
{
cout << "Too small.Received: ";
cout << xSize::itsSize << endl;
}
};
class xZero : public xTooSmall
{
public:
xZero(int size) : xTooSmall(size) { }
virtual void PrintError()
{
cout << "Zero!Received: ";
cout << xSize::itsSize << endl;
}
};
class xNegative : public xSize
{
public:
xNegative(int size) : xSize(size) { }
virtual void PrintError()
{
cout << "Negative!Received: ";
cout << xSize::itsSize << endl;
}
};
private:
int * pType;
int itsSize;
};
Array::Array(int size) : itsSize(size)
{
if (size == 0)
throw xZero(size);
if (size > 30000)
throw xTooBig(size);
if (size < 1)
throw xNegative(size);
if (size < 10)
throw xTooSmall(size);
pType = new int[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
int & Array::operator[] (int offset)
{
int size = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
const int & Array::operator[] (int offset) const
{
int mySize = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
int main()
{
try
{
Array intArray(9);
for (int j = 0; j < 100; j++)
{
intArray[j] = j;
cout << "intArray[" << j << "] okay..." << endl;
}
}
catch (Array::xBoundary)
{
cout << "Unable to process your input!" << endl;
}
catch (Array::xSize & theException)
{
theException.PrintError();
}
catch (...)
{
cout << "Something went wrong,but I've no idea what!" << endl;
}
cout << "Done." << endl;
return 0;
}
20.14.异常和模板:要在模板中使用异常,可以为模板的每个实例创建一个异常类,也可以使用在模板外声明异常类。
程序清单 20.9 在模板中使用异常
#include
using namespace std;
const int DefaultSize = 10;
class xBoundary { };
template
class Array
{
public:
Array(int itsSize = DefaultSize);
Array(const Array & rhs);
~Array() { delete[] pType; }
Array & operator= (const Array &);
T & operator[] (int offset);
const T & operator[] (int offset) const;
int GetitsSize() const { return itsSize; }
friend ostream & operator<< (ostream &, const Array &);
class xSize { };
private:
int * pType;
int itsSize;
};
template
Array::Array(int size) : itsSize(size)
{
if (size < 10 || size > 30000)
throw xSize();
pType = new T[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
template
Array & Array::operator = (const Array & rhs)
{
if (this = &rhs)
return *this;
delete[] pType;
itsSize = rhs.GetitsSize();
pType = new T[itsSize];
for (int i = 0; i < size; i++)
pType[i] = rhs[i];
}
template
Array::Array(const Array & rhs)
{
itsSize = rhs.GetitsSize();
pType = new T[itsSize];
for (int i = 0; i < size; i++)
pType[i] = rhs[i];
}
template
T & Array::operator[] (int offset)
{
int size = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
template
const T & Array::operator[] (int offset) const
{
int mySize = GetitsSize();
if (offset >= 0 && offset < GetitsSize())
return pType[offset];
throw xBoundary();
return pType[0];
}
template
ostream & operator<<(iostream * output, const Array & theArray)
{
for (int i = 0; i < theArray.GetitsSize(); i++)
output << "[" << i << "]" << theArray[i] << endl;
return output;
}
int main()
{
try
{
Array intArray(9);
for (int j = 0; j < 100; j++)
{
intArray[j] = j;
cout << "intArray[" << j << "] okay..." << endl;
}
}
catch (xBoundary)
{
cout << "Unable to process your input!" << endl;
}
catch (Array::xSize)
{
cout << "Bad Size!" << endl;
}
cout << "Done." << endl;
return 0;
}
20.15.没有错误的异常:一部分程序员认为异常只应用于那些可预见的、程序员必须预先考虑的异常情形,而不是代码中正常处理的部分;另一部分程序员认为:异常提供了强大而清晰的返回方式,可跨越多层函数而没有内存泄漏的危险。没有准确的结论,确定异常的使用情形,只有在合适的情况下使用合适的方式才是最佳的。
20.16.关于代码蜕变:代码蜕变(Code Rot)是一种众所周知的现象,指的是软件由于缺乏维护而恶化。编写的优秀、经过充分调试的程序在交付的几星期后,在用户的系统上变坏了。除将源代码放在密闭的容器中之外外,唯一可采取的防护措施是,编写程序时确保以后修复时能够快速、轻松地找到问题的所在。这意味着代码必须要易于理解,对微妙的地方需要进行注释。
20.17.bug和调试:几乎所有的现代开发环境都包括一个或多个功能强大的调试器。使用调试器的基本思想是:首先运行调试器,它加载源代码,然后在调试器中运行程序,这样能够看到每条指令的执行情况以及检查变量在程序执行期间的变化情况。所有的编译器都允许使用或不使用符号(symbol)进行编译,使用符号的编译命令编译器在源代码和生成的程序之间建立必要的映射关系;调试器根据映射关系指出程序的下一个操作的对应的源代码行。全屏幕符号调试器使这项繁琐的工作变得便利,加载源代码时,它将读取所有的源代码,并将其显示在窗口,可以将函数调用作为一步执行,也可以命令调试器进入函数,逐行执行其中的源代码。在大多数调试器中,可以在源代码和输出之间切换,查看每条语句的执行结果,更为强大的功能是:可以查看变量的当前状态,查看复杂的数据结构,查看类成员数据的值,查看各种指针指向内存和其他内存单元中的实际值。可以在调试器中使用多种控制关系,包括设置断点、设置监视点、查看内存和汇编代码。
20.17.1.断点:断点命令调试器在达到某行代码后暂停执行程序,折让程序一直运行到被怀疑的代码行。断点可帮助分析关键代码执行前后、变量的变化。
20.17.2.监视点:可让调试器显示某个变量的值或某个变量被读写时暂停程序执行。监视点可以设置这些条件,设置在程序运行时修改变量的值。
20.17.3.查看内存:现代调试器可以用实际变量的格式显示值,即将字符串显示为字符、长整型显示为数字而是不会4个字节的内容等。高级C++调试器甚至可以显示整个对象,提供包括this指针在内的所有成员变量的当前值。
20.17.4.查看汇编代码:当诸多手段都无法修复bug时,可以命令调试器显示根据每行源代码生成的汇编代码。可以查看内存寄存器和标记,并在必要时深入研究程序的内部工作原理。
第二十一部分 杂项内容
21.1.预处理器和编译器:每次运行编译器时,预处理器都将首先运行。预处理器查找预处理器指令,每条预处理器指令都以#打头。这些指令的作用是修改源代码的文本。结果为一个新的源代码文件:一个通常看不到的临时文件,但可以命令编译器保存它,以便在需要时查看。编译器不是读取原始的源代码文件,而是读取预处理器的输出并对其进行编译,#include命令预处理器查找其名称位于#include后面的文件,并将其写入到中间文件这个位置,编译器看到源代码时,包含的文件内容已经在源代码中了。注:几乎每个编译器都有一个可在集成开发环境(IDE)或命令行中设置的开关,用于指示编译器保存中间文件,如果要查看中间文件,可参阅编译器手册了解需要为编译器设置哪个开关。
21.2.预处理器指令#define:可使用命令#define来定义字符串替换。例:#define BIG 520 遇到这行代码后,编译器就将所有的BIG替换为520。这不是C++意义上的字符串。在中间文件中,预编译语句都将被删除,它们根本不会出现在最终的源代码中。注意,其后是不能添加分号的。
21.2.1.使用#define来定义常量:#define的用途之一是定义常量,然而,由于#define只是进行字符串替换而不做类型检查,因此这很危险。
21.2.2.将#define用于检测:#define的另一种用途是,指出某个字符串定义过,例:#define DEBUG 然后在程序中检测DEBUG是否被定义,并采取相应的措施。要检查字符串是否被定义过,可使用预处理命令#If和命令defined;即:#if defined DEBUG cout <<”Debug defined";#endif 如果检测的字符串已定义过,则defined表达式的结果为true。注:这发生在预处理器中,而不是编译器中或执行程序时。预处理器遇到#defined时,将检查一个已经创建的表,看#if defined后面的值是否定义过,如果定义了,则defined表达式的结果为true,#if defined DEBUG和#endif之间的所有内容都将被写入中间文件进行编译。如果为false,#if definde DEBUG和#endif之间的所有内容都不会写入中间文件。还有一个简化的编译指令可用于检测某个值是否被定义,这既是#ifdef:#ifdef DEBUG cout<<"Debug defined"; #endif.还可以检测某个是否未被定义,为此可将运算符!用于编译指令defined:#if !defined DEBUG cout<<"Debug is not defined"; #endif 也可以使用其简化版本#ifndef:#ifndef DEBUG cout<<"Debug is not define."; #endif。注:#ifndef和#ifdef时逻辑逆,如果在此之前没有定义被检测的字符串,则#ifndef的结果为true。这些检测都需要使用#endif来指定受监测影响的代码到什么地方结束。
21.2.3.预编译器命令#else:可在#ifdef(或#ifndef)和#endif之间使用编译指令#else。
程序清单 21.1. 使用#define
#define DemoVersion
#define SW_VERSION 5
#include
using std::endl;
using std::cout;
int main()
{
cout << "Checking on the definitions of DemoVersion,";
cout << "SW_VERSION,and WINDOWS_VERSION..." << endl;
#ifdef DemoVersion
cout << "DemoVersion defined." << endl;
#else
cout << "DemoVersion not defined." << endl;
#endif
#ifndef SW_VERSION
cout << "SW_VERSION not defined!" << endl;
#else
cout << "SW_VERSION defined as: "
<< SW_VERSION << endl;
#endif
#ifdef WINDOWS_VERSION
cout << "WINDOWS_VERSION defined!" << endl;
#else
cout << "WINDOWS_VERSION was not defined." << endl;
#endif
cout << "Done." << endl;
return 0;
}
21.3.包含和多重包含防范:创建项目时将使用很多不同的文件。可能需要组织目录,使每个类都有包含类声明的头文件和包含类方法源代码的实现文件。main()函数保存在一个独立的.cpp文件中,所有.cpp文件都被编译到.obj文件,然后由链接器将它们链接成一个程序。由于程序使用了很多类的方法,因此很多头文件都将包含到每个文件中,另外,头文件经常需要包含其他头文件。为了解决这种问题,可使用多重包含防范(inclusion guard)。即:添加#ifndef #define .... #endif 其中,在#define语句和#endif语句之间是整个文件的内容。注:使用多重包含防范不会有任何害处,使用它常常可以节省大量时间。
21.4.宏:编译指令#define也可以用于创建宏。宏是使用#define创建的符号,它像函数那样能够接受参数。预处理器将用指定的参数值替换宏中的替换字符串。注:在宏的定义中,参数列表的左括号必须紧跟在宏名后面,中间不能有空格。预处理器不像编译器那样允许使用空格。如果有空格,则使用标准替换。且为了避免复杂的值传递给宏,需要使用多个括号,可避免副作用。
程序清单 21.2 在宏中使用括号
#include
using namespace std;
#define CUBE(a) ((a)*(a)*(a))
#define THREE(a) a * a * a
int main()
{
long x = 5;
long y = CUBE(x);
long z = THREE(x);
cout << "y: " << y << endl;
cout << "z:" << z << endl;
long a = 5, b = 7;
y = CUBE(a + b);
z = THREE(a + b);
cout << "y: " << y << endl;
cout << "z:" << z << endl;
return 0;
}
21.5.字符串操纵:预处理器提供了两个特殊的运算符,用于在宏中操纵字符串。字符串化运算符(#)将其后面的内容转换为用引号括起的字符串:拼接运算符将两个字符串合并成一个。
21.5.1.字符串化:字符串化运算符将其后面到以一个空格为止的所有字符用引号括起。例:#define WRITESTRING(x) cout << #x 然后这样调用它WRITESTRING (This is a srting); 预编译器将它转换为:cout<<"This is a stringg";
21.5.2.拼接:拼接运算符让你能够将多个单词合并为一个新词。该新词实际是一个符号,可用作类名、变量名、数组下标或出现任任何使用字符串的地方。例:假设有五个函数,分别名为:fOnePrint、fTwoPrint、fThreePrint、fFourPrint和fFivePrint。可以这样声明一个宏:#define fPRINT(x) f ## x ## Print。然后使用fPRINTF(Two)来生成fTwoPrintf。也可以在类声明是使用宏和拼接运算符。
例如:
#define Listof(Type) class Type##List \
{\
public:\
Type##List() { }\
private:\
int itsLength;\
};
注:代码中插入换行不会影响含义,而在宏中也是可以加入回车,但是必须在会车前奖赏反斜杠\才可以。
21.5.预定义的宏:很多编译器预定义了大量有用的宏,其中包括_DATE_、_TIME_、_LINE_和_FILE_。每个宏名都以两个下划线字符开头和结尾,以降低这些宏名与程序员在程序中使用的名称发生冲突的可能性。预编译器看到这些宏时,将执行合适的替换。对于_DATE_,替换为当前日期;_TIME_,替换为当前时间;_LINE_和_FILE_,分别替换为源代码行数和文件名。需要注意的是,这些替换是在预编译源代码时进行的,而不是在程序运行时进行的。如果要打印_DATE_,打印的将不是当前日期,而是程序被编译时的日期,这些预定义的宏在调试中是非常有用的。
21.6.assert()宏:很多编译器都提供了一个assert()宏。参数的值为true时,assert()返回true,否则执行某种操作,很多编译器在assert()的参数值为false时中止程序,其他编译器则引发异常。assert()宏用于在发布前对其进行调试。事实上,如果DEBUG没有定义,预处理器将简化assert(),使得其中的任何代码都不会出现在编译器生成的源代码中,这在开发过程中很有用,发布最终产品时,assert()不会影响性能,也不会增加程序可执行版本的大小。也可以使用编译器提供的assert(),而编写自己的assert()宏。
程序清单 21.3 一个简单的assert()宏
#define DEBUG
#include
using namespace std;
#ifndef DEBUG
#define ASSERT(x)
#else
#define ASSERT(x)\
if(!x)\
{\
cout << "ERROR!!Assert "<< #x <<" failed "<
21.7.使用assert()进行调试:编写程序时,通常知道某些事情是真的:函数有特定的值、指针时有效的等。Bug的本质是,在某些情况下为真的事情却是假的。assert()可帮助发现这类bug,但仅当在大量代码中使用assert()的习惯。例如当给指针赋值、将其作为函数参数或返回值时,务必确定指针是有效的,每当代码依赖某个变量包含的特定值时,都应使用assert()来确认这一点,频繁使用assert()没有坏处。解除DEBUG的定以后,assert()将冲代码中删除。它还提供了优秀的内部文档,提醒阅读者在程序执行的某个时刻,是真的事实。
21.8.assert()与异常之比较:assert()并非为处理运行阶段的错误状态而设计的,而是为了捕获编程错误而设计的,也就是说,当assert()生效了,那么说明代码中有bug。一种常见的错误是,使用assert()来测试内存分配的返回值,即:int * p = new int;assert(p);这是一种经典的编程错误:每当程序测试运行时,有足够的内存可用,assert()不会“开火”,但是到用户使用时,可能机器配置相对较低,然后运行到此处时,将可能引发程序崩溃。
21.9.副作用:仅当assert()实例被删除后,才出现的bug很常见,这几乎总是由于程序无意中依赖了assert()和其他调试代码的副作用引起的。
21.10.类的不变量:大多数类都有一些条件,每当执行完成员函数后,这些条件都将满足,这些类不变量时类的要素。声明这样的一个Invariants()方法可能会很有帮助:它仅在所有这些条件都为true时才返回true。这样,可以在调用每个类方法之前和之后使用ASSERT(Invariants())。例外情况是,在构造函数执行前和析构函数结束后,Invariants()将不会返回true。
程序清单 21.4 使用Invariants()
#define DEBUG
#define SHOW_INVARINATS
#include
#include
using namespace std;
#ifndef DEBUG
#define ASSERT(x)
#else
#define ASSERT(x)\
if(!x)\
{\
cout << "ERROR!!Assert "<< #x <<" failed "< itsLen)
{
ASSERT(Invariants());
return itsString[itsLen - 1];
}
else
{
ASSERT(Invariants());
return itsString[offset];
}
}
char String::operator[] (int offset) const
{
ASSERT(Invariants());
char retVal;
if (offset > itsLen)
retVal = itsString[itsLen - 1];
else
retVal = itsString[offset];
ASSERT(Invariants());
return retVal;
}
BOOL String::Invariants() const
{
#ifdef SHOW_INVARINATS
cout << "String Tested OK" << endl;
#endif
return ((itsLen && itsString) || (!itsLen && !itsString));
}
class Animal
{
public:
Animal() : itsAge(1), itsName("John Q.Animal")
{ ASSERT(Invariants()); }
Animal(int, const String &);
~Animal() { }
int GetAge() { ASSERT(Invariants()); return itsAge; }
void SetAge(int age)
{
ASSERT(Invariants());
itsAge = age;
ASSERT(Invariants());
}
String & GetName()
{
ASSERT(Invariants());
return itsName;
}
void SetName(const String & name)
{
ASSERT(Invariants());
itsName = name;
ASSERT(Invariants());
}
BOOL Invariants();
private:
int itsAge;
String itsName;
};
Animal::Animal(int age, const String & name) : itsAge(age), itsName(name)
{
ASSERT(Invariants());
}
BOOL Animal::Invariants()
{
#ifdef SHOW_INVARINATS
cout << "Animal Texted OK" << endl;
#endif
return (itsAge > 0 && itsName.GetLen());
}
int main()
{
Animal sparky(5, "Sparky");
cout << endl << sparky.GetName().GetString() << " is ";
cout << sparky.GetAge() << " years old.";
sparky.SetAge(8);
cout << endl << sparky.GetName().GetString() << " is ";
cout << sparky.GetAge() << " years old.";
return 0;
}
21.11.打印中间值:除使用ASSERT()宏确认真实为真外,还可能想打印指针、变量和字符串的当前值,这对于检验有关程序进程的假定以及确定bug在循环中的位置都很有帮助。
程序清单 21.5 在调试模式下打印值
#include
using namespace std;
#define DEBUG
#ifndef DEBUG
#define PRINT(x)
#else
#define PRINT(x)\
cout <<#x<<":\t"<
21.12.宏与函数及模板之比较:在C++中,宏存在4个问题:1、如果宏很大,会让人很难理解;2、宏在每次被使用时都按内联方式展开(当然,它们的速度比函数快得多);3、宏不能出现在编译器使用的中间源代码中,因此在大多调试器中无法看到宏,导致调试困难;4、最大的问题是宏不是类型安全的。这破坏了C++的强类型功能,因此被C++程序员视为瘟疫。当然,解决问题的方法时使用模板。
21.13.内联函数:通常可以不使用宏,而声明一个内联函数。
程序清单 21.6 使用内联函数而不是宏
#include
using namespace std;
inline unsigned long Square(unsigned long a) { return a * a; }
inline unsigned long Cube(unsigned long a) { return a * a * a; }
int main()
{
unsigned long x = 1;
for (;;)
{
cout << "Enter a number (0 to quit): ";
cin >> x;
if (x == 0)
break;
cout << "You entered: " << x;
cout << ". Square(" << x << "):";
cout << Square(x);
cout << " . Cube(" << x << "):";
cout << Cube(x) << "." << endl;
}
}
注:宏名应该大写,这是一种约定,如果不这样做,会让其他程序员感到迷惑;且应用括号将宏中每个参数括起;不要在宏中递增变量或给变量赋值;在使用const可行的情况下,不要使用#define来定义常量。
21.14.位运算:经常需要在对象中设置标记以跟踪对象的状态,为此,可使用用户定义的布尔变量,但有些应用程序(尤其是低级驱动程序)和硬件设备要求你能够将变量的位用作标记。每个字节包含8位,因此4字节的long变量能够存储32个标记。位的值为1时称为被设置,为0时称为被清除。设置位时使其值为1;清除位时使其值为0。C++提供的位运算符可以对变量的各个位进行操作。这些运算符看起来很像逻辑运算符,但实际上并不同。
21.14.1.“与”运算符:符号为&,对两个位进行“与”运算,如果他们都是1,则结果为1,如果至少有一个为0,则结果为0。
21.14.2.“或”运算符:符号为|,对两个位进行“或”运算,如果他们都是0,则结果为0,如果至少有一个为1,则结果为1.
21.14.3.“异或”运算符:符号为^,对两个位进行“异或”运算式,如果两个位的值相同,则结果为0;
21.14.4.“求补”运算符:符号为~,清除被设置的位并设置被清除的位。
21.15.设置位:要设置或清除某个位。可使用掩码运算。而设置位使用的是“或运算”来设置位,清除位使用的是“与”运算来清除位。反转位使用的是“异或”运算。
21.16.位字段:在有些情况下,每个字节的空间都很宝贵,如果类或结构中有一系列布尔变量或只有少数几个可能取值的变量,可以使用位字段来节省空间。在类中可以使用的最小标准C++数据类型是char,它可能只占用1个字节,通过使用位字段,可在插入变量中存储8个二进制值。位字段的命名和访问方式和其他类成员相同。它们的类型总是unsigned int,并在位字段名后加上冒号和数字。这个数字告诉编译器,为该变量分配多少位内存。
程序清单 21.7 使用位字段
#include
#include
using namespace std;
enum STAUS { FullTime, PartTime };
enum GRADLEVEL { UnderGrad, Grad };
enum HOUSING { Dorm, offCampus };
enum FOODPLAN { OneMeal, AllMeals, WeekEnds, NoMeals };
class student
{
public:
student() :
myStatus(FullTime),
myGradLevel(UnderGrad),
myHousing(Dorm),
myFoodPlan(NoMeals)
{ }
~student() { }
STAUS GetStatus();
void SetStatus(STAUS);
unsigned GetPlan() { return myFoodPlan; }
private:
unsigned myStatus : 1;
unsigned myGradLevel : 1;
unsigned myHousing : 1;
unsigned myFoodPlan : 2;
};
STAUS student::GetStatus()
{
if (myStatus)
return FullTime;
else
return PartTime;
}
void student::SetStatus(STAUS theStatus)
{
myStatus = theStatus;
}
int main()
{
student Jim;
if (Jim.GetStatus() == PartTime)
cout << "Jim is part-time" << endl;
else
cout << "Jim is full-time" << endl;
Jim.SetStatus(PartTime);
if (Jim.GetStatus() == PartTime)
cout << "Jim is part-time" << endl;
else
cout << "Jim is full-time" << endl;
cout << "Jim is on the ";
char Plan[80];
switch (Jim.GetPlan())
{
case OneMeal:
strcpy_s(Plan, "One meal");
break;
case AllMeals:
strcpy_s(Plan, "All meal");
break;
case WeekEnds:
strcpy_s(Plan, "Weekend meal");
break;
case NoMeals:
strcpy_s(Plan, "No Meals");
break;
default:
cout << "Something bad went wrond!" << endl;
break;
}
cout << Plan << " food plan." << endl;
return 0;
}
注:使用位字段的最重要一点是:类的客户无需关心数据存储实现,由于位字段被声明为私有的,因此以后修改他们,而无需修改接口。
21.17.编程风格:采用一致的编程风格很重要。
21.17.1.缩进:如果使用制表符,那应该为3个空格,确保编译器将制表符转换为3个空格
21.17.2.大括号:匹配的大括号应水平对齐,定义和声明中最外面的大括号在最左边,内部语句应该缩进;其他所有大括号与相关联的命令左对齐;大括号应单独占一行。
21.17.3.长代码行和函数长度:确保不用水平滚动就能看到整行代码,超出右边界的代码易被忽略,且水平滚动很烦人;将一行代码分成多行;对后续进行缩进;尽量在合理的地方分行,将插入运算符放在前一行的末尾(而不是下一行的开头),这样可以清楚的知道,该行不是完整的,后面有其他代码;在C++中,函数通常比C语言短的多,尽量使函数足够短,以便可以在一页打印函数的全部代码;
21.17.4.格式化switch语句:对case分支进行适当的缩进,更加利于阅读;
21.17.5.程序文本:1、使用空格提高可读性;2、不要在对象、数组名和运算符(.、->、[ ])之间使用空格;3、单目运算符与其操作数相关联,因此不要在它们之间添加空格,但在操作数的另一边添加空格;4、数目运算符两边应该都有空格;5、在逗号和分号后面加上空格,但在它们前面不加;6、圆括号两边不应有空格;7、用空格将关键字隔开;8、使用空格将单行注释的内容同//分开;9、将指针或引用紧靠类型名而不是变量名;
21.17.6.表示命名符:1、标识符名称应足够长以便具有描述性;2、避免意义不明确的缩写;3、花时间和精力将含义写出来;4、不使用匈牙利表示法(例外的情况是在指针前加p,在引用前加r,在类前加its);5、仅当简短性可提高代码的可读性或用途十分明显而不需要描述性名称时,才使用短名称;6、变量名的长度应与其作用域相称;7、确保标识符看上去和听上去都不同,尽量减少混淆;8、函数(或方法)名通常为动词或动词-名词短语;变量名通常为抽象名词,可以带一个附加名词。
21.17.7.名称的拼写和大写:1、对于使用#define定义的常量,采用全部大写并用下划线将其中的单词分开;2、其他标识符应采用大小写混合、没有下划线。函数、方法、类、typedef和结构的名称应采用首字母大写;3、枚举常量应以表示枚举类型缩写的小写字母开头;
21.17.8.注释:注释可使程序更容易理解,技巧为:1、尽可能使用C++单行注释,而不是多行注释。将多行注释用于可能包含单行注释的代码块注释掉;2、高级注释比处理细节更重要。要增加价值而不复述代码。将重点放在函数和代码块的语义上:指出函数的功能、副作用、参数类型和返回值、描述作出的(或没有作出的)假设。在复杂的逻辑中,指出当前的状态;3、使用采用合适标点符号和大小写的完整的句子,注释不要过于晦涩,也不要使用缩写;4、使用空行来帮助理解所发生的事情,将语句分成逻辑组;
21.17.9.设置访问权限:访问程序各部分的方式应保持一致。设置访问权限的技巧:1、总是使用publice、private和protected不依赖默认访问设置;2、先列出公有成员、其次是保护成员、然后是私有成员,在方法后面列出数据成员;3、在各部分首先列出构造函数然后是析构函数。集中列出同名的重载方法,竟可能集中列出存取器函数。4、考虑按字母顺序排列每组中的方法和成员变量、包含文件时,按字母顺序排列它们;4、虽然覆盖函数时关键字virtual时可选的,但应尽可能使用它,它有助于提醒函数时虚函数以及保持声明的一致性
21.17.10.类定义:尽可能确保方法实现排序与声明顺序一致,这可时方法更容易找到;定义函数时,将返回值类型和其他所有限定符都放在前一行,让类名和函数名位于行首,这样更容易找到函数。
21.17.11.包含文件:尽可能少用#inlcude,最大限度地减少头文件中包含的文件,理想的情况是,只包含基类的头文件,其他必须包含的文件是声明类成员对象的头文件;不要因为.cpp文件需要包含某种文件,而在头文件中也包含该文件,不要因为被包含的文件需要包含某个文件而再包含该文件。所有头文件都应使用多重包含防范。
21.17.12.使用assert():尽可能使用assert(),它可棒了解程序编写者所做的假设以及他认为什么是合法的、什么是非法的。
21.17.13.使用const:在合适的地方使用const:参数、变量和方法。通常,方法需要const版本和非const版本,显示地在const和非const之间转换时,必须要小心,且要确保这类转换有意义并进行注释。
第三周复习
程序清单 R3.1 第三周复习程序清单
#include
using namespace std;
//exception classes
class Exception { };
class OutofMemory : public Exception { };
class NullNode : public Exception { };
class EmptyList : public Exception { };
class BoundsError : public Exception { };
//*******************************************************************
//Part
//Abstract base class of parts
class Part
{
public:
Part() : itsObjectNumber(1) { }
Part(int ObjectNumber) : itsObjectNumber(ObjectNumber) { }
virtual ~Part() { }
int GetObjectNumber() const { return itsObjectNumber; }
virtual void Display() const = 0;
private:
int itsObjectNumber;
};
void Part::Display() const
{
cout << endl << "Part Number: " << itsObjectNumber << endl;
}
//*******************************************************************
ostream & operator<< (ostream & theStream, Part & thePart)
{
thePart.Display();
return theStream;
}
//*******************************************************************
//Car Part
class CarPart : public Part
{
public:
CarPart() : itsModelYear(94) { }
CarPart(int year, int partNumber);
int GetModelYear() const { return itsModelYear; }
virtual void Display() const;
private:
int itsModelYear;
};
CarPart::CarPart(int year, int partNumber) : itsModelYear(year), Part(partNumber)
{ }
void CarPart::Display() const
{
Part::Display();
cout << "Model Year: " << itsModelYear << endl;
}
//*******************************************************************
//AirPlane Part
class AirPlanePart : public Part
{
public:
AirPlanePart() : itsEngineNumber(1) { }
AirPlanePart(int EngineNumber, int PartNumber);
virtual void Display() const;
int GetEngineNumber() const { return itsEngineNumber; }
private:
int itsEngineNumber;
};
AirPlanePart::AirPlanePart(int EngineNumber, int PartNUmber) :
itsEngineNumber(EngineNumber), Part(PartNUmber)
{ }
void AirPlanePart::Display() const
{
Part::Display();
cout << "Engine No." << itsEngineNumber << endl;
}
//*******************************************************************
//forward declaration of class List
template
class List;
//Node
template
class Node
{
public:
friend class List;
Node(T *);
~Node();
void SetNext(Node * node) { itsNext = node; }
Node * GetNext() const;
T * GetObject() const;
private:
T * itsObject;
Node * itsNext;
};
template
Node::Node(T * pOjbect) : itsObject(pOjbect), itsNext(0)
{ }
template
Node::~Node()
{
delete itsObject;
itsObject = 0;
delete itsNext;
itsNext = 0;
}
template
Node * Node::GetNext() const
{
return itsNext;
}
template
T * Node::GetObject() const
{
if (itsObject)
return itsObject;
else
throw NullNode();
}
//*******************************************************************
//List
template
class List
{
public:
List();
~List();
T * Find(int & position, int ObjectNumber) const;
T * GetFirst() const;
void Insert(T *);
T * operator[] (int) const;
int GetCount() const { return itsCount; }
private:
Node * pHead;
int itsCount;
};
template
List::List() : pHead(0), itsCount(0)
{ }
template
List::~List()
{
delete pHead;
}
template
T * List::GetFirst() const
{
if (pHead)
return pHead->itsObject;
else
throw EmptyList();
}
template
T * List::operator[] (int offset) const
{
Node * pNode = pHead;
if (!pHead)
throw EmptyList();
if (offset > itsCount)
throw BoundsError();
for (int i = 0; i < offset; i++)
pNode->itsNext;
return pNode->itsObject;
}
template
T * List::Find(int & position, int ObjectNumber) const
{
Node * pNode = 0;
for (pNode = pHead, position = 0; pNode != NULL; pNode = pNode->itsNext, position++)
{
if (pNode->itsObject->GetObjectNumber() == ObjectNumber)
break;
}
if (pNode == NULL)
return NULL;
else
return pNode->itsObject;
}
template
void List::Insert(T * pObject)
{
Node * pNode = new Node(pObject);
Node * pCurrent = pHead;
Node * pNext = 0;
int New = pObject->GetObjectNumber();
int Next = 0;
itsCount++;
if (!pHead)
{
pHead = pNode;
return;
}
if (pHead->itsObject->GetObjectNumber() > New)
{
pNode->itsNext = pHead;
pHead = pNode;
return;
}
for (;;)
{
if (!pCurrent->itsNext)
{
pCurrent->itsNext = pNode;
return;
}
pNext = pCurrent->itsNext;
Next = pNext->itsObject->GetObjectNumber();
if (Next > New)
{
pCurrent->itsNext = pNode;
pNode->itsNext = pNext;
return;
}
pCurrent = pNext;
}
}
//*******************************************************************
int main()
{
List theList;
int choice = 99;
int ObjectNumber;
int value;
Part * pPart;
while (choice != 0)
{
cout << "(0)Quit (1)Car (2)Plane: ";
cin >> choice;
if (choice != 0)
{
cout << "New PartNumber?: ";
cin >> ObjectNumber;
if (choice == 1)
{
cout << "Model Year?: ";
cin >> value;
try
{
pPart = new CarPart(value, ObjectNumber);
}
catch (OutofMemory)
{
cout << "Not enough memory; Exiting..." << endl;
return 1;
}
}
else
{
cout << "Engine Number?: ";
cin >> value;
try
{
pPart = new AirPlanePart(value, ObjectNumber);
}
catch (OutofMemory)
{
cout << "Not enough memory;Exiting..." << endl;
return 1;
}
}
try
{
theList.Insert(pPart);
}
catch (NullNode)
{
cout << "The list is broken, and the node is null!" << endl;
return 1;
}
catch (EmptyList)
{
cout << "The list is empty!" << endl;
return 1;
}
}
}
try
{
for (int i = 0; i < theList.GetCount(); i++)
cout << *(theList[i]);
}
catch (NullNode)
{
cout << "Thelist is broken ,and the node is null!" << endl;
return 1;
}
catch (EmptyList)
{
cout << "The list is empty!" << endl;
return 1;
}
catch (BoundsError)
{
cout << "Tried to read beyond the end of the list!" << endl;
return 1;
}
return 0;
}
注:C++根据函数参数的实际类型调用正确的函数。这就是所谓的反变性(contravariance),C++不支持。在C++中实现多态的方式只有两种;函数多态和虚函数。函数多态在这里不管用,因为在每种情形下匹配的的都是相同的特征标。虚函数在这里也不管用,因为operator<<不是Part的成员函数。
反变性:是将基类指针赋给派生类指针的能力。如果C++支持反变性,可以在运行阶段根据对象的实际类型覆盖函数。
程序清单 R3.2 反变性(该程序无法通过编译)
#include
using namespace std;
class Animal
{
public:
virtual void Speak()
{
cout << "Animal Speaks" << endl;
}
};
class Dog : public Animal
{
public:
void Speak() { cout << "Dog Speaks" << endl; }
};
class Cat : public Animal
{
public:
void Speak() { cout << "Cat Speaks" << endl; }
};
void DoIt(Cat *);
void DoIt(Dog *);
int main()
{
Animal * pA = new Dog;
DoIt(pA);
return 0;
}
void DoIt(Cat * c)
{
cout << "They passed a cat!" << endl;
c->Speak();
}
void DoIt(Dog * d)
{
cout << "They passed a dog!" << endl;
d->Speak();
}
注:该程序可以使用虚函数
程序清单 R3.3 使用虚函数
#include
using namespace std;
class Animal
{
public:
virtual void Speak()
{
cout << "Animal Speaks" << endl;
}
};
class Dog : public Animal
{
public:
void Speak() { cout << "Dog Speaks" << endl; }
};
class Cat : public Animal
{
public:
void Speak() { cout << "Cat Speaks" << endl; }
};
void DoIt(Animal *);
int main()
{
Animal * pA = new Dog;
DoIt(pA);
return 0;
}
void DoIt(Animal * c)
{
cout << "They passed some kind of animal!" << endl;
c->Speak();
}
附录
C++关键字
关键字是语言保留给编译器使用的。不能讲关键字用作类、变量或函数的名称
asm false sizeof auto float static bool for static_cast break friend
struct case goto switch catch if template char inline this class
int throw const long true const_cast mutable try continue namespace typedef
default new typeid delete operator typename do private union double protected
unsigned dynamic_cast public using else register virtual enum reinterpret_cast void explicit
return volatile export short wchar_t extern signed while
除上述关键字外,下述单词也被保留:
and compl or_eq and_eq not xor bitand not_eq xor_eq bitor or