Skip to content

继承和派生

C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。

一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。

派生类中的成员,包含两大部分:

一类是从基类继承过来的,一类是自己增加的成员。

从基类继承过过来的表现其共性,而新增的成员体现了其个性。

派生类定义

派生类定义格式:

cpp
class 派生类名 : 继承方式 基类名{
    //派生类新增的数据成员和成员函数
}

三种继承方式:

  • public : 公有继承
  • private : 私有继承
  • protected : 保护继承

从继承源上分:

  • 单继承:指每个派生类只直接继承了一个基类的特征
  • 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征

继承示例

cpp
#include <iostream>
using namespace std;

//网页类
class IndexPage {
public:
    //网页头部
    void Header() {
        cout << "网页头部!" << endl;
    }

    //网页左侧菜单
    void LeftNavigation() {
        cout << "左侧导航菜单!" << endl;
    }

    //网页主体部分
    void MainBody() {
        cout << "首页网页主题内容!" << endl;
    }

    //网页底部
    void Footer() {
        cout << "网页底部!" << endl;
    }

private:
    string mTitle;//网页标题
};

#if 0
//如果不使用继承,那么定义新闻页类,需要重新写一遍已经有的代码
class NewsPage {
public:
    //网页头部
    void Header() {
        cout << "网页头部!" << endl;
    }
    //网页左侧菜单
    void LeftNavigation() {
        cout << "左侧导航菜单!" << endl;
    }
    //网页主体部分
    void MainBody() {
        cout << "新闻网页主体内容!" << endl;
    }
    //网页底部
    void Footer() {
        cout << "网页底部!" << endl;
    }

private:
    string mTitle;//网页标题
};
void test() {
    NewsPage* newspage = new NewsPage;
    newspage->Header();
    newspage->MainBody();
    newspage->LeftNavigation();
    newspage->Footer();
}

#else
//使用继承,可以复用已有的代码,新闻页除了主体部分不一样,其他都是一样的
class NewsPage : public IndexPage {
public:
    //网页主体部分
    void MainBody() {
        cout << "新闻网页主主体内容!" << endl;
    }
};

void test() {
    NewsPage* newspage = new NewsPage;
    newspage->Header();
    newspage->MainBody();
    newspage->LeftNavigation();
    newspage->Footer();
}
#endif

int main(int argc, char *argv[]) {
    test();

    return 0;
}

程序输出:

shell
网页头部!
新闻网页主主体内容!
左侧导航菜单!
网页底部!

派生类访问控制

派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。

派生类的访问权限规则如下

示例代码
cpp
#include <iostream>
using namespace std;

//基类
class A {
public:
    int mA;

protected:
    int mB;

private:
    int mC;
};

//1. 公有(public)继承
class B : public A {
public:
    void PrintB() {
        cout << mA << endl;//可访问基类public属性
        cout << mB << endl;//可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};

class SubB : public B {
    void PrintSubB() {
        cout << mA << endl;//可访问基类public属性
        cout << mB << endl;//可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};

void test01() {
    B b;
    b.mA = 10;
    cout << b.mA << endl;//可访问基类public属性
    //cout << b.mB << endl; //不可访问基类protected属性
    //cout << b.mC << endl; //不可访问基类private属性
}


//2. 私有(private)继承
class C : private A {
public:
    void PrintC() {
        cout << mA << endl;//可访问基类public属性
        cout << mB << endl;//可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};

class SubC : public C {
    void PrintSubC() {
        //cout << mA << endl; //不可访问基类public属性
        //cout << mB << endl; //不可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};

void test02() {
    C c;
    //cout << c.mA << endl; //不可访问基类public属性
    //cout << c.mB << endl; //不可访问基类protected属性
    //cout << c.mC << endl; //不可访问基类private属性
}


//3. 保护(protected)继承
class D : protected A {
public:
    void PrintD() {
        cout << mA << endl;//可访问基类public属性
        cout << mB << endl;//可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};

class SubD : public D {
    void PrintD() {
        cout << mA << endl;//可访问基类public属性
        cout << mB << endl;//可访问基类protected属性
        //cout << mC << endl; //不可访问基类private属性
    }
};

void test03() {
    D d;
    //cout << d.mA << endl; //不可访问基类public属性
    //cout << d.mB << endl; //不可访问基类protected属性
    //cout << d.mC << endl; //不可访问基类private属性
}


int main(int argc, char *argv[]) {
    test01();
    test02();
    test03();

    return 0;
}

程序输出:

shell
10

默认的继承方式是什么?(也就是说继承的时间没有写访问限定)

要看派生类是用class定义的还是使用struct定义的

  • class定义派生类,默认继承方式就是private
  • struct定义派生类,默认继承方式就是public

继承中的构造和析构

继承中的对象模型

在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成。

示例代码
cpp
#include <iostream>
using namespace std;

class Aclass {
public:
    int mA;
    int mB;
};

class Bclass : public Aclass {
public:
    int mC;
};

class Cclass : public Bclass {
public:
    int mD;
};

void test() {
    cout << "A size:" << sizeof(Aclass) << endl;
    cout << "B size:" << sizeof(Bclass) << endl;
    cout << "C size:" << sizeof(Cclass) << endl;
}

int main(int argc, char *argv[]) {
    test();

    return 0;
}

程序输出:

shell
A size:8
B size:12
C size:16

父类中的私有属性子类继承下去了,但是被编译器隐藏了,访问不到。

可以利用Visual Studio带的开发人员工具查看对象模型

C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\Shortcuts路径下找到开发人员工具

打开工具后进入到代码文件所在目录使用命令cl /d1 reportSingleClassLayoutPerson main.cpp查看

格式为cl /d1 reportSingleClassLayout类名 文件名

对象构造和析构的调用原则

继承中的构造和析构

  • 子类对象在创建时会首先调用父类的构造函数
  • 父类构造函数执行完毕后,才会调用子类的构造函数
  • 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
  • 析构函数调用顺序和构造函数相反

cpp
#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 B {
public:
    C() {
        cout << "C类构造函数!" << endl;
    }
    ~C() {
        cout << "C类析构函数!" << endl;
    }
};
void test() {
    C c;
}

int main(int argc, char *argv[]) {
    test();

    return 0;
}

程序输出:

shell
A类构造函数!
B类构造函数!
C类构造函数!
C类析构函数!
B类析构函数!
A类析构函数!

继承与组合混搭的构造和析构

cpp
#include <iostream>
using namespace std;

class D {
public:
    D() {
        cout << "D类构造函数!" << endl;
    }
    ~D() {
        cout << "D类析构函数!" << endl;
    }
};

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 B {
public:
    C() {
        cout << "C类构造函数!" << endl;
    }
    ~C() {
        cout << "C类析构函数!" << endl;
    }

public:
    D c;
};

void test() {
    C c;
}

int main(int argc, char *argv[]) {
    test();

    return 0;
}

程序输出:

shell
A类构造函数!
B类构造函数!
D类构造函数!
C类构造函数!
C类析构函数!
D类析构函数!
B类析构函数!
A类析构函数!

总结

  • 先调用父类构造,然后调用其他成员构造,最后调用自身构造;析构顺序与构造顺序相反
  • 可以利用初始化列表,显式的调用父类中的有参构造
  • 父类中的 默认构造、拷贝构造、析构函数、operator= 这四个函数(编译器默认提供的)不会被子类继承

继承中同名成员的处理方法

  • 当子类成员和父类成员同名时,子类依然从父类继承同名成员
  • 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
  • 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显式使用类名限定符)
示例代码
cpp
#include <iostream>
using namespace std;

class Base {
public:
    Base() : mParam(0) {}
    void Print() { cout << mParam << endl; }

public:
    int mParam;
};

class Derived : public Base {
public:
    Derived() : mParam(10) {}
    void Print() {
        //在派生类中使用和基类的同名成员,显式使用类名限定符
        cout << Base::mParam << endl;
        cout << mParam << endl;
    }
    //返回基类重名成员
    int& getBaseParam() { return Base::mParam; }

public:
    int mParam;
};

int main(int argc, char *argv[]) {
    Derived derived;

    //派生类和基类成员属性重名,子类访问成员默认是子类成员
    cout << derived.mParam << endl;//10
    derived.Print();

    //类外如何获得基类重名成员属性
    derived.getBaseParam() = 100;
    cout << "Base:mParam:" << derived.getBaseParam() << endl;

    return 0;
}

程序输出:

shell
10
0
10
Base:mParam:100
注意: 如果重新定义了基类中的重载函数,将会发生什么?
cpp
#include <iostream>
using namespace std;

class Base {
public:
    void func1() {
        cout << "Base::void func1()" << endl;
    };
    void func1(int param) {
        cout << "Base::void func1(int param)" << endl;
    }
    void myfunc() {
        cout << "Base::void myfunc()" << endl;
    }
};

class Derived1 : public Base {
public:
    void myfunc() {
        cout << "Derived1::void myfunc()" << endl;
    }
};

class Derived2 : public Base {
public:
    //改变成员函数的参数列表
    void func1(int param1, int param2) {
        cout << "Derived2::void func1(int param1,int param2)" << endl;
    };
};

class Derived3 : public Base {
public:
    //改变成员函数的返回值
    int func1(int param) {
        cout << "Derived3::int func1(int param)" << endl;
        return 0;
    }
};

int main(int argc, char *argv[]) {
    Derived1 derived1;
    derived1.func1();
    derived1.func1(20);
    derived1.myfunc();
    cout << "-------------" << endl;

    Derived2 derived2;
    //derived2.func1();  //func1被隐藏
    //derived2.func1(20); //func2被隐藏
    derived2.func1(10, 20);//重载func1之后,基类的函数被隐藏
    derived2.myfunc();
    cout << "-------------" << endl;

    Derived3 derived3;
    //derived3.func1();  没有重新定义的重载版本被隐藏
    derived3.func1(20);
    derived3.myfunc();

    return 0;
}

程序输出:

shell
Base::void func1()
Base::void func1(int param)
Derived1::void myfunc()
-------------
Derived2::void func1(int param1,int param2)
Base::void myfunc()
-------------
Derived3::int func1(int param)
Base::void myfunc()
  • Derive1 了Base类的myfunc函数,derive1可访问func1及其重载版本的函数
  • Derive2 了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问
  • Derive3 了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏

非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建

另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。

在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

继承中的静态成员特性

静态成员函数和非静态成员函数的共同点:

  1. 他们都可以被继承到派生类中
  2. 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏
  3. 如果我们在派生类中改变基类中某一个函数的特征,所有使用该函数名的基类版本都会被隐藏
示例代码
cpp
#include <iostream>
using namespace std;

class Base {
public:
    static int getNum() { return sNum; }
    static int getNum(int param) {
        return sNum + param;
    }

public:
    static int sNum;
};

int Base::sNum = 10;

class Derived : public Base {
public:
    static int sNum;//基类静态成员属性将被隐藏

#if 0
    //重定义一个函数,基类中重载的函数被隐藏
    static int getNum(int param1, int param2) {
        return sNum + param1 + param2;
    }

#else
    //改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
    static void getNum(int param1, int param2) {
        cout << sNum + param1 + param2 << endl;
    }
#endif
};

int Derived::sNum = 20;

int main(int argc, char *argv[]) {

    return 0;
}

多继承

多继承概念

我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。

示例代码
cpp
#include <iostream>
using namespace std;

class Base1 {
public:
    void func1() { cout << "Base1::func1" << endl; }
};

class Base2 {
public:
    void func1() { cout << "Base2::func1" << endl; }
    void func2() { cout << "Base2::func2" << endl; }
};

//派生类继承Base1、Base2
class Derived : public Base1, public Base2 {};

int main(int argc, char *argv[]) {
    Derived derived;
    //func1是从Base1继承来的还是从Base2继承来的?
    //derived.func1();
    derived.func2();

    //解决歧义:显示指定调用那个基类的func1
    derived.Base1::func1();
    derived.Base2::func1();

    return 0;
}

程序输出:

shell
Base2::func2
Base1::func1
Base2::func1

多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?

解决方法就是显式指定调用那个基类的版本(加上作用域)。

菱形继承和虚继承

两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。

这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
  2. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
示例代码
cpp
#include <iostream>
using namespace std;

class BigBase {
public:
    BigBase() { mParam = 0; }
    void func() { cout << "BigBase::func" << endl; }

public:
    int mParam;
};

class Base1 : public BigBase {};
class Base2 : public BigBase {};
class Derived : public Base1, public Base2 {};

int main(int argc, char *argv[]) {
    Derived derived;

    //1. 对“func”的访问不明确
    //derived.func();
    //cout << derived.mParam << endl;
    cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
    cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;

    //2. 重复继承
    cout << "Derived size:" << sizeof(Derived) << endl;//8

    return 0;
}

程序输出:

shell
derived.Base1::mParam:0
derived.Base2::mParam:0
Derived size:8

上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?

对于这种菱形继承所带来的两个问题,C++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:

点击查看代码
cpp
#include <iostream>
using namespace std;

class BigBase {
public:
    BigBase() { mParam = 0; }
    void func() { cout << "BigBase::func" << endl; }

public:
    int mParam;
};

class Base1 : virtual public BigBase {};
class Base2 : virtual public BigBase {};
class Derived : public Base1, public Base2 {};

int main(int argc, char *argv[]) {
    Derived derived;

    //二义性问题解决
    derived.func();
    cout << derived.mParam << endl;
    //输出结果:12
    cout << "Derived size:" << sizeof(Derived) << endl;

    return 0;
}

程序输出:

shell
BigBase::func
0
Derived size:24

以上程序Base1 ,Base2采用继承BigBase,那么BigBase被称为虚基类。通过虚继承解决了菱形继承所带来的二义性问题。

但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?

虚继承实现原理

cpp
#include <iostream>
using namespace std;

class BigBase {
public:
    BigBase() { mParam = 0; }
    void func() { cout << "BigBase::func" << endl; }

public:
    int mParam;
};

#if 0
//虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};

#else
//普通继承
class Base1 : public BigBase {};
class Base2 : public BigBase {};
#endif

class Derived : public Base1, public Base2 {};

int main(int argc, char *argv[]) {

    return 0;
}

通过内存图(对象布局图),我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。

  • BigBase 菱形最顶层的类,内存布局图没有发生改变。
  • Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),
  • Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。

由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 Derived三个类对象共享了一份BigBase数据。

当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用

代码示例
cpp
#include <iostream>
using namespace std;

class BigBase {
public:
    BigBase(int x) { mParam = x; }
    void func() { cout << "BigBase::func" << endl; }

public:
    int mParam;
};

class Base1 : virtual public BigBase {
public:
    Base1() : BigBase(10) {}//不调用BigBase构造
};

class Base2 : virtual public BigBase {
public:
    Base2() : BigBase(20) {}//不调用BigBase构造
};

class Derived : public Base1, public Base2 {
public:
    Derived() : BigBase(100) {}//调用BigBase构造
};
//每一次继承子类中都必须书写初始化语句

int main(int argc, char *argv[]) {
    Derived derived;

    return 0;
}

注意

虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的。

工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。

输入输出流(iostream)的作者Jerry Schwarz,曾在个别场合表示如何他重新设计iostream的话,很可能从iostream中去除多重继承。