当前位置: 首页 > news >正文

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++ 类的成员函数 内部隐式存在的一个指针,指向当前对象自身的地址。

  1. this 是一个 指针,类型为 ClassName* const(常量指针,指向不可变,但指向的对象可变)。
  2. 用于在成员函数内部明确表示 “当前对象”。
  3. 隐藏传入,每个成员函数的调用背后,都隐含传入了 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.

http://www.dtcms.com/a/278025.html

相关文章:

  • 在 Ubuntu 下安装 MySQL 数据库
  • 从文本中 “提取” 商业洞察“DatawhaleAI夏令营”
  • 电路分析基础(02)-电阻电路的等效变换
  • Matlab批量转换1km降水数据为tiff格式
  • 【LeetCode100】--- 5.盛水最多的容器【复习回顾】
  • ssm学习笔记day05
  • QT 多线程 管理串口
  • 《[系统底层攻坚] 张冬〈大话存储终极版〉精读计划启动——存储架构原理深度拆解之旅》-系统性学习笔记(适合小白与IT工作人员)
  • springboot高校竞赛赛事管理系统 计算机毕业设计源码23756
  • Java行为型模式---策略模式
  • 第1章 概 述
  • dll文件缺失解决方法
  • C++——static成员
  • HiPPO: Recurrent Memory with Optimal Polynomial Projections论文精读(逐段解析)
  • QT控件命名简写
  • Linux内核高效之道:Slab分配器与task_struct缓存管理
  • 编译器优化——LLVM IR,零基础入门
  • 学习C++、QT---23(QT中QFileDialog库实现文件选择框打开、保存讲解)
  • 7月13日日记
  • 时间管理四象限理论
  • 小白学Python,操作文件和文件夹
  • 阶段性渗透总结
  • 第五章 Python手写数字识别【CNN卷积神经网络实现】
  • Windows怎样同步时间服务器?
  • 最简约的Windows多标签页文件管理器推荐 - 360文件夹 - 免费开源绿色软件推荐
  • Lucene原理
  • Android自定义View的事件分发流程
  • (33)记录描述窗体组件属性的枚举量 enum Qt :: WidgetAttribute, 简记为 WA_
  • Java结构型模式---外观模式
  • 和 *,以及 -> 和 .