目录

一、一个现实世界的例子

二、初始化列表:组合类的关键

三、构造与析构的顺序

构造顺序:先成员,后整体

析构顺序:先整体,后成员

四、三种成员对象初始化方式

方式1:用现有对象初始化(拷贝构造)

方式2:用临时对象初始化

方式3:成员对象的默认构造

五、完整例子:汽车制造厂

六、组合 vs 继承:什么时候用?

七、三个常见错误

1. 成员类没有默认构造,但忘记在初始化列表指定

2. 误以为初始化列表的顺序决定构造顺序

3. 在构造函数体里“初始化”成员对象

八、这一篇的收获


一、一个现实世界的例子

你要写一个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)会在多处出现,代码重复

正确的做法:先定义CPURAMHardDisk类,然后把它们的对象作为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只有带参构造),编译就会失败。

初始化列表允许我们在成员对象“被构造的那一刻”就传入正确的参数。

执行顺序

  1. 进入构造函数体之前,按声明顺序调用成员对象的构造函数

  2. 按初始化列表给的参数进行构造

  3. 最后执行构造函数体中的代码


三、构造与析构的顺序

构造顺序:先成员,后整体

当创建一个Computer对象时:

cpp

Computer myPC("Intel i7", 3.4, 16, 512);

构造顺序:

  1. 先构造cpu(因为它在类中第一个声明)

  2. 再构造ram(第二个声明)

  3. 再构造disk(第三个声明)

  4. 最后执行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(声明顺序最后一个),然后wheel3wheel2wheel1,最后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篇《继承(一):基本语法——公有继承、保护继承、私有继承》——进入面向对象三大特性之二的“继承”。三种继承方式有什么区别?为什么公有继承最常用?下篇讲清楚。

更多推荐