多继承
C++中有两种继承:单一继承和多重继承。
对于单继承,派生类只能有一个基类;
对于多继承,派生类可以有多个基类。
定义
一个类从多个基类派生的一般形式是:
class 类名1:访问控制 类名2, 访问控制 类名3 ,
..., 访问控制 类名n
{...// 定义派生类自己的成员 };
类名1继承了类名2到类名n的所有数据成员和成员函数,访问控制用于限制其后的类中的成员在类名1中的访问权限,其规则和单继承情况一样。
多继承可以视为是单继承的扩展。
构造函数
派生类名::派生类名(基类形参,本类形参):基类名1(参数), 基类名2(参数), ...基类名n(参数)
{
本类成员初始化赋值语句;
};
有内嵌对象时的构造函数
派生类名::派生类名(基类1形参,基类2形参,...基类n形参,本类形参):基类名1(参数), 基类名2(参数), ...基类名n(参数),对象数据成员的初始化
{
本类成员初始化赋值语句;
};
构造函数的调用次序
1. 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
2. 调用成员对象的构造函数,调用顺序按照它们在类中声明的顺序。
3. 派生类的构造函数体中的内容。
多继承:例10.1
#include <iostream>
using namespace std;
class A
{private:
int a;
public:
void setA( int x ){a=x;};
void showA( ){cout <<"a="<< a << endl; }
};
class B
{private:
int b;
public:
void setB( int x ) { b = x; }
void showB ( ) { cout << "b="<< b << endl; }
};
class C : public A, private B
//2个基类,多继承
{ private:
int c;
public:
void setC(int x, int y ){c=x; setB(y);}
void showC( )
{showB( );
cout << "c="<<c << endl;}
};
int main( )
{ C obj;
obj.setA(53);//a=53
obj.setC(55,58);// b=58 c=55
obj.showA( ); //输出a=53
obj.showC(); //输出b=58 c=55
return 0;
}
类C从类A公有派生,因此,类A公有成员(保护成员)在类C中仍是公有(保护)。类C从类B私有派生,类B的所有成员在类C中是私有的。
这些成员在派生类中的可访问性和单一继承中讨论的一样。
类B被私有继承,因此,类C还需要负责维护类B数据成员值和显示,所以在showC和setC中分别调用类B成员函数showB和setB。使用obj.setB(5)和obj.showB( )都错误。
多继承中同名覆盖问题
-
同名覆盖
在多继承中,一个派生类的多个基类具有同名成员,如果派生类也声明了同名成员,派生类的成员覆盖多个基类中的同名成员。 -
域作用符
派生类对象d要想访问基类中的同名成员,就必须使用域作用符“::”。使用规则如下:
基类名::成员名; //数据成员
基类名::函数名(参数);//函数成员
作用就是指明要访问的是哪一个类中的同名成员。 -
当派生类与基类中有相同成员时:
若未强行指名,则通过派生类对象使用的是派生类中的同名成员。
如要通过派生类对象访问基类中被覆盖的同名成员,应使用基类名限定。
二义性问题
- 对基类成员的访问必须是无二义性的。
- 如果使用一个表达式的含义能解释为可以访问多个基类中的成员,则这种对基类成员的访问就是不确定的,称这种访问具有二义性。
注意:适合基类与派生类存在同名的成员。
解决方法:
- 作用域分辨符
- 同名覆盖原则
- 虚函数
例10.2多继承中的同名问题示例
#include <iostream>
using namespace std;
class B1
{public:
int num;
void fun() {cout<<"member of B1"<<endl;}
};
class B2
{public:
int num;
void fun() {cout<<"member of B2"<<endl;}
};
class D: public B1, public B2
{public:
int num;
void fun() {cout<<"member of D"<<endl;}
};
int main()
{ D d;
d.num = 1; //访问D类成员
d.fun(); //访问D类成员
d.B1::num = 2; //访问B1类成员
d.B1::fun(); //访问B1类成员
d.B2::num = 3; //访问B2类成员
d.B2::fun(); //访问B2类成员
//作用域分辨符“::”访问被隐藏的基类
//成员问题
return 0;
}
运行结果:
member of D:1
member of B1:2
member of B2:3
例10.3 访问具有二义性举例
#include <iostream>
using namespace std;
class A {
public:
void func( ){cout<<"a.func"<<endl;}
};
class B {
public:
void func( ){cout<<"b.func"<<endl;}
void gunc( ){cout<<"b.gunc"<<endl;}
};
class C : public A, public B {
public:
void gunc( ){cout<<"c.gunc"<<endl;}
void hunc( )
{func(); // 具有二义性}
//C类的成员函数hunc访问func时,无法确定是访问基类A还是基类B,出现二义性。使用A :: func( )或B :: func( )可以解决这种二义性。
};
//正确的派生类C的实现方法:
class C : public A, public B {
public:
void gunc( ){cout<<"c.gunc”<<endl;}
void hun1( ){A::func();} // 使用基类A的func
void hun2( ){B::func();} // 使用基类B的func
};
int main( )
{
C obj;
obj.func();//具有二义性
//用C类的对象obj访问函数func具有二义性,不能确定是A的func还是B的func
obj.gunc( ); // C的gunc,同名覆盖
return 0;
}
//C类对象obj访问函数func,使用成员名限定可消除二义性:
obj.A :: func( ); // A的func
obj.B :: func( ); // B的func
使用作用域分辨符“::”解决二义性问题。
作用域分辨不仅可用于类中,
而且可以用在函数调用时。
例10.4:多继承示例2
#include <iostream>
using namespace std;
class B0 //声明基类B0
{public:
B0(int n=0){ nV=n;}//构造函数
int nV;
void fun(){cout<<"Member of B0:"<<nV<<endl;}
};
class B1: public B0 //B0为虚基类,派生B1类
{public:
B1(int a=0) : B0(a) {}//构造函数
int nV1;
};
class B2: public B0 //B0为虚基类派生B2类
{ public:
B2(int a=0) : B0(a) {}//构造函数
int nV2;
};
class D1: public B1, public B2 //派生类D1声明
{ public:
D1(int a=0) : B1(a+1), B2(a+2){}//构造函数
int nVd;
void fund(){cout<<"Member of D1:"<<nVd<<endl;}
};
int main()
{ D1 d1;
d1.fun();//error
d1.B1::fun();
d1.B2::fun();
return 0;
}
注释掉d1.fun();语句后,程序运行结果为:Member of B0:1
Member of B0:2
例10.4派生类D1的对象的存储结构示意图:
类C的对象在内存中同时拥有B0两份同名拷贝,二义性问题。
解决方法:使用虚基类,保证B0只有1份拷贝。
虚基类
- 声明:以virtual修饰说明基类
例:class B1:virtual public B
- 作用:要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题。为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝。
- 注意:在第一级继承时就要将共同基类设计为虚基类。
虚基类的引入:用于有共同基类的场合
当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生二义性。
- 应用:例10.5 多继承共同基类带来的二义性。
#include <iostream>
using namespace std;
class B0 //声明基类B0
{public:
B0(int n=0){ nV=n;}//构造函数
int nV;
void fun(){cout<<"Member of B0:"<<nV<<endl;}
};
class B1: virtual public B0 //B0为虚基类,派生B1类
{public:
B1(int a) : B0(a) {}//构造函数
int nV1;
};
class B2: virtual public B0 //B0为虚基类派生B2类
{ public:
B2(int a) : B0(a) {}//构造函数
int nV2;
};
class D1: public B1, public B2 //派生类D1声明
{ public:
D1(int a=0) : B1(a+1), B2(a+2){}//构造函数
int nVd;
void fund(){cout<<"Member of D1:"<<nVd<<endl;}
};
int main()
{ D1 d1;
d1.fun();
d1.B1::fun();
d1.B2::fun();
return 0;
}
运行结果:
Member of B0:0
Member of B0:0
Member of B0:0
基类B0在D1的构造函数列表中如果未列出,则表示调用该虚基类的缺省无参构造函数。
例10.5中派生类对象存储结构示意图:
从不同路径继承过来的同名数据成员在内存中只有一个拷贝,同一个函数名也只有一个映射。
虚基类及其派生类构造函数
- 建立对象时所指定的类称为最(远)派生类。
- 虚基类的成员是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。
- 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的缺省构造函数。
- 在建立对象时,只有最(远)派生类的构造函数调用虚基类的构造函数,该派生类的其它基类对虚基类构造函数的调用被忽略。
示例
将例10.5的D类构造函数作如下修改:
D1(int a=0) :B0(a+3),B1(a+1), B2(a+2){}//构造函数
则运行结果为
Member of B0:3
Member of B0:3
Member of B0:3
出现这种结果是因为在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其它基类对虚基类构造函数的调用被忽略。
虚函数与多态性
重点:运行时多态性(围绕动态联编、虚函数、纯虚函数和抽象类等)
#include <iostream>
using namespace std;
class CPerson
{public:
void set(char *p,int x,int y)
{strcpy(name,p); num=x; sex=y;}
void out();
private:
char name[8];
int num,sex;
};
void CPerson::out()
{ cout<<"name:"<<name<<",num:"<<num;
if(sex==0) cout<<",sex:男"<<endl;
else cout<<",sex:女"<<endl;
}
class CStudent:public CPerson
{public:
void addscore(float x)
{ score=x;}
void out()
{ CPerson::out();
cout<<"score:"<<score<<endl;
}
private:
float score;
};
int main()
{ CStudent s1; CPerson s;
s1.set("wu",1,0);
s1.addscore(95.5f);
s=s1;
s.out();
return 0;
}
结果:
name:wu,num:1,sex:男
#include <iostream>
using namespace std;
class CPerson
{public:
void set(char *p,int x,int y)
{strcpy(name,p); num=x; sex=y;}
void out();
private:
char name[8];
int num,sex;
};
void CPerson::out()
{ cout<<"name:"<<name<<",num:"<<num;
if(sex==0) cout<<",sex:男"<<endl;
else cout<<",sex:女"<<endl;
}
class CStudent:public CPerson
{public:
void addscore(float x)
{ score=x;}
void out()
{ CPerson::out();
cout<<"score:"<<score<<endl;
}
private:
float score;
};
int main()
{ CStudent s1; CPerson *s;
s1.set("wu",1,0);
s1.addscore(95.5f);
s=&s1;
s->out();
return 0;
}
结果:
name:wu,num:1,sex:男
赋值兼容规则(向上转型:子类的对象可以赋值给父类,也就是子类对象可以向上转型为父类类型。 )
一个公有派生类的对象在使用上可以被当作基类的对象,反之则禁止。具体表现在:
- 派生类的对象可以被赋值给基类对象。
- 派生类的对象可以初始化基类的引用。
- 指向基类的指针也可以指向派生类,即一个公有派生类对象的指针值可以赋值给(或初始化)一个基类指针。
- 利用这样的指针或引用,只能访问派生类对象中从基类继承过来的成员,无法访问派生类的自有成员。
例11.1 赋值兼容规则示例
#include <iostream>
#include <cstring>
using namespace std;
class B
{
char name[80];
public:
void put_name(char *s)
{ strcpy(name,s); }
void show_name()
{ cout<<name<<endl; }
};
class D: public B
{
char phone_num[80];
public:
void put_phone(char *num)
{ strcpy(phone_num,num); }
void show_phone()
{ cout<<phone_num<<endl; }
};
int main()
{
B *p;
B Bobj;
D *dp;
D Dobj;
p=&Bobj;
p->put_name("Zhang Fang");
p=&Dobj;
p->put_name("Wang Ming");
Bobj.show_name();
Dobj.show_name();
dp=&Dobj;
dp->put_phone("83768493");
dp->show_phone();
p->show_phone(); //不能访问
((D *)p)->show_phone(); //强制类型转换,即类型适应性原则,动态多态
return 0;
}
结果:
Zhang Fang
Wang Ming
83768493
83768493
注意:如果希望用基类指针访问其公有派生类的特定成员,必须将基类指针用显式类型转换为派生类指针。根据类型适应性的原则,一个指向基类的指针可用来指向以公有派生的任何对象。这是是C++ 实现运行时多态性的关键。
多态性和虚函数
封装性、继承性和多态性构成了面向对象程序设计语言的三大特性。
封装性是基础,继承性是关键,多态性是扩充。
多态性即:
- 一个接口:主要指对类的成员函数的调用。
- 多种实现:不同的实现。
即对不同类的对象发出相同的消息将会有不同的行为。
多态从实现的角度来讲可以划分类两类:
- 编译时的多态:编译(静态联编)的过程中确定操作对象的函数。编译时多态通过函数重载和运算符重载来体现。
- 运行时的多态:程序运行过程(动态联编)中才动态地确定操作对象的函数。运行时的多态通过继承与虚函数来体现。
上述确定操作的具体对象的过程就是联编。
虚函数允许函数调用与函数体的联系在运行时才给出。当需要同一接口、多种实现时,这种功能显得尤其重要。
例11.2 静态联编示例
#include <iostream>
using namespace std;
class Base
{protected:
int x;
public:
Base(int a)
{ x=a; }
void print()
{cout<<"Base:"<<x<<endl;}
};
class First_d: public Base
{public:
First_d(int a):Base(a){}
void print()
{cout<<"First derivation:"<<x<<endl;}
};
class Second_d: public Base
{ public:
Second_d(int a):Base(a){}
void print()
{cout<<"Second derivation:"<<x<<endl;}
};
int main()
{
Base *p;
Base obj1(1);
First_d obj2(2);
Second_d obj3(3);
p=&obj1;
p->print();
p=&obj2;
p->print();
p=&obj3;
p->print();
obj2.print();
obj3.print();
return 0;
}
运行结果:
Base:1
Base:2
Base:3
First derivation:2
Second derivation:3
注意:指向基类的指针P,在运行前,p->print() 已确定为访问基类的成员函数print()。所以不管P 指向基类,还是派生类的对象,p->print()都是基类绑定的成员函数,结果都相同。这是静态联编的结果。
例11.3动态联编示例
#include <iostream>
using namespace std;
class Base
{protected:
int x;
public:
Base(int a)
{ x=a; }
virtual void print()//继承是动态联编的前提,虚函数是动态联编的基础。
{cout<<"Base:"<<x<<endl;}
};
class First_d: public Base
{public:
First_d(int a):Base(a){}
void print()
{cout<<"First derivation:"<<x<<endl;}
};
class Second_d: public Base
{ public:
Second_d(int a):Base(a){}
void print()
{cout<<"Second derivation:"<<x<<endl;}
};
int main()
{
Base *p;
Base obj1(1);
First_d obj2(2);
Second_d obj3(3);
p=&obj1;
p->print();
p=&obj2;
p->print();
p=&obj3;
p->print();
obj2.print();
obj3.print();
return 0;
}
运行结果:
Base:1
First derivation:2
Second derivation:3
First derivation:2
Second derivation:3
注意:采用动态联编,则随p 指向的对象不同,使p->print() 能调用不同类中print()版本。即该函数调用依赖于运行时P 所指向的对象,具有多态性。
虚函数
- 虚函数是动态联编的基础。
是非静态的成员函数。 - 在类的声明中,在函数原型之前写virtual。
virtual 只用来说明类声明中的原型,不能用在函数实现时。
具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。 - 本质:不是重载声明而是覆盖。
- 调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。
如果是通过基类对象调用虚函数,不适合动态联编。
例11.4虚函数示例
#include <iostream>
using namespace std;
class B0 //基类B0声明
{
public: //外部接口
virtual void display( )
{cout<<"B0::display( )"<<endl;}
};
class B1: public B0 //公有派生
{ public:
void display( )
{ cout<<"B1::display( )"<<endl; }
};
class D1: public B1 //公有派生
{ public:
void display( )
{ cout<<"D1::display( )"<<endl; }
};
void fun(B0 *ptr) //普通函数
{ ptr->display( ); }
int main( ) //主函数
{
B0 b0, *p;//声明基类对象和指针
B1 b1;//声明派生类对象
D1 d1;//声明派生类对象
p=&b0;
fun(p);//调用基类B0函数成员
p=&b1;
fun(p);//调用派生类B1函数成员
p=&d1;
fun(p);//调用派生类D1函数成员
return 0;
}
程序的运行结果为:
B0::display()
B1::display()
D1::display()
抽象类的一般形式
带有纯虚函数的类称为抽象类:
class 类名
{
virtual 类型 函数名(参数表)=0; //纯虚函数
...
}
抽象类作用:
- 为抽象和设计的目的而建立,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
- 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意:
1、抽象类只能作为基类来使用。
2、不能声明抽象类的对象。
3、构造函数不能是虚函数,析构函数可以是虚函数。
例11.5纯虚函数与抽象类用法示例1
B0类的display函数就是一个纯虚函数,没有函数体,因此B0是一个抽象类。
class B0 //抽象基类B0声明
{
public: //外部接口
virtual void display( )=0;
//纯虚函数成员
};
class B1: public B0 {
public:
void display ( ){cout<<"B1::display( )"<<endl;}
};
class D1: public B1 //公有派生
{
public:
void display ( ) {cout<<"D1::display( )"<<endl;}
};
void fun(B0 *ptr) //普通函数
{ ptr->display ( ); }
int main ( ) //主函数
{ B0 *p; //声明抽象基类指针
B1 b1; //声明派生类对象
D1 d1; //声明派生类对象
p=&b1;
fun(p); //调用派生类B1函数成员
p=&d1;
fun(p); //调用派生类D1函数成员
return 0;
}
程序的运行结果为:
B1::display( )
D1::display( )
例11.6 纯虚函数与抽象类用法示例2
#include <iostream>
using namespace std;
class Shape
{
public:
virtual float area() = 0; // 将area定义成纯虚函数
};
int main()
{
Shape *pShape;
Triangle tri(3, 4);
cout<<tri.area()<<endl;
pShape = &tri;
cout<<pShape->area()<<endl;
Circle cir(5);
cout<<cir.area()<<endl;
pShape = ○
cout<<pShape->area()<<endl;
return 0;
}
程序的运行结果为:
6
6
78.5398
78.5398