C++

C++中继承总结

C++中继承总结

Posted by Anriku on April 30, 2018

我们都知道面向对象的三大特点就是继承、多态和封装。今天我们就来看看C++中的继承,当前像继承的基本概念啥的我不会在博客中进行解释,今天只是想对C++的继承需要注意的地方做一个总结。

C++继承的实现语法

//继承语法
class 派生类名:继承方式 基类名1,继承方式 基类名2,...,继承方式 基类名n{
    派生类成员声明;
};

其中派生类也叫做子类,基类也叫做父类。如果学过Java的童鞋,从上面应该可以看到C++中继承派生和Java的最大区别就是C++支持多继承,而Java是不支持的,Java是通过接口来实现多继承的。

访问控制

访问控制就是通过不同的继承方式来是基类中的不同访问权限的成员在派生类中有不同的访问权限权限。这是和Java基础中很大的一个不同之处,Java中的继承没有访问控制,其实Java中的继承就相当于C++中的public继承。下面我们先看一个表然后在进行一一的解释:

基类成员属性 \ 继承方式 public private protected
共有成员 共有 私有 保护
保护成员 保护 私有 保护
私有成员 不可访问 不可访问 不可访问

上面的表格已经把我想说的全部表达出来了。但是下面我们还有来进行的具体的解释吧!

公有继承

当我继承的方式(看到这里还不知道继承方式是什么的,快跑到前面去看看继承语法的模版)为pulic的时候,基类中的公有成员、保护成员的访问权限在子类都是不变的,而私有成员是不可以访问的。

私有继承

当继承方式为private的时候,基类中的公有成员、保护成员的访问权限在子类中都变成private,而私有成员同样是不可访问的

保护继承

当继承方式为protected的时候,基类中的公有成员、保护成员的访问权限在子类中都变成 protected,而私有成员一样是不可访问的

类型兼容规则

这个规则是指在需要基类对象(指针)的任何地方,都可以使用公有(注意是公有)派生类对象(指针)来代替。

一般包括下面的几种情况:

//这里先给出共同的代码
class Base {...}

class Derived : public Base {...}
  • 派生类的对象可以隐含转换为基类对象。下面是具体的代码:
Base base;
Derived derived;
base = derived;
  • 派生对象可以初始化基类的引用。
    • 这里说到了引用我就对引用需要注意的地方进行一下解释。
      • 引用本身是需要分配空间的。它的作用也就是和指针一样用来存储变量的地址的。但是引用所本身所占用的空间是被完全隐藏的(比如说你不能通&引用变量来获取引用的地址)。
      • 虽然引用和指针都是用来存储变量的地址的。但它们是有区别的。最大的区别就是,引用必需是在其声明的时候就对其进行初始化。引用变量不能多次改变所引用的对象。
Derived derived;
Base &base = derived;
  • 派生类的指针(地址)可以隐含转换为基类的指针。
Base *pBase;
Derived *pDerived;
Derived derived;

pDerived = &derived;
pBase = &derived;
//或者
pBase = pDerived;

派生类构造函数和析构函数

在讲派生类构造函数的执行顺序的时候,我们先来说明一下一个普通的类执行的顺序:

  • 先是对这个类的成员对象进行初始化(当然成员对象的初始化又会递归的执行现在讲的这个顺序)
  • 然后才是这个类的对应的构造函数进行执行(对应的构造函数就是你进行对象构造的那个函数)

派生类构造函数执行的次序:

  • 首先调用基类的构造函数。如果是多继承的情况的话,那么按照继承声明的顺序从左到右进行调用父类的构造函数
  • 对派生类中新增的成员对象进行初始化,调用顺序是它们在类中声明的顺序
  • 执行派生类对应构造器的内容

上面讲的不管是普通不含继承的类还是,包含继承的类对于析构函数来说,它的执行顺序是和构造函数的完全相反的

下面结合具体的实例来进行一下理解:

#include <iostream>

using namespace std;

class BaseMember {
public:
    BaseMember() {
        cout << "Constructing BaseMember" << endl;
    }

    ~BaseMember() {
        cout << "Destructing BaseMember" << endl;
    }
};

class Base1 {
public:
    Base1(int i) {
        cout << "Constructing Base1 " << i << endl;
    }

    ~Base1() {
        cout << "Destructing Base1" << endl;
    }

    BaseMember baseMember;
};

class Base2 {
public:
    Base2(int j) {
        cout << "Constructing Base2 " << j << endl;
    }

    ~Base2() {
        cout << "Destructing Base2" << endl;
    }
};

class Derived : public Base2, public Base1 {
public:
    Derived(int a, int b, int c, int d) : Base1(a), member2(d), member1(c), Base2(b) {
        cout << "Constructing Derived" << endl;
    }

    ~Derived() {
        cout << "Destructing Derived" << endl;
    }

private:
    Base1 member1;
    Base2 member2;
};

int main() {
    Derived obj(1, 2, 3, 4);
    return 0;
}

其执行结果如下:

C++构造函数和析构函数的调用

具体我就不进行分析的,请读者自行根据前面所说的顺序进行分析。

作用域分辨符

作用域分辨符是用于限定要访问的成员所在的类。现在看到这句话可能没啥感觉。不过放心,我们在下面进行一一的解释你就能体会到它的作用了。

我们应该很清楚,对于两个或多个嵌套的作用域。如果外层作用域和内层作用域中有同名的标识符,那么在内的作用域中外层的会被自动的屏蔽掉,这叫做隐藏规则。

当然上面的这个规则对类作用域是一样,也有相同的情况。对于类的公有继承(讨论公有继承是因为这会原封不动的继承基类除构造函数、析构函数的所有成员。其它的继承都会改变基类成员的访问权限,没有隐藏的讨论意义)来说有下面两种隐藏:

  • 基类和派生类中有同名的变量,在派生类中会把基类中的变量隐藏掉
  • 在派生类中声明了与基类同名的函数,即使函数的参数列表不同,基类中的一个或多个(重载)这样的函数都会被隐藏掉。

对于上面的变量的隐藏和函数的隐藏我们都可以通作用域分辨符来进行对应类变量和函数的调用。下面我们举个栗子:

#include <iostream>

using namespace std;

class Base{
public:
    int var;

    void fun(){
        cout << "Base " << var <<endl;
    }

    //这个可以暂时不管,到后面讲使用using的时候再来理解
    void fun(int i){
        cout << "Base " << var << endl;
    }
};

class Derived:public Base{
public:
    int var;

    //这个可以暂时不管,到后面讲使用using的时候再来理解
    using Base::fun;

    void fun(){
        cout << "Derived " << var <<endl;
    }
};

int main(){
    Derived derived;
    derived.var = 10;
    derived.fun();
    derived.Base::var = 20;
    derived.Base::fun();
    
    //这个可以暂时不管,到后面讲使用using的时候再来理解
    derived.fun(1);
    return 0;
}

下面是结果:

作用域分辨符

从上面我们可以看到基类的变量和函数都被隐藏了。derived.var和derived.fun()都是派生类的。但是我们使用作用域分辨符就能调用对应的变量和函数。

然后下面是它们的继承关系和Derived类中的成员:

作用域2

前面的代码中看到了暂时不看的using了吗?这里我们就进行讲解。对于函数来说,如果基类中的函数和派生类中的函数是函数名相同但是参数列表不同的情况。我们可以用using关键字让其在派生类中不隐藏。注意一点的就是,即使使用了using,基类与派生类中的同名并且同参的函数还是会被隐藏。

虚基类

我们先由一个栗子来根据问题引出我们要讲的内容:

#include <iostream>

using namespace std;

class Base0{
public:
    int var0;
    void fun0(){
        cout << "Member of Base0 " << var0 <<endl;
    }
};


class Base1: public Base0{
};

class Base2: public Base0{
};


class Derived: public Base1,public Base2{

};


int main(){
    Derived d;
    d.var0 = 10;
    d.fun0();
}

上面这个程序运行时会出问题的,我们来看一下上面的UML图并进行一些讲解:

虚基类

我们可以看到在Derived类中有Base1的var0和fun0还有Base2的var0和fun0,因此直接像上面一样的调用会出问题。那么我们用上面讲的作用域分辨符是能解决问题的。但是其实我们会发现var0和fun0完全没有必要有两个。我们其实只需要一个就行,虚基类就是用来解决这样的情况的

虚基类的作用就是如果将共同的基类(这是时Base0)设置为虚基类的话,那么最终派生类从共同基类下来的不同路径得到的共同基类的成员只有一个副本。

虚基类的语法就是在共同基类的直接派生类的对共同基类的派生的继承方式前面加上virtual。

...

class Base1:virtual public Base0{
    ...
}

class Base2:virtual public Base0{
    ...
}

...

下面是虚基类的UML图:

虚基类

当虚基类中没有默认构造参数的在我们的最终派生类中还要调用虚基类的构造函数才行。代码就行下面一样:

#include <iostream>

using namespace std;

class Base0 {
public:
    int var0;

    Base0(int var0) : var0(var0) {}

    void fun0() {
        cout << "Member of Base0 " << var0 << endl;
    }
};


class Base1 : virtual public Base0 {
public:
    Base1(int var0) : Base0(var0) {}
};

class Base2 : virtual public Base0 {
public:
    Base2(int var0) : Base0(var0) {}
};


class Derived : public Base1, public Base2 {
public:
    //可以看到在最终的派生类中我们调用了Base0的构造函数,不然会报错
    Derived(int var0, int var01) : Base0(var0), Base1(var0), Base2(var01) {}

};


int main() {
    Derived d(1, 2);
    d.fun0();
}

看了上面的代码是否会感觉Base0的构造函数会调用三次的想法。但其实这只会调用一次,C++编译器会对其进行一些处理。

总结

这篇博客我对C++继承相关的主要内容进行了一下总结。主要对访问控制、类型兼容规则、作用域分辨符和虚基类进行了相关的讲解。本来还想对C++继承时的内存布局进行一下讲解的。发现如果要进行讲解的话篇幅可能就太长了,因此就不在这里讲了。想了解的可以自己去找相关的资料进行一下学习!

参考

C++语言程序设计

转载请注明链接