C++面向对象编程
一、面向对象编程的三大特性是:封装、继承、多态
访问权限:
C++ 通过关键字 public、protected 和 private 来控制类成员(属性和方法)的访问权限:
| 访问权限 | 同类内部访问 | 子类访问 | 外部访问 |
|---|---|---|---|
public | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ |
private | ✅ | ❌ | ❌ |
1. 封装(Encapsulation)
(1) 定义
将对象的状态(数据)和行为(方法)打包封装在一起,并控制对外暴露的接口,保护内部数据。
(2) 功能
- 限制访问:防止外部直接修改内部状态,增加安全性。
- 提高代码可维护性:如果内部结构变化,外部代码不受影响。
- 隐藏实现细节:只暴露接口,内部如何实现对用户来说是透明的。
(3) 举例
你定义一个银行账户类,把账户余额 balance 设置为 private,提供 deposit() 和 withdraw() 方法供外部使用,不能直接改余额,避免非法操作。
2. 继承(Inheritance)
(1) 定义
子类可以“继承”父类的属性和方法,使得子类具有父类的功能,同时还能进行功能扩展或重写。
(2) 功能
- 代码重用:不用重复写一样的代码。
- 扩展功能:可以对父类功能进行扩展或定制。
- 分类清晰:方便建立一套更有层次的类结构。
(3) 三种继承方式
- 公有继承(public):父类的
public和protected成员保持权限不变继承到子类。 - 保护继承(protected):父类的
public 和protected成员都变成protected。 - 私有继承(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 获得 Printer 和 Scanner 两个功能模块的内容,不带壳,直接用。
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 内容!
机制解释:
Mammal和Bird都声明了 “我需要一个Animal,但我不拆壳,我只是声明我虚继承了它”- 到了
Bat,它看到两个路径都需要一个虚基类Animal,就必须自己提供一份唯一的实例 - 编译器会在
Bat的对象内存中只构造一次Animal,而且所有路径都引用这个共享版本 - 虚继承时,最底层的最终类(Bat)负责实际“拆壳子、粘内容”,其他类只是声明“我会用”
结果:
Bat:
├── Mammal(只有自己的成员,没有 Animal 内容)
├── Bird(同样没有 Animal 内容)
└── Animal(由 Bat 拆+粘 的,唯一一份内容)
也就是说:
Mammal和Bird的结构中原本该嵌入的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). 函数重写特征
- 用于继承关系中;
- 父类的方法必须被声明为
virtual; - 子类方法通常会加上
override关键字(C++11 以后),用于确保是重写; - 子类方法签名必须和父类完全一致(包括参数数量、类型、顺序、返回值类型);
还有其他可能:
| 情况 | 是否是重写? | 说明 |
|---|---|---|
| 函数名相同,参数不同 | ❌ 不是重写,只是“隐藏” | |
| 函数名和参数都一样,但父类不是 virtual | ❌ 不是重写,只是重新定义 | |
| 函数名、参数都一样,父类是 virtual | ✅ 是重写(推荐加 override) |
- 情况一:函数名相同,参数不同
不是重写,只是 “隐藏(函数名隐藏)”
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 的方法
- 情况二:函数名、参数都一样,但父类不是 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 模拟上述三个例子:
- 情况一:参数相同,父类是 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(),实现多态
- 情况二:参数不同 ➝ 函数名隐藏
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 中。
- 情况三:参数相同但父类不是 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. 区分两种调用方式
- 情况一:通过子类对象或子类指针调用
这时用的是名字查找 + 编译期绑定
隐藏就真的生效了!
Derived d;
d.show(); // ✅ 调用子类的 show()
d.show(10); // ❌ 编译错误:Base::show(int) 被隐藏了
编译器看到你用的是 Derived,就只查找 Derived 的作用域,不会管父类有没有其他 show(int),所以你只能用 Derived::show(),不能用 Base::show(int)。
- 情况二:通过父类指针调用
这时如果父类的 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();
- 虽然
shapePtr是Shape*类型,但它实际指向的是Circle类型的对象; - 这是多态的基础:编译时看的是 Shape 类型,运行时调用的是 Circle 实现。
4. 调用虚函数 → 会根据实际对象类型调用
shapePtr->draw();
// 实际调用的是 Circle::draw()
- 因为
draw()是虚函数,运行时通过 vtable 查表找到正确的函数; - 所以尽管
shapePtr是Shape*,也能调用到Circle::draw()。
5. 虚函数表(vtable)的作用
编译器会为每个含有虚函数的类维护一张 虚函数表(vtable):
- 每个对象中会隐含一个指针,指向它所属类的 vtable;
- vtable 里存着当前对象所用的虚函数的地址;
- 当你调用虚函数时,程序就通过这个表来“找”正确的函数地址 → 实现 运行时多态。

被折叠的 条评论
为什么被折叠?



