C++八股 | Day5 | 一篇文章讲清:面向对象—封装、继承、多态 / 多重继承—菱形继承、虚继承 / 重载vs重写 / 虚函数表 / 多态的实现_含具体代码

C++面向对象编程

文章目录

  • C++面向对象编程
    • 一、面向对象编程的三大特性是:封装、继承、多态
      • 1. 封装(Encapsulation)
        • (1) 定义
        • (2) 功能
        • (3) 举例
      • 2. 继承(Inheritance)
        • (1) 定义
        • (2) 功能
        • (3) 三种继承方式
        • (4) 三种继承方式的权限
      • 3. 多态(Polymorphism)
        • (1) 定义
        • (2) 功能
        • (3) 分类
          • a. 编译时多态(静态多态)
          • b. 运行时多态(动态多态)
        • (4) 举例
      • 4. 总结
    • 二、多重继承
      • 1. 组合(有壳)
      • 2. 继承(拆壳)
      • 3. 普通多重继承(拆多个壳,各塞一份)
      • 3. 菱形继承(Diamond Inheritance)(拆同一个壳两次 ➜ 内容重复)
      • 5. 虚继承(两条路都只拿引用,让 Bat 来提供壳子)
    • 三、重载 vs 重写
      • 1. 函数重载(Overload)
      • 2. 函数重写(Override)
        • (1). 函数重写特征
        • (2). 函数重写注意事项
      • 3. 对比
    • 四、虚函数表简介
      • 1. 虚函数表
      • 2. 区分两种调用方式
    • 五、多态的实现
      • 1. 定义
      • 2. 实现
        • 1. 在基类中声明虚函数(virtual)
        • 2. 在子类中重写虚函数(override)
        • 3. 使用基类类型的指针或引用,指向子类对象
        • 4. 调用虚函数 → 会根据实际对象类型调用
        • 5. 虚函数表(vtable)的作用

一、面向对象编程的三大特性是:封装、继承、多态

访问权限:
C++ 通过关键字 publicprotectedprivate 来控制类成员(属性和方法)的访问权限:

访问权限 同类内部访问 子类访问 外部访问
public
protected
private

1. 封装(Encapsulation)

(1) 定义

将对象的状态(数据)和行为(方法)打包封装在一起,并控制对外暴露的接口,保护内部数据。

(2) 功能
  • 限制访问:防止外部直接修改内部状态,增加安全性。
  • 提高代码可维护性:如果内部结构变化,外部代码不受影响。
  • 隐藏实现细节:只暴露接口,内部如何实现对用户来说是透明的。
(3) 举例

你定义一个银行账户类,把账户余额 balance 设置为 private,提供 deposit()withdraw() 方法供外部使用,不能直接改余额,避免非法操作。

2. 继承(Inheritance)

(1) 定义

子类可以“继承”父类的属性和方法,使得子类具有父类的功能,同时还能进行功能扩展或重写。

(2) 功能
  • 代码重用:不用重复写一样的代码。
  • 扩展功能:可以对父类功能进行扩展或定制。
  • 分类清晰:方便建立一套更有层次的类结构。
(3) 三种继承方式
  1. 公有继承(public):父类的 publicprotected 成员保持权限不变继承到子类。
  2. 保护继承(protected):父类的 public 和 protected 成员都变成 protected
  3. 私有继承(private):父类所有成员在子类中都变成 private,只供内部使用。
(4) 三种继承方式的权限
父类成员权限 公有继承 class A : public B 保护继承 class A : protected B 私有继承 class A : private B
public 保持 public 变成 protected 变成 private
protected 保持 protected 保持 protected 变成 private
private 存在,但不可访问 存在,但不可访问 存在,但不可访问

private成员永远不会被子类访问到,但它仍存在于对象中(可以通过父类方法间接使用)

Base 的 private 成员会被继承,但子类看不见,也不能访问!
不过它确实存在,比如构造函数可以初始化它,Base 的成员函数也可以操作它。

举例:
基类定义:

class Base {
public:
    int pub = 1;

protected:
    int prot = 2;

private:
    int priv = 3;
};

公有继承:

class PublicDerived : public Base {
public:
    void test() {
        pub = 10;   // ✅ 可以访问(是 public)
        prot = 20;  // ✅ 可以访问(是 protected)
        // priv = 30; // ❌ 错误,private 不能访问
    }
};

void testPublic() {
    PublicDerived d;
    d.pub = 100;     // ✅ 外部也能访问
    // d.prot = 200; // ❌ 外部不能访问
}

保护继承:

class ProtectedDerived : protected Base {
public:
    void test() {
        pub = 10;   // ✅ 可以访问(继承后变 protected)
        prot = 20;  // ✅ 可以访问
        // priv = 30; // ❌ 不可以访问
    }
};

void testProtected() {
    ProtectedDerived d;
    // d.pub = 100;  // ❌ 外部无法访问(现在是 protected)
}

私有继承:

class PrivateDerived : private Base {
public:
    void test() {
        pub = 10;   // ✅ 可以访问(继承后变 private)
        prot = 20;  // ✅ 可以访问
        // priv = 30; // ❌ 不可以访问
    }
};

void testPrivate() {
    PrivateDerived d;
    // d.pub = 100; // ❌ 外部访问失败(现在是 private)
}
继承方式 父类 public 成员变成 父类 protected 成员变成 外部能否访问 pub 成员
公有继承 public protected ✅ 可以
保护继承 protected protected ❌ 不可以
私有继承 private private ❌ 不可以

3. 多态(Polymorphism)

(1) 定义

相同接口(方法名)可以表现出多种行为(即:多种“形态”)。

  • 重载实现编译时多态
  • 虚函数实现运行时多态
(2) 功能
  • 在 C++ 中,多态允许你使用一个“父类类型的指针或引用”来指向“子类对象”;
  • 当你通过这个父类指针调用方法时,运行时会根据指向的实际子类对象来决定调用哪个版本的方法
  • 换句话说,即使你写的是父类运行的是子类的实现
  • 这是“运行时多态”的核心(基于虚函数 virtual)。
(3) 分类

多态是一种允许相同的接口在不同对象上表现出不同行为的机制。根据实现时机的不同,多态分为两种:

a. 编译时多态(静态多态)

编译阶段就决定了调用哪个函数,实现方式:

函数重载(Overload)不是核心多态,不依赖继承

  • 定义:在同一个类种,存在多个同名的函数,但这些函数的参数类型或数量不同
  • 属于编译期决策,不是“继承”导致的多态,更多是一种语法特性。

举例:

class Printer {
public:
    void print(int i)         { std::cout << "int" << std::endl; }
    void print(double d)      { std::cout << "double" << std::endl; }
    void print(std::string s) { std::cout << "string" << std::endl; }
};
b. 运行时多态(动态多态)

程序运行时根据对象的实际类型决定调用哪个函数,实现方式:

函数覆盖(Override)是核心多态,虚函数+继承

  • 定义:子类重新定义父类中的虚函数(virtual);
  • 通过父类指针或引用调用时,实际执行的是子类版本
  • 必须使用 virtual 声明父类函数,建议子类使用 override 明确声明;
  • 属于继承体系下的“真正的多态”。

允许将子类类型的指针赋值给父类类型的指针,并在调用函数时表现出子类的行为。

举例:

class Base {
public:
    virtual void print() {
        std::cout << "Base" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived" << std::endl;
    }
};

Base* b = new Derived();
b->print();  // 输出: Derived(即使 b 是 Base 类型)
(4) 举例

定义一个基类 Animal 有一个 speak() 方法,猫类 Cat 和狗类 Dog 继承后各自实现不同的 speak(),当我们用 Animal* 调用时,会根据实际对象类型执行正确的方法。

4. 总结

特性 目的 关键词/机制
封装 数据保护,隐藏细节 private, public, 类
继承 复用代码,扩展功能 class 派生
多态 接口统一,行为多样 virtual, 重载等

二、多重继承

一个类可以同时继承多个父类(基类),从而获得多个父类的成员变量和成员函数。

下面,为了便于理解,这里我将类定义为壳子+内容

  • 壳子:类的名字、类型身份,如 Animal animal; → 这里的 animal 是“壳子”,你必须通过它访问内容。
  • 内容:类里的成员变量、函数(如 eat()x 等),这是我们真正要用的功能。

1. 组合(有壳)

class Animal {
public:
    void eat() {}
};

class Mammal {
public:
    Animal a;  // 这是组合
};

这里创建了一个叫 a 的“壳子”,内部装着 Animal 的功能:

Mammal m;
m.a.eat();  // ✅ 必须通过壳子访问
  • Animal 是一个 成员变量
  • 你不能直接调用 eat(),必须 m.a.eat()
  • 就像一个盒子,你得先打开盒子(壳子)才能用里面的功能

2. 继承(拆壳)

class Mammal : public Animal {};

这里不需要壳子了,直接把 Animal 的功能全塞到 Mammal 里。
所以:

Mammal m;
m.eat();  // ✅ 直接用,不需要 m.animal.eat()
  • 编译器把 Animal 的成员全部复制粘贴到 Mammal 的类里
  • 不再需要 animal 这个名字(壳子)
  • Mammal 看起来就像自己定义了 eat() 一样

3. 普通多重继承(拆多个壳,各塞一份)

class Printer {
public:
    void print() {}
};

class Scanner {
public:
    void scan() {}
};

class Copier : public Printer, public Scanner {};

Printer 的内容和 Scanner 的内容都剥掉壳子,统统塞到 Copier 里面。
所以:

Copier c;
c.print();  // ✅ 来自 Printer
c.scan();   // ✅ 来自 Scanner

结构上就像:

Copier:
- void print();  // 来自 Printer
- void scan();   // 来自 Scanner

各功能独立、不冲突,Copier 获得 PrinterScanner 两个功能模块的内容,不带壳,直接用。

3. 菱形继承(Diamond Inheritance)(拆同一个壳两次 ➜ 内容重复)

子类通过多条路径继承了同一个祖先类

class Animal {
public:
    void eat() {}
};

class Mammal : public Animal {};
class Bird   : public Animal {};

class Bat : public Mammal, public Bird {};

这里相当于从 Mammal 拆一次壳子嵌进来(内容还是之前 Animal 的内容),从 Bird 又拆一次壳子嵌进来(内容还是之前 Animal 的内容)。两段内容重复了!!!

结果:Bat 里有两个 eat() 函数!来自两个 Animal 内容副本

错误原因:

Bat b;
b.eat();  // ❌ 编译器懵了,不知道你是想用 Mammal::Animal::eat 还是 Bird::Animal::eat

5. 虚继承(两条路都只拿引用,让 Bat 来提供壳子)

class Mammal : virtual public Animal {};
class Bird   : virtual public Animal {};
class Bat : public Mammal, public Bird {};

这里不拆壳子,只要个引用!让 Bat 拆壳子,塞一份就够了,大家共享用它。

在**所有使用 virtual 继承的结构中,只有“最底层的最终派生类”负责真正拆壳子、提供内容。所以,是 Bat 负责真正创建那一份 Animal 内容!

机制解释:

  • MammalBird 都声明了 “我需要一个 Animal,但我不拆壳,我只是声明我虚继承了它”
  • 到了 Bat,它看到两个路径都需要一个虚基类 Animal,就必须自己提供一份唯一的实例
  • 编译器会在 Bat 的对象内存中只构造一次 Animal,而且所有路径都引用这个共享版本
  • 虚继承时,最底层的最终类(Bat)负责实际“拆壳子、粘内容”,其他类只是声明“我会用”

结果:

Bat:
├── Mammal(只有自己的成员,没有 Animal 内容)
├── Bird(同样没有 Animal 内容)
└── Animal(由 Bat 拆+粘 的,唯一一份内容)

也就是说:

  • MammalBird 的结构中原本该嵌入的 Animal 内容,被抽离了出来
  • 它们保留的是一个"指向虚基类的偏移指针(vbase pointer)",指向 Bat 里的那一份 Animal

所以,编译器在它们访问 eat() 的时候,底层操作是:

通过 vbase pointer → 找到 Bat 里的那份 Animal → 调用 eat()

区域 含义
Mammal 区块 自己的成员 + 虚基指针
Bird 区块 自己的成员 + 虚基指针
Animal 区块 真正构造出来的唯一一份内容
Bat b;
b.eat();  // ✅ 只存在一个 eat,没有冲突

三、重载 vs 重写

1. 函数重载(Overload)

重载是指同一个作用域中,多个函数名称相同,但参数个数不同或参数类型不同。(参数类型必须不一样)

特征:

  • 返回值可以相同或不同(但不能仅靠返回值来区分重载函数);
  • 编译器根据调用时传入的参数类型和个数来判断调用哪个函数;
  • 重载是编译时多态(静态多态)。
int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

这是典型的重载:函数名都是 add,但参数类型和返回值类型不同(一个是 int,一个是 double)。

2. 函数重写(Override)

重写是指在子类中重新定义一个和父类中同名、同参数的函数,以实现不同的功能。运行时多态(动态多态)

(1). 函数重写特征
  1. 用于继承关系中;
  2. 父类的方法必须被声明为 virtual
  3. 子类方法通常会加上 override 关键字(C++11 以后),用于确保是重写;
  4. 子类方法签名必须和父类完全一致(包括参数数量、类型、顺序、返回值类型);

还有其他可能:

情况 是否是重写? 说明
函数名相同,参数不同 ❌ 不是重写,只是“隐藏”
函数名和参数都一样,但父类不是 virtual ❌ 不是重写,只是重新定义
函数名、参数都一样,父类是 virtual ✅ 是重写(推荐加 override)
  1. 情况一:函数名相同,参数不同
    不是重写,只是 “隐藏(函数名隐藏)”
class Base {
public:
    virtual void show(int x) {
        cout << "Base::show(int)" << endl;
    }
};

class Derived : public Base {
public:
    void show() {
        cout << "Derived::show()" << endl;
    }
};

分析:

  • 子类的 show() 与父类的 show(int) 参数不同;
  • 所以这不是重写,而是函数隐藏(function hiding)
  • 即使父类是 virtual,这也不是重写
  • 此时 Derived 中的 show() 会隐藏掉父类所有同名函数(不管参数对不对);
  • 函数名一样,参数不同,会隐藏整个同名函数集。

调用效果:

Derived d;
d.show();        // ✅ 调用的是 Derived::show()
d.show(10);      // ❌ 编译错误:Derived 隐藏了 Base::show(int)
d.Base::show(10); // ✅ 你可以手动指定调用 Base 的方法
  1. 情况二:函数名、参数都一样,但父类不是 virtual
    不是重写,只是 重新定义(非虚函数覆盖)
class Base {
public:
    void show() {
        cout << "Base::show()" << endl;
    }
};

class Derived : public Base {
public:
    void show() {
        cout << "Derived::show()" << endl;
    }
};

分析:

  • Derived::show()Base::show() 签名完全一致
  • 但由于 Base::show() 不是 virtual,这不是重写
  • 是一种重新定义(redefinition)或叫静态隐藏(shadowing)
  • 函数绑定发生在编译时,不会有多态行为。

调用效果:

Base* p = new Derived();
p->show();   //  输出 Base::show(),因为没有虚函数,静态绑定

即使你用的是父类指针指向子类对象,调用的还是父类的函数,而不是子类的。

总结:

情况 名称 是否隐藏父类函数? 是否重写? 是否支持多态? 绑定类型
参数相同,父类是 virtual 重写(override) ❌ 否 ✅ 是 ✅ 是 运行时绑定(多态)
参数不同 函数名隐藏 ✅ 是 ❌ 否 ❌ 否 编译时绑定
参数相同,父类非 virtual 静态覆盖 ❌ 否(同名但没隐藏) ❌ 否 ❌ 否 编译时绑定
(2). 函数重写注意事项
  • 被重写的方法不能为private
    • (如果父类中的虚函数是 private,那么子类根本访问不到这个函数,也就无法进行“重写”)
    • 即使子类定义了一个相同名字的函数,也只是重新定义了一个新函数,并不是重写。
class Base {
private:
    virtual void print() {
        cout << "Base class" << endl;
    }
};

class Derived : public Base {
public:
    void print() override {  // ❌ 报错:父类的 print 是 private,看不到
        cout << "Derived class" << endl;
    }
};
  • 重写方法的访问修饰符一定 >= 被重写方法的访问修饰符(public > protected > private)
    • 重写方法的访问修饰符不能更严格
    • 子类重写(override)方法的访问权限可以与父类相同,也可以更高,但不能更低。
class Base {
public:
    virtual void print() {
        cout << "Base class" << endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        cout << "Derived class" << endl;
    }
};

调用:

Base* b = new Derived();
b->print(); // 输出: Derived class

因为使用了虚函数和 override,所以调用的是子类的方法,实现了多态

3. 对比

特性 重载(Overload) 重写(Override)
是否需要继承关系 是(必须有父类和子类)
函数名 相同 相同
参数列表 不同 必须相同
返回值 可以不同 一般相同(不能仅靠返回值区分)
调用方式 编译时根据参数判断调用 运行时根据对象类型判断调用
多态类型 编译时多态(静态绑定) 运行时多态(动态绑定)

四、虚函数表简介

1. 虚函数表

在 C++ 中:

  • 当一个类中含有 virtual 函数时,编译器会为它生成一个虚函数表(vtable)
  • 每个含虚函数的对象会拥有一个指向 vtable 的指针
  • 调用虚函数时,是运行时通过 vtable 查找函数地址来调用的,这就是多态的基础。

通过 vtable 模拟上述三个例子:

  1. 情况一:参数相同,父类是 virtual ➝ 正确重写
class Base {
public:
    virtual void show() { cout << "Base::show()\n"; }
};

class Derived : public Base {
public:
    void show() override { cout << "Derived::show()\n"; }
};

Base::show() 是虚函数;
Derived::show() 是合法的重写;
Derived 的 vtable 会替换掉原来指向 Base 的函数地址;

虚函数表模拟:

Base vtable:
[ show()Base::show() ]

Derived vtable:
[ show()Derived::show() ]  ✅ 替换成功

调用效果:

Base* ptr = new Derived();
ptr->show();  // 输出 Derived::show(),实现多态
  1. 情况二:参数不同 ➝ 函数名隐藏
class Base {
public:
    virtual void show(int x) { cout << "Base::show(int)\n"; }
};

class Derived : public Base {
public:
    void show() { cout << "Derived::show()\n"; }  // 参数不同
};

Base::show() 是虚函数;
Derived::show() 不是重写,因为参数不同;
所以 Derived 的 vtable 中 还是 Base::show(int),不会有 Derived 的函数;

虚函数表模拟:

Base vtable:
[ show(int)Base::show(int) ]

Derived vtable:
[ show(int)Base::show(int) ]   // 注意!Derived::show() 不在 vtable 里

调用效果:

Base* ptr = new Derived();
ptr->show(42);  // 输出 Base::show(int)

即使是 Derived 实例,调用的还是 Base 的版本,因为 Derived::show() 是新函数,不在 vtable 中。

  1. 情况三:参数相同但父类不是 virtual ➝ 静态覆盖
class Base {
public:
    void show() { cout << "Base::show()\n"; }  // 非 virtual
};

class Derived : public Base {
public:
    void show() { cout << "Derived::show()\n"; }  // 签名一致
};

父类 Base::show() 不是虚函数;
子类也定义了一个新版本,和父类名字完全一样;
没有 vtable,编译器使用静态绑定;
编译期根据指针类型决定调用哪个版本;

没有虚函数表

无 vtable,全靠编译期决定调用哪个函数

调用效果:

Base* ptr = new Derived();
ptr->show();  // 输出 Base::show(),因为是静态绑定

2. 区分两种调用方式

  1. 情况一:通过子类对象或子类指针调用

这时用的是名字查找 + 编译期绑定
隐藏就真的生效了!

Derived d;
d.show();      // ✅ 调用子类的 show()
d.show(10);    // ❌ 编译错误:Base::show(int) 被隐藏了

编译器看到你用的是 Derived,就只查找 Derived 的作用域,不会管父类有没有其他 show(int),所以你只能用 Derived::show(),不能用 Base::show(int)。

  1. 情况二:通过父类指针调用

这时如果父类的 show(int) 是 virtual,那它会进入 vtable,即使被隐藏了,也可以调用!

Base* ptr = new Derived();
ptr->show(10);  // ✅ 调用 Base::show(int),没问题

虽然 Derived 中写了一个 show(),参数不同,但它没有重写 show(int),所以 Derived 的 vtable 中仍然保留的是 Base::show(int)。

调用方式 是否受隐藏影响 会不会调用被隐藏的 Base::show(int)
Derived d; d.show(10) ✅ 会受影响 ❌ 不会,编译错误(名字找不到)
Base* p = new Derived(); p->show(10) ❌ 不受影响 ✅ 会,正常多态调用 Base::show(int)

五、多态的实现

1. 定义

C++ 的多态性是通过:虚函数(virtual function)& 虚函数表(vtable)实现的。

多态的作用:允许你使用父类类型的指针或引用指向子类对象,在运行时调用真正属于子类的函数,从而实现 “调用不变,行为多变”。

2. 实现

1. 在基类中声明虚函数(virtual)
class Shape {
public:
    virtual void draw() const {
        // 基类的默认实现
    }
};
  • 关键字 virtual 表明 draw() 是一个虚函数;
  • 这样派生类(如 Circle)就可以“重写 override”这个函数;
  • 如果后续用 Shape* 指向 Circle,依旧可以调用到正确的 Circle::draw()
2. 在子类中重写虚函数(override)
class Circle : public Shape {
public:
    void draw() const override {
        // 派生类的实现
    }
};
  • override 关键字(C++11 引入)告诉编译器:这是要重写一个父类虚函数
  • 如果签名写错了(比如少了 const、参数不一致),编译器就会报错,避免逻辑错误;
  • 虚函数 + override = 正确使用多态的前提。
3. 使用基类类型的指针或引用,指向子类对象
Shape* shapePtr = new Circle();
  • 虽然 shapePtrShape* 类型,但它实际指向的是 Circle 类型的对象;
  • 这是多态的基础:编译时看的是 Shape 类型,运行时调用的是 Circle 实现
4. 调用虚函数 → 会根据实际对象类型调用
shapePtr->draw();  
// 实际调用的是 Circle::draw()
  • 因为 draw() 是虚函数,运行时通过 vtable 查表找到正确的函数;
  • 所以尽管 shapePtrShape*,也能调用到 Circle::draw()
5. 虚函数表(vtable)的作用

编译器会为每个含有虚函数的类维护一张 虚函数表(vtable):

  • 每个对象中会隐含一个指针,指向它所属类的 vtable;
  • vtable 里存着当前对象所用的虚函数的地址;
  • 当你调用虚函数时,程序就通过这个表来“找”正确的函数地址 → 实现 运行时多态。

你可能感兴趣的:(C++八股 | Day5 | 一篇文章讲清:面向对象—封装、继承、多态 / 多重继承—菱形继承、虚继承 / 重载vs重写 / 虚函数表 / 多态的实现_含具体代码)