C++之继承

面向对象的三大特性:封装、继承和多态,总结一下C++的继承以及注意事项。


继承关系

C++共有三种继承方式:public、protected和private,一般语法为:

class 类名 : [继承方式] 基类名 {
};

如果不写继承方式,默认是private继承。继承方式和访问修饰符的关系如下:

继承方式 public成员 protected成员 private成员
public public protected 不可见
protected protected protected 不可见
private private private 不可见
  • 使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public,但是不能改变private成员的访问权限;
  • 同名隐藏:子类和父类中有同名成员时,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用 基类::基类成员 访问父类成员),主要名字相同就会发生隐藏,这个应该避免;
  • 函数覆盖:子类对象和父类对象的函数名字和参数相同时,就会发生函数覆盖;
  • 函数重载:函数名字相同,参数列表不同(参数类型、参数个数),就会发生函数重载;
  • 构造函数,析构函数,拷贝构造函数、赋值拷贝函数不能被继承;
  • 继承中构造函数的调用顺序:基类构造函数->派生类中对象构造函数->派生类构造函数,析构函数调用顺序刚好相反。

多继承和菱形继承

一个子类只有一个直接父类,这种关系为单继承。
一个子类两个或以上的直接父类,这种关系为多继承,多继承里面比较典型的菱形继承。

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
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};
class B : public A {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};
class C : public A {
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
};
class D : public B, public C {
public:
D() {
cout << "D()" << endl;
}
~D() {
cout << "D()" << endl;
}
};
int main() {
D d;
return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()

菱形继承会产生二义性和数据冗余的问题,解决这个的办法是使用虚继承。
本来以为这部分的内容会比较简单,总结起来比较容易,没想到这部分的内容还是比较复杂的,先来看一个例子:

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
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};
class B : public virtual A {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};
class C : public virtual A {
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
};
class D : public B, public C {
public:
D() {
cout << "D()" << endl;
}
~D() {
cout << "~D()" << endl;
}
};
int main() {
D d;
return 0;
}

运行结果:

1
2
3
4
5
6
7
8
A()
B()
C()
D()
~D()
~C()
~B()
~A()

从运行结果来看,当虚继承的时候,D只保留了一份A的数据,具体是怎么实现的呢?这是通过虚类指针来实现的,下面我用逐步调试的方式来揭开虚类指针的神秘面纱。

1
2
3
4
5
int main() {
C c;
cout << sizeof(c) << endl;
return 0;
}

打印对象c占用4个字节,查看c的内存:

查看内存0xf5f7a8,发现其指向一块内存:

查看内存0x00e49b3c中的值:

其中低地址为0,高地址为4,这两个值的含义是:

  • 第一个指向类中虚类指针的偏移量;
  • 第二个指向的是类中虚继承的基类的偏移量,由于我们基类中没有任何成员,所以这个地址是4。
    对象c的内存模型如下:

    自此我们也看到了虚表指针,再来看看对象d,这里就不详细说明过程了,只贴出图片。

    sizeof(d) = 8



因为d继承了两个基类,所有有两张虚基类表(vbptr),第一个里面的偏移量是0和8,第二是0和4,内存模型如下:

对于这部分内容举的例子不是很到位,多态会更详细说明这一点。


友元函数和static继承

  • 友元函数不能被子类继承,派生类的friend函数可以访问派生类本身的一切变量,包括从父类继承下来的protected域中的变量。但是对父类来说,他并不是friend的。
  • 静态成员可以被继承,类的模型是所有对象的数据成员单独存储,但是所有成员函数和静态成员是共用一段空间的 。

参考资料:
C++继承权限和继承方式
C++继承详解之二——派生类成员函数详解
c++继承详解之一——继承的三种方式、派生类的对象模型