前言
前面讲了封装,但封装只是隐藏了类内部实现。如果使用多态隐藏类本身的话,只有封装是不够的,还需要继承。
继承
通过封装。我们把一些相关的函数和变量包裹在了一起,这些函数和变量就叫做类的成员函数和成员变量。继承就是一种获取这个类的成员函数和成员变量的方式。通常,继承了某个类的类叫做该类的派生类或者子类。
根据封装的意义,当父类将部分成员函数和成员变量的访问权限设置为private时,即使被继承了,子类仍然无法访问。
下面通过例子来说明一下,子类是如何保存这些继承来的成员函数和成员变量的。
class A
{
public:
A() : a( 1 )
{
}
void foo()
{
std::cout << "A::foo()" << std::endl;
return;
}
private:
int a;
};
class B : public A
{
public:
B() : b( 2 )
{
}
private:
int b;
};
int main()
{
B x;
x.foo();
return 0;
}
继承可以是public、protected或private,不同关键字为父类设置了不同访问权限。
- public继承意味着父类所有成员成员变量的访问权限在子类中维持不变
- protected继承意味着父类中public的成员函数和成员变量在子类中变为protected
- private继承意味着父类中public和protected的成员函数和成员变量在子类中变为private
成员函数
因为类成员函数可以通过隐式参数this区分具体的调用对象,所以类成员函数只需要存在一份就可以。当子类继承父类的成员函数时,子类只是得到了通过子类对象访问父类成员函数的权利。
隐式参数意味着你没有写,但是编译器帮你写了。
当我们gdb调试上面的代码时,会发现x.foo()
实际调用的就是A::foo()
,而不是A::foo()
的一份拷贝。
0x00000000004004fe <+8>: lea -0x10(%rbp),%rax # -0x10(%rbp)就是x的地址
0x0000000000400502 <+12>: mov %rax,%rdi # 将x的地址作为A::foo()的参数,也就是this
0x0000000000400505 <+15>: callq 0x400512 <A::foo()>
其实任何函数都只需要存在一份,成员函数只是一个稍微特殊的函数。
虚函数又是一个稍微特殊的成员函数,它也只存在一份,只不过是在调用上可能要多些操作,细节在讲多态的时候再说。
成员变量
每个类的实例对象都要变更自己的成员变量,因此其空间肯定都是独立的。当子类继承父类的成员变量时,实际只是继承了父类的数据结构。
当通过gdb打印x
的值时,我们会发现它的结构如下
(gdb) p x
$1 = {<A> = {a = 1}, b = 2}
当我们直接查看x
的地址的内容时,会发现A::a
和B::b
就是连续排布的。
(gdb) x /2x &x
0x7fffffffe540: 0x00000001 0x00000002
也就是说当子类继承父类时,子类的数据结构就是父类的成员变量加上子类自身的成员变量。
之所以父类的成员变量放在前,是因为父类指针可以直接指向子类,当以父类指针操作父类成员变量时就不需要额外进行地址偏移了。如果子类成员变量在前,那么父类指针操作时还需要跳过子类成员变量。
我们大胆推测当子类继承多个父类时,子类的数据结构就是写在前的父类的成员变量加上写在后的父类的成员变量再加上子类自身的成员变量,事实也确实如此。
多继承时,因为存在多个父类数据结构,所以当不同的父类指针指向子类时,会进行一定偏移,保证该父类指针刚好指向自己的数据结构的起始位置。
多继承会复杂化类关系图,而且在一些场景下会带来歧义,因此都不建议使用多继承。反正我自己到现在为止都没在实际项目中用过,只是在一些开源代码中看到过。
结语
继承除了是多态的基础外,还是一种复用代码的方式。但是谨记只有存在父子关系时才使用继承,如果只是为了复用代码的话,应当使用组合。