C++ 学习教程,从入门到精通,C++类继承 — 完整知识点详解(13)

C++类继承 --- 完整知识点详解


1. 一个简单的基类

什么是基类?

基类(Base Class)就是被其他类继承的类。它就像一个"模板"或"蓝图",定义了通用的属性和行为,让其他类可以复用这些内容。

为什么要学?

在实际开发中,很多类之间存在共性。比如"学生"和"老师"都是"人",都有名字、年龄,都有走路、说话的行为。如果不使用继承,我们就得在每个类里重复写相同的代码。基类让我们把共性提取出来,减少重复。

基本结构

cpp 复制代码
class Brass {
private:
    std::string fullName;   // 私有成员:外部不能直接访问
    long acctNum;           // 账号
    double balance;         // 余额

public:
    // 构造函数
    Brass(const std::string &s = "Nullbody", long an = -1,
          double bal = 0.0);

    // 成员函数
    void Deposit(double amt);           // 存款
    void Withdraw(double amt);          // 取款
    double Balance() const;             // 查看余额
    void ViewAcct() const;              // 显示账户信息
    virtual ~Brass() {}                 // 虚析构函数
};

关键点解读

关键点 说明
private 只有本类的成员函数可以访问,派生类和外部都不能直接访问
public 任何人都可以访问
virtual ~Brass() 虚析构函数,确保正确释放派生类的资源(后面详细讲)
const 修饰函数 表示该函数不会修改对象的状态

实际类的实现

cpp 复制代码
Brass::Brass(const std::string &s, long an, double bal) 
    : fullName(s), acctNum(an), balance(bal) {}

void Brass::Deposit(double amt) {
    if (amt < 0)
        std::cout << "存款金额不能为负!\n";
    else
        balance += amt;
}

void Brass::Withdraw(double amt) {
    if (amt < 0)
        std::cout << "取款金额不能为负!\n";
    else if (amt <= balance)
        balance -= amt;
    else
        std::cout << "余额不足!\n";
}

double Brass::Balance() const {
    return balance;
}

void Brass::ViewAcct() const {
    std::cout << "账户持有人: " << fullName << std::endl;
    std::cout << "账号: " << acctNum << std::endl;
    std::cout << "余额: ¥" << balance << std::endl;
}

通俗比喻

把基类想象成盖房子的地基。所有房子都需要地基,但每栋房子的具体样子不同。基类提供了最基本的结构(成员变量和函数),派生类在这个基础上添加自己的特色。


2. 派生一个类

什么是派生类?

派生类(Derived Class)是通过"继承"基类而创建的新类。派生类自动拥有基类的所有公有和保护成员,同时可以添加自己的新成员或修改基类的行为。

为什么要学?

继承是面向对象编程的三大特性之一(封装、继承、多态)。它让我们:

  • 复用代码:不用重写基类已有的功能
  • 建立层次关系:真实世界的事物本身就是分层次的(动物→哺乳动物→狗)
  • 实现多态:同一个接口,不同的行为

语法格式

cpp 复制代码
class 派生类名 : 继承方式 基类名 {
private:
    // 派生类自己的私有成员
protected:
    // 派生类自己的保护成员
public:
    // 派生类自己的公有成员
};

完整示例:从Brass派生出BrassPlus

cpp 复制代码
class BrassPlus : public Brass {    // <-- 关键:public表示公有继承
private:
    double maxLoan;     // 最大贷款额度
    double rate;        // 贷款利率
    double owesBank;    // 当前欠银行的钱

public:
    // 构造函数
    BrassPlus(const std::string &s = "Nullbody", long an = -1,
              double bal = 0.0, double ml = 500,
              double r = 0.11125);

    // 重写基类的ViewAcct函数
    virtual void ViewAcct() const;
    // 重写基类的Withdraw函数
    virtual void Withdraw(double amt);

    // 派生类自己的新函数
    void ResetMax(double m) { maxLoan = m; }
    void ResetRate(double r) { rate = r; }
};

继承方式的区别

继承方式 基类public成员 基类protected成员 基类private成员
public 保持public 保持protected 不可访问
protected 变为protected 变为protected 不可访问
private 变为private 变为private 不可访问

最常用的是 public 继承,因为它保持了"is-a"关系------派生类的公有接口依然是公有的。

通俗比喻

派生类就像在已有的衣服款式上加新设计。Brass是普通银行账户,BrassPlus是"Plus版"银行账户------它有普通账户的所有功能,额外多了贷款功能。你不需要从零开始做一件衣服,只需要在已有的基础上改。

构造函数的写法

cpp 复制代码
BrassPlus::BrassPlus(const std::string &s, long an, double bal,
                     double ml, double r)
    : Brass(s, an, bal)     // 先调用基类构造函数初始化基类部分
{
    maxLoan = ml;
    rate = r;
    owesBank = 0.0;
}

重要规则:派生类的构造函数必须调用基类的构造函数来初始化基类部分。因为派生类不能直接访问基类的私有成员,所以必须通过基类的构造函数来完成初始化。


3. 构造函数:访问权限的考虑

核心问题

派生类对象包含了基类部分和派生类部分。创建派生类对象时,这两部分分别由谁负责初始化?

为什么学?

如果不理解构造函数的调用顺序,就可能:

  • 写出有bug的代码(基类成员未初始化)
  • 出现难以排查的错误
  • 不知道为什么程序崩溃

构造顺序(必须牢记)

复制代码
第1步:调用基类构造函数(初始化基类部分)
第2步:如果派生类有成员对象,调用成员对象的构造函数
第3步:执行派生类构造函数体中的代码

析构顺序恰好相反(先析构派生类,再析构基类)。

示例演示

cpp 复制代码
class Base {
public:
    Base() { std::cout << "Base构造函数被调用\n"; }
    Base(int x) { std::cout << "Base构造函数, x=" << x << "\n"; }
};

class Member {
public:
    Member() { std::cout << "Member构造函数被调用\n"; }
};

class Derived : public Base {
private:
    Member m;           // 成员对象
    int data;
public:
    // 三步:1.基类 2.成员对象 3.自身
    Derived(int x, int y) : Base(x), m(), data(y) {
        std::cout << "Derived构造函数被调用, data=" << data << "\n";
    }
};

// 创建 Derived d(10, 20); 输出:
// Base构造函数, x=10
// Member构造函数被调用
// Derived构造函数被调用, data=20

三种构造场景

场景一:派生类不使用new(不需要额外资源)

cpp 复制代码
// 派生类可以直接使用默认的构造函数
class DerivedSimple : public Brass {
    // 如果没有新增需要初始化的数据,编译器会自动生成
    // 默认的构造函数,自动调用基类的默认构造函数
};

场景二:派生类使用new(有动态内存分配)

cpp 复制代码
class DerivedNew : public Brass {
private:
    char *label;    // 需要用new分配内存
public:
    // 必须显式定义构造函数、拷贝构造函数、赋值运算符、析构函数
    DerivedNew(const char *l = "null", int n = 0, 
               double v = 0.0, const char *lab = "new label");
    ~DerivedNew();
    DerivedNew(const DerivedNew &dn);              // 拷贝构造
    DerivedNew & operator=(const DerivedNew &dn);  // 赋值运算符
};

场景三:基类没有默认构造函数

cpp 复制代码
class Base {
private:
    int id;
public:
    Base(int i) : id(i) {}   // 没有默认构造函数!
};

class Derived : public Base {
private:
    int value;
public:
    // 必须在初始化列表中显式调用基类的带参构造函数
    Derived(int i, int v) : Base(i), value(v) {}
};

注意事项

  • 不能在初始化列表中直接初始化基类成员 :Derived(int x) : Brass::balance(x) 是非法的
  • 不能在初始化列表中初始化成员对象:只能调用成员对象的构造函数
  • 基类先构造,所以基类析构函数在派生类析构之后才被调用

4. 使用派生类

为什么学?

掌握了继承语法后,我们需要知道如何在实际程序中创建和使用派生类对象,这是把理论变成实践的关键一步。

使用方式一:直接创建派生类对象

cpp 复制代码
int main() {
    // 创建普通银行账户
    Brass smelly("Snuffy", 12345, 4183.45);
    // 创建Plus版银行账户
    BrassPlus gassy("Snuffy", 12345, 4183.45, 500, 0.10);

    std::cout << "=== 普通账户 ===\n";
    smelly.ViewAcct();

    std::cout << "\n=== Plus账户 ===\n";
    gassy.ViewAcct();

    // 取钱测试
    std::cout << "\n取款 ¥1000:\n";
    smelly.Withdraw(1000);   // 普通账户:直接从余额扣
    gassy.Withdraw(1000);    // Plus账户:余额不够可以透支!

    return 0;
}

使用方式二:基类指针指向派生类对象(非常重要!)

cpp 复制代码
int main() {
    Brass p1("Alice", 1001, 5000.0);
    BrassPlus p2("Bob", 1002, 3000.0, 800, 0.12);

    // 基类指针可以指向派生类对象
    Brass *pb1 = &p1;
    Brass *pb2 = &p2;

    pb1->ViewAcct();    // 调用Brass::ViewAcct()
    pb2->ViewAcct();    // 这里调用谁的?(后面多态会详细讲)

    // 基类引用可以绑定派生类对象
    Brass &pr1 = p1;
    Brass &pr2 = p2;

    pr1.ViewAcct();
    pr2.ViewAcct();

    return 0;
}

关键规则:

  • 基类指针/引用可以指向派生类对象 ✅
  • 派生类指针/引用不可以指向基类对象 ❌

为什么?因为派生类比基类"更大"(有更多成员),基类指针无法访问派生类特有的成员。

使用方式三:数组中存储派生类对象

cpp 复制代码
Brass *accounts[2];
accounts[0] = new Brass("Alice", 1001, 5000.0);
accounts[1] = new BrassPlus("Bob", 1002, 3000.0, 800, 0.12);

for (int i = 0; i < 2; i++) {
    accounts[i]->ViewAcct();
    delete accounts[i];  // 记得释放!
}

5. 派生类和基类之间的特殊关系

五种核心特殊关系

关系一:派生类可以使用基类的公有和保护方法
cpp 复制代码
// Brass基类有:Deposit(), Withdraw(), Balance(), ViewAcct()

class BrassPlus : public Brass {
public:
    void SomeMethod() {
        Deposit(100.0);     // ✅ 可以调用基类的公有方法
        double b = Balance(); // ✅ 可以调用基类的公有方法
        // 不能直接访问 balance(基类private成员)
    }
};
关系二:基类指针/引用可以指向派生类对象(无需显式类型转换)
cpp 复制代码
BrassPlus bp("Tom", 1003, 2000.0, 500, 0.10);
Brass *p = &bp;     // ✅ 自动向上转型,不需要强制转换
Brass &r = bp;      // ✅ 同样不需要强制转换

// 反过来不行:
// BrassPlus *bp2 = &someBrass;  ❌ 编译错误
关系三:基类指针/引用只可访问基类成员
cpp 复制代码
Brass *p = &bp;      // p指向一个BrassPlus对象
p->ViewAcct();       // ✅ Brass::ViewAcct()
p->Deposit(100);     // ✅ Brass::Deposit()
// p->ResetMax(1000); // ❌ 编译错误!Brass类没有ResetMax方法

这是一个限制:基类指针看不到派生类新增的成员。但通过虚函数,我们可以实现"通过基类指针调用派生类的方法"(多态)。

关系四:基类引用可以隐式转换为派生类引用(不安全,需要强制转换)
cpp 复制代码
Brass &r = bp;       // 基类引用绑定到派生类对象
BrassPlus &rp = (BrassPlus &)r;  // 需要显式强制转换,有风险
rp.ResetMax(1000);   // 现在可以访问派生类成员了

⚠️ 强制转换是有风险的。只有你确定引用实际绑定的是派生类对象时才应该这样做。

关系五:可以将基类对象初始化/赋值为派生类对象(只复制基类部分)
cpp 复制代码
BrassPlus bp("Tom", 1003, 2000.0, 500, 0.10);
Brass b;
b = bp;   // 只复制了基类部分(fullName, acctNum, balance)
          // maxLoan, rate, owesBank 全部丢失!

切片问题(Slicing):派生类的特有部分被"切掉"了,这通常是不期望的行为。

关系总结图

复制代码
基类Brass                    派生类BrassPlus
┌──────────────┐            ┌──────────────┐
│ fullName  ✅ │ ──继承──→  │ fullName  ✅ │
│ acctNum   ✅ │            │ acctNum   ✅ │
│ balance   ✅ │            │ balance   ✅ │
│ ViewAcct  ✅ │            │ maxLoan   ✅ (新增)
│ Deposit   ✅ │            │ rate      ✅ (新增)
│ Withdraw  ✅ │            │ owesBank  ✅ (新增)
│              │            │ ViewAcct  ✅ (重写)
│              │            │ ResetMax  ✅ (新增)
└──────────────┘            └──────────────┘

6. 继承:is-a关系

什么是is-a关系?

public 继承表示的是 "是一种"(is-a) 关系。派生类是一种基类。

为什么学?

这是选择公有继承还是其他方式的核心判断标准。选错了继承方式,程序设计就是错误的。

正确的is-a关系

复制代码
BrassPlus 是一种 Brass           ✅ 合理
学生 是一种 人                    ✅ 合理
狗 是一种 动物                    ✅ 合理
圆形 是一种 形状                  ✅ 合理

错误的关系(不应该用继承)

复制代码
鸡 是一种 蛋                     ❌ 这是has-a关系
车 是一种 引擎                   ❌ 这是has-a关系

什么时候用继承,什么时候用组合?

cpp 复制代码
// ❌ 错误:用车-引擎做例子
class Car : public Engine {   // 车不是一种引擎!错误!
};

// ✅ 正确:用组合(has-a)
class Car {
private:
    Engine engine;    // 车有一个引擎
};

判断口诀:

  • 能说"A 是一种 B" → 用继承
  • 能说"A 有一个 B" → 用组合

is-a关系的注意事项

cpp 复制代码
// 举例:考虑公有继承中,派生类会获得基类的全部公有方法
class Person {
public:
    void eat();
    void sleep();
};

class Student : public Person {
public:
    void study();
};

Student继承Person意味着:

  • 每个Student是一种Person ✅
  • Student可以吃饭、睡觉、学习 ✅
  • 但不是每个人都学习(有些Person不是Student)→ 合理 ✅

is-a关系应该是单向的:每个Student都是Person,但不是每个Person都是Student。


7. 多态公有继承(重点和难点)

什么是多态?

多态(Polymorphism)字面意思是"多种形态"。同一个操作,作用在不同类型的对象上,产生不同的行为。

为什么学?

多态是C++面向对象编程中最强大的特性。没有多态,继承就只是一种代码复用机制。有了多态,我们可以通过统一的接口处理不同类型的对象。

实际应用场景:

  • 一个绘图程序:通过基类指针调用draw(),不同形状(圆、方形、三角形)画出不同的图形
  • 一个游戏:通过基类指针调用attack(),不同角色(战士、法师、刺客)执行不同的攻击
  • 一个银行系统:通过基类指针调用ViewAcct(),不同类型的账户显示不同的信息

问题引入:没有虚函数会怎样?

cpp 复制代码
class Brass {
public:
    void ViewAcct() const {
        std::cout << "显示普通账户信息\n";
    }
};

class BrassPlus : public Brass {
public:
    void ViewAcct() const {     // 注意:这不是重写,是隐藏
        std::cout << "显示Plus账户信息(含贷款额度)\n";
    }
};

int main() {
    Brass b;
    BrassPlus bp;
    Brass *p = &bp;

    p->ViewAcct();   // 输出:显示普通账户信息  ← 不是我们期望的!
    // 因为编译器在编译时就知道p是Brass*类型,直接调用Brass::ViewAcct()
    return 0;
}

问题 :我们希望p->ViewAcct()调用的是BrassPlus版本,但实际上调用的是Brass版本。

解决方案:虚函数

cpp 复制代码
class Brass {
public:
    virtual void ViewAcct() const {    // 加上 virtual!
        std::cout << "显示普通账户信息\n";
    }
};

class BrassPlus : public Brass {
public:
    virtual void ViewAcct() const {    // 重写基类的虚函数
        std::cout << "显示Plus账户信息(含贷款额度)\n";
    }
};

int main() {
    Brass b;
    BrassPlus bp;
    Brass *p = &bp;

    p->ViewAcct();   // 输出:显示Plus账户信息  ← 现在正确了!
    // 运行时,程序发现p实际指向BrassPlus对象,调用BrassPlus::ViewAcct()
    return 0;
}

多态的完整实例

cpp 复制代码
const int CLIENTS = 3;

int main() {
    Brass *p_clients[CLIENTS];

    p_clients[0] = new Brass("Alice", 1001, 5000.0);
    p_clients[1] = new BrassPlus("Bob", 1002, 3000.0, 800, 0.12);
    p_clients[2] = new BrassPlus("Carol", 1003, 500.0, 1000, 0.10);

    for (int i = 0; i < CLIENTS; i++) {
        p_clients[i]->ViewAcct();  // 根据实际类型调用不同的函数
        std::cout << std::endl;
    }

    for (int i = 0; i < CLIENTS; i++) {
        delete p_clients[i];
    }
    return 0;
}

输出:

复制代码
账户持有人: Alice
账号: 1001
余额: ¥5000

账户持有人: Bob
账号: 1002
余额: ¥3000
最大贷款额度: ¥800
当前欠款: ¥0
利率: 0.12

账户持有人: Carol
...

同一个循环,同一个函数调用ViewAcct(),却表现出了不同的行为------这就是多态!

多态的使用条件(三个缺一不可)

  1. 必须通过指针或引用来调用(不能通过对象直接调用)
  2. 基类中的方法必须声明为virtual
  3. 派生类中必须重新定义(重写)该虚函数

8. 静态联编和动态联编

什么是联编(Binding)?

联编就是把函数调用和函数实现代码关联起来的过程------决定"调用哪个函数"。

为什么学?

理解联编机制,你才能明白:

  • 什么时候加virtual,什么时候不加
  • 为什么有些函数调用"快",有些"慢"
  • 为什么有时候多态不生效

静态联编(Static Binding / 编译时联编)

编译器在编译时就知道调用哪个函数。

cpp 复制代码
class A {
public:
    void show() { std::cout << "A::show\n"; }
};

class B : public A {
public:
    void show() { std::cout << "B::show\n"; }  // 注意:没有virtual
};

int main() {
    B obj;
    A *p = &obj;
    p->show();      // 输出:A::show  ← 编译时就确定了
    obj.show();     // 输出:B::show

    // 函数重载也是静态联编
    void func(int x) { std::cout << "int version\n"; }
    void func(double x) { std::cout << "double version\n"; }
    func(5);        // 输出:int version  ← 编译时根据参数类型确定

    return 0;
}

特点:效率高,但不够灵活。

动态联编(Dynamic Binding / 运行时联编)

编译器在运行时根据对象的实际类型来决定调用哪个函数。

cpp 复制代码
class A {
public:
    virtual void show() { std::cout << "A::show\n"; }  // virtual!
};

class B : public A {
public:
    virtual void show() { std::cout << "B::show\n"; }
};

int main() {
    B obj;
    A *p = &obj;
    p->show();      // 输出:B::show  ← 运行时才确定,因为有virtual

    // 动态联编只能通过指针或引用实现
    A a;
    a.show();       // 输出:A::show  ← 对象直接调用,没有多态效果

    return 0;
}

特点:灵活,但有少量运行时开销。

对比总结

特性 静态联编 动态联编
确定时机 编译时 运行时
效率 高(无额外开销) 略低(需要查表)
实现方式 函数重载、运算符重载 虚函数
需要virtual 不需要 需要
调用方式 对象直接调用即可 必须通过指针或引用

什么时候用哪个?

  • 如果不需要多态行为:用静态联编(不加virtual)→ 高效
  • 如果需要多态行为:用动态联编(加virtual)→ 灵活

9. 指针和引用类型的兼容性

为什么学?

这是理解多态的基础。很多编译错误都是因为不理解指针和引用的兼容规则。

核心规则

规则一:向上转型(隐式安全)
cpp 复制代码
BrassPlus bp("Bob", 1002, 3000.0, 800, 0.12);

// 指针:派生类→基类(隐式,安全)
Brass *p = &bp;        // ✅ 完全合法

// 引用:派生类→基类(隐式,安全)
Brass &r = bp;         // ✅ 完全合法

// 对象赋值:派生类→基类(隐式,但会切片)
Brass b = bp;          // ✅ 合法,但只保留基类部分

为什么安全? 派生类包含基类的所有公有方法,所以基类指针可以正常工作。

规则二:向下转型(需要强制转换,有风险)
cpp 复制代码
Brass b("Alice", 1001, 5000.0);

// 基类指针→派生类指针
// BrassPlus *bp = &b;          ❌ 编译错误
BrassPlus *bp = (BrassPlus *)&b;  // ⚠️ 强制转换,但运行时可能出问题

// 基类引用→派生类引用
// BrassPlus &br = b;           ❌ 编译错误
BrassPlus &br = (BrassPlus &)b;  // ⚠️ 强制转换,有风险

为什么危险? 基类对象没有maxLoan、rate等成员,如果通过bp->ResetMax()访问,会导致未定义行为。

规则三:数组和指针的兼容
cpp 复制代码
BrassPlus bpa[5];
Brass *pa[5];      // 基类指针数组

// pa = bpa;       ❌ 数组名不能直接赋值

// 但可以这样用:
for (int i = 0; i < 5; i++) {
    pa[i] = &bpa[i];  // ✅ 每个元素可以赋值
}
规则四:函数参数中的兼容
cpp 复制代码
// 函数接受基类引用
void ShowBalance(const Brass &account) {
    std::cout << "余额: " << account.Balance() << "\n";
}

int main() {
    Brass b("Alice", 1001, 5000.0);
    BrassPlus bp("Bob", 1002, 3000.0, 800, 0.12);

    ShowBalance(b);    // ✅ 传入基类对象
    ShowBalance(bp);   // ✅ 传入派生类对象(隐式向上转型)
    return 0;
}

兼容性总结图

复制代码
BrassPlus (派生类)
    │
    │ 隐式向上转型(安全 ✅)
    ↓
Brass (基类)

Brass (基类)
    │
    │ 强制向下转型(危险 ⚠️)
    ↓
BrassPlus (派生类) → 只有当实际对象是BrassPlus时才是安全的

10. 虚成员函数和动态联编(重点中的重点)

为什么学?

虚函数是C++多态的核心机制。理解虚函数的工作原理,才能正确使用多态,避免常见的陷阱。

虚函数的工作原理

C++通过虚函数表(vtable)实现动态联编
复制代码
当一个类中有虚函数时,编译器会为该类创建一个"虚函数表"(vtable),
表中存放了指向虚函数实现的指针。

Brass的vtable:                BrassPlus的vtable:
┌────────────────────┐      ┌────────────────────┐
│ → Brass::ViewAcct  │      │ → BrassPlus::ViewAcct│
│ → Brass::Withdraw  │      │ → BrassPlus::Withdraw │
│ → Brass::~Brass    │      │ → BrassPlus::~BrassPlus│
└────────────────────┘      └────────────────────┘

每个对象内部都有一个隐藏的vptr指针,指向其所属类的vtable。

对象bp的内存布局:
┌──────────────────┐
│  fullName         │
│  acctNum          │
│  balance          │
│  maxLoan          │  ← 派生类新增的
│  rate             │
│  owesBank         │
│  vptr ────────────┼──→ BrassPlus的vtable
└──────────────────┘

虚函数的声明和重写

cpp 复制代码
class Base {
public:
    virtual void func() const;        // 声明为虚函数
    virtual ~Base();                   // 虚析构函数
};

class Derived : public Base {
public:
    virtual void func() const;        // 重写(override)
    // C++11推荐使用override关键字
    // void func() const override;    // 更安全的写法
    virtual ~Derived();
};

override关键字(C++11)

cpp 复制代码
class Derived : public Base {
public:
    // 使用override,编译器会检查是否正确重写了基类的虚函数
    virtual void func() const override;  // ✅ 编译器确认Base有这个虚函数

    // 如果拼错了:
    virtual void fUnc() const override;  // ❌ 编译错误!
    // 编译器告诉你:Base中没有叫fUnc的虚函数
};

强烈推荐使用override,它能帮你捕获许多低级错误。

析构函数为什么必须是虚函数?

cpp 复制代码
class Base {
public:
    ~Base() { std::cout << "Base析构\n"; }  // 非虚析构函数!
};

class Derived : public Base {
private:
    int *data;
public:
    Derived() { data = new int[100]; }
    ~Derived() { 
        delete[] data;
        std::cout << "Derived析构\n"; 
    }
};

int main() {
    Base *p = new Derived();   // 基类指针指向派生类对象
    delete p;                  // 只调用 Base::~Base()
    return 0;
}

输出:

复制代码
Base析构

问题 :Derived::~Derived() 没有被调用!data指向的内存泄漏了!

cpp 复制代码
class Base {
public:
    virtual ~Base() { std::cout << "Base析构\n"; }  // 虚析构函数!
    // ... 其余不变
};

// 重新运行,输出:
// Derived析构
// Base析构
// ✅ 两个析构函数都被正确调用了!

规则 :只要一个类要被继承,并且通过基类指针删除派生类对象,析构函数必须 声明为virtual。

虚函数的额外开销

每个包含虚函数的类都有一个虚函数表 (vtable),每个对象有一个虚表指针(vptr):

  • 内存开销:每个对象多一个指针的大小(64位系统上是8字节)
  • 性能开销:每次虚函数调用需要额外一次指针间接寻址

这些开销通常很小,但要注意:不要对所有函数都加virtual,只在确实需要多态行为时才使用。


11. 有关虚函数注意事项

注意事项一:构造函数不能是虚函数

cpp 复制代码
class Base {
public:
    virtual Base();    // ❌ 编译错误!构造函数不能是虚函数
};

原因:构造时,对象还没有完全创建好,vptr还没有被设置,无法进行动态联编。

注意事项二:友元函数不能是虚函数

cpp 复制代码
class Base {
    friend void print(const Base &b);  // 友元函数
public:
    virtual void show();               // 普通虚函数
};

友元函数不是类的成员函数,无法被继承或多态调用。

注意事项三:如果使用override,基类函数签名必须完全匹配

cpp 复制代码
class Base {
public:
    virtual void func(int x);
};

class Derived : public Base {
public:
    virtual void func(double x) override;  // ❌ 参数类型不同!
    // 这不是重写,是全新的函数(隐藏了基类版本)
};

// 正确写法:
class Derived : public Base {
public:
    virtual void func(int x) override;  // ✅ 参数类型匹配
};

注意事项四:返回类型协变(Covariant Return Type)

cpp 复制代码
class Base {
public:
    virtual Base* clone() const;
};

class Derived : public Base {
public:
    // 返回类型可以是派生类的指针
    virtual Derived* clone() const override;  // ✅ 合法!
};

只有返回类型是指针或引用时,才允许返回类型不同。

注意事项五:虚函数的默认参数

cpp 复制代码
class Base {
public:
    virtual void func(int x = 10) {
        std::cout << "Base::func, x=" << x << "\n";
    }
};

class Derived : public Base {
public:
    virtual void func(int x = 20) override {
        std::cout << "Derived::func, x=" << x << "\n";
    }
};

int main() {
    Derived d;
    Base *p = &d;
    p->func();   // 输出:Derived::func, x=20  ← 调用的是Derived版本
                 // 但默认参数用的是Base的!因为参数是静态联编的
    return 0;
}

重要 :函数体是动态联编的(根据实际类型),但默认参数是静态联编 的(根据指针类型)。所以不要在虚函数中重新定义默认参数!

注意事项六:可以阻止重写

cpp 复制代码
class Base {
public:
    virtual void func() final;  // C++11:禁止子类重写此函数
};

class Derived : public Base {
public:
    virtual void func() override;  // ❌ 编译错误!Base::func被标记为final
};

注意事项七:重新定义和重写的区别

cpp 复制代码
class Base {
public:
    virtual void func() { std::cout << "Base\n"; }
};

class Derived : public Base {
public:
    // 重写(override):函数签名完全匹配
    virtual void func() override { std::cout << "Derived\n"; }
};

// 注意:如果不加override,且参数列表不同,则是重新定义(不是重写)
// 重新定义不会产生多态行为

12. 访问控制:protected

为什么学?

private太严格(派生类访问不了),public太宽松(谁都能访问)。protected提供了中间地带。

三种访问控制对比

cpp 复制代码
class Base {
private:
    int secret;         // 只有Base自己的成员函数能访问
protected:
    int hidden;         // Base和派生类能访问,外部不能
public:
    int visible;        // 谁都能访问
};

class Derived : public Base {
public:
    void show() {
        // secret;    // ❌ 编译错误!不能访问基类的private成员
        hidden = 42;   // ✅ 可以访问基类的protected成员
        visible = 10;  // ✅ 可以访问基类的public成员
    }
};

int main() {
    Base b;
    b.visible = 10;    // ✅ 可以
    b.hidden = 42;     // ❌ 编译错误!外部不能访问protected
    return 0;
}

三种访问控制总结

访问控制 本类成员 派生类成员 外部代码
private ✅ ❌ ❌
protected ✅ ✅ ❌
public ✅ ✅ ✅

使用建议

优先使用private 。只有在派生类确实需要直接访问某个成员时,才使用protected。

原因:protected成员暴露给所有派生类,而派生类又可以被进一步继承,数据的暴露范围可能比你想象的更大。

cpp 复制代码
class Account {
private:
    double balance;      // 首选:封装保护
protected:
    double rate;         // 只在确实需要时使用
public:
    void deposit(double amt);
};

class Savings : public Account {
public:
    void adjustRate() {
        // balance += 100;    // ❌ 不能直接访问private成员
        // 但可以通过public/protected方法间接操作
        rate = 0.05;         // ✅ 可以直接修改protected成员
    }
};

13. 抽象基类(ABC)

什么是抽象基类?

抽象基类(Abstract Base Class, ABC)是不能被实例化的类。它定义了接口规范,要求派生类必须实现某些函数。

为什么学?

在实际项目中,有些概念太抽象,不应该直接创建对象。比如"形状"这个概念------你不能画一个"形状",只能画"圆""方形""三角形"等具体形状。

ABC让我们:

  • 定义统一的接口规范
  • 强制派生类实现必要的方法
  • 让代码更灵活、更可扩展

纯虚函数

在虚函数声明末尾加 = 0 就变成了纯虚函数:

cpp 复制代码
class Shape {   // 抽象基类
public:
    virtual double area() const = 0;       // 纯虚函数
    virtual double perimeter() const = 0;  // 纯虚函数
    virtual void draw() const = 0;         // 纯虚函数
    virtual ~Shape() {}
};

// Shape s;  // ❌ 编译错误!不能创建抽象基类的对象

完整示例:用ABC管理银行账户

cpp 复制代码
// 抽象基类:所有银行账户的公共接口
class AcctABC {
private:
    std::string fullName;
    long acctNum;
    double balance;

protected:
    // 受保护的访问函数,派生类可以用
    struct Formatting {
        std::ios_base::fmtflags flag;
        std::streamsize pr;
    };
    Formatting SetFormat() const;
    void Restore(Formatting &f) const;

public:
    AcctABC(const std::string &s = "Nullbody", long an = -1,
            double bal = 0.0);
    ~AcctABC() {}
    
    // 公有接口
    void Deposit(double amt);
    double Balance() const { return balance; }
    void ViewAcct() const { ViewAcctAux(); }  // 模板方法模式

    // 纯虚函数:派生类必须实现
    virtual void Withdraw(double amt) = 0;
    virtual void ViewAcctAux() const = 0;

    // 非纯虚函数:派生类可以选择性重写
    virtual ~AcctABC() {}
};
cpp 复制代码
// 普通账户
class BrassAcct : public AcctABC {
public:
    BrassAcct(const std::string &s = "Nullbody", long an = -1,
              double bal = 0.0);
    
    virtual void Withdraw(double amt);        // 必须实现
    virtual void ViewAcctAux() const;         // 必须实现
};

// Plus账户
class BrassPlusAcct : public AcctABC {
private:
    double maxLoan;
    double rate;
    double owesBank;
public:
    BrassPlusAcct(const std::string &s = "Nullbody", long an = -1,
                  double bal = 0.0, double ml = 500, 
                  double r = 0.11125);
    
    virtual void Withdraw(double amt);        // 必须实现
    virtual void ViewAcctAux() const;         // 必须实现
    
    void ResetLoan(double m) { maxLoan = m; }
};
cpp 复制代码
// 使用示例
int main() {
    // AcctABC abc;            // ❌ 不能实例化
    BrassAcct ba("Alice", 1001, 5000.0);
    BrassPlusAcct bpa("Bob", 1002, 3000.0, 800, 0.12);

    // 通过基类指针实现多态
    AcctABC *accounts[2];
    accounts[0] = &ba;
    accounts[1] = &bpa;

    for (int i = 0; i < 2; i++) {
        accounts[i]->ViewAcct();    // 多态调用
    }

    return 0;
}

ABC的设计要点

  1. 至少包含一个纯虚函数 → 类变成抽象类
  2. 可以同时包含纯虚函数和普通函数 → 普通函数提供默认实现
  3. 可以包含数据成员 → 用于存储公共状态
  4. 可以有构造函数 → 初始化公共数据
  5. 派生类必须实现所有纯虚函数 → 否则派生类也会变成抽象类
cpp 复制代码
class Base {
public:
    virtual void func1() = 0;   // 纯虚函数
    virtual void func2() = 0;   // 纯虚函数
};

class Derived : public Base {
public:
    virtual void func1() override { /* 实现了 */ }
    // func2没有实现 → Derived仍然是抽象类!
};

// Derived d;  // ❌ 仍然是抽象类,不能实例化

14. 应用ABC概念 / ABC理念

ABC的典型应用场景

场景一:插件架构
cpp 复制代码
// 定义插件接口(ABC)
class Plugin {
public:
    virtual void initialize() = 0;
    virtual void execute() = 0;
    virtual void shutdown() = 0;
    virtual const char* getName() const = 0;
    virtual ~Plugin() {}
};

// 不同的插件实现
class LogPlugin : public Plugin {
public:
    virtual void initialize() override { std::cout << "日志插件初始化\n"; }
    virtual void execute() override { std::cout << "记录日志\n"; }
    virtual void shutdown() override { std::cout << "日志插件关闭\n"; }
    virtual const char* getName() const override { return "LogPlugin"; }
};

class NetworkPlugin : public Plugin {
public:
    virtual void initialize() override { std::cout << "网络插件初始化\n"; }
    virtual void execute() override { std::cout << "发送数据\n"; }
    virtual void shutdown() override { std::cout << "网络插件关闭\n"; }
    virtual const char* getName() const override { return "NetworkPlugin"; }
};

// 主程序只依赖抽象接口
void runPlugin(Plugin *p) {
    p->initialize();
    p->execute();
    p->shutdown();
}
场景二:策略模式
cpp 复制代码
// 排序策略(ABC)
class SortStrategy {
public:
    virtual void sort(int arr[], int n) = 0;
    virtual ~SortStrategy() {}
};

class BubbleSort : public SortStrategy {
public:
    void sort(int arr[], int n) override {
        // 冒泡排序实现
    }
};

class QuickSort : public SortStrategy {
public:
    void sort(int arr[], int n) override {
        // 快速排序实现
    }
};

// 使用时可以动态切换排序策略
class Sorter {
private:
    SortStrategy *strategy;
public:
    void setStrategy(SortStrategy *s) { strategy = s; }
    void doSort(int arr[], int n) { strategy->sort(arr, n); }
};
场景三:图形界面系统
cpp 复制代码
class Widget {   // 抽象基类:所有UI组件的基类
public:
    virtual void paint() = 0;       // 纯虚函数:每个组件必须定义自己的绘制方式
    virtual void handleInput() = 0; // 纯虚函数:每个组件必须定义自己的输入处理
    virtual int getWidth() = 0;
    virtual int getHeight() = 0;
    virtual ~Widget() {}
};

class Button : public Widget {
private:
    int width, height;
    std::string label;
public:
    void paint() override { /* 绘制按钮 */ }
    void handleInput() override { /* 处理点击 */ }
    // ...
};

class TextBox : public Widget {
private:
    int width, height;
    std::string text;
public:
    void paint() override { /* 绘制文本框 */ }
    void handleInput() override { /* 处理输入 */ }
    // ...
};

ABC的核心理念

  1. "契约"思想:ABC定义了"契约",所有派生类必须履行这个契约
  2. 接口与实现分离:用户只需要知道接口,不需要知道具体实现
  3. 开闭原则:对扩展开放(可以新增派生类),对修改关闭(不需要修改现有代码)
  4. 依赖抽象而非具体:代码应该依赖于抽象基类,而不是具体的派生类

15. 继承和动态内存分配

为什么学?

当基类或派生类使用了new进行动态内存分配时,继承会变得更加复杂。你必须正确实现拷贝构造函数、赋值运算符和析构函数("三/五法则"),否则会出现内存泄漏或悬空指针。

回顾:基类使用new的情况

cpp 复制代码
class BaseDMA {
private:
    char *label;
    int rating;
public:
    BaseDMA(const char *l = "null", int r = 0);
    BaseDMA(const BaseDMA &rs);                    // 拷贝构造
    BaseDMA & operator=(const BaseDMA &rs);        // 赋值运算符
    ~BaseDMA();
};

16. 第一种情况:派生类不使用new

场景说明

基类使用new分配了内存,但派生类没有新增任何需要new的成员。

cpp 复制代码
class BaseDMA {
private:
    char *label;
    int rating;
public:
    BaseDMA(const char *l = "null", int r = 0);
    BaseDMA(const BaseDMA &rs);
    BaseDMA & operator=(const BaseDMA &rs);
    ~BaseDMA();
};

// 派生类:不使用new
class LackDMA : public BaseDMA {
private:
    char color[40];    // 数组,不需要new
public:
    LackDMA(const char *l = "null", int r = 0, const char *c = "black");
    void View() const;
};

派生类需要做什么?

cpp 复制代码
// 派生类的构造函数:调用基类构造函数初始化基类部分
LackDMA::LackDMA(const char *l, int r, const char *c)
    : BaseDMA(l, r)     // 委托给基类构造函数
{
    strncpy(color, c, 39);
    color[39] = '\0';
}

// 派生类不需要定义拷贝构造函数和赋值运算符!
// 编译器自动生成的版本会自动调用基类的拷贝构造函数和赋值运算符
// 这对"成员是数组"的情况是完全正确的

为什么不需要额外定义?

函数 为什么不用额外定义
拷贝构造函数 编译器自动生成的会调用BaseDMA(基类部分)的拷贝构造函数✅
赋值运算符 编译器自动生成的会调用operator=(基类部分)的赋值运算符✅
析构函数 编译器自动生成的会先析构派生类部分,再调用基类析构函数✅

17. 第二种情况:派生类使用new

场景说明

派生类也使用new分配了额外的内存。

cpp 复制代码
class BaseDMA {
private:
    char *label;
    int rating;
public:
    BaseDMA(const char *l = "null", int r = 0);
    BaseDMA(const BaseDMA &rs);
    BaseDMA & operator=(const BaseDMA &rs);
    ~BaseDMA();
};

class HasDMA : public BaseDMA {
private:
    char *style;    // 使用new分配内存!
public:
    HasDMA(const char *l = "null", int r = 0, const char *s = "none");
    HasDMA(const HasDMA &hs);
    HasDMA & operator=(const HasDMA &hs);
    ~HasDMA();
};

派生类必须显式定义的函数

cpp 复制代码
// 构造函数
HasDMA::HasDMA(const char *l, int r, const char *s)
    : BaseDMA(l, r)   // 基类部分委托给基类
{
    style = new char[strlen(s) + 1];   // 派生类部分自己管理
    strcpy(style, s);
}

// 析构函数
HasDMA::~HasDMA() {
    delete[] style;     // 释放派生类分配的内存
    // 基类的析构函数 ~BaseDMA() 会被自动调用
}

// 拷贝构造函数(关键!)
HasDMA::HasDMA(const HasDMA &hs)
    : BaseDMA(hs)       // ⚠️ 传入基类引用,调用基类的拷贝构造函数
{
    style = new char[strlen(hs.style) + 1];
    strcpy(style, hs.style);
}

// 赋值运算符(关键!)
HasDMA & HasDMA::operator=(const HasDMA &hs) {
    if (this == &hs)
        return *this;       // 1. 检查自赋值

    BaseDMA::operator=(hs); // 2. 先调用基类的赋值运算符,处理基类部分

    delete[] style;          // 3. 释放旧的style内存

    style = new char[strlen(hs.style) + 1];  // 4. 分配新内存
    strcpy(style, hs.style);                   // 5. 复制数据

    return *this;
}

关键点详解

拷贝构造函数中 BaseDMA(hs) 做了什么?

复制代码
hs是HasDMA对象,它"是一种"BaseDMA。
BaseDMA(hs) 调用 BaseDMA 的拷贝构造函数,
把hs的BaseDMA部分(label和rating)正确地拷贝过来。

赋值运算符中 BaseDMA::operator=(hs) 做了什么?

复制代码
显式调用基类的赋值运算符,处理基类部分的数据(label和rating)。
如果不写这一行,基类部分不会被更新!

常见错误

cpp 复制代码
// ❌ 错误:忘记调用基类的赋值运算符
HasDMA & HasDMA::operator=(const HasDMA &hs) {
    if (this == &hs)
        return *this;
    // 基类部分没有被赋值!
    delete[] style;
    style = new char[strlen(hs.style) + 1];
    strcpy(style, hs.style);
    return *this;
}

// ❌ 错误:在拷贝构造函数中调用赋值运算符
HasDMA::HasDMA(const HasDMA &hs)
    : BaseDMA(hs) {
    *this = hs;  // 不要在构造函数中用赋值运算符!
    // this->style还没有被初始化,delete[] style会出问题
}

18. 使用动态内存分配和友元的继承示例

完整示例

cpp 复制代码
#include <iostream>
#include <cstring>

// ===== 基类 BaseDMA =====
class BaseDMA {
private:
    char *label;
    int rating;

public:
    // 构造函数
    BaseDMA(const char *l = "null", int r = 0);
    // 拷贝构造函数
    BaseDMA(const BaseDMA &rs);
    // 赋值运算符
    BaseDMA & operator=(const BaseDMA &rs);
    // 析构函数
    ~BaseDMA();

    // 友元函数
    friend std::ostream & operator<<(std::ostream &os, const BaseDMA &rs);
};

// 实现
BaseDMA::BaseDMA(const char *l, int r) {
    label = new char[strlen(l) + 1];
    strcpy(label, l);
    rating = r;
}

BaseDMA::BaseDMA(const BaseDMA &rs) {
    label = new char[strlen(rs.label) + 1];
    strcpy(label, rs.label);
    rating = rs.rating;
}

BaseDMA & BaseDMA::operator=(const BaseDMA &rs) {
    if (this == &rs)
        return *this;
    delete[] label;
    label = new char[strlen(rs.label) + 1];
    strcpy(label, rs.label);
    rating = rs.rating;
    return *this;
}

BaseDMA::~BaseDMA() {
    delete[] label;
}

std::ostream & operator<<(std::ostream &os, const BaseDMA &rs) {
    os << "标签: " << rs.label << ", 评分: " << rs.rating;
    return os;
}
cpp 复制代码
// ===== 派生类 HasDMA(不使用new)=====
class LackDMA : public BaseDMA {
private:
    char color[40];

public:
    LackDMA(const char *l = "null", int r = 0, const char *c = "black");
    friend std::ostream & operator<<(std::ostream &os, const LackDMA &rs);
};

LackDMA::LackDMA(const char *l, int r, const char *c)
    : BaseDMA(l, r) {
    strncpy(color, c, 39);
    color[39] = '\0';
}

// 重载 << 运算符(友元函数)
std::ostream & operator<<(std::ostream &os, const LackDMA &rs) {
    // 需要强制转换才能使用基类的友元函数
    os << (const BaseDMA &)rs;         // 调用基类的operator<<
    os << "\n颜色: " << rs.color;
    return os;
}
cpp 复制代码
// ===== 派生类 HasDMA(使用new)=====
class HasDMA : public BaseDMA {
private:
    char *style;

public:
    HasDMA(const char *l = "null", int r = 0, const char *s = "none");
    HasDMA(const HasDMA &hs);
    HasDMA & operator=(const HasDMA &hs);
    ~HasDMA();
    friend std::ostream & operator<<(std::ostream &os, const HasDMA &hs);
};

HasDMA::HasDMA(const char *l, int r, const char *s)
    : BaseDMA(l, r) {
    style = new char[strlen(s) + 1];
    strcpy(style, s);
}

HasDMA::HasDMA(const HasDMA &hs)
    : BaseDMA(hs) {
    style = new char[strlen(hs.style) + 1];
    strcpy(style, hs.style);
}

HasDMA & HasDMA::operator=(const HasDMA &hs) {
    if (this == &hs)
        return *this;
    BaseDMA::operator=(hs);     // 调用基类赋值运算符
    delete[] style;
    style = new char[strlen(hs.style) + 1];
    strcpy(style, hs.style);
    return *this;
}

HasDMA::~HasDMA() {
    delete[] style;
}

std::ostream & operator<<(std::ostream &os, const HasDMA &hs) {
    os << (const BaseDMA &)hs;  // 调用基类的operator<<
    os << "\n风格: " << hs.style;
    return os;
}
cpp 复制代码
// ===== 使用示例 =====
int main() {
    BaseDMA b1("标签A", 5);
    LackDMA l1("标签B", 3, "红色");
    HasDMA h1("标签C", 4, "现代风格");

    std::cout << "=== 直接输出 ===\n";
    std::cout << b1 << "\n\n";
    std::cout << l1 << "\n\n";
    std::cout << h1 << "\n\n";

    // 拷贝构造测试
    HasDMA h2 = h1;
    std::cout << "=== 拷贝构造 ===\n";
    std::cout << h2 << "\n\n";

    // 赋值运算测试
    HasDMA h3("标签D", 1, "旧风格");
    h3 = h1;
    std::cout << "=== 赋值运算 ===\n";
    std::cout << h3 << "\n";

    return 0;
}

友元函数与继承的关系

cpp 复制代码
// 派生类的友元函数需要访问基类数据时:
std::ostream & operator<<(std::ostream &os, const HasDMA &hs) {
    // hs可以访问自己的私有成员style
    // 但不能访问基类BaseDMA的私有成员label和rating
    
    // 方法1:强制类型转换
    os << (const BaseDMA &)hs;     // 调用基类的友元operator<<
    
    // 方法2:如果基类提供了公有访问函数
    // os << "标签: " << hs.GetLabel() << ", 评分: " << hs.GetRating();
    
    os << "\n风格: " << hs.style;
    return os;
}

关键点:友元关系不能被继承。基类的友元函数不自动成为派生类的友元函数。


19. 类设计回顾

编译器生成的成员函数

编译器会自动为每个类生成以下四个默认成员函数(如果程序员没有手动定义):

cpp 复制代码
class MyClass {
    // 编译器自动生成的:
    // MyClass();                                    // 默认构造函数
    // MyClass(const MyClass &other);                // 拷贝构造函数
    // MyClass & operator=(const MyClass &other);    // 赋值运算符
    // ~MyClass();                                   // 析构函数
};
默认构造函数
cpp 复制代码
class Widget {
    int x;
    double y;
    // 编译器生成的默认构造函数:
    // Widget() : x(0), y(0.0) {}   // 内置类型初始化为0
};
拷贝构造函数
cpp 复制代码
class Widget {
    int x;
    double y;
public:
    Widget(int a, double b) : x(a), y(b) {}
    // 编译器生成的拷贝构造函数:
    // Widget(const Widget &w) : x(w.x), y(w.y) {}
    // 逐成员拷贝(浅拷贝)
};

浅拷贝 vs 深拷贝:

cpp 复制代码
class Shallow {
    int *data;
public:
    Shallow(int val) { data = new int(val); }
    // 编译器生成的拷贝构造函数做浅拷贝:
    // Shallow(const Shallow &s) : data(s.data) {}  // data指向同一块内存!
};

class Deep {
    int *data;
public:
    Deep(int val) { data = new int(val); }
    // 必须手动实现深拷贝:
    Deep(const Deep &d) { data = new int(*d.data); }  // 分配新内存
};

其他的类方法

静态成员函数
cpp 复制代码
class Counter {
private:
    static int count;      // 静态成员变量:所有对象共享
public:
    Counter() { count++; }
    static int getCount() { return count; }  // 静态成员函数
};

int Counter::count = 0;   // 静态成员变量必须在类外初始化

int main() {
    // 静态成员函数可以通过类名调用(不需要对象)
    std::cout << "计数: " << Counter::getCount() << "\n";  // 输出:0

    Counter c1, c2, c3;
    std::cout << "计数: " << Counter::getCount() << "\n";  // 输出:3
    return 0;
}
类成员函数作为函数参数
cpp 复制代码
class Compare {
public:
    bool operator()(int a, int b) const {
        return a < b;
    }
};

int main() {
    Compare comp;
    bool result = comp(5, 3);   // 像函数一样调用对象
    // 这就是"函数对象"(functor)
}

公有继承的考虑因素

在决定使用公有继承之前,问自己以下问题:

问题 如果答案是"否"
派生类是否is-a基类? 不要使用公有继承
基类是否有虚函数? 可能不需要虚析构函数
需要通过基类指针操作派生类? 考虑使用组合而非继承
基类使用了new? 派生类也需要管理内存
需要保护成员? 大多数时候用private就够了

什么时候不用继承:

cpp 复制代码
// ❌ "有一个"关系 → 用组合
class Car : public Engine { };     // 错误!
class Car { Engine engine; };      // 正确!

// ❌ 只是为了复用代码,但不是is-a关系
class Stack : private Vector { };  // 不是is-a!用组合更好

20. 类函数小结

C++类的所有函数类型一览

函数类型 说明 示例
构造函数 对象创建时调用 MyClass(int x);
拷贝构造函数 用已有对象创建新对象 MyClass(const MyClass &m);
移动构造函数(C++11) 从临时对象"偷"资源 MyClass(MyClass &&m);
析构函数 对象销毁时调用 ~MyClass();
赋值运算符 给已有对象赋值 MyClass &operator=(const MyClass &m);
友元函数 能访问类私有成员的非成员函数 friend ostream &operator<<(...);
静态成员函数 不需要对象即可调用 static int getCount();
虚函数 支持多态的函数 virtual void func();
纯虚函数 强制派生类实现的接口 virtual void func() = 0;
const成员函数 不修改对象状态的函数 int get() const;
内联函数 在类内定义的小函数 int get() { return x; }
运算符重载函数 自定义运算符行为 MyClass operator+(const MyClass &);

构造函数的初始化列表 vs 函数体赋值

cpp 复制代码
// ✅ 推荐:使用初始化列表
class MyClass {
    const int id;       // const成员必须在初始化列表中初始化
    int &ref;           // 引用成员必须在初始化列表中初始化
    std::string name;
public:
    MyClass(int i, int &r, const std::string &n)
        : id(i), ref(r), name(n) { }  // 效率高
};

// ❌ 不推荐:在函数体中赋值
class MyClass2 {
    const int id;
    std::string name;
public:
    MyClass2(int i, const std::string &n) {
        id = i;         // 错误!const成员不能赋值
        name = n;       // 效率低:先默认构造,再赋值
    }
};

const成员函数的重要性

cpp 复制代码
class Account {
    double balance;
public:
    void Deposit(double amt) { balance += amt; }        // 可以修改
    double Balance() const { return balance; }           // 不能修改
};

int main() {
    const Account ca(1000);   // 常量对象
    ca.Balance();             // ✅ const函数可以被常量对象调用
    ca.Deposit(100);          // ❌ 非const函数不能被常量对象调用
}

21. 总结

核心知识体系

复制代码
                    C++类继承知识体系
                         │
         ┌───────────────┼───────────────┐
         │               │               │
      基础概念         进阶特性         高级应用
         │               │               │
    ├── 基类定义    ├── 虚函数      ├── 抽象基类(ABC)
    ├── 派生类语法  ├── 动态联编    ├── ABC设计理念
    ├── 构造顺序    ├── protected   ├── 继承与new
    ├── 访问控制    ├── override    ├── 友元与继承
    ├── is-a关系    ├── 虚析构函数  └── 类设计回顾
    └── 指针兼容性  └── 注意事项

一句话记忆每个知识点

知识点 一句话
基类 提取共性,减少重复
派生类 在基类基础上增加新特性
构造顺序 基类→成员→自身
is-a关系 "是一种"才用公有继承
虚函数 实现"一个接口,多种实现"
动态联编 运行时决定调用哪个函数
protected 派生类友好,外部不友好
纯虚函数 强制派生类实现的接口
抽象基类 不能实例化,只定义接口
继承与new 派生类用new就必须自定义"三件套"
虚析构函数 通过基类指针删除时必须有

常见面试/考试考点

  1. 构造函数的调用顺序
  2. 虚析构函数的作用和必要性
  3. protected和private的区别
  4. 纯虚函数的语法和作用
  5. 派生类使用new时必须重写的函数
  6. 静态联编和动态联编的区别
  7. 基类指针/引用的兼容规则
  8. 切片问题(Slicing)

22. 复习题

1. 什么是公有继承?

答:公有继承表示"is-a"关系,派生类保持基类的公有接口不变。基类的public成员在派生类中仍然是public,protected成员仍然是protected。

2. 什么时候需要使用虚析构函数?

答:当通过基类指针删除派生类对象时。如果不使用虚析构函数,派生类的析构函数不会被调用,可能导致资源泄漏。

3. 构造函数可以是虚函数吗?为什么?

答:不能。因为对象在构造时还没有完全创建,vptr尚未初始化,无法进行动态联编。

4. protected和private有什么区别?

答:protected成员可以被派生类访问,private成员不能。但两者都不能被外部代码直接访问。

5. 什么是纯虚函数?什么是抽象基类?

答:纯虚函数是声明为= 0的虚函数,没有实现。包含至少一个纯虚函数的类是抽象基类,不能被实例化。

6. 为什么赋值运算符中要检查自赋值?

答:避免在 a = a 这种情况下,先把 a 的旧数据删除(delete[]),再尝试从 a 读取数据(此时数据已被删除),导致未定义行为。


23. 编程练习

练习一:简单的继承

cpp 复制代码
// 任务:创建一个Account基类和SavingsAccount派生类

#include <iostream>
#include <cstring>

class Account {
private:
    char name[50];
    double balance;
public:
    Account(const char *n = "未命名", double b = 0.0);
    void Deposit(double amt);
    virtual void Withdraw(double amt);
    virtual void Display() const;
    virtual ~Account() {}
};

class SavingsAccount : public Account {
private:
    double interestRate;
public:
    SavingsAccount(const char *n, double b, double rate);
    void ApplyInterest();    // 计算利息
    void Display() const override;
};

// 请完成以上类的实现,并在main函数中测试

练习二:ABC抽象基类

cpp 复制代码
// 任务:设计一个Shape抽象基类,派生出Circle、Rectangle

#include <iostream>
#include <cmath>

class Shape {
public:
    virtual double Area() const = 0;
    virtual void Describe() const = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r);
    double Area() const override;
    void Describe() const override;
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h);
    double Area() const override;
    void Describe() const override;
};

int main() {
    Shape *shapes[3];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);
    shapes[2] = new Circle(3.0);

    for (int i = 0; i < 3; i++) {
        shapes[i]->Describe();
        std::cout << "面积: " << shapes[i]->Area() << "\n\n";
        delete shapes[i];
    }
    return 0;
}

练习三:继承与动态内存分配

cpp 复制代码
// 任务:设计一个StringBase基类(使用char*)和StringUpper派生类(将内容转为大写)

#include <iostream>
#include <cstring>
#include <cctype>

class StringBase {
protected:
    char *str;
public:
    StringBase(const char *s = "");
    StringBase(const StringBase &other);
    StringBase & operator=(const StringBase &other);
    virtual ~StringBase();
    virtual void Show() const;
};

class StringUpper : public StringBase {
public:
    StringUpper(const char *s = "");
    StringUpper(const StringUpper &other);
    StringUpper & operator=(const StringUpper &other);
    void Show() const override;
};

int main() {
    StringBase s1("hello");
    StringUpper s2("world");

    s1.Show();     // 输出:hello
    s2.Show();     // 输出:WORLD

    // 拷贝测试
    StringUpper s3 = s2;
    s3.Show();     // 输出:WORLD

    return 0;
}

学习建议 :本章内容多且重要,建议按顺序学习,每学一个知识点就写对应的代码练习。特别是虚函数和多态部分,一定要通过大量代码示例来加深理解。继承和动态内存分配的组合是考试和面试的重点,务必掌握构造函数、赋值运算符和析构函数的正确写法。

相关推荐
YYRAN_ZZU1 小时前
如何编一个gcc的交叉工具链
linux·c++
弹简特1 小时前
【Java项目-企悦抽】15-活动管理模块02-活动创建测试与活动列表实现
java·开发语言·springboot
Frank_refuel2 小时前
【C++】手撕JRpc框架(梳理RPC流程)
c++
Joe_Wang52 小时前
【从0到1学习JVM · 25】同样都要停顿所有线程,Parallel比Serial到底强在哪
java·jvm·学习·垃圾回收
wuminyu2 小时前
JVM虚拟线程的底层实现原理分析
java·linux·c语言·jvm·c++
keyipatience2 小时前
跳表详细解析
开发语言·数据结构·c++·算法·跳表
喆星时瑜2 小时前
R语言基础语法入门
开发语言·r语言
汉克老师2 小时前
GESP2026年9月认证C++一级( 第一部分选择题(8~15题)精讲
c++·gesp·小学生·学c++编程
M78佐菲2 小时前
ARM学习笔记(9)
linux·arm开发·笔记·嵌入式硬件·学习