【c++面向对象编程】第10篇:类的组合与嵌套:一个类中包含另一个类的对象
目录
一、一个现实世界的例子
你要写一个Computer类。一台电脑有CPU、内存、硬盘。
如果不用组合,你可能会这样写:
cpp
class Computer {
private:
string cpuModel; // 把CPU的属性展开
int ramSize; // 把内存的属性展开
int storageSize; // 把硬盘的属性展开
public:
Computer(string cpu, int ram, int storage)
: cpuModel(cpu), ramSize(ram), storageSize(storage) {}
};
这样写很快,但你很快会发现:
-
CPU不只是型号,还有频率、核心数、功耗...
-
每增加一个属性,
Computer就要多一个成员变量 -
很多概念(比如CPU)会在多处出现,代码重复
正确的做法:先定义CPU、RAM、HardDisk类,然后把它们的对象作为Computer的成员。
cpp
class CPU {
private:
string model;
double frequency; // GHz
public:
CPU(string m, double f) : model(m), frequency(f) {
cout << "CPU构造: " << model << endl;
}
~CPU() { cout << "CPU析构: " << model << endl; }
void run() { cout << model << " 正在运行" << endl; }
};
class RAM {
private:
int size; // GB
public:
RAM(int s) : size(s) {
cout << "RAM构造: " << size << "GB" << endl;
}
~RAM() { cout << "RAM析构: " << size << "GB" << endl; }
};
class HardDisk {
private:
int capacity; // GB
public:
HardDisk(int c) : capacity(c) {
cout << "硬盘构造: " << capacity << "GB" << endl;
}
~HardDisk() { cout << "硬盘析构: " << capacity << "GB" << endl; }
};
现在,Computer类就可以组合这些部件:
cpp
class Computer {
private:
CPU cpu;
RAM ram;
HardDisk disk;
public:
Computer(string cpuModel, double freq, int ramSize, int diskSize)
: cpu(cpuModel, freq), ram(ramSize), disk(diskSize) {
cout << "Computer构造完成" << endl;
}
~Computer() {
cout << "Computer析构开始" << endl;
}
void work() {
cpu.run();
cout << "电脑正在工作..." << endl;
}
};
二、初始化列表:组合类的关键
上面的Computer构造函数用了初始化列表:
cpp
Computer(string cpuModel, double freq, int ramSize, int diskSize)
: cpu(cpuModel, freq), ram(ramSize), disk(diskSize) {
// 构造函数体
}
为什么不能直接在函数体里初始化?
cpp
// ❌ 错误写法
Computer(string cpuModel, double freq, int ramSize, int diskSize) {
cpu = CPU(cpuModel, freq); // 编译错误!
}
原因:当程序进入构造函数体时,所有的成员对象(cpu、ram、disk)已经被构造完成了。在进入{}之前,它们已经调用了各自的默认构造函数。如果这些类没有默认构造函数(比如CPU只有带参构造),编译就会失败。
初始化列表允许我们在成员对象“被构造的那一刻”就传入正确的参数。
执行顺序:
-
进入构造函数体之前,按声明顺序调用成员对象的构造函数
-
按初始化列表给的参数进行构造
-
最后执行构造函数体中的代码
三、构造与析构的顺序
构造顺序:先成员,后整体
当创建一个Computer对象时:
cpp
Computer myPC("Intel i7", 3.4, 16, 512);
构造顺序:
-
先构造
cpu(因为它在类中第一个声明) -
再构造
ram(第二个声明) -
再构造
disk(第三个声明) -
最后执行
Computer构造函数体
关键:初始化列表的书写顺序不影响构造顺序,构造顺序只由成员在类中声明的顺序决定。
cpp
class Computer {
private:
CPU cpu; // 第一个声明 → 最先构造
RAM ram; // 第二个声明 → 第二个构造
HardDisk disk; // 第三个声明 → 第三个构造
};
cpp
// 输出: CPU构造: Intel i7 RAM构造: 16GB 硬盘构造: 512GB Computer构造完成
析构顺序:先整体,后成员
当myPC离开作用域时,析构顺序刚好相反:
cpp
// 输出: Computer析构开始 硬盘析构: 512GB RAM析构: 16GB CPU析构: Intel i7
口诀:构造先成员后整体,析构先整体后成员(像堆栈一样,后进先出)。
四、三种成员对象初始化方式
方式1:用现有对象初始化(拷贝构造)
cpp
class Computer {
private:
CPU cpu;
public:
Computer(const CPU& existingCpu) : cpu(existingCpu) {
// 拷贝构造
}
};
方式2:用临时对象初始化
cpp
Computer() : cpu(CPU("Default", 2.5)) {
// 先创建临时CPU,再拷贝给cpu(可能被优化掉)
}
方式3:成员对象的默认构造
如果成员类有默认构造函数(无参构造),可以不在初始化列表中显式写出:
cpp
class CPU {
public:
CPU() : model("Unknown"), frequency(0) {} // 默认构造
CPU(string m, double f) : model(m), frequency(f) {}
};
class Computer {
private:
CPU cpu; // 会用默认构造,不用在初始化列表写
public:
Computer() {} // cpu已经被默认构造
};
五、完整例子:汽车制造厂
把组合关系完整演示一遍:
cpp
#include <iostream>
#include <string>
using namespace std;
class Engine {
private:
string type;
int horsepower;
public:
Engine(string t, int hp) : type(t), horsepower(hp) {
cout << " 引擎 " << type << " (" << horsepower << "马力) 已组装" << endl;
}
~Engine() {
cout << " 引擎 " << type << " 已拆解" << endl;
}
void start() { cout << type << "引擎 轰隆隆~~" << endl; }
};
class Wheel {
private:
int position; // 1:左前, 2:右前, 3:左后, 4:右后
public:
Wheel(int pos) : position(pos) {
cout << " 车轮" << position << "号 已安装" << endl;
}
~Wheel() {
cout << " 车轮" << position << "号 已拆卸" << endl;
}
void rotate() { cout << "车轮" << position << "号 转动" << endl; }
};
class Car {
private:
string brand;
Engine engine; // 组合:汽车有引擎
Wheel wheel1, wheel2, wheel3, wheel4; // 组合:汽车有4个轮子
public:
Car(string b, string engineType, int hp)
: brand(b),
engine(engineType, hp),
wheel1(1), wheel2(2), wheel3(3), wheel4(4) {
cout << "汽车 " << brand << " 总装完成!" << endl;
}
~Car() {
cout << "汽车 " << brand << " 开始报废拆解" << endl;
}
void drive() {
cout << brand << " 启动!" << endl;
engine.start();
wheel1.rotate();
wheel2.rotate();
wheel3.rotate();
wheel4.rotate();
cout << brand << " 正常行驶" << endl;
}
};
int main() {
cout << "=== 开始制造汽车 ===" << endl;
Car myCar("特斯拉", "电动", 670);
cout << "\n=== 汽车制造完成,开始驾驶 ===" << endl;
myCar.drive();
cout << "\n=== 驾驶结束,程序退出 ===" << endl;
return 0;
}
输出:
text
=== 开始制造汽车 === 引擎 电动 (670马力) 已组装 车轮1号 已安装 车轮2号 已安装 车轮3号 已安装 车轮4号 已安装 汽车 特斯拉 总装完成! === 汽车制造完成,开始驾驶 === 特斯拉 启动! 电动引擎 轰隆隆~~ 车轮1号 转动 车轮2号 转动 车轮3号 转动 车轮4号 转动 特斯拉 正常行驶 === 驾驶结束,程序退出 === 汽车 特斯拉 开始报废拆解 车轮4号 已拆卸 车轮3号 已拆卸 车轮2号 已拆卸 车轮1号 已拆卸 引擎 电动 已拆解
注意析构的顺序完全反过来:先Car析构体,然后wheel4(声明顺序最后一个),然后wheel3、wheel2、wheel1,最后engine。
六、组合 vs 继承:什么时候用?
很多新手会把“组合”和“继承”搞混。简单区分:
| 组合(has-a) | 继承(is-a) | |
|---|---|---|
| 关系 | 整体-部分 | 子类-父类 |
| 例子 | 汽车有引擎 | 卡车是汽车 |
| 代码 | class Car { Engine e; }; |
class Truck : public Car {}; |
| 复用方式 | 功能委托 | 接口继承 |
经验法则:优先用组合,只在明确的“is-a”关系时才用继承。组合更灵活、耦合更低。
七、三个常见错误
1. 成员类没有默认构造,但忘记在初始化列表指定
cpp
class NoDefault {
public:
NoDefault(int x) {} // 只有带参构造
};
class Container {
private:
NoDefault obj;
public:
Container() {} // ❌ 编译错误!obj不知道如何构造
};
修复:Container() : obj(0) {}
2. 误以为初始化列表的顺序决定构造顺序
cpp
class Demo {
private:
int a;
int b;
public:
Demo() : b(10), a(b) { // 先写b,但a先声明!
// a会先用未初始化的b赋值,危险!
}
};
因为a先声明,所以a先构造,此时b还没初始化,a(b)是未定义行为。
3. 在构造函数体里“初始化”成员对象
cpp
Container() {
obj = NoDefault(10); // 先默认构造obj,再重新赋值,浪费效率
}
不如直接在初始化列表:Container() : obj(10) {}
八、这一篇的收获
你现在应该理解:
-
组合:一个类包含另一个类的对象,表达“has-a”关系
-
成员对象必须在初始化列表中构造(如果没有默认构造的话)
-
构造顺序:先按声明顺序构造成员,再执行构造函数体
-
析构顺序:先执行析构函数体,再按声明逆序析构成员
-
优先用组合,谨慎用继承
💡 小作业:实现一个
Library类,包含一个Book对象的数组(用vector)。Book类有标题、作者、ISBN。Library类提供addBook()、findByAuthor()等方法。注意思考:Library的构造函数/析构函数需要做什么?
下一篇预告:第11篇《继承(一):基本语法——公有继承、保护继承、私有继承》——进入面向对象三大特性之二的“继承”。三种继承方式有什么区别?为什么公有继承最常用?下篇讲清楚。
更多推荐
所有评论(0)