C++中变量、函数存储、包括虚函数多态实现机制说明

news2025/4/19 10:27:27

从C语言转到C++开发,对于类内变量的存储,类内函数的存储,存在疑惑; 

  1. 子类如何继承父类的变量的?如果子类和父类变量同名了怎么办?
  2. C++中,函数有了作用域,类内函数,只能是这个类或对应对象才能访问,这个控制怎么实现的?
  3. 多个对象要编译多个函数实体吗?
  4. 父类和子类同名的函数如何控制?

下面就这个差异做下分析 ,一一给出答案

1、子类和父类成员变量的存储

        子类和父类的成员变量存储。 如果只有父类,那么成员变量的存储,类似于C语言的结构体,不再赘述。

        C++中,如何实现,子类继承父类的呢?

       :在 C++ 中,父类的成员变量在内存中位于子类对象的前部,而子类新增的成员变量则紧随其后。这种内存布局是继承机制的核心实现方式之一,它直接影响了通过父类指针访问成员时的行为。

可以理解为include,就是子类直接包含了父类的变量,父类的放到前面,子类在后面扩展。

这块内存,通过父类对象访问,就只能访问到父类自己管理的内存域,通过子类访问可以访问父类+子类的区域

 示例:

class Base {
public:
    int base_var = 10;  // 父类成员变量
};

class Derived : public Base {
public:
    int derived_var = 20;  // 子类新增成员变量
};

如果定义一个子类对象 Derived d,d的内存布局如下

|------------------|
| Base::base_var   |  <- 父类部分(内存起始地址)
|------------------|
| Derived::derived_var |  <- 子类新增部分
|------------------|

        当通过父类指针 Base* ptr = &d 访问时,指针实际指向的是子类对象中父类部分的起始地址。此时,编译器只能“看到”父类定义的内存范围,因此只能访问父类的成员变量,即使子类有同名变量。

关键说明:内存布局决定了先后顺序;变量访问是在编译阶段确定的,编译器默认只能寻址到变量类型自己对应的内存范围;(一般会将类编译成一个符号表,0地址是类的第一个变量,max地址是类的最后一个变量),编译器访问某个变量时候,是根据当时对象的基地址+偏移地址决定的

1.1 子类和父类成员变量访问

按照上面结论,子类会继承父类所有的成员变量,并放在内存块的前面;

访问规则:

  • 子类对象可以访问父类的变量(如果是protected 可以通过成员函数访问),如果是private则虽然在子类的内存中,但是不可以直接访问,只能通过封装的函数访问。
  • 父类指针指向子类对象,可以通过指针访问父类管理的变量,子类部分不可以访问

1.2 子类和父类同名成员变量访问

       在C++中,当父类和子类定义了同名且同类型的成员变量时,子类中实际上会存在两个独立的变量:一个属于父类,另一个属于子类。按照上一章节的解释,父类只能看到父类的变量,它们的访问规则如下:

核心结论

  1. 变量共存:子类对象中会同时存在父类和子类的同名变量,二者位于不同的内存位置。对于开发看到的虽然是两个同名变量,其实在C++内部是不同的,内部编译时候会增加类名称区分。

  2. 默认访问子类变量:在子类中直接使用变量名时,默认访问的是子类自己的变量。

  3. 显式访问父类变量:需要通过作用域解析运算符 Base:: 访问父类的变量(例如 Base::a)。

  4. 隐藏机制:子类的变量会**隐藏(hide)**父类的同名变量,但不会覆盖或删除父类的变量。

#include <iostream>

class Base {
public:
    int a;  // 父类成员变量
    Base() : a(10) {}
};

class Derived : public Base {
public:
    int a;  // 子类同名成员变量
    Derived() : a(20) {}
};

int main() {
    Derived d;
    std::cout << d.a << std::endl;          // 输出子类的a:20
    std::cout << d.Base::a << std::endl;    // 输出父类的a:10
    
    Base ptr* = &d;
    std::cout << ptr->a << std::endl;      // 输出父类的a:10
    
    return 0;
}

常见问题

1、同名变量类型不同会怎样?

   和类型相同类似,都是会共存,访问父类仍然需要通过作用域解析运算法;

2、父类变量是private类型的,子类是否可以同名?

    可以,依旧保持共存,但是子类对象是无法访问父类;

2、子类和父类成员函数分析(不考虑虚函数情况)

结论:在C++中,子类和父类的非虚成员函数的调用规则与成员变量类似,包括子类和父类如果存在完全相同的函数(入参类型也相同),因此不再具体分析子类和父类的函数访问规则,可以参照第一章的变量访问带入即可。

不过成员函数和成员变量在内部实现机制上是不同的。

  • 成员变量:父类变量在子类对象内存中位于子类新增变量之前(物理顺序)。

  • 非虚函数:函数代码本身不存储在对象内存中,而是位于代码段。所有对象共享同一份代码段,即针对一个类的成员函数,只有一个函数实体;

2.1 成员函数的编译机制

针对C++成员函数的,编译做一下简要说明

  • 非静态成员函数

编译器会隐式添加 this 指针作为第一个参数,例如:

class A {
public:
    void func(int x) { /* ... */ }  // 编译器生成 void A_func(A* this, int x)
};
  • 成员变量的访问通过 this 指针实现偏移量计算(如 this->x 编译为 *(this + offset))。

  • 调用时需要通过对象(如 obj.func(10))隐式传递 &obj 作为 this

  • 实际编译过程,会做翻译, obj.func(10)会被编译成 func(&obj,10);说明func在编译出来的代码中,也并非就是func名称,编译器会自动增加类名称,变量名称,主要用于区分不同类的同名函数,变量名称主要是为了重载时候,区分不同函数。

妙啊,怪不得在C++的面向对象编程中,不用传递对象的成员变量,原来是编译器帮我们做了封装,实际上的原理是和C语言还是一致的。


前面是C语言转C++时候总有一个疑问,为啥函数能直接访问成员变量;今日终于了解;

可以理解为,类就是一个结构体,只是定义在类内的函数,编译器编译时候会帮忙增加一个结构体指针作为参数因而才能访问结构体内的所有成员; 这个实现确实高明,减少了写代码时候的成本。

  • 静态成员函数

静态函数没有 this 指针,编译器处理方式与普通函数类似:

class A {
public:
    static void s_func(int x) { /* ... */ }  // 编译为普通函数 void A_s_func(int x)
};
  • 只能访问静态成员变量,无法直接访问非静态成员。

  • 可通过类名直接调用(如 A::s_func(10)),无需对象实例。

特性非静态成员函数静态成员函数
this 指针隐式传递
访问成员变量通过 this 指针只能访问静态成员
调用方式必须通过对象(obj.func()类名或对象均可调用
函数签名修饰包含类名(如 A::func类似普通函数
内存占用不占用对象空间不占用对象或类空间

2.2 成员函数的存储

        所有成员函数,都存储在代码段,所有的对象共享同一个函数;(和C语言的函数编译其实是一样的,只是函数名称,被编译器加工了,为了区分不同的类的同名函数,编译器会增类名称,变量类型等)。

静态绑定:非虚函数的调用在编译时根据指针或引用的静态类型(声明时的类型)确定,而非实际对象的类型。注意是在编译阶段确定需要访问的那个函数。

隐藏而非覆盖:若子类定义了与父类同名的非虚函数,父类的函数会被隐藏,但不会被覆盖。需通过作用域解析符显式调用父类版本

3、子类和父类成员函数分析(虚函数)

       通过前面的学习,可以了解到,在没有虚函数情况下,多态是没法做到的;通过父类指针只能访问父类的成员变量和成员函数;每个子类的差异无法体现,

        虚拟函数是C++里多态的核心,下面就虚函数的管理进行说明;

C++中的多态是一种允许不同类的对象通过同一接口进行访问的机制,其核心在于运行时动态绑定。以下是其实现机理的详细解析:

  • 动态多态(运行时多态):通过虚函数和继承实现,允许在运行时根据对象类型决定调用的函数。

  • 静态多态(编译时多态):通过函数重载和模板实现,在编译时确定具体调用。

3.1 虚函数Virtual  function

  • 基类使用virtual关键字声明的函数,可以在基类做默认实现,也可以不做实现。

  • 派生类根据实际需要可以可重写(override)这些函数。

多态举例

举例:Animal类里面有speak方法,声明为虚函数; 那么派生的具体的Dog和Cat子类,可以根据具体的特征,重写该函数;  比如:Dog的实现是Woof; Cat的实现是 Meow;

 示例:

#include <iostream>
using namespace std;

class Animal {
public:
    String animal ="Animal"; //没有实际用途,便于后面解释内存布局

    virtual void speak() { cout << "Animal sound" << endl; }
};

class Dog : public Animal {
public:
    String  Dog= "dog";
    
    void speak() override { cout << "Woof!" << endl; } //覆写为Woof
};

class Cat : public Animal {
public:
    String  Cat= "cat";
   
    void speak() override { cout << "Meow!" << endl; } //覆写为Meow
    virtual void display() { cout << "I am  cat" << endl; }
};

class Mouse: public Animal {
public:
    String  mouse= "mouse";

  //函数未覆写speak方法
};

int main() {
    Dog  d;
    Cat  c;
    Mouse m;

    Animal * ani =  &d;
    ani->speak();// // 动态绑定,输出Woof! 调用的是Dog类实现的方法

    ani = &c;
    ani->speak();// // 动态绑定,输出Meow! 调用的是Cat类实现的方法

    ani = &m;
    ani->speak();// // 动态绑定,输出Animal sound!  调用的是Animal的默认实现

    return 0;
}

关键问题是,如何通过基类能够访问到子类的函数,实现运行时候多态呢?

回答这个问题,首先要解释两个概念,vptr和vtable;

vptr:指向vtable的指针;

存在虚函数的基类,会有一个vptr,位置是类的第一个变量;(开发人员看不到,编译器给产生的)。

示例代码的内存布局如下。

Animal对象内存布局:
+----------------+
| Animal的vptr          | → 指向Animal的vtable 存储函数指针{Animal::speak}
| 字符串变量 animal | 
+----------------+

Dog对象内存布局:
+----------------+
| Animal的vptr           | → 指向一个新的的vtable 存储函数指针{Dog::speak}
| 字符串变量 animal | 基类成员变量在前,子类成员变量在后
| 字符串变量 dog      | dog类成员变量
+----------------+


Cat对象内存布局:
+----------------+
| Animal的vptr          | → 指向一个新的vtable 存储函数指针{cat::speak; Cat::display}
| 字符串变量 animal | 基类成员变量在前,子类成员变量在后
| 字符串变量 cat       | cat类成员变量
+----------------+


Mouse对象内存布局:
+----------------+
| Animal的vptr           | → 指向Animal的vtable 存储函数指针{Animal::speak}
| 字符串变量 animal  | 基类成员变量在前,子类成员变量在后
| 字符串变量 mouse  | mouse类成员变量
+----------------+

总结:对于单继承关系的,子类继承基类的vptr变量;(针对多层继承关系,也都只是继承基类的vptr),即从一个基类派生的所有的子类,都只是继承整个派生关系的第一个基类的vptr;

vtable:存储虚函数指针的容器

特征:

  • vtable和函数类似,vtable不占用成员变量的地址,在编译阶段生成,类似于一个全局变量;
  • vtable存在共享和覆盖;参见上面的示例,Mouse类没有覆写,因此编译器不会再生成一个vtable; Cat类,增加了自己的虚函数display; vtable会在后面追加子类的虚函数指针。

vtable和全局变量,静态变量类似,存储在数据段;

vtable是按照组管理的,上面示例中的vtable,从一个基类派生出来的子类,包括多层子类,都会放到一起,如下管理。

Vtable存储的函数指针
默认的vtable{Animal::speak}
被Dog覆写后的vtable{Dog::speak}
被Cat覆写并追加了display的vtable{cat::speak; Cat::display}

编译器是管理了,Animal类的一组vtable,这些vtable只是指针组合,如果有多个子类有相同的指针组合,编译器也只会产生一个vtable;

这种管理方式优点:

1、vtable存储在数据段,而不是类内的成员变量。 减少了每一个对象的内存消耗。

2、相同的指针组合,共享一个vtable,减少了数据段空间的内存消耗。

3.2 虚函数实现多态

vtable是在编译阶段根据虚函数指针组合生成。

动态绑定过程

  • 当通过基类指针或引用调用虚函数时,编译器生成代码:

  1. 通过对象的vptr找到其对应的vtable。
  2. 根据函数声明顺序索引vtable,获取实际函数地址。
  3. 链接目标函数。

3.3 其他说明

抽象类:包含纯虚函数的类virtual void func() = 0;,继承抽象类的子类,必须覆写纯虚函数。

多重继承:如果一个类继承了多个父类,则会包含多个vptr;但是只有第一个基类的指针具备多态特性。

class Base1 {
public:
    virtual void f1() {}
};

class Base2 {
public:
    virtual void f2() {}
};

class Derived : public Base1, public Base2 {
public:
    void f1() override {}  // 覆盖Base1::f1
    void f2() override {}  // 覆盖Base2::f2
    virtual void f3() {}   // 新增虚函数
};

Derived对象内存布局:
+----------------+
| Base1的vptr     | → 指向Derived中Base1部分的vtable(包含f1和f3)
| Base1成员数据   |
| Base2的vptr     | → 指向Derived中Base2部分的vtable(仅包含f2)
| Base2成员数据   |
| Derived成员数据 |
+----------------+

 问题1:按照上面布局,那么通过derived对象访问f2如何做到?

答:编译器在编译时候,发现是derived对象要去访问f2,会在vptr里查找,找到base2对应的vptr,并进行关系绑定。

问题2:通过base2类型的指针,是无法调用函数f3的?

答:是的,无法访问

结束语:

      在C转C++过程,对于类,继承,多态的疑惑导致设计和编码时候总是不知道什么时候该用什么,上文历时一周完成,在学习和应用过程中,针对遇到的问题,逐一进行了分析确认,对于C转C++有了一次跳跃性理解。 期望本文也能对于在C++中苦恼的各位有所帮助

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2336475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android Studio开发知识:从基础到进阶

引言 Android开发作为移动应用开发的主流方向之一&#xff0c;曾吸引了无数开发者投身其中。然而&#xff0c;随着市场饱和和技术迭代&#xff0c;当前的Android开发就业形势并不乐观&#xff0c;竞争日益激烈。尽管如此&#xff0c;掌握扎实的开发技能仍然是脱颖而出的关键。本…

ocr-身份证正反面识别

在阿里云官网&#xff0c;申请一个token [阿里官方]身份证OCR文字识别_API专区_云市场-阿里云 (aliyun.com) 观察一下post请求body部分json字符串&#xff0c;我们根据这个创建一个java对象 先默认是人像面 public class IdentityBody {public String image;class configure…

单节锂电池4.2V升压5V都有哪些国产芯片推荐?国产SL4011高效,高性价比

针对单节锂电池&#xff08;4.2V&#xff09;升压至5V应用中 SL4011升压芯片 的核心优势解析&#xff0c;结合其技术参数与典型应用场景进行详细说明&#xff1a; 1. 宽输入电压与高兼容性 输入范围&#xff1a;2.7V-12V&#xff0c;完美覆盖单节锂电池全周期电压&#xff08;3…

机器学习 | 神经网络介绍 | 概念向

文章目录 &#x1f4da;从生物神经元到人工神经元&#x1f4da;神经网络初识&#x1f407;激活函数——让神经元“动起来”&#x1f407;权重与偏置——调整信息的重要性&#x1f407;训练神经网络——学习的过程&#x1f407;过拟合与正则化——避免“死记硬背” &#x1f440…

视频孪生重构施工逻辑:智慧工地的数字化升级

当"智慧工地"概念在2017年首次写入《建筑业发展"十三五"规划》时&#xff0c;行业普遍将其等同于摄像头与传感器的简单叠加。十年数字浪潮冲刷下&#xff0c;智慧工地的内涵已发生本质跃迁&#xff1a;从工具层面的信息化改造&#xff0c;进化为基于视频数…

六根觉性:穿透表象的清净觉知之光

在喧嚣的禅堂里&#xff0c;老禅师轻叩茶盏&#xff0c;清脆的声响划破沉寂。这声"叮"不仅震动耳膜&#xff0c;更叩击着修行者的心性——这正是佛教揭示的六根觉性在世间万相中的妙用。当我们凝视《楞严经》中二十五圆通法门&#xff0c;六根觉性犹如六道澄明之光&a…

spring:注解@Component、@Controller、@Service、@Reponsitory

背景 spring框架的一个核心功能是IOC&#xff0c;就是将Bean初始化加载到容器中&#xff0c;Bean是如何加载到容器的&#xff0c;可以使用spring注解方式或者spring XML配置方式。 spring注解方式直接对项目中的类进行注解&#xff0c;减少了配置文件内容&#xff0c;更加便于…

Halcon应用:九点标定-手眼标定

提示&#xff1a;若没有查找的算子&#xff0c;可以评论区留言&#xff0c;会尽快更新 Halcon应用&#xff1a;九点标定-手眼标定 前言一、Halcon应用&#xff1f;二、应用实战1、图形理解[eye-to-hand]&#xff1a;1.1、开始应用2 图形理解[eye-in-hand] 前言 本篇博文主要用…

【iOS】OC高级编程 iOS多线程与内存管理阅读笔记——自动引用计数(一)

自动引用计数 前言alloc/retain/release/dealloc实现苹果的实现 autoreleaseautorelease实现苹果的实现 总结 前言 此前&#xff0c;写过一遍对自动引用计数的简单学习&#xff0c;因此掠过其中相同的部分&#xff1a;引用计数初步学习 alloc/retain/release/dealloc实现 由于…

Python爬虫第15节-2025今日头条街拍美图抓取实战

目录 一、项目背景与概述 二、环境准备与工具配置 2.1 开发环境要求 2.2 辅助工具配置 三、详细抓取流程解析 3.1 页面加载机制分析 3.2 关键请求识别技巧 3.3 参数规律深度分析 四、爬虫代码实现 五、实现关键 六、法律与道德规范 一、项目概述 在当今互联网时代&a…

智慧城市像一张无形大网,如何紧密连接你我他?

智慧城市作为复杂巨系统&#xff0c;其核心在于通过技术创新构建无缝连接的网络&#xff0c;使物理空间与数字空间深度融合。这张"无形大网"由物联网感知层、城市数据中台、人工智能中枢、数字服务入口和安全信任机制五大支柱编织而成&#xff0c;正在重塑城市运行规…

网络安全·第四天·扫描工具Nmap的运用

今天我们要介绍网络安全中常用的一种扫描工具Nmap&#xff0c;它被设计用来快速扫描大型网络&#xff0c;主要功能包括主机探测、端口扫描以及版本检测&#xff0c;小编将在下文详细介绍Nmap相应的命令。 Nmap的下载安装地址为&#xff1a;Nmap: the Network Mapper - Free Se…

黑龙江 GPU 服务器租用:开启高效计算新征程

随着人工智能、深度学习、大数据分析等技术的广泛应用&#xff0c;对强大计算能力的需求日益迫切。GPU 服务器作为能够提供卓越并行计算能力的关键设备&#xff0c;在这一进程中发挥着至关重要的作用。对于黑龙江地区的企业、科研机构和开发者而言&#xff0c;选择合适的 GPU 服…

SparseDrive---论文阅读

纯视觉下的稀疏场景表示 算法动机&开创性思路 算法动机&#xff1a; 依赖于计算成本高昂的鸟瞰图&#xff08;BEV&#xff09;特征表示。预测和规划的设计过于直接&#xff0c;没有充分利用周围代理和自我车辆之间的高阶和双向交互。场景信息是在agent周围提取&#xff…

Unchained 内容全面上链,携手 Walrus 迈入去中心化媒体新时代

加密新闻媒体 Unchained — — 业内最受信赖的声音之一 — — 现已选择 Walrus 作为其去中心化存储解决方案&#xff0c;正式将其所有媒体内容&#xff08;文章、播客和视频&#xff09;上链存储。Walrus 将替代 Unchained 现有的中心化存储架构&#xff0c;接管其全部历史内容…

确保连接器后壳高性能互连的完整性

本文探讨了现代后壳技术如何促进高性能互连的电气和机械完整性&#xff0c;以及在规范阶段需要考虑的一些关键因素。 当今的航空航天、国防和医疗应用要求连接器能够提供高速和紧凑的互连&#xff0c;能够承受振动和冲击&#xff0c;并保持对电磁和射频干扰 &#xff08;EMI/R…

C++学习Day0:c++简介

目录 一、.C语言的发展史二、C特点三、面向对象的重要术语四、面向过程和面向对象的区别&#xff1f;五、开发环境&#xff1a;六、创建文件步骤&#xff1a;1.点击新建项目2.在弹出的开始栏中按如下操作3.在.pro文件中添加&#xff08;重要&#xff01;&#xff01;&#xff0…

从零开始构建 Ollama + MCP 服务器

Model Context Protocol&#xff08;模型上下文协议&#xff09;在过去几个月里已经霸占了大家的视野&#xff0c;出现了许多酷炫的集成示例。我坚信它会成为一种标准&#xff0c;因为它正在定义工具与代理或软件与 AI 模型之间如何集成的新方式。 我决定尝试将 Ollama 中的一…

MATLAB学习笔记(二) 控制工程会用到的

MATLAB中 控制工程会用到的 基础传递函数表达传递函数 零极点式 状态空间表达式 相互转化画响应图线根轨迹Nyquist图和bode图现控部分求约旦判能控能观极点配置和状态观测 基础 传递函数表达 % 拉普拉斯变换 syms t s a f exp(a*t) %e的a次方 l laplace(f) …

C++ 线程间通信开发从入门到精通实战

C 线程间通信开发从入门到精通实战 在现代软件开发中&#xff0c;多线程程序已成为提升应用性能、实现并行处理的重要手段。随着多核处理器的普及和复杂应用需求的增加&#xff0c;C作为一门高性能的编程语言&#xff0c;在多线程开发中扮演着不可或缺的角色。然而&#xff0c…