目录
继承与派生是面向对象程序设计的重要特征之一。有单继承、多继承。派生类(子类)的基类(父类)只有一个为单继承,多个则是多继承。
1.派生类的定义、成员
(1)定义:
class 派生类名 :继承方式 基类名
(2)成员:
1.吸收除了构造函数、析构函数外的其他基类成员
2.新增成员:可以增加新的数据成员、成员函数;
3.对基类进行改造:一是通过定义继承方式改变基类成员的访问属性;二是派生类中定义与基类同名的成员(成员函数要求参数也相同)来覆盖基类的成员。
2.继承方式
(1)public继承:
基类的public、protected成员在派生类中访问属性不变;
基类的private成员在派生类中不可见;
(2)private继承:
基类的public、protected成员在派生类中访问属性变为private成员;
基类的private成员和不可见成员在派生类中不可访问;
(3)protected继承:
基类的public、protected成员在派生类中访问属性变为protected成员;
基类的private成员和不可见成员在派生类中不可访问;
3.派生类的构造函数、析构函数(单继承、多层继承)
3.1派生类的构造函数
“自力更生”:派生类的构造函数需要:
要初始化派生类自己新增的数据成员,还要为基类的构造函数传递参数;
(1)只有一个基类,也没有基类或其他类的对象
构造函数格式:
派生类构造函数名(总参数列表):基类构造函数名(参数列表)
{
派生类中新增数据成员初始化语句;
}
注意:C++ 语言规定:建立一个派生类对象,派生类构造函数会先调用基类构造函数,再执行派生类构造函数本身(派生类构造函数的函数体).其中基类构造函数名的参数列表只需要写变量名,不再需要写类型,因为派生类构造函数名的总参数列表已经说明过类型。
(2)有内嵌对象的派生类的构造函数
如果派生类的成员有定义了其他类的对象,此时建立一个派生类对象,派生类构造函数会先调用基类构造函数,然后是内嵌对象的构造函数,最后再执行派生类构造函数本身(派生类构造函数的函数体)。
格式:
派生类构造函数名(总参数列表):基类构造函数名(参数列表),内嵌对象名(内嵌对象参数)
{
派生类中新增数据成员初始化语句;
}
例子:
class A
{public:
int a;
A(int a1)
{a=a1;
cout<<"A has been"<<endl;}};
class B
{public:
int b;
B(int b1)
{b=b1;
cout<<"B has been"<<endl;}};
class C:public B
{public:
int c;
A obja;
C(int a1,int b1,int c1):B(b1),obj_a(a1)//基类、内嵌对象的类构造函数参数不再需要说明类型。
{
c=c1;
cout<<"C has been"<<endl;
}};
int main()
{
C objc(10,20,30);
cout<<endl<<objc.obja.a<<" "<<objc.b<<'\0'<<objc.c;
return 0;}
结果:
(3)多层派生的派生类的构造函数
基类:A;B继承A;C再继承B;此时,写派生类C的构造函数的总参数表列,只需要列出B类和B类构造函数的参数,而不用列出A类(虚基类的情况除外)。
例子:
class A
{public:int a;
A (int a1){a=a1; } };
class B : public A
{public:int b;
B (int b1,int a1):A( a1 ) { b=b1 } };
class C : public B
{public:int c;
B (int c1,int b1):B(b1 ) { c=c1 } };
(4)派生类构造函数几种特殊情况:
1.派生类不需要对自己新增成员初始化时,它的函数体可以为空;
2.如果基类没有定义构造函数(或者默认构造函数),在派生类的构造函数初始化列表中不用列出基类名及其参数;
3. 若在基类和内嵌对象类型的声明中都没有定义带参数的构造函数,而且派生类也不需要对新增成员初始化,那么派生类的构造函数没必要显式的声明(定义);
4. 若基类或内嵌对象类型的声明中定义了带参数的构造函数,那么派生类的构造函数必须显式的声明(定义);
5.若基类既定义了无参数、也定义了有参数的构造函数,那么派生类的构造函数既可以包含基类构造函数,也可以不包含。
3.2派生类的析构函数
与构造函数类似,析构函数不能被继承,派生类需要“自力更生”,派生类的析构函数特征:
(1)派生类的析构函数与一般(无继承关系)类析构函数相同;
(2)不需要显式调用基类析构函数,系统会自动隐式调用;
(3)析构函数调用次序与构造函数相反。即先执行派生类析构函数、然后执行派生类内嵌对象的类的析构函数,最后执行基类的析构函数。
4.多继承
(1)含义:
多继承与多层继承不同;上文提及的多层继承是指继承的层次超过2层;多继承则是指派生类的直接基类>=2个。
多继承定义格式:
class 派生类名:继承方式1 基类1名,继承方式2 基类2名·····…
{}
4.1多继承的派生类的构造函数
派生类构造函数名(总参数列表):基类1名(参数列表),基类2名(参数列表),内嵌对象名(内嵌对象参数)
{
派生类中新增数据成员初始化语句;
}
(1)注意:
1.当多继承的派生类的某个基类的构造函数无参数时,该派生类的构造函数初始化列表可不必列出某个基类及其参数;
2.多继承的派生类构造函数执行顺序为:定义类的“class 派生类名:继承方式1 基类1名,继承方式2 基类2名···”该语句中的基类(基类按照继承顺序,即基类1à基类2…),然后是派生类中内嵌对象的类的构造函数、最后是派生类本身的构造函数。
(2)例子:
#include <QCoreApplication>
#include <iostream>
using namespace std;
class BaseA1 //基类BaseA1
{
public:
BaseA1(int i)
{
cout<<"Constructing BaseA1"<<endl;
x1=i;
}
void print()
{
cout<<"x1="<<x1<<endl;
}
protected:
int x1;
};
class BaseA2 //基类BaseA1
{
public:
BaseA2(int j)
{
cout<<"Constructing BaseA2"<<endl;
x2=j;
}
void print()
{
cout<<"x2="<<x2<<endl;
}
protected:
int x2;
};
class BaseA3 //派生类内嵌对象的类
{
public:
BaseA3()
{
cout<<"Constructing BaseA3"<<endl;
}
void print()
{
cout<<"Costructing BaseA3 No Value!"<<endl;
}
};
class MderivedB : public BaseA1, public BaseA2 //基类构造函数执行顺序按照基承顺序,即先执行BaseA1、再BaseA2.
{
public:
MDerivedB(int a,int b,int d):BaseA1(a),BaseA2(b),obja3()//顺序按照class声明时写(否则虽然不报错,但是会警告)
{x4=d;}
void print()
{
BaseA1::print();
BaseA2::print();
obja3.print();
cout<<"x4="<<x4;
}
private:
int x4;
BaseA3 obja3; //派生类内嵌对象obja3
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
MDerivedB obj(1,2,4);
obj.print(); //函数print同名覆盖了基类中的print
return a.exec();
}
结果:
4.2多继承引起的二义性问题
在多继承时最应该注意二义性问题,派生类中对基类的成员访问应该是明确(唯一)的。但是多继承有时会出现对基类访问不明确的问题(二义性)
解决方法有:
(1)同名覆盖原则。如上例子的obj.print(),这使得派生类对象调用print函数只能是派生类的print;
(2)通过类名和作用域运算符(::)。如有基类A1、A2,派生类B:public A1,public A2;基类A1、A2中有同名函数show()都被B继承了,那B obj_b;对象obj_b调用该函数时,即obj_b.show()就会导致该派生类对象不知道使用哪一个基类的show()函数(即使基类的函数他们参数不同);解决途径:obj_b.show()改为obj_b.A1::show()则调用基类A1的show函数,obj_b.A2::show()则调用A2的show函数。
(3)虚基类
5.虚基类
(1)含义:
在1—X—1型(“橄榄型”)的多继承关系中,采用“类名和作用域运算符(::)”的方法能解决二义性问题。但是会带来新的问题:
1.数据的冗余问题:
类A派生出B1、B2;类C直接继承了B1和B2;在A中有一个public数据成员int data,这在B1、B2被继承,类C又继承了B1、B2;这就导致了类C中为int data保留了两个副本。
2.多个副本会导致维护数据一致性困难,这就增加了程序出错的概率
使用虚基类技术,派生类可以再内存中只保留基类的一个副本,从而消除数据冗余。
(2)虚基类的声明:
格式:class 派生类:visual 继承方式 基类
可见增加限定符visual后,这样基类就被定义为派生类的虚基类。
(3)例子:
class A
{
public:
int data;
};
class B1: virtual public A{};
class B2:virtual public A{};
class C:public B1,public B2{};
int main()
{
C obj;//定义C类对象obj
obj.data=1000;//若不使用虚基类可见报错:error: request for member 'data' is ambiguous
}
通过以上例子可见,若不说明B1/B2是A的virtual类,那么就需要使用obj.B1::data,但是这种方式在B2中还有一个data的备份,这就会引起前述中的数据冗余、多副本维护数据一致性困难的问题。
5.1虚基类及其派生类的构造函数
(1)含义、特点:
虚基类的初始化与一般多继承的初始化语法类似,但要注意构造函数有以下特点:
1.若派生类有一个直接或间接的虚基类,那么派生类构造函数初始化列表要负责列出虚基类构造函数及其参数;若虚基类构造函数无参数,可以不列出列出虚基类构造函数及其参数。
2.构造函数执行顺序,按照虚基类优先、class声明定义的继承顺序优先两个原则
3.在包含虚基类的多层继承中,通常把建立对象那个类称为“最远派生类”;C++规定,只有最远派生类的构造函数才会调用虚基类的构造函数,该派生类的基类的构造函数所列出对虚基类的构造函数调用在执行时会被忽略,这样可以保证虚基类的对象只初始化一次。
(2)例子:
class A
{
public:
int data;
A(int i)
{
data=i;
cout<<"virtual Bese class A "<<endl;
}
};
class B1: virtual public A
{public:
B1(int a1):A(a1){
cout<<"B1 class"<<endl;}
};
class B2:virtual public A
{public:
B2(int a1):A(a1){
cout<<"B2 class"<<endl;}
};
class C:public B1,public B2
{public:
C(int a1,int b1,int b2):A(a1),B1(b1),B2(b2)
{cout<<"C class"<<endl;}
};
int main()
{
C obj(1,2,3);//定义C类对象obj
cout<<"class C:"<<obj.data<<endl;
cout<<"class A:"<<obj.A::data<<endl;
cout<<"class B1:"<<obj.B1::data<<endl;
cout<<"class B2:"<<obj.B2::data<<endl;
return 0;}
结果:
(3)例子分析:
1.虚基类具有传递性。和没有虚基类的多层派生区别于,最远派生类构造函数初始化列表不仅要初始化直接基类,也要负责间接虚基类的初始化
2.构造函数执行顺序:A、B1、B2、C.优先虚基类、再是按照继承基类的顺序,其中基类顺序按照class C:public B1,public B2的继承顺序。(C类构造函数初始化列表顺序不影响C构造函数执行顺序,但建议按照实际构造函数执行顺序书写,可读性、逻辑性更好)
3.C定义的obj对象,所以C是最远派生类。只有C的构造函数对虚基类构造函数调用有作用,即使B1、B2构造函数参数列表里面包含了对虚基类A的初始化(因为是形式上的);
4.虚基类的对象只初始化一次,即使C(int a1,int b1,int b2)有三个参数,如3所述,传递给B1、B2的参数再执行A的构造被忽略了,一次只有int a1参数传递给A(int i)构造。正因如此C的构造函数总参数列表不再包含B1、B2构造函数参数也可,可修改为:C(int a1):A(a1),B1(a1),B2(a1),定义对象时:修改为C obj(1).运行结果完全相同。
6.基类与派生类的赋值兼容
基类与派生类的赋值兼容规则指的是在需要基类对象的任何地方都可以用public派生类的对象来替代。有三种情况:以基类A,派生类B说明:
(1)派生类对象可以赋值给基类对象;
定义对象A a1;B b1;则有a1=b1;
可把派生类B的对象b1赋值给基类A的对象a1;即用B类对象b1从基类A继承的成员逐个赋值给基类A对象a1的成员。
(2)派生类对象可以初始化基类的引用;
定义对象B b2; 则有A &aa2=b2;
引用只是给变量起个别名(如int x=10,int &rx=x,那么使用变量名rx和x都能操作该变量,注意该变量只是有两个变量名,该变量只占一块存储空间),因此此处可理解为派生类先赋值给另一个基类对象(当然是隐式的),再引用:A a2;B b2——>a2=b2;B &bb2=b2;——> B b2;A &aa2=b2。
(3)派生类对象的地址可以赋值给基类的指针。
定义了A *ptr;B b3;则可以ptr=&b3;(取地址和引用都是用&符号,但它们含义不同,注意不要混淆)
例子:
class A{};
class B1:public A{};
int main()
{
A a1;
cout<<&a1<<endl;
B1 b1;
cout<<&b1<<endl;
a1=b1;
cout<<&a1<<endl;
A &aa1=a1;
cout<<&aa1<<endl;
A *ptr=&b1;
cout<<ptr<<endl;}
//看输出的变量地址,体会赋值兼容规则。