1.4 C++面向对象程序设计
面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)是一种计算机编程架构。OOP的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP达到了软件工程的三个主要目标:重用性、灵活性和扩展性。为了实现整体运算,每个对象都能够接收信息、处理数据和向其他对象发送信息。本节简单复习了与面向对象编程相关的一些重要且初学者容易混淆的内容,如果读者想具体系统地学习这一部分内容,请参考其他相关书籍。
1.4.1 基本概念
面向对象程序设计中的概念主要包括:类、对象、数据封装、继承、动态绑定、多态性、数据抽象、消息传递。通过这些概念面向对象的思想得到了具体的体现。
1.类
类是具有相同类型的对象的抽象。一个对象所包含的所有数据和代码可以通过类来构造。
C++中,声明一个类的一般格式如下:
其中,class是类声明的关键字,class的后面是要声明的类名。类中的数据和函数都是类的成员,分别称为数据成员和成员函数。
类中关键字public、private和protected声明了类中的成员与类外之间的关系,称为访问权限。其中,对于private成员来说,它们是私有的,不能在类外访问,数据成员只能由类中的函数所使用,成员函数只允许在类中调用;对于public成员来说,它们是公有的,可以在类外访问;而对于protected成员来说,它们是受保护的,具有半公开性质,可在类中或其子类中访问。
需要说明的是,若一个成员函数的声明和定义同时在类体中完成,则该成员函数的实现将不需要单独出现。如果所有的成员函数都在类体中定义,则实现部分可以省略。
而当类的成员函数的定义是在类体外部完成时,必须用作用域运算符“::”来告知编译系统该函数所属的类。此时,成员函数的定义格式如下:
2.对象
对象是运行期的基本实体,它是一个封装了数据和操作这些数据的代码的逻辑实体。
作为一种复杂的数据构造类型,类声明后,就可以定义该类的对象。类对象有3种定义方式:声明之后定义、声明之时定义和一次性定义。例如:
class A { … }; A a; // 声明之后定义 class B { … } b, c; // 声明之时定义 class {… } d, e; // 一次性定义
但由于“类”比任何数据类型都要复杂得多,为了提高程序的可读性,真正将“类”当成一个密闭、“封装”的盒子(接口),在程序中应尽量使用对象的“声明之后定义”方式,即按下列格式进行:
其中,类名是已声明过的类的标识符,对象名可以有一个或多个,多个对象时要用逗号隔开。被定义的对象既可以是一个普通对象,也可以是一个数组对象或指针对象。例如:
CStuscore one, *Stu, Stus[2];
这时,one是类CStuscore的一个普通对象,Stu和Stus分别是该类的一个指针对象和对象数组。若对象是一个指针,则还可像指针变量那样进行初始化,例如:
CStuscore *two = &one;
可见,在程序中,对象的使用和变量是一样的,只是对象还有成员的访问等操作。
对象成员的访问操作:
一个对象的成员就是该对象的类所定义的数据成员和成员函数。访问对象的成员变量和成员函数与访问一般变量和函数的方法是一样的,只不过需在成员前面加上对象名和成员运算符“.”,其表示方式如下:
例如:
cout<<one.getName()<<endl; // 调用对象one中的成员函数getName,然后输出其结果 cout<< Stus[0].getNo()<<endl; // 调用对象数组元素Stus[0]中的成员函数getNo,然后输出
需要说明的是,一个类对象只能访问该类的公有型成员,不能访问私有型成员。例如getName和getNo等公有成员可以由对象通过上述方式来访问,但strName、strStuNo、fScore等私有成员则不能被对象访问。
若对象是一个指针,则对象的成员访问形式如下:
“->”是另一个表示成员的运算符,它与“.”运算符的区别是:“->”用来表示指向对象的指针的成员,而“.”用来表示一般对象的成员。
需要说明的是,下面的两种表示是等价的(对于成员函数也适用):
例如:
CStuscore *two = &one; cout<<(*two).getName()<<endl; // A cout<<two->getName()<<endl; // 与A等价
需要说明的是,类外通常是指在子类中或其对象等的一些场合。对于访问权限public、private和protected来说,只有在子类中或用对象来访问成员时,它们才会起作用。在用类外对象来访问成员时,只能访问public成员,而对private和protected均不能访问。对类中的成员访问或通过该类对象来访问成员都不受访问权限的限制。
3.封装
封装是将数据和代码捆绑在一起,以避免外界的干扰和不确定性。对象的某些数据和代码可以是私有的,不能被外界访问,以此实现对数据和代码不同级别的访问权限。
4.继承
继承是让某个类型的对象获得另一个类型的对象的特征。通过继承可以实现代码的重用:从已存在的类派生出的一个新类将自动具有原来那个类的特性,同时,它还可以拥有自己的新特性。
在C++中,类的继承具有下列特性:
(1) 单向性。类的继承是有方向的。例如,若A类是子类B的父类,则只有子类B继承了父类A中的属性和方法,在B类中可以访问A类的属性和方法,但在父类A中却不能访问子类B的任何属性和方法。而且,类的继承还是单向的。例如,若A类继承了B类,反之,此时B类不能再继承A类。同样,若A类是B类的基类,B类是C类的基类,此时C类不能是A类的基类。
(2) 传递性。若A类是B类的基类,B类是C类的基类,则基类A中的属性和方法传递给了子类B以后,通过子类B也传递给子类C,这是类继承的传递性。正因为继承的传递性,才使子类自动获得基类的属性和方法。
(3) 可重用性。自然界中存活的同物种具有遗传关系的层次通常是有限的,而C++中的类却不同,类的代码可一直保留。所以,当基类的代码构造完之后,其下一代的派生类的代码往往新增一些属性和方法,它们一代代派生下去,整个类的代码越来越完善。若将若干代的类代码保存在一个头文件中,而在新的程序文件中包含进来,然后定义一个派生类,则这样的派生类就具有前面所有代基类的属性和方法,而不必从头开始重新定义和设计,从而节略了大量的代码。由此可见,类的继承机制也体现了代码重用或软件重用的思想。
在C++中,一个派生类的定义格式如下:
类的继承使得基类可以向派生类传递基类的属性和方法,但在派生类中访问基类的属性和方法不仅取决于基类成员的访问属性,而且还取决于其继承方式。
继承方式能有条件地改变在派生类中的基类成员的访问属性,从而使派生类对象对派生类中的自身成员和基类成员的访问均取决于成员的访问属性。C++的继承方式有3种:public(公有)、private(私有)及protected(保护)。
一个派生类中的数据成员通常有3类:基类的数据成员、派生类自身的数据成员以及派生类中其他类的对象。由于基类在派生类中常常是隐藏的,也就是说在派生类中无法访问它,因此必须通过调用基类构造函数来设定基类的数据成员的初值。需要说明的是,通常将派生类中的基类,称为基类拷贝,或称为“base class subobject,基类子对象”。
C++规定,派生类中对象成员初值的设定应在初始化列表中进行,因此一个派生类的构造函数的定义可有下列格式:
说明:
(1) 在派生类构造函数的成员初始化列表中,既可有基类拷贝的数据成员的初始化,也可有派生类中对象成员的初始化。当然,派生类的数据成员也可在成员初始化列表中进行初始化,但数据成员的初始化形式必须是“数据成员名(参数)”的形式。
(2) 在成员初始化列表中,多个成员初始化之间必须用逗号分隔。
(3) 派生类中各数据成员的初始化次序总体是:首先是基类拷贝成员的初始化,然后才是派生类自己的数据成员初始化。
(4) 基类拷贝成员的初始化次序与它在成员初始化列表中的次序无关。在单继承中,它取决于继承层次的次序,即优先初始化上层类的对象。而在多继承中,基类成员的初始化次序取决于派生类声明指定继承时的基类的先后次序。
(5) 派生类自身数据成员的初始化次序与成员初始化列表中的次序无关,它们取决于在派生类中声明的先后次序。
派生类对象和派生类中的成员函数对基类的访问是不同的。那么,在派生类或派生类对象中究竟有哪些访问基类成员的方式呢?
假设,派生类B公有继承了基类A,A中的公有成员为m,则在派生类B及其对象中访问基类A成员m的方式有:
(1) 若派生类B中无任何和A基类成员m同名的成员时,则可在派生类B中直接引用m。若有同名成员存在,则在派生类B中须指定成员所属的类,即访问形式为“A::m”。
(2) 若派生类B对象oB是一个普通对象,当派生类B中无任何和A基类成员m同名的成员时,则通过oB访问基类成员m的形式为“oB.m”。若派生类B中有同名成员m存在,则通过oB访问基类成员m的形式为“oB.A::m”。
(3) 若派生类B对象poB是一个指针对象,当派生类B中无任何和A基类成员m同名的成员时,则通过poB访问基类成员m的形式为“poB->m”,若派生类B中有同名成员m存在,则通过poB访问基类成员m的形式为“poB->A::m”。
5.多态
多态是指不同事物具有不同表现形式的能力。多态机制使具有不同内部结构的对象可以共享相同的外部接口,通过这种方式减少代码的复杂度。
多态是面向对象程序设计的重要特性之一,它与封装和继承构成了面向对象程序设计的三大特性。在C++中,多态具体体现在运行时和编译时两个方面,程序运行时的多态是通过继承和虚函数来体现的,它是在程序执行之前,根据函数和参数还无法确定应该调用哪一个函数,必须在程序执行过程中,根据具体的执行情况动态地确定;而在程序编译时的多态体现在函数的重载和运算符的重载上。
与这两种多态方式相对应的是两种编译方式:静态联编和动态联编。所谓联编(binding,又称为绑定),就是将一个标识符和一个内存地址联系在一起的过程,或是一个源程序经过编译、连接,最后生成可执行代码的过程。
静态联编是指这种联编在编译阶段完成,由于联编过程是在程序运行前完成的,故又称为早期联编。动态联编是指这种联编要在程序运行时动态进行,所以又称为晚期联编。
在C++中,函数重载是静态联编的具体实现方式。调用重载函数时,编译根据调用时参数类型与个数在编译时实现静态联编,将调用地址与函数名进行绑定。
事实上,在静态联编的方式下,同一个成员函数在基类和派生类中的不同版本是不会在运行时根据程序代码的指定进行自动绑定的。必须通过类的虚函数机制,才能实现基类和派生类中的成员函数不同版本的动态联编。
6.虚函数
虚函数是用关键字virtual来修饰基类中的public或protected的成员函数。当在派生类中进行重新定义后,就可在此类层次中具有该成员函数的不同版本。在程序执行过程中,依据基类对象指针所指向的派生类对象,或通过基类引用对象所引用的派生类对象,才能确定哪一个版本被激活,从而实现动态联编。
在基类中,虚函数定义的一般格式如下:
需要说明的是:
(1) 虽然虚函数定义只是在一般函数定义前添加了关键字virtual,但虚函数必须是类中的成员函数。
(2) 可把析构函数定义为虚函数,但不能将构造函数定义为虚函数。通常在释放基类中及其派生类中动态申请的存储空间时,也要把析构函数定义为虚函数,以便实现撤销对象时的多态性。
(3) 虚函数在派生类重新定义时,参数的个数和类型以及函数类型必须和基类中的虚函数完全匹配,这一点和函数重载完全不同。虚函数派生下去仍是虚函数,且可省略virtual关键字。
7.消息传递
对象之间需要相互沟通,沟通的途径就是对象之间收发信息。消息内容包括接收消息的对象的标识,需要调用的函数的标识,以及必要的信息。消息传递的概念使得对现实世界的描述更容易。
8.this指针
this指针是类中一个特殊指针,当类实例化(用类定义对象)时,this指针指向对象自己,而在类的声明时指向类本身。打个比方,this指针就好比你自己一样,当你在屋子里面(类的声明)时,你只知道“房子”这个概念(类名),而不知道房子是什么样子,但你可以看到里面的一切(可以通过this指针引用所有成员),而当你走到屋子外(类的实例),你看到的是一栋具体的房子(this指针指向类的实例)。
1.4.2 类的拷贝构造函数和赋值函数
在C++中对于一个空类,编译器默认4个成员函数:构造函数、析构函数、拷贝构造函数和赋值函数。例如空类:
class Empty { public: };
事实上,一个类总有两种特殊的成员函数:构造函数和析构函数。构造函数的功能是在创建对象时,给数据成员赋初值,即给对象初始化。析构函数的功能是用来释放一个对象,在对象删除前,用它来做一些内存释放等清理工作,它与构造函数的功能正好相反。
类的构造函数和析构函数的一个典型应用是在构造函数中用new为指针成员开辟独立的动态内存空间,而在析构函数中用delete释放它们。
C++还常用下列形式的初始化来将另一个对象作为对象的初值:
例如:
CName o2("DING"); // A:通过构造函数设定初值 CName o3(o2); // B:通过指定对象设定初值
B语句是将o2作为o3的初值,同o2一样,o3这种初始化形式要调用相应的构造函数,但此时找不到相匹配的构造函数,因为CName类没有任何构造函数的形参是CName类对象。事实上,CName还隐含一个特殊的默认构造函数,其原型为Cname(const CName&),这种特殊的默认构造函数称为默认拷贝构造函数。
这种仅仅将内存空间的内容拷贝的方式称为浅拷贝。对于数据成员有指针类型的类来说,由于默认拷贝构造函数无法解决,因此必须自己定义一个拷贝构造函数,在进行数值拷贝之前,为指针类型的数据成员另辟一个独立的内存空间。由于这种拷贝还需另辟内存空间,因而称其为深拷贝。
拷贝构造函数是一种比较特殊的构造函数,除遵循构造函数的声明和实现规则外,还应按下列格式进行定义:
可见,拷贝构造函数的格式就是带参数的构造函数。
由于拷贝操作实质是类对象空间的引用,因此C++规定,拷贝构造函数的参数个数可以1个或多个,但左起的第1个参数必须是类的引用对象,它可以是“类名&对象”或是“const类名&对象”形式,其中“类名”是拷贝构造函数所在类的类名。也就是说,对于CName的拷贝构造函数,可有下列合法的函数原型:
CName( CName &x ); // x为合法的对象标识符 CName( const CName &x ); CName( CName &x , …); // "…"表示还有其他参数 CName( const CName &x, …);
需要说明的是,一旦在类中定义了拷贝构造函数,则隐式的默认拷贝构造函数和隐式的默认构造函数就不再有效了。
下面通过编写类String的实例介绍如何实现类的构造函数、析构函数、拷贝构造函数和赋值函数。
源文件main.cpp的具体内容如下:
#include <QApplication> #include <QtDebug> #include <QObject> class String { public: String(const char *str=NULL); //普通构造函数 String(const String &other); //拷贝构造函数 ~String(void); //析构函数 String &operator=(const String &other); //赋值函数 //private: public: char *m_data; //用于保存字符串 };
构造函数可以根据一个字符串常量创建一个String对象。这个构造函数首先分配了足量的内存,然后把这个字符串常量复制到这块内存,具体代码实现如下:
String::String(const char *str) //普通构造函数 { if(str==NULL) { m_data=new char[1]; *m_data='\0'; } else { int length=strlen(str); m_data = new char[length+1]; strcpy(m_data,str); } }
其中:
● int length=strlen(str):strlen()函数返回字符串常量的实际字符数(不包括NULL终止符)。
● strcpy(m_data,str):将字符串常量的所有字符赋值到在String对象创建过程中为m_data数据成员新分配的内存中。此时,可以像下面这样根据一个字符串常量创建一个新的String对象:
String str("hello ");
所有需要分配系统资源的用户定义类型都需要一个拷贝构造函数。它还可以帮助在函数调用中以传值方式传递一个String参数,并且在当一个函数以值的形式返回String对象时实现“返回时复制”,具体代码实现如下:
String::String(const String &other) //拷贝构造函数 { int length=strlen(other.m_data); m_data = new char[length+1]; strcpy(m_data,other.m_data); }
定义析构函数是为了防止内存泄漏。当一个String对象超出它的作用域时,这个析构函数将会释放它所占用的内存,具体代码实现如下:
String::~String(void) //析构函数 { delete[] m_data; }
其中:
delete[] m_data:由于m_data是内部数据类型,也可以写成delete m_data;
赋值函数可以实现字符串的传值活动,具体代码实现如下:
String & String::operator =(const String &other)//赋值函数 { if(this==&other) //检查自赋值 return *this; delete[] m_data; //释放原有的内存资源 int length=strlen(other.m_data); //分配新的内存资源,并复制内容 m_data = new char[length+1]; strcpy(m_data,other.m_data); return *this; //返回本对象的引用 }
其中:
const有两个作用:
(1) 由于一个const变量是不能随意转换成非const变量的,因此,如不加入const,就会出现问题。例如:
String str1("hello "); const String str2("world "); str1=str2;
会出现错误。
(2) 由于用“+”赋值必须返回一个操作值已知的String对象(除非它是一个const对象),因此,如不加入const,也会出现问题。例如:
String str3("welcome "); String str4("to "); String str5("you "); String str6("you "); str6=str3+str4+str5;
也会出现错误。
main()函数的具体内容如下:
int main(int argc,char *argv[]) { QApplication a(argc,argv); String MyString("My String!"); String MyString2,MyString3=MyString; MyString2=MyString; qDebug()<<"MyString:"<<MyString.m_data<<"MyString2:"<<MyString2.m_data <<"MyString3:"<<MyString3.m_data; return a.exec(); }
最后的运行结果如图1.1所示。
图1.1 类的构造函数实例结果
1.4.3 模板类
可以使用模板类创建对一个类型进行操作的类家族。
template <class T, int i> class TempClass { public: TempClass( void ); ~TempClass( void ); int MemberSet( T a, int b ); private: T Tarray; int arraysize; };
在这个例子中,模板类使用了两个参数,即一个类型T和一个整数i。T参数可以传递一个类型,包括结构和类,i参数必须传第一个整数,因为i在编译时是一个常数,用户可以使用一个标准数组声明来定义一个长度为i的成员。
下面通过实现顺序表的例子来介绍模板类的应用。
首先,在文件SeqList.pro中添加:
CONFIG += console
其次,源文件main.cpp的具体代码如下:
#include <QtCore/QCoreApplication> #include<iostream> using namespace std; //顺序表的实现 模板类 const int MaxSize=100; bool error; template<class T> class SeqList { public: SeqList(){length=0;}; //无参构造函数 SeqList(T a[],int n); //有参构造函数 ~SeqList(){}; //析构 void Insert(int i,T x); //在i位置插入x int GetLength(){return length;}; //求表长 T Del(int i); //删除第i个元素 T Get(int i); //按位查找 取第i个元素 int Locate(T x); //按值查找 求x为第几个元素 void PrintList(); //遍历并输出 private: T data[MaxSize]; int length; };
有参构造函数的具体实现代码如下:
template<class T> SeqList<T>::SeqList(T a[],int n) { int i; if (n>MaxSize) {cout<<"上溢"<<endl; error=true;} else for(i=0;i<n;i++) data[i]=a[i]; }
在i位置插入x的具体实现代码如下:
template<class T> void SeqList<T>::Insert(int i,T x) { int j; if (length>=MaxSize) {cout<<"表满,无法插入"<<endl; error=true;} else if(i>length+1||i<1) {cout<<"位置异常"<<endl; error=true;} else { for(j=length-1;j>=i-1;j--) data[j+1]=data[j]; data[i-1]=x; length++; }; }
删除第i个元素的具体实现代码如下:
template<class T> T SeqList<T>::Del(int i) { int x,j; if(i>length||i<1) {cout<<"位置异常"<<endl; error=true;return 0;} else { x=data[i-1]; for(j=i-1;j<=length-1;j++) data[j]=data[j+1]; length--; return x; } }
按位查找,取第i个元素的具体实现代码如下:
template<class T> T SeqList<T>::Get(int i) { if(i>MaxSize||i<1) {cout<<"位置异常"<<endl; error=true;return -1;} else return data[i-1]; }
按值查找,求x为第几个元素的具体实现代码如下:
template<class T> int SeqList<T>::Locate(T x) { int i; for(i=0;i<length;i++) if (data[i]==x) return i+1; //相同的值? return 0; }
遍历并输出的具体实现代码如下:
template<class T> void SeqList<T>::PrintList() { int i; cout<<"-----------------------------------------------------------"; cout<<endl; cout<<"|"; for(i=0;i<length;i++) cout<<" "<<data[i]<<" |"; cout<<endl; cout<<"-----------------------------------------------------"<<endl; }
main()函数的具体内容如下:
int main(int argc,char *argv[]) { QCoreApplication a(argc,argv); cout<<"顺序表的实现(模板类)"<<endl; cout<<"************************************"<<endl; cout<<"* 1.插入 *"<<endl; cout<<"* 2.按位查找 取第i个元素 *"<<endl; cout<<"* 3.按值查找 求x为第几个元素 *"<<endl; cout<<"* 4.删除 *"<<endl; cout<<"* 5.输出顺序表 *"<<endl; cout<<"* 6.输出表长 *"<<endl; cout<<"* 7/help 输出此表 *"<<endl; cout<<"* 8/exit.退出 *"<<endl; cout<<"************************************"<<endl; int flag,ins_loc,x,tab,len; flag=0; SeqList<int>List; while (flag==0) { error=false; cout<<"Please input the command(input 7 to get the command list):"<<endl; cin>>tab; switch(tab) { case 1: { cout<<"Please input the insert location:"<<endl; cin>>ins_loc; cout<<"Please input the insert num:"<<endl; cin>>x; List.Insert(ins_loc,x); break; } case 2: { cout<<"Please input the location:"<<endl; cin>>ins_loc; x=List.Get(ins_loc); if(!error) cout <<"The number is:"<<x<<endl; break; } case 3: { cout<<"Please input the number you want to find in the list:"<<endl; cin>>x; ins_loc=List.Locate(x); if(!error) cout<<"The number's location is:"<<ins_loc<<endl; break; } case 4: { cout<<"Please input the location you want to delete:"<<endl; cin>>ins_loc; List.Del(ins_loc); break; } case 5: { List.PrintList(); break; } case 6: { if(!error) len=List.GetLength(); cout<<"The length is:"<<len<<endl; break; } case 7: { cout<<"************************************"<<endl; cout<<"* 1.插入 *"<<endl; cout<<"* 2.按位查找 取第i个元素 *"<<endl; cout<<"* 3.按值查找 求x为第几个元素 *"<<endl; cout<<"* 4.删除 *"<<endl; cout<<"* 5.输出顺序表 *"<<endl; cout<<"* 6.输出表长 *"<<endl; cout<<"* 7/help 输出此表 *"<<endl; cout<<"* 8/exit.退出 *"<<endl; cout<<"************************************"<<endl; break; } case 8: { flag=1; break; } default: { cout<<"The command is no found!"<<endl; break; } } } return a.exec(); }
1.4.4 继承与接口
类的继承特性是C++面向对象程序设计的一个非常关键的机制。继承特性可以使一个新类获得其父类的操作和数据结构,程序员只需在新类中增加原有类中没有的成分。
派生类常用的3种继承方式是公有继承(public)、私有继承(private)和保护继承(protected)。
1.公有继承(public)方式
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里保护成员与私有成员相同。
基类成员对派生类的可见性,对派生类来说,基类的公有成员和保护成员可见,它们作为派生类的成员时,都保持原有的状态;基类的私有成员不可见,它们仍然是私有的,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性,对派生类对象来说,基类的公有成员是可见的,其他成员是不可见的。
所以,在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员。
2.私有继承(private)方式
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里私有成员与保护成员相同。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见;它们都作为派生类的私有成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见;它们仍然是私有的,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
3.保护继承(protected)方式
这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言。
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,它们都作为派生类的保护成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见,它们仍然是私有的,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以,在保护继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
例如以下代码:
在文件Animal.pro中添加如下内容:
CONFIG += console
源文件main.cpp中的具体内容如下:
#include <QtCore/QCoreApplication> #include <iostream> using namespace std; class Animal { public: Animal(){}; void eat(){cout<<"eat\n";}; }; class Giraffe:private Animal { public: Giraffe(){}; void StrechNeck(){cout<<"strech neck\n";}; }; class Cat:public Animal { public: Cat(){}; void Meaw(){cout<<"meaw";}; }; void Func(Animal &an) { an.eat(); } int main(int argc,char *argv[]) { QCoreApplication a(argc,argv); Cat dao; Giraffe gir; Func(dao); //right result=eat. Func(gir); //error return a.exec(); }
其中:
● Func(dao):函数Func()要用一个Animal类型的对象,但调用Func(dao)实际上传递的是Cat类的对象。因为Cat是公共继承Animal类,所以Cat类中的对象可以使用Animal类的所有的公有成员变量或函数。Animal对象可以做的事,Cat对象也可以做。
● Func(gir):Giraffe类私有继承了Animal类,意味着对象gir不能直接访问Animal类的成员。其实,在gir对象空间中,包含Animal类的对象,只是无法让其公开访问。
如果将以上代码改成以下形式:
#include <QtCore/QCoreApplication> #include <iostream> using namespace std; class Animal { public: Animal(){}; void eat(){cout<<"eat\n";}; }; class Giraffe:private Animal { public: Giraffe(){}; void StrechNeck(){cout<<"strech neck\n";}; void take(){eat();}; }; void Func(Giraffe &an) { an.take(); } int main(int argc,char *argv[]) { QCoreApplication a(argc,argv); Giraffe gir; gir.StrechNeck(); Func(gir); //ok return a.exec(); }
运行结果如图1.2所示。
图1.2 私有继承实例结果
● Giraffe的成员函数可以像Animal对象那样访问其Animal成员。
保护继承与私有继承类似,继承之后的类相对于基类来说是独立的。保护继承的类对象,在公开场合同样不能使用基类的成员。例如:
#include <QtCore/QCoreApplication> #include <iostream> using namespace std; class Animal { public: Animal(){}; void eat(){cout<<"eat\n";}; }; class Giraffe:protected Animal { public: Giraffe(){}; void StrechNeck(){cout<<"strech neck\n";}; void take(){eat();}; }; int main(int argc,char *argv[]) { QCoreApplication a(argc,argv); Giraffe gir; gir.StrechNeck(); gir.eat(); //error gir.take(); //ok }
1.4.5 多重继承及虚继承
C++支持多重继承,从而大大增强了面向对象程序设计的能力。多重继承是一个类从多个基类派生而来的能力。派生类实际上获取了所有基类的特性。当一个类是两个或多个基类的派生类时,必须在派生类名和冒号之后,列出所有基类的类名,基类间用逗号隔开。派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们。派生类可以是另一个类的基类,相当于形成了一个继承链。当派生类的构造函数被激活时,它的所有基类的构造函数也都会被激活。在面向对象的程序设计中,继承和多重继承一般指公共继承。在无继承的类中,protected和private控制符是没有差别的。在继承中,基类的private对所有的外界都屏蔽(包括自己的派生类),基类的protected控制符对应用程序是屏蔽的,但对其派生类是可访问的。
虚继承是多重继承中特有的概念。虚拟基类是为了解决多重继承而出现的,如图1.3所示。
图1.3 虚继承1
类D继承自类B和类C,而类B和类C都继承自类A,所以等同于如图1.4所示。
图1.4 虚继承2
在类D中会两次出现A。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A就成了虚拟基类。最后形成如图1.5所示的情况。
图1.5 虚继承3
以上内容可以用如下代码表示:
class A; class B:public virtual A; //虚拟继承 class C:public virtual A; //虚拟继承 class D:public B, public C;
1.4.6 多态
通俗地讲,比如开门、开窗户、开电视,这里的“开”就是多态。
多态性可以简单地概括为“一个接口,多种方法”,在程序运行的过程中才决定调用的函数。多态性是面向对象编程领域的核心概念。
多态(Polymorphisn),按字面的意思就是“多种形状”。多态性是允许将父对象设置成为和它的一个或更多的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它子对象的特性以不同的方式运作。简单地说,即允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function)实现的。
虚函数就是允许被其子类重新定义的成员函数。子类重新定义父类虚函数的做法,称为“覆盖”(override)或“重写”。
覆盖(override)和重载(overload)是一个初学者经常混淆的概念。覆盖是指子类重新定义父类的虚函数的做法。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,但是很少有编译器支持这个特性)。而重载,是指编写一个与已有函数同名但是参数表不同的函数。即指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如一个函数既可以接受整型数作为参数,也可以接受浮点数作为参数。
其实,重载的概念并不属于“面向对象编程”。它的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数function func(p: integer):integer和function func(p:string):integer,编译器做过修饰后的函数名称可能是int_func和str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!真正与多态相关的是“覆盖”。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地(记住:是动态!)调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(所调子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。结论就是:重载只是一种语言特性,与多态无关,与面向对象也无关!
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
封装可以隐藏功能实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是为了代码重用。而多态则是为了实现另一个目的:接口重用。而且现实往往是,要有效重用代码很难,真正最具有价值的重用是接口重用,因为“接口是公司最有价值的资源。设计接口比用一堆类来实现这个接口更费时间。而且接口需要耗费更昂贵的人力和时间”。其实,继承为重用代码而存在的理由已经越来越薄弱,因为“组合”可以很好地取代继承的扩展现有代码的功能,而且“组合”的表现更好(至少可以防止“类爆炸”)。因此,继承的存在很大程度上是作为“多态”的基础而非扩展现有代码的方式。
每个虚函数都在虚函数表(vtable)中占有一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存了它的入口地址)。当一个包含虚函数的对象(注意,不是对象的指针)被创建的时候,它在头部附加一个指针,指向vtable中相应的位置。调用虚函数的时候,不管是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了“动态联编”。而普通函数只是简单地跳转到一个固定地址。
例如:实现一个Vehicle类,使其成为抽象数据类型。类Car和类Bus都是从类Vehicle里派生的。
class Vehicle { public: virtual void Move()=0; virtual void Haul()=0; }; class Car : public Vehicle { public: virtual void Move(); virtual void Haul(); }; class Bus : public Vehicle { public: virtual void Move(); virtual void Haul(); };
1.4.7 友元
类具有封装和信息隐藏的特性。只有类的成员函数才能访问类的私有成员,程序中的其他函数是无法访问私有成员的。非成员函数可以访问类中的公有成员。但是如果将数据成员都定义为公有的,这又破坏了隐藏的特性。另外,应该看到在某些情况下,特别是在对某些成员函数多次调用时,由于参数传递、类型检查和安全检查等都需要时间开销,从而影响了程序的运行效率。
为解决上述问题,提出一种使用友元的方案。友元是一种定义在类外部的普通函数,但它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。友元不是成员函数,但是它可以访问类中的私有成员。友元可以提高程序的运行效率。但是,它破坏了类的封装性和隐藏性,使非成员函数也可以访问类的私有成员。
友元可以是一个函数,称为友元函数;友元也可以是一个类,称为友元类。
例1:编写一个程序,设计一个点类Point,求两个点之间的距离。
建立新工程Point.pro,其中,源文件main.cpp的具体代码如下:
#include <QApplication> #include <math.h> #include <QtDebug> class Point { public: Point(float a=0.0f,float b=0.0f):x(a),y(b){}; friend float distance(Point & left,Point & right); private: float x; float y; }; float distance(Point & left,Point & right) { return sqrt((left.x-right.x)*(left.x-right.x)+(left.y-right.y)*(left.y-right.y)); } int main(int argc,char * argv[]) { QApplication a(argc,argv); Point p1(1,2); Point p2(1,4); float d=distance(p1,p2); qDebug()<<"distance:"<<d; return a.exec(); }
最后运行结果如图1.6所示。
图1.6 类Point例子结果
例2:编写一个程序实现模板类的友元重载。
在文件FriendTest.pro中添加的代码如下:
CONFIG += console
源文件main.cpp的具体代码如下:
#include <QtCore/QCoreApplication> #include <iostream> using namespace std; template<class T> class Test; template<class T> ostream& operator<<(ostream& out,const Test<T> &obj); template<class T> class Test { public: Test(int n=0){number=n;}; Test(const Test<T> & copy){number=copy.number;}; friend ostream& operator<< <> //在"<<"后加上"<>"表明这是个函数模板 (ostream& out,const Test<T> &obj); private: int number; }; template<class T> ostream& operator<<(ostream& out,const Test<T> &obj) { out<<obj.number; return out; } int main(int argc,char *argv[]) { QCoreApplication a(argc,argv); Test<int> t(2); cout<<t; return a.exec(); }
最后运行结果如图1.7所示。
图1.7 模板类的友元重载例子结果