OOP_C++_继承
一、什么是继承?
继承是面向对象程序设计(OOP)的核心思想之一。继承是让代码可以复用的重要手段之一,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。由基类产生出来的新类称为派生类。使用继承可以定义类似的类型并对其相似关系建模,有利于呈现面向对象程序设计的层次结构。
PS:上面这段话几乎囊括了所有C++书籍中对继承的解释,但是显然对于初学者而言,它不那么友好。。。下面我用通俗的例子来说明什么是继承:
请看这张图:

我用手机的演变来说明继承,p2继承自p1,所以p2拥有p1的成员,并且可以再创建属于自己的成员。p1就是p2的直接基类,p2就是p1的派生类,由于p3继承自p2,所以p1是p3的间接基类。
由于在手机的演变中,以后的每一款手机都需要由“接打电话”和“短信”的功能,所以就把它作为了基类,以后的派生类只要说明继承自基类,就不用再写基类的代码,这样就起到了一种代码复用的效果。
二、继承的格式
我还是先写一个继承的实例,然后读者再结合实例来理解继承的格式:
//基类
class Base
{
public:
int _data1;
}
//派生类
class Derived:public:Base
{
public:
int _data2;
}
格式:
class 派生类名称:继承权限 基类名称
{
派生类代码
}
三、继承的权限
上面的继承格式中提到了继承权限,那么什么是继承权限呢?
既然派生类中会有基类的成员,那么这些成员在基类中的访问权限到了派生类会不会改变呢?比方说:基类中的_data1在派生类中是否也是public权限呢?这种继承方式有什么来决定,答案就是继承权限。继承权限就决定了基类中的成员的访问限定在派生类中是否改变?怎么改变?
下面表格列出的就是成员访问权限的变化方式:

验证起来也很简单,下面我验证一下其中的protected继承,其他的两种验证方法也类似:
#include<iostream>
using namespace std;
class A
{
public:
int aa;
protected:
int bb;
private:
int cc;
};
class B : private A
{
public:
int test_get_aa(){
return aa;
}
int test_get_bb(){
return bb;
}
int test_get_cc(){
return cc;//编译的时候这里会报错
}
};
class C :public B
{
public:
int test_get_aa(){
return aa;//编译的时候这里会报错
}
int test_get_bb(){
return bb;//编译的时候这里会报错
}
int test_get_cc(){
return cc;//编译的时候这里会报错
}
};
int main(void)
{
return 0;
}
编译结果:
这样的结果就说明了派生类B使用private方式从A中继承的cc无法访问,因为cc在A中的访问权限是private,所以虽然cc存在于派生类B,但是对B不可见,也就是B无权访问。
而B采用private继承A,所以A中所有成员的访问权限在B中都变成了private,所以B的派生类C不可访问从B中继承来的成员,因为它们的访问权限都变成了private。
下面是对这些继承权限一些说明:
- public继承是一个接口继承,每个父类的成员对子类也都可用。继承关系中采用的大都是这种继承方式,其他两种极少使用。
- 使用class继承时,采用的默认继承方式是private;使用struct继承时,采用的默认继承方式是public
四、派生类默认成员函数
在继承体系下,如果没有显式定义下面这六个成员函数,则编译器会隐式合成它们。(构造函数,拷贝构造函数,析构函数,赋值操作符重载函数,取地操作符重载函数,const修饰的取地址操作符重载函数)
继承体系下派生类和基类的构造函数执行次序:从最顶层的基类开始,依次向下执行每个类的构造函数。
注意:这里我强调了是执行次序,而不是调用次序,那么调用次序是什么呢?下面我用一个例子说明。
class A
{
public:
A(int data) :_a(data){ }
private:
int _a;
};
class B: public A
{
public:
B(int data) :A(data), _b(data)
{}
private:
int _b;
};
代码说明:在创建派生类B的对象的时候,需要调用派生类的构造函数吧,而基类的构造函数是在派生类构造函数的初始化表中调用的,所以,调用顺序是:先调用最底层的类的构造函数,然后依次往上调用每个类的构造函数
继承体系下派生类和基类的析构函数调用次序:从最下面的派生类开始,依次向上调用每个类的析构函数
注意:
- 基类没有定义构造函数,则派生类也可以不用定义,全部都使用缺省构造函数
- 基类定义了带有形参表的构造函数,派生类就一定要定义构造函数
原因:编译器无法缺省给基类的自定义构造函数传值
继承体系中的作用域
1.需要注意,基类和派生类是两个不同的作用域。
2.子类会屏蔽父类的同名成员,哪怕成员类型不相同。这种特性称为隐藏 / 重定义如果确实需要访问父类中的同名成员,需要指定作用域:基类名::同名基类成员
需要注意的是:在实际继承体系中,最好不要定义重名的成员!!!
赋值兼容规则
这种规则一般仅局限于public继承。
- 子类对象可以赋值给父类对象(切片),父类对象不能赋给字类对象
- 父类指针/引用可以指向子类对象,子类指针/引用不可以指向父类对象(可以用强制类型转换强制指向,不过最好不要这样做)
五、继承体系下派生类的对象模型
以下的对象模型都用这段代码来辅助说明:
class A
{
protected:
int _a;
};
class B :public A
{
protected:
int _b;
};
class C :public A
{
protected:
int _c;
};
class D :public B, public C
{
protected:
int _d;
};
1.单继承
B和C各自对A的继承方式就称之为单继承。如果创建一个类B的对象obj_b,那么它的对象模型就是这个样子的:
那为什么不是派生类独有的数据在上面呢?
因为在创建派生类B的对象的时候,先执行基类的构造函数。
2.多继承
多继承是指派生类对象继承自多个对象,也就是它有多个直接基类。上面代码中对象D就是一个多继承的例子,它继承自对象B和对象C。
注:由于上面D的继承是多继承中的一种比较复杂的继承方式,我就不用上面的例子了,单独举一个简单的能说明问题的例子。
class Base1
{
public:
int _b1;
}
class Base2
{
public:
int _b2;
}
class Derived:public Base1,public Base2
{
public:
int _d;
}
类Derived的对象obj_d的对象内存模型如下:

3.菱形继承
菱形继承就是这种继承路线的继承:
上面代码中D的继承方式就是一个典型的菱形继承。菱形继承的对象内存模型是这样子的:

那么问题就来了,如果需要访问对象D中关于基类的数据,编译器是访问A中继承自A的数据呢?还是访问B中继承自A的数据?
答案是编译器也不知道访问哪个数据,要解决这种二义性问题,除非显式地用类域指明访问的是哪一个直接基类中的数据,eg:
cout<<
菱形继承会带来的两个问题就是:
- 二义性问题
- 数据冗余问题
指明类域是一种解决二义性问题的方法,但是明显这样操作非常的不方便。由于对象内存中存储了多份对于间接基类的数据,这样是非常浪费内存的。为了更好的解决这两个问题,虚继承应运而生。
4.虚拟继承
虚继承是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承导致的问题而出现的。
只要让B和C继承A的时候在继承类型前面加上virtual,那么D对于B和C的继承就会变成虚拟菱形继承。这样D的对象内存模型就会变成这样子:
PS:为了避免没有汇编语言基础的朋友看到这里看的云里雾里,我就不从汇编层面来解释对象模型了,直接介绍和解释这里的概念,如果想要了解汇编层面的虚拟继承,请在评论区指出,我会回复。

从内存模型中我们可以看出,在用虚拟继承来处理菱形继承,派生类D的对象模型只有一份继承自A的数据,并且放到了对象模型的最底层。如果需要通过访问类C/B的子对象来访问A的数据,编译器是怎么知道_a的地址的?所以就有了虚继承表,在每一个直接基类的子对象模型的开头都会存放一个地址,这个地址就指向当前类的虚继承表。虚继承表中就存放了类A的子对象关于当前类的子对象的偏移量。而通过这个偏移量,就可以通过类B/C来访问继承自A中数据了。

本文详细介绍了C++中的继承概念,包括什么是继承、继承的格式、继承权限、派生类默认成员函数以及继承体系下派生类的对象模型。通过实例解析了单继承、多继承、菱形继承以及如何处理继承中的二义性和数据冗余问题,重点讨论了虚继承的作用和内存模型。

被折叠的 条评论
为什么被折叠?



