关于组合类中构造函数调用的分析
组合类相关的概念
一种包含与被包含的关系。
例如,用一个类来描述计算机系统.首先可以把它分解为硬件和软件
硬件包含中央处理单元(CPU)存储器、输入设备和输出设备,软件可以包括系统软件和应用软件。这些部分每一个都可以进一步分解,用类的观点来描述,它就是一个类的组合。
组合的概念
类成员是另一个类对象
可以在已有抽象的基础上实现更复杂的抽象
组合类的构造函数
不仅要负责本类成员数据初始化,还要对对象成员初始化
当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将首先被自动创建。因为部件对象是复杂对象的一部分,因此,在创建对象时既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化。
在创建一一个组合类的对象时,不仅它自身的构造函数的函数体将被执行,而且还将调用其内嵌对象的构造函数。这时构造函数的调用顺序如下。
(1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序。
注意,内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
(2)执行本类构造函数的函数体。
提示如果有些内嵌对象没有出现在构造函数的初始化列表中,那么在第(1)步中,该内嵌对象的默认构造函数将被执行。这样,如果一个类存在内嵌的对象,尽管编译系统自动生成的隐含的默认构造函数的函数体为空,但在执行默认构造函数时,如果声明组合类的对象时没有指定对象的初始值,则默认形式(无形参)的构造函数被调用,这时内嵌对象的默认形式构造函数也会被调用。这时隐含的默认构造函数并非什么也不做。
注意有些数据成员 的初始化,必须在构造函数的初始化列表中进行。
这些数据成员包括两类,
一类是那些没有默认构造函数的内嵌对象——因为这类对象初始化时必须
提供参数。(内嵌对象中仅提供了其他形式的构造(有参)(拷贝),没有重写默认构造)
另一类是引用类型的数据成员——因为引用型变量必须在初始化时绑定引用的对象。
//int &p;不能独立存在
int &p=x; //引用型变量必须在初始化时绑定引用的对象。
如果一个类包括这两类成员,那么编译器不能够为这个类提供隐含的默认构造函数,这时必须编写显式的构造函数,并且在每个构造函数的初始化列表中至少为这两类数据成员初始化。
析构函数的调用执行顺序与构造函数刚好相反。析构函数的函数体被执行完毕后,内嵌对象的析构函数被一一执行.这些内嵌对象的析构函数调用顺序与它们在组合类的定义中出现的次序刚好相反。(先进后出)
由于要调用内嵌对象的析构函数,所以有时隐含的析构函数并非什么也不做。
当存在类的组合关系时,拷贝构造函数该如何编写呢?
对于一个类.如果程序员没有编写拷贝构造函数,编译系统会在必要时自动生成一个隐含的拷贝构造函数,这个隐含的函数会自动调用内嵌对象的拷贝构造函数,为各个内嵌对象初始化。
如果要为组合类编写拷贝构造函数,则需要为内嵌成员对象的拷贝构造函数传递参数。
下面给出一组组合类的代码
点类
//point.h
using namespace std;
#ifndef _POINT_H
#define _POINT_H
class Point {
public:
Point(int xx = 0, int yy = 0);//带默认形参值的有参构造
Point(Point &p); //拷贝构造
void setXY(int xx, int yy); //设置x,y的值
int getX() { return x; } //获取x
int getY() { return y; } //获取y
private: int x, y;
};
#endif
//point.cpp
#include <iostream>
#include "point.h"
using namespace std;
Point::Point(int xx, int yy) {
x = xx; y = yy;
cout << "\nThe Point (" << x << ", " << y << ")" << endl;
cout << "Calling the constructor of Point...\n";
}
Point::Point(Point &p) {
x = p.x; y = p.y;
cout << "\nThe Point (" << x << ", " << y << ")" << endl;
cout << "Calling the copy constructor of Point...\n";
}
void Point::setXY(int xx, int yy) {
x = xx; y = yy;
}
线段类
//line.h
#include "point.h"
#ifndef _LINE_H
#define _LINE_H
class Line {
public:
Line(Point xp1, Point xp2); //传入两个圆类做形参的有参构造
Line(Line &l); //拷贝构造
double getLen() { return len; } //获取线段长度
private:
Point p1, p2;
double len;
};
#endif
//line.cpp
#include <iostream>
#include <math.h>
#include "line.h"
using namespace std;
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
p2 = xp2;
cout << "\nThe Line (" << p1.getX() << ", " << p1.getY() << ")"
<< "------" << "(" << p2.getX() << ", " << p2.getY() << ")"
<< endl;
cout << "Calling constructor of Line...\n";
double x = p1.getX() - p2.getX();
double y = p1.getY() - p2.getY();
len = sqrt(x * x + y * y);
}
Line::Line(Line &l) : p1(l.p1), p2(l.p2) {
cout << "\nThe Line (" << p1.getX() << ", " << p1.getY() << ")"
<< "------" << "(" << p2.getX() << ", " << p2.getY() << ")"
<< endl;
cout << "Calling the copy constructor of Line...\n";
len = l.len;
}
测试代码
#include <iostream>
#include "point.h"
#include "line.h"
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main() {
Point myp1(1, 1);
Point myp2(4, 5);
Line line1(myp1, myp2);
Line line2(line1);
cout << "The length of the line1 is: ";
cout << line1.getLen() << '\n';
cout << "The length of the line2 is: ";
cout << line2.getLen() << '\n';
return 0;
}
分别对以上4个对象的创建进行分析
Point::Point(int xx, int yy) {
x = xx; y = yy;
cout << "\nThe Point (" << x << ", " << y << ")" << endl;
cout << "Calling the constructor of Point...\n";
}
Point myp1(1, 1);
Point myp2(4, 5);
对点对象myp1的创建
由于是值传递,系统先后创建出形参yy,xx接受传入的1,1(形参列表中是从左至右创建的)然后将形参的值赋值给myp1自身。局部对象xx,yy在函数执行完后释放。
1、调用了一次点(1,1)的有参构造创建myp1。
对象myp2与myp1同理
2、调用了一次点(4,5)的有参构造创建myp2。
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
p2 = xp2;
cout << "\nThe Line (" << p1.getX() << ", " << p1.getY() << ")"
<< "------" << "(" << p2.getX() << ", " << p2.getY() << ")"
<< endl;
cout << "Calling constructor of Line...\n";
double x = p1.getX() - p2.getX();
double y = p1.getY() - p2.getY();
len = sqrt(x * x + y * y);
}
Line line1(myp1, myp2);
对线段对象line1的创建:(回顾一下我上面讲的重点)
当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将首先被自动创建。因为部件对象是复杂对象的一部分,因此,在创建对象时既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化。
在创建一一个组合类的对象时,不仅它自身的构造函数的函数体将被执行,而且还将调用其内嵌对象的构造函数。这时构造函数的调用顺序如下。
(1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序。
注意,内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
(2)执行本类构造函数的函数体。
即在创建line1之前先要创建他的点类p1,p2,而他的点类的初始化拷贝是通过形参xp1,xp2传入的,而形参xp1,xp2为类对象,而且是值传递,故还要先后创建出xp2,xp1俩个局部point类的变量,拷贝传入的点类myp2,myp1
3、调用了一次点(4,5)的拷贝构造函数构造xp2; (xp2(myp2))
4、调用了一次点(1,1)的拷贝构造函数构造xp1; (xp1(myp1))
p.s.若形参列表中用引用形式,没有3,4过程
Line::Line(Point &xp1, Point &xp2) : p1(xp1), p2(xp2)
然后再按照Line类中定义的顺序先后构造p1,p2(与初始化列表中的顺序无关,即初始化列表中先写p2,再写p1,也是p1先构造)
5、调用了一次点(1,1)的拷贝构造函数构造line1的p1; (p1(xyp1))
6、调用了一次点(4,5)的拷贝构造函数构造line1的p2; (p2(xyp2))
最后待line1成员对象创建好了之后
7、调用了一次线段(1,1)–(4,5)的有参构造函数构造line1; line1(myp1,myp2)
p.s.在这里如果没有初始化列表构造p1,p2会怎样?
Line::Line(Point xp1, Point xp2) {
p1 = xp1;
p2 = xp2;
}
提示如果有些内嵌对象没有出现在构造函数的初始化列表中,那么在第(1)步中,该内嵌对象的默认构造函数将被执行。这样,如果一个类存在内嵌的对象,尽管编译系统自动生成的隐含的默认构造函数的函数体为空,但在执行默认构造函数时,如果声明组合类的对象时没有指定对象的初始值,则默认形式(无形参)的构造函数被调用,这时内嵌对象的默认形式构造函数也会被调用。这时隐含的默认构造函数并非什么也不做。
即系统会自动调用p1,p2自身的默认构造函数,将p1,p2构造出来。
然后再调用系统默认的重载=运算符,用xp1,xp2给p1,p2值拷贝
但这里point类中并没有默认构造函数
注意有些数据成员 的初始化,必须在构造函数的初始化列表中进行。
这些数据成员包括两类,
一类是那些没有默认构造函数的内嵌对象——因为这类对象初始化时必须
提供参数。(内嵌对象中仅提供了其他形式的构造(有参)(拷贝),没有重写默认构造)
上述写法却可以运行,为啥?
Point(int xx = 0, int yy = 0);//带默认形参值的有参构造
因为point类中有参构造的所有参数都带了默认参数值,不传参数也可以构造p1,p2,相当于默认构造。故上述写法可以加上一条。
没有默认构造的执行所有参数都带了默认参数值的有参构造
这样没有默认构造函数的内嵌对象也可以不初始化列表。
但改成如下这样
Point(int xx , int yy = 0);//带默认形参值的有参构造
系统就不能愉快的调用
Line::Line(Point xp1, Point xp2) {
p1 = xp1;
p2 = xp2;
}
这个了,会显示point类缺乏默认构造。
好的,言归正传分析最后一个对象的创建过程
Line::Line(Line &L) : p1(L.p1), p2(L.p2) {
cout << "\nThe Line (" << p1.getX() << ", " << p1.getY() << ")"
<< "------" << "(" << p2.getX() << ", " << p2.getY() << ")"
<< endl;
cout << "Calling the copy constructor of Line...\n";
len = L.len;
}
Line line2(line1);
由于引用的方式(Line &L)传入line1 ,故不会启动拷贝构造再创建一个参数对象L,相当于&L直接寄生到line1上,L就是line1,然后在创建line2前先创建啊他的对象成员p1,p2。
8、调用了一次点(1,1)的拷贝构造函数构造line2的p1; (p1(L.p1))
9、调用了一次点(4,5)的拷贝构造函数构造line2的p2; (p2(L.p2))
最后待line2成员对象创建好了之后
10、调用了一次线段(1,1)–(4,5)的拷贝构造函数构造line2; line2(line1)
总结一下
Point myp1(1, 1);
Point myp2(4, 5);
Line line1(myp1, myp2);
Line line2(line1);
这4个过程一共调用了10次构造函数
1、调用了一次点(1,1)的有参构造创建myp1。
2、调用了一次点(4,5)的有参构造创建myp2。
3、调用了一次点(4,5)的拷贝构造函数构造xp2; (xp2(myp2))
4、调用了一次点(1,1)的拷贝构造函数构造xp1; (xp1(myp1))
5、调用了一次点(1,1)的拷贝构造函数构造line1的p1; (p1(xyp1))
6、调用了一次点(4,5)的拷贝构造函数构造line1的p2; (p2(xyp2))
7、调用了一次线段(1,1)–(4,5)的有参构造函数构造line1; line1(myp1,myp2)
8、调用了一次点(1,1)的拷贝构造函数构造line2的p1; (p1(L.p1))
9、调用了一次点(4,5)的拷贝构造函数构造line2的p2; (p2(L.p2))
10、调用了一次线段(1,1)–(4,5)的拷贝构造函数构造line2; line2(line1)
运行结果如图
之前没有初始化列表的调用结果如下:
可以清晰的看到差别,第一个line构造之前启用的是两个0,0的构造
Line::Line(Point xp1, Point xp2) {
p1 = xp1;
p2 = xp2;
}
好的,关于组合类中构造函数调用的分析圆满完成!