【出处】http://www.jizhuomi.com/software/53.html
http://www.cnblogs.com/whitewolf/archive/2010/05/03/1726519.html
在我们对现实中的某些事物抽象成类时,可能会形成很复杂的类,为了更简洁的进行软件开发,我们经常把其中相对比较独立的部分拿出来定义成一个个简单的类,这些比较简单的类又可以分出更简单的类,最后由这些简单的类再组成我们想要的类。比如,我们想要创建一个计算机系统的类,首先计算机由硬件和软件组成,硬件又分为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
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是一个类,它具有类的所有属性,具体的定义在其他地方。
2、关于类的组合和继承首先它们都是实现系统功能重用,代码复用的最常用的有效的设计技巧,都是在设计模式中的基础结构。相信大家已了解的,类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的,所以我们一般称之为白盒复用。对象持有(其实就是组合)要求建立一个号的接口,但是整体类和部分类之间不会去关心各自的实现细节,即它们之间的实现细节是不可见的,故成为黑盒复用。
继承是在编译时刻静态定义的,即是静态复用,在编译后子类和父类的关系就已经确定了。而组合这是运用于复杂的设计,它们之间的关系是在运行时候才确定的,即在对对象没有创建运行前,整体类是不会知道自己将持有特定接口下的那个实现类。在扩展方面组合比集成更具有广泛性。
继承中父类定义了子类的部分实现,而子类中又会重写这些实现,修改父类的实现,设计模式中认为这是一种破坏了父类的封装性的表现。这个结构导致结果是父类实现的任何变化,必然导致子类的改变。然而组合这不会出现这种现象。
对象的组合还有一个优点就是有助于保持每个类被封装,并被集中在单个任务上(类设计的单一原则)。这样类的层次结构不会扩大,一般不会出现不可控的庞然大类。而累的继承就可能出来这些问题,所以一般编码规范都要求类的层次结构不要超过3层。组合是大型系统软件实现即插即用时的首选方式。
在设计模式中这两个概念同时出现的地方就是Adapter模式:对象适配(组合)和类适配(继承)。一般我们提倡用对象适配而不是类适配。基于上面的原因。还有就是在我们的Java和.NET这些完全面向对象的语言而言类的继承是单继承,取消了C++等的多继承。下面放两个这两种方式的UML图:
类适配图:
对象适配图:
最后还说一句,“优先使用对象组合,而不是继承”是面向对象设计的第二原则。但并不是说什么都设计都用组合,只是优先考虑组合,更不是说继承即使不好的设计,应该用组合,应为他们之间也有各自的优势。下面是他们之间的优缺点比比较表:
组 合 关 系 |
继 承 关 系 |
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 |
缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 |
优点:具有较好的可扩展性 |
缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 |
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 |
缺点:不支持动态继承。在运行时,子类无法选择不同的父类 |
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 |
缺点:子类不能改变父类的接口 |
缺点:整体类不能自动获得和局部类同样的接口 |
优点:子类能自动继承父类的接口 |
缺点:创建整体类的对象时,需要创建所有局部类的对象 |
优点:创建子类的对象时,无须创建父类的对象 |