派生类的构造函数和析构函数
为什么会有派生类的构造函数? 我们怎么使用?
构造函数的主要作用是初始化。我们前面说过 ,基类的构造函数是不能继承的,在声明派生类时,派生类并没有把基类的构造函数给继承过来,所以基类的初始化工作也要由派生类的构造函数承担。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员,还要考虑基类成员的初始化。也就是说,希望在执行派生类构造函数时,使派生类的数据成员和基类的数据成员同时都被初始化。
怎么解决这个问题呢?
在执行派生类的构造函数时,调用基类的构造函数。
简单的说就是,你在派生类中的构造函数也要调用基类的构造函数。
一。简单类的派生函数
class Student
{
public:
Student(int n, string nam, char s) : num(n), name(nam), s(sex){}
~Student(){}
protected:
int num;
string name;
char sex;
};
class Student1: public Student
{
public :
Student1(int n, string nam, char s, int ag, string add) : Stdudent(n, nam, s)
{
age = ag;
addr = addr;
}
~Student1(){}
protected :
int age;
string addr;
}
一般形式
派生类构造函数名(总参数表):基类构造函数名(参数表)
{派生类中新增数据成员的初始化}
在基类构造函数名后面的参数表 传的是实参而不是形参。Stuendt(n, nam, s)中 n nam, s是实参。
类外定义
Student1(int n, string nam, char s, int ag, string add); //声明派生类构造函数
//在派生类外定义构造函数
Student1::Student1(int n, string nam, char s, int ag, string add) : Student(n, nam, s)
{
age = ag;
addr = add;
}
在调用基类构造函数时的实参是从派生类构造函数的总参数表中得到的,也可以不从派生类构造函数的总参数表中传递过来,而直接使用常量或全局变量。
Student1(string nam, char s, int ag, string add) : Student(1001, nam, s)
一个是常量,另外两个从派生类中继承。
初始化参数表
Student1::Student1(int n, string nam, char s, int ag, string add) : Student(n, nam, s), age(ag), addr(add){}
派生类中构造函数的顺序(无子对象的情况下)
先基类再派生类
二。有子对像的派生类
class Student
{
public:
Student(int n, string nam) : num(n), name(nam){}
~Student(){}
protected:
int num;
string name;
};
class Student1: public Student
{
public :
Student1(int n, string nam, int n1, int nam1, int ag, string add);
~Student1(){}
protected :
Student monitor; //子对象
int age;
string addr;
}
Student1::Student1(int n, string nam, int n1, int nam1, int ag, string add) : Student(n, nam), monitor(n1, nam1), age(ag), addr(add){}
Student stud(170, "Mr.Li", 171, "Mr.Dun", 20, "Henu")
派生类构造函数的任务应该包括3个部分:
(1)对基类数据成员初始化
(2)对子对象数据成员的初始化
(3)对派生类数据成员初始化
一般形式
派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
{派生类中新增数据成员的初始化}
基类构造函数和子对象的次序是任意的。
Student1(int n, string nam, char s, int n1, int nam1, int s1, int ag, string add) : monitor(n1, nam1), Student(n, nam)
三。多层派生时的构造函数
多重继承的形式入下
Student(int n, string nam) : num(n), name(nam){}
Student1(int n, string nam, int ag) : Student(n, nam), age(ag){}
Studnet2(int n, string nam, int ag,int s):Student1(n, nam, ag), score(s){}
不需要列出每一层派生类的构造函数,只需要写出上一层的派生类(即它的直接基类)的构造函数。
调用顺序: 先基类——》派生类 ——》再派生
class Student
{
public:
Student(int n, string nam) : num(n), name(nam){}
void display()
{
cout << "num : " << endl;
cout << "name : " << endl;
}
~Student(){}
protected:
int num;
string name;
};
class Student1: public Student
{
public :
Student1(int n, string nam, int ag) : Student(n, nam), age(ag){}
void show()
{
display();
cout << "age : " << age << endl;
}
~Student1(){}
protected :
int age;
}
class Student2 : public Student1
{
public :
Studnet2(int n, string nam, int ag,int s):Student1(n, nam, ag), score(s){}
void show1()
{
show();
cout << "score : " << endl;
}
private :
int score;
}
四。派生类构造函数的特殊形式
(1)当不需要对派生类新增成员进行初始化时,派生类构造函数的函数体内可以空。
Student1(int n, string nam) : Student(n, nam){}
(2)如果基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时,可以不写基类构造函数。因为此时派生类构造函数没有向基类构造函数传递参数的任务。在调用派生类构造函数时,系统会自动首先调用基类默认构造函数。
如果基类和子对象类型声明中都没有定义带参数的构造函数,而且你也不需要对派生类中的构造函数进行初始化,那就不需要定义派生类的构造函数。系统会自动调用默认构造函数。
如果在基类中即定义了无参的,有定义的了有参数的构造函数,则在定义派生类构造函数时,既可以包含基类构造函数,也可以不包含。在调用派生类构造函数时,根据构造函数的内容决定调用哪个构造函数。
五。派生类的析构函数
析构顺序: 本身 ——》子对象——》基类
六。多重继承
一个派生类可以继承多个基类
声明方式:
class D:public A,private B, protected C
{类新加的成员}
多重继生派生类的构造函数
派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造参数表(参数表),基类3构造函数(参数表)
{派生类中新增成员初始化}
调用顺序由 派生顺序决定。
#include<iostream>
#include <string.h>
using namespace std;
class Teacher
{
public :
Teacher(string nam, int a, string t): name(nam), age(a), title(t){}
void display()
{
cout << "name : " << name << endl;
cout << "age : " << age << endl;
cout << "title : " << title << endl;
}
protected:
string name;
int age;
string title;
};
class Student
{
public :
Student(string nam, char s, float sco) : name1(nam), sex(s), score(sco){}
void display()
{
cout << "name : " << name1 << endl;
cout << "sex : " << sex << endl;
cout << "score : " << score << endl;
}
protected :
string name1;
char sex;
float score;
};
class Graduate: public Teacher, public Student
{
public :
Graduate(string nam, int a, char s, string t, float sco, float w) : Teacher(nam, a, t), Student(nam, s, sco), wage(w){}
void show()
{
cout << "name : " << name << endl;
cout << "age : " << age << endl;
cout << "title : " << title << endl;
cout << "name : " << name1 << endl;
cout << "sex : " << sex << endl;
cout << "score : " << score << endl;
}
private :
float wage;
};
当我们将Student 中的string name1 改成 name 和 Teacher 中的 name 名字相同时,这时 Graduate 中引用 name就会出现二义性,怎么解决呢?
在两个基类中可以都使用同一个数据名name ,而在show函数内中引用数据成员时应指明其作用域
cout << "name : " << Teacher::name << endl;
多重继承还有一个巨大缺点:我们会重复继承一些数据,例如上面的名字,会造成数据的冗余。
多重继承引起的二义性
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();
};
C c1; //定义一个c1对象
c1.a; //引用数据成员a。错误 二义性
c1.display(); //引用成员函数,错误,二义性
C c1;
c1.A::a; //正确
c1.B::display(); //正确
在类C的成员函数show()中 调用 a 和 display()函数前要加作用域
A::a;
B::display();
这时 我们如果在类C中 改为
class C : public A, public B
{
public :
int a;
void display();
};
这时在C内调用或在
C c1;
c1.a; //正确 ,覆盖基类中的a 就近原则
c1.display();
规则
基类的同名成员在派生类中被屏蔽,成为“不可见的”,或者的说,派生类新增加的同名成员覆盖了基类中的同名成员。
不同的成员函数,只有在函数名和参数个数相同,类型匹配情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数的重载。
七。虚基类
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终派生的基类中会保留该间接基类数据成员的多份同名成员,在引用这些同名成员时,必须在派生类对象后增加直接基类名,以避免二义性,使其唯一的标识一个成员,例如c1.A::display();
在一个类中保留间接共同基类的多份同名成员,虽然有时是有必要的,可以在不同的数据成员中分别存放不同的数据,也可以通过构造函数分别对它们进行初始化。但是多数清况下 ,这种情况人们并不需要。
这种菱形继承的缺点:保留了多份数据成员,占用较多的储存空间,还增加了访问这些成员时的困难,容易出错。事实上不需要多份拷贝。
C++提供虚基类,使得继承间接共同基类时只保存只保留一份成员。
class A
{~~~}
class B : virtual public A
{}
class C : virtual public A
{}
class 派生类名:virtual 继承方式 基类名
当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是基类成员只保留一次。
为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。
虚基类的初始化
class A
{
A(int n){}
};
class B : virtual public A
{
B(int n) : A(n){}
};
class C : virtual public A
{
C(int n) : A(n){}
};
class D : public B, public C
{
D(int n) : A(n), B(n), C(n){}
};
在最好的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
上面虽然对基类进行了三次构造。但是真正在编译时只执行最后派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(B和C)对虚基类构造函数的调用,这样就保证了虚基类的数据成员不会被多次初始化。
这种多重继承并不是太好,因为会引起二义性,而且数据冗余。在JAVA 等语言就不支持多重继承。
基类与派生类的转换
基类和派生类之间存在赋值兼容关系。
(1)派生类的对象可以基类对象赋值;
A a1; //定义基类A 的对a1
B b1; //定义A的派生类B 的对象b1
a1 = b1; //用派生类对象 b1 给基类 a1赋值
派生时舍弃派生类自己的成员,用形象的话来说就是 “ 大才小用 ”。
(2)只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值。
(3)同一基类的不同派生类对象之间也不能赋值
(4)派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。
A a1; //定义基类A 的对a1
B b1; //定义A的派生类B 的对象b1
A &r = a1; //定义A对象的引用r ,并用a1进行初始化
A &r = b1; //定义A对象的引用r ,并用b1进行初始化
r = b1; //用派生类 B 对象b1 对a1 的引用r赋值
(5)如果函数的参数是基类对象或基类对象的引用,相应的参数可以用子类对象
void fun(A &r){ //形参是类A的对象引用
cout << r.num << endl; //输出该引用中的数据成员
}
fun (b1); //输出类B的对象b1 的基类数据成员Num 的值
//在fun函数中只能输出派生类中基类成员的值
(6)派生类对象的地址可以赋值给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以用来指向派生类对象。简单的说,指向基类的指针可以指向派生类
通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。
继承与组合
在一个类中以另一个类的对象作为数据成员,称为类的组合。
#include<iostream>
using namespace std;
class Teacher{
public :
private :
int num ;
string name;
char sex;
};
class Birthday{
public :
private:
int year;
int month;
int day;
};
class Professor : public Teacher{
public :
private :
Birthday birthday; //Birthday类的对象作为数据成员
};
类的组合和继承我认为很重要 也很常用,能够有效减少多重继承的数据冗余,并且提高代码效率
通过继承建立了派生类与基类的关系,它是一种 “ 是 ” 的关系,如 ” 白猫是猫 “ 。
派生类是基类的具体实现化,是基类的具体化实现。
而在上述 Brithday 是成员类,Professor 是组合类(在一个类中包含另一个类的对象成员)。他们之间不是 “ 是 ” 的关系 而是 ” 有 “ 的关系。 不能说教授是一个生日,可以说教授有一个生日。