Skip to content

Chapter 5 Composition and Inheritance

1 Composition

  • 组合(Composition):用已有对象构造新对象,表示 has-a 关系
carenginetyre

  • 组合的包含方式
    • Fully embedded:完整嵌入子对象
    • By reference:通过引用/指针关联,允许多个对象共享同一子对象
Employee 组合关系

一个 Employee 可能包含 NameAddressHealth PlanSalary HistorySupervisor(另一个 Employee)。

#include <string>
#include <vector>

// 前向声明:HealthPlan 和 SalaryHistory 类的完整实现省略
class HealthPlan {...};
class SalaryHistory {...};

class Employee {
private:
    std::string name;                  // 员工姓名
    std::string address;               // 员工地址
    HealthPlan healthPlan;             // 医保计划
    std::vector<SalaryHistory> salaryHistories;  // 薪资历史记录
    Employee* supervisor;              // 直属上级指针,无上级时为 nullptr

public:
    // 构造函数:初始化姓名、地址,上级指针默认置空
    Employee(const std::string& name, const std::string& address)
        : name(name), address(address), supervisor(nullptr) {}

    // 添加一条薪资历史记录
    void addSalaryHistory(const SalaryHistory& history) {
        salaryHistories.push_back(history);
    }

    // 获取所有薪资历史记录(返回拷贝,保护内部数据)
    std::vector<SalaryHistory> getSalaryHistories() const {
        return salaryHistories;
    }

    // 设置直属上级
    void setSupervisor(Employee* sup) {
        supervisor = sup;
    }

    // 获取直属上级指针
    Employee* getSupervisor() const {
        return supervisor;
    }

    // 析构函数(默认实现,无手动释放的资源)
    ~Employee() {}
};
  • 嵌入对象(Embedded Objects)
    • 所有嵌入对象都必须被初始化
    • 若未显式提供参数,则会尝试调用其默认构造函数
    • 构造函数初始化列表可以为各个子对象传参
    • 语法:name( args ) [':' init-list] '{'
SavingsAccount
// 前向声明:Person(储户信息)和 Currency(货币/余额)类的完整实现省略
class Person { ... };
class Currency { ... };

class SavingsAccount {
public:
    // 构造函数:初始化储户姓名、地址、初始余额(单位:分,避免浮点数精度问题)
    SavingsAccount(const char* name, const char* address, int cents);

    // 析构函数:释放账户相关资源
    ~SavingsAccount();

    // 打印账户完整信息(储户信息+余额)
    void print();

private:
    Person m_saver;    // 储户对象(组合关系,账户包含储户)
    Currency m_balance;// 账户余额对象(组合关系,账户包含余额)
};

// SavingsAccount 类成员函数实现
SavingsAccount::SavingsAccount(
    const char* name, 
    const char* address, 
    int cents
) : m_saver(name, address), 
    m_balance(0, cents)
{}

void SavingsAccount::print()
{
    m_saver.print();
    m_balance.print();
}
为什么不用构造函数体内赋值?
SavingsAccount::SavingsAccount(
    const char* name, const char* address, int cents) {
    m_saver.set_name(name);
    m_saver.set_address(address);
    m_balance.set_cents(cents);
}
  • 这样写时,子对象会先默认构造,再被赋值
  • 若子对象没有默认构造函数,代码甚至无法编译
  • 即使能编译,也比直接初始化多做一步工作

嵌入对象通常设为 private,因为它们通常属于实现细节,新类不一定要暴露子对象的完整接口。

只有当你确实希望外部直接使用子对象的完整公共接口时才考虑设为 public。

public 子对象
// 前向声明:Person 类的完整实现省略
class Person { ... };

class SavingsAccount {
public:
    Person m_saver;  // 公有成员变量:储户对象(Person类型)
    // ... 其他成员(变量/函数)省略
};  

// 假设 Person 类中包含 set_name() 成员函数

int main() {
    SavingsAccount account;  // 创建 SavingsAccount 类的实例 account
    // 通过公有成员访问储户对象,并调用 set_name 方法设置姓名
    account.m_saver.set_name("Fred");

    return 0;
}

2 Inheritance

  • 继承(Inheritance):以现有类为基础复制并扩展,表示 is-a 关系,可以共享成员数据、成员函数和接口。

DoME 多媒体娱乐数据库示例

一个媒体数据库需要同时管理 CDDVD,并支持按条件检索。

无继承的写法
// 前置依赖:CD/DVD类需提前定义,且包含print()方法
class CD {
    public void print() { /* 类实现省略 */ }
}
class DVD {
    public void print() { /* 类实现省略 */ }
}

public class Database {
    // 私有成员:存储 CD、DVD 的列表
    private ArrayList<CD> cds;
    private ArrayList<DVD> dvds;

    // 构造函数:初始化列表,避免空指针
    public Database() {
        cds = new ArrayList<>();
        dvds = new ArrayList<>();
    }

    // 添加 CD 到列表
    public void addCD(CD theCD) {
        cds.add(theCD);
    }

    // 添加 DVD 到列表
    public void addDVD(DVD theDVD) {
        dvds.add(theDVD);
    }

    // 打印所有 CD 和 DVD
    public void list() {
        // 打印 CD 列表
        for (CD cd : cds) {
            cd.print();
            System.out.println();
        }

        // 打印 DVD 列表
        for (DVD dvd : dvds) {
            dvd.print();
            System.out.println();
        }
    }
}

如果没有继承,CDDVD 会各自保存一份相似字段与相似方法,这些重复代码会提高维护成本,以后若新增媒体类型,还要继续复制粘贴相似逻辑,管理这些对象的 Database 类也容易出现重复接口和重复遍历代码。

我们可以把共有状态与共有行为提升到父类 Item,让 CDDVD 继承 Item,只保留各自独有部分,这样类层次更符合抽象关系:CD is-a ItemDVD is-a Item

// 父类 Item:CD 和 DVD 的共同抽象父类
class Item {
    // 子类重写该方法,实现各自的打印逻辑
    public void print() {}
}

// CD 类,继承 Item
class CD extends Item {
    // CD 特有属性(如标题、歌手、时长等)、构造函数、其他方法省略
    @Override
    public void print() {
        // 实现 CD 信息的打印逻辑
    }
}

// DVD 类,继承 Item
class DVD extends Item {
    // DVD 特有属性(如标题、导演、时长等)、构造函数、其他方法省略
    @Override
    public void print() {
        // 实现 DVD 信息的打印逻辑
    }
}

// 重构后的 Database 类(
public class Database {
    // 统一存储所有媒体项(CD/DVD),替代原有的 cds、dvds 两个独立列表
    private ArrayList<Item> items;

    // 构造函数:初始化列表,避免空指针
    public Database() {
        items = new ArrayList<>();
    }

    /**
     * 统一添加媒体项的方法,替代原有的 addCD、addDVD
     * @param theItem 要添加的 Item(CD 或 DVD 实例)
     */
    public void addItem(Item theItem) {
        items.add(theItem);
    }

    // 打印所有存储的媒体项,利用多态自动调用 CD/DVD 各自的 print() 方法
    public void list() {
        for(Item item : items) {
            item.print();
            System.out.println(); // 项之间打印空行
        }
    }
}
  • 继承带来的直接收益
    • 消除重复代码,实现公共逻辑复用
    • 修改公共行为时,只需优先调整父类
    • 新增媒体类型时,只需增加新的派生类并复用现有 Database 逻辑
    • 更容易构建清晰的类层次结构,提升可读性与可扩展性
Employee & Manager
// Employee 类声明
class Employee {
public:
    Employee(const std::string& name, const std::string& ssn);
    const std::string& get_name() const;
    void print(std::ostream& out) const;
    void print(std::ostream& out, const std::string& msg) const;
protected:
    std::string m_name;
    std::string m_ssn;
};

// Employee 构造函数实现
Employee::Employee(const std::string& name, const std::string& ssn)
    : m_name(name), m_ssn(ssn) {}

// Employee 成员函数实现
inline const std::string& Employee::get_name() const { return m_name; }
inline void Employee::print(std::ostream& out) const {
    out << m_name << std::endl;
    out << m_ssn << std::endl;
}
inline void Employee::print(std::ostream& out, const std::string& msg) const {
    out << msg << std::endl;
    print(out);
}

// Manager 类声明
class Manager : public Employee {
public:
    Manager(const std::string& name, const std::string& ssn, const std::string& title);
    const std::string& get_title() const; // 修正:返回值应为 const 引用
    void print(std::ostream& out) const;
private:
    std::string m_title;
};

// Manager 构造函数实现
Manager::Manager(const std::string& name, const std::string& ssn, const std::string& title)
    : Employee(name, ssn), m_title(title) {}

// Manager 成员函数实现
inline void Manager::print( std::ostream& out ) const {
    Employee::print( out ); // 调用基类 print 方法
    out << m_title << endl;
}
inline const std::string& Manager::get_title() const {
    return m_title;
}
inline const std::string Manager::title_name() const {
    return string( m_title + ": " + m_name );
    // 直接访问基类 protected 成员 m_name
}

int main () {
    Employee bob( "Bob Jones", "555-44-0000" );
    Manager bill( "Bill Smith", "666-55-1234",
                "ImportantPerson" );

    string name = bill.get_name(); // okay Manager inherits Employee
    string title = bob.get_title(); // Error -- bob is an Employee!
    cout << bill.title_name() << '\n' << endl;
    bob.print(cout);
    bob.print(cout, "Employee:");
    bill.print(cout);
    bill.print(cout, "Employee:"); // Error -- hidden!
}

基类总是先构造,若未显式给基类传参,会尝试调用其默认构造函数,析构顺序与构造顺序完全相反。

Name Hiding

  • 派生类重新定义某个成员函数后,基类中同名的其他重载版本也会被隐藏
  • 这就是 bill.print(cout, "Employee:") 报错的原因

3 Access Protection

成员访问控制

  • public:所有客户端可见
  • protected:本类、友元、派生类可见
  • private:仅本类与友元可见

友元(friends)

  • 类可以显式授予外部函数/外部类访问其私有成员的权限
  • friend 可以是全局函数、另一个类的成员函数或整个类
  • class 默认访问权限是 privatestruct 默认访问权限是 public
继承方式 基类 public 成员在派生类中 基类 protected 成员在派生类中 基类 private 成员在派生类中
public 继承 public protected 不可直接访问
protected 继承 protected protected 不可直接访问
private 继承 private private 不可直接访问
  • public 继承应表达可替换性(substitution),如果 B is-a A,那么任何能使用 A 的地方都应能安全地使用 B

  • 向上转型(Up-casting)是把派生类对象/引用/指针视为基类对象/引用/指针,如:把学生看作人类,把经理看作员工。

示例

Manager pete("Pete", "444-55-6666", "Bakery");
Employee* ep = &pete; // Upcast
Employee& er = pete;  // Upcast
  • 代价:会丢失派生类特有的静态类型信息;如果函数不是虚函数,那么通过基类接口调用时只会执行基类版本。