目录
[1. 基类构造函数的调用位置](#1. 基类构造函数的调用位置)
[2. 如果没有显式调用基类构造](#2. 如果没有显式调用基类构造)
[3. 初始化列表的顺序不影响构造顺序](#3. 初始化列表的顺序不影响构造顺序)
[1. 基类没有默认构造,派生类忘记在初始化列表中调用](#1. 基类没有默认构造,派生类忘记在初始化列表中调用)
[2. 在派生类构造函数体中给基类成员赋值](#2. 在派生类构造函数体中给基类成员赋值)
[3. 构造函数和析构函数中调用虚函数](#3. 构造函数和析构函数中调用虚函数)
一、派生类对象的内存布局
先看一个简单的继承关系:
cpp
class Base {
public:
int baseData;
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
public:
int derivedData;
Derived() { cout << "Derived构造" << endl; }
~Derived() { cout << "Derived析构" << endl; }
};
当创建一个Derived对象时,内存里发生了什么?
text
Derived对象内存布局:
┌─────────────────┐
│ Base子对象 │ ← 包含baseData
├─────────────────┤
│ derivedData │
└─────────────────┘
关键理解 :派生类对象不是"独立的一块",它里面完整地包含了一个基类子对象。这意味着:
-
构造派生类时,必须先构造好基类子对象
-
析构派生类时,基类子对象最后才被析构
二、构造顺序:从根到叶
基本规则
派生类对象构造的顺序是:
-
先构造基类子对象(调用基类构造函数)
-
再构造成员变量(按声明顺序)
-
最后执行派生类构造函数体
cpp
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "1. Base构造函数" << endl; }
~Base() { cout << "6. Base析构函数" << endl; }
};
class Member {
public:
Member() { cout << "2. Member构造函数" << endl; }
~Member() { cout << "5. Member析构函数" << endl; }
};
class Derived : public Base {
private:
Member m;
public:
Derived() { cout << "3. Derived构造函数体" << endl; }
~Derived() { cout << "4. Derived析构函数体" << endl; }
};
int main() {
Derived d;
return 0;
}
输出:
text
1. Base构造函数
2. Member构造函数
3. Derived构造函数体
4. Derived析构函数体
5. Member析构函数
6. Base析构函数
构造顺序 :基类 → 成员 → 派生类自身
析构顺序:正好相反:派生类自身 → 成员 → 基类
三、向基类构造函数传参
如果基类没有默认构造函数(无参构造),派生类必须显式在初始化列表中调用基类构造函数。
错误示例
cpp
class Base {
public:
int x;
Base(int val) : x(val) { // 只有带参构造,没有默认构造
cout << "Base带参构造: " << x << endl;
}
};
class Derived : public Base {
public:
Derived() { // ❌ 编译错误!Base没有默认构造函数
// 编译器不知道如何构造Base子对象
}
};
正确写法
cpp
class Derived : public Base {
public:
Derived() : Base(100) { // ✅ 显式调用基类构造函数
cout << "Derived构造" << endl;
}
Derived(int val) : Base(val) { // 也可以把参数传进去
cout << "Derived带参构造: " << val << endl;
}
};
带参数的完整例子
cpp
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n), age(a) {
cout << "Person构造: " << name << ", " << age << "岁" << endl;
}
~Person() {
cout << "Person析构: " << name << endl;
}
void introduce() {
cout << "我是" << name << ", " << age << "岁" << endl;
}
};
class Student : public Person {
private:
string studentId;
string major;
public:
// 派生类构造函数:先调用基类构造函数初始化基类部分
Student(string name, int age, string id, string m)
: Person(name, age), studentId(id), major(m) {
cout << "Student构造: " << id << ", " << major << "专业" << endl;
}
~Student() {
cout << "Student析构: " << studentId << endl;
}
void study() {
introduce(); // 可以调用基类的方法
cout << "学号" << studentId << "正在学习" << major << endl;
}
};
int main() {
cout << "=== 创建Student对象 ===" << endl;
Student s("张三", 20, "2021001", "计算机科学");
cout << "\n=== 调用学生方法 ===" << endl;
s.study();
cout << "\n=== 程序结束,对象析构 ===" << endl;
return 0;
}
输出:
text
=== 创建Student对象 ===
Person构造: 张三, 20岁
Student构造: 2021001, 计算机科学专业
=== 调用学生方法 ===
我是张三, 20岁
学号2021001正在学习计算机科学
=== 程序结束,对象析构 ===
Student析构: 2021001
Person析构: 张三
四、多级继承的构造链
继承可以有多层:GrandChild → Child → Parent。
cpp
#include <iostream>
using namespace std;
class GrandParent {
public:
GrandParent() { cout << "GrandParent构造" << endl; }
~GrandParent() { cout << "GrandParent析构" << endl; }
};
class Parent : public GrandParent {
public:
Parent() { cout << "Parent构造" << endl; }
~Parent() { cout << "Parent析构" << endl; }
};
class Child : public Parent {
public:
Child() { cout << "Child构造" << endl; }
~Child() { cout << "Child析构" << endl; }
};
int main() {
Child c;
return 0;
}
输出:
text
GrandParent构造
Parent构造
Child构造
Child析构
Parent析构
GrandParent析构
构造链规则:从最顶层的基类开始,一层一层往下构造,就像一个多层的蛋糕。析构时从最顶层(派生类本身)开始,一层一层往下析构。
五、构造函数执行顺序的细节
1. 基类构造函数的调用位置
基类构造函数只能在派生类构造函数的初始化列表中调用,不能在函数体中调用。
cpp
Derived::Derived() {
Base(); // ❌ 这不是调用基类构造,而是创建一个临时Base对象
}
2. 如果没有显式调用基类构造
如果基类有默认构造函数(无参或全缺省参数),编译器会自动调用它。
cpp
class Base {
public:
Base() { cout << "Base默认构造" << endl; } // 有默认构造
};
class Derived : public Base {
public:
Derived() { // 不用写Base(),编译器自动调用
cout << "Derived构造" << endl;
}
};
3. 初始化列表的顺序不影响构造顺序
这和组合类一样:构造顺序只由继承/声明顺序决定,不是由初始化列表的书写顺序决定。
cpp
class Derived : public Base1, public Base2 {
Member m1, m2;
public:
Derived() : m2(), m1(), Base2(), Base1() {
// 实际构造顺序:Base1 → Base2 → m1 → m2
// 初始化列表的书写顺序被忽略
}
};
六、完整例子:游戏角色继承体系
用继承来实现一个简单的游戏角色系统:
cpp
#include <iostream>
#include <string>
using namespace std;
// 基类:角色
class Character {
protected:
string name;
int health;
int level;
public:
Character(string n, int hp, int lv)
: name(n), health(hp), level(lv) {
cout << "角色【" << name << "】创建,生命值" << health
<< ",等级" << level << endl;
}
~Character() {
cout << "角色【" << name << "】销毁" << endl;
}
void takeDamage(int damage) {
health -= damage;
if (health < 0) health = 0;
cout << name << "受到" << damage << "点伤害,剩余生命" << health << endl;
}
void heal(int amount) {
health += amount;
cout << name << "恢复" << amount << "生命,当前生命" << health << endl;
}
virtual void attack() const { // virtual:下一讲详解
cout << name << "发动普通攻击!" << endl;
}
};
// 派生类:战士
class Warrior : public Character {
private:
int rage; // 怒气值
public:
Warrior(string n, int hp, int lv)
: Character(n, hp, lv), rage(0) {
cout << " 战士【" << n << "】获得铠甲和战斧" << endl;
}
~Warrior() {
cout << " 战士【" << name << "】的武器碎裂" << endl;
}
void charge() {
rage += 30;
cout << name << "冲锋!怒气+" << 30 << ",当前怒气" << rage << endl;
}
void attack() const override {
cout << name << "使用旋风斩!" << endl;
}
};
// 派生类:法师
class Mage : public Character {
private:
int mana; // 法力值
public:
Mage(string n, int hp, int lv)
: Character(n, hp, lv), mana(100) {
cout << " 法师【" << n << "】获得法杖和魔法书" << endl;
}
~Mage() {
cout << " 法师【" << name << "】的法杖断裂" << endl;
}
void castFireball() {
if (mana >= 30) {
mana -= 30;
cout << name << "施放火球术!法力剩余" << mana << endl;
} else {
cout << name << "法力不足,无法施法" << endl;
}
}
void attack() const override {
cout << name << "施放魔法飞弹!" << endl;
}
};
int main() {
cout << "=== 创建角色 ===" << endl;
Warrior w("亚瑟", 300, 10);
Mage m("甘道夫", 180, 10);
cout << "\n=== 战斗中 ===" << endl;
w.charge();
w.attack();
m.castFireball();
m.attack();
w.takeDamage(50);
m.heal(30);
cout << "\n=== 战斗结束,角色即将销毁 ===" << endl;
return 0;
}
输出:
text
=== 创建角色 ===
角色【亚瑟】创建,生命值300,等级10
战士【亚瑟】获得铠甲和战斧
角色【甘道夫】创建,生命值180,等级10
法师【甘道夫】获得法杖和魔法书
=== 战斗中 ===
亚瑟冲锋!怒气+30,当前怒气30
亚瑟使用旋风斩!
甘道夫施放火球术!法力剩余70
甘道夫施放魔法飞弹!
亚瑟受到50点伤害,剩余生命250
甘道夫恢复30生命,当前生命210
=== 战斗结束,角色即将销毁 ===
法师【甘道夫】的法杖断裂
角色【甘道夫】销毁
战士【亚瑟】的武器碎裂
角色【亚瑟】销毁
注意析构顺序:先销毁后创建的对象(Mage先析构),然后再销毁先创建的Warrior。
七、三个常见错误
1. 基类没有默认构造,派生类忘记在初始化列表中调用
cpp
class Base {
public:
Base(int x) {} // 只有带参构造
};
class Derived : public Base {
public:
Derived() {} // ❌ 编译错误
};
2. 在派生类构造函数体中给基类成员赋值
cpp
Derived::Derived() {
baseData = 10; // 虽然能编译,但效率低
}
更好的做法是在初始化列表中调用基类构造函数:
cpp
Derived::Derived() : Base(10) {} // ✅ 直接构造时初始化
3. 构造函数和析构函数中调用虚函数
在构造函数和析构函数中调用虚函数,不会产生多态行为------调用的是当前类的版本,而不是派生类的版本。这是一个容易踩的坑。
cpp
class Base {
public:
Base() { func(); } // 调用Base::func,不是Derived::func
virtual void func() { cout << "Base" << endl; }
};
class Derived : public Base {
public:
virtual void func() override { cout << "Derived" << endl; }
};
八、这一篇的收获
你现在应该理解:
-
派生类对象包含一个完整的基类子对象
-
构造顺序:基类 → 成员变量 → 派生类构造函数体
-
析构顺序:派生类析构函数体 → 成员变量 → 基类
-
如果基类没有默认构造,派生类必须在初始化列表中显式调用基类构造函数
-
多级继承的构造链从最顶层开始,一层层往下
💡 小作业:设计一个
Vehicle基类,有brand和speed属性,构造函数接收这两个参数。再设计Car和Motorcycle继承它。Car额外有doorCount,Motorcycle额外有hasSidecar。测试创建这些对象时构造函数的调用顺序。
下一篇预告:第13篇《继承(三):同名隐藏与作用域覆盖》------如果派生类定义了和基类同名的成员,基类的成员会被"隐藏"而不是"重载"。什么是同名隐藏?怎么访问被隐藏的基类成员?下篇讲清楚作用域的规则。