C++之类的组合
类的组合
1.组合的概念
- 类中的成员数据是另一个类的对象。
- 可以在已有抽象的基础上实现更复杂的抽象。
类组合:构建复杂对象的强大技术
在面向对象编程 (OOP) 中,类组合是一种通过将其他类的对象作为成员变量来构建更复杂类的方式。 这种设计原则模拟了现实世界中“has-a”(有一个)的关系。例如,一辆汽车“有一个”引擎和多个轮子。在这种关系中,“汽车”类将包含“引擎”类和“轮子”类的实例。
核心概念
类组合的核心思想是将简单、明确的类作为构建块,来创建功能更强大的类,而无需复制和粘贴代码。 这种方法允许一个类(复合类)包含另一个类(组件类)的实例,并利用其功能。 组件类的对象不能独立于复合类的对象存在;例如,如果汽车被销毁,其引擎和轮子也将不复存在。
一个简单的例子:
想象一下我们正在构建一个程序来模拟一辆汽车。我们可以创建Engine(引擎)和Wheel(轮子)类,它们分别具有自己的属性和方法。然后,我们可以创建一个Car(汽车)类,它在其内部实例化Engine和Wheel的对象。
#include <iostream>
#include <array> // 用于 C++ 的固定大小数组// 1. Engine 类
class Engine {
public:void start() {std::cout << "引擎启动" << std::endl;}
};// 2. Wheel 类
class Wheel {
public:// 轮子的相关属性和方法Wheel() {}
};// 3. Car 类 (核心转换部分)
class Car {
private:Engine engine; std::array<Wheel, 4> wheels;public:// C++ 构造函数。成员 'engine' 和 'wheels' 会在这里被自动默认构造。Car() {std::cout << "一辆新车已制造完成!" << std::endl;}// 启动汽车的方法void startCar() {// 直接调用成员对象的 start() 方法engine.start();std::cout << "汽车启动" << std::endl;}
};int main() {// 创建 Car 对象Car myCar;// 调用方法myCar.startCar();return 0;
}
在这个例子中,Car类通过“组合”Engine和Wheel类的对象来构建。Car类可以委托引擎相关的任务给Engine对象,从而实现模块化的设计。
2.类组合的优势
在软件开发中,通常推荐“组合优于继承”的原则,这是因为组合提供了许多好处:
- 灵活性和可维护性:组合提供了更高的灵活性。我们可以轻松地在运行时更改或替换组件,而无需改变复合类的代码。例如,我们可以为汽车更换不同类型的引擎。
- 代码重用:组合允许我们在不同的上下文中重用组件类,从而提高了代码的整体可重用性。
- 松散耦合:与继承创建的紧密耦合关系不同,组合中的类之间的耦合度较低。 组件类的改变对复合类的影响很小,使得系统更易于维护。
- 更好的封装: 组合有助于实现更好的封装,因为每个类都只关注自己的职责。
- 避免继承层次结构的问题:复杂的继承层次结构可能变得脆弱且难以管理。组合通过将功能分解为独立的组件来避免这些问题。
3.类的组合举例
#include <iostream>// ==========================================================
// 1. 定义基础组件类 (Engine, Wheel, Window)
// 这些类不依赖于其他自定义类,可以先定义。
// ==========================================================class Engine {
public:// const 成员函数表示该函数不会修改任何类成员变量void start() const {std::cout << "引擎启动 (Engine::start)" << std::endl;}void stop() const {std::cout << "引擎停止 (Engine::stop)" << std::endl;}
};class Wheel {
public:void inflate(int psi) const {std::cout << "轮胎充气至 " << psi << " PSI (Wheel::inflate)" << std::endl;}
};class Window {
public:void rollup() const {std::cout << "车窗摇上 (Window::rollup)" << std::endl;}void rolldown() const {std::cout << "车窗摇下 (Window::rolldown)" << std::endl;}
};// ==========================================================
// 2. 定义组合类 (Door)
// Door 类包含一个 Window 对象,所以必须在 Window 定义之后。
// ==========================================================class Door {
public:// Door 类 "拥有" 一个 Window 对象,这是组合关系Window window;void open() const {std::cout << "车门打开 (Door::open)" << std::endl;}void close() const {std::cout << "车门关闭 (Door::close)" << std::endl;}
};// ==========================================================
// 3. 定义顶层类 (Car)
// Car 类包含了 Engine, Wheel, 和 Door 对象。
// ==========================================================class Car {
public:// Car "拥有" 以下部件:Engine engine; // 一个引擎对象Wheel wheel[4]; // 一个包含4个轮子对象的数组Door left, right; // 两个车门对象,分别命名为 left 和 right
};// ==========================================================
// 4. 程序入口 main 函数
// ==========================================================int main() {// 当创建一个 Car 对象时,它的所有成员对象(engine, wheel, left, right)// 也会被自动创建(调用它们各自的默认构造函数)。Car car;// 访问嵌套对象的成员函数// 语法: car -> door -> window -> functioncar.left.window.rollup();// 访问数组中的对象的成员函数// 语法: car -> wheel_array[index] -> functioncar.wheel[0].inflate(72); // 给第一个轮子充气// 也可以调用其他成员的函数car.engine.start();car.right.open();return 0;
}
代码解析
组合 (Composition):这个例子是典型的“has-a”(有一个)关系。
一辆 Car 有一个 Engine。
一辆 Car 有四个 Wheel。
一扇 Door 有一个 Window。
这种通过将一个类的对象作为另一个类的成员的方式,就是组合。它允许你构建出更复杂的对象。
对象访问:在 main 函数中,你可以看到如何通过点号 (.) 操作符来逐级访问成员对象的属性和方法。
car.left:访问 car 对象的 left 成员(它是一个 Door 对象)。
car.left.window:接着访问 left 这个 Door 对象的 window 成员(它是一个 Window 对象)。
car.left.window.rollup():最后调用这个 Window 对象的 rollup() 方法。
car.wheel[0]:访问 car 对象的 wheel 成员(它是一个数组),并获取索引为 0 的元素(一个 Wheel 对象)。
car.wheel[0].inflate(72):调用这个 Wheel 对象的 inflate() 方法。
const 关键字:在成员函数声明的末尾使用 const 关键字,表示这个函数是一个“只读”操作。它向编译器承诺,调用此函数不会改变对象的任何成员变量。
4.类组合的构造函数设计
原则:不仅要负责对本类中的基本类型成员数据赋初值,也要对对象成员初始化。
声明形式: 类名::类名(对象成员所需的形参,本类成员形参) :对象1(参数),对象2(参数),...... { 本类初始化 }
5.类组合的构造函数调用
构造函数调用顺序:先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。然后调用本类的构造函数。(析构函数的调用顺序相反)
若调用默认构造函数(即无形参的),则内嵌对象的初始化也将调用相应的默认构造函数。
示例程序:
1.A 有无参构造,B 不显式调用 A 的构造
#include <iostream>
using namespace std;class A {
public:A() {cout << "A默认构造" << endl;}
};class B {
private:A a; // B 组合了 A
public:B() { // B 的默认构造cout << "B默认构造" << endl;}
};int main() {B b1; // 创建对象return 0;
}
输出结果:
A默认构造
B默认构造
解释:
创建
b1
时,先构造其成员对象a
(调用A()
)。然后执行
B()
。
2.B 想调用 A 的带参构造,必须显式写在初始化列表中
#include <iostream>
using namespace std;class A {
public:A() {cout << "A默认构造" << endl;}A(int x) {cout << "A带参构造: " << x << endl;}
};class B {
private:A a; // B 组合了 A
public:B(int val) : a(val) { // 在初始化列表中显式调用 A(int)cout << "B带参构造" << endl;}
};int main() {B b2(10); // 创建对象return 0;
}
运行结果:
A带参构造: 10
B带参构造
解释:
创建
b2(10)
时,先在初始化列表里调用A(int)
。然后执行
B(int)
的函数体。
6.示例程序
#include <iostream>
using namespace std;// =======================================================
// 定义类 A
// =======================================================
class A {
public:// 类 A 的构造函数,接收一个整数A(int a);// 类 A 的析构函数~A();private:int m_A; // 类 A 的私有成员变量
};// 类 A 构造函数的实现
// 使用成员初始化列表来初始化 m_A
A::A(int a) : m_A(a) {cout << "A construct: " << m_A << endl;
}// 类 A 析构函数的实现
A::~A() {cout << "A destruct: " << m_A << endl;
}// =======================================================
// 定义类 B,它包含一个类 A 的对象
// =======================================================
class B {
public:// 类 B 的构造函数,接收两个整数B(int b, int c);// 类 B 的析构函数~B();private:A a; // 类 B 的私有成员,是类 A 的一个对象int m_B; // 类 B 的私有成员变量
};// 类 B 构造函数的实现
// 重点:使用成员初始化列表来初始化成员 a 和 m_B
// a(b) 的意思是:调用成员 a (类型为 A) 的构造函数,并将参数 b 传递给它。
// m_B(c) 的意思是:将成员 m_B 初始化为参数 c 的值。
B::B(int b, int c) : a(b), m_B(c) {cout << "B construct: " << m_B << endl;
}// 类 B 析构函数的实现
B::~B() {cout << "B destruct: " << m_B << endl;
}// =======================================================
// main 函数,程序的入口
// =======================================================
int main() {cout << "--- Creating object of class B ---" << endl;// 当我们创建类 B 的对象时,观察构造函数的调用顺序B b_obj(100, 200);cout << "--- Object of class B created ---" << endl;cout << endl;cout << "--- Program is about to end, object will be destroyed ---" << endl;// 当 main 函数结束时,b_obj 对象将被销毁,观察析构函数的调用顺序return 0;
}
运行结果:
--- Creating object of class B ---
A construct: 100
B construct: 200
--- Object of class B created ---
--- Program is about to end, object will be destroyed ---
B destruct: 200
A destruct: 100
代码解析:
- 类组合 (Composition): 类 `B` 内部有一个 `A a;` 成员。这意味着每个 `B` 的实例都“包含一个” `A` 的实例。这是一种 "has-a" 关系。
- 成员初始化列表: 这是 C++ 中初始化类成员的标准且高效的方式。 在 `B` 的构造函数 `B::B(int b, int c) : a(b), m_B(c)` 中,`:` 后面的部分就是成员初始化列表。 `a(b)`: 这行代码至关重要。它确保了在 `B` 的构造函数体执行之前,成员 `a` 能够被正确地初始化。它直接调用了 `A` 类的构造函数 `A(int a)`,并将 `b` 作为参数传递进去。必须使用初始化列表的场景:如果一个成员对象(如此处的 `a`)没有默认构造函数,或者需要带参数的构造函数,就必须在初始化列表中进行初始化。
- 构造函数调用顺序: 当创建 `b_obj` 对象时,输出结果会是: A construct: 100 B construct: 200这证明了先构造内嵌的成员对象 (`A`),再构造外层的对象 (`B`)。
- 析构函数调用顺序: 当 `main` 函数结束,`b_obj` 被销毁时,输出结果会是: B destruct: 200 A destruct: 100, 这证明了析构的顺序与构造的顺序正好相反:先析构外层的对象 (`B`),再析构内嵌的成员对象 (`A`)。
7.多重组合
当有多个类组合,按照成员变量顺序,从上到下执行
成员变量的声明顺序决定了它们的构造顺序
#include <iostream>using namespace std;// =======================================================
// 定义类 C (新增加的组合类)
// =======================================================
class C {
public:// C 的默认构造函数C() {cout << "C construct" << endl;}// C 的析构函数~C() {cout << "C destruct" << endl;}
};// =======================================================
// 定义类 A
// =======================================================
class A {
public:// A 的带参构造函数A(int a, int b);// A 的析构函数~A();private:int m_AA;int m_AB;
};// A 构造函数的实现
A::A(int a, int b) : m_AA(a), m_AB(b) {cout << "A construct: " << m_AA << " : " << m_AB << endl;
}// A 析构函数的实现
A::~A() {cout << "A destruct: " << m_AA << " : " << m_AB << endl;
}// =======================================================
// 定义类 B,它组合了类 C 和类 A 的对象
// =======================================================
class B {
public:// B 的构造函数B(int b, int c);// B 的析构函数~B();private:// 重点:成员变量的声明顺序决定了它们的构造顺序C ccc; // 1. 首先是 cccA aaa; // 2. 然后是 aaaint m_B; // 3. 最后是 B 自己的基本类型成员
};// B 构造函数的实现
// 初始化列表中的顺序 (ccc, aaa, m_B) 与实际构造顺序一致,但不起决定作用
// 决定作用的是类定义中的声明顺序
B::B(int b, int c) : ccc(), aaa(b, c), m_B(c) {cout << "B construct: " << m_B << endl;
}// B 析构函数的实现
B::~B() {cout << "B destruct: " << m_B << endl;
}// =======================================================
// main 函数,程序入口
// =======================================================
int main(int argc, char *argv[]) {// 创建一个 B 类的对象,观察构造和析构的输出顺序B b(123, 456);// 尝试访问私有成员(这将导致编译错误,如此处被注释掉)// b.aaa.m_AA = 11122; // 错误:m_AA 是 A 的私有成员,B 无法访问return 0;
}
程序输出:
C construct
A construct: 123 : 456
B construct: 456
B destruct: 456
A destruct: 123 : 456
C destruct
重点总结:
构造函数调用顺序(重点)
当创建一个包含其他类对象的类(如 B)时,构造函数的执行遵循一个严格的顺序。
原则:先构造成员,再构造自身。
成员构造顺序:如果一个类有多个成员对象(如 B 中的 ccc 和 aaa),它们的构造顺序由它们在类定义中的声明顺序决定,从上到下执行。这个顺序与它们在构造函数初始化列表中的顺序无关。
完整顺序:
调用成员对象 ccc 的构造函数。
调用成员对象 aaa 的构造函数。
初始化类 B 自己的基本类型成员 (m_B)。
最后,执行类 B 自身的构造函数体。
析构函数调用顺序
原则:析构的顺序与构造的顺序完全相反。
完整顺序:
首先,执行类 B 自身的析构函数体。
然后,按照声明顺序的逆序,销毁成员对象。先销毁 aaa,再销毁 ccc。
成员初始化列表的责任
当一个类(如 B)组合了另一个需要构造参数的类(如 A)时,外层类(B)的构造函数必须负责通过其成员初始化列表来调用内层类(A)的相应构造函数,并传递必要的参数。
语法为 B(...) : aaa(param1, param2), ... {}。
封装与访问权限
类组合关系并不会破坏封装。
外层类(B)不能直接访问其成员对象(aaa)的私有(private)成员(如 m_AA)。aaa.m_AA = 11122; 这样的代码是错误的,会导致编译失败。这是 C++ 封装原则的体现。