【VC++】虚函数 内存结构 - 第一篇(单类)

本文探讨了C++虚函数的内存结构,适合有一定C++面向对象基础和内存知识的读者。通过对比分析,揭示了不同编译器对虚函数实现的细节,指出了一些知名博主文章中的错误。文章强调理解底层原理对解决实际问题和提高编程安全性的重要性,并提供了代码示例以解释潜在的访问风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言 - 为何写此系列文章?

网上讨论虚函数内存结构的文章很少,好不容易有几位大神写了几篇很精彩的文章,结果里面还有错误:

陈皓大神的《C++ 虚函数表解析》:
https://blog.csdn.net/haoel/article/details/1948051
《C++ 对象的内存布局(上)》:
https://blog.csdn.net/haoel/article/details/3081328
《C++ 对象的内存布局(下)》:
https://blog.csdn.net/haoel/article/details/3081385

在这里插入图片描述
还有位大神的《图说C++对象模型:对象内存布局详解》:
https://www.cnblogs.com/QG-whz/p/4909359.html
同样有错误。

但是,不否认,几位大神的文章写得确实精彩,我看了之后也受益匪浅,再次感谢几位大神的分享精神。
我并不是做C++的技术极客,几位大神的经验和水平肯定也会比我高,所以我写的此系列文章,重在研究探索与实践,难免有错误的地方,恳请大家大力吐槽,并恳求给出具体的解决方法,在此先谢过。会不定期更新,并记录提出修正方案的朋友,以示感谢,以帮助到更多的人。

前言 - 适合什么样的读者?

引用《深度探索C++对象模型》这本书中的话,有两个概念可以解释C++对象模型:
1、语言中直接支持面向对象程序设计的部分。
2、对于各种支持的底层实现机制。

直接支持面向对象程序设计,包括了构造函数、析构函数、多态、虚函数等等,这些内容在很多书籍上都有讨论,也是C++最被人熟知的地方(特性)。而对象模型的底层实现机制却是很少有书籍讨论的。对象模型的底层实现机制并未标准化,不同的编译器有一定的自由来设计对象模型的实现细节。在我看来,对象模型研究的是对象在存储上的空间与时间上的更优,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态特性。

文章主要来讨论C++对象在内存中的布局,属于第二个概念的研究范畴。故读者需要有一定的C++面向对象基础、内存相关知识,比如内存对齐,指针操作等。看完之后,对C++虚函数会有一个底层的理解,可以解决工作中的一些实际问题,比如解决一些很迷惑的bug、逆向工程……

【VC++】虚函数 内存结构 - 第一篇(单类)

#include <IOSTREAM>
using namespace std;

class Base
{
public:
	int nBase1;
	int nBase2;

	Base(int n1,int n2):nBase1(n1),nBase2(n2)
	{
		cout<<"Base::Base("<<n1<<","<<n2<<")"<<endl;
	}

	virtual void F()
	{
		cout<<"Base::F()"<<endl;
	}
	virtual void G()
	{
		cout<<"Base::G()"<<endl;
	}
};


typedef void(*Fun)(void);	

void TestBase(Base &b)
{
	Fun pFun = NULL;

	cout << "类对象地址 / 虚函数表指针的地址:" << (&b) << " / " <<    (int*)(&b)+0 << endl;//虽然两个地址值一样,但前者是类对象的地址,后者是对象里面的首地址。好比字符串的地址,和字符串里第一个字符的地址,是一样的。
	cout << "虚函数表指针值(虚函数表的地址)(十进制):"	<<        *(int*)(&b) <<endl; //就省略+0了(就是首地址偏移0,还是首地址)
	cout << "虚函数表指针值(虚函数表的地址)(十六进制):" << (int*)*(int*)(&b) <<endl; //显式指针转换,告知是个指针,输出就是十六进制了
	cout << "虚函数表 — 第 1 个函数地址的地址:"  <<		    (int*)*(int*)(&b) << " , ";	 //好比一个变量的地址
					   cout << "函数地址值:"<<			 (int*)*(int*)*(int*)(&b) << " 调:";//好比一个变量的值(这个值也是个地址)
	pFun = (Fun)*(int*)*(int*)(&b);//(Fun)(int*)*(int*)*(int*)(&b)的简化,上行代码最左边的(int*)是显式指针转换,可直接替换成(Fun)
	pFun();
	
	cout << "虚函数表 — 第 2 个函数地址的地址:"  <<		    (int*)*(int*)(&b)+1 << " , ";
					   cout << "函数地址值:"<<		    (int*)*((int*)*(int*)(&b)+1) << " 调:";
	pFun = (Fun)*((int*)*(int*)(&b)+1);
	pFun();

	cout << "虚函数表 — 第 3 个函数地址的地址:"  <<		    (int*)*(int*)(&b)+2 << " , ";
					   cout << "函数地址值:"<<		    (int*)*((int*)*(int*)(&b)+2)<<endl;	//如果仅有一个虚函数,后面的节点不是NULL???
	
	cout << "类对象 — 第 1 个变量地址:" << ((int*)(&b)+1)  << " 值:"<< *((int*)(&b)+1) <<endl;	
	cout << "类对象 — 第 2 个变量地址:" << ((int*)(&b)+2)  << " 值:"<< *((int*)(&b)+2) <<endl;
	cout << "类对象 — 第 3 个变量地址:" << ((int*)(&b)+3)  << " 值:"<< *((int*)(&b)+3) <<endl;	//之后的内存是什么???
	
	cout<<endl;
}

void TestBase2(Base &b)
{
	Fun pFun = NULL;
	int** pVtab = (int**)&b;
	
	cout << "类对象地址 / 虚函数表指针的地址:" << (&b) << " / " << pVtab << endl;
	cout << "虚函数表指针值(虚函数表的地址)(十进制):" << (int)pVtab[0] <<endl;
	cout << "虚函数表指针值(虚函数表的地址)(十六进制):" << pVtab[0] <<endl; 

	cout << "虚函数表 — 第 1 个函数地址的地址:"  <<  &pVtab[0][0] << " , ";	//好比一个变量的地址
					   cout << "函数地址值:"  << (int*)pVtab[0][0] << " 调:";	//好比一个变量的值(这个值也是个地址)
	pFun = (Fun)pVtab[0][0];
	pFun();

	cout << "虚函数表 — 第 2 个函数地址的地址:"  <<  &pVtab[0][1] << " , ";
					   cout << "函数地址值:"  << (int*)pVtab[0][1] << " 调:";
	pFun = (Fun)pVtab[0][1];
	pFun();

	cout << "虚函数表 — 第 3 个函数地址的地址:"  <<  &pVtab[0][2] << " , ";
					   cout << "函数地址值:"  << (int*)pVtab[0][2] <<endl ;	//如果仅有一个虚函数,后面的节点不是NULL???
	//pFun = (Fun)pVtab[0][2];
	//pFun();
	
	cout << "类对象 — 第 1 个变量地址:" << &pVtab[1]  << " 值:"<< (int)pVtab[1] <<endl;	
	cout << "类对象 — 第 2 个变量地址:" << &pVtab[2]  << " 值:"<< (int)pVtab[2] <<endl;
	cout << "类对象 — 第 3 个变量地址:" << &pVtab[3]  << " 值:"<< (int)pVtab[3] <<endl;	//之后的内存是什么???
	
	cout<<endl;
}

void main()
{
	Base b1(123,321);
	Base b2(456,654);

	cout<<"------------------------- 法一:地址偏移"<<endl;
	TestBase(b1);
	TestBase(b2);
	cout<<"------------------------- 法二:二维指针"<<endl;
	TestBase2(b2);
	
	cout<<endl;
}

运行结果:
在这里插入图片描述
比对看三种Test(红色表示不同的地方):
同一个类,两个不同的对象 比较:
在这里插入图片描述

同一个类对象,两种不同的方法 比较:
在这里插入图片描述

在内存中的结构是:
在这里插入图片描述
上图中,类对象b1的首地址 0019FF24,是类对象b2末尾紧接的地址 0019FF24,此例b2和b1在内存空间里是连续分布的,但并不是同一类的所有对象,在内存空间里都是连续分布的,而是取决于栈上的顺序。本例中,b1先入栈,紧接着b2入栈,栈的增长方向是往低地址增长,故b2在低地址,往高地址紧接着就是b1。因为定义顺序是紧挨着的,如果中间插入定义了其它局部变量,b2和b1就不连续。若有兴趣详细研究,可以看这篇文章:
《变量在内存里的存储区域》:https://blog.csdn.net/maoyeahcom/article/details/108940270

在本文结束前,我也还是来介绍下自己吧。我从03年初学编程,一直到现在都在做软件开发方面的事,做过VC++、VB/Delphi/C++Builder、C#/Asp.Net、Java、Web前后端、数据库、服务器端、客户端、js、lua、Flash AS、cocos2dx/Unity3d……都有接触过,甚至做过很长一段时间的运营、产品策划、美工、客服、线下推广员、公司经理 多职务……技术只是一种工具,用来实现业务需求,所以最大的特长是逻辑思维、挖掘业务需求、强化用户体验、快速的产品实现,最自以为豪的,我做过的很多产品,很稳定,bug极少(当然也出现过没考虑周全的漏洞在所难免但总体上也还算少),有很多独立开发的游戏运营到现在还在稳定的运行。不是技术极客,而是业务需求带动技术的学习研究,所以也不怎么挑语言,编程语言大体上也都是相通的,底层原理也有很多类同的地方。所以缺点从不敢说精通哪一门语言。遇到小工具、小需求的订制实现,我个人最喜欢的还是用VC6,简洁干净,编译出来的东西 体积小,速度快,占CPU内存小。当然实际工作中,还是随项目。欢迎大家和我交流,我的QQ是:七6.肆-陆_柒-4`7_四.
生活中的我,热爱 街舞、飙车、花式游泳、游戏一条命通关……极限运动,我抖音号(一不小心玩出花样)是:1917940952

文章没有考虑字节对齐(都用默认的字节对齐),并且是在32位环境下,VC6、VS2017 都测试过。

	//文章没有考虑字节对齐(都用默认的字节对齐),并且是在32位环境下
	cout<<"sizeof(Base): "<<sizeof(Base)<<endl;	//输出值:12(4*3) 两个int各占4字节,32位指针占4字节

在这里插入图片描述

安全性

C++有很多搬石头砸自己脚的东西,比如const修饰就是想不被更改,但又有个const_cast来破坏这种约定。同样的,对于类成员变量、虚函数,用non-public(比如private或是protected)修饰,就是一种限制约定(比如不能直接 对象. 来访问),外部访问时编译器会报错,但通过本文的方法(下面的代码示例),可以绕过编译器检查,在运行时又能正常的访问。水可载舟,亦可覆舟。重在清楚原理,然后视场景情况而运用。

#include <IOSTREAM>
using namespace std;

class Base 
{
//public:
private:	
	virtual void F() 
	{ 
		cout << "Base::F()" << endl; 
	}	
};

typedef void(*Fun)(void);

void main() 
{	
    Base b;	
	//b.F();//访问private修饰的,会报错
    Fun  pFun = (Fun)*(int*)*(int*)(&b);	
    pFun();	
}

再来看看下面这个代码:

#include <IOSTREAM>
using namespace std;

class Base 
{
public:	
	int nBase;
	virtual void F() 
	{ 
		nBase = 123;
		cout << "Base::F()" << endl; 
	}	
};

typedef void(*Fun)(void);

void main() 
{	
    Base b;	
	b.nBase = 0;

	//b.F();	//执行后,nBase值改变了

    Fun  pFun = (Fun)*(int*)*(int*)(&b);	
    pFun();	

	cout<<"b.nBase : "<<b.nBase<<endl<<endl;
}

在 F() 加入了对成员变量的访问,一般用法 b.F(),能正常访问,但用本文的方法,会出错:
在这里插入图片描述
这又是为什么?因为 成员变量 nBase是类对象的,b.F()访问时,会正常传入类对象的this指针,也就能正常访问nBase。但用本文的方法访问 F(),把它当成一种普通的函数,解析this指针地址是F()的函数地址,从这个地址开始的内存块 被解析成一个Base对象……想象一下多乱,导致非法访问。
再延伸 再刨根,有无穷无尽的东西要研究,偏离本文的主题,有兴趣的童鞋可以再去研究。

(转载时请注明作者和出处。未经许可,请勿用于商业用途)
原创出处:https://blog.csdn.net/maoyeahcom/article/details/108728608

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值