派生类的生成过程

1.吸收基类成员

将基类的成员(除析构函数和构造函数外)全盘接收,经过派生过程,基类的这些成员则隐藏在派生类中,派生类对象的内存空间也会有基类成员的相应位置

2.改造基类成员

  • 改造基类成员的访问权限:依靠派生类声明时的继承方式来控制,比如说基类的private成员,它明明就存在于派生类中,但是自身无法访问,必须call基类的成员函数访问

  • 对基类数据或函数成员的覆盖:

    如果派生类声明了一个和某个基类成员同名的新成员,派生的新成员就覆盖了外层同名函数。这时,在派生类中或者通过派生类的对象直接使用成员名就只能访问到派生类中声明的同名成员,这称为同名覆盖。

3.添加新的成员

我们可以根据实际情况的需要,给派生类添加适当的数据和函数成员,来实现必要的新增功能。在派生过程中,由于基类的构造函数和析构函数是不能被继承下来的,因此我们就需要在派生类重新加入新的构造函数和析构函数来实现一些特别的初始化和清理工作

C++派生类中与基类同名函数的调用

对于同名数据成员,我们可以通过 对象名.基类名::成员名 来访问基类成员。对于同名函数,情况比较复杂,我们来讨论下。

子类中的函数与基类的函数的情况共有三种情况

1、不是同名函数

2、函数名相同,形参的个数或类型不同。

3、函数名相同,形参的个数和类型也相同。

1、在一般情况下,子类中的函数与基类的函数不是同名函数,此时,可以直接通过子类对象调用基类的函数。

1
2
3
4
//基类A中有print函数
//派生类B中有bprint函数
B b;
b.print();

2&3.当子类中的函数与基类的函数,函数名相同,无论形参的个数或类型相同或不同 ,基类的函数 会被子类的函数覆盖,直接用子类对象调用同名函数会默认调用子类的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;
class A{
public:
void print(int a,int b){
cout<<"A"<<endl;
}
};
class C{
public:
void print(int a){
cout<<"C"<<endl;
}
};
class B:public A,public C{
public:
void print(int a){
cout<<"B"<<endl;
}
};
int main(){
B b;
b.print(3,4);//报错
b.print(3);//调用子类函数
}

此时要从子类中访问基类的函数有两种方法:
1、定义基类指针或者引用 ,让基类指针或引用指向派生类对象,用指针或引用调用基类函数。

1
2
3
4
5
6
//基类A void print(int a,int b);
//派生类 void print(int a);
A *p = &b;
A &a = b;
p->print(3,4);
a.print(3,4);

2、显式调用基类函数,既在基类函数之前加上基类名和域说明符。

1
b.A::print(3,4);//对象名.类名::函数

注:

同名函数的重载动作,只发生在自由函数(即非成员),及同一个class/struct内部的函数之间。而不能跨越基类和派生类。当派生类写一个和基类同名(无论参数列表相同或不相同)的函数时,此时发生的动作叫“覆盖”。覆盖的意思,就是基类的同名函数,在派生类内,将变得无法直接调用(但可以间接调用)。

基类指针和引用可以指向派生类对象

1.基类指针可以指向派生类对象
2.基类引用可以引用派生类对象

反之不可

试想,基类引用可以引用派生类对象,则可以使用基类引用调用派生类对象的方法,因为派生类继承了基类的方法,基类调用派生类是没问题的。 若派生类引用能引用基类对象,这就要出问题了。因为派生类中可能有基类没有的方法,用派生类掉用基类会出现不确定的情况,编译器不允许这种情况出现。
总而言之可以这样认为:派生类包含基类,少的调用多的没问题,多的调用少的就可能出问题了。

3.基类指针和引用只能用来调用基类的成员函数 (数据成员由于是私有属性,当然不行,所以不讨论这个)。

如果试图通过基类指针调用派生类才有的成员函数,则编译器会报错。

为了避免这种错误,必须将基类指针强制转化为派生类指针。然后派生类指针可以用来调用派生类的功能。这称为向下强制类型转换,这是一种潜在的危险操作。

4.如果在基类和派生类中定义了虚函数(通过继承和重写),并通过基类指针在派生类对象上调用这个虚函数,则实际调用的是这个函数的派生类版本 。(下面会详细叙说)

5.对于基类指针和引用,可以在参数传递和赋值的时候指向同类对象和派生类对象,对于参数传值调用和基类变量,只能传递同类对象和赋值同类对象 (因为传值调用的是基类的拷贝构造函数,无法构造派生类中特有的成员)

虚基类的用法和意义

1.意义:

解决多重多级继承造成的二义性问题。例如有基类B,从B派生出C和D,然后类F又同时继承了C和D,现在类F的一个对象里面包含了两个基类B的对象,如果F访问自己的从基类B那里继承过来的的数据成员或者函数成员那么编译器就不知道你指的到底是从C那里继承过来的B对象呢还是从D那里继承过来的B对象。

于是虚基类诞生了,将C和D的继承方式改为虚继承,那么F访问自己从B那里继承过来的成员就不会有二义性问题了,也就是将F对象里的B对象统一为一个,只有一个基类B对象(注意是F,而不是C和D)

注:

针对多义性问题,可使用作用域运算符来避免,具体地说,就是通过作用域运算符明确指定调用哪个类的成员。

作用域运算符就是“::”,具体使用的格式如下:

派生类对象名.基类名::数据成员名; //访问数据成员

派生类对象名.基类名::成员函数名(参数表); //访问成员函数

2.用法:

虚基类由关键字virtual标识,一般语法格式如下:

class 派生类名: virtual 继承方式 基类名

虚基类不是在声明基类时声明,而是在声明派生类时,在继承方式前加关键字vitual加以声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;
class A
{
public:
int i;
void showa(){cout<<"i="<<i<<endl;}
};
class B:virtual public A //此处采用虚继承
{
public:
int j;
};
class C:virtual public A //此处采用虚继承
{
public:
int k;
};
class D:public B,public C
{
public:
int m;
};
int main()
{
A a;
B b;
C c;
a.i=1;
a.showa();
b.i=2;
b.showa();
c.i=3;
c.showa();
D d;
d.i=4;//若D的父类B C没有声明为虚基类,就会在d中产生两个i副本,从而报错
d.showa();
return 0;
}

下一篇讨论多态性,会详细说到虚函数的用处。