C++ 为什么将指针回退到原始类时会出现奇怪的行为?
假设在我的代码中,我必须将一个C++ 为什么将指针回退到原始类时会出现奇怪的行为?,c++,reference,void-pointers,reinterpret-cast,strict-aliasing,C++,Reference,Void Pointers,Reinterpret Cast,Strict Aliasing,假设在我的代码中,我必须将一个void*存储为数据成员,并在需要时将其类型转换回原始类指针。为了测试它的可靠性,我编写了一个测试程序(linux-ubuntu 4.4.1g++-04-Wall),看到这种行为我感到震惊 struct A { int i; static int c; A () : i(c++) { cout<<"A() : i("<<i<<")\n"; } }; int A::c; int main () { void *p
void*
存储为数据成员,并在需要时将其类型转换回原始类
指针。为了测试它的可靠性,我编写了一个测试程序(linux-ubuntu 4.4.1g++-04-Wall),看到这种行为我感到震惊
struct A
{
int i;
static int c;
A () : i(c++) { cout<<"A() : i("<<i<<")\n"; }
};
int A::c;
int main ()
{
void *p = new A[3]; // good behavior for A* p = new A[3];
cout<<"p->i = "<<((A*)p)->i<<endl;
((A*&)p)++;
cout<<"p->i = "<<((A*)p)->i<<endl;
((A*&)p)++;
cout<<"p->i = "<<((A*)p)->i<<endl;
}
但是,如果您更改void*p代码>至A*p代码>它给出了为什么?
另一个问题,我不能逃脱(A*&)
,否则我不能使用操作符+
;但它也发出警告,如。有什么方法可以克服警告吗?正如编译器警告您的那样,您违反了严格的别名规则,这正式意味着结果是未定义的
您可以通过为增量使用函数模板来消除严格的别名冲突:
template<typename T>
void advance_pointer_as(void*& p, int n = 1) {
T* p_a(static_cast<T*>(p));
p_a += n;
p = p_a;
}
正如其他人所评论的,您的代码看起来应该可以工作。只有一次(在用C++编写代码的17年多时间里),我遇到了一些事情,我直接看代码和行为,就像在你的例子中一样,根本没有意义。我最终通过调试器运行代码并打开了一个反汇编窗口。我在VS2003编译器中发现了一个只能解释为bug的东西,因为它只缺少一条指令。只需在函数顶部重新排列局部变量(从错误开始大约30行),编译器就会将正确的指令放回。所以,尝试使用反汇编的调试器并跟踪内存/寄存器,看看它实际在做什么
至于推进指针,您应该能够通过执行以下操作来推进它:
p = (char*)p + sizeof( A );
VS2003到VS2010从不对此进行投诉,不确定g++您已经收到了正确的答案,而且确实是违反了严格的别名规则,导致了代码的不可预测行为。我只想指出,你问题的标题提到了“回溯指向原始类的指针”。事实上,您的代码与“返回”无关。代码将void*
指针占用的原始内存内容重新解释为a*
指针。这不是“回头看”。这是重新解释。一点也不一样
说明差异的一个好方法是使用andint
和float
示例。一个浮点值
声明并初始化为
float f = 2.0;
cab可以强制转换(显式或隐式转换)为int
类型
int i = (int) f;
达到了预期的效果
assert(i == 2);
这确实是一个演员阵容(转换)
或者,相同的float
值也可以重新解释为int
值
int i = (int &) f;
但是,在这种情况下,i
的值将完全没有意义,并且通常不可预测。我希望从这些例子中很容易看出转换和内存重新解释之间的区别
重新解释正是您在代码中所做的。(A*&)p
表达式只是将指针void*p
占用的原始内存重新解释为A*
类型的指针。该语言不能保证这两种指针类型具有相同的表示形式,甚至大小相同。因此,从代码中期望可预测的行为就像期望上面的(int&)f
表达式的计算结果为2
真正“回退”您的void*
指针的正确方法是执行(A*)p
,而不是(A*)p
。(A*)p
的结果实际上是原始指针值,可以通过指针算法安全地进行操作。获得作为左值的原始值的唯一正确方法是使用附加变量
A *pa = (A *) p;
...
pa++;
...
而且,没有合法的方法来“就地”创建左值,正如您试图通过(A*&)p
铸造的那样。你的代码的行为是一个例证。使用G+4.5.1,CLAN3.0.1275,Visual C++ 2010 SP1,程序输出{ 0, 1, 2 }。您正在使用哪个编译器生成意外结果?@trutheality:No。您不能使用void*
执行算术。实际上p=sizeof(a)+(char*)p代码>按预期工作,没有警告。使用g++4.3、4.4或4.5,当在-O2
或更高级别编译时,此代码输出0 1
;当在-O1
或更低级别编译时,它输出0 1 2
。显然平台也很重要;在Win32上,带有-O4
的g++4.5.1产生了预期的{0,1,2}@Adam。它不会增加简单++
操作的机器指令吗(或者我们必须依赖于进行优化)?一个缓慢、正确的程序远远优于一个快速的程序,不正确的程序。@iammilind除非您自己测量和比较,否则无法知道。尽管我有讽刺性的评论,我还是希望编译器即使在低优化级别也能内联该程序,因此我不希望它有显著的性能成本。您是否检查了启用最大优化标志的MSVC?因为我保留了-O4
。正如@Adam在评论中所建议的,即使是g++
也能正确地输出较低的优化。这是一个很好的解释。我不确定“没有合法的方法在适当的地方创建左值”,尽管:(char&)p
相当于*reinterpret\u cast(&p)
,这是有效的,不违反严格的别名,并且有效地“在适当的地方创建左值”。@James:我认为这意味着没有合法的方法在适当的地方创建带有A*
类型的左值,这正是提问者所尝试的,因此也是安德烈所说的。类型char
,unsigned char
,以及与引用对象的有效类型兼容的任何其他类型,当然。我想再挑剔一次,我认为创建左值是有效的
int i = (int &) f;
A *pa = (A *) p;
...
pa++;
...