下面我将从代码分析、偏移量计算原理和内存访问安全性三个方面进行说明:
#include<iostream>
#include<list>
#include<string>
#include<vector>
using namespace std;
class base
{
public:
int m_i1;
char m_c1;
char m_c2;
char m_c3;
};
#define GET(A , m) (int)(&((A*)0)->m)
int main()
{
cout << "大小:"<<sizeof(base) << endl;
cout <<"m_i1:"<< GET(base, m_i1) << endl;
cout <<"m_c1:"<< GET(base, m_c1) << endl;
cout <<"m_c2:"<< GET(base, m_c2) << endl;
cout << "m_c3:" << GET(base, m_c3) << endl;
base s1;
return 0;
}
代码功能解析
这段C++代码主要演示了如何计算类成员变量的内存偏移量。通过自定义的GET
宏,程序输出了base
类中各个成员变量的偏移位置和类的总大小。
偏移量计算原理
-
(A)0的含义*
将空指针0
强制转换为A*
类型,这告诉编译器将内存地址0x00000000
视为一个A
类型的对象。注意,这里只是进行类型转换,并没有真正访问该地址的内存。 -
成员偏移量的计算
表达式((A*)0)->m
通过刚才创建的"虚拟对象"访问其成员m
。此时,&((A*)0)->m
获取的是成员m
在这个"虚拟对象"中的地址。由于该对象起始于地址0
,这个地址值恰好等于成员m
相对于对象起始位置的偏移量(以字节为单位)。 -
宏定义的作用
GET(A, m)
宏通过(int)(&((A*)0)->m)
将偏移地址转换为整数类型,从而得到一个可以直接打印的偏移量数值。
关于内存访问安全性
虽然代码中使用了0x00000000
这个通常被视为无效的地址,但整个过程中并没有对该地址进行读写操作。编译器在处理&((A*)0)->m
时,只是根据类型信息计算成员的偏移量,并不需要真正访问内存。因此,这个操作是安全的,不会导致程序崩溃。
输出结果说明
在32位系统中,程序的输出可能如下:
大小:8
m_i1:0
m_c1:4
m_c2:5
m_c3:6
这表明:
base
类的总大小为8字节(由于内存对齐)m_i1
位于对象起始位置(偏移量0)m_c1
、m_c2
、m_c3
依次排列在m_i1
之后(偏移量4、5、6)- 最后2字节为填充字节,用于保证内存对齐
&((A*)0)->m_i1即是m_i1的成员变量的偏移地址,那为什么这么说呢,来详细分析一下:
1、(A*)0什么意思?标识0x00000000这个地址空间强行转化成(A*)类型,这是告诉编译器,0x00000000这块空间你按照(A*)来解析;
2、那么接下来&((A*)0)->m_i1就代表m_i1从0x00000000这块内存开始的所在的地址空间,因为他的起始地址是0,那么&((A*)0)->m_i1也就代表了m_i1在类对象内的偏移地址,当对象的首地址为0时,得到的成员变量地址就是它的偏移量。
3、那么访问0x000000不会内存崩溃吗;答案是不会,因为在整个过程中,我们只是告诉编译器0x00000000这块内存的空间你按照A*去解析,但是我们并未对这块内存进行任何读写操作,只是进行了获取偏移地址m_i1,所以不会崩溃。