C++ 类的组合

解决复杂问题的有效方法就是将其层层分解为简单问题的组合,首先解决简单问题,复杂问题也就迎刃而解了。实际上,这种部件组装的生产方式广泛应用在工业生产中。例如,电视机的一个重要部件是显像管,但很多电视机厂自己并不生产显像管,而是向专门的显像管生产厂去购买。生产显像管的厂家会同时向多家电视机厂供货。这样专业化分工合作,极大提高了生产效率。
在面向对象程序设计中,可以对复杂对象进行分解、抽象,把一个复杂对象分解为简单对象的组合,由比较容易理解和实现的部件对象装配完成。

1.组合

下面创建一个圆类:

class Circle//定义类Cricle及其数据和方法
{
public:       //外部接口
	Circle(float r){}//构造函数
	float circumference();//计算圆的周长
	float area();//计算圆的面积
private://私有数据成员
	float r;//圆的半径
};

可以看到,类Circle中包含着float类型的数据,这样用C++的基本数据类型作为类的组成部件。实际上类的成员数据既可以是基本类型也可以是自定义类型,当然也可以是类的对象。由此便可以采用部件组装的方法,利用已有类的对象来构建新类,这些部件类比起整体类来说,更易于设计和实现。

类的组合描述的就是一个类内嵌其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。

**当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将首先被自动创建。**因为部件对象是复杂对象的一部分,因此,在创建对象时既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化

理解所创建的对象和其内嵌对象的构造函数被调用的顺序

组合类构造函数定义的一般形式为:

类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),...
{类的初始化}

其中,“内嵌对象1(形参表),内嵌对象2(形参表),…”称为初始化列表,其作用是对内嵌对象进行初始化。
对基本类型的数据成员也可以这样初始化:

class Circle//定义类Cricle及其数据和方法
{
public:       //外部接口
	Circle(float r)radius(r){}//构造函数,成员初始化列表
	float circumference();//计算圆的周长
	float area();//计算圆的面积
private://私有数据成员
	float radius;//圆的半径
};

在创建一个组合类的对象时,不仅它自身的构造函数的函数体被执行,而且还调用其内嵌对象的构造函数。这时构造函数的调用顺序如下:
(1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的顺序。注意,内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
(2)执行本类构造函数的函数体。
【提示】
如果有些内嵌对象没有出现在构造函数的初始化列表中,那么在第(1)步,该内嵌对象的默认构造函数将被执行。这样,如果一个类存在内嵌对象,尽管编译系统自动生成的隐含默认构造函数的函数体为空,但在执行默认构造函数时,如果声明组合类的对象没有指定对象的初始值,则默认形式(无形参)的构造函数被调用,这时内嵌对象的默认形式构造函数也会被调用。这时隐含的默认构造函数并非什么也不做。
【注意】
有些数据成员的初始化,必须在构造函数的初始化列表中进行。这些数据成员包括两类,一类是那些没有默认构造函数的内嵌对象——因为这类对象初始化时必须提供参数,另一类是引用类型的数据成员——因为引用型变量必须在初始化时绑定引用的对象。如果一个类包括着两类成员,那么编译器不能够为这个类提供隐含的默认构造函数,这时必须编写显式的构造函数,并且在每个构造函数的初始化列表中至少为这两类数据成员初始化。
**析构函数的调用执行顺序与构造函数相反。**析构函数的函数体被执行完毕后,内嵌对象的析构函数被一 一执,这些内嵌对象的析构函数调用顺序与它们在组合类的定义中出现的顺序相反。
【提示】
由于要调用内嵌函数的析构函数,所以隐含的析构函数并非什么也不做。
当存在组合类关系时,拷贝构造函数如何编写:
对于一个类,如果程序员没有编写拷贝构造函数,编译系统会在必要时自动生成一个隐含的拷贝构造函数,这个隐含的拷贝构造函数会自动调用内嵌对象的拷贝构造函数,为各个内嵌对象初始化。
如果要为组合类编写拷贝构造函数,则需要为内嵌成员对象的拷贝构造函数传递参数。
【例】假设C类中包含B类的对象b作为成员,C类的拷贝构造函数形式如下:

C::C(C&c1):b(c1.b){...}

【例题】我们使用一个类Line来描述线段,使用另一个类Point的对象来表示端点。用组合类来解决,使Line类包括Point类的两个对象p1和p2,作为其数据成员。Line类具有计算线段长度的功能,在构造函数中实现。

//类Point的定义
class Point
{
public:
	Point(int xx = 0, int yy = 0)
	{
		x = xx;
		y = yy;
	}
	Point(Point& p);
	int getX()
	{
		return x;
	}
	int getY()
	{
		return y;
	}
private:
	int x, y;
};

//Point拷贝构造函数的实现
Point::Point(Point& p)
{
	x = p.x;
	y = p.y;
	cout << "调用Point的拷贝构造函数" << endl;	
}

//Line类的定义
class Line
{
public:
	Line(Point xp1, Point xp2);
	Line(Line& l);
		double getLen()
	{
		return len;
	}
private:
	Point p1, p2;//Point类的对象p1,p2
	double len;
};

//组合类的构造函数Line的实现
Line::Line(Point xp1, Point xp2):p1(xp1),p2(xp2)
{
	cout << "调用Line的构造函数" << endl;
	double x = static_cast<double>(p1.getX() - p2.getX());
	double y = static_cast<double>(p1.getY() - p2.getY());
	len = sqrt(x * x + y * y);
}

//组合类的拷贝构造函数的实现
Line::Line(Line& l) :p1(l.p1), p2(l.p2)
{
	cout << "调用Line的拷贝构造函数" << endl;
	len = l.len;
}

//主函数
int main()
{
	Point myp1(1, 1), myp2(4, 5);//建立Point类的对象myp1,myp2
	Line line1(myp1, myp2);//建立Line类的对象line1
	Line line2(line1);  //利用拷贝构造函数用旧对象line1建立一个新对象line2
	cout << "两点之间的距离为:" ;
	cout << line1.getLen() << endl;
	cout << "两点之间的距离为:" ;
	cout << line2.getLen() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
结果分析:
主程序在执行时,首先生成两个Point类的对象myp1和myp2,然后在构造Line类的对象line1,接着通过拷贝构造函数建立Line类的第二个对象line2,最后输出两点之间的距离。在整个运行过程中,Point类的拷贝构造函数被调用了6次,而且都是在Line类构造函数体运行之前进行的,它们分别是两个对象在Line类构造函数进行函数参数形实结合时,初始化内嵌对象时,以及拷贝构造line2时被调用的。两点的距离在Line类的构造函数中求得,存放在其私有数据成员len中,只能通过公有成员函数getLen()来访问。
在这里插入图片描述

2.前向引用声明

C++的类应当先定义,再使用,但是在处理复杂问题、考虑到组合时,很可能遇到两个类相互引用的情况,这种情况也称为循环依赖。例如:

class A   //A类的定义
{
public:   //外部接口
	void f(B b);  //以B类对象b为形参的成员函数
};
class B  //B类的定义
{
public:  //外部接口
	void g(A a);  //以A类对象a为形参的成员函数
};

这里类A的公有成员函数f的形参是B类的对象,同时B的公有成员函数g也以A类的对象为形参。由于在使用一个类之前,必须首先定义该类,因此无论将哪一个类的定义放在前面都会引起编译错误。解决这种问题的办法就是使用前向引用声明。前向引用声明,是在引用未定义的类之前,将该类的名字告诉编译器,使编译器知道那是一个类名。这样当程序中使用这个类名时,编译器就不会认为时错误的,而类的完整定义可以在程序的其他地方。
在上述程序中加上下面的前向引用声明问题就解决了:

class B;   //前向引用声明
class A   //A类的定义
{
public:   //外部接口
	void f(B b);  //以B类对象b为形参的成员函数
};
class B  //B类的定义
{
public:  //外部接口
	void g(A a);  //以A类对象a为形参的成员函数
};

使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,但是在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联函数中使用该类的对象。如下:

class Fred;   //前向引用声明
class Barney
{
	Fred x;  //error  类Fred的定义尚不完善
};
class Fred
{
	Barney y;
};

对于这段程序,编译器指出错误。原因是对类名Fred的前向引用声明只能说明Fred是一个类名,而不能给出该类的完整定义,因此在类Barney中就不能定义Fred的数据成员。因此使两个类以彼此的对象为数据成员是不合法的。
再看下面这段程序:

class Fred;   //前向引用声明
class Barney
{
public:
	void method()
	{
		x.yabbaDabbaDo();//error Fred类的对象在定义前被使用
	}
private:
	Fred& x;  //正确 结果前向引用声明,可以声明Fred类的对象引用或指针
};
class Fred
{
public:
	void yabbaDabbaDo();
	Barney& y;
};

编译器在编译时会指出错误,因为在类Barney的内联函数中使用了由x所指向的Fred类的对象,而此时Fred类尚未被完整定义。解决这个问题的方法是,更改这两个类的定义顺序,或者将函数method()改为非内联形式,并且在类Fred的完整定义之后,再给出函数的定义。
【注意】使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值