
2.1 面向对象程序设计与C++语言
在现实生活中,人不懂拼音也会讲普通话;但如果懂拼音,则会把普通话讲得更好。同样的道理,对于程序员来说,不懂面向对象程序设计也可以使用C++语言进行编程;但如果懂得面向对象程序设计,则会将C++程序编制得更加完善。
2.1.1 面向对象程序设计概述
20世纪80年代后期,人们提出了面向对象(Object Oriented Programming, OOP)的程序设计方法。这种先进的软件设计方法吸收了软件工程领域中诸多有益概念和有效方法,更加符合人的思维模式,编写的程序更加健壮和强大。更重要的是,面向对象程序设计方法有利于系统开发的责任分工,能有效组织和管理较复杂的应用程序开发过程。
在面向对象程序设计中,将数据和处理数据的方法紧密地结合在一起,形成类,再将类实例化,就形成了对象。在面向对象的世界中,程序员不再需要考虑数据结构和功能函数,只要关注对象就可以了。对象就是客观世界中存在的人、事和物体等实体。现实世界中的对象随处可见。例如,天上的飞鸟、水里的游鱼和路上的汽车等。但这里所说的鸟、鱼和车都是对同一类事物的总称,这就是面向对象中的类(class)。那么对象和类之间的关系是什么呢?对象就是符合某种类定义所产生出来的实例(instance)。虽然在现实生活中,人们习惯用类名称呼这些对象,但是实际上看到的还是对象的实例,而不是一个类。例如,你看见树上落着一只鸟,这里的“鸟”虽然是一个类名,但实际上你看见的是鸟类的一个实例对象,而不是鸟类。由此可见,类只是个抽象的称呼,而对象则是与现实生活中的事物相对应的实体。类与对象的关系如图2-1所示。

图2-1 类和对象的关系
在现实生活中,单纯使用类或对象都不能很好地描述一个事物。例如,小明对妈妈说他放学路上看见一只鸟,妈妈就不知道小明说的鸟是什么样子。但是,如果小明说看见一只绿色会说话的鸟,那么妈妈就可以想象这只鸟的样子。这里的绿色是指对象的属性,而会说话则是指对象的方法。由此可见,对象还具有属性和方法。在面向对象程序设计中,使用属性来描述对象的状态,使用方法来处理对象的行为。
面向对象程序设计的特点可以概括为封装性、继承性和多态性。
1. 封装性
面向对象程序设计的核心思想之一就是将对象的属性和方法封装起来,使程序员只需知道并使用对象提供的属性和方法等接口即可,而不需知道对象的具体实现。例如,一部手机就是一个封装的对象,当使用手机拨打电话时,只需要使用它提供的键盘输入电话号码,并按下发送键即可,而不需要知道手机内部是如何工作的。
封装使程序员不能随意存取对象的内部数据,从而有效避免了外部错误对内部数据的影响,实现了错误局部化,大大降低了程序员查找和解决错误的难度。此外,封装也提高了程序的可维护性,当对象内部结构或者实现方法发生改变时,只要接口不变,就不用改变外部部分的处理。
2. 继承性
面向对象程序设计中,允许通过继承原有类的某些特性或全部特性而产生新的类。此时,原有类被称为父类(或超类),产生的新类称为子类(或派生类)。子类不仅可以直接继承父类的共性,而且也允许子类创建它特有的个性。例如已经存在一个手机类,该类中包括两个方法,分别是接听电话的方法receive()和拨打电话的方法send(),这两个方法对于任何手机都适用。现在要定义一个时尚手机类,该类中除了要包括普通手机类包括的receive()和send()方法外,还需要包括拍照方法photograph()、视频摄录的方法kinescope()和播放MP4的方法playmp4(),这时就可以通过先让时尚手机类继承手机类,然后再添加新的方法完成时尚手机类的创建。由此可见,继承性简化了对新类的设计。使用UML类图描述的手机类与时尚手机类如图2-2所示。

图2-2 使用UML类图描述的手机类与时尚手机类
3. 多态性
多态性是面向对象程序设计的又一重要特征,它是指在基类中定义的属性和方法被子类继承后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或方法在基类及其子类中具有不同的语义。例如,定义一个动物类Animal,该类中存在一个指定动物行为的通用方法cry()及调用该方法的doCry(Animal a)方法。其中,在doCry(Animal a)方法中,只有一句代码“a.cry();”。再定义两个动物类的子类,狗类Dog和猫类Cat,这两个类都具有cry()方法,并且都进行了相应的处理,如图2-3所示。

图2-3 使用UML类图描述各类间的关系
在动物园类中执行doCry(animal)方法时,如果参数为Animal类型,则会输出“动物发出叫声!”;如果参数为Dog类型,则会输出“狗发出‘汪汪⋯⋯’声!”;如果参数为Cat类型,则会输出字符串“猫发出‘喵喵⋯⋯’声!”。由此可见,在doCry(Animal a)方法中,根本不用判断应该去执行哪个类的cry()方法,因为系统编译器会根据所传递的参数进行自动判断,这就是动态绑定,即根据运行时对象的类型不同而执行不同的操作。多态性丰富了对象的内容,扩大了对象的适应性,改变了对象单一继承的关系。
此时,你可能对面向对象程序设计的基本概念和特性不是很清楚。没有关系,当你使用C++语言进行一段时间的程序编制后,回过头再看这些基本概念和特性时,就会对它们理解的更加深刻。
2.1.2 C++语言基础
C++语言充分利用了OOP的优势,功能强大,倍受程序员推崇。从名称上看,C++语言完全从C语言的基础上发展;而在实现上,C++语言则是对C语言支持最好的编程语言,它继承了C语言很多优点,增强编译器的纠错能力,增加对面向对象程序设计的语法支持。C++语言被广泛运用于编写大型应用软件,例如,Microsoft公司的Office软件、Adobe公司的Photoshop软件。前者是全世界使用最广泛的办公软件,后者是全世界使用最广泛的图像处理软件。此外,Windows操作系统的COM组件、Linux操作系统的部分GUI,也都是使用C++语言编制的。最后,需要提醒读者的是,C++语言是一种集大成者的编程语言,不仅支持“面向对象程序设计”思想,同时还支持“基于对象”、“面向过程”以及“泛型”编程思想。本节将简单介绍C++语言基础知识。
1. C++基本控制结构
按结构化程序设计的观点,C++基本控制结构可分为三种:顺序结构、选择结构和循环结构。在C++语言中,顺序结构语句包括说明语句、赋值语句、I/O语句、子函数调用和返回语句等;选择结构语句包括if选择语句、if...else选择语句和switch选择语句等;循环结构语句包括while循环语句、do...while循环语句和for循环语句等。
下面通过例2-1来简单说明C++基本控制结构。在这个实例中,程序将用户输入的一组百分制成绩转化成五分制成绩。例2-1的模块结构和逻辑框图如图2-4所示。

图2-4 例2-1的模块结构和逻辑框图
使用C++语言编程时,采用for循环语句实现循环结构,采用switch选择语句实现选择结构。
【例2-1】 百分制成绩转换为五分制成绩
评定学生成绩时可采用五分制成绩和百分制成绩。百分制的5分代表百分制中的90~100;4分代表80~89;3分代表70~79;2分代表60~69;1分代表60以下。请编制程序将一组百分制成绩转换成对应的五分制成绩。
#include <iostream.h> int TranGrade (int old_grade) { int new_grade; switch(old_grade/10) //switch选择语句 { case 10: case 9: new_grade=5; break; case 8: new_grade=4; break; case 7: new_grade=3; break; case 6: new_grade=2; break; default: new_grade=1; } return new_grade; } void main() { int grade[10]={100, 85, 72, 69, 94, 74, 66, 51, 89, 45}; int i; for(i=0;i<10;i++) // for循环语句 cout<<"百分制成绩:"<<grade[i]<<",五分制分数:"<<TranGrade(grade[i])<<endl; }
运行情况:
在Visual C++ 6.0集成开发环境中编译例2-1程序成功后,运行得到如图2-5所示的结果。

图2-5 例2-1运行结果示意图
2. C++基本数据类型
C++语言的数据分为常量和变量。常量即固定不变的量,主要包括整型常量、实型常量、字符型常量、字符串常量、符号常量、浮点型常量等形式。变量即变化的量,主要包括整型变量、实型变量、字符型变量、Const变量等形式(注意没有字符串变量类型)。
变量声明语句格式为:
<类型说明符><变量名>;
声明变量时通常需要对变量进行初始化,即给变量赋一个初值,例如:
int i=0; //声明了1个整型变量,初值为0 char c='a'; //声明了1个字符型变量,初值为a
C++程序中每个数据都需定义其数据类型。C++语言的基本数据类型、长度和数据表示范围如表2-1所示。
表2-1 C++语言的基本数据类型

通过在基本数据类型前加类型修饰符,可以改变数据表示范围。常用的类型修饰符包括无符号(unsigned)、长型(long)和短型(short)。
3. C++运算符
同C语言类似,C++语言中的运算符分为算术运算符、关系运算符、逻辑运算符、条件运算符、逗号运算符、位运算符、指针运算符等。表2-2给出了每种运算符的优先级和结合性。
表2-2 C++运算符的优先级和结合性

(续)

4. C++函数
直观上看,每个C++程序是由一个或者多个被称为函数的程序模块组成的。每个函数有固定接口并能完成独立功能。除可以使用系统提供的标准函数外,程序员可以调用自定义函数。在每个C++程序中,有且只有一个主函数(通常函数名称是main),其他函数则是子函数。C++程序的执行总是从主函数开始,顺序执行每条语句,如果执行语句时遇到其他子函数,则调用其他子函数,调用完毕返回发生函数调用的下一条语句继续执行,一旦主函数执行完毕,那么就意味着整个程序执行完毕。注意,函数可以被其他函数调用,也可以调用其他函数,即函数调用上可以存在嵌套关系。但是,函数定义不允许嵌套,即在函数定义中再定义另一个函数是非法的。
调用函数前要定义函数,否则会产生编译错误。函数定义一般从四方面进行:函数值类型、函数名、形式参数表、函数体。
<函数值类型><函数名>(<形式参数表>) { <函数体> }
函数值类型规定了函数体return返回函数值的数据类型。如果函数没有返回值,那么函数值类型是void。
函数名是符合C++语法规则的一个标识符,一般来说,函数名应尽可能使用能够反映函数功能的单词组合。
形式参数(通常简称为“形参”)表包含0个或1个以上的参量,用来向函数传入数值或者从函数传出数值。注意,即使多个参量的数据类型相同,也必须单独定义每个参量的数据类型。如果函数的形式参数表中没有参量,那么这个函数被称为无参函数,形式参数表为void。
函数体包含若干语句,用来完成函数功能。
【例2-2】C++程序中函数定义和函数调用的简单方法
#include "iostream.h" int func(int n) //定义子函数func,该函数判断整数n是正整数、0、负整数,分别返回1,0,-1 { if(n>0) return 1; else if(n==0) return 0; else return -1; } void main(void) //定义主函数main,不要求函数返回值,没有形式参数 { int n; cout<<"Please input n:"<<endl; cin>>n; cout<<"\nthe result:"<<func(n)<<endl; //输出语句中调用了子函数func }
运行情况:
在Visual C++ 6.0集成开发环境中编译例2-2程序成功后,运行可执行文件,输入正整数“4”,输出“1”,如图2-6所示。

图2-6 例2-2程序运行结果
相对于C语言函数,C++函数增加了重载(overloaded)、内联(inline)、固定(const)和虚拟(virtual)四种新机制。其中,重载和内联机制既可以用于全局函数也可用于类的成员函数;固定和虚拟机制仅用于类的成员函数。考虑到串口通信编程通信很少使用C++函数的这些高级特性进行详细讨论,本书不对这些特性进行详细介绍。
5. C++指针
指针是C++程序中无所不在的重要概念。指针基本功能是指示数据的内存地址。
声明指针的一般格式为:
数据类型 *指针变量名
指针经过初始化才能使用,下面给出了几种指针初始化方法。
例如:
int *ptr, i=10; ptr=&i; //指向单个变量 char *sp="string"; //指向字符串 int a[5],*ap; ap=a; //指向数组 int max(),(*fp)(); fp=max; //指向函数
这里,“&”被称为取地址运算符,用来返回变量的内存地址;“*”可以被称为取内容运算符,用来返回指针变量的值。
【例2-3】C++程序中指针的使用方法
#include "iostream.h" void main(void) { int nNumber; int *pPointer; //声明指针变量 nNumber = 15; pPointer = &nNumber; //给指针赋值 cout<<"nNumber is equal to:"<<nNumber<<endl; //输出变量nNumber的值 *pPointer = 25; //通过指针改变变量nNumber的值 cout<<"nNumber is equal to:"<<nNumber<<endl; //再次输出nNumber,证明值已改变 }
请读者编译并运行该程序,理解其实现原理。
2.1.3 C++的面向对象特性
C++语言是支持面向对象程序设计方法的最具代表性编程语言。它以类(class)和对象(object)为基础,支持类的继承(inheritance)和多态(polyphorhism),本节将简要介绍C++语言中和面向对象相关的知识。
1. 类的声明
类是具有相同属性和相同方法的对象的集合。在C++语言中,类的声明格式为:
class类名 { private: 私有的数据成员和成员函数 public: 公有的数据成员和成员函数 protected: 受保护的数据成员和成员函数 }
这里,数据成员和成员数据都被称为类成员。其中,数据成员定义了类具有的数据,成员函数则定义了可以对数据成员进行的操作。
每个类成员都具有访问权限属性,访问权限修饰符(或访问控制修饰符)private、public和protected表示类成员访问权限属性分别为私有的、公有的和受保护的。私有的类成员完全对类外隐藏,只能通过本类的成员函数访问;公有的类成员是全局开放的,既可以被本类的成员函数访问,也可以被程序的其他部分直接访问;受保护的类成员则是半隐藏的,只能被本类的成员函数或者派生类的成员函数访问。
【例2-4】Student类的声明和类成员实现代码
class Student { //Student类的声明 private: char m_strName[30]; int m_nAge; public: Student(const char *name, int age); //构造函数 ~Student(); //析构函数 void Register(char *name, int age); char * GetName(); int GetAge(); void Show(); } //类成员实现代码 Student:: Student (const char *name, int age) //构造函数实现代码 { strcpy(m_strName, name); m_nAge=age; } Student::~Student //析构函数实现代码 { cout<<"Student类将被删除"; } void Student:: Register(char *name, int age) { Strcpy(m_strName, name); m_nAge=age; } char * Student:: GetName() { return m_strName; } int Student:: GetAge() { return m_nAge; } void Student:: Show() { cout<< GetName() << '\t' << GetAge()<< endl; }
这里,构造函数(constructor)保证在声明类的对象时,编译系统自动创建并初始化对象。类声明可以包含一组构造函数,但至少包含一个,如果类中没有定义构造函数,则编译系统自动生成一个默认的构造函数。构造函数的函数名必须和类名相同,且不指定函数返回值类型。
析构函数(destructor)保证在对象生命期即将结束时,编译系统完成某些清理工作,析构函数执行后,对象才消失,其占用的内存才被释放。类声明必须包含一个析构函数,该函数不能有任何参数。不允许在程序中直接调用析构函数,而只能由编译系统自动调用。
2. 类的对象
程序员声明一个类,即定义了一种新的数据类型。程序员使用类的方法就是声明类的变量(或对象)。例如,声明Student类的对象的方法是:
Student stu1; Student stu2[10];
当声明Student类的对象之后,程序就可以按照访问权限属性对类成员进行访问了,访问时需在对象名和类成员之间加上“.”。例如,访问对象stu中Show成员函数的语句是:
stu.Show();
程序员还可以通过声明对象指针来访问类成员。例如:
Student * pstu=&stu; stu→Show();
3. 类的继承
现实生活中的“继承”通常指接续前人事业或者承继遗产。与之类似,面向对象程序设计中的继承机制可以利用现有类来定义新的类。新类不仅拥有现有类中旧的类成员,而且同时还拥有新定义的类成员。我们称用来派生新类的现有类为基类(或称为父类),由现有类派生出的新类称为派生类(或称为子类)。
在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。
单继承的定义格式为:
class <派生类名>:<继承方式><基类名> { <派生类新定义成员> };
多继承的定义格式如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,... { <派生类新定义成员> };
这里,<派生类名>是新类名称,它是从<基类名>派生的,并遵循指定的<继承方式>。从定义格式上看,多继承与单继承的区别主要是多继承的基类多于一个。
<继承方式>表示派生类的继承方式,公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的。
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
三种继承方式的访问权限如表2-3所示。
表2-3 三种继承方式的访问权限

任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。派生类将其本身与基类区别开来的方法是添加数据成员和成员函数。因此,继承的机制将使得在创建新类时,只需说明新类与已有类的区别,从而大量原有的程序代码都可以复用,因此C++语言中的类也可以被看作“可复用的软件构件”。
4. 类的多态
类的多态理解起来不太容易。为方便理解,读者请先看例2-5的程序:
【例2-5】系统无法分清传递对象来源的实例
#include <iostream> using namespace std; //汽车类Vehicle的定义为: class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed=speed; Vehicle::total=total; } void ShowMember() { cout<<speed<<"|"<<total<<endl; } protected: float speed; int total; }; //小汽车类Car的定义为: class Car:public Vehicle { public: Car(int aird,float speed,int total):Vehicle(speed,total) { Car::aird=aird; } void ShowMember() { cout<<speed<<"|"<<total<<"|"<<aird<<endl; } protected: int aird; }; void test(Vehicle &temp) { temp.ShowMember(); } //主函数 void main() { Vehicle a(120,4); Car b(180,110,4); test(a); test(b); cin.get(); }
运行情况如图2-7所示。

图2-7 例2-5程序运行结果示意图
在例2-5中,对象a与b分别是基类Vehicle和派生类Car的对象,而函数test的形参却只是Vehicle类的引用。按照类继承的思想,系统将Car类的对象看成Vehicle类的对象,因为Car类包含Vehicle类,所以test函数的定义并没错。例2-5希望通过使用test函数,传递不同类对象的引用,分别调用不同类的重载后的ShowMember成员函数。然而,程序运行结果却不如人意,系统分不清传递来的对象是基类对象,或是派生类对象,所以调用的都是基类的ShowMember成员函数。
为了解决不能正确区分对象所属类的问题,C++语言提供了多态技术。系统在运行时根据对象所属类确定调用特定重载成员函数的能力,就被称为多态性(或称为滞后联编)。
【例2-6】利用多态技术解决例2-5的问题
#include <iostream> using namespace std; //汽车类Vehicle的定义为: class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed = speed; Vehicle::total = total; } virtual void ShowMember()//虚函数 { cout<<speed<<"|"<<total<<endl; } protected: float speed; int total; }; //小汽车类Car的定义为: class Car:public Vehicle { public: Car(int aird,float speed,int total):Vehicle(speed,total) { Car::aird = aird; } virtual void ShowMember() //虚函数,在派生类中也可以不加virtual { cout<<speed<<"|"<<total<<"|"<<aird<<endl; } public: int aird; }; void test(Vehicle &temp) { temp.ShowMember(); }; int main(void) { Vehicle a(120,4); Car b(180,110,4); test(a); test(b); cin.get(); return 0; }
运行情况如图2-8所示。

图2-8 例2-6程序运行结果示意图
这里,需要解决多态问题的重载成员函数被加上virtual前缀关键字,变成了虚函数。读者可以编译并运行例2-6,运行结果表明系统成功地区分出对象所属类,并调用了特定重载成员函数。多态让程序员省去了考虑细节的麻烦,提高了开发效率。但是,因为多态特性增加了数据存储和执行指令的开销,所以能不用多态最好不用。