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