C++ 对象关系:关联 (Association)

在前两节课程中,我们讨论了两种对象组合——组合(composition)与聚合(aggregation)。对象组合用于刻画“复杂对象由一个或多个简单对象(部件)构成”的关系。

本节课程将探讨一种更为松散的对象间关系,称为关联(association)。与对象组合不同,关联不隐含“整体/部分”关系。

关联

要成为关联,两个对象之间必须满足以下条件:

  1. 被关联对象(成员)与当前对象(类)在其他方面彼此独立
  2. 被关联对象(成员)可同时隶属于多个对象(类);
  3. 被关联对象(成员)的生命周期不由当前对象(类)管理;
  4. 被关联对象(成员)可以知道,也可以不知道当前对象(类)的存在。

与组合或聚合相比,关联中的对象并非“部分”与“整体”的关系;如同聚合,被关联对象可同时属于多个对象且不受管理;但与聚合不同,聚合总是单向,而关联既可是单向,亦可是双向(两对象彼此感知)。

医生与患者的关系便是关联的典型例子。医生与患者之间显然存在关系,但概念上并非“整体/部分”。一名医生一天可看多位患者,一名患者也可就诊于多位医生(求第二意见或看不同专科)。两对象的生命周期互不依赖。

我们可说,关联刻画“uses-a”关系:医生“使用”患者(获得收入),患者“使用”医生(获得医疗服务)。

实现关联

关联是宽泛的关系类型,实现方式多样,但最常用的是指针:对象通过指针指向被关联对象。

以下示例实现双向 Doctor/Patient 关系:医生需知道患者,患者也需知道医生。

#include <functional>   // reference_wrapper
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

// 前向声明,解决循环依赖
class Patient;

class Doctor
{
private:
    std::string m_name{};
    std::vector<std::reference_wrapper<const Patient>> m_patients{};

public:
    Doctor(std::string_view name) : m_name{ name } {}

    void addPatient(Patient& patient);

    // 由于需要 Patient 定义,实现在 Patient 类后
    friend std::ostream& operator<<(std::ostream& out, const Doctor& doctor);

    const std::string& getName() const { return m_name; }
};

class Patient
{
private:
    std::string m_name{};
    std::vector<std::reference_wrapper<const Doctor>> m_doctors{};

    // 设为私有,禁止外部直接调用,应通过 Doctor::addPatient()
    void addDoctor(const Doctor& doctor)
    {
        m_doctors.push_back(doctor);
    }

public:
    Patient(std::string_view name) : m_name{ name } {}

    friend std::ostream& operator<<(std::ostream& out, const Patient& patient);
    const std::string& getName() const { return m_name; }

    // 允许 Doctor::addPatient 访问私有 addDoctor
    friend void Doctor::addPatient(Patient& patient);
};

void Doctor::addPatient(Patient& patient)
{
    m_patients.push_back(patient);
    patient.addDoctor(*this);
}

std::ostream& operator<<(std::ostream& out, const Doctor& doctor)
{
    if (doctor.m_patients.empty())
    {
        out << doctor.m_name << " has no patients right now";
        return out;
    }
    out << doctor.m_name << " is seeing patients: ";
    for (const auto& patient : doctor.m_patients)
        out << patient.get().getName() << ' ';
    return out;
}

std::ostream& operator<<(std::ostream& out, const Patient& patient)
{
    if (patient.m_doctors.empty())
    {
        out << patient.getName() << " has no doctors right now";
        return out;
    }
    out << patient.m_name << " is seeing doctors: ";
    for (const auto& doctor : patient.m_doctors)
        out << doctor.get().getName() << ' ';
    return out;
}

int main()
{
    Patient dave{ "Dave" }, frank{ "Frank" }, betsy{ "Betsy" };
    Doctor james{ "James" }, scott{ "Scott" };

    james.addPatient(dave);
    scott.addPatient(dave);
    scott.addPatient(betsy);

    std::cout << james << '\n';
    std::cout << scott << '\n';
    std::cout << dave << '\n';
    std::cout << frank << '\n';
    std::cout << betsy << '\n';
    return 0;
}

输出:

James is seeing patients: Dave
Scott is seeing patients: Dave Betsy
Dave is seeing doctors: James Scott
Frank has no doctors right now
Betsy is seeing doctors: Scott

一般而言,若单向关联即可满足需求,应避免双向关联,以降低复杂性并减少出错可能。

自反关联(Reflexive association)

有时对象与同类型的其他对象产生关系,称为自反关联。例如大学课程与其先修课(也是课程)。

简化示例:每门课最多一个先修课:

#include <string>
#include <string_view>

class Course
{
private:
    std::string m_name{};
    const Course* m_prerequisite{};

public:
    Course(std::string_view name, const Course* prerequisite = nullptr)
        : m_name{ name }, m_prerequisite{ prerequisite } {}
};

可形成链式关联:课程 → 先修课 → …

关联可间接实现

前述示例均用指针或引用直接连接对象,但关联并不限于此。任何能唯一标识并链接两对象的数据均可。

下例展示 Driver 与 Car 的单向关联,但 Driver 并不持有 Car 指针,而是通过 ID 间接关联:

#include <iostream>
#include <string>
#include <string_view>

class Car
{
private:
    std::string m_name{};
    int m_id{};

public:
    Car(std::string_view name, int id) : m_name{ name }, m_id{ id } {}
    const std::string& getName() const { return m_name; }
    int getId() const { return m_id; }
};

namespace CarLot
{
    Car carLot[4]{
        { "Prius", 4 }, { "Corolla", 17 }, { "Accord", 84 }, { "Matrix", 62 }
    };

    Car* getCar(int id)
    {
        for (auto& car : carLot)
            if (car.getId() == id) return &car;
        return nullptr;
    }
}

class Driver
{
private:
    std::string m_name{};
    int m_carId{};   // 用 ID 而非指针关联 Car

public:
    Driver(std::string_view name, int carId) : m_name{ name }, m_carId{ carId } {}
    const std::string& getName() const { return m_name; }
    int getCarId() const { return m_carId; }
};

int main()
{
    Driver d{ "Franz", 17 };  // Franz 驾驶 ID=17 的车
    Car* car = CarLot::getCar(d.getCarId());
    if (car)
        std::cout << d.getName() << " is driving a " << car->getName() << '\n';
    else
        std::cout << d.getName() << " couldn't find his car\n";
    return 0;
}

本例中 CarLot 静态保存所有汽车。Driver 并不直接持有 Car*,而是保存 ID,需要时通过 ID 从 CarLot 获取对应 Car。

此做法看似低效(需遍历查找),但用 ID 而非指针有其优势:

  • 可引用当前不在内存的对象(如存于文件或数据库,按需加载);
  • 若对象数量不多且内存紧张,用 8 位或 16 位整数代替 4/8 字节指针可大幅节省空间。

组合 vs 聚合 vs 关联 一览

下表助记三者差异:

属性组合 (Composition)聚合 (Aggregation)关联 (Association)
关系类型整体/部分整体/部分彼此独立
成员可属多对象
成员生命周期由类管
方向性单向单向单向或双向
关系动词part-ofhas-auses-a

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"