零基础学Visual C++(第4版)
上QQ阅读APP看书,第一时间看更新

第2章 C++语法基础

Visual C++6.0是一个集成开发环境,而C++是一门高级编程语言,学好Visual C++6.0的前提是学好C++语法理论。C++语言理论主要分为传统C语言语法部分、基于对象部分、面向对象部分和STL(Standard Template Library,标准模板库)部分。

本章涉及的知识点如下。

·数据类型的定义。读者将了解各种数据类型的定义和使用,并且有C语言基础的读者将学会从传统的面向过程数据定义观念转变为面向对象数据类型的对象创建观念。

·循环语句和表达式。读者将学会使用各种循环语句来处理问题。

·函数的定义和使用。读者将学会定义和使用各种函数。

·类的定义和使用。读者将学会类的定义和对象的创建方法,并学会重载运算符的理论和编写。

·类的继承。读者将学会面向对象的继承理论和实际编写方法。

·STL。读者将熟悉常用的几种STL的使用方法和原理。

·I/O流与C++的文件输入/输出操作。

2.1 C++简介

美国AT&T贝尔实验室的Bjarne Stroustrup博士在20世纪80年代初期发明并实现了C++,最初C++被称为“C with Classes”,即带类的C。C++语言是作为C语言的增强版出现的,起初只是在C的基础上增加类,然后不断地增加新的特性,如虚函数(virtual function)、运算符重载(operator overloading)、多重继承(multiple inheritance)、模板(template)、异常(exception)、RTTI和名字空间(name space)等。

1998年,国际标准组织(ISO)颁布了C++程序设计语言的国际标准ISO/IEC 14882-1998,即C++是具有国际标准的编程语言,通常称为ANSI/ISO C++。从1998年C++标准委员会成立时起,大概每5年就会视实际需要更新一次语言标准,最新一版的C++版本称为C++0X(也被称为C++11),其中核心语言的领域被大幅改善,包括多线程支持、泛型编程、统一的初始化以及表现的加强。

C++语言发展大概可以分为如下3个阶段。

·第一阶段从20世纪80年代到1995年。这一阶段C++语言基本上是传统类型的面向对象语言,并且凭借着接近C语言的效率,在工业界使用的开发语言中占据了相当大的市场份额。

·第二阶段从1995年到2000年。这一阶段由于标准模板库(STL)和后来的Boost等程序库的出现,泛型程序设计在C++中占据了越来越多的比重。同时,由于Java、C#等语言的出现和硬件价格的大规模下降,C++受到了一定的冲击。

·第三阶段从2000年至今。由于以Loki、MPL等程序库为代表的产生式编程和模板元编程的出现,C++的发展又达到一个新的历史高峰。这些新技术的出现以及与原有技术的融合,使C++成为当今主流程序设计语言中最复杂的一员。

分析机构Evans Data定期对开发人员展开调查,其调查结果与Stroustrup提出的C++正在扩张的说法相违背。Evans Data的数据显示,以C++为工具的开发人员在整个开发界所占的比例由1998年春的76%下降至2004年秋的46%。

Forrester最新的调查显示,C/C++、Visual Basic和Java是众多公司产品体系的首选语言,对100家公司的调查显示,使用比例分别是59%、61%和66%。

一般认为使用Java或C#的开发成本比C++低,开发成本即程序员的学习周期和软件研发周期。如果读者能够充分分析C++和这些高级语言的差别,会发现这句话的成立是有前提条件的。这个条件就是软件规模和复杂度都比较小。如果不超过3万行有效代码(不包括生成器产生的代码),这句话基本上还能成立,也就是说,在软件规模和复杂度较小的情况下,使用Java或C#的开发成本可能会比较低,但随着代码量和复杂度的增加,C++的优势将会越来越明显。

造成这种差别的原因就是C++在软件工程性上的优势。在Java和C#开始涉及软件工程的时候,C++实际上已经悄悄地将软件工程性提升到一个前所未有的高度。

语言在软件工程上的功能好坏依赖于语言的抽象能力。从面向过程到面向对象,语言的抽象能力有了一个质的飞跃。但在实践中,人们发现面向对象无法解决软件工程中的所有问题。于是,软件理论精英们逐步引入并拓展泛型编程,解决更高层次的软件工程问题。实际上,面向对象和泛型编程的起源都可以追溯到1967年,但由于泛型编程更抽象,因此应用远远落后于面向对象。

2.2 数据类型定义和使用

本节主要介绍C++数据类型的定义和使用,包括基本数据类型、数组、指针和结构体等。

2.2.1 简单变量

为了把信息存储在计算机中,并随时可以读取,程序必须记录以下3个基本属性。

·信息存放的位置

·要存储的值

·信息的类型

例如,定义一个整型变量,可以通过以下语句实现。

01      int MyBalls ;                  //定义一个整型变量
02      MyBalls = 5 ;                  //给该整型变量赋值为5

这两行语句说明,程序存储了一个信息类型为整型的变量,其值是5,并且存储在代号为MyBalls的变量中。但在这两行语句中,并不能体现这个值为“5”的整型存储的内存地址,事实上,程序是将这一切都安排好了的。使用“&MyBalls”命令即可查看MyBalls所在的内存地址。

信息存储的代号(即变量名)是有规定的,根据ANSI/ISO C++标准,必须遵循如下命名规则。

·变量名中只能使用英文字母字符、数字和下划线。

·变量名的第一个字符不能是数字,即第一个字符只能使用英文字母字符或下划线。

·区分英文字符大小写。即MyBalls、myballs、Myballs和myBalls是4个不同的变量名,分别表示4个不同的变量。

·不能将C++关键字用作变量名。

·变量名的长度没有限制。

如下代码定义了一些变量,读者可以参照命名规则判断其是否有效。

01      int myballs ;                        //有效
02      int 2myballs ;                  //无效,数字不能作为变量名的第一个字符
03      Int Myballs ;                        //无效,int关键字不能写成大写Int
04      int double ;                        //无效,不能将C++关键字作为变量名
05      int begin ;                        //有效,begin是Pascal语言的关键字,并非C++的关键字
06      int my-apple ;                  //无效,连字符不能作为变量名的字符
07      int my_apple ;                  //有效

注意 虽然变量名命名只要不违反命名规则即可,但原则上应尽量不使用a、b、c之类的简单字母或无意义的字母组合作为变量名,尽管这些命名是合法的,而应该以有意义的名词对变量命名,这样才有助于在软件研发中降低程序阅读的复杂性。

在变量名命名方面,业界也有一套默认的规则,即使用前缀来标记该变量的类型,如str前缀表示字符串、c前缀表示单个字符变量、p前缀表示指针类型、b表示布尔类型等,例如如下代码。

01      string strFirstName ;             //定义字符串变量,以str开头
02      int * pApple ;                  //定义指针变量,以p开头
03      bool bRight ;                        //定义布尔类型变量,以b开头
04      char cKey ;                         //定义字符变量,以c开头

C++作为一种高级语言,必须对数据类型进行分类,主要的基本数据类型有整型、浮点型、布尔类型和常量类型。

1.整型

整型就是没有小数部分的数字类型,即整数类型,而由于计算机不可能用有限的内存来表示所有整数,因此C++中的整型只能表示所有整数中的一个子集。C++提供了几种使用不同内存量的整型,使用的内存越大,可表示数值的范围也就越大。C++基本整型包括char、short、int和long,每种类型都有“有符号”和“无符号”两种版本,因此C++整型就有8种类型可供用户选择。在Win32环境下,不同类型的整型长度如表2-1所示。

表2-1 Win32环境下C++各类型的整型长度 (单位:字节)

注意 表2-1所使用的单位是字节(byte),它是计算机存储容量单位。计算机内存的基本单元是位(bit),8位即为1个字节。1KB等于1024字节,1MB等于1024KB,1GB等于1024MB,1TB等于1024GB。

【实例2-1】如下代码演示可以查看各类型整型的长度。

01       #include <iostream>
02       using namespace std;
03       int main()
04       {
05              cout<<"Hello,C++!"<<endl;
06              cout<<"char is : "sizeof(char)<< " bytes"<<endl;            //计算char的字长
07              cout<<"short is :"<<sizeof(short)<< "bytes"<<endl;      //计算short的字长
08              cout<<"int is : "<<sizeof(int)<< " bytes"<<endl;            //计算int的字长
09              cout<<"long is : "<<sizeof(long)<<" bytes" <<endl;            //计算long的字长
10              cin.get();
11              return 0;
12        }

【代码说明】第06~09行的sizeof操作符是返回变量或类型长度的操作符,返回值的单位是字节,sizeof后的括号对变量名是可选的,例如如下定义。

int number ;

则以下两行代码是等效的。

01      cout<<"char is : "sizeof(number)<<" bytes"<<endl;                  //使用括号
02      cout<<"char is : "sizeof number<<" bytes"<<endl;                        //不使用括号

但以下代码是非法的,编译器会弹出错误提示。

cout<<"char is : "sizeof int<<" bytes"<<endl;

整型变量定义的同时,可以进行变量的初始化,初始化的赋值号右操作数可以是数值,也可以是表达式,还可以是具有返回值的函数,例如如下代码。

01      int number = 16 ;                                                                  //数值初始化
02      int total = number + 34 ;                                                      //表达式初始化
03      int myNo = function() ;                                                      //函数返回值初始化

另外,C++还使用了C不支持的初始化方式,如下。

int number(234) ;

该方法主要用于在类构造函数的成员初始化列表中对数据成员进行初始化。

2.浮点类型

浮点类型是指小数点能够“自由浮动”的数据类型。浮点类型有float、double和long double 3种。浮点类型对于字长的要求如表2-2所示。

表2-2 Win32环境下C++各类型的浮点长度 (单位:字节)

3.布尔类型

在C语言中,程序员往往使用一个整型的变量标识一个对象的真假,C++中为此提供了布尔类型。布尔类型的名称来源于英国数学家布尔。布尔类型对象可以被赋予文字值true或者false,即真或假,同C语言中的整型衔接起来看,布尔类型的对象也可以被看做是一种整数类型的对象,更好的解释是布尔类型对象将被隐式地转换成整型对象,它们的值即false就是0,true就是1;同样,整型对象也可以向布尔型对象转换。但是它不能被声明成signed、unsigned、short long类型,否则会导致编译错误。

布尔变量可以定义如下。

01      bool bReally = true ;
02      bool bTrue = false ;

布尔类型的运算如表2-3和表2-4所示。

表2-3 布尔类型的“+”运算

表2-4 布尔类型的“*”运算

【实例2-2】表2-3和表2-4中的运算可以通过如下代码进行实验测试。

01      #include <iostream>                                                      //包含输入输出流头文件
02      using namespace std;                                                       //使用标准名称空间“std”
03      int main()
04      {
05         bool bTrue = true ;
06        bool bFalse = false ;
07        cout<<"True + True = "<<bool(bTrue + bTrue)<<endl;            //计算两true的和
08        cout<<"True + False = "<<bool(bTrue + bFalse)<<endl;      //计算true和false的和
09        cout<<"False + False = "<<bool(bFalse + bFalse)<<endl;       //计算两false的和
10        cout<<"True * True = "<<bool(bTrue * bTrue)<<endl;            //计算两true的积
11        cout<<"True * False = "<<bool(bTrue * bFalse)<<endl;      //计算true和false的积
12        cout<<"False * False = "<<bool(bFalse * bFalse)<<endl;      //计算两false的积
13        cin.get();
14        return 0;
15  }

【代码说明】在第07行中,布尔类型转换bool(bTrue+bTrue)是为了使该运算结果是布尔类型。由于“+”运算符会将两边的操作数转换成整型后再相加,因此返回的结果是整型,如果没有布尔类型转换,则此运算结果是2。代码第08~12也是如此。

4.常量类型

如果一个程序中多次使用到某一个数值,而程序员又希望这个数值能一次性全部修改而无需逐个修改,这时可以将这个数值定义为常量类型,定义常量类型有如下两种方式。

1  #define NUMBER 231 ;
2  const int NUMBER = 231 ;

第01行中的定义方法是C语言常用的定义方法,其原理是使用#define预编译器,将代码中的“NUMBER”变量在编译前就替换成“231”数值。不提倡在C++语言中使用这种方法,因为这在调试中显得漏洞百出。例如,在调试器中,如果出现了“231”数值相关的错误,但这个数值是什么,无从考究,因为“NUMBER”变量不会出现在调试器中,早在预编译器中就被替换了。

C++使用一种新的常量定义方法,即使用const限定符。该方法是在编译时进行替换的,因此出现错误时有据可查。

2.2.2 算术运算符

读者可能对数学学科中的各种算术运算符号记忆犹新,在计算机中也同样有各种算术运算符。

例如程序中有如下语句。

int apples = 2+7 ;

很显然,“2”和“7”是操作数,如果细化,可以称“2”为左操作数,“7”为右操作数,“+”符号是一个算术运算符,“2+7”是一个运算表达式,其值为“9”。

C++提供的算术运算符有下列5种。

·“+”:加运算符。

·“-”:减运算符。

·“*”:乘运算符。

·“/”:除运算符。

·“%”:取模运算符。

注意 取模运算符的左、右操作数必须为整数,其值是左操作数除以右操作数的余数,如19%5=4;除运算符的左、右操作数如果为整数,则其值的小数部分将被丢弃,如19/5=3。

C++中的操作符是有优先顺序的,跟数学运算原则一样,具体如下。

·从左到右进行运算。

·先进行乘除取模运算后再进行加减运算。

·有括号的先计算括号里面的表达式。

【实例2-3】测试运算符,代码如下。

01       #include <iostream>                              //包含输入输出流头文件
02       using namespace std;                              //使用标准名称空间“std”
03       int main()
04       {
05        int a = 19 ;                                    //定义两个普通的int变量a和b
06        int b = 5 ;
07        int result ;
08        result = a + b ;                              // result变量赋值为a与b的和
09        cout<<"a+b = "<<result<<endl;
10        result = a - b ;                              // result变量赋值为a与b的差
11        cout<<"a-b = "<<result<<endl;
12        result = a * b ;                              // result变量赋值为a与b的积
13        cout<<"a*b = "<<result<<endl;
14        result = a / b ;                              // result变量赋值为a与b的商
15        cout<<"a/b = "<<result<<endl;
16        result = a % b ;                              // result变量赋值为a与b的模
17    cout<<"a%b = "<<result<<endl;
18    result = a + a % b - b;                        // result变量赋值为a + a % b - b的运算结果
19    cout<<"a%b = "<<result<<endl;
20    cin.get();
21        return 0;
22      }

【代码说明】代码第05~06行定义了两个变量,并且为其设置初始值。代码第07行定义了一个变量result显示运算结果。代码第08~19行分别演示了不同的运算表达式。

注意 使用数学公式运算表达式,特别是比较长的运算表达式时,最好使用括号将运算顺序表达清楚,防止因运算符优先级等问题出现逻辑错误。

2.2.3 枚举、指针和数组

除了一些基本的数据类型外,C++还提供了其他额外的数据类型,如枚举、指针和数组类型以及将在后面几节中讲解的结构体和类类型,这些统称为复合数据类型,即由其他数据类型组合或以基本数据类型为基础衍生出来的类型。

1.枚举类型

在生活中,人们都有这样的常识:一个星期有7天,分别是星期一、星期二……星期日;交通灯只有红、黄、绿3种颜色。类似这样的情况还有很多,在计算机中可以用int、char等类型来表示这些数据。但是如果将星期一至星期日表示为1~7的整数,一方面在程序中容易将它们与其他不表示星期的整数混淆;另外,由于它们只能取有限的几种可能值,这样在程序中对数据的合法性检查成为一件比较麻烦的事。C++中的枚举类型就是专门用来解决这类问题的。

实质上,枚举类型是常量类型的一种衍生类型。C++提供了一个enum关键字,用来创建枚举类型,定义如下。

enum direction {east , south ,west ,north};

该语句定义了一个枚举类型direction,这个枚举类型可以表示东、南、西和北4个方向。对枚举类型的定义,不仅定义枚举的含义,还定义枚举变量的赋值类型,代码如下。

01      enum direction {east , south ,west ,north};      //定义枚举类型,该枚举类型只能取4个值
02      direction myDirection;
03      myDirection = east;                                          //枚举变量赋值
04      cout<<myDirection<<endl;

注意 enum direction{east,south,west,north}语句定义了一个枚举类型,而direction是一种类型而不是变量,myDirection才是该枚举类型的变量。

2.指针类型

指针是通过地址来访问变量的一种特殊的数据类型,属于动态的数据结构,它可以在需要时产生,用完后可以取消或回收,以减少占用的内存空间。指针变量与其他类型的变量不同,它占有的不是数据,而是地址。

由于动态数据结构的变量是在程序执行过程中动态生成的,所以不能预先予以说明,无法预先给这些变量起名字,访问时也无法通过名字直接输出或显示,而只能用指针得到其地址,然后间接访问。

为了把信息存储在计算机中,并随时可用,程序必须记录信息存放的位置、要存储的值和信息的类型。

变量名可以标记存储内存,也可以通过&取地址符来读取该变量名所指代的变量的存储地址。例如,如果myBalls是一个变量名,则可以通过“&myBalls”语句来取该变量名的地址。

C++提供了直接反映地址的变量,即指针变量。C++指针变量的定义有以下3种方式。

01  int* myBalls ;
02  int *myBalls ;
03  int * myBalls ;

这3种方式实质上是等效的,读者可以根据自己的编写习惯选择一种。本书将采用第01行中的定义方式,这样可以将int*看做是一种类型标示,这个类型称为“指向int的指针”。

如果读者习惯将多个变量定义在同一行,则应该注意如下定义歧义。

int* myBalls, number ;

该语句定义了一个int类型的myBalls指针变量和一个int类型的number整型变量,其等效于如下代码。

01      int* myBalls ;
02      int number ;

而不是如下代码。

01      int* myBalls ;
02      int* number ;

【实例2-4】演示指针的定义、使用和地址的读取,代码如下。

01  #include <iostream>                                                //包含输入输出流头文件
02  using namespace std;                                                //使用标准名称空间“std”
03  int main()
04  {
05       int* myBalls = 8 ;                                                //定义一个整型指针变量
06       cout<<"my balls are : "<<*myBalls<<endl;                  //输出指针所指向的整型
07       cout<<"Address is : "<<myBalls<<endl;                  //输出指针地址
08       *myBalls = *myBalls + 1 ;                                    //指针所指向的整型值加1
09       cout<<"Now , my balls are : "<<*myBalls<<endl;       //输出新的整型结果
10      cin.get();
11      return 0 ;
12  }

【代码说明】第08行中的语句*myBalls=*myBalls+1如果误写成myBalls=myBalls+1,由于没有语法错误,则该语句会被编译器通过,但其行为是未定义的,其语义是将myBalls这个地址加“1”,然后再赋值给地址变量myBalls。当程序使用到这个地址时,该地址所存放的值已经不是“8”了,而是在另外一个空间中,但这个空间究竟存放了什么是未知的。

编程谬误 数组的指针变量加1将指向数组下一个元素,而单个指针加1或减1后,指向的元素是所在内存块的下一个或上一个区域,这个区域未必是有意义的,因此编程时,随意移动指针并读取,会给软件安全带来隐患。

3.数组类型

在程序设计中,为了处理方便,编程人员会把相同类型的若干变量按有序的形式组织起来,这些按序排列的同类数据元素的集合称为数组。数组是程序设计语言中常用的数据类型,特别是在数值运算、矩阵应用或其他科学计算中。数组类型是一种复合的数据类型,是多个数据的组合,而组合成该数组的数据可以是基本数据类型,也可以是复合数据类型。例如,数组可以存放30个int类型的值,用来表示的是一个班30个学生的数学考试成绩。

数组的声明应该指出数组中每个元素的类型、数组名和数组的元素数目。

例如,可以声明一个数组,用来存放一个班30个学生的数学考试成绩,语句如下。

int math[30] ;

“int”指出数组中每个元素的数据类型是整型,即这30个学生的数学考试成绩都是整型;“math”指出该数组名;“30”指出这个数组的元素数目是30,即这个数组最多只能存放30个学生的数学考试成绩,如果这个班有31个或更多的学生,则该数组就不能完全存放这些学生的数学成绩。

注意 C/C++的数组下标是从“0”开始的,即数组math[3]含有的3个元素为math[0]、math[1]和math[2],而不是math[1]、math[2]和math[3]。

【实例2-5】数组的定义和使用,代码如下。

01      #include <iostream>                                          //包含输入输出流头文件
02      using namespace std;                                          //使用标准名称空间“std”
03      int main()
04      {
05         int math[3];                                                //定义一个数组
06         math[0] = 63 ;
07         math[1] = 80 ;
08         math[2] = 75 ;
09         int english[3] = {85,96,56};                              //定义一个数组并初始化
10     cout<<"Total math"<< math[0]+ math[1]+ math[2]<<endl;
11     cout<<"Total math"<< english[0]+ english [1]+ english [2]<<endl;
12     cout<<"First student total : " <<math[0]+english[0]<<endl;
13     cout<<"First student total : " <<math[1]+english[1]<<endl;
14     cout<<"First student total : " <<math[2]+english[2]<<endl;
15     cin.get();
16     return 0 ;
17  }

【代码说明】上述代码中,第09行的数组声明形式只能用于初始化,如果数组已经声明完毕,则不能再使用该方法来对数组元素进行赋值。第10~14行代码则显示成绩之和。

编程陷阱 有些初学者忘记了数组下标是从0开始的,容易读取math[3]数据,而math[3]是不存在的,因此会导致错误。

对于初学者来说,数组的赋值和初始化可能极易弄错,分别考虑如下初始化语句。

01      int math[3] = {12,32,23};                  //语法正确
02      int english[3];                              //语法正确
03      english[3] ={12,32,12};                  //语法错误,该方式只能用于初始化
04      math = english ;                              //语法错误

【代码说明】第04行中的语法错误实质上就是“math”不能作为“=”的左操作数,因为“math”是一个地址常量,它表示数组math[3]的起始地址,这个地址在编译时已经确定为常量地址了,所以将某个值(如“english”常量)赋给math这个地址常量,肯定不会通过编译器。

在数组的使用中,如果数组不是自己定义的,那么这个数组的元素个数往往是不可知的,因为数组本身不提供数组元素个数的信息,但使用前文所涉及的知识,通过一种巧妙的方法可以得到数组的元素个数,代码如下。

01      int math[4] ;
02      int count ;
03      count = sizeof(math) / sizeof(int) ;
04      cout<<count<<endl;

【代码说明】上述代码的本质就是先计算数组所占用的存储空间大小,然后除以每个元素的存储空间大小,进而得到数组元素的个数。

编程陷阱 sizeof函数可以自动识别参数是变量还是关键字,如果是关键字,则直接计算字长;如果是变量,则首先判断是单变量还是数组变量(如math),如果是数组,则通过定义式计算出元素个数和字长,再得出总的字长。有些读者不理解,误将sizeof(math)函数理解为计算指针字长,导致后面程序设计的计算错误。

2.2.4 string类和C风格字符串

顾名思义,字符串就是由字符组成的串。前文讲解了数组的使用,字符串也可以用数组来表示,语句如下。

char name[10] ;

这里就声明了可以由10个字符组成的字符串,用这个字符串来表示一个名字。使用字符数组来标记的字符串称为“C风格字符串”。但是使用字符数组来表示字符串有很多不方便的地方,如下所述。

·修改困难。如name的初始化和赋值都要逐个字符进行赋值操作。

·长度限制。如name的长度最多就是10,而且最后一个元素还要用于存放字符数组结束标记“\0”。

·粒度太小,不直观。如果使用字符数组来定义name,则程序员将花更多的时间和精力来处理关于字符粒度级别的操作,而分散了一些对于软件高层次的其他问题的注意力,如算法、抽象数据类型或软件架构等。

【实例2-6】C++不仅支持C风格字符串的使用,还引入string类的概念,该类提供了丰富的接口,例如以下代码。

01      #include <iostream>                              //包含输入输出流头文件
02      #include <string>                                    //包含string字符串类的头文件
03      using namespace std;                              //使用标准名称空间“std”
04      int main()
05      {
06              string str1 ;                                    //定义一个字符串类对象str1
07              string str2 = "Your name is :" ;      //定义一个字符串类对象str2并初始化
08              cout<<"Enter your name : "<<endl;
09              cin>>str1;
10              cout<<str2<<" "<<str1<<endl;
11              cout<<str1.size()<<endl;                  //调用size()函数获取str1字符串对象的长度
12              string str3 = str1 + str2;
13              cout<<str3 ;
14              cin.get();
15              return 0;
16       }

【代码说明】第11行表示调用str1对象的一个方法,该方法返回str1字符串对象的长度。在Visual C++6.0中,如果在一个对象或指向某对象的指针,后面输入代码“.”或“->”时,Visual C++6.0集成开发环境会自动显示智能提示,显示该对象或指针所指向的对象的数据成员和成员函数,如图2-1所示。

编程陷阱 字符串的字符串对象和容量是不一样的,size函数用于返回字符串对象的字符个数,但字符串构造函数中,虽然初始默认为0个字符,但容量却不是0,当字符串达到容量极限时,容量会倍增,就又多了一倍的空余容量。理解串长和容量的区别对于编程是有意义的。

图2-1 string对象的数据成员和成员函数

使用string类的对象相较于使用C风格字符串有如下优势。

·具有面向对象的操作方式,可以很方便地使用string类对象的接口,如size()等。

·可以使用重载运算符进行直观操作。如实例2-6代码的第12行,可以直接使用str3=str1+str2,这里使用了“+”的运算符重载函数,这个“+”运算符已经“认识”了左、右两边操作数的类型,并了解用户想使用字符串连接操作。而如果str1、str2和str3是字符数组,这样的操作是不允许的。

·长度无限制。使用string类对象,在声明时即使长度指定为0,在程序应用该类对象时,也可以无限制加入字符或字符串,而该类对象自动分配存储空间以存储新加入的字符或字符串。实质上是实现了长度的自我检测和自我增加。

注意 对于嵌入式或对内存等要求很高的程序设计,建议使用字符串数组,以节省内存和计算开销;若作为一般的应用,建议使用string类,以提高开发效率,减少出错率。

2.2.5 结构体类型

结构体是C++程序设计语言中重要的复合类型。结构体是由基本数据类型构成的并用一个标识符来命名的各种变量的组合。在结构体中可以使用不同的数据类型。

【实例2-7】假设要存储一个学生的信息,当然不能像前文所说的仅仅存储这个学生的数学成绩。编写一个程序,可以存储这个学生的以下信息。

·姓名

·所在班级

·学号

·家庭住址

·语文成绩

·数学成绩

·英语成绩

如果这个程序又不仅仅存储一个学生的成绩,比如可以存储一个班30位同学的成绩,那么程序设计基本思路如下。

01      struct student_info                              //定义学生信息结构体
02      {
03          string strName;                              //姓名
04          string strClass;                              //班级
05          int number;                                    //学号
06          string homeAddress;                        //家庭住址
07          int chinese;                                    //语文成绩
08          int math;                                          //数学成绩
09          int english;                                    //英语成绩
10       };
11       student_info student[30];                        //定义一个结构体数组

【代码说明】代码中第11行表示创建名称为“student”的数组,数组的元素个数为“30”,每个元素为“student_info”结构体类型,这个结构体中的数据成员、字符串都采用string类型。

具体实现代码如下。

01      #include <iostream>
02      #include <string>
03      using namespace std;
04      struct student_info
05      {
06         string strName;
07         string strClass;
08         int number;
09         string homeAddress;
10         int chinese;
11         int math;
12         int english;
13       };
14       int main()
15       {
16           student_info student[30] ;
17           student[0].strName = "Tom" ;                        //结构体数组的赋值
18           student[0].strClass ="Class 2";
19             student[0].number = 12324;
20             student[0].homeAddress ="BeiJing road";
21             student[0].chinese = 98;
22             student[0].math = 77;
23             student[0].english = 90;
24             cout<<student[0].strName                              //结构体数组的输出
25                   <<" live in "
26                   <<student[0].homeAddress
27                    <<" . His number is "
28                       <<student[0].number<<endl;
29        cin.get();
30        return 0;
31      }

【代码说明】与枚举类型一样,结构体的定义是一种类型定义。上述实例中的“student_info”是一个结构体类型,而不是一个结构体变量。对于结构体的赋值,可以在初始化时进行数组赋值,也可以通过数组分量进行结构体分量的赋值,第16~23行代码在main()函数中显式赋值结构体对象的各分量。

【运行效果】该程序的输出如下。

Tom live in BeiJing Road . His number is 12324

结构体的定义要声明如下3个方面。

·结构体的关键字struct。表示这是一个结构体定义。

·结构体的名称。如【实例2-7】代码中的“student_info”即是一个结构体类型的名称。

·结构体的结构成员。如【实例2-7】代码中的“strName”、“strClass”、“number”、“homeAddress”、“chinese”、“math”和“english”即为结构体类型“student_info”的结构成员。

注意 C++中的结构体与C中的结构体相比,具有类的性质,如继承、定义接口等。

2.2.6 类类型

所谓类(class),指的是一种软件包,它被OOP封装了数据(属性)和函数(行为),类的数据和成员是密切联系的。类的定义和结构体有点类似,具体内容将在2.5节详细介绍,这里简单讲解类定义的形式。

【实例2-8】假设前文所提到的学生信息要用到类来实现,即把一个具体的学生(人)看成一个对象,而把学生(这类人)看成一个类,那么可以通过以下形式进行定义。

01      class studentClass                        //类定义的标准形式
02      {
03          private:                                    //私有段,外部不能直接访问
04             string strName;
05             string strClass;
06             int number;
07             string homeAddress;
08             int chinese;
09             int math;
10             int english;
11          public:                              //公有段,供外部直接访问
12             function1();
13             function2();
14             …                              //其他成员函数定义
15       };

【代码说明】代码第01行是类定义的固有格式,由一个关键字class和类名组成;第3行是类定义的私有段,外部不能访问该段数据或函数;第11行是类定义的公有段,表示该段函数成员或数据成员提供给外部使用。

对象的创建类似于结构体或其他基本数据成员的创建,语句如下。

16      studentClass student1 ;                            //创建一个学生对象student1
17      studentClass student[30] ;                          //创建一个学生对象数组

【代码说明】第17行的studentClass student[30]创建了一个学生对象数组,该数组中的30个数组元素都是第01~15行定义的“studentClass”类型,即这个数组是由30个学生(人)组成的。

类定义要声明如下几个方面。

·类定义的关键字class。表明这个声明是一个类的声明。

·类名。例如studentClass就是一个类名,这个类名是抽象的概念,它表示一个学生类,而不是具体某个学生。

·访问控制关键字。类定义有3种访问控制关键字,即private、protected和public。使用private关键字限定的数据成员或成员函数,外界不能直接访问;而使用public关键字的数据成员或成员函数,外界可以直接访问。

在实例2-7代码的基础上,可以设计如下代码。

01       #include <iostream>
02       #include <string>
03       using namespace std;
04       class studentClass                                                      //类的定义
05       {
06       private:
07          string strName;
08          string strClass;
09          int number;
10          string homeAddress;
11          int chinese;
12          int math;
13          int english;
14      public:
15         string getstrName(){return strName;};                        //获取姓名
16            string gethomeAddress(){return homeAddress;};            //获取家庭住址
17      };
18      int main()
19      {
20          studentClass student[30] ;                        //类对象数组,即该数组是由对象组成的
21             student[0].strName = "Tom" ;                  //给第一个人赋值
22          student[0].strClass ="Class 2";
23             student[0].number = 12324;
24             student[0].homeAddress ="BeiJing road";
25             student[0].chinese = 98 ;
26             student[0].math = 77 ;
27             student[0].english = 90 ;
28             cout<<student[0].strName
29                  <<" live in "
30                  <<student[0].homeAddress
31                  <<" . His number is "
32                  <<student[0].number<<endl;
33            cin.get();
34            return 0;
35      }

【代码说明】代码第04行将结构体定义改成类定义;代码第04行同时也将结构体类型名称student_info改成类类型名称studentClass;代码第14~16行在类的public访问控制段增加了两个函数,用来返回学生的姓名和学生的家庭住址。

【实例2-9】运行实例2-8代码的时候,读者会发现编译器提示了编译语法错误。实例2-8的程序错误有10个之多,但这10个错误的性质是一样的,都属于main()函数非法访问了类的私有数据成员。将代码加以修正,以避免main()函数直接访问student类对象的私有成员。

修正后的代码如下。

01      #include <iostream>                                    //包含输入输出流头文件
02      #include <string>                                          //包含string类头文件
03      using namespace std;                                    //使用标准名称空间“std”
04      class studentClass                                    //类定义
05      {
06      private:                                                      //类的私有属性
07         string strName;
08         string strClass;
09         int number;
10         string homeAddress;
11         int chinese;
12         int math;
13         int english;
14      public:                                                      //类的公有属性和接口
15         studentClass()                                          //类的构造函数
16               {
17                     strName = "Tom";                              //在构造函数中赋值
18                     homeAddress = "BeiJing Road";
19                     number = 12342;
20               }
21         string getstrName(){return strName;};
22         string gethomeAddress(){return homeAddress;};
23         int getnumber(){return number;};
24        };
25        int main()
26        {
27          studentClass student ;
28          cout<<student.getstrName()
29                  <<" live in "
30                  <<student.gethomeAddress()
31                  <<" . His number is "
32                  <<student.getnumber()<<endl;
33             cin.get();
34         return 0;
35      }

【代码说明】代码第27~32行是main()函数调用类的公有函数的运行代码,main()函数作为studentClass类的“外部”,可以直接调用类对象的公有段成员,但不能调用私有段成员,就算写代码调用私有段成员,也会被编译器拒绝。第15行有一个跟studentClass类同名的函数,这个函数称为该类的“构造函数”,构造函数一般用于类对象的初始化,也就是在类对象创建之前,该函数就要被调用;相应地,在类对象消失前要调用一个类的“析构函数”,用于回收类对象分配的内存等。在实例2-9中并没有定义析构函数,编译器会给程序生成一个默认的析构函数。

编程陷阱 不要尝试直接在外部调用私有成员或保护成员,当然,这也会被编译器拒绝。设置私有成员,是面向对象的基本访问原则。许多编程初学者会喜欢将所有成员都放在公有段,但这种不良的编程习惯让C++面向对象功能成为摆设,而程序设计也无法使用面向对象设计的良好体系结构。

以实例2-9为例,一个类对象的创建和销毁就会经过了以下简化过程。

·操作系统调用main()函数,程序启动。

·main()函数调用studentClass构造函数。

·生成studentClass的类对象student。

·main()函数调用studentClass析构函数。

·销毁studentClass的类对象student。

·main()函数返回0值,退出main()函数,返回操作系统。

结构体与类的定义和声明具有很多相似性,因此结构体可以看成是特殊的类,不同点只在于结构体中的结构体成员是对外公有的,即等同于类的public成员;而类内的成员默认是private的,即如果不指定访问控制关键字,则类内的成员默认是私有的。结构体和类的相同点具体如下。

·都可以拥有数据成员和成员函数。

·都可以自定义构造函数和析构函数。

·都可以对成员进行访问控制的指定。

如读者可以作出如下结构体myStruct和类myClass的定义。

01       struct myStruct                        //结构体定义,使用struct关键字
02       {
03       private:                              //结构体内可以有私有段
04         int a;                                    //结构体内可以定义变量
05       public:
06         void fun();                        //结构体内可以定义函数
07       };
08       class myClass                        //类定义,使用class关键字
09       {
10        private:
11          int a;
12        public:
13          void fun();
14       };

【代码说明】代码第01行和第08行分别说明结构体定义和类定义使用的关键字是不同的,单从C++语法定义层面上来看是如此,但在面向对象设计中,代码实现建议还是以class定义为主。

注意 定义类只是在定义一个类型,而不占用存储空间,只有创建对象时才分配实际的内存空间。

2.2.7 实例:成绩管理系统(1.0版)

【实例2-10】现在考虑开发一个简单的成绩管理系统,用于管理记录高三(7)班的5位同学的个人信息和学习成绩等,要求使用前文所学的知识实现简单的数据录入、输出和统计等功能。

①可以输入全部同学的姓名、语文、数学和英语成绩。

②输出全部同学的姓名、语文、数学和英语成绩。

③统计全部同学的语文、数学和英语平均成绩。

该成绩管理系统的开发过程要经过以下几个步骤。

(1)新建项目。如图2-2所示,打开Visual C++6.0集成开发环境,单击“File”|“New”菜单命令。

(2)选择“Win32 Console Application”程序,命名为performance,然后单击“OK”按钮,如图2-3所示。

图2-2 新建项目

图2-3 选择项目类型和命名

(3)选中“An empty project”单选按钮,创建一个空的项目,如图2-4所示。

(4)弹出的对话框中显示了新项目信息,单击“OK”按钮,如图2-5所示。

图2-4 创建Win 32 Console空项目

图2-5 新项目信息

(5)创建好空项目,进入如图2-6所示的新项目的工作界面。

(6)给新项目中的源文件的文件夹添加新的源文件,如图2-7所示。

图2-6 创建的空项目界面

图2-7 添加源文件

(7)命名新的源文件为“main.cpp”,如图2-8所示。

(8)打开新的源文件,为空白源文件,如图2-9所示。

图2-8 命名新的源文件

图2-9 添加的空白源文件

(9)开始编程,添加代码。

按照成绩管理系统的要求,考虑到要对5位同学进行管理,而5位同学又有姓名数据和3门功课成绩,因此可以考虑使用结构体类型的数组来实现,代码如下。

01       struct student_info                  //定义学生结构体
02       {
03       string strName;                        //姓名
04       int chinese;                              //语文成绩
05       int math;                              //数学成绩
06       int english;                              //英语成绩
07       };
08       student_info student[5];            //学生结构体数组

【代码说明】这个结构体类型的数组的定义可以满足输入和输出学生姓名与成绩的功能要求,代码第03~06行分别定义了姓名、语文成绩、数学成绩和英语成绩数据段。统计学生平均成绩时,可以将各学生的各科目成绩相加后除以人数。

本实例实现源代码如下。

01      #include <iostream>                                          //包含输入输出流头文件
02      #include <string>                                                //包含字符串类头文件
03      using namespace std;                                          //使用标准名称空间
04      struct student_info
05      {
06        string strName;
07        int chinese;
08        int math;
09        int english;
10      };
11      student_info student[5];
12      int main()
13      {
14            cout<<"Enter the information about the students :"<<endl;
15            cin>>student[0].strName;                              //输入第1个学生的信息
16            cin>>student[0].chinese;
17            cin>>student[0].math;
18            cin>>student[0].english;
19            …;                                                            //输入其他学生的信息
20          cin>>student[4].strName                              //输入第5个学生的信息
21            cin>>student[4].chinese;
22            cin>>student[4].math;
23            cin>>student[4].english;
24            cout<<"Show the information about the students :"<<endl;
25            cout<<"Name:"<<student[0].strName;                  //输出第1个学生的信息
26            cout<<"Chinese performance:"<<student[0].chinese;
27            cout<<"Math performance:"<<student[0].math;
28            cout<<"English performance:"<<student[0].english;
29            …;                                                            //输出其他学生的信息
30            cout<<"Name:"<<student[4].strName<<endl;       //输出第5个学生的信息
31            cout<<"Chinese performance:"<<student[4].chinese<<endl;
32            cout<<"Math performance:"<<student[4].math<<endl;
33            cout<<"English performance:"<<student[4].english<<endl;
34            cout<<"Average of Chinese : "<<                  //输出语文平均成绩
35                  (student[0].chinese+
36                  student[1].chinese+
37                  student[2].chinese+
38                  student[3].chinese+
39                  student[4].chinese)/5<<endl;
40            cout<<"Average of Math : "<<                        //输出数学平均成绩
41                  (student[0].Math+
42                  student[1].Math+
43                  student[2].Math+
44                  student[3].Math+
45                  student[4].Math)/5<<endl;
46            cout<<"Average of English : "<<                  //输出英语平均成绩
47                  (student[0].English+
48                  student[1].English+
49                  student[2].English+
50                  student[3].English+
51                  student[4].English)/5<<endl;
52            cin.get();
53            return 0;
54      }

【代码说明】代码第34~51行简单实现了成绩计算和输出。随着读者语法知识的不断丰富,可以向此成绩管理系统不断地添加新功能,因此暂且将本小节开发的成绩管理系统命名为“成绩管理系统(1.0版)”。细心的读者可能就会发现,这个成绩管理系统(1.0版)存在如下许多问题。

·代码编写太烦琐。如输入5位同学的信息,在源代码中每位同学的输入(第15~18行)都要有cin输入流对象处理;输出也是如此(第24~51行)。

·可扩展性不强。假设要使用该成绩管理系统管理500名同学的信息,这个系统是不实用的,而且不能进行升级,要重新编写,寻求另外的实现方案。

·结构性不强。全部功能都实现在第12~54行的main()函数中,会导致后期维护成本很高,很难进行测试,并且不能将子功能提取出来复用。

这些问题,将在后继几节内容中逐步解决。

2.3 运算符、表达式和语句

本节将介绍几种常用的运算符,并在此基础上介绍循环语句和逻辑判断语句,最后利用本节的知识对成绩管理系统(1.0版)进行升级,将其升级到“成绩管理系统(2.0版)”。

2.3.1 几种常用的运算符

之前介绍了加、减、乘、除和取模5种算术运算符。本小节将着重介绍其他几种常用的运算符。

1.<<和>>运算符

运算符“<<”是输出流对象cout的输出运算符,该运算符具有多个重载版本,因此可以处理多个不同版本的参数。

【实例2-11】使用<<运算符的案例代码如下。

01      int number;
02      cout<<number;                              // <<运算符接受int类型
03      string strName;
04      cout<<strName;                        // <<运算符接受string类型

【代码说明】第02行和第04行都使用了输出流对象cout的输出变量。

什么是重载呢?具体的运算符重载的定义将在后继章节介绍,这里简单介绍,重载函数就是函数名相同,而函数参数不同的若干个函数。当用户使用这些函数时,调用的函数名是相同的,但需要传递的参数不同。

如实例2-11代码所示,第02行和第04行的代码可以等效于调用了如下函数。

01      cout.operator<<(int a);
02      cout.operator<<(string a);

而对比C语言版本的输出函数printf(),其形式如下。

printf("%d",number);

C语言版本的输出函数printf()需要用户指定输入格式“%d”才能正常工作,而printf本身无法自动判断。

而输出流对象cout调用不同的函数,对用户来说却是调用一样的函数,都使用的是“<<”,因此对用户来说,提供了很大的方便性。在用户看来,不管想输出什么变量,“<<”都能出色地完成任务。其实,如果<<运算符的右边出现了没有重载版本的参数类型时,<<运算符也会拒绝工作。

同样,使用输入流对象cin的>>运算符也同样能“毫不挑食地”输入常用的数据类型。

注意 事实上,使用输入输出流的重载运算符<<和>>也不是万能的,一般而言,其只能识别系统默认的基本数据类型,并不支持自定义的复合类型。

2.++和--运算符

【实例2-12】++和--运算符实质上是将某个对象或指针进行累加1或累减1的操作。先考虑以下代码。

01      #include <iostream>                              //包含输入输出流头文件
02      using namespace std;                              //使用标准名称空间
03      int main()
04      {
05       int number = 10 ;
06      cout<<"number++ : "<<number++<<endl;
07      number = 10 ;
08      cout<<"++number: "<<++number<<endl;
09      number = 10 ;
10      cout<<"number-- : "<<number--<<endl;
11      number = 10 ;
12      cout<<"--number: "<<--number<<endl;
13      cin.get();
14      return 0;
15 }

【代码说明】第05~12行代码分别运用了++和--运算符,并使用了前缀和后缀两种形式。

【运行效果】按F5键运行实例2-12代码可看到如下运行结果。

01      number++ :10
02      ++number:11
03      number-- :10
04      --number:9

【代码说明】从结果可以很显然地发现,在起始数值都是“10”的前提下,会出现以下3个规律。

·number++和++number的运算结果是不同的。

·number--和--number的运算结果是不同的。

·number++和number--的结果是相同的。

原因在于,++和--运算符处在变量的左、右两边所起的作用是不同的。

编程陷阱 在使用前缀或后缀的++与--运算符时要特别注意,很多运行时错误都是因为没有注意该问题,导致软件在正式交付时出现溢出异常,造成软件崩溃。

当运算符位于变量的左边时,变量先进行++或--,然后再执行其他任务,例如实例2-12代码中的“++number”和“--number”就是先将number加1或减1,然后再执行cout对象的<<操作。

当运算符位于变量的右边时,变量先执行操作,再进行++或--,例如实例2-12代码中的“number++”和“number--”是先执行cout对象的<<操作,然后再将number加1或减1。

注意 ++和--运算符在细节上,特别是在数组边界问题上容易出错,导致程序运行时崩溃,要特别注意这方面的问题。

2.3.2 循环语句

计算机的特点是运算速度快,而且非常适合处理大量重复性的操作。因此在任何一门程序设计语言中,循环语句都是不可或缺的。

1.for语句

for语句的基本格式如下。

01      for(起始条件;终止条件;累加)
02      {
03           //循环体
04      }

起始条件可以是声明语句或表达式。一般将它用于初始化一个在循环过程中递增的变量,或者赋给一个起始值。如果起始条件不需要初始化或者已经在别处出现,则可以省略,但是必须要有一个分号表明缺少该语句(或给出空语句)。

终止条件用于循环控制。终止条件计算结果为true多少次,则循环体就执行多少次。终止条件可以是单个语句,也可以是复合语句。如果终止条件的第一次计算结果为false,则从不会执行循环体。

在循环每次迭代后计算“累加”,一般用它来修改在起始条件中被初始化的、在终止条件中被测试的变量。

【实例2-13】对10个数进行自动赋值,并计算这10个数的平均数,实现代码如下。

01       #include <iostream>                              //包含输入输出流头文件
02       using namespace std;                              //使用标准名称空间
03       int main()
04       {
05             int ary[10];                                    //定义一个ary整型数组
06            int i = 0 ;
07            for( i = 0 ; i < 10 ; i++ )            //对ary数组进行赋值
08            {
09                  ary[i] = i * 3 ;
10            }
11            int total = 0 ;
12            for( i = 0 ; i < 10 ; i++ )            //累加ary数组的元素值,将结果存放在total变量中
13            {
14                  total += ary[i];
15            }
16            int avg = total / 10 ;                  //计算平均值
17            cout<<"Average is : "<<avg<<endl;
18            cin.get();
19            return 0;
20       }

【代码说明】第07~10行代码表示赋值数组,代码12~15行则是对数组进行累加,其中第15行代码中的“+=”是一个加号和赋值号组合的算术运算符,它的作用等效于如下代码。

total = total + ary[i] ;

前文所介绍的5种算术运算符都可以和赋值号组合,例如可以组合为+=、-=、*=、/=和%=。

2.while语句

while语句的基本格式如下。

01      while(条件表达式)
02      {
03           //循环体
04      }

条件表达式计算结果为true多少次,则循环就迭代多少次,语句或语句块也被执行多少次。执行次序为先计算条件表达式,如果条件表达式为true,则执行循环体(语句);如果条件的第一次计算结果为false,则永远不会执行循环体。

【实例2-14】编写具有与实例2-13同样功能的程序,即对10个数进行自动赋值并计算这10个数的平均数,代码如下。

01        #include <iostream>
02        using namespace std;
03        int main()
04      {
05            int ary[10];
06            int i = 0 ;
07            while(i<10)                              //使用while循环体
08            {
09                  ary[i] = i * 3 ;
10                  i++ ;
11            }
12            i = 0 ;
13            int total = 0 ;
14            while(i<10)                              //再次使用while循环体
15            {
16                  total += ary[i] ;
17                  i++;
18            }
19            int avg = total / 10 ;
20            cout<<"Average is : "<<avg<<endl;
21            cin.get();
22            return 0;
23      }

【代码说明】第07~11行是数组元素的赋值,第14~18行的while语句实质上可以和for语句交换使用,只是while语句没有明显的循环变量界限设定。

注意 for和while循环首先进行条件的真值测试,这意味着这两个循环都可以在相关语句或语句块还没有被执行的情况下就终止了。

3.do…while语句

do…while语句的基本格式如下。

01      do
02      {
03           //循环体
04      }while(条件表达式)

do…while语句和while语句极为相似,但还是有些区别的,do…while语句是将条件表达式放在后面,也就是必须先执行1次循环体,然后判断条件表达式;而while语句是先判断条件表达式,再决定是否执行循环体。因此do…while语句的循环体执行次数大于或等于1次,while语句的循环体执行次数大于或等于0次。

注意 使用循环语句要特别注意第一个元素和最后一个元素的处理问题,例如,经常会出现两端元素因初始化或设定边界方面的细节偏差导致不期望的结果。

2.3.3 判断语句

1.if语句

在实际的工作流处理中,经常会遇到这样的情况:如果出现A情况,则执行甲事情;如果不出现A情况,则执行乙事情。而为了处理这样的工作流选择的情况,绝大多数高级语言都提供了if语句进行处理。

if语句的语法格式如下。

01       if(条件表达式)
02       {
03         //执行体1
04       }
05       else
06       {
07        //执行体2
08      }

if语句可以嵌套,即可以写成如下形式。

01       if(条件表达式)
02       {
03           //执行体1
04       }
05       else if
06       {
07          //执行体2
08       }
09       else
10       {
11          //执行体3
12       }

【实例2-15】利用if语句的嵌套可以处理多种情况的工作流,代码如下。

01       #include <iostream>
02       using namespace std;
03       int main()
04       {
05            int a;
06            cin>>a;
07            if(a)                                                            //用整型变量作为布尔值
08            {
09                  cout<<"You enter is not a zero !"<<endl;
10            }
11            else
12            {
13                  cout<<"You enter is a zero!"<<endl;
14            }
15            cin.get();
16            return 0;
17       }

【代码说明】第07行代码首先判断用户输入的变量是否为真,0表示假,非0值都为真;然后根据结果分别执行第09行和第13行的代码。

2.switch语句

使用嵌套的if语句来处理3种或3种以上的不同情况的工作流也是可行的,但这种程序并不直观,因此,需要使用一种开关式的判断语句,将每条分支都明晰化。这种开关式的语句就是switch语句。

switch语句的格式如下。

01       switch(条件变量)
02       {
03         case条件值1:
04            //执行体1
05             break;                              //这个break不能少,否则会顺序往下执行
06         case条件值2:
07            //执行体2
08            break;
09        case条件值3:
10             //执行体3
11             break;
12         case条件值4:
13             //执行体4
14             break;
15         …;          //其他case字句
16         default:
17             //执行体5
18             break;
19      }

【实例2-16】switch语句就像一个选择性的开关,可以根据“条件变量”将程序定向到某特定的执行体,示例代码如下。

01      #include <iostream>
02      using namespace std;
03      int main()
04      {
05      int number;
06      cout<<"Enter number between 1 and 5 :"<<endl;
07      cin>>number;
08      switch(number)                               //对number变量进行判断
09      {
10            case 1:                                  //如果number等于1
11                  cout<<"number is : 1 "<<endl;
12                  break;
13            case 2:                                  //如果number等于2
14                  cout<<"number is : 2 "<<endl;
15                  break;
16            case 3:                                  //如果number等于3
17                  cout<<"number is : 3 "<<endl;
18                  break;
19            case 4:                                //如果number等于4
20                  cout<<"number is : 4 "<<endl;
21                  break;
22            case 5:                                //如果number等于5
23                  cout<<"number is : 5 "<<endl;
24                  break;
25            default:                               //其他情况
26                  cout<<"number is not between 1 and 5 "<<endl;
27                  break;
28            }
29            cin.get();
30            return 0;
31      }

【代码说明】上述代码实现的是使用switch语句来判断用户输入的number,进而选择相应的输出打印。第12行代码中有个break(在每个case子句中都有一个break关键字),这个关键字提醒编译器执行完这个case子句执行体后直接跳出switch语句,而不是执行下一个case子句的执行体。也就是说,如果没有break关键字,一个case子句执行完后,还将继续执行下一条执行体。例如如下代码。

01      case 1:
02           cout<<"number is 1 "<<endl;            //这里没有break关键字,因此会执行case2
03      case 2:
04           cout<<"number is 2"<<endl;
05           break;

当用户输入“1”时,程序的运行结果如下。

01      number is 1
02      number is 2

很显然,两个case子句的执行体都被执行了。

编程陷阱 switch语句中的break一定不能少,有些读者不理解switch语句的流程而漏了break关键字,则程序流程会偏离设计者设想。

为什么编译器不实现执行一个case子句时就跳出,而要用户自己写break子句进行跳出呢?原因在于并不是所有连续执行case子句都没用的,比如判断用户输入的字母而忽略大小写的switch语句就可以用到,代码如下。

01      switch(someChar)
02      {
03         case 'A':
04         case'a':
05               …;            //执行体1
06             break;
07         case 'B':
08         case'b':
09                …;            //执行体2
10             break;
11         case 'C':
12         case'c':
13                …;            //执行体3
14             break;
15               …;             //其他case子句执行体
16         default:
17                …;            //执行体n
18             break;
19      }

注意 使用判断语句要注重考虑各种情况的处理问题。

2.3.4 实例:成绩管理系统(2.0版)

在前文根据要求开发了成绩管理系统(1.0版),而在本节中,增加了新的知识,那么现在就可以对成绩管理系统进行升级了。

【实例2-17】将成绩管理系统(1.0版)的功能升级,具体如下。

·以比较少的代码量可以输入50位同学的姓名、语文、数学和英语成绩。

·以比较少的代码量可以输出50位同学的姓名、语文、数学和英语成绩。

·以比较少的代码量可以计算50位同学的语文、数学和英语平均成绩。

成绩管理系统(1.0版)中的代码非常烦琐,这个可能是其最明显的问题,将要开发的成绩管理系统(2.0版)将使用循环语句解决这个问题,而这样的方式可以明显增加可处理的学生信息的数目。

读者可以参照2.2.6小节的方式重新创建项目,也可以在成绩管理系统(1.0版)的基础上升级到成绩管理系统(2.0版),具体升级步骤如下。

(1)将存放成绩管理系统(1.0版)的文件夹“performance”重命名为“performance1.0”。

(2)复制粘贴存放成绩管理系统(1.0版)的文件夹“performance1.0”并重命名为“performance 2.0”,以便将成绩管理系统(1.0版)备份好,如图2-10所示。

图2-10 备份成绩管理系统(1.0版)

(3)打开“performance2.0”文件夹下的工作台文件“performance.dsw”,如图2-11所示。

(4)清空main.cpp源文件中main()函数体内的代码,如图2-12所示。

图2-11 打开工作台文件

图2-12 清空main()函数体内的代码

(5)修改student数组的元素个数,将student数组的元素个数修改为50,代码如下。

student_info student[50];

(6)编写main()函数体内的代码,代码如下。

01       #include <iostream>                                    //包含输入输出流文件
02       using namespace std;                                    //使用标准名称空间
03       struct student_info
04       {
05        string strName;
06        int chinese;
07        int math;
08        int english;
09       };
10        student_info student[50];                        //定义50个同学的数组
11       int main()
12       {
13            int i = 0 ;                                          //循环变量
14            char a[20];                                          //字符串临时存储变量
15            for( i = 0 ; i < 50 ; i++ )                  //循环50次,输入50位同学的信息
16            {
17                  cout<<"Enter the "<<i+1<<"student's information :"<<endl;
18                  cout<<"Enter name :"<<endl;
19                  cin>>a;
20                  student[i].strName = a;
21                  cout<<"Enter Chinese performance:"<<endl;
22                  cin>>student[i].chinese;
23                  cout<<"Enter Math performance:"<<endl;
24                  cin>>student[i].math;
25                  cout<<"Enter English performance:"<<endl;
26                  cin>>student[i].english;
27            }
28            for( i = 0 ; i < 50 ; i++ )                  //循环50次,输出50位同学的信息
29            {
30                  cout<<"The "<<i+1<<"student's information :"<<endl;
31                  cout<<"Name :"<<student[i].strName.c_str()<<endl;
32                  cout<<"Chinese performance:"<<student[i].chinese<<endl;
33                  cout<<"Math performance:"<<student[i].math<<endl;
34                  cout<<"English performance:"<<student[i].english<<endl;
35            }
36            int avgChinese = 0 ;
37            int avgMath = 0 ;
38            int avgEnglish = 0 ;
39            int total = 0 ;
40            i = 0 ;
41            while(i<50)                                          //循环50次,累加语文成绩
42            {
43                  total += student[i].chinese;
44                  i++;
45            }
46            avgChinese = total / 50 ;                        //计算语文平均成绩
47            total = 0 ;
48            i = 0 ;
49            while(i<50)                                          //循环50次,累加数学成绩
50            {
51                  total += student[i].math;
52                  i++;
53            }
54            avgMath = total / 50 ;                        //计算数学平均成绩
55            total = 0 ;
56            i = 0 ;
57            while(i<50)                                          //循环50次,累加英语成绩
58            {
59                  total += student[i].english;
60                  i++;
61            }
62            avgEnglish = total / 50 ;                        //计算英语平均成绩
63            cout<<"Enter 1 for look up the average of Chinese.\n"
64                  <<"Enter 2 for look up the average of Math.\n"
65                  <<"Enter 3 for look up the average of English.\n";
66            cin>>i;
67            switch(i)                                          //根据用户输入来显示不同科目平均成绩
68            {
69            case 1:
70                  cout<<"Average of Chinese is : "<<avgChinese<<endl;
71                  break;
72            case 2:
73             cout<<"Average of Math is : "<<avgMath<<endl;
74                     break;
75             case 3:
76              cout<<"Average of English is : "<<avgEnglish<<endl;
77                  break;
78            default:
79                  cout<<"Please enter 1,2or3"<<endl;
80                  break;
81            }
82            cin.get();
83            return 0;
84       }

【代码说明】代码的第47~61行功能是循环计算累加各门科目的成绩,第67~81行代码是根据用户输入来显示不同科目的平均成绩,第14行代码定义了一个字符数组,目的是使用cin输入流对象的>>运算符。>>是重载运算符,对于常用的数据类型输入是没有问题的,但它也不是万能的,它不能解决所有的数据类型,例如不能接受如下运算。

cin>>student[i].strName;

原因在于student[i].strName是string类类型的,而>>没有接受string类类型的重载版本。因此在该成绩管理系统中先使用字符串数组接收姓名字符串,再赋值到student[i].strName中,如实例2-17代码的第20行所示。

相对于成绩管理系统(1.0版),成绩管理系统(2.0版)消除了代码冗余的缺点,实现尽可能多的学生个数的同时而不增加代码量,同时,该系统还使用了本小节所讲述的各种循环语句。

尽管成绩管理系统(2.0版)与成绩管理系统(1.0版)相比有许多优点,但也还具有一些缺点没有更改,如代码都放在main()函数中,没有将功能模块化。在学习完2.4节后,可将成绩管理系统(2.0版)升级到成绩管理系统(3.0版),以优化代码的可扩展性,将系统的功能模块化。

2.4 函数定义和调用

本节主要介绍函数的定义和调用,涉及的内容有函数的常规使用方法、递归调用、函数重载和函数模板等。

2.4.1 定义函数和函数原型

到目前为止,本书内容已经多次使用了一个函数,即C++程序的主函数main()。main()具有C/C++函数的一般特性,函数的定义形式如下。

01      returnType functionName(参数列表)
02      {
03           函数体;
04           return returnTypeObject ;                  //返回值
05      }

returnType是函数的返回类型,该类型可以为void,也可以是基本数据类型,还可以是某结构体或类类型。

函数最大的功能就是将系统的子功能模块化,模块化后的功能可以更方便地进行复用。用户或用户程序只需要知道函数的接口即可使用该函数。而函数的原型是指函数的返回值和函数接口,它能正确反映函数调用和被调用之间的关系。模块化包括函数名和函数的参数列表(包括参数个数和参数类型)两方面的含义。

【实例2-18】求3个数的平均数,代码如下。

01       #include <iostream>                              //包含输入输出流
02       using namespace std;                              //使用标准名称空间
03       int avg(int a,int b,int c)                  //定义平均数函数
04       {
05            return (a+b+c)/3 ;
06       }
07       int main()
08      {
09            int x = 90 ;
10            int y = 34 ;
11            int z = 45 ;
12            int avgThree = 0 ;
13            avgThree = avg(x,y,z);                  //调用平均数函数
14            cout<<"The average is : "<<avgThree<<endl;
15            cin.get();
16            return 0;
17      }

【代码说明】代码第03~06行是avg()函数的定义,从函数定义可以看到,该函数对传递过来的参数进行了平均计算,然后返回这个运算结果。而在main()函数中的第13行调用avg()函数,并将返回值赋值给avgThree变量。

注意 被调用函数一定要在调用函数之前,这样调用函数才知道这个函数的原型。

2.4.2 函数通过指针调用数组

C++将数组名视为指针,即将数组名解释为其第一个元素的地址,代码如下。

apples = &apples[0] ;

但也有例外的情况,即使用sizeof语句来获取整个数组长度,这时就不能将数组名看做是数组的第一个元素的地址。

函数使用数组参数的一般调用方法如下。

function( arrayName , arrayNum ) ;

【实例2-19】编写一个程序,要求其中有一个对整型数组求和的函数,代码如下。

01      #include <iostream>                              //包含输入输出流
02      using namespace std;                              //使用标准名称空间
03      int sumArr(int arr[],int n)                  //求数组元素之和的函数
04      {
05            int total = 0 ;
06            for( int i = 0 ; i < n ; i++ )
07            {
08                  total += arr[i];
09            }
10            return total;
11      }
12      int main()
13      {
14            int myArray[5] = {334,45,56,3,23};
15            int sum ;
16            sum = sumArr(myArray,5);            //调用数组求和函数
17            cout<<"The sum is : "<<sum<<endl;
18            cin.get();
19            return 0;
20       }

【代码说明】代码第03~11行的功能是定义一个求数组元素之和的函数,它使用数组作为参数。使用数组作为参数的函数定义有如下两种形式。

·function(arrayType arrayName[],arrayNum)。例如,实例2-19的代码第03行就使用了这种方式。

·function(arrayTypearrayName,arrayNum)。

注意 以上两种使用数组作为参数的函数定义,在客户程序调用该函数时,使用的调用方式都是一样的,即使用指针调用数组参数。

2.4.3 函数指针

函数在内存中运行时,肯定也要像数据对象一样,占用一定的内存空间。既然占用了内存空间,那么它也就有一个内存地址,而指向这个内存地址的就是“函数指针”。对程序而言,函数指针是一个非常重要的概念。使用函数指针可以编写一种函数,这种函数调用了另外一个或若干个函数的指针,即定义调用函数参数的函数。其实这种使用方式在实例1-1中就出现过。

【实例2-20】将实例1-1中的代码使用的函数指针的代码段单独列举出来,如下。

01      LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
02                  LPARAM lParam)                              //窗口函数
03      {
04            int wmId, wmEvent;
05            PAINTSTRUCT ps;
06            HDC hdc;
07            TCHAR szHello[MAX_LOADSTRING];
08            LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
09            switch (message)
10            {
11                  case WM_COMMAND:                              //命令消息处理
12                        wmId    = LOWORD(wParam);
13                        wmEvent = HIWORD(wParam);
14                        // Parse the menu selections:
15                        switch (wmId)                              //菜单消息处理
16                        {
17                              case IDM_ABOUT:
18                                 DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd,
19                                    (DLGPROC)About);
20                                       break;                        //消息函数以函数指针的方式调用
21        …;   //其他代码
22       }
23        LRESULT CALLBACK About(HWND hDlg, UINT message,
24                  WPARAM wParam, LPARAM lParam)             //消息函数定义
25       {
26          …;                                                      // About对话框相应的处理函数
27       }

【代码说明】代码第01~02行中的WndProc()函数在第18行调用了一个DialogBox()函数,该函数就有一个实参数“About”,这个就是以函数名作为函数指针参数的调用。这在Win32 SDK编程中是常见的调用方式,特别在使用资源和相应的资源或消息处理函数调用中最常见。

编程陷阱 MFC里也有使用函数指针的地方,就是消息映射表,但使用VC++6.0集成开发环境无须自己手工添加,因此对于函数指针的使用了解即可,初学者使用起来非常容易出错,且编译器会因不认为是语法错误而没有语法提示。

2.4.4 具有引用参数的函数

变量或对象的引用实质上是变量或对象名的一个“别名”,即一个变量实体有两个名字。如某人姓名为“王孝峰”,熟知他的人都叫他的小名“王小二”,那么对这些熟知他的人来说,如果别人提到“王孝峰”就知道是指代他,如果提到“王小二”还是会自然想到指代的是他。变量的引用定义如下。

01      int rate = 30 ;
02      int &ptRate = rate ;

变量的引用具有终身忠诚的特性,它在定义时初始化成某变量实体的别名,就会一直保持到变量销毁。如果让其他变量赋值给该应用,只会改变该引用(即该变量实体)的数值,而不会改变该引用别名所指代的对象。例如如下代码。

01      int rate = 30 ;
02      int &ptRate = rate ;                        // ptRate初始化为rate的引用
03      int apples = 50 ;
04      ptRate = apples ;                              // ptRate被赋值为50,即rate被赋值为50;

【实例2-21】编写一个交换两个数字的函数,并在客户程序main()函数中调用和测试,代码如下。

01       #include <iostream>                        //包含输入输出流函数
02       using namespace std;                        //使用标准名称空间
03       void swap(int a,int b)                  // swap()函数定义
04       {
05            int temp ;
06            temp = a ;
07            a = b ;
08            b = temp ;
09       }
10       int main()
11       {
12            int numA = 123 ;
13            int numB = 4567 ;
14            swap( numA , numB );                  //调用swap()函数
15            cout<<"numA : "<<numA<<" ; numB : "<<numB<<endl;
16            cin.get();
17            return 0;
18       }

【运行效果】按F5键编译并运行代码,运行结果如下。

numA : 123 ; numB : 4567

【代码说明】代码第03~09行定义的是传递普通参数的swap()函数,编写该程序的本意是想编写一个具有可以随时调用的、可交换两数据的函数的处理程序,但从运行结果看,这次实验是失败的,原因在于函数体内的变量只具有局部生命周期。

编程陷阱 编写交换函数是一项非常简单轻松的工作,但也不能因此掉以轻心,在本例中,该交换的数据并没有完成交换,如果软件也出现类似的逻辑错误,上线后的软件错误跟踪将是一项非常麻烦的事情。

什么是变量的生命周期?例如如下代码。

01      int a =90 ;                                          //此处定义了一个全局变量
02      int main()
03      {
04        int b = 80 ;                                     //此处定义了一个在main()函数体内的局部变量
05      }
06      void function(int x , int a, int b)      //函数形式参数x、a、b是局部变量
07      {
08      }

【代码说明】代码第01行中的变量a不在任何函数体内,这样的变量称为全局变量。如果变量不是全局变量,那么就称为局部变量。例如第04行中的变量b以及第06行中的函数形式参数x、a和b。全局变量和局部变量有如下两方面不同。

·全局变量诞生于程序创建,销毁于程序退出;局部变量诞生于局部代码段启动,销毁于局部代码段结束。而变量的创建到销毁的过程就称为变量的生命周期

·如果全局变量没有指定初始化值,默认是“0”(全局指针变量则为NULL);如果局部变量没有指定初始化值,则默认是未定义的,因此局部变量的声明最好指定一个初始化值。

现在回到实例2-21,swap()函数实质上经过了如下过程(伪代码)。

01      swap()
02
03      {      int a = numA;
04            int b = numB;
05            int temp ;
06            temp = a ;
07            a = b ;
08            b = temp ;
09      }

由如上伪代码来看,作为main()函数体内的局部变量,numA和numB没有进行任何交换,交换的只是swap()函数体内的局部变量a和b,而这两个变量在swap()返回main()函数时自动销毁了,这就是本次实验失败的原因。

【实例2-22】为了使swap()函数的局部变量参数跟外界传递的实体参数同属一个实体,可以使用引用变量,代码如下。

01       #include <iostream>
02       using namespace std;
03       void swap(int &a,int &b)                        //此处的交换函数形式参数是引用类型的
04       {
05            int temp ;
06            temp = a ;
07            a = b ;
08            b = temp ;
09       }
10       int main()
11       {
12            int numA = 123 ;
13            int numB = 4567 ;
14            swap( numA , numB );                        //调用swap()函数
15            cout<<"numA : "<<numA<<" ; numB : "<<numB<<endl;
16            cin.get();
17            return 0;
18       }

【运行效果】按F5键编译并运行代码,运行结果如下。

numA : 4567 ; numB : 123

【代码说明】实例代码第03~09行定义的是以引用类型作为参数的交换函数,swap()函数实质上经过了如下过程(伪代码)。

01      swap()
02      {
03            int &a = numA;
04            int &b = numB;
05            int temp ;
06            temp = a ;
07            a = b ;
08            b = temp ;
09      }

由如上代码可知,作为numA和numB的引用的a和b发生了交换操作,而a和b就是numA和numB的实体的别名,因此numA和numB也发生了值交换操作。

使用指针也是可以达到交换效果的,从某种意义上来说,指针也是一种别名,例如如下代码。

01      void swap(int *a,int *b)
02      {
03            int temp ;
04            temp = *a ;
05            *a = *b ;
06            *b = temp ;
07      }

注意 对于函数参数的传递,实际上是将参数复制到一个临时变量(或对象)中,因此,如果该对象比较大,则建议使用引用参数,这样就不用经过创建新对象后再进行赋值操作,从而节省程序运行的内存和计算开销。

2.4.5 函数重载

面向对象的编程就是将自然界的物体或工作流中的处理对象看成对象。C++具有多态性,函数重载就是多态性中的一种表现形式。所谓的函数重载,就是一个函数名有多个不同版本的函数体。这样的好处在于给用户或用户程序提供了一个稳定不变的接口,而接口里面的内容根据用户程序提供的参数来确定。

如swap()函数,由于参数类型是int型的,因此只能处理int型的数据交换。可能有读者尝试使用swap()函数来处理double类型的交换,发现仍然能编译通过,那是因为编译器将double类型自动转换成int类型,实质上也只是能处理int类型的交换。例如,该函数不能通过处理string类型的交换,因为string类型不能自动转换成为int类型。

函数重载的步骤如下。

(1)确定函数调用考虑的重载函数的集合,确定函数调用中实参表的属性(确定候选函数,即重载函数集、实参的数目和类型)。

(2)从重载函数集合中选择函数,该函数可以在给出实参个数和类型的情况下用调用中指定的实参进行调用。

(3)选择与调用最匹配的函数(根据参数转换等级来确定)。

【实例2-23】编写项目代码,假设项目经常需要使用string类型、int类型和自定义类型otherType类型的交换,那么必须为这个项目提供三个swap()重载函数,以供项目中的主体程序使用,代码如下。

01      void swap(string &a,string &b)                  //使用string的swap函数重载版本
02      {
03            string temp ;
04            temp = &a ;
05            &a = &b ;
06            &b = temp ;
07      }
08      void swap(int &a,int &b)                        //使用int的swap函数重载版本
09      {
10            int temp ;
11            temp = &a ;
12            &a = &b ;
13            &b = temp ;
14      }
15      void swap(otherType &a,otherType &b)      //使用otherType的swap函数重载版本
16      {
17            otherType temp ;
18            temp = &a ;
19            &a = &b ;
20            &b = temp ;
21      }

【代码说明】代码第01行、第08行和第15行分别是不同参数类型的同名函数,这样的同名函数在同一个程序空间定义时,就称为函数重载。当用户使用如下客户程序时,编译器会自动选择正确的swap()函数版本。

01      int main()
02      {
03         string strA = "string of  A";
04         string strB = "string of  B";
05         int A =23;
06         int B = 34 ;
07         otherType otherA ;
08         otherType otherB ;
09         swap(strA , strB) ;                  //调用swap(string &a,string &b)
10         swap(A , B) ;                        //调用swap(int &a,int &b)
11         swap(otherA , otherB) ;            //调用swap(otherType &a,otherType &b)
12      }

【代码说明】使用函数重载时,当传递的实体参数的类型不吻合于所有函数重载版本时,编译器会首先自动转换参数类型,如果参数类型不能自动转换,则编译器会提示编译错误。如代码第09~11行所示,编译器就会分别调用不同参数类型的函数原型。

2.4.6 函数模板的定义和使用

C++语言是一种强类型语言,它要求对于每一个实现都有一个实例。如min()函数,它的int和double两种实例都要事先定义,这就给程序员带来很多不便。通过函数模板,允许程序员只定义一次函数的实现,即可使用不同类型的参数来调用该函数。函数模板提供了一种用来自动生成各种类型函数实例的算法,程序员只需要初始化其中部分参数和返回值,或者都不初始化,然后声明通用的类型即可,而函数体则不需要改变。

函数模板是C++语言新增的一种性质,它可以减小代码的书写的复杂度,同时也便于修改。

【实例2-24】由于项目的需要,开发人员经常要使用函数重载,例如swap()函数重载示例,代码如下。

01      void swap(string &a,string &b)                        //参数为字符串
02      {
03            string temp ;
04            temp = &a ;
05            &a = &b ;
06            &b = temp ;
07      }
08      void swap(int &a,int &b)                              //参数为整型
09      {
10            int temp ;
11            temp = &a ;
12            &a = &b ;
13            &b = temp ;
14      }
15      void swap(otherType &a,otherType &b)            //参数为其他类型(伪代码)
16      {
17            otherType temp ;
18            temp = &a ;
19            &a = &b ;
20            &b = temp ;
21      }

【代码说明】细心的读者可能会发现,这3个函数代码相同的地方非常多,而仅仅有两个地方不相同,即参数的类型声明和临时变量的类型。

因为临时变量是用来存放交换参数的,所以这3个函数代码本质上就只有一个不同点,即处理数据的类型不同。

【实例2-25】为了避免大量编写重复代码,C++提供了函数模板的功能。使用函数模板可以创建出不同版本的重载函数,而只需要编写代码一次。以swap()函数为例,函数模板的定义如下。

01       template<class SomeType>                        //函数模板定义格式
02       void swap(SomeType a,SomeType b)
03       {
04            SomeType temp ;
05            temp = a ;
06            a = b ;
07            b = temp ;
08       }

【代码说明】代码第01~02行是函数模板定义的语法形式。函数模板是以template<class Some Type>开头的,紧接着是函数的定义,而SomeType就是一个类型的代号,在实际调用函数模板时,会将这个类型代号替换成实际的类型。函数模板的调用跟普通函数的调用无异。

2.4.7 实例:成绩管理系统(3.0版)

为了进一步完善成绩管理系统(2.0版),本节将增强功能定位为修改系统源码,将系统源码功能模块化。这通过使用本节所介绍的知识是可以做到的,即使用函数。重新考虑成绩管理系统的如下3大功能。

·可以输入50位同学的姓名、语文、数学和英语成绩。

·可以输出50位同学的姓名、语文、数学和英语成绩。

·可以计算50位同学的语文、数学和英语平均成绩。

【实例2-26】使用本节介绍的函数,可以将以上3个功能都封装起来。先将成绩管理系统(2.0版)备份,然后复制一个备份文件作为成绩管理系统(3.0版)。完善后的代码如下所示。

01       #include <iostream>
02       using namespace std;
03       struct student_info                                          //定义学生数据对象的结构体
04       {
05        string strName;
06        int chinese;
07        int math;
08        int english;
09       };
10        student_info student[50];
11        void inputInfo()                                          //输入学生信息的函数定义
12        {
13            int i = 0 ;                                                //循环变量
14            char a[20];
15            for( i = 0 ; i < 50 ; i++ )
16            {
17                  cout<<"Enter the "<<i+1<<"student's information :"<<endl;
18                  cout<<"Enter name :"<<endl;
19                  cin>>a;
20                  student[i].strName = a;
21                  cout<<"Enter Chinese performance:"<<endl;
22                  cin>>student[i].chinese;
23                  cout<<"Enter Math performance:"<<endl;
24                  cin>>student[i].math;
25                  cout<<"Enter English performance:"<<endl;
26                  cin>>student[i].english;
27            }
28
29       void outputInfo()                                          //输出学生信息的函数定义
30       {
31            int i = 0 ;
32            for( i = 0 ; i < 50 ; i++ )
33            {
34                  cout<<"The "<<i+1<<"student's information :"<<endl;
35                  cout<<"Name :"<<student[i].strName.c_str()<<endl;
36                  cout<<"Chinese performance:"<<student[i].chinese<<endl;
37                  cout<<"Math performance:"<<student[i].math<<endl;
38                  cout<<"English performance:"<<student[i].english<<endl;
39            }
40        }
41       void computResult()                                    //计算结果的函数
42       {
43            int avgChinese = 0 ;
44            int avgMath = 0 ;
45            int avgEnglish = 0 ;
46            int total = 0 ;
47            int i = 0 ;
48            while(i<50)                                          //累加所有学生的语文成绩
49            {
50                  total += student[i].chinese;
51                  i++;
52            }
53            avgChinese = total / 50 ;
54            total = 0 ;
55            i = 0 ;
56            while(i<50)                                           //累加所有学生的数学成绩
57            {
58                  total += student[i].math;
59                  i++;
60            }
61            avgMath = total / 50 ;
62            total = 0 ;
63            i = 0 ;
64            while(i<50)                                           //累加所有学生的英语成绩
65            {
66                  total += student[i].english;
67                  i++;
68            }
69            avgEnglish = total / 50 ;
70            cout<<"Enter 1 for look up the average of Chinese.\n"
71                  <<"Enter 2 for look up the average of Math.\n"
72                  <<"Enter 3 for look up the average of English.\n";
73            cin>>i;
74            switch(i)                                          //判断输入,以输出不同科目的平均成绩
75            {
76            case 1:
77                  cout<<"Average of Chinese is : "<<avgChinese<<endl;
78                  break;
79            case 2:
80                  cout<<"Average of Math is : "<<avgMath<<endl;
81                  break;
82            case 3:
83                  cout<<"Average of English is : "<<avgEnglish<<endl;
84                  break;
85            default:
86                  cout<<"Please enter 1,2or3"<<endl;
87                  break;
88            }
89       }
90       int main()
91       {
92            inputInfo();                                                //输入数据
93            outputInfo();                                                //输出数据
94            computResult() ;                                          //计算全班各科目平均分
95            cin.get();
96            return 0;
97       }

【代码说明】成绩管理系统(3.0版)中的主函数已经最低限度精简了。如果把主函数看做是一个客户程序,就是调用其他函数为其服务的程序,而把其他函数称为服务程序,如代码第11行和第41行,那么从客户程序的角度来看,代码量越少越好,代码量越少,使用服务程序的客户程序开发人员的工作量就越小。由此设想,假设要设计一个函数库,那么这个函数库就是服务程序包,这个包越完善,则开发人员使用其进行软件开发就越方便。

注意 服务程序的功能并非越完善越好,理论上来说,一方面,服务程序不可能满足各种应用需要;另一方面,服务程序库越大,反而会增加开发人员学习使用该服务程序库的难度和成本。

2.5 类的定义和对象构造

本节将基于对象编程的理论来讲解,主要深入介绍类的定义和构造过程理论。读者通过学习本节,可以初步了解C++内核模型的基础理论。

2.5.1 自然界中的类型和C++的类定义

最初可以称为程序设计的语言是面向内存处理的,因此内存之间的存储转移等为开发人员的主题;而面向过程语言产生后,提出了模块化编程,于是将开发人员的注意力转移到了系统功能模块。

自然界的一切物体都可以看成是“对象”,如某个具体的人、房子、汽车和轮船等。注意,这里所说的对象是指“具体的”人或其他,如王小明是一个对象;而人类,则不能看做是对象,因为人类是一个抽象的概念,因此人类应该看成一个“类”。这样,对象和类的关系就很明确了,即王小明这个对象是具有人类这个类的特征的对象。

C++中,类的定义和对象的创建也遵循自然界法则,即类的定义不分配存储空间,也就是说,类没有生命;而对象创建是要分配存储空间的,也就是说,对象是有生命的。类的定义代码如下,延续前文的思路,定义了一个“人”类。

01      class CPeople
02      {
03         private:
04            …
05         public:
06            …
07      };

这里的人类是没有任何属性和行为的,类的名称使用默认的类名称前缀“C”。所谓的属性,是指类的数据成员;而行为是指类的成员函数,即类对象能够执行的操作。类的属性和行为所属的类访问控制域是没有限制的,即类的属性和行为可以随便被开发人员定义在私有域或公有域。原则上,类的数据成员(即类的属性)放在私有域中,类的成员函数(即类的行为)放在公有域中。

【实例2-27】重新考虑“人”类的属性,将人类的属性列举如下。

·姓名

·年龄

·性别

·身高

·体重

“人”类的行为列举如下:

·行走

·学习

·工作

·睡觉

·娱乐

那么,根据“人”类的属性和行为,在C++中可以将“人”类定义如下。

01      class CPeople
02      {
03          private :                        //类的私有成员
04             string strName ;
05             unsigned int age ;
06       int sex;                      // sex可以使用枚举类型表示,这里为了简洁,使用1表示男,0表示女
07             unsigned int height ;
08             unsigned int weight;
09         public :                         //类的公有成员
10             void walk() ;
11             void study();
12             void work();
13             void sleep();
14             void entertainment();
15      };

【代码说明】代码第10~14行分别定义的是人的行为,如行走、学习、工作、睡觉和娱乐,到目前为止,一个“人”类的定义基本上完成了,只是这个人是不完善的,具体如下。

·这个人类没有办法初始化(没有构造函数)。

·这个人类没有办法更改属性(没有私有属性修改的接口)。如果该人类生成的对象是一个具体的人,如王小明,而王小明除了姓名和性别不变外,其他属性每年都可能在变化。

·这个人类没有办法展示属性(没有私有属性展示的接口)。如王小明,其他人或对象没有办法知道王小明的姓名、身高等数据,因为外界不能直接访问类对象的私有属性,而应该通过王小明这个对象提供的返回私有属性的公有接口。

这个类的完善工作,本章将在以后的小节逐步进行介绍。

注意 类的定义是生成这种对象的前提,因此,考虑该类的属性和对外的接口是类定义的重点和难点。基本的原则是尽量让内部数据和操作私有化,提供简单易用的接口函数,尽量降低类与类之间的耦合度。

2.5.2 实现类成员函数

本小节将实现“人”类的成员函数,该类及其对象的创建将在Visual C++6.0环境中实现。

【实例2-28】在Visual C++6.0环境中创建实现“人”类的成员函数,具体步骤如下。

(1)创建一个新项目。

(2)创建一个Win32 Console Application程序,将项目命名为“Human”,如图2-13所示。

(3)创建空的Win32 Console Application程序。

(4)在项目工作区的源代码文件中创建新的源文件“People.cpp”,在头文件夹中创建新的头文件“People.h”,如图2-14所示。

图2-13 项目命名

图2-14 创建新的头文件和源文件

(5)将“人”类定义代码填写在头文件“People.h”中,代码如下。

01       #ifndef _PEOPLE_H_                  //预编译符号
02       #define _PEOPLE_H_
03       #include <string>
04       using namespace std;
05       class CPeople
06       {
07         private :                              //私有数据成员(私有段往往用于定义数据成员)
08            string strName ;
09            unsigned int age ;
10         int sex;                        // sex可以用枚举类型表示,为了简洁,使用1表示男,0表示女
11            unsigned int height ;
12            unsigned int weight;
13        public :                              //公有成员函数(公有段往往用于定义函数接口)
14            void walk() ;
15            void study();
16            void work();
17            void sleep();
18            void entertainment();
19       };
21       #endif

【代码说明】代码中第01、02和21行的预编译符号如下。

01      #ifndef _PEOPLE_H_                  //如果没有定义_PEOPLE_H_
02      #define _PEOPLE_H_                  //则定义_PEOPLE_H_
03      …                                          //类的头文件定义
04      #endif                                    // if宏结束

该预编译符号的语义是,如果没有定义_PEOPLE_H_,则定义_PEOPLE_H_,再定义“人”类,最后结束。其目的是防止因头文件中的多重包含而导致类的多次定义。

假设“main.cpp”有一个#include“People.h”,而在“People.cpp”文件中也有一个#include“People.h”,则class CPeople相当于被定义了两次。

注意 实例2-28代码第19行在一个类或结构体定义完后,后面有一个分号“;”,如果没有该分号,则编译会出现错误,而有些编译器也不能指出编译错误的准确位置。

(6)将“人”类的成员函数代码填写在源文件“People.cpp”中,代码如下。

01       #include <iostream>                  //包含输入输出流头文件
02       #include "people.h"                  //包含people.h头文件,该头文件有CPeople类定义
03       using namespace std ;                  //使用标准名称空间
04       void CPeople::walk()                  //实现walk()函数
05       {
06             cout<<"I am walking ! "<<endl;
07       }
08       void CPeople::sleep()                  //实现sleep ()函数
09       {
10              cout<<"I am sleeping ! "<<endl;
11       }
12       void CPeople::study()                  //实现study ()函数
13       {
14              cout<<"I am studying ! "<<endl;
15       }
16       void CPeople::work()                  //实现work ()函数
17       {
18              cout<<"I am working ! "<<endl;
19       }
20       void CPeople::entertainment()      //实现entertainment ()函数
21       {
22              cout<<"I am entertainmenting ! "<<endl;
23       }

【代码说明】如上述代码所示,读者可以以walk()函数为例(代码第04~07行所示),类的成员函数的实现格式如下。

01      返回值类型(如果是空则写void)类名 ::函数名(函数参数)
02      {
03            …                              //函数体
04      }

2.5.3 实例化类对象并使用

如前文所讲述,一个不怎么完善的“人”类已经定义完成了,如何在其他程序中使用这类呢?若要使用这个类,首先要生成这个类的对象。

【实例2-29】在main()函数中创建“人”类的对象,即具体的人。类对象的创建又称为类的实例化,步骤如下。

(1)在项目工作区中的源代码文件中创建新的源文件“main.cpp”。

(2)在源文件“main.cpp”中添加如下代码。

01      #include <iostream>
02      #include "People.h"
03      using namespace std;
04      int main()
05      {
06             CPeople firstPeople ;                  //定义一个“人”对象
07             firstPeople.work();                  //调用具体的人对象的公有函数,如work、sleep等
08             firstPeople.sleep();
09             firstPeople.walk();
10             firstPeople.entertainment();
11             firstPeople.study();
12             cin.get();
13             return 0;
14       }

【代码说明】上述代码中,第06行是CPeople类的实例化,创建了类的对象“firstPeople”;第07~11行调用了“firstPeople”对象的行为。

【运行效果】本实例代码运行结果如下。

01      I am working !
02      I am sleeping !
03      I am walking !
04      I am entertainmenting !
05      I am studying !

2.5.4 类的构造函数和析构函数

正如2.5.1小节所述,已经定义的“人”类具有不能初始化的缺点,即假设想做以下操作,编译器不通过该编译。

firstPeople.strName = "Wang Xiao ming" ;

因此,这个“人”类是不完善的,这样的类的属性定义是没有意义的。因此,“人”类的定义应该有一个构造函数。

【实例2-30】“人”类的定义中应该包含的构造函数代码如下。

01  #ifndef _PEOPLE_H_
02  #define _PEOPLE_H_
03  #include <string>
04  using namespace std;
05  class CPeople
06  {
07  private:
08      string strName;
09      unsigned int age;
10      int sex;                        // sex可以用枚举类型表示,为了简洁,使用1表示男,0表示女
11      unsigned int height ;
12      unsigned int weight;
13  public:
14      CPeople(string name , unsigned int Age,int Sex ,
15            unsigned int Height , unsigned int Weight);            //带参数的构造函数
16      CPeople();                                                            //不带参数的构造函数
17      void walk() ;
18      void study();
19      void work();
20      void sleep();
21      void entertainment();
22  };
23  #endif

【代码说明】上述代码第14、15和16行添加了两个构造函数,这两个是重载构造函数。构造函数有一个明显的特点,即没有返回值。事实上,构造函数也不需要返回值,因为没有其他函数或对象需要一个对象构造函数的返回值。

构造函数的实现部分代码如下。

01      CPeople::CPeople(string name , unsigned int Age,int Sex ,
02                  unsigned int Height , unsigned int Weight)            //带参数的构造函数的实现
03      {
04            strName = name ;
05            age = Age ;
06            sex = Sex ;
07            height = Height ;
08            weight = Weight ;
09      }
10      CPeople::CPeople()                                                      //不带参数的构造函数的实现
11      {
12            strName = "General people" ;
13            age = 20 ;
14            sex = 1 ;
15            height = 175 ;
16            weight = 70 ;
17      }

注意 第一个重载的构造函数中,小写字母开头的age是类的属性,而大写字母开头的Age是类构造函数的形式参数,其他参数定义与此类似。

【代码说明】由于这两个重载的构造函数是类的成员函数,所以使用构造函数来访问类的私有数据成员是没有问题的。

在main()函数中可以调用如下两个版本的对象创建方式。

01      CPeople firstPeople ;
02      CPeople secondPeople("Wang Xiao Ming" , 28 , 1 , 178 , 78 );

【代码说明】第01行创建了一个没有参数的对象,即名字为“General people”的人对象firstPeople,对应调用的构造函数是CPeople::CPeople();第02行创建了一个名字为“Wang Xiao Ming”的人对象secondPeople,对应调用的构造函数是CPeople::CPeople(string name,unsigned int Age,int Sex,unsigned int Height,unsigned int Weight)。

使用类的构造函数并在函数体内初始化是个好办法,但这个办法并不是在任何时候都是万能的。假设将sex属性定义为常量,即将一个人的性别看成自出生到死亡都是不变的,代码如下。

const int sex ;          //性别是从类对象生成到销毁都不变的。

如果是这样定义,则前文中的两个重载版本的构造函数都不能编译通过,原因在于如下两条语句操作对常量来说是不允许的。

01      sex = Sex ;
02      sex = 1 ;

常量不能作为赋值号的左操作数是个基本常识。C++有一个特殊的类初始化方法,即成员初始化列表法。改写类的构造函数实现如下。

01      CPeople::CPeople(string name , unsigned int Age,int Sex ,
02                              unsigned int Height , unsigned int Weight)
03                               :strName(name),age(Age),sex(Sex),height(Height),weight(Weight)
04                               //带参数的构造函数的成员初始化列表实现
05      {
06      }
07      CPeople::CPeople()
08      :strName("General people"),age(20),sex(1),height(175),weight(70)
09                               //不带参数的构造函数的成员初始化列表实现
10      {
11      }

注意 变量的初始化和赋值可以写成object=2或object(2)两种形式;在成员初始化列表方法中只能使用object(2)。该方法是一种初始化的方法,因此,作为常量的sex也可以顺利初始化。

【代码说明】上述代码第01~04行和第07~09行分别给出了两种类构造函数初始化的方法。

类的析构函数定义如下。

~CPeople();

析构函数与构造函数相比,函数名前多了一个~符号,该符号也是析构函数名的一部分。除了该符号外,析构函数也要和类的名称相同。由于“人”类中没有对内存做动态内存分配,因此在该类中,析构函数的函数体可以为空或不定义析构函数。

2.5.5 运算符重载与this指针的使用

运算符重载实质上也是函数的重载。使用人类对象来进行+、-、*和/的运算符重载似乎不符合情理,因此,本小节暂且放下对“人”类的研究,使用一个自定义的向量类。

【实例2-31】为了简单起见,该类的成员函数暂时只定义两个重载的构造函数。该向量类定义如下。

01      class myVector                                          //自定义向量类
02      {
03      private:
04            int x;
05            int y;
06            int z;
07      public:
08            myVector();                                          //定义一个无参数的构造函数
09            myVector(int X,int Y,int Z);                  //定义一个带参数的重载的构造函数
10      };
11      myVector::myVector()                                    //不带参数的构造函数实现
12      {
13            x = 0 ;
14            y = 0 ;
15            z = 0 ;
16      }
17      myVector::myVector(int X, int Y, int Z)            //带参数的构造函数实现
18      {
19            x = X ;
20            y = Y ;
21            z = Z ;
22      }

【代码说明】该类和2.5.4小节介绍的“人”类定义类似,这些代码比较简单,第11~22行定义了两个重载的构造函数。现要运行以下代码。

01            myVector Vec1(4,5,8);
02            myVector Vec2(8,9,6);
03            myVector Vec3;
04            Vec3 = Vec1 + Vec2 ;

这时,编译器会提示错误,原因在于,+号没有办法认识到它左、右两边的Vec1和Vec2对象。根据向量的加减法规则可知,两个向量的和或差等于两个向量所有分量的和或差。即向量a={12,56,7},b={21,4,58},则a+b={12+21,56+4,7+58}={33,60,65},减法为a-b={12-21,56-4,7-58}={-9,52,-51}。

重载运算符函数的定义格式如下。

返回值 operator
运算符(
右操作数) ;

因此可以定义“myVector”类的加号和减号重载运算符如下。

myVector operator+(myVector V);

该重载运算符实现如下。

01       myVector myVector::operator+(myVector V)
02       {
03            myVector temp;
04            temp.x = this->x + V.x ;            // this表示本对象的意思,就是本对象的x+参数的x
05            temp.y = this->y + V.y ;             //赋值给temp对象的x
06            temp.z = this->z + V.z ;
07            return temp;
08       }

注意 重载运算符函数的形式看起来有点古怪,读者可以将其理解为函数名是operator+的成员函数。

【代码说明】代码第04~06行中出现了一个this符号,该符号是一个指针,该指针指向当前的实例对象。这样可能不好理解,先看如下代码。

Vec3 = Vec1 + Vec2 ;

当调用该语句时,实际上相当于如下语句。

Vec3 = Vec1.operator+(Vec2)  ;

Vec1对象调用了operator+()函数,而this指针就是指向Vec1的指针,该指针类型就是myVector类型。Vec2就是operator+()函数的参数,而函数体内的temp作为返回值返回,并赋值给了Vec3。

同理可得减法的重载运算符。

2.5.6 友元函数和友元类

2.5.5小节中定义了一个自定义向量类myVector,同时也实现了该向量类的加减法重载运算符,但到目前为止读者都没有看到运行效果。因此,本小节将通过再重载一个输出流cout的运算符<<来让读者看到效果。

【实例2-32】重载一个输出流cout的运算符<<,具体步骤如下。

(1)创建一个新项目。

(2)创建一个Win32 Console Application程序,将项目命名为“VectorTest”,如图2-15所示。

(3)创建空的Win32 Console Application程序。

(4)在项目工作区中的源代码文件中创建新的源文件“Vector.cpp”,在头文件夹中创建新的头文件“Vector.h”,如图2-16所示。

图2-15 项目命名

图2-16 创建新的头文件和源文件

(5)将“人”类定义代码填写在头文件“Vector.h”中,具体如下。

01      #ifndef _MYVECTOR_H_
02      #define _MYVECTOR_H_
03      #include <string>                                                      //包含字符串类的头文件
04      #include <iostream>                                                //包含输入输出流的头文件
05      using namespace std;                                                //使用标准名称空间
06      class myVector                                                      //自定义向量类
07      {
08      private:
09              int x;
10              int y;
11              int z;
12       public:
13              myVector();
14              myVector(int X,int Y,int Z);
15              myVector operator+(myVector V);                                 //重载加号运算符声明
16              friend ostream& operator<<(ostream& out , myVector V); //重载输出流运算符声明
17       };
18      #endif

【代码说明】代码第16行是类的友元函数的定义。友元函数不是类的成员函数,它不属于类的成员,而是一个独立的函数。但对该类来说,该友元函数可以访问类内的所有成员,包括私有成员,因为myVector已经把该函数看成是“朋友”。

编程陷阱 友元函数是指函数可以访问该类,反之则不然,这个倒不容易出错,因为函数毕竟是可以自由调用的。但在使用友元类时要注意,友元定义是单向的,可以这样简单理解:A将B看成朋友,而B未必将A看成朋友。

定义输出流对象的<<是一个二元运算符,即与加号类似,可以连接两个对象。因此在友元函数中定义两个参数,一个参数是左操作数输出流对象,一个是被输出的对象myVector,从而定义就可以使用这样的输出了,代码如下。

cout<<Vec3;

这时,<<的两边分别就是这个友元函数的两个参数。

(6)将“人”类的成员函数代码填写在源文件“Vector.cpp”中,代码如下。

01      myVector::myVector()
02      {
03            x = 0 ;
04            y = 0 ;
05            z = 0 ;
06      }
07      myVector::myVector(int X, int Y, int Z)
08      {
09            x = X ;
10            y = Y ;
11            z = Z ;
12      }
13      myVector myVector::operator+(myVector V)                        //重载加号运算符实现
14      {
15            myVector temp;
16            temp.x = this->x + V.x ;
17            temp.y = this->y + V.y ;
18            temp.z = this->z + V.z ;
19            return temp;
20      }
21      ostream& operator <<(ostream& out , myVector V)                  //重载输出流运算符实现
22      {
23            out<<"( "<<V.x<<" , "<<V.y<<" , "<<V.z<<" )"<<endl;
24            return out;
25      }

【代码说明】第13~20行重载了加号运算符,第21~25行重载了输出流运算符。

(7)在项目工作区的源代码文件中创建新的源文件“main.cpp”,源文件“main.cpp”中添加的代码如下。

01      #include <iostream>
02      #include "Vector.h"
03      using namespace std;
04      int main()
05      {
06            myVector Vec1(4,5,8);            //定义Vec1向量对象,调用的是带参数的构造函数
07            myVector Vec2(8,9,6);            //定义Vec2向量对象,调用的是带参数的构造函数
08            myVector Vec3;                  //定义Vec3向量对象,调用的是无参数的构造函数
09            Vec3 = Vec1 + Vec2 ;            // Vec1调用加号运算符重载函数
10            cout<<Vec3;                        // cout对象调用<<输出重载运算符
11            cin.get();
12            return 0;
13      }

【代码说明】第06~08行定义了3个myVector向量对象,其中第3个没有参数。第09~10行调用的都是重载后的运算符。

【运行效果】输出结果如下。

(12,14,14)

该结果符合Vec1和Vec2的向量运算法则的结果。

友元类和友元函数类似,也是相对于某个类而言,如果A类定义为B类的友元类,则A类的任意一个成员函数都可以访问B类的所有私有或公有的成员函数或数据成员,代码如下。

01      class B
02      {
03         friend A;
04         …
05      };

反过来却不成立,即B类对A类来说,B类不是A类的友元类,因此B类仍然不能访问A类的私有成员。因此可以看出,友元类的定义不具有相互性。

注意 之所以要给向量或其他自定义类类型对象增加运算符重载,是因为在自然界中很多类对象是可以相加或进行其他运算的。而这些类的类型种类繁多,结构各异,因此C++编译器不可能事先预料到用户会定义哪些类型,所以需要用户自己定义,以符合该类型的运算需要。

2.5.7 实例:成绩管理系统(4.0版)

虽然成绩管理系统(3.0版)已经很完美了,事实上,经过到2.4节为止的学习,读者已经可以编写任何程序,因为面向过程的部分到2.4节已经介绍完,但面向对象的程序设计也是必不可少的,因此,本小节会将成绩管理系统(3.0版)进一步升级到成绩管理系统(4.0版),也可以称为基于对象程序设计的版本。

【实例2-33】由于成绩管理系统(4.0版)是面向对象的第一个项目,而面向对象编程和面向过程编程的思路是截然不同的,因此重新创建成绩管理系统的项目文件。创建过程如下。

(1)创建新项目。

(2)创建一个Win32 Console Application程序,并将项目命名为“Performance4”,如图2-17所示。

图2-17 命名项目

(3)在工作区源文件夹中新建“main.cpp”和“student.cpp”源文件,在头文件夹中新建“student.h”头文件。

(4)定义CStudent类,将如下代码填写入“student.h”头文件。

01      #ifndef _STUDENT_H_
02      #define _STUDENT_H_
03      #include <string>
04      using namespace std;
05      class CStudent
06      {
07      private:
08              string strName ;
09              double chinese ;
10              double math;
11               double english;
12        public:
13              CStudent();                                                            //无参数的构造函数
14              CStudent(string Name,double c,double m,double e);      //带参数的构造函数
15              CStudent operator+(CStudent S);                              //加号运算符重载
16            friend ostream& operator<<(ostream& out , CStudent S);//友元运算符<<重载
17              void SetChinese(double a);                                    //重新设置语文成绩
18              void SetMath(double a);                                          //重新设置数学成绩
19              void SetEnglish(double a);                                    //重新设置英语成绩
20              string returnName();                                                //获取姓名
21              double returnChinese();                                          //获取语文成绩
22              double returnMath();                                                //获取数学成绩
23              double returnEnglish();                                          //获取英语成绩
24              double returnTotalPerformance();                              //获取3门功课总分
25              double returnAverage();                                          //获取3门功课平均分
26              ~CStudent(){}
27        };
28        #endif

【代码说明】代码第05~27行定义了CStudent类,第08~11行是类的内部变量。第13~25行是类的公共方法,第26行是类的析构函数。

(5)实现CStudent类的成员函数,将如下代码填写入“student.cpp”头文件。

01      #include <iostream>
02      #include "student.h"
03      using namespace std;
04      CStudent::CStudent()
05      {
06            strName = "General Student";      //在构造函数体中初始化各数据成员
07            chinese = 0 ;
08            math = 0 ;
09            english = 0 ;
10      }
11      CStudent::CStudent(std::string Name, double c, double m, double e)
12      {
13            strName = Name ;                        //通过参数在构造函数体中初始化各数据成员
14            chinese = c ;
15            math = m ;
16            english = e ;
17      }
18      CStudent CStudent::operator +(CStudent S)//定义重载的+函数,使之能将两同学的成绩直接相加
19      {
20            CStudent temp ;
21            temp.strName = "Total Performance";
22            temp.chinese = this->chinese + S.chinese;
23            temp.math = this->math + S.math ;
24            temp.english = this->english + S.english;
25            return temp;
26      }
27      ostream& operator <<(ostream& out ,  CStudent S)      //定义重载的<<运算符
28      {
29            out<<S.strName<<"( "<<S.chinese<<" , "<<S.math<<" , "<<S.english<<" )"<<endl;
30            return out;
31      }
32      void CStudent::SetChinese(double a)                  //重新设置语文成绩实现
33      {
34            chinese = a;
35      }
36      void CStudent::SetEnglish(double a)                  //重新设置英语成绩实现
37      {
38            english = a;
39      }
40      void CStudent::SetMath(double a)                        //重新设置数学成绩实现
41      {
42            math = a;
43      }
44      double CStudent::returnChinese()                        //返回语文成绩
45      {
46            return chinese ;
47      }
48      double CStudent::returnEnglish()                        //返回英语成绩
49      {
50            return english ;
51      }
52      double CStudent::returnMath()                              //返回数学成绩
53      {
54            return math ;
55      }
56      double CStudent::returnTotalPerformance()            //返回个人总成绩
57      {
58            return chinese + math + english ;
59      }
60      double CStudent::returnAverage()                        //返回个人平均成绩
61      {
62            return (chinese + math + english)/3;
63      }
64      string CStudent::returnName()                              //返回个人姓名
65      {
66            return strName;
67      }

【代码说明】代码第06~09行首先为4个变量赋值,第27~31行重载输出运算符,第32~43行实现设置功能,第44~67行实现返回功能。

编程陷阱 在重载定义<<运算符时,应使用友元定义,并且将ostream&out作为形式参数。

(6)编写客户程序“main.cpp”。

01      #include <iostream>
02      #include "student.h"
03      using namespace std;
04      int main()
05      {
06            CStudent student1("XiaoMing",50,80,98);
07            CStudent student2("XiaoHua",70,78,68);
08            CStudent student3("NingJian",86,78,83);
09            //显示各同学的单科成绩
10            cout<<student1<<endl;                              //调用重载的输出运算符
11            cout<<student2<<endl;
12            cout<<student3<<endl;
13            //重新设置成绩
14            student1.SetChinese(56);
15            student2.SetEnglish(87);
16            student3.SetMath(87);
17            //计算不同学生同学科的总成绩
18            CStudent General ;
19            General = student1+student2+student3;
20            cout<<General;
21            //查看多个人的各科目平均成绩
22            cout<<"Average of Chinese :"<<General.returnChinese()/3
23                  <<"\nAverage of Math :"<<General.returnMath()/3
24                  <<"\nAverage of English :"<<General.returnEnglish()/3<<endl;
25            //查看个人平均成绩
26            cout<<"Average of students : \n";
27            cout<<student1.returnName()<<" : "<<student1.returnAverage()<<endl;
28            cout<<student2.returnName()<<" : "<<student2.returnAverage()<<endl;
29            cout<<student3.returnName()<<" : "<<student3.returnAverage()<<endl;
30            //查看个人总成绩
31            cout<<"Total of students : \n";
32            cout<<student1.returnName()<<" : "<<student1.returnTotalPerformance()<<endl;
33            cout<<student2.returnName()<<" : "<<student2.returnTotalPerformance()<<endl;
34            cout<<student3.returnName()<<" : "<<student3.returnTotalPerformance()<<endl;
35            cin.get();
36            return 0 ;
37      }

【代码说明】第06~08行初始化3个同学对象,第10~12行显示各同学的单科成绩,第14~16行设置成绩,第18~20行计算不同同学同学科的总成绩,第31~34行查看个人总成绩。

注意 面向对象的编程主要将精力集中在类的设计方面,包括类的属性和接口,而原则就是尽量使客户程序使用方便,合乎该对象现实意义的使用法则。

2.6 类继承

本节开始将正式介绍面向对象的本质,即类继承。2.5节所介绍的类的定义和创建,实质上是基于对象的部分,并没有完全涵盖面向对象的本质问题。自然界中的对象是有一般性和特殊性的,同时,生物界也具有繁殖和变异的普遍现象,因此,仅仅单独、孤立地定义类和创建对象,并不符合自然界的法则。

2.6.1 is-a关系

自然界中的类具有相似性和归属性。所谓相似性,就是类和类之间具有相似的特性,例如同属于灵长目中的动物金丝猴、大猩猩和人类,这3类动物的行为具有一定的相似性,表现在前爪(手)能灵活地抓东西,喜欢群居等;这3类动物的属性也具有一定的相似性,比如具有四肢,头脑容量较大等。

假设有简单的关系,即人类和动物类,很显然,人类属于动物类。因此,可以说人类is-a动物类。这种从属关系在自然界中是普遍存在的,而C++中以公有继承来表现这种称为“is-a关系”的从属关系。

公有继承的语法格式如下。

01      class CHuman : public CAnimal
02      {
03         …                    //类定义体
04      };

在这个关系中,可以说是人类继承了动物类,或者说动物类派生了人类,也可以说人类从动物类中派生出来。

其中,动物类具有如下属性。

·生命健康状态。可以用int整型表示,以10为生命最佳状态,0为死亡。

·年龄。

·体重。

·性别。

动物类具有如下行为。

·会行走。

·会生育。

·会呼吸。

人类具有如下属性。

·生命健康状态。可以用int整型表示,以10为生命最佳状态,0为死亡。

·年龄。

·体重。

·性别。

·具有与一般动物类不同的属性,即姓名。

人类具有如下行为。

·会行走。

·会生育。

·会呼吸。

·具有与一般动物类不同的行为,即会使用语言。

在面向对象的设计中,特别是涉及类继承的面向对象设计方法,一般都使用UML(统一建模语言)。动物类和人类的关系可以如图2-18所示,即为动物类和人类的UML类图。

图2-18 动物类和人类的关系

如图2-18所示,一个类图中的类由3个矩形组成,第1个矩形是类名;第2个矩形是类的数据成员或特征,“-life:int”表示life变量是int型,“-”表示属于private域;第3个矩形表示类的成员函数或操作,“+walk():void”表示walk()函数返回值是void,“+”表示属于public域。由于CHuman类具有CAnimal类的属性,因此可以不将CHuman类中从CAnimal类的属性画出,只画出自身特有的属性和方法即可。

类与类之间的继承关系可用一个三角箭头表示,在UML中,这个三角箭头符号又称为“泛化”关系,即由被泛化的类指向泛化的类,也就是说,由子类指向父类。因为父类具有比子类更抽象的公共性质,所以称为“泛化”。

注意 在使用UML画继承关系图时,三角箭头的方向比较容易弄错,应该是继承类(子类)指向被继承的类(父类)。

【实例2-34】定义动物类,代码如下。该代码可以保存在“animal.h”文件中。

01      #ifndef _ANIMAL_H_
02      #define _ANIMAL_H_
03      class CAnimal                                    // CAnimal类的定义
04      {
05      private:
06            int lift ;                              //生命健康状态
07            int age ;                              //年龄
08            int sex ;                              //性别
09      public:
10            void walk();                              //走的行为
11            void birth();                              //生育的行为
12            void breath();                        //呼吸的行为
13      };
14      #endif

【代码说明】代码第03~13行定义了类CAnimal,其中包括3个公共行为,如代码第10~12行所示。

注意 从类定义来看,符合人类的一些基本特征,比如年龄、性别和健康状态,作为私有成员,而各种与外界进行交互的动作,则作为公有函数。

人类可以定义为如下代码。该代码可以保存在“human.h”文件中。

01      #ifndef _HUMAN_H_
02      #define _HUMAN_H_
03      #include"animal.h"                  //包含animal.h头文件,该头文件有动物类定义
04      #include<string>                        //包含string类的头文件
05      class CHuman:public CAnimal      // CHuman类以公有继承的方式派生于CAnimal
06      {
07      private:
08            std::string name ;            //新增姓名属性name
09      public:
10            void talk();                        //新增行为方式语言交流
11      };
12      #endif

【代码说明】代码第05行表示CHuman类以公有继承的方式派生于CAnimal,所以必须添加第03行代码来包含“animal.h”头文件。代码第08行是CHuman特有的姓名属性,第10行是CHuman特有的行为。

到目前为止,相信读者可以很轻松地完成这两个类的实现,有兴趣的读者还可以以此为参考作适当扩展,以编写一个完整的游戏,具体如下。

·以CHuman为基础派生出各类的人,如农民、战士和将军等。

·在农民、战士和将军类中设置各种新的属性,如生命值、武力值、所忠诚的国家及所在的位置坐标等。

·可以设置若干个国家,以此进行战争。

注意 这个游戏逻辑设计上并不困难,但需要其他辅助技术来完善游戏效果,如图像处理、AI基础等。

2.6.2 多态公有继承

本小节将介绍多态公有继承,内容较多,主要分为不同类型元素的数组以及虚拟函数和动态联编两部分。

1.不同类型元素的数组

客户类是可以定义对象数组的,例如如下代码。

01      class CHuman ;
02      CHuman human[100] ;

该代码在此定义了一个human对象数组,该数组的全部元素都是一个CHuman类的对象。使用数组来处理大量同类型数据的确非常方便,但使用数组也有非常不方便的地方,就是在同一个数组中不能存放不同类型的元素。例如定义一个整型数组,则数组的全部元素都必须是整型;定义一个CHuman类对象的数组,则数组元素必须全是CHuman类对象。

有什么办法使数组中具有不同类型的元素呢?方法是有的,就是使用类继承链中的指针。例如定义一个指向CAnimal的指针和一个指向CHuman的指针,代码如下。

01      CAnimal* panimal ;                  //定义一个指向CAnimal类对象的指针panimal
02      CHuman* phuman ;                        //定义一个指向CHuman类对象的指针phuman

则又会有如下指针赋值情况。

01      CAnimal animal ;                        //定义一个animal对象
02      CHuman human ;                        //定义一个human对象
03      panimal = &animal ;                  //正确
04      panimal = &human ;                  //正确,基类指针可以指向派生类对象
05      phuman = &animal ;                  //不正确,派生类指针不能指向基类对象
06      phuman = &human ;                        //正确

指针赋值的情况表明,派生类对象是可以隐式转换为基类对象的,反之则不然。原因在于,派生类对象转换为基类对象时,多余的属性可以丢弃,但基类对象如果转换为派生类对象,则不可能无故多出派生类对象自定义的属性部分。

因此,如果采用如下定义,则可以实现数组中存储不同类型的元素。

CAnimal* array[100] ;                        //定义array数组,数组元素类型为“CAnimal*”
array[0] = new CAnimal ;                  // array[0]指向CAnimal类对象
array[1] = new CHuman ;                  // array[1]指向CHuman类对象
array[2] = new CHuman ;                  // array[2]指向CHuman类对象
…;                                                // array数组其他元素赋值情况
array[99] = new CAnimal ;                  // array[99]指向CAnimal类对象

2.虚拟函数和动态联编

虚拟函数又称为虚函数,是C++面向对象重要的理论知识部分。与虚函数相对的暂且称为“普通函数”。虚函数的定义与“普通函数”相比,只是在函数前面加了一个virtual关键字,定义格式如下。

virtual 返回值 函数名(参数列表 ) ;

虚函数和普通函数除了定义形式不同,区别还在于如下方面。

·普通函数的多态以重载函数来表项,而虚函数的多态有重载函数和动态联编两种。

·具有虚函数的类对象要维护一个“虚函数表”,用于保证动态联编的正确性,因而存储开销和运行开销较大;而没有虚函数的类对象则存储和运行开销都较小。

注意 既然虚拟函数能实现普通函数的全部功能,为什么C++标准不将类内的函数定义为默认都是虚函数呢?那是因为C++之父Bjarne Stroustrup始终强调一个观点,就是C++不使用会带来额外开销的特性。

【实例2-35】分析如下语句。

01        int a ;
02        for( int i = 0 ; i < 100 ; i++ )
03        {
04            cout<<"Enter 1 for new CAnimal , 2 for new CHuman : \ n" <<;
05            cin>>a ;
06            if(a==1)
07                array[i] = new CAnimal ;
08            else if(a==2)
09                array[i] = new CHuman ;
10           array[i].walk();                 //调用哪个类的walk()函数
11        }

【代码说明】代码第10行中调用了数组元素array[i]的walk()函数,但在程序编译并生成执行代码后,仍然无法知道具体是调用哪个类的walk()函数,因为array[i]指向的对象要通过用户输入数字“1”或“2”来进行指定。这样理解对吗?是不对的。因为walk()函数不是虚函数,所以调用的方法是指针类型的方法,array[100]都是指向CAnimal类型的指针,即调用CAnimal::walk(),这种方式调用的函数在编译时就可以确定。假设有如下定义。

virtual void walk() ;

将walk()定义为虚拟函数之后,通过array[i]调用的函数是指针指向的对象的函数,这种方式调用的函数在运行时才能确定。因此可以引出如下两个概念。

·静态联编,又称为早期联编,不使用虚函数,在编译时就确定了函数调用版本的联编方式。

·动态联编,又称为晚期联编,使用虚函数,在运行时才可以确定函数调用版本的联编方式。

【实例2-36】动态联编实例,代码如下。

01      CAnimal* array[100] ;                        // 定义array数组,数组元素类型为“CAnimal*”
02      array[0] = new CAnimal ;                     // array[0]指向CAnimal类对象
03      array[1] = new CHuman ;                      // array[1]指向CHuman类对象
04      array[2] = new CHuman ;                      // array[2]指向CHuman类对象
05      …;                                          // array数组其他元素赋值情况
06      array[99] = new CAnimal ;                    // array[99]指向CAnimal类对象
07      //静态联编方式:
08      void walk() ;                                // 函数定义
09      array[0].walk();                             // 调用CAnimal::walk()
10      array[1].walk();                             // 调用CAnimal::walk()
11      array[2].walk();                             // 调用CAnimal::walk()
12      //动态联编方式:
13      virtual void walk() ;                        // 函数定义
14      array[0].walk();                             // 调用CAnimal::walk()
15      array[1].walk();                             // 调用CHuman::walk()
16      array[2].walk();                             // 调用CHuman::walk()

【代码说明】使用虚函数的动态联编实现多态公有继承是面向对象编程的理论重点,它即实现了使用数组来管理不同的对象,又实现了调用具体对象的虚拟函数。

编程陷阱 使用动态联编,除了会带来额外开销外,有时还会弄晕开发者,因此在使用或调试时,应小心跟踪调用栈。

2.6.3 protected访问控制

到目前为止,本书还没有提到protected域,protected域用于在继承链中的访问控制。实际上,如果单纯创建若干个互不相干的类,则完全不必要使用protected域。

前文已经定义了两个类,即CHuman和CAnimal类。虽然这两个类是“父子关系”,但还远没有友元的“朋友关系”那么亲密。理由在于友元类(或友元函数)可以访问被友元类的私有域成员,而CHuman和CAnimal类并不能相互访问私有域成员。

【实例2-37】如何做到父类的私有域成员不被外界直接访问,又能保证子类能直接访问呢?使用protected访问控制符即可。protected域(保护域)的成员对外界来说相当于private域的,而对子类来说,又相当于public域的。代码如下。

01      ////////////          animal.h           //////////////////
02      #ifndef _ANIMAL_H_
03      #define _ANIMAL_H_
04      class CAnimal                                    // CAnimal类的定义
05      {
06      private:
07            int lift ;                              //生命健康状态
08            int age ;                              //年龄
09            int sex ;                              //性别
10      protected:
11          int other ;                              //其他属性
12      public:
13            void walk();                              //行走的行为
14            void birth();                              //生育的行为
15            void breath();                        //呼吸的行为
16      };
17      #endif
18      ///////////             main.cpp           //////////////
19      #include "animal.h"
20      #include "human.h"
21      int main()
22      {
23           CHuman human1 ;
24           human1.other ;                        //错误,在main()函数中不可以调用保护域成员
25      }
26      ///////////////         human.cpp          /////////////
27      #include "human.h"
28      void CHuman::talk()
29      {
30            other = 0 ;                              //正确,子类可以访问父类的protected域成员
31            age = 0 ;                              //错误,子类不可以访问父类的private域成员
32      }

【代码说明】代码第04~16行定义了CAnimal类的属性和行为,其中第10行使用了protected关键字定义other属性。代码第24行演示在main()函数中调用保护域成员的操作。第30~31行用一个正确的和一个错误属性引用方式,帮助读者明白protected关键字的意义。

编程陷阱 对于无继承关系的类而言,保护域就等于私有域;而对于子类而言,父类的保护域就是公有域。在类设计时应该记牢这个关系,并注意相关事项。

2.6.4 抽象基类

前文所讲述的类都是可以实例化的,也是可以被继承的。本小节将介绍一种只能被继承而不能实例化的类,即抽象类。由于只能被继承,因此一般称为抽象基类(Abstract Base Class)。

【实例2-38】定义一个抽象基类很简单,在其中一个虚函数定义的后面添加一个“=0”即可。例如要把CAnimal类定义为抽象基类,代码如下。

01      #ifndef _ANIMAL_H_
02      #define _ANIMAL_H_
03      class CAnimal                                    // CAnimal类的定义
04      {
05      private:
06              int lift ;                              //生命健康状态
07              int age ;                              //年龄
08              int sex ;                              //性别
09       protected:
10            int other ;                              //其他属性
11       public:
12              virtual void walk()=0 ;            //行走的行为
13              void birth();                              //生育的行为
14              void breath();                        //呼吸的行为
15       };
16       #endif

【代码说明】第12行代码中具有“=0”符号的虚拟函数称为“纯虚函数”。因此也可以这样定义:具有纯虚函数的类称为抽象基类。一旦将一个类定义为抽象基类,该类就不可以实例化,如下。

CAnimal monkey ;                                   //错误!抽象基类不能实例化。

为什么要在C++标准中作如此规定?抽象基类有什么作用呢?理由是为了强制规定接口,让子类一定要改写该接口。如果不改写该虚拟函数,则该虚拟函数仍然原样被子类继承,因此子类仍然是一个抽象类,也就不能被实例化。因此,抽象类的主要职能就是定义纯虚拟函数接口,提醒它的子类继承它的时候要改写这些纯虚拟函数接口。

注意 继承抽象基类时,如果想让该类生成对象,一定要重写抽象基类中的全部纯虚函数。

2.6.5 私有继承和保护继承

前文已经详细介绍了公有继承,本小节将介绍私有继承和保护继承。继承的语法格式如下。

01      class CHuman : private CAnimal
02      {
03      …; //类定义体
04      }
05      class CHuman : protected CAnimal
06      {
07      …; //类定义体
08      }

它们跟公有继承的区别如下。

·公有继承:基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态。

·私有继承:基类的公有成员和保护成员都作为派生类的私有成员。

·保护继承:基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问。

这3种继承方式也有共同点,即不能访问基类的私有域成员。

2.6.6 多继承

有两个或两个以上的基类的继承关系称为多继承。图2-19所示是一个简单的多继承示例。

如图2-19所示,“技术人才”类和“管理人才”类是相互之间没有关联的类,而“技术和管理人才”类多继承自“技术人才”和“管理人才”,具有“技术人才”和“管理人才”的所有特性。而多继承应该遵循的原则是继承链不能有环,即如果将类图看成是一个有向图,则该有向图是可进行拓扑排序的。例如图2-20所示的类图是合法的,因为该多继承链无环,即是可拓扑排序的。

图2-19 简单的多继承

图2-20 复杂的多继承

例如图2-21所示,该类图是不合法的,因为该多继承链有环,即不能进行拓扑排序。

图2-21 不合法的类图

注意 多继承容易引起二义性,即同名同参数函数接口调用的二义性,该专题超出了本书范围,有兴趣的读者可以查阅相关资料进行更深入研究。

2.6.7 类模板

【实例2-39】与函数模板类似,类也可以定义成类模板。类模板的使用示例代码如下。

01      /////////          stack.h              /////////////////
02      #ifndef _STACK_H_
03      #define _STACK_H_
04      template <class type>
05      class myStack                                    //定义myStack模板类,注意定义格式
06      {
07      private:
08            type object[10];                        // object数组的元素类型未确定,推迟到创建时确定
09      public:
10            void push(type& e);
11            void pop();
12      };
13      #endif
14      /////////          stack.cpp              /////////////////
15      #include"stack.h"
16      template <class type>
17      void myStack<type>::push(type& e)      //实现模板类的函数,注意定义格式
18      {
19            ;
20      }
21      template <class type>
22      void myStack<type>::pop()
23      {
24            ;
25      }
26      /////////          main.cpp              /////////////////
27      #include "stack.h"
28      int main()
29      {
30            myStack<int> s;                        //创建模板类的对象,注意创建格式
31            return 0;
32      }

【代码说明】代码第04~12行首先定义一个类模板。代码第17~25行实现类模板的函数。代码第30行创建类模板对象。

注意 模板类的使用在C++语言中非常常见,如STL就是标准模板库,STL以模板库的方式向研发人员提供基本的类库。

2.7 C++异常机制

程序在运行时可能会遇到运行阶段错误,导致程序无法继续运行,例如程序试图打开一个不可用的文件或请求过多的内存。一般情况下,程序员都会尽量在编码时避免这类情况的发生,但有时在编码时却不一定能防止。本节将主要介绍C++异常机制,详细介绍C++异常机制的使用方法。

2.7.1 异常处理类型

在算术表达式中,除数不能为0是基本常识,但很多情况下,除数是否为0在编译时是未知的。例如如下表达式。

a = 2*x*y/(x+y) ;

其中,x和y变量是用户输入或经过计算的返回结果,则该表达式的除数“x+y”是否为0是未知的,而在运行时,很可能会遇到x是y的相反数的情况,这时“x+y”就为0了。常用的异常处理有两种类型,即调用abort()使异常终止以及使用异常机制进行异常处理。

1.调用abort()使异常终止

调用abort()是一种异常终止的常用处理方法,它返回一个随实现而异的值,并告诉操作系统某处理失败了。例如如下代码。

01      double result(double x , double y )
02      {
03            if( x == -y )
04            {
05                  std::cout<<"Unable to comput the result! \n" ;
06                  abort();                                    //调用abort()处理异常终止
07            }
08            return 2.0 * x * y / ( x + y ) ;
09      }

2.使用异常机制进行异常处理

对异常的处理有3部分。分别为引发异常、捕获异常、处理异常。

【实例2-40】使用异常机制进行异常处理,代码如下。

01       #include <iostream>
02       double result(double x , double y);            // result()函数声明
03       int main()
04       {
05               double a;
06               double b;
07               double z;
08               std::cout<<"Enter two numbers : a and b \n";
09               while(std::cin>>a>>b)                              //当a和b有输入时进入循环
10               {
11                     try                                                //对可能出现异常的块进行监测
12                      {
13                           z = result(a,b);
14                     }
15                     catch(const char * e)                        //出现异常则捕获异常字符串并处理
16                     {
17                           std::cout<<e<<std::endl;
18                           std::cout<<"Enter a new pair of a and b : \n";
19                           continue;                              //跳出该轮循环,进入下一轮while循环
20                     }
21                     std::cout<<"Result is : "<<z<<std::endl;
22               }
23               std::cin.get();
24               return 0 ;
25        }
26       double result(double x , double y)
27       {
28              if( x == -y )
29              {
30                    throw " x = -y is not allowed! ";      //抛出异常字符串
31              }
32              return 2.0 * x * y / ( x + y ) ;
33       }

【代码说明】程序在第28行发现了异常,并在第30行引发了一个异常,将这个异常抛出。代码第11行的try块中将监控一个throw抛出的异常,将在catch块中进行捕获并处理。代码第8行使用std::表示使用标准名称空间的对象,与using namespace std不同的是,该符号只针对一个对象,而使用using namespace std表示在此声明以下的同源文件部分都使用标准名称空间的变量。代码第19行的continue表示跳出此循环,进入下一轮循环(同一个循环体内);而break则表示退出循环,不再进行下一轮循环。

编程谬误 许多程序员非常习惯使用弹出对话框的方式进行错误跟踪,弹出对话框的方式固然有其优越性,如编写方便、无须对程序结构进行大修改,且弹出对话框可以附带一些变量信息,如将一些变量转换成为字符串进行弹出显示,但弹出对话框方式需要输出特定变量,而不能检测函数段错误。另外,调试后容易忽略清除弹出对话框语句,在软件使用时让用户突然看到无错误程序弹出调试信息。

因此可以知道,如果实例2-40代码出现异常情况,异常处理机制的处理流程如下。

(1)进入while循环。

(2)进入try块。

(3)调用result()函数。

(4)运行throw抛出字符串“x=-y is not allowed!”,如代码第30行。

(5)catch捕获到异常字符串“x=-y is not allowed!”,并进入catch块进行异常处理。

(6)异常处理完毕,跳出本轮while循环,进入下一轮while循环。

2.7.2 exception类

【实例2-41】C++语言也将异常封装在类中,使用exception头文件定义exception类,用户可以把它用作其他异常类的基类。该类有一个虚函数what(),它返回一个字符串,用户可以改写该虚函数,代码如下。

01       #include <exception>
02       class badResult : public std::exception
03       {
04       public:
05            const char * what(){return "bad result of the result() function!";}
06       …;
07       };

【代码说明】该代码第05行重写了基类的虚函数,该虚函数直接定义在类体内。那么,异常处理块可以这样调用它了。

01      try
02      {
03      …;
04      }
05      catch(std::exception & e)
06      {
07           std::cout<<e.what()<<std::endl;
08      …;
09      }

2.8 标准模板库

标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。现在STL已经成为C++的一部分,目的是标准化组件,减少重新开发,便于使用现成的组件。

2.8.1 auto_ptr类的使用

auto_ptr是一个模板类,用于管理动态内存分配的用法。auto_ptr是一种智能指针,智能指针的对象的特征类似于指针;它同时又是一个类,因此它可以修改或扩充简单指针的行为。

【实例2-42】用智能指针修改或扩充简单指针的行为,代码如下。

01      void f()
02      {
03         type * p= new type;
04         //若在这中间有异常发生或函数提前返回,则内存泄漏
05         delete p;
06      }
07      void f()
08      {
09         auto_ptr<type> p= new type;
10         //若在这中间有异常发生或函数提前返回,则不会有内存泄漏
11         return ;
12      }

【代码说明】代码第09行中的智能指针auto_ptr就在此自动调用了delete对释放分配的内存,因此对单个内存的创建不用担心内存泄漏问题。

2.8.2 vector模板类的定义和使用

前文多次使用了数组定义,本小节将介绍一种智能数组,即vector,该模板类的中文含义是向量。vector和数组相比,区别在于如下方面。

·vector是模板类,它创建的对象具有类对象性质,可以做各种操作,比如返回自身长度、自身搜索等;而数组不具备这些自动功能,需要靠外界函数才能处理。

·vector是模板类,创建类时要调用构造函数等一系列动作,因此内存和计算开销较大;而数组的内存和计算开销较小。

【实例2-43】创建vector,代码如下。

01       #include <iostream>                                    //包含输入输出流的头文件
02       #include <vector>                                    //包含vector模板类的头文件
03       int main()
04       {
05               std::vector<int> vec;                              //创建一个vector对象,注意创建格式
06               int i = 0 ;                                          //循环变量
07               for( i = 0 ; i < 10 ; i++ )
08               {
09                     vec.push_back(i*3);                        //写入vec对象的尾部
10               }
11               for( i = 0 ; i < 10 ; i++)
12               {
13                      std::cout<<vec[i]<<std::endl;            //输出vec对象的所有元素
14               }
15               std::cin.get();
16               return 0;
17        }

【代码说明】如上程序看起来似乎还不太自动化,因为第11行中for循环的界限要手动设置。当然也可以使用vec.size()函数取得vec对象的向量数目,但还有一种更智能化的方式,就是使用迭代器。

编程陷阱 vector固然好用,在编写桌面应用程序时,基本上可以使用向量代替数组,但在一些特殊情况下,如嵌入式编程、需要节省空间和提高运行效率时,使用普通数组则是更好的选择。

迭代器是一个广义指针,要为vector定义一个迭代器,可以利用如下代码。

std::vector<int>::iterator pv ;

【实例2-44】使用该迭代器可以将前文中不太自动化的程序修改得更具自动化,代码如下。

01       #include <iostream>
02       #include <vector>
03       int main()
04       {
05            std::vector<int> vec;
06            std::vector<int>::iterator pv ;            //定义一个迭代器pv,指向vector<int>对象
07
08            for( int i = 0 ; i < 10 ; i++ )
09            {
10                  vec.push_back(i*3);
11            }
12            for( pv = vec.begin() ; pv != vec.end() ; pv++ )      //设定迭代器pv的变化范围
13            {
14                  std::cout<<*pv<<std::endl;                  //迭代器看成指针使用,使用“*”取值
15            }
16            std::cin.get();
17            return 0;
18       }

【代码说明】代码第12~15行演示了迭代器的使用,用begin()和end()来设定其范围。要注意代码第14行,迭代器会被看成指针使用,使用“*”来取值。

注意 标准模板库是C++标准中的库,与具体编译器和开发环境无关。在大型引用项目中,使用标准模板库能较好地提高开发效率和代码质量。

2.8.3 通用算法

STL是一种通用编程技术,它关注的是算法,而面向对象编程关注的是编程的数据结构方面的问题,两者之间的共同点是抽象和创建可重用的代码。

1.为什么要使用迭代器

模板使得算法独立于数据类型,即可以接受不同的数据类型来生成具体的函数或类定义;而迭代器独立于使用的容器类型。什么是容器类型呢?容器就是STL中的可存放多个数据元素的模板类,如vector。而迭代器是可以独立于不同的容器来定义并使用的。

在程序设计语言中,使用循环语句是非常常见的,因此,不同的循环、不同的数据和不同的容器内使用循环在算法方面其实是一样的,但每次循环迭代都要进行设置,不仅烦琐,而且容易出错,而迭代器正是因为有这个抽象功能而被提出的。

2.序列

常见的STL序列容器类型有如下6种。

·deque:双向队列。

·list:单向链表。

·queue:队列。

·priority_queue:优先队列,可以保证队列中的元素始终保持有序。

·stack:栈。

·vector:向量。

这些都是序列,序列的基本特征是能在其中添加和删除元素。另外,序列还要求元素按照线性的顺序排列,有一个头元素,有一个尾元素,除了头元素和尾元素外,其他元素都只能有一个前驱和后继。

由于这些容器的外部形式和运算过程都具有抽象的相似性,因此可以设计通用算法处理这些容器类型的对象。常用的通用算法如下。

·sort(),排序。

·copy(),复制。

·find(),查找。

【实例2-45】通用算法不属于任何一个类,是独立的函数库,但这些函数库接受迭代器类型、对象指针类型或一般对象的参数,例如如下代码。

01      #include <iostream>                              //包含输入输出流的头文件
02      #include <vector>                                    //包含vector的头文件
03      #include <algorithm>                              //包含通用算法函数的头文件
04      int main()
05      {
06            // sort()算法
07            std::vector<int> vec1;                  //定义一个vector<int>类型的对象vec1
08            std::vector<int>::iterator pv ;      //定义一个指向vector<int>类型对象的迭代器
09            for( int i = 10 ; i > 0 ; i-- )
10            {
11                  vec1.push_back(i*3);                  // vec1容器对象赋值
12            }
13            std::cout<<"Before sort: \n" ;
14            for( pv = vec1.begin() ; pv != vec1.end() ; pv++ )
15            {
16                  std::cout<<*pv<<std::endl;            //输出vec1容器内容
17            }
18            std::sort( vec1.begin() , vec1.end() );//调用通用算法函数对vec1容器内容进行排序
19
20            std::cout<<"After sort: \n" ;
21            for( pv = vec1.begin() ; pv != vec1.end() ; pv++ )
22            {
23                  std::cout<<*pv<<std::endl;            //输出排序后的vec1容器的内容
24            }
25            std::cin.get();
26
27            // copy()算法
28            int ar[6] = {4,5,4,6,4,2} ;            //定义一个int型的普通数组
29            std::cout<<"Original contents : \n";
30            for( pv = vec1.begin() ; pv != vec1.end() ; pv++ )
31            {
32                  std::cout<<*pv<<std::endl;
33            }
34            pv = vec1.begin();
35            std::copy( ar , ar+5, pv );            //将ar数组的内容复制到pv迭代器指向的容器中
36            std::cout<<"After copy(), contents : \n";
37            for( pv = vec1.begin() ; pv != vec1.end() ; pv++ )
38            {
39                  std::cout<<*pv<<std::endl;            //输出复制后vec1容器的内容
40            }
41            std::cin.get();
42            return 0;
43      }

【代码说明】代码第07~12行定义了一个vector<int>类型的对象vec1,并为其赋值。代码第18行调用通用算法函数对vec1容器内容进行排序。代码第35行使用算法copy()将数组复制到pv迭代器指向的容器中。

说明 通用算法函数只接受指针或迭代器(可看成是特殊的指针)参数。

2.8.4 实例:使用STL和通用算法开发成绩管理系统(5.0版)

前文介绍了STL和通用算法,本小节将使用前面所学的知识对“成绩管理系统”进行进一步的升级。

【实例2-46】升级成绩管理系统4.0,增加如下两个功能。

·搜索功能——根据学生的姓名打印出该学生的所有成绩。

·排序功能——将学生的成绩按照总分排序。

设计思路如下。

①将学生定义为类,每个学生是一个对象。该类要求能说出自己的各门功课的分数,能求出自己的总分,能求出自己的平均分。

②将一个班的学生定义为向量,每个学生是向量中的一个元素。

③使用迭代器迭代向量容器,返回要搜索的学生的迭代器,以此迭代器来查看该学生的相关成绩信息。

④使用通用算法std::sort()对学生成绩排序。

开发步骤如下。

(1)创建新的Win32 Console Application项目,并命名为performance5。

(2)创建空的项目。

(3)定义并实现学生类。在源文件视图中添加“student.h”和“student.cpp”文件,代码如下。

01      /////////////             student.h                  //////////////////////////
02      #ifndef _STUDENT_H_
03      #define _STUDENT_H_
04      #include <string>                                    //包含string类的头文件
05      class CStudent                                    //定义CStudent类
06      {
07      private:                                                // CStudent类的私有域
08            std::string strName ;
09            double chinese ;
10            double math;
11            double english;
12      public:                                                // CStudent类的公有域
13            CStudent();
14            CStudent(std::string Name,double c,double m,double e);
15            void SetChinese(double a);
16            void SetMath(double a);
17            void SetEnglish(double a);
18            void SetName(std::string c);
19            std::string returnName();
20            double returnChinese();
21            double returnMath();
22            double returnEnglish();
23            double returnTotalPreformance();
24            double returnAverage();
25            ~CStudent(){}
26      };
27      #endif
28
29      /////////////             student.cpp              //////////////////////////
30      #include <iostream>
31      #include "student.h"
32      using namespace std;
33      CStudent::CStudent()
34      {
35            strName = "General Student";
36            chinese = 0 ;
37            math = 0 ;
38            english = 0 ;
39      }
40      CStudent::CStudent(std::string Name, double c, double m, double e)
41      {
42            strName = Name ;
43            chinese = c ;
44            math = m ;
45            english = e ;
46      }
47      void CStudent::SetChinese(double a)
48      {
49            chinese = a;
50      }
51      void CStudent::SetEnglish(double a)
52      {
53            english = a;
54      }
55      void CStudent::SetMath(double a)
56      {
57            math = a;
58      }
59      double CStudent::returnChinese()
60      {
61            return chinese ;
62      }
63      double CStudent::returnEnglish()
64      {
65            return english ;
66      }
67      double CStudent::returnMath()
68      {
69            return math ;
70      }
71      double CStudent::returnTotalPreformance()
72      {
73            return chinese + math + english ;
74      }
75      double CStudent::returnAverage()
76      {
77            return (chinese + math + english)/3;
78      }
79      string CStudent::returnName()
80      {
81            return strName;
82      }
83      void CStudent::SetName(std::string c)
84      {
85            strName = c;
86      }

【代码说明】代码第05~26行定义CStudent类。第40~86行实现CStudent类的成员函数。前面已经多次介绍过,这里不再详细说明。

(4)编写输入学生信息的函数,代码如下。

01      void inputStudentInfo(std::vector<CStudent>& vec)      //输入函数
02      {
03            int a=1;                                    //输入控制,1表示继续输入,0是退出
04                                                      //输入
05            CStudent temp;                        //临时学生对象
06            double b ;                              //临时double变量,用于输入成绩数据
07            char c[20];                              //临时字符串变量,用于输入学生姓名
08            std::cout<<"Enter number 1 for input students' information: \nnumber 0 for end                   of input : \n" ;
09            while(a)
10            {
11                  std::cin>>a;                                    //接受a变量
12                  if(a == 1)
13                  {
14                        std::cout<<"Input student's name : \n";
15                        std::cin>>c;                              //输入姓名
16                        temp.SetName(c);                        //临时学生对象填写姓名
17                        std::cout<<"Input chinese : \n ";
18                        std::cin>>b;
19                        temp.SetChinese(b);                  //临时学生对象填写语文成绩
20                        std::cout<<"Input math : \n ";
21                        std::cin>>b;
22                        temp.SetMath(b);                        //临时学生对象填写数学成绩
23                        std::cout<<"Input english : \n ";
24                        std::cin>>b;
25                        temp.SetEnglish(b);                  //临时学生对象填写英语成绩
26                        vec.push_back(temp);                  //将临时学生变量复制到学生对象向量中
27                  }
28                  else continue;
29                  std::cout<<"Enter number 1 for input students' information: \nnumber 0 for                         end of input : \n" ;
30            }
31      }

【代码说明】上述代码创建的函数inputStudentInfo主要负责修改一个存放学生信息的容器,用于存放学生类对象的向量。因此,该函数可以设计成接收一个学生类对象的向量的引用,修改该引用即可修改学生向量。

(5)实现搜索功能,根据学生的姓名来输出学生的信息,代码如下。

01      int findStudent(std::vector<CStudent>& vec ,      //将向量以引用的形式传递进入函数体
02                        std::vector<CStudent>::iterator& it ,      //向量的迭代器引用
03                        std::string str)                                    //接收的待查询的学生姓名字符串
04      {
05            for( it=vec.begin() ; it != vec.end() ; it++ )
06            {
07                  if(str == (*it).returnName())
08                  {
09                        return 1 ;
10                        break ;
11                  }
12            }
13            it = NULL ;                                                      //迭代器恢复空值
14            return 0 ;
15      }

【代码说明】代码第04行是查询条件,就是学生的姓名。第06~13行的功能是使用向量迭代器返回学生信息。

说明 一般没有索引或排序的搜索算法都是顺序搜索,如本例所示。

(6)实现排序功能,传递一个学生对象向量参数,再对该参数的向量进行向量内元素的排序,代码如下所示。

01      void InsertSort(std::vector<CStudent>& vec)      //传递一个学生向量
02      {
03            CStudent temp ;                                    //设置为“哨兵”的临时对象
04            for( int i = 1 ; i < vec.size() ; i++ )
05            {
06                  if(vec[i].returnTotalPreformance()<vec[i-1].returnTotalPreformance())
07                  {
08                        temp = vec[i];
09                        for( int j = i-1 ;
10                        temp.returnTotalPreformance() < vec[j].returnTotalPreformance() ;
11                        --j )                                    //每个学生记录总分都跟“哨兵”总分相比
12
13                        {
14                              vec[j+1] = vec[i] ;            //向量总的学生记录逐个后移
15                        }
16                        vec[j+1] = temp ;                        //比较完毕,插入到正确位置
17                  }
18            }
}

【代码说明】排序算法InsertSort使用的是“直接插入排序”方式,即逐个将向量中的学生按照成绩总分插入相应位置。

(7)编写主函数,代码如下。

01      #include <iostream>
02      #include <vector>
03      #include "student.h"
04      void inputStudentInfo(std::vector<CStudent>& vec);      //使用到的函数的声明
05      int findStudent(std::vector<CStudent>& vec ,
06                              std::vector<CStudent>::iterator& it ,
07                              std::string str);
08      void InsertSort(std::vector<CStudent>& vec);
09      int main()
10      {
11            std::vector<CStudent> classes1;                        //创建学生向量
12            std::vector<CStudent>::iterator iter ;                  //创建向量迭代器
13            inputStudentInfo(classes1);                              //输入学生的信息
14            std::cout<<"Enter the name you want to check : " <<std::endl;
15            char a[20];
16            std::cin>>a;
17            if(findStudent(classes1,iter,a))                        //找到指定姓名的学生
18            {                                                                  //输出该学生的相关信息
19                  std::cout<<iter->returnName()<<" performance : \n"
20                        <<"Chinese : "<<iter->returnChinese()
21                        <<"\nMath : "<<iter->returnMath()
22                        <<"\nEnglish : "<<iter->returnEnglish()<<std::endl;
23            }
24            else
25            {
26                  std::cout<<"No found!\n";
27            }
28            InsertSort(classes1);                                          //将这些学生按照成绩总分排序
29            int i ;
30            std::cout<<"All students' performance descend by total performance :\n" ;
31            for( iter = classes1.begin() , i = 1 ; iter != classes1.end() ; iter++ , i++ )
32            {                                                                  //打印按总分排序后的学生信息
33                  std::cout<<"The rank "<<i<<" is : "<<std::endl;
34                  std::cout<<iter->returnName()<<std::endl;
35                  std::cout<<iter->returnTotalPreformance()<<std::endl;
36            }
37            std::cin.get();
38            std::cin.get();
39            return 0 ;
40      }

【代码说明】代码第11~12行创建学生向量和向量迭代器。第17行调用findStudent算法找到指定姓名的学生。第28行调用InsertSort对学生总成绩进行排序。代码第31~36行打印按总分排序后的学生信息。

2.9 I/O流和文件

I/O流和文件是一门高级程序设计语言中的必要部分,本节将介绍C++中的基本输入和输出流,最后讲述文件中的输入和输出。

2.9.1 C++的输入和输出

C++程序把输入和输出看做字节流,输入时从输入流中抽取字节,输出时将字节插入输出流。输入/输出的字节可能来自于键盘输入,也可能来自于外部存储设备或其他程序,还可能来自网络。

C++的iostream库有8个流对象,4个用于窄字符流,4个用于宽字符流如下。

·cin对象是标准输入流对象,它默认关联到标准输入设备——键盘。wcin与此类似,处理wchar_t类型。

·cout对象是标准输出流对象,它默认关联到标准输出设备——显示器。wcout与此类似,处理wchar_t类型。

·cerr对象是标准错误流对象,它默认被关联到标准输出设备——显示器。wcerr与此类似,处理wchar_t类型。

·clog对象也是标准错误流,它默认也是被关联到标准输出设备——显示器,与cerr不同的是,这个流被缓冲。

2.9.2 文件输入和输出

本小节主要讲解文本文件的输入和输出。多数计算机和操作系统都有“文件”这个概念,文件的概念很广,常见的主要有如下几种。

·文本文件,字处理程序创建和修改的文件。

·数据库文件,数据库管理系统创建和修改的文件。

·执行文件,编译器读取源代码并编译生成的可执行文件。

【实例2-47】编写输出字符到文本文件的程序,代码如下。

01      #include <iostream>
02      #include <fstream>
03      #include <string>
04      int main()
05      {
06           //////输出到文件
07            std::string fileName;
08            std::cout<<"Enter the file name : \n" ;
09            std::cin>>fileName ;
10            std::ofstream fout(fileName.c_str());            //创建输出流,并关联到文件
11            fout<<"Write something to this file!~~ " ;
12            fout.close();                                                //关闭该对象的输出流文件
13           ///////从文件输入
14            std::ifstream fin(fileName.c_str());            //创建输入流,并关联到文件
15            char ch;
16            while(fin.get(ch))
17            {
18                  std::cout<<ch;
19            }
20            fin.close();                                                //关闭该文件的输入流文件
21            std::cin.get();
22            return 0;
23      }

【代码说明】代码第10行创建一个输出流对象,第14行创建输入流对象。通过上述代码可以看出,要让一个程序写入文件,必须经过如下步骤。

(1)创建一个ofstream对象来管理输出流。相对于程序来说,文本文件是外部文件,因此使用输出流输出到程序外部。

(2)将该对象与特定的文本文件关联起来。

(3)使用该对象的<<重载运算符将字符输出到文件中。

编程陷阱 标准C++提供的是fstream及其子类进行输入输出流,MFC类中也定义了CFile类和CStudioFile类进行文件读写,同时还提供序列化和反序列化机制(本书后面章节将详细介绍);但标准C++和MFC提供的类库最好不要在同一个程序中交叉使用,这涉及字符类型的转换,特别是涉及中文读写将会非常烦琐。

2.9.3 实例:在Visual C++6.0环境下创建C++源文件,使用磁盘文件读写

前文所开发的5个版本的成绩管理系统程序有一个共同的不足,即不能将学生的信息保留下来,程序关闭后,此前输入的学生信息,包括学生姓名和学生成绩等信息都消失了。原因在于程序中的一切数据和命令都是在内存中进行的,当程序退出时,内存中相应的空间会被操作系统回收,因此前文讲述的5个版本的成绩管理系统中输入的学生数据无法长期保存。要想保存程序中输入的数据,比较有效的方式就是保存在磁盘存储器中,即可以写入文本文件或数据库文件。本小节将要介绍的是将数据保存在文本文件中。

本小节以成绩管理系统(5.0版)作为基础,将其升级到成绩管理系统(5.1版)。

【实例2-48】为成绩管理系统(5.0版)新增一个输出到文件,升级到成绩管理系统(5.1版),修改步骤如下。

(1)新增一个文件输出函数,代码如下。

01      void outToFile(std::string fileStr , std::vector<CStudent>& s)
02      {
03            std::string temp ;
04            temp = fileStr+".txt";                              //将用户输入的文件名加文本后缀
05            std::ofstream fout(temp.c_str());                  //创建输出流对象,并与文件关联
06            std::vector<CStudent>::iterator it;            //创建CStudent向量的迭代器
07            for( it = s.begin() ; it != s.end() ; it++ )//迭代输出所有学生的信息
08            {
09                  fout<<it->returnName()<<std::endl;
10                  fout<<"Chinese : "<<it->returnChinese()<<std::endl;
11                  fout<<"Math : "<<it->returnMath()<<std::endl;
12                  fout<<"English : "<<it->returnEnglish()<<std::endl;
13            }
14      }

【代码说明】代码第04行通过字符串连接的方式为用户指定的文件名添加.txt后缀。第7行使用向量的迭代器,迭代输出所有学生的信息。

(2)在main()函数之前增加文件输出函数的声明,代码如下。

void outToFile(std::string fileStr , std::vector<CStudent>& s);

(3)在main()函数内调用文件输出函数,代码如下。

std::cout<<"Enter the file name to store the information : \n" ;
std::cin>>a;                                          //接受用户输入文件名
outToFile(a,classes1);                              //调用文件输出函数

完成以上3个步骤后,该成绩管理系统就可以将信息写到硬盘上了。将硬盘的文本文件读取到程序中也很简单。由于是使用文本文件存放,所以读取的关键在于分析文本文件存放信息的格式,这个功能的升级留给感兴趣的读者自行完成。

注意 为了方便地读取文本文件,最好在输出完每一个学生的信息后用一个“*”或其他特殊符号分割,读取时可以以该特殊符号作为一个学生记录的分割线。

2.10 小结

本章详细讲解了C++的语法理论知识,内容比较多,读者应该慢慢消化,反复阅读,这对以后的学习是很有帮助的。

本章应该掌握的内容如下。

·基本数据类型和复合数据类型的定义和使用。

·循环语句和判断语句的语法格式。

·函数的定义和调用。

·类的定义和对象的创建。

·类继承的原理,主要是共有继承和虚函数动态联编的理解。

·标准模板库。理解容器的概念和泛型算法的使用,熟练使用常用的容器,特别是vector。

·文本文件输入和输出的定义和与使用。

2.11 习题

一、填空题

1.浮点类型有3种,分别为_____________、_____________和_____________。

2.C++提供了直接反映地址的变量为_____________。

3.类定义有3种访问控制的关键字分别为_____________、_____________和_____________。

4.有两个或两个以上的基类的继承关系称为_____________。

5.常用的异常处理有两种类型分别为_____________和_____________。

二、上机实践

1.编写一个简单的C++应用程序函数,要求函数实现的功能是交换两个数,返回值为bool类型,表示交换成功,函数参数为两个,分别表示待交换的两个数。

【提示】本题主要要求读者学习C++函数定义和引用类型的知识,重点是掌握C++函数定义技术。

【关键代码】

01                  temp = *a ;
02                  *a = *b ;
03                  *b = temp ;

2.参考本章知识内容中定义的“人”类定义一个Dog类,并定义Dog类应该有的属性和行为。

【提示】本题主要要求读者学习类定义知识,重点是掌握面向对象的类定义技术。

【关键代码】

01      class dog
02      {
03           Private:
04                 …;
05           Public:
06                 …;
07      };

3.编写一个程序,要求使用C++异常处理机制使程序在应用过程中能很方便地判断异常类型。

【提示】本题主要是要求读者学习C++异常处理知识,重点是掌握异常处理的基本编程结构技术。

【关键代码】

01      try
02      {
03       …;
04      }
05      catch
06      {
07       …;
08      }

4.使用标准模板库STL编写一个排序的小程序。

【提示】本题主要要求读者学习STL知识,重点是掌握STL使用技术。

【关键代码】

std::sort( vec1.begin() , vec1.end() );