上一讲鸡啄米给大家讲了构造函数和析构函数,让大家了解了类的创建和释放过程。这一节讲讲类的组合。
在我们对现实中的某些事物抽象成类时,可能会形成很复杂的类,为了更简洁的进行软件开发,我们经常把其中相对比较独立的部分拿出来定义成一个个简单的类,这些比较简单的类又可以分出更简单的类,最后由这些简单的类再组成我们想要的类。比如,我们想要创建一个计算机系统的类,首先计算机由硬件和软件组成,硬件又分为CPU、存储器等,软件分为系统软件和应用软件,如果我们直接创建这个类是不是很复杂?这时候我们就可以将CPU写一个类,存储器写一个类,其他硬件每个都写一个类,硬件类就是所有这些类的组合,软件也是一样,也能做成一个类的组合。计算机类又是硬件类和软件类的组合。
类的组合其实描述的就是在一个类里内嵌了其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。简单说,一个类中有若干数据成员是其他类的对象。以前的教程中我们看到的类的数据成员都是基本数据类型的或自定义数据类型的,比如int、float类型的或结构体类型的,现在我们知道了,数据成员也可以是类类型的。
如果在一个类中内嵌了其他类的对象,那么创建这个类的对象时,其中的内嵌对象也会被自动创建。因为内嵌对象是组合类的对象的一部分,所以在构造组合类的对象时不但要对基本数据类型的成员进行初始化,还要对内嵌对象成员进行初始化。
组合类构造函数定义(注意不是声明)的一般形式为:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),...
{
类的初始化
}
其中,“内嵌对象1(形参表),内嵌对象2(形参表),...”成为初始化列表,可以用于完成对内嵌对象的初始化。其实,一般的数据成员也可以这样初始化,就是把这里的内嵌对象都换成一般的数据成员,后面的形参表换成用来的初始化一般数据成员的变量形参,比如,Point::Point(int xx, int yy):X(xx),Y(yy) { },这个定义应该怎么理解呢?就是我们在构造Point类的对象时传入实参初始化xx和yy,然后用xx的值初始化Point类的数据成员X,用yy的值初始化数据成员Y。
声明一个组合类的对象时,不仅它自身的构造函数会被调用,还会调用其内嵌对象的构造函数。那么,这些构造函数的调用是什么顺序呢?首先,根据前面说的初始化列表,按照内嵌对象在组合类的声明中出现的次序,依次调用内嵌对象的构造函数,然后再执行本类的构造函数的函数体。比如下面例子中对于Distance类中的p1和p2就是先调用p1的构造函数,再调用p2的构造函数。因为Point p1,p2;是先声明的p1后声明的p2。最后才是执行Distance构造函数的函数体。
如果声明组合类的对象时没有指定对象的初始值的话,就会自动调用无形参的构造函数,构造内嵌对象时也会对应的调用内嵌对象的无形参的构造函数。析构函数的执行顺序与构造函数正好相反。
这里鸡啄米给大家一个类的组合的例子,其中,Distance类就是组合类,可以计算两个点的距离,它包含了Point类的两个对象p1和p2。
#include <iostream> using namespace std; class Point { public: Point(int xx,int yy) { X=xx; Y=yy; } //构造函数 Point(Point &p); int GetX(void) { return X; } //取X坐标 int GetY(void) { return Y; } //取Y坐标 private: int X,Y; //点的坐标 }; Point::Point(Point &p) { X = p.X; Y = p.Y; cout << "Point拷贝构造函数被调用" << endl; } class Distance { public: Distance(Point a,Point b); //构造函数 double GetDis() { return dist; } private: Point p1,p2; double dist; // 距离 }; // 组合类的构造函数 Distance::Distance(Point a, Point b):p1(a),p2(b) { cout << "Distance构造函数被调用" << endl; double x = double(p1.GetX() - p2.GetX()); double y = double(p1.GetY() - p2.GetY()); dist = sqrt(x*x + y*y); return; } int _tmain(int argc, _TCHAR* argv[]) { Point myp1(1,1), myp2(4,5); Distance myd(myp1, myp2); cout << "The distance is:"; cout << myd.GetDis() << endl; return 0; }
这段程序的运行结果是:
Point拷贝构造函数被调用
Point拷贝构造函数被调用
Point拷贝构造函数被调用
Point拷贝构造函数被调用
Distance构造函数被调用
The distance is:5
Point类的构造函数是内联成员函数,内联成员函数在鸡啄米:C++编程入门系列之十三(类与对象:类的声明、成员的访问控制和对象)中已经讲过。
鸡啄米给大家分析下这个程序,首先生成两个Point类的对象,然后构造Distance类的对象myd,最后输出两点的距离。Point类的拷贝构造函数被调用了4次,而且都是在Distance类构造函数执行之前进行的,在Distance构造函数进行实参和形参的结合时,也就是传入myp1和myp2的值时调用了两次,在用传入的值初始化内嵌对象p1和p2时又调用了两次。两点的距离在Distance的构造函数中计算出来,存放在其私有数据成员dist中,只能通过公有成员函数GetDis()来访问。
鸡啄米再跟大家说下类组合时的一种特殊情况,就是两个类可能相互包含,即类A中有类B类型的内嵌对象,类B中也有A类型的内嵌对象。我们知道,C++中,要使用一个类必须在使用前已经声明了该类,但是两个类互相包含时就肯定有一个类在定义之前就被引用了,这时候怎么办呢?就要用到前向引用声明了。前向引用声明是在引用没有定义的类之前对该类进行声明,这只是为程序声明一个代表该类的标识符,类的具体定义可以在程序的其他地方,简单说,就是声明下这个标识符是个类,它的定义你可以在别的地方找到。
比如,类A的公有成员函数f的形参是类B的对象,同时类B的公有成员函数g的形参是类A的对象,这时就必须使用前向引用声明:
class B; //前向引用声明 class A { public: void f(B b); }; class B { public: void g(A a); };
这段程序的第一行给出了类B的前向引用声明,说明B是一个类,它具有类的所有属性,具体的定义在其他地方。
今天鸡啄米讲了类的组合,这个知识还是很实用的,可以让我们的程序更灵活,不至于太庞杂让人摸不着头脑。面向对象是不是很强大啊?希望大家从思想上领会它。