# i、基本概念

# 继承关系中的构造函数

在 C++ 的继承关系中,派生类的构造函数必须在其初始化列表中调用基类的构造函数来构造基类部分

在派生类的构造函数中,构造基类部分的步骤是隐式的,但必须确保基类的构造函数被正确调用,以初始化基类的成员。这是因为派生类不仅包含自己的成员,还继承了基类的成员,这些成员需要在构造过程中得到适当的初始化。

构造函数调用基类构造函数的方式是在派生类构造函数的初始化列表中使用基类的构造函数,并提供适当的参数。

以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base {
public:
Base(int value) : baseValue(value) {
// ...
}

// ...
private:
int baseValue;
};

class Derived : public Base {
public:
Derived(int baseValue, int derivedValue)
: Base(baseValue), derivedValue(derivedValue) {
// ...
}

// ...
private:
int derivedValue;
};

int main() {
Derived d(42, 24);
return 0;
}

另外,如果是在多重继承的情况下,派生类继承了多个基类。基类构造函数的调用顺序是按照派生类中基类的声明顺序进行的,而不是按照派生类构造函数初始化列表中的顺序。

追问:抽象父类的构造函数能否在子类的构造函数初始化列表中被调用?

首先,抽象类它是不能被实例化的。抽象类中包含纯虚函数,意味着派生类必须实现这些纯虚函数,否则派生类也会成为抽象类,无法实例化。

但是,初始化列表可以在派生类的构造函数中调用基类的构造函数,即使基类是抽象类(包含纯虚函数)。这样做是合法的,并不会导致抽象类的实例化。

初始化列表的目的是在派生类构造函数中初始化基类的成员。对于抽象类来说,由于它不能被实例化,派生类必须在其构造函数中调用基类的构造函数来初始化基类的成员。

# 多重继承

多重继承是面向对象编程中的一个概念,它允许一个类继承多个父类的特性和行为。在多重继承中,一个子类可以同时拥有多个父类的属性和方法。

在 C++ 中,多重继承的语法如下:

1
2
3
class DerivedClass : public BaseClass1, public BaseClass2, ... {
// 子类的成员变量和成员函数
};

  • DerivedClass 是子类,它将继承多个父类的特性。
  • BaseClass1 , BaseClass2 , 等等,是父类或基类,它们定义了子类可以继承的属性和方法。
  • public 关键字表明继承是公共的,子类可以访问父类的公共成员。

多重继承的特点:

  1. 多样性和复用性: 多重继承允许一个类从多个父类中继承不同的特性,从而增加了类的多样性和复用性。这有助于构建更灵活的类层次结构。
  2. "Diamond Problem": 多重继承可能引发潜在的问题,其中多个父类中包含具有相同名称的成员,导致命名冲突。这种情况通常称为 "Diamond Problem",即菱形继承。为了解决这个问题,C++ 引入了虚拟继承。
  3. 虚拟继承: C++ 提供了虚拟继承,它允许通过关键字 virtual 来避免菱形继承问题。使用虚拟继承,只会创建一个共享的基类实例,从而解决了冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal {
public:
void eat() { /* 实现吃的行为 */ }
};

class Bird {
public:
void fly() { /* 实现飞翔的行为 */ }
};

class BirdWithLegs {
public:
void walk() { /* 实现行走的行为 */ }
};

class Sparrow : public Animal, public Bird, public BirdWithLegs {
// Sparrow 类继承了 Animal、Bird 和 BirdWithLegs 的行为
};

int main() {
Sparrow sparrow;
sparrow.eat(); // Sparrow 可以调用 eat(),它继承自 Animal
sparrow.fly(); // Sparrow 可以调用 fly(),它继承自 Bird
sparrow.walk(); // Sparrow 可以调用 walk(),它继承自 BirdWithLegs
return 0;
}

上述示例展示了多重继承的概念, Sparrow 类继承了 AnimalBirdBirdWithLegs 类的行为,拥有多个父类的特性。

# 菱形继承

"菱形问题"(Diamond Problem)是一种多重继承中的命名冲突和二义性的问题,通常出现在支持多重继承的编程语言中,如 C++。这个问题的名称来自于继承关系图的形状,它看起来像一个菱形。

在菱形问题中,有一个基类(父类)A,它有两个子类(派生类)B 和 C,分别继承自 A。此外,还有一个类 D,它同时继承自 B 和 C。
这会导致一个菱形继承结构。菱形继承的问题主要表现在 D 类中有两份 A 类的数据成员拷贝。

具体问题如下:

  1. 数据冗余D 类继承了两份 A 类的成员,即 B 继承了一份 AC 也继承了一份 A 。这意味着 D 类实际上有两份 A::value 变量。
  2. 二义性问题:在 D 类中,当你访问 A 类的成员(比如 value ),编译器不知道你是想通过 B 继承的那份 A 还是通过 C 继承的那份 A 。这种情况下,你需要显式指定是哪一条继承路径,比如 D::B::valueD::C::value
    (同样地,如果派生类 BC 都重写了基类 A 中的某个函数,而子类 D 通过多重继承同时继承了 BC ,那么在 D 类中调用这个函数时,会产生 二义性 问题。因为编译器不知道应该调用 B 中的实现,还是 C 中的实现。)

以下是一个示例,说明了菱形问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class A {
public:
int value;
void foo() {
std::cout << "A's foo" << std::endl;
}
};

class B : public A {
public:
void foo() {
std::cout << "B's foo" << std::endl;
}
};

class C : public A {
public:
void foo() {
std::cout << "C's foo" << std::endl;
}
};

class D : public B, public C {
};

int main() {
D d;
d.value; // 这里将会导致二义性
d.foo(); // 这里将会导致二义性,因为 D 继承了 B 和 C,它们都有不同的 foo() 函数实现。
return 0;
}

在上述示例中,类 D 继承了类 B 和类 C,它们都重载了基类 A 的 foo() 函数。当在类 D 的实例上调用 foo() 函数时,编译器无法确定应该使用哪个版本的 foo() 函数,因此会导致二义性。

要解决这个问题,可以采用虚继承。(注意:虚继承只能解决数据冗余的问题,虚继承本身并不能直接解决这种函数重写的二义性问题)

# 虚继承

C++ 的虚继承是一种解决 "菱形问题"(Diamond Problem)的机制,它允许在多重继承关系中通过虚基类来消除二义性。
虚继承通过使用虚基类(virtual base class)来确保在继承层次结构中只存在一个共享的基类实例,而不会出现多次实例化,从而避免二义性。

在虚继承中,如果一个类作为虚基类,它的派生类(继承它的类)将共享同一个基类子对象,而不是创建多个独立的基类子对象。这就确保了无论多少次继承,只有一个共享的虚基类子对象。这有助于解决菱形问题,避免出现二义性。

以下是一个示例,说明虚继承的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A {
public:
int dataA;
};

class B : public virtual A {
public:
int dataB;
};

class C : public virtual A {
public:
int dataC;
};

class D : public B, public C {
public:
int dataD;
};

int main() {
D d;
d.dataA = 10; // 可以直接访问共享的虚基类A的成员
d.dataB = 20;
d.dataC = 30;
d.dataD = 40;
return 0;
}

在上述示例中,类 B 和类 C 都通过虚继承继承了虚基类 A。这确保了在类 D 中只存在一个共享的虚基类 A 的实例。通过虚继承,可以避免菱形问题,使继承层次结构更清晰,并减少二义性。

虚继承的主要作用是解决菱形继承中的数据的冗余和二义性问题,即确保基类的成员在最终派生类中只有一份。但虚继承本身并不能直接解决这种函数重写的二义性问题。解决二义性问题的关键依然在于你如何设计和调用派生类中的函数。

那么如何解决函数菱形继承中的函数二义性问题呢?

方案一:显式调用
如果你希望在 D 中调用 B::show() 或者 C::show() ,可以通过显式指定调用路径来解决这个问题。例如:

1
2
3
D d;
d.B::show(); // 调用 B::show()
d.C::show(); // 调用 C::show()

方案二:再次覆写
如果希望 D 类自动决定调用哪个父类的函数实现,可以在 D 中重写 show() 函数,从而显式选择调用 BC 的实现。例如:

1
2
3
4
5
6
class D : public B, public C {
public:
void show() override {
B::show(); // 选择调用 B::show()
}
};

更新于

请我喝杯咖啡吧~

Rick 微信支付

微信支付

Rick 支付宝

支付宝