十四、继承与组合(Inheritance & Composition)

十四、继承与组合(Inheritance & Composition)

引言

  • C++最引人注目的特性之一是代码复用。
  • 组合:在新类中创建已有类的对象。
  • 继承:将新类作为已有类的一个类型来创建。

14.1 组合的语法

Useful.h

cpp 复制代码
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{
    int i;
public:
    X(){i = 0;}
    void set(int ii){i = ii;}
    int read() const {return i;}
    int permute(){return i = i * 47;}
};
#endif

Composition,cpp

cpp 复制代码
#include "Useful.h"
class Y{
	int i;
public:
    X x;//嵌入对象,子对象
    Y(){ i = 0;}
    void f(int ii) {i = ii;}
    int g() cosnt{return i;}
};

void main()
{
    Y y;
    y.f(47);
    y.x.set(37);
}

这里Y y;语句执行的时候,y 里面的x 是利用构造函数进行初始化的

14.2 继承的语法

Useful.h

cpp 复制代码
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{
    int i;
public:
    X (){i = 1;}
    void set(int ii){i = ii;}
    int read() const{return i;}
    int permute() {return i = i*47;}
};
#endif

Inheritance.cpp

cpp 复制代码
//C14:Inheritance.cpp
#include "Useful.h"
#include <iostream>
using namespace std;
class Y:public X
{
    int i;//不是X的i
public:
    Y(){i = 2;}
    int change()
    {	
        i = permute();//调用不同名称的函数
        return i;
    }
    void set(int ii){
        i = ii;
        X::set(ii);//调用同名函数,需要加::
    }
};

int main(){
    cout << "sizeof(X) = " << sizeof(X) << endl;
    cout << "sizeof(Y) = " << sizeof(Y) << endl;
    Y D;//X::i = 1,Y::i = 2
    D.change();//return Y::i = 47(X::i = 47)
    D.read();	//X::i = 47
    D.permute();//X::i = 47 * 47
    D.set(15);//Y::i = 12,X::i = 12
    return 0;
}

这里对y里面的x初始化也是通过X的无形参构造函数。

输出

cpp 复制代码
sizeof(X) = 4
sizeof(Y) = 8
  • Y 继承自 X ,这意味着 Y 内将包含一个 X 类型的子对象,就像在 Y 内部直接创建了一个 X 成员对象一样。无论是成员对象还是基类所占的存储都称为子对象。

  • YX 的派生类,X基类。派生类继承基类的属性,这种关系称为继承。

  • X 的所有私有成员在 Y 中仍然是私有的(因此 Y 里面不能访问 X 的私有成员,只能通过 X 的函数)。通过 public 继承,基类的所有公有成员 在派生类也保持公有(后面还会有 private 继承、 protected 继承),也就是说 public 继承,X 中的私有在 Y 中仍私有,公有仍公有,protectedprotectedprotected

    是指派生类可以访问,外部代码不可以访问。

  • 将一个类用作基类 相当于声明了一个该类的(未命名)对象。因此。必须先定义这个类才能将其用作基类。

    cpp 复制代码
    class X;
    class Y:public X{
    	............    
    };

14.3 构造函数初始化列表

  • 在组合和继承中,确保**子对象被正确初始化**非常重要。
  • 构造函数和析构函数不会被继承 (赋值运算符也不会被继承)。因此派生类的构造函数无法直接初始化基类的成员。
  • 新类的构造函数无法访问子对象的私有数据元素。
  • 如果不使用默认构造函数,该如何初始化子对象的私有数据元素。

解决方法

  • 构造函数初始化列表中调用子对象的构造函数。
  • 构造函数初始化列表允许显式调用成员对象的构造函数。其原理是:在进入新类构造函数的函数体之前,所有成员对象的构造函数都会被调用。
  • 内置类型的变量也可以在构造函数初始化列表 中初始化。而且初始化列表会自动帮忙初始化(避免垃圾值)。

注意 :在非虚继承(普通继承) 中,派生类只需要构造它的直接基类 ,间接基类会自动由中间类网上构造,构造链自动完成。

示例1

cpp 复制代码
#include <iostream>
using namespace std;
class X{
    int a;
public:
    X(int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y{
    int b;
public:
    Y(int i,int j):b(i),x(j){cout << "Constructor Y:" << b << endl;}
    X x;
};

int main(){
    Y y(1,2);
    return 0;
}

该实例中 xy 的成员

输出

cpp 复制代码
Constructor X:2
Constructor Y:1

示例2

cpp 复制代码
#include <iostream>
using namespace std;
class X {
    int a;
public:
    X(int i = 7) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y {
    int b;
public:
    Y(int i) :b(i) { cout << "Constructor Y:" << b << endl; }
    X x;
};

int main() {
    Y y(1);
    return 0;
}

该实例中 xy 的成员

输出

cpp 复制代码
Constructor X:7
Constructor Y:1

示例3

cpp 复制代码
#include <iostream>
using namespace std;
class X{
    int a;
public:
    X (int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X
{
    int b;
public:
    Y(int i,int j):b(i),X(j){cout << "Constructor Y:" << b << endl;}
}

int main(){
    Y y(1,2);
    return 0;
}

输出

cpp 复制代码
Constructor X:2
Constructor Y:1

示例4

cpp 复制代码
#include <iostream>
using namespace std;
class X {
    int a;
public:
    X(int i = 9) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y :public X
{
    int b;
public:
    Y(int i, int j) :b(i){ cout << "Constructor Y:" << b << endl; }
};

int main() {
    Y y(1, 2);
    return 0;
}

输出

cpp 复制代码
Constructor X:9
Constructor Y:1

14.4 组成与继承结合

  • 当创建一个派生类对象 时,可能会创建以下对象:基类对象、成员对象和派生类对象本身。构造顺序自下而上:首先构造基类、然后构造成员对象,最后构造派生类自身。
  • 基类或成员对象构造函数的调用顺序以它们在派生类中声明的顺序为准,而不是它们在初始化列表中出现的顺序。
  • 默认构造函数可以被隐式调用。

示例

cpp 复制代码
#include <iostream>
using namespace std;
class X{
    int a;
public:
    X(int i = 0):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X{
    int b;
    X x1,x2;
public:
    Y(int i,int j,int m,int n):b(i),x2(j),x1(m),X(n){cout << "Constructor Y:" << b << endl;}
};
int main(){
    Y y(1,2,3,4);
    return 0;
}

输出

cpp 复制代码
Constructor X:4
Constructor X:3
Constructor X:2
Constructor Y:1

14.5 名字隐藏

​ 先介绍下面三种相关机制

  • 重载(Overload):发生在同一作用域(类)内
  • 重新定义(Redefining):继承关系中的普通成员函数
  • 重写(Overriding):继承关系中的虚成员函数

​ 名字隐藏:在继承关系中 ,如果派生类定义了一个与基类中同名的成员(不论是函数还是变量),那么这个基类的同名成员(函数)就会被"隐藏",即使参数不同也无法通过派生类对象直接访问。上面三种机制中,重载并非是名字隐藏,其余两个是名字隐藏。

概念 英文术语 适用情形 是否是名字隐藏 说明
重载 Overload 同一个类或者同一作用域 函数名相同,但参数不同(返回值不同不算重载啊)
重新定义 Redifining 继承中的普通函数 派生类中定义了同名函数(或变量),会隐藏基类中所有同名函数(变量),需要显示访问
重写 Overriding 继承中的虚函数 派生类用virtual修饰的函数覆盖基类中的虚函数

虚函数会在后续的章节继续提到

  • 在派生类中,只要重新定义了基类中重载的函数名 (假设基类有重载函数),基类中 该名字的其他版本派生类中都会被自动隐藏。

示例

cpp 复制代码
//C14:NameHidding.cpp
#include <iostream>
#include <string>
using namespace std;

class Base{
public:
    int f() const
    {
        cout << "Base::f()" << endl;
        return 1;
    }
    int f(string) const {return 1;}
    void g(){}
};

class Derived1:public Base
{
public:
    //重新定义
    void g() const{};
};

class Derived2:public Base{
public:
    //重定义
    int f() const {
        cout << "Derived2::f()" << endl;
        return 2;
    }
};

class Derived3:public Base{
public:
    //改变return的类型
    void f() const{
        cout << "Derived3::f()" << endl;
    }
};

class Derived4:public Base{
public:
    //改变return的类型
    int f(int) const{
        cout << "Derived4::f()" << endl;
        return 4;
    }
};

void main(){
    string s("hello");
    Derived1 d1;
    int x = d1.f();
    d1.f(s);
    Derived2 d2;
    x = d2.f();
    //!d2.f(s);//string版本被隐藏
    Derived3 d3;
    //!x = d3.f();return int 版本被隐藏
    Derived4 d4;
    //!x = d4.f();f()版本被隐藏
    x = d4.f(1);
}

输出

cpp 复制代码
Base::f()
Derived2::f()
Derived4::f()

14.6 不会自动继承的函数

以下函数不会被自动继承

  • 构造函数
  • 析构函数
  • 赋值运算符函数

继承和静态成员函数(Inheritance and static member funcitons)

  • 静态成员函数可以被继承到派生类中。
  • 如果在派生类中重新定义了一个静态成员函数 ,那么基类中所有同名的重载函数也会被隐藏
  • 静态成员函数只能访问静态数据成员
  • 静态成员函数不能是是virtual (虚函数)
  • 静态成员函数没有this 指针

14.7 选择组合还是继承

  • 共同点:组合(Composition)和继承(Inheritance)都会在新类中放置子对象(subojects) 。它们都使用构造函数初始化列表(intializer list) 来构造这些子对象。
  • 当我们希望在新类中包含某个已有类的功能 (作文数据成员),但不想继承它的接口 时,通常使用组合
  • 当我们希望新类具有与现有类完全相同的接口 时,使用继承 。这被称为子类型化

14.8 基类的子类型化

  • 如果一个派生类继承自一个 public 的基类 ,那么该派生类就继承了基类的所有成员 ,但只能访问基类中的 publicprotected 成员 。派生类就是基类的子类型 ,基类是派生类的超类型

    外部使用者的角度 来看,这个派生类就具有与基类相同的 public 接口 (可以再加自己的新接口),因此它可以在需要基类对象的地方替代使用,这就是**子类型(subtyping)**的概念。

  • 通过子类型化 ,当我们使用指针或引用 操作派生类对象时,它可以被当作基类对象来处理(即:可以指向或引用基类)

    cpp 复制代码
    #include <iostream>
    using namespace std;
    class Base {
    public:
        void speak() { cout << "Base speaking" << endl; }
    };
    
    class Derived : public Base {
    public:
        void shout() { cout << "Derived shouting" << endl; }
    };
    
    int main() {
        Derived d;
    
        // 子类型化:Base* 指向 Derived 对象
        Base* ptr = &d;
        ptr->speak();   // OK:Base 的函数可调用
        // ptr->shout(); // 错误:Base* 看不到 Derived 的接口
    
        // 同样适用于引用
        Base& ref = d;
        ref.speak();    // OK
    }

    这在后续还会提到。

14.9 继承的访问控制

  • 访问说明符:
    • public
    • private
    • protected
  • 访问说明符 用于控制派生类对基类的成员的访问 ,以及从派生类到基类的指针和引用转换的权限。(无论哪种访问,基类的所有成员都会被继承,派生类的内存里都会有它们,只是能不能访问的差别。)

下面展示三种访问控制的不同

public 继承

如果一个派生类使用public 继承:

  • 派生类的成员函数及其类内部可以访问其基类中的 publicprotected 成员。private不可以访问。
  • 派生类的对象可以访问其基类中的 public 成员。

说明:

  • public 继承会让基类的public 成员在派上类里面仍是publicprotected成员仍是protected
  • 可以形成"子类型关系 ",即能用 Base* 指向 Derived

示例

cpp 复制代码
#include <iostream>
using namespace std;
class employee {
public:		void print() {}
protected:	short number;
private:	string name;
};

class manager :public employee {
public:
    void meeting(int n) {
        print();	//ok
        number = n;//ok
        //name = "Jhoo" //error
    }
private:
    short level;
};
int main() {
    employee E;
    manager M;
    E.print();//ok
    //E.number = 3; //error
    //M.name = "Jhoo"; //error
    //M.number = 3;	//error
    M.print();	//ok
    M.meeting(1);	//ok
    return 0;
}

可见protectedprivate 的区别就是:

  • 派上类里面可以访问protected ,而不可以访问private,而派生类的对象两个都不可以访问

private 继承

如果一个类使用private 说明符继承:

  • 派生类的成员函数及其类内部可以访问其基类中的 publicprotected 成员。private不可以访问。
  • 派生类的对象不能访问基类中的任何成员

说明:

  • private 继承会把基类的publicprotected 成员变成派生类中的private 成员。
  • 所以 只有派生类内部能访问 基类的publicprotected 成员,类外(包括派生类对象)都不能访问。
  • 同时,不会形成"子类型关系 ",即不能Base* 指向 Derived

示例

cpp 复制代码
#include <iostream>
using namespace std;
class employee {
public:		void print() {}
protected:	short number;
private:	string name;
};

class manager :private employee {
public:
    void meeting(int n) {
        print();	//ok
        number = n;//ok
        //name = "Jhoo" //error
    }
private:
    short level;
};
int main() {
    employee E;
    manager M;
    E.print();//ok
    //E.number = 3; //error
    //M.name = "Jhoo"; //error
    //M.number = 3;	//error
    //M.print();	//error
    M.meeting(1);	//ok
    return 0;
}

protected 继承

如果一个类使用protected 说明符进行继承:

  • 派生类的成员函数及其类内部可以访问其基类中的 publicprotected 成员。private不可以访问。
  • 派生类的对象不能访问基类中的任何成员

说明

  • protected 继承会将基类的 publicprotected 成员变成派生类中的 protected
  • 所以,只有派生类的内部能用,类外部(包括对象)都无法访问;
  • 同时不形成"子类型关系 ",不能用 Base* 指向 Derived

protected继承和private继承当派生类再次被继承时,会体现出来差别

总结

三种继承方式对比
方面 public继承 protected继承 private继承
基类的public成员在派生类中变成 public protected private
基类的proitected成员在派生类中变成 protected protected ptivate
基类的private成员在派生类中变成 不可访问 不可访问 不可访问
派生类成员函数能访问哪些基类成员 publicprotected publicprotected publicprotected
派生类对象能访问哪些基类成员 public 无法访问任何成员 无法访问热河成员
是否支持子类型转换(Base* = new Derived 不支持 不支持
适用场景 接口继承、支持多态、面向对象设计 实现复用,不暴露接口 强封装
类中成员访问权限对比
访问标识符 类中是否可以访问 派生类是否可以访问 类外部(对象)是否可以访问 是否可以继承
public 可以 可以 可以 可以
protected 可以 可以 不可以 可以
private 可以 不可以 不可以 可以(但不可见)

示例

Example1
cpp 复制代码
#include <iostream>
using namespace std;
class Location {
public:
    void InitL(int xx, int yy) {
        X = xx;
        Y = yy;
    }
    void Move(int xOff, int yOff) {
        X += xOff;
        Y += yOff;
    }
    int GetX() { return X; }
    int GetY() { return Y; }
private:
    int X, Y;
};
class Rectangle :public Location {
public:
    void InitR(int x, int y, int w, int h);
    int GetH() { return H; }
    int GetW() { return W; }
private:
    int W, H;
};

void Rectangle::InitR(int x, int y, int h, int w)
{
    InitL(x, y);
    W = w;
    H = h;
}

int main() {
    Rectangle rect;//派生类的对象
    rect.InitR(2, 3, 20, 10);
    rect.Move(3, 2);
    cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;
    return 0;
}

输出

cpp 复制代码
5,5,20,10
Example2

这里在Example1 里面添加一个V 类。

cpp 复制代码
class Location {
public:
    void InitL(int xx, int yy) {
        X = xx;
        Y = yy;
    }
    void Move(int xOff, int yOff) {
        X += xOff;
        Y += yOff;
    }
    int GetX() { return X; }
    int GetY() { return Y; }
private:
    int X, Y;
};
class Rectangle :public Location {
public:
    void InitR(int x, int y, int w, int h);
    int GetH() { return H; }
    int GetW() { return W; }
private:
    int W, H;
};

void Rectangle::InitR(int x, int y, int h, int w)
{
    InitL(x, y);
    W = w;
    H = h;
}
// 派生类
class V : public Rectangle {
public:
    void Function() {
        Move(3, 2);  // 来自基类的函数
    }
};
  • 如果这里的clas V :public Rectangle继承改为private继承,那么Move(3,2)还能用吗?

    解答:

    • 如果 Rectangle 是以 publicprotected 方式继承 Location
      那么类 V 无论使用哪种继承方式(public / protected / private),都可以访问 Move() 函数
    • 如果 Rectangle 是以 private 方式继承 Location
      那么类 V 无论如何继承 Rectangle,都无法访问 Move() 函数
    • 因为:
      • publicprotected 继承会让基类的 public / protected 成员保留可见性(对类内仍可访问); - 而 private 继承会将基类的 public / protected 成员都变为 private,对子类完全不可见。
Example3

这里将Example1public继承改为private继承

cpp 复制代码
#include <iostream>
using namespace std;
class Location {
public:
    void InitL(int xx, int yy) {
        X = xx;
        Y = yy;
    }
    void Move(int xOff, int yOff) {
        X += xOff;
        Y += yOff;
    }
    int GetX() { return X; }
    int GetY() { return Y; }
private:
    int X, Y;
};
class Rectangle :private Location {
public:
    void InitR(int x, int y, int w, int h);
    int GetH() { return H; }
    int GetW() { return W; }
private:
    int W, H;
};

void Rectangle::InitR(int x, int y, int h, int w)
{
    InitL(x, y);	//OK
    W = w;
    H = h;
}

那么Example1里面的主函数需要修改

cpp 复制代码
int main() {
    Rectangle rect;//派生类的对象
    rect.InitR(2, 3, 20, 10);
    rect.Move(3, 2);
    cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;
    return 0;
}

rect.GetX()rect.GetY() 以及rect.Move(3,2) 会报错,因为它们在Rectangle里面已经是private。所以Rectangle对象无法访问它们。

所以我们将Exampel1修改成如下

cpp 复制代码
#include <iostream>
using namespace std;
class Location {
public:
    void InitL(int xx, int yy) {
        X = xx;
        Y = yy;
    }
    void Move(int xOff, int yOff) {
        X += xOff;
        Y += yOff;
    }
    int GetX() { return X; }
    int GetY() { return Y; }
private:
    int X, Y;
};
class Rectangle :private Location {
public:
    void InitR(int x, int y, int w, int h);
    void Move(int xOff, int yOff) {
        Location::Move(xOff,yOff);
    }
    int GetX() {return Location::GetX();}
    int GetY() {return Location::GetY();}
    int GetH() { return H; }
    int GetW() { return W; }
private:
    int W, H;
};

void Rectangle::InitR(int x, int y, int h, int w)
{
    InitL(x, y);	//OK
    W = w;
    H = h;
}

int main() {
    Rectangle rect;//派生类的对象
    rect.InitR(2, 3, 20, 10);
    rect.Move(3, 2);
    cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;
    return 0;
}

这里在 Rectangle里面重新定义Move()GetX()GetY()

输出

cpp 复制代码
5,5,20,10
Example4
cpp 复制代码
class Base {
public: 	void f1() {}
protected:	void f3() {}
};
class Derived1 : protected Base {};

class Derived2 : public Derived1 {
    public:
        void fun() {
            f1();	//ok
            f3();	//ok
        }
};
int main() {
    Derived1 d;
    //d.f1();	//error
    //d.f3();	//error
    return 0;
}

这里如果将class Derived2 : public Derived1 改为private或者protected,那么

cpp 复制代码
void fun() {
	f1();	//ok
	f3();	//ok
}

正确吗?

是正确的,因为,private或者protected仅仅是改变了Derived1的成员在Derived1里面是什么访问权限(public / protectecd / private),而基类的publicprotected 成员在派生类的内部是都可以访问的。

14.10 运算符重载与继承

  • 除了赋值运算符(=)之外,其他运算符会自动被继承到派生类中。

14.11 多重继承

  • 多重继承是指:一个派生类可以拥有多个直接基类
cpp 复制代码
class A{
    //......
}
class B{
    //......
}
class C:access A,access B{};

"access"是占位词,代表publicprotectedprivate 中任意的一种访问方式。

复制代码
Base classes:      A       B
                    ↖     ↗
                     C  ← Derived class

示例

cpp 复制代码
#include <iostream>
using namespace std;
class B1 {
public:
    B1(int i) {
        b1 = i;
        cout << "Constructor B1:" << b1 << endl;
    }
    void Print() { cout << b1 << endl; }
private:
    int b1;
};

class B2 {
public:
    B2(int i)
    {
        b2 = i;
        cout << "Constructor B2:" << b2 << endl;
    }
    void Print() { cout << b2 << endl; }
private:
    int b2;
};

class B3 {
public:
    B3(int i) {
        b3 = i;
        cout << "Constructor B3:" << b3 << endl;
    }
    int Getb3() { return b3; }
private:
    int b3;
};

class A :public B2, public B1//多重继承
{
public:
    A(int i, int j, int k, int l);
    void Print();
private:
    B3 bb;
    int a;
};

A::A(int i, int j, int k, int l) :a(l), bb(k), B2(j), B1(i) {
    cout << "Constructor A:" << a << endl;
}

void A::Print() {
    B1::Print();
    B2::Print();
    cout << bb.Getb3() << endl << a << endl;
}
int main() {
    A aa(1, 2, 3, 4);
    aa.Print();
    return 0;
}

输出

cpp 复制代码
Constructor B2:2
Constructor B1:1
Constructor B3:3
Constructor A:4
1
2
3
4

14.12 增量式开发

  • 增量式开发:在不破坏已有代码 的前提下添加新代码
  • 继承和组合的一个优点是:它们支持增量式开发

歧义问题:

  • 歧义1:当多个基类中拥有同名成员函数 时(多重继承情况下),可能会发生名字冲突
  • 歧义2:如果一个派生类有两个基类,而这两个基类又都继承自一个类,那么就可能触发歧义(即,一个类在继承链中被"继承了两次")。也就是"菱形继承问题"。

消除歧义1

歧义1:

当多个基类中拥有同名成员函数 时(多重继承情况下),可能会发生名字冲突

歧义1示例
cpp 复制代码
class A{
public:
    void f(){}
};
class B{
public:
    void f(){}
    void g(){}
};
class C:public A,public B{
public:		void g(){}
};
void main(){
    C c;
    //c.f();		//error:编译器不知道式A还是B的f()
    c.g();		//ok
    c.B::g();	//ok
}
解决方法:
  1. 使用作用域解析运算符::(比如c.A::f();
  2. 在派生类中定义一个新的函数(以覆盖或隐藏同名函数)
方法一:使用作用域解析运算符::
cpp 复制代码
class A{
public:
    void f(){}
};
class B{
public:
    void f(){}
    void g(){}
};
class C:public A,public B{
public:		void g(){}
};
void main(){
    C c;
    c.A::f();	//ok
    c.g();		//ok
    c.B::g();	//ok
}
方法二:在派生类中定义一个新的函数
cpp 复制代码
class A {
public:
    void f() {}
};
class B {
public:
    void f() {}
    void g() {}
};
class C :public A, public B {
public:
    void g() {}
    void f() { A::f(); }
};
void main() {
    C c;
    c.f();	//ok
    c.g();		//ok
    c.B::g();	//ok
}

上述讲的只是第一种歧义:当多个基类中拥有同名成员函数 时(多重继承情况下),可能会发生名字冲突

接下来讲述第二种歧义:如果一个派生类又有两个基类,且这两个基类又都继承自同一个类,就可以出现歧义。(即,同一个类被继承了两次)


消除歧义2

歧义2:

如果一个派生类有两个基类,且这两个基类又都继承自同一个类 ,就可能出现歧义。(即,同一个类被继承了两次)

歧义2示例
cpp 复制代码
class A
{
    public:
    void f(){};
};

class B:public A{
	//......  
};

class C:public A{
    //......
};

class D:public B,public C{
    //......
};

void main(){
    D d;//A,B,A,C,D
    d.f();	//error
}

A B A C D

解决方法:
  1. 使用作用域解析运算符 ::
  2. 在派生类中定义一个新函数来隐藏或重写冲突函数
  3. 使用虚基类(virtual base class) 避免重复继承
方法一:使用作用域解析运算符::
cpp 复制代码
class A
{
    public:
    void f(){};
};

class B:public A{
	//......  
};

class C:public A{
    //......
};

class D:public B,public C{
    //......
};

void main(){
    D d;//A,B,A,C,D
    d.B::f();	//ok	
}
方法二:在派生类中重新定义一个新的函数
cpp 复制代码
class A
{
    public:
    void f(){};
};

class B:public A{
	//......  
};

class C:public A{
    //......
};

class D:public B,public C{
public:
    void f(){}
};

void main(){
    D d;//A,B,A,C,D
    d.f();	//ok	
}
方法三:使用虚基类
  • 关键字virtual 只作用于其后紧跟的基类。
cpp 复制代码
class D:virtual public A,public B,virtual public C{
    //............
};

上述中A和C都是虚基类,B不是虚基类

cpp 复制代码
class A
{
    public:
    void f(){};
};

class B:virtual public A{
	//......  
};

class C:virtual public A{
    //......
};

class D:public B,public C{
	//......
};

void main(){
    D d;//A,B,A,C,D
    d.f();	//ok	
}

采用虚基类后,A,B,C,D的关系就变为
A B C D

虚基类与非虚基类的内存比较
  • 虚基类

  • 非虚基类

    虚基类只存在一份,而非虚基类会重复拷贝

虚基类的构造函数
  • 它只会被调用一次。

  • 它是由最底层派生类 的构造函数调用的(可以是显示 也可以是隐式调用)。

  • 它会在非基类的构造函数之前被调用。

  • 虚基类的构造函数会出现在所有派生类的构造函数的成员初始化列表中。

  • 如果没有显示调用,则会自动调用它的默认构造函数。

下面解释显示隐式调用

cpp 复制代码
//显示调用
class A {
public:
    A(int x) { cout << "A(" << x << ")\n"; }
};

class B : virtual public A {
public:
    B() : A(1) { cout << "B\n"; } 
};
cpp 复制代码
//隐式调用
class A {
public:
    A() { /*...*/ } // 默认构造函数
};

class B : virtual public A {
public:
    B() { // 隐式调用 A()
    }
};

下面解释最底层派生类

最底层派生类:最终的创建那个类对象,那个类就是最底层派生类,也就是最终构造函数调用者。

示例

cpp 复制代码
#include <iostream>
using namespace std;
class A {
public:
    A(int i) { cout << "A" << i << endl; }
};

class B : virtual public A {
public:
    B(int i = 1):A(i) { cout << "B\n"; }
};

class C : virtual public A {
public:
    C(int i = 2):A(i) { cout << "C\n"; }
};

class D : public B, public C {
public:
    D(int i = 3):A(i) { cout << "D\n"; }
};

​ 现在看两种对象的构造

cpp 复制代码
int main(){
    B b;
    return 0;
}

​ 此时构造的是 B , B最底层派生类 ,所以它负责构虚基类A

输出

cpp 复制代码
A1
B

cpp 复制代码
int main(){
    D d;
    return 0;
}

​ 此时构造的是 DD最底层派生类 ,所以它负责构造虚基类A,即使 BC也继承了 A

输出

cpp 复制代码
A3
B
C
D
  • A 只被调用一次,由 D 构造;

  • BC 如果重新定义了 A 的初始化,那也会被忽略


依旧是对最底层派生类的解释

假设有如下图的类
A B C D E

最底层派生类:

c++ 复制代码
E e;//E是最底层派生类
D d;//D是最底层派生类
B b;//B是最底层派生类
C c;//C是最底层派生类

构造函数:

cpp 复制代码
B(......):A(......){......}
C(......):A(......){......}
D(......):B(......),C(......),A(......){......}
E(......):D(......),A(......){......}

E 不需要构造 BC,是因为它们不是 E直接基类

E 必须构造 A ,是因为 A 是一个虚基类虚基类总是由"最底层派生类"负责构造,即使它不是直接基类。

示例
cpp 复制代码
#include <iostream>
using namespace std;
class A {
public:
    A(const char* s) { cout << "Class A:" << s << endl; }
    ~A() {}
};

class B :virtual public A
{
public:
    B(const char* s1, const char* s2) :A(s1) {
        cout << "Class B:" << s2 << endl;
    }
};

class C :virtual public A
{
public:
    C(const char* s1, const char* s2) :A(s1) {
        cout << "Class C:" << s2 << endl;
    }
};

class D :public B, public C
{
public:
    D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2), A(s1) {
        cout << "Class D:" << s4 << endl;
    }
};
void main() {
    D* ptr = new D("str1", "str2", "str3", "str4");
    delete ptr;
}

输出

cpp 复制代码
Class A:str1
Class B:str2
Class C:str3
Class D:str4

这里先构造BC的原因是 class D:public B,public C ,先继承 B 再继承 C


示例(A 不是虚函数)

cpp 复制代码
#include <iostream>
using namespace std;
class A {
public:
    A(const char* s) { cout << "Class A:" << s << endl; }
    ~A() {}
};

class B :public A
{
public:
    B(const char* s1, const char* s2) :A(s1) {
        cout << "Class B:" << s2 << endl;
    }
};

class C :public A
{
public:
    C(const char* s1, const char* s2) :A(s1) {
        cout << "Class C:" << s2 << endl;
    }
};

class D :public B, public C
{
public:
    D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2) {
        cout << "Class D:" << s4 << endl;
    }
};
void main() {
    D* ptr = new D("str1", "str2", "str3", "str4");
    delete ptr;
}

输出

cpp 复制代码
Class A:str4
Class B:str2
Class A:str2
Class C:str3
Class D:str4

这里D 就不需要再构造A了,简洁基类会自动构造。

14.13 向上转型(Upcasing)

  • 继承最重要的方面,并不是它为了新类提供了成员函数。
  • 更关键的是:它表达了新类和基类之间的关系。
  • 这种关系可以总结为这样的一句话:新类是已有类的一种类型。

向上转型(Upcasting): 将一个 派生类 类型的引用或指针转换为

基类 的引用或指针,就叫做"向上转型"。

示例

cpp 复制代码
class Instrument {
public:
    void play() const {}
};
//Wind是Instrument的派生类
class Wind :public Instrument {};
void tune(Instrument& i) { i.play(); }
void main() {
    Wind flute;
    tune(flute);	//向上转型(Upcasting)
    Instrument* p = &flute;//Upcasting
    Instrument& l = flute;//Upcasting
}

Instrument Wind

  • 当通过指针或引用 (指向或引用基类)来操作派生类对象时,派生类对象可以被当作其基类对象来处理。

在第十五章还会接着讲述"Upcasting" 。

14.14 总结

  • 继承和组合
  • 多重继承
  • 访问控制
  • 向上转型
相关推荐
工藤新一¹18 分钟前
蓝桥杯算法题 -蛇形矩阵(方向向量)
c++·算法·矩阵·蓝桥杯·方向向量
fallzzzzz2 小时前
C++ stl中的list的相关函数用法
c++·list
悦悦子a啊3 小时前
PTA:jmu-ds-最短路径
c++·算法·图论
小王努力学编程4 小时前
高并发内存池(三):TLS无锁访问以及Central Cache结构设计
jvm·数据结构·c++·学习
字节高级特工6 小时前
【C++】”如虎添翼“:模板初阶
java·c语言·前端·javascript·c++·学习·算法
.Vcoistnt6 小时前
Codeforces Round 1024 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划·图论
越甲八千6 小时前
MFC listctrl修改背景颜色
c++·mfc
炯哈哈6 小时前
【上位机——MFC】序列化机制
开发语言·c++·mfc·上位机
隐世16 小时前
C++多态讲解
开发语言·c++