派生类的构造函数
在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员初始化。解决这个问题的思路是,在执行派生类的构造函数时,调用基类的构造函数。
#include <iostream>
#include<string>
using namespace std;
class Student//声明基类Student
{
public:
Student(int n,string nam,char s) //基类构造函数
{
num=n;
name=nam;
sex=s;
}
~Student( ){ } //基类析构函数
protected : //保护部分
int num;
string name;
char sex ;
};
class Student1: public Student //声明派生类Student1
{
public : //派生类的公用部分
Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)//派生类构造函数
{
age=a; //在函数体中只对派生类新增的数据成员初始化
addr=ad;
}
void show( )
{
cout<<"num: "<<num<<endl;
cout<<"name: "<<name<<endl;
cout<<"sex: "<<sex<<endl;
cout<<"age: "<<age<<endl;
cout<<"address: "<<addr<<endl<<endl;
}
~Student1( ){ } //派生类析构函数
private : //派生类的私有部分
int age;
string addr;
};
int main( )
{
Student1 stud1(10010,"Wang-li",'f',19,"115 Beijing Road,Shanghai");
Student1 stud2(10011,"Zhang-fun",'m',21,"213 Shanghai Road,Beijing");
stud1.show( ); //输出第一个学生的数据
stud2.show( ); //输出第二个学生的数据
return 0;
}
请注意派生类构造函数首行的写法:
Student1(int n, string nam, char s, int a, string ad):Student(n, nam, s)
其一般形式为:
派生类构造函数名(总参数表列): 基类构造函数名(参数表列) {派生类中新增数据成员初始化语句}
也可以将派生类构造函数在类外面定义,而在类体中只写该函数的声明:
Student1(int n, string nam, char s, int a, string ad);
在类的外面定义派生类构造函数:
Student1 :: Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)
{
age=a;
addr=ad;
}
可以将对age和addr的初始化也用初始化表处理,将构造函数改写为以下形式:
Student1(int n, string nam,char s,int a, string ad):Student(n,nam,s),age(a),addr(ad){}
在建立一个对象时,执行构造函数的顺序是:
- 派生类构造函数先调用基类构造函数;
- 再执行派生类构造函数本身(即派生类构造函数的函数体)。
- 在派生类对象释放时,先执行派生类析构函数~Student1( ),再执行其基类析构函数~Student( )。
有子对象的派生类的构造函数
#include <iostream>
#include <string>
using namespace std;
class Student//声明基类
{
public: //公用部分
Student(int n, string nam ) //基类构造函数,与例11.5相同
{
num=n;
name=nam;
}
void display( ) //成员函数,输出基类数据成员
{
cout<<"num:"<<num<<endl<<"name:"<<name<<endl;
}
protected: //保护部分
int num;
string name;
};
class Student1: public Student //声明公用派生类Student1
{
public:
Student1(int n, string nam,int n1, string nam1,int a, string ad):Student(n,nam),monitor(n1,nam1) //派生类构造函数
{
age=a;
addr=ad;
}
void show( )
{
cout<<"This student is:"<<endl;
display(); //输出num和name
cout<<"age: "<<age<<endl; //输出age
cout<<"address: "<<addr<<endl<<endl; //输出addr
}
void show_monitor( ) //成员函数,输出子对象
{
cout<<endl<<"Class monitor is:"<<endl;
monitor.display( ); //调用基类成员函数
}
private: //派生类的私有数据
Student monitor; //定义子对象(班长)
int age;
string addr;
};
int main( )
{
Student1 stud1(10010,"Wang-li",10001,"Li-sun",19,"115 Beijing Road,Shanghai");
stud1.show( ); //输出学生的数据
stud1.show_monitor(); //输出子对象的数据
return 0;
}
请注意在派生类Student1中有一个数据成员:
Student monitor; //定义子对象 monitor(班长)
派生类构造函数的任务应该包括3个部分:
- 对基类数据成员初始化;
- 对子对象数据成员初始化;
- 对派生类数据成员初始化。
程序中派生类构造函数首部如下:
Student1(int n, string nam,int n1, string nam1,int a, string ad):
Student(n,nam),monitor(n1,nam1)
在上面的构造函数中有6个形参,前两个作为基类构造函数的参数,第3、第4个作为子对象构造函数的参数,第5、第6个是用作派生类数据成员初始化的。
归纳起来,定义派生类构造函数的一般形式为:
派生类构造函数名(总参数表列): 基类构造函数名(参数表列), 子对象名(参数表列)
{
派生类中新增数成员据成员初始化语句
}
执行派生类构造函数的顺序是:
- 调用基类构造函数,对基类数据成员初始化;
- 调用子对象构造函数,对子对象数据成员初始化;
- 再执行派生类构造函数本身,对派生类数据成员初始化。
派生类构造函数的总参数表列中的参数,应当包括基类构造函数和子对象的参数表列中的参数。基类构造函数和子对象的次序可以是任意的。如果有多个子对象,派生类构造函数的写法依此类推,应列出每一个子对象名及其参数表列。
多层派生时的构造函数
#include <iostream>
#include<string>
using namespace std;
class Student//声明基类
{
public://公用部分
Student(int n, string nam)//基类构造函数
{
num=n;
name=nam;
}
void display( )//输出基类数据成员
{
cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
}
protected://保护部分
int num;//基类有两个数据成员
string name;
};
class Student1: public Student//声明公用派生类Student1
{
public:
Student1(int n,string nam,int a):Student(n,nam)//派生类构造函数
{age=a;}//在此处只对派生类新增的数据成员初始化
void show( ) //输出num,name和age
{
display( ); //输出num和name
cout<<"age: "<<age<<endl;
}
private://派生类的私有数据
int age; //增加一个数据成员
};
class Student2:public Student1 //声明间接公用派生类Student2
{
public://下面是间接派生类构造函数
Student2(int n, string nam,int a,int s):Student1(n,nam,a) {score=s;}
void show_all( ) //输出全部数据成员
{
show( ); //输出num和name
cout<<"score:"<<score<<endl; //输出age
}
private:
int score; //增加一个数据成员
};
int main( )
{
Student2 stud(10010,"Li",17,89);
stud.show_all( ); //输出学生的全部数据
return 0;
}
基类的构造函数首部:
Student(int n, string nam)
派生类Student1的构造函数首部:
Student1(int n, string nam],int a):Student(n,nam)
派生类Student2的构造函数首部:
Student2(int n, string nam,int a,int s):Student1(n,nam,a)
不要列出每一层派生类的构造函数,只需写出其上一层派生类(即它的直接基类)的构造函数即可。初始化的顺序是:
- 先初始化基类的数据成员num和name。
- 再初始化Student1的数据成员age。
- 最后再初始化Student2的数据成员score。
派生类构造函数的特殊形式
在使用派生类构造函数时,有以下特殊的形式。
当不需要对派生类新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为空,即构造函数是空函数,如:
Student1(int n, strin nam,int n1, strin nam1):
Student(n,nam),monitor(n1,nam1) { }如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数。
- 如果在基类和子对象类型的声明中都没有定义带参数的构造函数,而且也不需对派生类自己的数据成员初始化,则可以不必显式地定义派生类构造函数。
- 如果在基类或子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数,并在派生类构造函数中写出基类或子对象类型的构造函数及其参数表。
- 如果在基类中既定义无参的构造函数,又定义了有参的构造函数(构造函数重载),则在定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数。
派生类的析构函数
在派生时,派生类是不能继承基类的析构函数的,也需要通过派生类的析构函数去调用基类的析构函数。
在派生类中可以根据需要定义自己的析构函数,用来对派生类中所增加的成员进行清理工作。基类的清理工作仍然由基类的析构函数负责。
在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理。
调用的顺序与构造函数正好相反:先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进行清理。
类的多重继承
如果已声明了类A、类B和类C,可以声明多重继承的派生类D:
class D: public A, private B, protected C
{
类D新增加的成员
}
D是多重继承的派生类,它以公用继承方式继承A类,以私有继承方式继承B类,以保护继承方式继承C类。D按不同的继承方式的规则继承A、B、C的属性,确定各基类的成员在派生类中的访问权限。
多重继承派生类的构造函数形式与单继承时的构造函数形式基本相同,只是在初始表中包含多个基类构造函数。如
派生类构造函数名(总参数表列): 基类1构造函数(参数表列), 基类2构造函数(参数表列), 基类3构造函数(参数表列)
{
派生类中新增数成员据成员初始化语句
}
各基类的排列顺序任意。
派生类构造函数的执行顺序同样为:先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序。
多重继承的二义性问题
如果类A和类B中都有成员函数display和数据成员a,类C是类A和类B的直接派生类。分别讨论下列3种情况。
1) 两个基类有同名成员
class A
{
public:
int a;
void display();
};
class B
{
public:
int a;
void display ();
};
class C: public A, public B
{
public:
int b;
void show();
};
如果在main函数中定义C类对象cl,并调用数据成员a和成员函数display :
C cl;
cl.a=3;
cl.display();
由于基类A和基类B都有数据成员a和成员函数display,编译系统无法判别要访问的是哪一个基类的成员,因此程序编译出错。那么,应该怎样解决这个问题呢?可以用基类名来限定:
cl.A::a=3; //引用cl对象中的基类A的数据成员a
cl.A::display(); //调用cl对象中的基类A的成员函数display
如果是在派生类C中通过派生类成员函数show访问基类A的display和a,可以不 必写对象名而直接写
A::a = 3; //指当前对象
A::display();
2) 两个基类和派生类三者都有同名成员
将上面的C类声明改为:
class C: public A, public B
{
int a;
void display();
};
如果在main函数中定义C类对象cl,并调用数据成员a和成员函数display:
C cl;
cl.a = 3;
cl.display();
规则是:基类的同名成员在派生类中被屏蔽,成为“不可见”的,或者说,派生类新增加的同名成员覆盖了基类中的同名成员。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员。
要在派生类外访问基类A中的成员,应指明作用域A,写成以下形式:
cl.A::a=3; //表示是派生类对象cl中的基类A中的数据成员a
cl.A::display(); //表示是派生类对象cl中的基类A中的成员函数display
3) 类A和类B是从同一个基类派生的
class N
{
public:
int a;
void display(){ cout<<"A::a="<<a<<endl; }
};
class A: public N
{
public:
int al;
};
class B: public N
{
public:
int a2;
};
class C: public A, public B
{
public:
int a3;
void show(){ cout<<"a3="<<a3<<endl; }
}
int main()
{
C cl; //定义C类对象cl
// 其他代码
}
应当通过类N的直接派生类名来指出要访问的是类N的哪一个派生类中的基类成员。如
cl.A::a=3; cl.A::display(); //要访问的是类N的派生类A中的基类成员
虚基类
C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。假设类D是类B和类C公用派生类,而类B和类C又是类A的派生类。 设类A有数据成员data和成员函数fun;派生类B和C分别从类A继承了data和fun,此外类B还增加了自己的数据成员data_b,类C增加了数据成员data_c。如果不用虚基类,就会在类D中保留了类A成员data的两份拷贝,分别表示为int B::data和int C::data。同样有两个同名的成员函数,表示为void B::fun()和void C::fun()。类B中增加的成员data_b和类C中增加的成员dat_c不同名,不必用类名限定。此外,类D还增加了自己的数据成员data_d和成员函数fun_d。
现在,将类A声明为虚基类,方法如下:
class A //声明基类A
{
// 代码
};
class B: virtual public A //声明类B是类A的公用派生类,A是B的虚基类
{
// 代码
};
class C: virtual public A //声明类C是类A的公用派生类,A是C的虚基类
{
// 代码
};
注意: 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
声明虚基类的一般形式为:
class 派生类名: virtual 继承方式 基类名
即在声明派生类时,将关键字 virtual 加到相应的继承方式前面,经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次。
需要注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。例如
class A //定义基类A
{
A(int i){ } //基类构造函数,有一个参数};
class B :virtual public A //A作为B的虚基类
{
B(int n):A(n){ } //B类构造函数,在初始化表中对虚基类初始化
};
class C :virtual public A //A作为C的虚基类
{
C(int n):A(n){ } //C类构造函数,在初始化表中对虚基类初始化
};
class D :public B,public C //类D的构造函数,在初始化表中对所有基类初始化
{
D(int n):A(n),B(n),C(n){ }
};
由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
基类与派生类的转换
基类的公用或保护成员的访问权限在派生类中全部都按原样保留下来了,在派生类外可以调用基类的公用成员函数访问基类的私有成员。因此,公用派生类具有基类的全部功能,所有基类能够实现的功能, 公用派生类都能实现。因此,只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。
基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面。
1) 派生类对象可以向基类对象赋值
可以用子类(即公用派生类)对象对其基类对象赋值。如
A a1; //定义基类A对象a1
B b1; //定义类A的公用派生类B的对象b1
a1=b1; //用派生类B对象b1对基类对象a1赋值
在赋值时舍弃派生类自己的成员。
请注意,赋值后不能企图通过对象a1去访问派生类对象b1的成员,因为b1的成员与a1的成员是不同的。
应当注意,子类型关系是单向的、不可逆的。B是A的子类型,不能说A是B的子类型。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值,理由是显然的,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。
2) 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化(即java中所说父类引用指向子类对象)
如已定义了基类A对象a1,可以定义a1的引用变量:
A a1; //定义基类A对象a1
B b1; //定义公用派生类B对象b1
A& r=b1; //定义基类A对象的引用变量r,并用a1对其初始化
注意,此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。
3) 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。
4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。
继承与组合详解
在一个类中以另一个类的对象作为数据成员的,称为类的组合(composition)。
例如,声明Professor(教授)类是Teacher(教师)类的派生类,另有一个类BirthDate(生日),包含year,month,day等数据成员。可以将教授生日的信息加入到Professor类的声明中。
class Teacher //教师类
{
public:
// Some Code
private:
int num;
string name;
char sex;
};
class BirthDate //生日类
{
public:
// Some Code
private:
int year;
int month;
int day;
};
class Professor:public Teacher //教授类
{
public:
// Some Code
private:
BirthDate birthday; //BirthDate类的对象作为数据成员
};
继承在软件开发中的重要意义
- 有许多基类是被程序的其他部分或其他程序使用的,这些程序要求保留原有的
基类不受破坏。使用继承是建立新的数据类型,它继承了基类的所有特征,但不改变基类本身。基类的名称、构成和访问属性丝毫没有改变,不会影响其他程序的使用。 - 用户往往得不到基类的源代码。如果想修改已有的类,必须掌握类的声明和类的实现(成员函数的定义)的源代码。但是,如果使用类库,用户是无法知道成员函数的代码的,因此也就无法对基类进行修改。
在类库中,一个基类可能已被指定与用户所需的多种组件建立了某种关系,因此
在类库中的基类是不容许修改的(即使用户知道了源代码,也决不允许修改)。实际上,许多基类并不是从已有的其他程序中选取来的,而是专门作为基类设计的。有些基类可能并没有什么独立的功能,只是一个框架,或者说是抽象类。人们根据需要设计了一批能适用于不同用途的通用类,目的是建立通用的数据结构,以便用户在此基础上添加各种功能,从而建立各种功能的派生类。
- 在面向对象程序设计中,需要设计类的层次结构,从最初的抽象类出发,每一层派生类的建立都逐步地向着目标的具体实现前进,换句话说,是不断地从抽象到具体的过
程。每一层的派生和继承都需要站在整个系统的角度统一规划,精心组织。