C++封装、多态、继承
C++封装、多态、继承
C++封装
封装的定义与目的:把数据和方法统一组织在类中,对外隐藏内部实现,隐藏实现细节- 提供稳定接口- 保证数据有效性- 降低耦合性,提高维护性。
封装的实现机制:类作用域、访问控制符、this指针
类作用域
作用域:代码中名字(变量、函数等)可被识别和使用的有效范围。
在 C++ 中,常见作用域有:{全局作用域、命名空间作用域、函数作用域、块作用域、类作用域}
类的作用域控制了:类内部成员的名字有效范围、成员访问权限上下文
补充:
如果将类指针强转,则也可以访问到public数据。
访问控制符
虽然类作用域统一包含了所有成员,但访问控制符将它分成了不同“访问级别的区域”,
常见的访问控制符如下:
访问控制符 | 类内访问 | 子类访问 | 类外访问 |
private | 允许 | 禁止 | 禁止 |
protected | 允许 | 允许 | 禁止 |
public | 允许 | 允许 | 允许 |
访问控制符(private / protected / public)的底层逻辑:
访问控制符是 “编译器层面的语义标签”,它不影响内存,不存在运行时信息,也不会生成额外的机器指令。
编译器经历如下处理流程:源代码 -> 词法分析 -> 语法分析 -> 抽象语法树(AST) -> 语义分析 -> 生成中间表示(IR)
访问控制实现阶段:语法分析 + 语义分析阶段
编译器构建 AST(抽象语法树)时,会为每个类成员生成节点,并在节点上打上访问标签。
示例:
CXXRecordDecl MyClass |- FieldDecl x (access: private) |- FieldDecl y (access: protected) |- FieldDecl z (access: public) |- CXXMethod setX (access: public) |
当编译器在解析类体的时候,它会维护一个 currentAccessSpecifier,一开始是 private(因为 class 默认是 private)。
遇到:private,它会更新 currentAccessSpecifier = AS_private
遇到:public,会更新 currentAccessSpecifier = AS_public
遇到:protected,会更新 currentAccessSpecifier = AS_ protected
然后,编译器接下来遇到的所有成员声明,就会统一打上当前 access 标记。
访问控制符不会影响内存结构,无论成员是 public / protected / private,其在类对象中所占用的内存空间是完全一样的。
this指针
this 指针是 C++ 类的成员函数 内部隐式存在的一个指针,指向当前对象自身的地址。
- this 是一个 指针,类型为 ClassName* const(常量指针,指向不可变,但指向的对象可变)。
- 用于在成员函数内部明确表示 “当前对象”。
- 隐藏传入,每个成员函数的调用背后,都隐含传入了 this 指针。
示例:
class Person { private: int age; public: void setAge(int a) { this->age = a; } }; |
编译器底层会转成类似这样:
void setAge(Person* this, int a) { this->age = a; } |
C++继承
继承:子类(派生类)复用父类(基类)的成员,形成 “is-a” 的关系。
{
注解:
is-a 表示“X 是一种 Y”。
is-a 的本质:子类“属于”父类这个大分类、子类对象可以隐式转换为父类对象指针 / 引用、 子类对象前半部分布局 == 父类布局(子类对象开头就是父类对象,这就是指针兼容性的硬逻辑依据)
}
如果想要运行时多态,继承是必要前提。
继承的三种访问控制
继承方式 | 父类 public 成员 | 父类 protected 成员 | 父类 private 成员 |
public | 保持 public | 变成 protected | 访问不到 |
protected | 变成 protected | 变成 protected | 访问不到 |
private | 变成 private | 变成 private | 访问不到 |
子类对象的内存模型
class Base { int a; }; class Derived : public Base { int b; }; |
对象内存布局:
Derived 对象:
[ Base::a ][ Derived::b ],父类成员在子类对象中有“物理存在”!这就是为什么子类对象可以当父类对象用(对象模型兼容性)。
this 指针与继承
Derived d; d.breathe(); 内部等价于: Animal::breathe(&d); // this 指向 Derived 对象,但用父类部分去访问 |
this 指针可以自动向上转为父类类型。这也是为什么多态里 virtual 必须基于指针或引用。
底层实现:对象布局与虚表
普通继承:没有虚表,仅通过偏移访问成员。
Derived d; Base* p = &d; p->breathe(); // 编译期已确定,直接偏移访问成员 |
多态(virtual 函数):
编译器在对象头部插入 vptr(虚表指针):
Derived 对象: [ vptr ][ Base::a ][ Derived::b ] |
vptr 指向 vtable(虚表),vtable 记录所有虚函数地址
C++多态
概念
多态(Polymorphism):同一个接口,不同对象有不同表现。
用统一的父类指针 / 引用,调用不同子类的实现。
动态绑定:运行时决定调用哪一个函数(虚函数机制)。
多态 = vptr 指向 vtable,运行时根据对象真实类型查表找函数
class Animal { public: virtual void speak() { std::cout << "Animal speaks" << std::endl; } }; class Dog : public Animal { public: void speak() override { std::cout << "Dog barks" << std::endl; } }; Animal* p = new Dog(); p->speak(); // 输出:Dog barks |
如果没有 virtual,输出就永远是 Animal speaks。
静态绑定vs动态绑定
类型 | 绑定时间 | 依赖条件 | 效果 |
静态绑定 | 编译期 | 成员函数(非虚函数) | 快,无灵活性 |
动态绑定 | 运行期 | 虚函数 + 指针 / 引用 | 灵活,代价更高 |
为什么用指针 / 引用?
只有指针 / 引用才能脱离对象实际类型,让编译器 defer 到运行时决定实际类型。
底层实现原理
本质:虚表(vtable)+ 虚表指针(vptr)
步骤:
1.编译器生成 vtable
类名 | vtable 内容 |
Animal | &Animal::speak |
Dog | &Dog::speak |
2. 对象布局插入 vptr
Dog d; [ vptr -> Dog::vtable ] [ Animal::成员 ] [ Dog::成员 ] |
3. 调用过程拆解
p->speak(); 调用过程等价于 (*(p->vptr)[0])(p); |
图解结构:
Dog 对象: ┌──────────┐ │ vptr ─────────┐ └──────────┘ │ ↓ ┌────────────┐ │ vtable │ │ ─────── │ │ speak ──┐ │ └────────┬┘ │ │ │ │ │ ┌───┴───┴────────────┐ │ void Dog::speak() │ └────────────────────┘ |
this 指针与多态的结合
void Dog::speak() { std::cout << "Dog says, this->type = " << this->type << std::endl; } 本质: void speak(Dog* this) { ... } |
虚表继承规则
行为 | 结果 |
子类未重写虚函数 | 继承父类 vtable 项 |
子类重写虚函数 | 重写覆盖 vtable 对应项 |
子类新增虚函数 | vtable 追加新项 |
虚表多态 vs 非虚函数
场景 | 调用行为 |
非虚函数(静态绑定) | 直接根据类型偏移确定地址 |
虚函数(动态绑定) | 运行时查 vtable 决定地址 |
多态的条件总结
必须条件 | 理由 |
虚函数 | 没有虚表就没有动态绑定 |
指针 / 引用 | 否则编译期已确定类型 |
派生类重写 | 否则子类不会改变行为 |
额外知识:vptr 位置
编译器 | vptr 插入位置 |
MSVC | 对象头部第一个字节 |
GCC | 对象头部第一个成员位置(隐式) |
多态的核心底层模型总结
多态成立条件 | 底层逻辑 |
有虚函数 | 产生虚表,vptr 指向 |
指针 / 引用 | 通过 vptr 查虚表调用 |
重写函数 | 更新虚表中的函数地址 |
易错与误解总结
错误理解 | 正确理解 |
虚表存在于类 | vtable 存在于类,vptr 存在对象 |
子类一定有新表 | 没有重写就共享父类表 |
无指针也有多态 | 多态只有指针 / 引用才有效 |
示例:
#include <iostream> using namespace std; // ----------- 父类 ----------- class Animal { public: string name = "Animal"; virtual void speak() { // 虚函数,多态基础 cout << "Animal speaks!" << endl; } void breathe() { // 非虚函数 cout << "Animal breathes." << endl; } }; // ----------- 子类 ----------- class Dog : public Animal { public: string breed = "Bulldog"; void speak() override { // 重写虚函数 cout << "Dog barks!" << endl; } void fetch() { cout << "Dog fetches!" << endl; } }; // ----------- 使用父类指针演示多态 ----------- void animalSpeak(Animal* a) { a->speak(); // 多态,运行时动态绑定 a->breathe(); // 非虚函数,静态绑定 } int main() { Dog d; cout << "直接调用子类对象:" << endl; d.speak(); // Dog::speak,静态绑定 d.breathe(); // Animal::breathe,静态绑定 d.fetch(); // Dog::fetch,静态绑定 cout << "----------" << endl; cout << "父类指针指向子类对象,演示多态:" << endl; Animal* p = &d; p->speak(); // 动态绑定 -> Dog::speak p->breathe(); // 静态绑定 -> Animal::breathe cout << "----------" << endl; cout << "通过统一接口调用:" << endl; animalSpeak(&d); } |
输出:
直接调用子类对象: Dog barks! Animal breathes. Dog fetches! ---------- 父类指针指向子类对象,演示多态: Dog barks! Animal breathes. ---------- 通过统一接口调用: Dog barks! Animal breathes. |