考虑对齐和内存布局假设,在缓冲区中管理C ++对象
-
03-07-2019 - |
题
我将对象存储在缓冲区中。现在我知道我不能对对象的内存布局做出假设。
如果我知道对象的整体大小,是否可以创建指向此内存的指针并在其上调用函数?
e.g。说我有以下课程:
[int,int,int,int,char,padding*3bytes,unsigned short int*]
1) 如果我知道这个类的大小为24,我知道它在内存中的起始位置 虽然假设内存布局是可以接受将它转换为指针并调用此对象上的函数来访问这些成员是不安全的? (c ++是否知道成员的正确位置?)
2) 如果这不安全/没有,除了使用构造函数之外还有其他任何方法,它可以获取所有参数并一次从缓冲区中拉出每个参数吗?
编辑:更改了标题,使其更符合我的要求。
解决方案
您可以创建一个构造函数来获取所有成员并分配它们,然后使用placement new。
class Foo
{
int a;int b;int c;int d;char e;unsigned short int*f;
public:
Foo(int A,int B,int C,int D,char E,unsigned short int*F) : a(A), b(B), c(C), d(D), e(E), f(F) {}
};
...
char *buf = new char[sizeof(Foo)]; //pre-allocated buffer
Foo *f = new (buf) Foo(a,b,c,d,e,f);
这样做的好处是甚至可以正确生成v表。但是,请注意,如果您使用它进行序列化,则无符号短整数指针在反序列化时不会指向任何有用的东西,除非您非常小心地使用某种方法将指针转换为偏移然后再返回
this
指针上的各个方法是静态链接的,只是直接调用该函数, this
是显式参数之前的第一个参数。
使用 this
指针的偏移量引用成员变量。如果对象的布局如下:
0: vtable
4: a
8: b
12: c
etc...
将通过解除引用 this + 4 bytes
来访问 a
。
其他提示
基本上你提议做的是读取一堆(希望不是随机的)字节,将它们转换为已知对象,然后在该对象上调用类方法。它实际上可能有用,因为这些字节最终将在“this”中出现。该类方法中的指针。但是你真的有机会处理编译代码所期望的事情。与Java或C#不同,没有真正的“运行时”。要抓住这些问题,所以最多你会得到一个核心转储,更糟糕的是你会得到损坏的内存。
听起来你想要一个Java的序列化/反序列化的C ++版本。可能有一个图书馆可以做到这一点。
非虚函数调用直接链接,就像C函数一样。对象(this)指针作为第一个参数传递。调用该函数不需要对象布局的知识。
听起来你并没有将对象本身存储在缓冲区中,而是存储它们的数据。
如果这些数据在内存中按字段在您的类中定义(对于平台有适当的填充)和,那么您的类型是 POD ,然后你可以 memcpy
来自缓冲到指向你的类型的指针(或者可能会抛出它,但要注意,有一些特定于平台的陷阱,其中有不同类型的指针)。
如果您的类不是POD,则不保证字段的内存布局,并且您不应该依赖任何观察到的顺序,因为允许在每次重新编译时更改。
但是,您可以使用来自POD的数据初始化非POD。
就非虚函数所在的地址而言:它们在编译时静态链接到代码段中的某个位置,对于您的类型的每个实例都是相同的。请注意,没有“运行时”。参与其中。当你编写这样的代码时:
class Foo{
int a;
int b;
public:
void DoSomething(int x);
};
void Foo::DoSomething(int x){a = x * 2; b = x + a;}
int main(){
Foo f;
f.DoSomething(42);
return 0;
}
编译器生成的代码类似于:
- function
main
:- 在堆栈上为对象“
f
" 分配8个字节
- 为类调用默认初始值设定项“
Foo
” (在这种情况下什么都不做) - 将参数值
42
推入堆栈 - 将指针推送到对象“
f
”到堆栈 - 调用函数
Foo_i_DoSomething @ 4
(实际名称通常更复杂) - 将返回值
0
加载到累加器寄存器 - 返回来电者
- 在堆栈上为对象“
- function
Foo_i_DoSomething @ 4
(位于代码段的其他位置)- load"
x
"堆栈中的值(由调用者推送) - 乘以2
- load"
this
"堆栈指针(由调用者推送) - 计算字段的偏移量“
a
”在Foo
对象 中
- 将计算的偏移量添加到
此
指针,在步骤3中加载 - 将在步骤2中计算的产品存储到步骤5中计算的偏移
- load"
x
"来自堆栈的价值,再次 - load"
this
"来自堆栈的指针,再次 - 计算字段的偏移量“
a
”在Foo
对象中,再次 - 将计算出的偏移量添加到
此
指针,在步骤8中加载 - load"
a
"值存储在offset, - 添加"
a
"值,在步骤12中加载到“x
”在第7步中加载的值 - load"
this
"来自堆栈的指针,再次 - 计算字段的偏移量“
b
”在Foo
对象 中
- 将计算出的偏移量添加到
此
指针,在步骤14中加载 - 在步骤13中计算的存储和对在步骤16中计算的偏移
- 返回来电者
醇>
- load"
换句话说,它或多或少是相同的代码,就好像你已经编写了这个(具体的,例如DoSomething函数的名称和传递这个
指针的方法取决于编译器) :
class Foo{
int a;
int b;
friend void Foo_DoSomething(Foo *f, int x);
};
void Foo_DoSomething(Foo *f, int x){
f->a = x * 2;
f->b = x + f->a;
}
int main(){
Foo f;
Foo_DoSomething(&f, 42);
return 0;
}
-
在这种情况下,已经创建了一个具有POD类型的对象(无论你是否调用new。分配所需的存储已经足够了),你可以访问它的成员,包括调用一个函数宾语。但是只有当你精确地知道T的所需对齐和T的大小(缓冲区可能不小于它)以及T的所有成员的对齐时,这才会起作用。即使对于pod类型,编译器也是如此。如果需要,允许在成员之间放置填充字节。对于非POD类型,如果您的类型没有虚函数或基类,没有用户定义的构造函数(当然)并且也适用于基类及其所有非静态成员,则可以获得相同的运气。
-
对于所有其他类型,所有投注均已关闭。您必须首先使用POD读取值,然后使用该数据初始化非POD类型。
醇>
我将对象存储在缓冲区中。 ...如果我知道对象的整体大小,是否可以创建指向此内存的指针并在其上调用函数?
在使用演员阵容的情况下,这是可以接受的:
#include <iostream>
namespace {
class A {
int i;
int j;
public:
int value()
{
return i + j;
}
};
}
int main()
{
char buffer[] = { 1, 2 };
std::cout << reinterpret_cast<A*>(buffer)->value() << '\n';
}
将对象转换为原始内存并再次返回实际上很常见,尤其是在C世界中。但是,如果您正在使用类层次结构,则使用指向成员函数的指针会更有意义。
说我有以下课程:...
如果我知道这个类的大小为24,我知道它在内存中的起始位置......
这是事情变得困难的地方。对象的大小包括其数据成员的大小(以及来自任何基类的任何数据成员)加上任何填充以及任何函数指针或依赖于实现的信息,减去从某些大小优化中保存的任何内容(空基类优化)。如果结果数字是0字节,则该对象需要在内存中至少占用一个字节。这些是大多数CPU对内存访问的语言问题和常见要求的组合。 尝试让事情正常工作可能会非常痛苦。
如果您只是分配一个对象并在原始内存中进行强制转换,则可以忽略这些问题。但是如果你将一个对象的内部复制到某种缓冲区,那么它们会很快地抬起头。上面的代码依赖于一些关于对齐的一般规则(即,我碰巧知道A类将具有与int相同的对齐限制,因此阵列可以安全地转换为A;但我不一定能保证如果我将数组的一部分转换为A和其他类与其他数据成员的部分,则相同。
哦,当复制对象时,你需要确保正确处理指针。
您可能也对 Google的协议缓冲区或 Facebook的节俭。
是的,这些问题很难解决。而且,是的,一些编程语言将它们扫地出门。 但是有很多东西要搞定席卷地毯:
在Sun的HotSpot JVM中,对象存储与最近的64位边界对齐。除此之外,每个对象在内存中都有一个2字的标题。 JVM的字大小通常是平台的本机指针大小。 (一个只包含32位int和64位double - 96位数据的对象将需要)对象头有两个单词,int有一个单词,double有两个单词。这是5个字:160位。由于对齐,此对象将占用192位内存。
这是因为Sun依赖于相对简单的内存对齐问题策略(在假想的处理器上,可以允许char存在于任何内存位置,任何可被4整除的位置的int,以及双可能只需要在可被32整除的内存位置上进行分配 - 但最严格的对齐要求也满足所有其他对齐要求,因此Sun根据最严格的位置对齐所有内容。)
- 如果该类不包含虚函数(因此类实例没有vptr),并且如果您对类成员数据在内存中的布局方式做出正确假设,那么做你所建议的可能有用(但可能不便携)。
- 是的,另一种方式(更惯用但不太安全......你仍然需要知道课程如何列出其数据)将使用所谓的“贴片操作员新”。和默认构造函数。 醇>
这取决于你所说的“安全”。无论何时以这种方式将内存地址转换为某个点,您都会绕过编译器提供的类型安全功能,并对自己负责。如果像Chris所暗示的那样,你对内存布局或编译器实现细节做出了错误的假设,那么你将得到意想不到的结果和松散的可移植性。
因为你担心“安全”对于这种编程风格,您可能值得花时间研究可移植和类型安全的方法,例如预先存在的库,或者为此目的编写构造函数或赋值运算符。