派生类中的构造函数与初始化

在前两章中,我们探讨了 C++ 继承的基础知识以及派生类的初始化次序。本章将深入剖析构造函数在派生类初始化中的作用。为此,我们继续使用上节定义的简单 BaseDerived 类:

```cpp
class Base
{
public:
    int m_id{};

    Base(int id = 0)
        : m_id{ id }
    {
    }

    int getId() const { return m_id; }
};

class Derived : public Base
{
public:
    double m_cost{};

    Derived(double cost = 0.0)
        : m_cost{ cost }
    {
    }

    double getCost() const { return m_cost; }
};

一、非派生类构造回顾  
对于非派生类,构造函数只需关注自身成员。例如:

```cpp
```cpp
int main()
{
    Base base{ 5 }; // 调用 Base(int) 构造函数
    return 0;
}

实际步骤:  
1. 为 `base` 分配内存。  
2. 调用对应 `Base` 构造函数。  
3. 成员初始化列表初始化变量。  
4. 构造函数体执行。  
5. 控制权返回调用者。  

二、派生类构造过程  
派生类则略复杂:

```cpp
```cpp
int main()
{
    Derived derived{ 1.3 }; // 调用 Derived(double) 构造函数
    return 0;
}

实际步骤:  
1. 为 `derived` 分配内存(包含 `Base` 与 `Derived` 两部分)。  
2. 调用对应 `Derived` 构造函数。  
3. **先**使用合适 `Base` 构造函数构造 `Base` 子对象;若未指定,则使用默认构造函数。  
4. 成员初始化列表初始化变量。  
5. 构造函数体执行。  
6. 控制权返回调用者。  

唯一区别在于:在 `Derived` 构造函数执行实质工作前,必须**先**完成 `Base` 构造,以建立对象中的 `Base` 部分。

三、如何初始化基类成员  
现有 `Derived` 无法直接设置 `m_id`。若希望在创建 `Derived` 对象时同时设置 `m_cost`(派生部分)与 `m_id`(基类部分),常见错误尝试如下:

1. 试图在派生类初始化列表直接初始化继承成员:

```cpp
```cpp
class Derived : public Base
{
public:
    double m_cost{};

    Derived(double cost = 0.0, int id = 0)
        : m_cost{ cost },
          m_id{ id } // ❌ 错误:不能在此初始化继承成员
    {
    }
};

**原因**:C++ 禁止在派生类构造函数的初始化列表中直接初始化继承成员。若允许,const 或引用成员可能被多次初始化,违背语言规则。

2. 在构造体内赋值:

```cpp
```cpp
Derived(double cost = 0.0, int id = 0)
    : m_cost{ cost }
{
    m_id = id; // 可行,但若 m_id 是 const 或引用则失败;且效率低
}

上述方法虽在本例可用,但对 const/引用成员无效,且会导致 `m_id` 两次赋值,亦无法在 `Base` 构造期间使用该值。

四、正确做法:显式调用基类构造函数  
C++ 允许在派生类初始化列表**显式指定**基类构造函数:

```cpp
```cpp
class Derived : public Base
{
public:
    double m_cost{};

    Derived(double cost = 0.0, int id = 0)
        : Base{ id }   // 调用 Base(int) 并传入 id
        , m_cost{ cost }
    {
    }
};

使用示例:

```cpp
```cpp
#include <iostream>

int main()
{
    Derived derived{ 1.3, 5 };
    std::cout << "Id: " << derived.getId() << '\n';
    std::cout << "Cost: " << derived.getCost() << '\n';
    return 0;
}

输出:

Id: 5 Cost: 1.3


详细过程:  
1. 分配 `derived` 内存。  
2. 调用 `Derived(double, int)`,形参 `cost = 1.3`, `id = 5`。  
3. 编译器发现显式指定了 `Base(int)`,于是以 `5` 调用之。  
4. `Base` 初始化列表将 `m_id` 设为 `5`,函数体空。  
5. `Base` 构造完毕返回。  
6. `Derived` 初始化列表将 `m_cost` 设为 `1.3`,函数体空。  
7. 构造完成。

注意:无论 `Base{ id }` 在初始化列表的哪个位置,**基类构造总是最先执行**。

五、恢复成员为 private  
既然已学会初始化基类成员,便无需再将成员设为 public。应恢复为 private:

```cpp
```cpp
#include <iostream>

class Base
{
private:
    int m_id{};

public:
    Base(int id = 0)
        : m_id{ id }
    {
    }

    int getId() const { return m_id; }
};

class Derived : public Base
{
private:
    double m_cost{};

public:
    Derived(double cost = 0.0, int id = 0)
        : Base{ id }
        , m_cost{ cost }
    {
    }

    double getCost() const { return m_cost; }
};

int main()
{
    Derived derived{ 1.3, 5 };
    std::cout << "Id: " << derived.getId() << '\n';
    std::cout << "Cost: " << derived.getCost() << '\n';
    return 0;
}

输出:

Id: 5 Cost: 1.3


派生类**不能**直接访问基类 private 成员,需通过基类提供的 public 接口。

六、再举一例——更新 `BaseballPlayer`  
回顾先前使用的 `Person` 与 `BaseballPlayer`:

```cpp
```cpp
#include <string>
#include <string_view>

class Person
{
public:
    std::string m_name;
    int m_age{};

    Person(std::string_view name = "", int age = 0)
        : m_name{ name }, m_age{ age }
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
};

class BaseballPlayer : public Person
{
public:
    double m_battingAverage{};
    int m_homeRuns{};

    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
        : m_battingAverage{ battingAverage },
          m_homeRuns{ homeRuns }
    {
    }
};

BaseballPlayer 仅初始化自身成员,未指定 Person 构造函数,因此默认使用 Person(),导致姓名为空、年龄为 0。更合理做法是在创建 BaseballPlayer 时同时提供姓名和年龄,并显式调用 Person 构造函数:

```cpp
#include <iostream>
#include <string>
#include <string_view>

class Person
{
private:
    std::string m_name;
    int m_age{};

public:
    Person(std::string_view name = "", int age = 0)
        : m_name{ name }, m_age{ age }
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
};

class BaseballPlayer : public Person
{
private:
    double m_battingAverage{};
    int m_homeRuns{};

public:
    BaseballPlayer(std::string_view name = "", int age = 0,
                   double battingAverage = 0.0, int homeRuns = 0)
        : Person{ name, age } // 调用 Person(std::string_view, int)
        , m_battingAverage{ battingAverage }
        , m_homeRuns{ homeRuns }
    {
    }

    double getBattingAverage() const { return m_battingAverage; }
    int getHomeRuns() const { return m_homeRuns; }
};

使用示例:

```cpp
```cpp
#include <iostream>

int main()
{
    BaseballPlayer pedro{ "Pedro Cerrano", 32, 0.342, 42 };

    std::cout << pedro.getName() << '\n';
    std::cout << pedro.getAge() << '\n';
    std::cout << pedro.getBattingAverage() << '\n';
    std::cout << pedro.getHomeRuns() << '\n';

    return 0;
}

输出:
Pedro Cerrano
32
0.342
42

七、继承链  
继承链中的类以同样规则工作:

```cpp
```cpp
#include <iostream>

class A
{
public:
    A(int a)
    {
        std::cout << "A: " << a << '\n';
    }
};

class B : public A
{
public:
    B(int a, double b)
        : A{ a }
    {
        std::cout << "B: " << b << '\n';
    }
};

class C : public B
{
public:
    C(int a, double b, char c)
        : B{ a, b }
    {
        std::cout << "C: " << c << '\n';
    }
};

int main()
{
    C c{ 5, 4.3, 'R' };
    return 0;
}

`C` 继承自 `B`,`B` 继承自 `A`。实例化 `C` 时:

1. `main()` 调用 `C(int, double, char)`  
2. `C` 构造函数调用 `B(int, double)`  
3. `B` 构造函数调用 `A(int)`  
4. `A` 无父类,故先构造,输出 `A: 5`  
5. `B` 构造,输出 `B: 4.3`  
6. `C` 构造,输出 `C: R`  

程序输出:
A: 5
B: 4.3
C: R

注意:构造函数只能调用**直接父类**的构造函数;`C` 无法直接调用 `A` 构造函数。

八、析构函数  
销毁派生类时,各析构函数按构造的**逆序**调用。上例中销毁 `c` 时,顺序为:先 `C` 析构,再 `B` 析构,最后 `A` 析构。

**警告**:若基类含虚函数,则析构函数也应为虚函数,否则在特定情况下会导致未定义行为。此内容将在第 25.4 章“虚析构函数、虚赋值与覆盖虚拟化”中详述。

九、小结  
构造派生类时,派生类构造函数负责决定调用哪一个基类构造函数;若未指定,则使用默认基类构造函数。若找不到默认构造函数,编译器报错。构造顺序始终自最基类至最派生类。

至此,你已掌握足够知识,可自行编写继承类!

---

**随堂测验**  
实现引言中提到的 `Fruit` 示例。创建 `Fruit` 基类,含两个 `private` 成员:`name`(`std::string`)与 `color`(`std::string`)。创建继承 `Fruit` 的 `Apple` 类,增加 `private` 成员 `fiber`(`double`)。再创建同样继承 `Fruit` 的 `Banana` 类,无额外成员。  
运行以下程序:

```cpp
```cpp
#include <iostream>

int main()
{
    const Apple a{ "Red delicious", "red", 4.2 };
    std::cout << a << '\n';

    const Banana b{ "Cavendish", "yellow" };
    std::cout << b << '\n';

    return 0;
}

应输出:
Apple(Red delicious, red, 4.2)
Banana(Cavendish, yellow)

提示:由于 `a` 与 `b` 为 `const`,需正确处理 `const` 限定。请确保参数和函数均恰当使用 `const`。

---

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"