C++面向对象三大特性

前言 :C++ 是一门支持面向对象编程(OOP)的语言,其三大特性------封装、继承、多态,是构建高内聚、低耦合、可扩展软件的基石。本文将从概念到实现,结合代码详细讲解每一个特性,并深入剖析多态的底层机制(vptr、vtable、虚析构原理),帮助你彻底理解 C++ 的 OOP 精髓。


一、封装(Encapsulation)

封装 是将数据(属性)和操作数据的方法(函数)捆绑在一起,并隐藏内部实现细节,仅对外暴露必要的接口。

1.1 为什么需要封装?

  • 安全性防止外部 代码随意修改对象内部状态

  • 模块化 :使用者只需关心接口,不依赖内部实现。

  • 维护性 :内部逻辑可以自由修改,只要接口不变,外部代码不受影响

1.2 访问控制修饰符

修饰符 类内部 派生类 类外部
private
protected
public

1.3 代码示例:银行账户

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class BankAccount {
private:
    double balance;          // 私有成员,外部无法直接访问
    string password;         // 密码也隐藏起来

public:
    BankAccount(string pwd, double init = 0.0) 
        : password(pwd), balance(init) {}

    // 公开的存款接口
    void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    // 公开的取款接口(内部验证密码)
    bool withdraw(string pwd, double amount) {
        if (pwd != password) return false;
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }

    // 只读查询余额(不暴露修改能力)
    double getBalance(string pwd) const {
        if (pwd == password) return balance;
        return -1;
    }
};

int main() {
    BankAccount acc("1234", 1000);
    // acc.balance = 9999;   // 错误:private 成员不可访问
    acc.deposit(500);
    acc.withdraw("1234", 200);
    cout << "余额: " << acc.getBalance("1234") << endl; // 输出 1300
    return 0;
}

封装的好处 :银行账户的余额只能通过公开的 deposit / withdraw 修改,无法直接篡改。密码验证也隐藏在内部,外部无需关心。


二、继承(Inheritance)

继承 允许一个类(子类/派生类)获得另一个类(父类/基类)的成员(属性和方法),并可以增加新的功能或重写已有的功能。

2.1 继承的类型

  • 实现继承:派生类直接复用基类的实现代码。

  • 接口继承:派生类只继承基类的函数声明(纯虚函数),必须自己实现。

  • 可视继承 :主要与 GUI相关,一般指子类继承父类的界面外观。

2.2 继承的访问控制

继承方式 基类 public 基类 protected 基类 private
public 继承 仍是 public 仍是 protected 不可访问
protected 继承 变成 protected 变成 protected 不可访问
private 继承 变成 private 变成 private 不可访问

实际开发中,public 继承最常用

2.3 代码示例:交通工具

cpp 复制代码
#include <iostream>
using namespace std;

// 基类
class Vehicle {
protected:
    string brand;
    int speed;
public:
    Vehicle(string b, int s = 0) : brand(b), speed(s) {}
    void accelerate(int delta) { speed += delta; }
    void show() const {
        cout << brand << " 当前速度: " << speed << " km/h" << endl;
    }
};

// 派生类 Car(public 继承)
class Car : public Vehicle {
private:
    int doors;
public:
    Car(string b, int d, int s = 0) : Vehicle(b, s), doors(d) {}
    void honk() const { cout << "嘟嘟!" << endl; }
    // 可以重写 show 函数
    void show() const {
        Vehicle::show();        // 调用基类 show
        cout << "车门数: " << doors << endl;
    }
};

int main() {
    Car myCar("Tesla", 4, 50);
    myCar.accelerate(30);      // 继承自 Vehicle
    myCar.honk();               // Car 自己的方法
    myCar.show();               // 重写的方法
    return 0;
}

继承的好处Car 复用了 Vehiclebrandspeedaccelerate避免重复代码 ,并且可以扩展新功能(honk、增加车门数)。


三、多态(Polymorphism)

多态的意思是"同一个接口,不同的实现"。C++ 支持两种形式的多态:

  • 编译时多态(静态多态) :在编译阶段确定调用哪个函数。包括函数重载运算符重载模板

  • 运行时多态(动态多态) :在程序运行时根据对象实际类型决定调用哪个函数。通过虚函数和继承实现。

3.1 编译时多态

(1) 函数重载

同名函数,参数列表不同(类型、个数或顺序),编译器根据实参选择合适版本。

cpp 复制代码
#include <iostream>
using namespace std;

void print(int i) { cout << "整数: " << i << endl; }
void print(double d) { cout << "浮点数: " << d << endl; }
void print(string s) { cout << "字符串: " << s << endl; }

int main() {
    print(42);      // 调用 print(int)
    print(3.14);    // 调用 print(double)
    print("Hello"); // 调用 print(string)
    return 0;
}
(2) 运算符重载

允许自定义类型使用 +、-、[] 等运算符。

cpp 复制代码
#include <iostream>
using namespace std;

class Vector2D {
public:
    int x, y;
    Vector2D(int x = 0, int y = 0) : x(x), y(y) {}
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }
};

int main() {
    Vector2D v1(1, 2), v2(3, 4);
    Vector2D v3 = v1 + v2;
    cout << v3.x << ", " << v3.y << endl; // 输出 4, 6
    return 0;
}
(3) 模板(泛型编程)

模板实现了"编译时多态"的另一种形式:同一段代码可以操作不同数据类型。

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    cout << max(3, 7) << endl;        // int
    cout << max(3.14, 2.71) << endl;  // double
    cout << max('a', 'c') << endl;    // char
    return 0;
}

模板的实例化在编译 时完成,编译器根据调用参数生成不同版本函数,因此没有运行时开销


3.2 运行时多态(虚函数)

运行时多态是面向对象最核心的机制:用基类指针或引用指向派生类对象,调用虚函数时会执行派生类的版本

3.2.1 虚函数基本示例
cpp 复制代码
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() const {   // 声明为虚函数
        cout << "Animal makes a sound." << endl;
    }
    virtual ~Animal() {}           // 虚析构函数(后面详解)
};

class Dog : public Animal {
public:
    void speak() const override {  // override 表示重写
        cout << "Dog barks: Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        cout << "Cat meows: Meow!" << endl;
    }
};

// 多态函数:接受基类指针,调用实际对象版本的 speak()
void makeSound(const Animal* a) {
    a->speak();
}

int main() {
    Dog d;
    Cat c;
    makeSound(&d);   // 输出 Dog barks: Woof!
    makeSound(&c);   // 输出 Cat meows: Meow!
    return 0;
}

为什么需要 virtual

如果没有 virtualmakeSound 会根据编译时类型(Animal*)调用 Animal::speak,永远输出"Animal makes a sound."。virtual 让调用在运行时动态决定


3.2.2 虚函数的底层原理:vptr 和 vtable(彻底详解)

这是理解多态的关键。我们先从内存布局入手。

① 没有虚函数时的对象

cpp 复制代码
class Animal {
    int age;
public:
    void eat() {}
};

sizeof(Animal) 在 32 位系统下是 4 字节(只有 age)。普通成员函数不占用对象内存,它们像普通函数一样存在代码段。

② 引入一个虚函数

cpp 复制代码
class Animal {
    int age;
public:
    virtual void speak() {}
};

现在 sizeof(Animal) 在 32 位系统下通常是 8 字节int age 4字节 + 一个隐藏指针 4字节)。这个隐藏指针称为 vptr (虚指针)。

在 64 位系统下,指针是 8 字节,所以对象大小可能是 16 字节(对齐后)。

③ 每个类都有自己的虚函数表(vtable)

编译器在编译时,会为每个包含虚函数的类 生成一张 虚函数表(vtable)vtable 是一个数组 ,里面存储了该类的所有虚函数的函数指针

  • Animal 的 vtable:包含 &Animal::speak

  • Dog 的 vtable:包含 &Dog::speak

  • Cat 的 vtable:包含 &Cat::speak

④ 每个对象都有一个 vptr,指向它所属类的 vtable

cpp 复制代码
Animal a;   // 对象 a 的 vptr 指向 Animal 的 vtable
Dog d;      // 对象 d 的 vptr 指向 Dog 的 vtable

内存布局示意图(32位系统):

cpp 复制代码
Animal 对象(无虚函数):
+-------+
|  age  |  4字节
+-------+

Animal 对象(有虚函数):
+-------+-------+
| vptr  |  age  |  8字节(vptr 在对象开头)
+-------+-------+
   |
   +---------> Animal 的 vtable:
               +----------------+
               | &Animal::speak |
               +----------------+

Dog 对象:
+-------+-------+
| vptr  |  age  |  (继承自 Animal 的成员)
+-------+-------+
   |
   +---------> Dog 的 vtable:
               +--------------+
               | &Dog::speak  |
               +--------------+

⑤ 动态绑定的具体过程

当通过基类指针调用虚函数时,比如:

cpp 复制代码
Animal* p = new Dog();
p->speak();

编译器生成的代码大致等价于:

cpp 复制代码
// 1. 从 p 指向的对象中取出 vptr
void** vptr = *(void***)p;
// 2. 从 vtable 中取出 speak 函数的地址(假设 speak 是 vtable 的第一个条目)
void* func = vptr[0];
// 3. 调用该函数
((void(*)())func)();

因为 p 指向的实际对象是 Dog ,所以它的 vptr 指向 Dog 的 vtable ,因此调用的是 Dog::speak

关键总结

概念 说明
vtable(虚函数表) 类级别的 ,每个有虚函数的类都有一张表,存储虚函数地址
vptr(虚指针) 对象级别 的,每个对象都有一个隐藏的 vptr指向所属类的 vtable
动态绑定过程 通过 vptr 找到 vtable ,再通过偏移取出函数地址并调用。
性能开销 一次间接寻址(比普通函数调用稍慢),但通常可以忽略。

3.2.3 overridefinal 关键字
  • override:显式声明函数重写基类的虚函数,如果签名不匹配会编译报错,避免笔误。

  • final:禁止派生类继续重写该虚函数,或者禁止类被继承。

cpp 复制代码
class Base {
    virtual void foo();
    virtual void bar() final;   // 不能在派生类中重写
};

class Derived : public Base {
    void foo() override;        // 正确
    // void bar() override {}    // 错误:bar 是 final
};

3.2.4 虚析构函数:为什么基类析构函数必须是虚函数?(彻底讲透)

先看一个错误的例子:

cpp 复制代码
#include <iostream>
using namespace std;

class Base {
public:
    ~Base() { cout << "Base destructor" << endl; }   // 非虚
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() { 
        delete[] data; 
        cout << "Derived destructor" << endl; 
    }
};

int main() {
    Base* p = new Derived();
    delete p;   // 只调用 Base::~Base()
    return 0;
}

运行结果:

cpp 复制代码
Base destructor

Derived 的析构函数没有被调用 !这导致了 data 指向的堆内存泄漏

为什么会这样?

  • delete p 时,编译器看到**pBase* 类型** 。如果 Base 的析构函数不是虚函数 ,那么编译器就静态绑定 ,直接调用 Base::~Base()

  • 它不会去查找实际对象类型(Derived),因此 Derived 的析构函数不会执行。

解决方案 :将基类析构函数声明为 virtual

cpp 复制代码
class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

修改后运行结果:

cpp 复制代码
Derived destructor
Base destructor

为什么 virtual 就能调用子类析构?

  • 析构函数也是虚函数。当一个类有虚函数时,它的对象就有 vptr

  • delete p 时,编译器生成代码:通过 pvptr 找到虚函数表,从表中取出析构函数的地址(也是动态绑定)。

  • 由于 p 实际指向 Derived 对象,vptr 指向 Derived 的 vtable,所以调用的是 Derived::~Derived()

  • Derived 的析构函数执行完毕后,会自动调用基类析构函数(这是 C++ 保证的)。

虚析构函数的规则

  • 只要一个类会被作为基类使用,就应该将其析构函数声明为 virtual

  • 如果一个类没有虚函数(不可能被继承),析构函数不需要虚析构。

  • 抽象类的析构函数也应该是虚函数


3.3 模板多态 vs 运行时多态

特性 模板多态(编译时) 虚函数多态(运行时)
绑定时机 编译期 运行期
性能 无额外开销(内联友好) 一次间接寻址,稍微慢一点
灵活性 类型必须编译期确定 可以在运行时动态决定对象类型
代码膨胀 可能产生多份实例化代码 无额外膨胀
适用场景 高性能、类型严格、无继承的泛型 多态容器、插件架构、框架设计

3.4 常见面试追问与扩展

Q1:内联函数可以是虚函数吗?

:可以,但内联请求会被忽略 。因为内联是在编译期展开函数体,而虚函数调用是运行时动态绑定,两者矛盾。不过如果通过对象(而非指针/引用)调用虚函数,编译器可能静态解析并内联。

Q2:静态成员函数可以是虚函数吗?

:不能。静态成员函数属于类,没有 this 指针,无法参与动态绑定。

Q3:构造函数可以是虚函数吗?

:不能。构造函数执行时,对象的vptr 还未正确设置(还没完全构造完成),无法进行动态绑定。

Q4:一个类最多有几个虚函数表?

单继承时有一个 ;多继承时,每个直接或间接基类如果含有虚函数,就会有自己的 vtable 子表,实际编译器可能会生成多个 vtable 或一个包含多个部分的大表

Q5:虚函数表存放在哪里?

:通常是只读数据段.rodata 或类似),不是堆也不是栈。所有对象共享同一张表


四、三大特性协同工作示例

最后,用一个完整的例子展示封装、继承和多态如何配合。

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// 抽象基类(接口)
class Shape {
public:
    virtual double area() const = 0;   // 纯虚函数
    virtual ~Shape() = default;        // 虚析构
};

// 派生类:Rectangle
class Rectangle : public Shape {
private:        // 封装
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override {
        return width * height;
    }
};

// 派生类:Circle
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

// 多态函数
void printArea(const Shape& s) {
    cout << "面积: " << s.area() << endl;
}

int main() {
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Rectangle>(4, 5));
    shapes.push_back(make_unique<Circle>(3));

    for (const auto& sp : shapes) {
        printArea(*sp);   // 运行时多态调用正确的 area()
    }
    return 0;
}
  • 封装Rectanglewidthheight 私有,外部只能通过构造函数或接口间接使用。

  • 继承RectangleCircle 继承了 Shape 接口,并实现了纯虚函数。

  • 多态printArea 接受基类引用 ,调用虚函数 area 动态分发到具体类版本。


五、总结

特性 核心作用 实现方式
封装 隐藏内部数据,提供安全接口 private / protected 成员
继承 复用代码,建立 is-a 关系 class Derived : public Base
多态 同一接口,不同行为(扩展性) 虚函数(运行时),模板/重载(编译时)

理解虚函数表(vtable)和虚指针(vptr)是掌握多态的关键

  • 每个有虚函数的类 都有一张 vtable

  • 每个对象 都有一个 vptr 指向 vtable

  • 通过 vptr 间接调用虚函数实现了运行时动态绑定。

  • 基类析构函数必须是虚函数 ,否则派生类资源无法正确释放。

希望本文能帮助你彻底理解 C++ 的封装、继承、多态,并在实际开发中灵活运用。

相关推荐
驭渊的小故事1 小时前
java中的进程的详细解析
java·开发语言
烟雨江南aabb1 小时前
Python第六弹:python爬虫篇:什么是爬虫
开发语言·爬虫·python
沐知全栈开发1 小时前
Servlet 文件上传详解
开发语言
无限进步_1 小时前
【C++】C++11的类功能增强与STL变化
java·前端·数据结构·c++·后端·算法
小鱼️遨游2 小时前
openCPU SDK 安装和第一次编译方法、注意事项
c++·opencpu·ml307
basketball6162 小时前
C++ iostream 完全指南:从 cin/cout 到流式编程的奥秘
开发语言·c++
SilentSamsara2 小时前
运算符重载:让自定义对象支持 +、[]、in 操作
开发语言·python·算法·青少年编程·pycharm
threelab2 小时前
Three.js 3D 热力图效果 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
Royzst2 小时前
图书管理案例
java·开发语言