1.什么是继承?
在编程中,人们总喜欢把需要经常使用的,具有相同逻辑的代码提取出来,放在一个公共的代码块内,以达到代码简洁的效果;比如我们经常需要用到的一些交换,排序逻辑的代码,就可以提取出来,放在一个函数中,需要使用的时候,只需要调用这个函数即可,即使在程序中需要大量使用,也只需要使用一条调用语句即可,大大简化的代码的编写工作。这就是代码复用的好处。
继承其实也是利用了代码复用的思想。我们都知道C++是一门面向对象的编程语言,面向对象的语言必须要提供能够描述对象的能力,这种能力就是类 ,简单来说就是,类可以用来描述对象;那,不同的类之间会不会具有某些相似性呢?其实是有的,比如不同种类的狗狗,有泰迪犬、拉布拉多犬、还有我们的中华田园犬等等......那么在描述这些对象的时候,他们的类中肯定有相同的部分,那,我们就可以这些类中相同的部分提取出来放在一个公共的类当中,这个类就是父类(也叫基类);当我们需要描述其他狗狗时,我们就可以用继承它的父类,通过继承,该子类(也叫派生类)直接就能获得父类中的成员变量和成员方法 ,所以,可以看出,继承确实是利用了代码复用的思想,是类设计层次的代码复用;正是因为有了继承,我们还能在原有类的基础上进行拓展。
2.C++中如何使用继承?
C++中通过 " : " 表明该类需要继承哪个类,使用继承的方式如以下代码:
class A
{};
class B : public A // B继承了A
{}
我们都知道,父类和子类中的成员变量和成员方法都是受到不同的访问方式的限制的,那么子类继承之后,从父类中继承而来的那些成员的访问方式是怎么样的呢?子类中继承自父类的成员的访问权限变化如下表:
|----------------|----------------|----------------|--------------|
| 访问权限/继承方式 | public继承 | protected继承 | private继承 |
| 父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
| 父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
| 父类的private成员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可见 |
[子类中继承自父类的成员访问方式的变化]
- 从表中可以看出,父类的私有成员在子类中不可见;意思就是,父类的私有成员也会被子类继承,但是在子类中不可访问,这也是访问限定符private的意义。
- 对于其他的可以总结为,子类中继承自父类的成员的访问权限 = min(访问权限,继承方式);
- 继承方式可以省略;使用class定义类时,默认的访问方式是private,使用struct定义类时,默认的访问方式是public。(但是不建议省略)
3.父类和子类之间的转化
当有了继承之后,也就有了父类和子类,我们都知道,不同的类型之间相互赋值时,会产生一个具有常性的临时变量,那父类对象和子类对象之间的转换是否也是这样呢?非也非也!
子类转父类(向上转换):我们知道,子类会继承父类中的成员,所以子类对象天生就具有父类中的成员。
- 子类对象转父类对象,子类对象中的成员 >= 父类中的成员的,所以要想把子类对象交给一个父类对象,需要把子类中父类的那一部分切割出来,交给父类对象。
- 父类的引用去引用子类,同理,父类的引用去引用子类,实际上引用的是子类中父类的那一部分。
- 子类的指针转父类的指针,指针的类型决定了其解引用之后的步长,子类的指针转成父类的指针之后,该指针只能访问子类中父类的那一部分。
父类转子类(向下转换):
- 父类的对象不能转换成子类的对象。父类中本来就缺少子类中独有的成员,所以不能进行转换。
- 父类的指针转换子类的指针,父类的指针既可以指向父类,又可以指向子类;如果父类的指针原来是指向子类的,是可以转换成子类的指针;如果父类的指针原来是指向父类的,是不可以转换成子类的指针,因为,解引用之后,会看到不属于它的那一部分。这个转换是不安全的。
- 子类的引用去引用父类,这个过程是不安全的,道理同上。
- 所以父类的指针or引用需要向下转换时,可以使用dynamic_cast操作符来进行,这个操作符内部会自己判断转换能否发生,是安全的。
4.继承中的隐藏关系
子类继承父类,会继承父类中的成员变量和成员函数,也可以去定义自己的成员变量和成员函数,如果子类中定义了和父类中名字相同的成员变量和成员函数会发生什么呢?看一下下面这段简单的程序,输出的结果为:B:func()" 2;
class A
{
public:
int func(){cout << "A:func()" << _a;}
protected:
int _a = 1;
}
class B : public A
{
public:
int func(){cout << "B:func()" << _a;}
private:
int _a = 2;
}
int main()
{
B b;
b.func();
return 0;
}
- 这是因为子类中会隐藏父类中的同名成员,如果不隐藏的话,调用的时候,可能会出现歧义。
- 这种情况也叫重定义。
5.子类中的默认成员函数
子类继承父类,会继承父类中的成员变量和成员方法,那么子类中如何操作继承自父类的成员呢?
- 子类中继承自父类的成员的初始化:子类的默认构造函数会去调用父类的默认构造函数,完成子类中父类的那一部分的初始化。如果基类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
- 子类中继承自父类的成员的拷贝构造:子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。拷贝构造也是构造,也需要在初始化列表显示调用。
- 子类中继承自父类的成员的赋值:子类的operator=必须要调用父类的operator=完成父类的赋值。在子类的operator=()中调用父类的operator=()需要指定类域,否则就会栈溢出。
- 子类中继承自父类的成员的析构:子类的析构函数会在被调用完成后自动调用父类的析构函数清理基类成员 。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。(因为多态的缘故,析构函数的函数名被统一处理成destrutor(),所以父子类的析构函数天然就构成隐藏)
总体来说,子类中的成员变量可以划分为三类,子类中独有的内置类型的成员变量,子类中独有的自定义类型的成员变量,子类中继承自父类的成员变量;所有的操作中,对于继承自父类的成员变量,都要调用父类中相应的函数完成对应的操作;形象来说就是,父亲和儿子,自己的事情自己做。
6.继承中两个特殊的成员
a.友元关系不能继承;可以形象的理解成,父亲的朋友不是儿子的朋友。
b.父类中的定义静态成员,在整个继承体系中只有一个这样的成员;因为静态成员并不是存储在对象中的,而是存储在静态区上的,所以,所有对象只能看到这一份静态成员。
7.复杂的菱形继承
C++中的继承可以分为单继承和多继承,单继承就是指一个类只有一个父类,多继承就是指一个类不止一个父类,菱形继承是一种特殊的多继承。几种继承如下图所示:
菱形继承的问题
分析菱形继承可知,D类中的成员在A类中存有一份,在B类中存有一份,当C类继承A和B的时候,就会继承两份D类中的成员,这个时候就造成数据冗余问题 ,当我们使用C类的对象访问A类的成员的时候,编译器不知道是要调用A类中的D成员还是B类中的D成员,这个时候,还会造成二义性问题。 所以,实践中,我们尽量不要定义菱形继承。
**解决菱形继承的方法:**可以使用virtual关键字解决菱形继承的缺陷。如以下代码
class A
{}
class B : virtual public A
{}
class c : virtual public A
{}
class d : public B,public C
{}
解决菱形继承的原理
请看下面图片:
分析上面图片可知,加了virtual关键字修饰之后,冗余的A被存放在了对象的最下面,B和C中原来存A的地方,现在存的是两个不同的地址(这个地址也叫虚基表指针),这两个指针分别指向了一张表(这张表叫做虚基表),这张表中存放的是该对象模型中,B or C相较于A的偏移量;也就是说,公共的A只存了一份,可以通过虚基表中的偏移量找到公共的A;这就是virtual解决菱形继承的大致原理。