96SEO 2026-02-20 07:36 0
语言中直接支持面向对象程序设计的部分。

对于各种支持的底层实现机制。
直接支持面向对象程序设计包括了构造函数、析构函数、多态、虚函数等等这些内容在很多书籍上都有讨论也是C最被人熟知的地方特性。
而对象模型的底层实现机制却是很少有书籍讨论的。
对象模型的底层实现机制并未标准化不同的编译器有一定的***来设计对象模型的实现细节。
在我看来对象模型研究的是对象在存储上的空间与时间上的更优并对C面向对象技术加以支持如以虚指针、虚表机制支持多态特性。
这篇文章主要来讨论C对象在内存中的布局属于第二个概念的研究范畴。
而C直接支持面向对象程序设计部分则不多讲。
文章主要内容如下
虚函数表解析。
含有虚函数或其父类含有虚函数的类编译器都会为其添加一个虚函数表,vptr先了解虚函数表的构成有助对C对象模型的理解。
虚基类表解析。
虚继承产生虚基类表(vbptr)虚基类表的内容与虚函数表完全不同我们将在讲解虚继承时介绍虚函数表。
对象模型概述介绍简单对象模型、表格驱动对象模型以及非继承情况下的C对象模型。
继承下的C对象模型。
分析C类对象在下面情形中的内存布局
单继承子类单一继承自父类分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局。
多继承子类继承于多个父类分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局同时分析了非虚继承下的菱形继承。
虚继承分析了单一继承下的虚继承、多重继承下的虚继承、重复继承下的虚继承。
理解对象的内存布局之后我们可以分析一些问题
C封装带来的布局成本是多大由空类组成的继承层次中每个类对象的大小是多大
至于其他与内存有关的知识我假设大家都有一定的了解如内存对齐指针操作等。
本文初看可能晦涩难懂要求读者有一定的C基础对概念一有一定的掌握。
C中虚函数的作用主要是为了实现多态机制。
多态简单来说是指在继承层次中父类的指针可以具有多种形态——当它指向某个子类对象时通过它能够调用到子类的函数而非父类的函数。
prt2-print();//调用Drive1::print()
prt3-print();//调用Drive2::print()这是一种运行期多态即父类指针唯有在程序运行时才能知道所指的真正类型是什么。
这种运行期决议是通过虚函数表来实现的。
setI(){cout调用了虚函数Base::setI();}virtual
baseI;};当一个类本身定义了虚函数或其父类有虚函数时为了支持多态机制编译器将为该类添加一个虚函数指针vptr。
虚函数指针一般都放在对象内存布局的第一个位置上这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。
当vprt位于对象内存最前面时对象的地址即为虚函数指针地址。
我们可以取得虚函数指针的地址
类型取得了虚函数指针的地址。
虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址虚函数地址出现的顺序与类中虚函数声明的顺序一致。
对虚函数指针地址值可以得到虚函数表的地址也即是虚函数表第一个虚函数的地址:
通过地址调用虚函数Base::print();vfunc();我们把虚表指针的值取出来
(int)(b)它是一个地址虚函数表的地址把虚函数表的地址强制转换成
这样我们就取得了类中的第一个虚函数我们可以通过函数指针访问它。
)(*(int*)(b)1)同样可以通过函数指针访问它这里留给读者自己试验。
到目前为止我们知道了类中虚表指针vprt的由来知道了虚函数表中的内容以及如何通过指针访问虚函数表。
下面的文章中将常使用指针访问对象内存来验证我们的C对象模型以及讨论在各种继承情况下虚表指针的变化先把这部分的内容消化完再接着看下面的内容。
functions:static、nonstatic和virtual:
};那么这个类在内存中将被如何表示5种数据都是连续存放的吗如何布局才能支持C多态
这个模型非常地简单粗暴。
在该模型下对象由一系列的指针组成每一个指针都指向一个数据成员或成员函数也即是说每个数据成员和成员函数在类中所占的大小是相同的都为一个指针的大小。
这样有个好处——很容易算出对象的大小不过赔上的是空间和执行期效率。
想象一下如果我们的Point3d类是这种模型将会比C语言的struct多了许多空间来存放指向函数的指针而且每次读取类的数据成员都需要通过再一次寻址——又是时间上的消耗。
这个模型在简单对象模型的基础上又添加一个间接层它把类中的数据分成了两个部分数据部分与函数部分并使用两张表格一张存放数据本身一张存放函数的地址也即函数比成员多一次寻址而类对象仅仅含有两个指针分别指向上面这两个表。
这样看来对象的大小是固定为两个指针大小。
这个模型也没有用于实际应用于真正的C编译器上。
数据成员被置于每一个类对象中而static数据成员被置于类对象之外。
static与nonstatic函数也都放在类对象之外而对于virtual
table简称vtbl。
虚表中存放着一堆指针这些指针指向该类每一个虚函数。
虚表中的函数地址将按声明时的顺序排列不过当子类有多个重载函数时例外后面会讨论。
每个类对象都拥有一个虚表指针(vptr)由编译器为其生成。
虚表指针的设定与重置皆由类的复制控制也即是构造函数、析构函数、赋值操作符来完成。
vptr的位置为编译器决定传统上它被放在所有显示声明的成员之后不过现在许多编译器把vptr放在一个类对象的最前端。
关于数据成员布局的内容在后面会详细分析。
另外虚函数表的前面设置了一个指向type_info的指针用以支持RTTIRun
Identification运行时类型识别。
RTTI是为多态而生成的信息包括对象继承关系对象本身的描述等只有具有虚函数的对象在会生成。
b(1000);可见对象b含有一个vfptr即vprt。
并且只有nonstatic数据成员被放置于对象内。
我们展开vfprt
vfptr中有两个指针类型的数据地址第一个指向了Base类的析构函数第二个指向了Base的虚函数print顺序与声明顺序相同。
*((RTTICompleteObjectLocator*)*((int*)*(int*)(p)
classname(str.pTypeDescriptor-name);classname
endl;//输入static函数的地址p.countI();//先调用函数以产生一个实例cout
*)(p)取得虚函数表的地址type_info信息的确存在于虚表的前一个位置。
通过((int)(int*)(p)
1))取得type_infn信息并成功获得类的名称的Base虚函数表的第一个函数是析构函数。
虚函数表的第二个函数是虚函数print()取得地址后通过地址调用它而非通过对象验证正确虚表指针的下一个位置为nonstatic数据成员baseI。
可以看到static成员函数的地址段位与虚表指针、baseI的地址段位不同。
好的至此我们了解了非继承下类对象五种数据在内存上的布局也知道了在每一个虚函数表前都有一个指针指向type_info负责对RTTI的支持。
而加入继承后类对象在内存中该如何表示呢
DeriveI(d){};//overwrite父类虚函数virtual
一个派生类如何在机器层面上塑造其父类的实例呢在简单对象模型中可以在子类对象中为每个类子对象分配一个指针。
如下图
简单对象模型的缺点就是因间接性导致的空间存取时间上的额外负担优点则是类的大小是固定的基类的改动不会影响子类对象的大小。
在表格驱动对象模型中我们可以为子类对象增加第三个指针基类指针(bptr)基类指针指向指向一个基类表(base
table),同样的由于间接性导致了空间和存取时间上的额外负担优点则是无须改变子类对象本身就可以更改基类。
表格驱动模型的图就不再贴出来了。
在C对象模型中对于一般继承这个一般是相对于虚拟继承而言若子类重写overwrite了父类的虚函数则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表)若子类并无overwrite父类虚函数而是声明了自己新的虚函数则该虚函数地址将扩充到虚函数表最后在vs中无法通过监视看到扩充的结果不过我们通过取地址的方法可以做到子类新的虚函数确实在父类子物体的虚函数表末端。
而对于虚继承若子类overwrite父类虚函数同样地将覆盖父类子物体中的虚函数表对应位置而若子类声明了自己新的虚函数则编译器将为子类增加一个新的虚表指针vptr这与一般继承不同,在后面再讨论。
endl;//vprt[1]析构函数无法通过地址调用故手动输出cout
单继承中一般继承子类会扩展父类的虚函数表。
在多继承中子类含有多个父类的子对象该往哪个父类的虚函数表扩展呢当子类overwrite了父类的函数需要覆盖多个父类的虚函数表吗
子类的虚函数被放在声明的第一个基类的虚函数表中。
overwrite时所有基类的print()函数都被子类的print()函数覆盖。
内存布局中父类按照其声明顺序排列。
其中第二点保证了父类指针指向子类对象时总是能够调用到真正的函数。
endl;//vprt[0]析构函数无法通过地址调用故手动输出cout
endl;//vprt[0]析构函数无法通过地址调用故手动输出cout
[4]Drive_multyBase::Drive_multyBaseI
菱形继承也称为钻石型继承或重复继承它指的是基类被某个派生类简单重复继承了多次。
这样派生类对象中拥有多份基类实例这会带来一些问题。
为了方便叙述我们不使用上面的代码了而重新写一个重复继承的继承层次
}};这时根据单继承我们可以分析出B1B2类继承于B类时的内存布局。
又根据一般多继承我们可以分析出D类的内存布局。
我们可以得出D类子对象的内存布局如下图
D类对象内存布局中图中青色表示b1类子对象实例黄色表示b2类子对象实例灰色表示D类子对象实例。
从图中可以看到由于D类间接继承了B类两次导致D类对象中含有两个B类的数据成员ib一个属于来源B1类一个来源B2类。
这样不仅增大了空间更重要的是引起了程序歧义
//二义性错误,调用的是B1的ib还是B2的ibd.B1::ib
//正确尽管我们可以通过明确指明调用路径以消除二义性但二义性的潜在性还没有消除我们可以通过虚继承来使D类只拥有一个ib实体。
虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。
虚继承的派生类的内存布局与普通继承很多不同主要体现在
虚继承的子类如果本身定义了新的虚函数则编译器为其生成一个虚函数指针vptr以及一张虚函数表。
该vptr位于对象内存最前面。
vs非虚继承直接扩展父类虚函数表。
虚继承的子类也单独保留了父类的vprt与虚函数表。
这部分内容接与子类内容以一个四字节的0来分界。
虚继承的子类对象中含有四字节的虚表指针偏移值。
在C对象模型中虚继承而来的子类会生成一个隐藏的虚基类指针vbptr在Microsoft
C中虚基类表指针总是在虚函数表指针之后因而对某个类实例来说如果它有虚基类指针那么虚基类指针可能在实例的0字节偏移处该类没有vptr时vbptr就处于类实例内存布局的最前面否则vptr处于类实例内存布局的最前面也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表与虚函数表一样虚基类表也由多个条目组成条目中存放的是偏移值。
第一个条目存放虚基类表指针vbptr所在地址到该类内存首地址的偏移值由第一段的分析我们知道这个偏移值为0类没有vptr或者-4类有虚函数此时有vptr。
我们通过一张图来更好地理解。
虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值这点我们在下面会验证。
B根据我们前面对虚继承的派生类的内存布局的分析B1类的对象模型应该是这样的
这个结果与我们的C对象模型图完全符合。
这时我们可以来分析一下虚表指针的第二个条目值12的具体来源了回忆上文讲到的
第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。
在我们的例子中也就是B类实例内存地址相对于vbptr的偏移值也即是[4]-[1]的偏移值结果即为12从地址上也可以计算出来007CFDFC-007CFDF4结果的十进制数正是12。
现在我们对虚基类表的构成应该有了一个更好的理解。
菱形虚拟继承下最派生类D类的对象模型又有不同的构成了。
在D类对象的内存构成上有以下几点
在D类对象内存中基类出现的顺序是先是B1最左父类然后是B2次左父类最后是B虚祖父类D类对象的数据成员id放在B类前面两部分数据依旧以0来分隔。
编译器没有为D类生成一个它自己的vptr而是覆盖并扩展了最左父类的虚基类表与简单继承的对象模型相同。
超类B的内容放到了D类对象内存布局的最后。
在C语言中“数据”和“处理数据的操作函数”是分开来声明的也就是说语言本身并没有支持“数据和函数”之间的关联性。
在C中我们通过类来将属性与操作绑定在一起称为ADT抽象数据结构。
C语言中使用struct结构体来封装数据使用函数来处理数据。
举个例子如果我们定义了一个struct
{printf((%f,%f,%f),pd-x,pd-y,pd_z);
}而在C中我们更倾向于定义一个Point3d类以ADT来实现上面的操作:
pt){os(pr.x(),pt.y(),pt.z());}看到这段代码很多人第一个疑问可能是加上了封装布局成本增加了多少答案是class
Point3d并没有增加成本。
学过了C对象模型我们知道Point3d类对象的内存中只有三个数据成员。
上面的类声明中三个数据成员直接内含在每一个Point3d对象中而成员函数虽然在类中声明却不出现在类对象object之中这些函数(non-inline)属于类而不属于类对象只会为类产生唯一的函数实例。
所以Point3d的封装并没有带来任何空间或执行期的效率影响。
而在下面这种情况下C的封装额外成本才会显示出来
虚继承关系产生虚基类用于在多重继承下保证基类在子类中拥有唯一实例。
不仅如此Point3d类数据成员的内存布局与c语言的结构体Point3d成员内存布局是相同的。
C中处在同一个访问标识符指public、private、protected下的声明的数据成员在内存中必定保证以其声明顺序出现。
而处于不同访问标识符声明下的成员则无此规定。
对于Point3类来说它的三个数据成员都处于private下在内存中一起声明顺序出现。
我们可以做下实验
a(1,2,3);TestPoint3Member(a);运行结果
不考虑虚函数与虚继承当数据都在同一个访问标识符下C的类与C语言的结构体在对象大小和内存布局上是一致的C的封装并没有带来空间时间上的影响。
编译器为空类安插1字节的char以使该类对象在内存得以配置一个地址。
b1虚继承于b编译器为其安插一个4字节的虚基类表指针32为机器此时b1已不为空编译器不再为其安插1字节的char优化。
b2同理。
d含有来自b1与b2两个父类的两个虚基类表指针。
大小为8字节。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback