一.多继承
派生类都只有一个基类,成为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类。
多继承的语法很简答,将多个基类用逗号隔开即可,例如已申明了类A、类B和类C,那么可以这样来声明类D。
class D: public A, private B, protected C
{ //类D新增加的成员
}
D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。
多继承形式下的构造函数和单继承形式相同,只要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:
D(形参列表) : A(实参列表),B(实参列表),C(实参列表) { //其他操作}
基类构造函数的调用顺序和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类的出现顺序相同
#include <iostream>
using namespace std;
class BaseA
{
public:
BaseA(int a,int b);
~BaseA();
protected:
int m_a;
int m_b;
};
BaseA::BaseA(int a, int b):m_a(a),m_b(b)
{
cout << "BaseA constructor" << endl;
}
BaseA::~BaseA()
{
cout << "BaseA destructor" << endl;
}
class BaseB
{
public:
BaseB(int c,int d);
~BaseB();
protected:
int m_c;
int m_d;
};
BaseB::BaseB(int c, int d):m_c(c),m_d(d)
{
cout << "BaseB constructor" << endl;
}
BaseB::~BaseB()
{
cout << "BaseB destructor" << endl;
}
class Derived:public BaseA,public BaseB
{
public:
Derived(int a,int b,int c,int d,int e);
~Derived();
public:
void show();
private:
int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e):BaseA(a,b),BaseB(c,d),m_e(e)
{
cout << "Derived constructor" << endl;
}
Derived::~Derived()
{
cout << "Derived destructor" << endl;
}
void Derived::show()
{
cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}
int main()
{
Derived d(1,2,3,4,5);
d.show();
return 0;
}
运行结果: 多多继承形式下析构函数的执行顺寻和构造函数的执行顺序与单继承相同
[root@localhost CPP]# ./djc3_14
BaseA constructor
BaseB constructor
Derived constructor
1, 2, 3, 4, 5
Derived destructor
BaseB destructor
BaseA destructor
二.命名冲突
当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个积累的成员,这个时候需要在成员名字前面加上类名和域解析符::,以显示地指明到底使用哪个类的成员,消除二义性。
#include <iostream>
using namespace std;
class BaseA
{
public:
BaseA(int a,int b);
~BaseA();
public:
void show();
protected:
int m_a;
int m_b;
};
BaseA::BaseA(int a, int b):m_a(a),m_b(b)
{
cout << "BaseA conatrutor" << endl;
}
BaseA::~BaseA()
{
cout << "BaseA destructor" << endl;
}
void BaseA::show()
{
cout << "m_a = " << m_a << endl;
cout << "m_b = " << m_b << endl;
}
class BaseB
{
public:
BaseB(int c,int d);
~BaseB();
public:
void show();
protected:
int m_c;
int m_d;
};
BaseB::BaseB(int c, int d):m_c(c),m_d(d)
{
cout << "BaseB conatrutor" << endl;
}
BaseB::~BaseB()
{
cout << "BaseB destructor" << endl;
}
void BaseB::show()
{
cout << "m_c = " << m_c << endl;
cout << "m_d = " << m_d << endl;
}
class Derived:public BaseA,public BaseB
{
public:
Derived(int a,int b,int c,int d,int e);
~Derived();
public:
void display();
private:
int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e)
{
cout << "Derived constructor" <<endl;
}
Derived::~Derived()
{
cout << "Derived destructor" <<endl;
}
void Derived::display()
{
BaseA::show(); //调用BaseA里面的show()函数
BaseB::show(); //调用BaseB里面的show()函数
cout << "m_e = " << m_e << endl;
}
int main()
{
Derived obj(1,2,3,4,5);
obj.display();
return 0;
}
运行结果:
[root@localhost CPP]# ./djc3_15
BaseA conatrutor
BaseB conatrutor
Derived constructor
m_a = 1
m_b = 2
m_c = 3
m_d = 4
m_e = 5
Derived destructor
BaseB destructor
BaseA destructor
多继承是指 多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来丛总复杂的设计问题,命名冲突就是不可回避的一个。
多继承时很容易产生命名冲突,即使哦我们很小心的将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突仍然存在有可能发生,如:
三.C++虚继承和虚基类
为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。
在继承方式的前面加上 virtual 关键字
虚继承的目的是让某个类作出声明,承诺愿意共享他的基类,其中,这个被共享的基类就被称为虚基类,本例中的A就是一个虚基类,在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员
#include <iostream>
using namespace std;
//间接基类A
class A
{
protected:
int m_a;
};
//直接基类B
class B:virtual public A
{
protected:
int m_b;
};
//直接基类C
class C: virtual public A
{
protected:
int m_c;
};
//派生类D
class D: public B, public C
{
public:
//void seta(int a){ m_a = a; } //命名冲突
void seta1(int a){ B::m_a = a; }
void seta2(int a){ C::m_a = a; }
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main()
{
D d;
return 0;
}
四.C++向上转型
在C/C++中经常会发生数据类型的转换,例如将int 类型的数据赋值给float类型的变量时,编译器会先把int 类型的数据转换为float类型再赋值,反过来float类型的数据在经过类型转换后也可以赋值给int 类型的变量。
数据类型转换的前提是,编译器知道如何对数据进行取舍。例如
int a = 10.9; printf("%d\n", a);
输出结果为 10,编译器会将小数部分直接丢掉(不是四舍五入)。
再如: float b = 10; printf("%f\n", b);
输出结果为 10.000000,编译器会自动添加小数部分。
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在C++中成为给向上转型(Upcasting)。相应的,将基类赋值给派生类称为向下转型(Downingcasting)。
向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。
赋值的本质是将现有的数据写入已分配好的内存当中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。运行结果也有利的证明了这一点,虽然有a=b;这样的赋值过程,但是a.display()始终调用的A类的。换句话说,对象之间的赋值不会影响成员函数,也不会影响this指针。
将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”,如下图所示:
#include <iostream>
using namespace std;
class A
{
public:
A(int a);
public:
void display();
public:
int m_a;
};
A::A(int a):m_a(a){}
void A::display()
{
cout<<"Class A: m_a="<<m_a<<endl;
}
class B:public A
{
public:
B(int a,int b);
public:
void display();
public:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display()
{
cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
int main()
{
A a(10);
B b(19,6);
//赋值前
a.display();
b.display();
cout<<"--------------"<<endl;
//赋值后
a = b;
a.display();
b.display();
return 0;
}
运行结果:
[root@localhost CPP]# ./xs3_17
Class A: m_a=10
Class B: m_a=19, m_b=6
--------------
Class A: m_a=19
Class B: m_a=19, m_b=6
可以发现,即使将派生类对象赋值给基类对象, 基类对象也不会包含派生类的成员,所以依然不同用过基类对象来访问派生类成员。对于上面的例子,a.m_a 是正确的,但 a.m_b 就是错误的,因为 a 不包含成员 m_b。
这种转换关系不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。
要理解这个问题,还得从赋值的本质入手。赋值实际上是向内存填充数据,当数据较多时很好处理,舍弃即可;本例中将 b 赋值给 a 时(执行a=b;语句),成员 m_b 是多余的,会被直接丢掉,所以不会发生赋值错误。但当数据较少时,问题就很棘手,编译器不知道如何填充剩下的内存;如果本例中有b= a;这样的语句,编译器就不知道该如何给变量 m_b 赋值,所以会发生错误。
除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值)。
#include <iostream>
using namespace std;
class A
{
public:
A(int a);
public
:
void display();
protected:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display()
{
cout<<"Class A: m_a="<<m_a<<endl;
}
//中间派生类B
class B: public A
{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display()
{
cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//基类C
class C
{
public:
C(int c);
public:
void display();
protected:
int m_c;
};
C::C(int c): m_c(c){ }
void C::display()
{
cout<<"Class C: m_c="<<m_c<<endl;
}
//最终派生类D
class D: public B, public C
{
public:
D(int a, int b, int c, int d);
public:
void display();
private:
int m_d;
};
D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
void D::display()
{
cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main()
{
A *pa = new A(1);
B *pb = new B(2, 20);
C *pc = new C(3);
D *pd = new D(4, 40, 400, 4000);
pa = pd;
pa -> display();
pb = pd;
pb -> display();
pc = pd;
pc -> display();
cout<<"-----------------------"<<endl;
cout<<"pa="<<pa<<endl;
cout<<"pb="<<pb<<endl;
cout<<"pc="<<pc<<endl;
cout<<"pd="<<pd<<endl;
return 0;
}
运行结果: 本例中我们将最终派生类的指针 pd 分别赋值给了基类指针 pa、pb、pc,按理说它们的值应该相等,都指向同一块内存,但是运行结果却有力地反驳了这种推论,只有 pa、pb、pd 三个指针的值相等,pc 的值比它们都大。也就是说,执行pc = pd;语句后,pc 和 pd 的值并不相等。
[root@localhost CPP]# ./xs3_18
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
-----------------------
pa=0x9adf038
pb=0x9adf038
pc=0x9adf040
pd=0x9adf038
我们将派生类指针pd赋值给基类指针pa,从运行结果可以看出,调用display()函数虽然使用了派生类的成员变量,但是display()函数本身确实基类的,也就是说,将派生类的指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,大但不能使用派生类的成员函数,这看起来优点不伦不类,为什么?
pa本来是基类A的指针,现在指向了派生类D的对象,这使得隐式指针this发生了变化,也指向了D类的对象,所以最终在display()内部使用的是D类对象的成员变量。
编译器虽然通过指针的指向来访问成员变量,但是不能通过指针的指向访问成员函数:编译器通过指针的类型来访问成员函数。对于pa ,它的类型是A,不管他指向哪一个对象,使用的都是A类的成员函数。
概括起来就是:编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据,;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的成员函数。
(pa = pd 意思就是,pa这个指针指向了D类对象,但是不能改变pa的类型,依然是A)