我最近遇到了一个严重的错误,我忘了在函数中返回一个值。问题是,即使没有返回任何内容,它在Linux / Windows下运行良好,只在Mac下崩溃。当我打开所有编译器警告时,我发现了这个错误。
所以这是一个简单的例子:
#include <iostream>
class A{
public:
A(int p1, int p2, int p3): v1(p1), v2(p2), v3(p3)
{
}
int v1;
int v2;
int v3;
};
A* getA(){
A* p = new A(1,2,3);
// return p;
}
int main(){
A* a = getA();
std::cerr << "A: v1=" << a->v1 << " v2=" << a->v2 << " v3=" << a->v3 << std::endl;
return 0;
}
我的问题是如何在Linux / Windows下运行而不会崩溃?如何在较低级别上返回值?
答案 0 :(得分:7)
在英特尔架构上,简单值(整数和指针)通常在eax
寄存器中返回。该寄存器(以及其他寄存器)在内存中移动值时也用作临时存储,在计算过程中也用作操作数。因此,该寄存器中剩余的任何值都被视为返回值,在您的情况下,它确实是您想要返回的内容。
答案 1 :(得分:3)
幸运的是,&#39; a&#39;留在一个恰好用于返回单指针结果的寄存器中,就像那样。
调用/约定和函数结果返回是依赖于体系结构的,因此您的代码在Windows / Linux上运行而在Mac上运行并不奇怪。
答案 2 :(得分:2)
首先,您需要稍微修改您的示例以使其编译。该函数必须至少具有返回值的执行路径。
A* getA(){
if(false)
return NULL;
A* p = new A(1,2,3);
// return p;
}
其次,它显然是未定义的行为,这意味着任何事情都可能发生,但我想这个答案不会让你满意。
第三,在Windows中它可以在调试模式下工作,但如果你在Release下编译,它就不会。
以下是在Debug:
下编译的 A* p = new A(1,2,3);
00021535 push 0Ch
00021537 call operator new (211FEh)
0002153C add esp,4
0002153F mov dword ptr [ebp-0E0h],eax
00021545 mov dword ptr [ebp-4],0
0002154C cmp dword ptr [ebp-0E0h],0
00021553 je getA+7Eh (2156Eh)
00021555 push 3
00021557 push 2
00021559 push 1
0002155B mov ecx,dword ptr [ebp-0E0h]
00021561 call A::A (21271h)
00021566 mov dword ptr [ebp-0F4h],eax
0002156C jmp getA+88h (21578h)
0002156E mov dword ptr [ebp-0F4h],0
00021578 mov eax,dword ptr [ebp-0F4h]
0002157E mov dword ptr [ebp-0ECh],eax
00021584 mov dword ptr [ebp-4],0FFFFFFFFh
0002158B mov ecx,dword ptr [ebp-0ECh]
00021591 mov dword ptr [ebp-14h],ecx
第二条指令,即operator new
的调用,移动到eax
指向新创建实例的指针。
A* a = getA();
0010484E call getA (1012ADh)
00104853 mov dword ptr [a],eax
调用上下文期望eax
包含返回的值,但它不包含由new
分配的最后一个指针,顺便提一下,p
。
这就是它起作用的原因。
答案 3 :(得分:2)
编译器有两种主要的返回值的方法:
#1通常与适合寄存器的任何东西一起使用; #2适用于其他所有内容(大型结构,数组等)。
在您的情况下,编译器使用#1 来返回new
和返回您的函数。在Linux和Windows上,编译器没有对寄存器执行任何值失真操作,返回值写入指针变量并从函数返回;在Mac上,它确实如此。因此,您看到的结果有所不同:在第一种情况下,返回寄存器中的剩余值恰好与您想要的值共同内部无论如何都要回来。
答案 4 :(得分:1)
正如Kerrek SB所提到的,你的代码冒险进入了未定义行为的领域。
基本上,您的代码将编译为汇编。在汇编中,没有一个函数需要返回类型的概念,只是期望。我对MIPS最熟悉,所以我将用MIPS来说明。
假设您有以下代码:
int add(x, y)
{
return x + y;
}
这将被翻译成:
add:
add $v0, $a0, $a1 #add $a0 and $a1 and store it in $v0
jr $ra #jump back to where ever this code was jumped to from
要添加5和4,代码将被称为:
addi $a0, $0, 5 # 5 is the first param
addi $a1, $0, 4 # 4 is the second param
jal add
# $v0 now contains 9
请注意,与C不同,没有明确要求$ v0包含返回值,只是期望值。那么,如果你实际上没有将任何内容推入$ v0会发生什么?好吧,$ v0总是有一些值,因此该值将是它最后的值。
注意:这篇文章做了一些简化。此外,你的计算机可能没有运行MIPS ......但希望这个例子成立,如果你在大学学习集会,MIPS可能就是你所知道的。
答案 5 :(得分:0)
从函数返回值的方式取决于体系结构和值的类型。它可以通过寄存器或通过堆栈完成。 通常在x86体系结构中,如果它是一个整数类型,则返回EAX寄存器中的值:char,int或pointer。 如果未指定返回值,则该值未定义。只有你的运气,你的代码有时才能正常工作。
答案 6 :(得分:0)
关于n3242草案C ++标准第6.6.3.2段中的以下声明,您的示例产生未定义的行为:
离开函数末尾相当于没有返回 值;这会导致值返回时出现未定义的行为 功能
查看实际发生情况的最佳方法是检查给定编译器在给定体系结构上生成的汇编代码。对于以下代码:
#pragma warning(default:4716)
int foo(int a, int b)
{
int c = a + b;
}
int main()
{
int n = foo(1, 2);
}
... VS2010编译器(在调试模式下,在Intel 32位机器上)生成以下程序集:
#pragma warning(default:4716)
int foo(int a, int b)
{
011C1490 push ebp
011C1491 mov ebp,esp
011C1493 sub esp,0CCh
011C1499 push ebx
011C149A push esi
011C149B push edi
011C149C lea edi,[ebp-0CCh]
011C14A2 mov ecx,33h
011C14A7 mov eax,0CCCCCCCCh
011C14AC rep stos dword ptr es:[edi]
int c = a + b;
011C14AE mov eax,dword ptr [a]
011C14B1 add eax,dword ptr [b]
011C14B4 mov dword ptr [c],eax
}
...
int main()
{
011C14D0 push ebp
011C14D1 mov ebp,esp
011C14D3 sub esp,0CCh
011C14D9 push ebx
011C14DA push esi
011C14DB push edi
011C14DC lea edi,[ebp-0CCh]
011C14E2 mov ecx,33h
011C14E7 mov eax,0CCCCCCCCh
011C14EC rep stos dword ptr es:[edi]
int n = foo(1, 2);
011C14EE push 2
011C14F0 push 1
011C14F2 call foo (11C1122h)
011C14F7 add esp,8
011C14FA mov dword ptr [n],eax
}
foo()
中的加法运算结果存储在eax
寄存器(累加器)中,其内容用作函数的返回值,移到变量n
。
eax
也用于存储以下示例中的返回值(指针):
#pragma warning(default:4716)
int* foo(int a)
{
int* p = new int(a);
}
int main()
{
int* pn = foo(1);
if(pn)
{
int n = *pn;
delete pn;
}
}
汇编代码:
#pragma warning(default:4716)
int* foo(int a)
{
000C1520 push ebp
000C1521 mov ebp,esp
000C1523 sub esp,0DCh
000C1529 push ebx
000C152A push esi
000C152B push edi
000C152C lea edi,[ebp-0DCh]
000C1532 mov ecx,37h
000C1537 mov eax,0CCCCCCCCh
000C153C rep stos dword ptr es:[edi]
int* p = new int(a);
000C153E push 4
000C1540 call operator new (0C1253h)
000C1545 add esp,4
000C1548 mov dword ptr [ebp-0D4h],eax
000C154E cmp dword ptr [ebp-0D4h],0
000C1555 je foo+50h (0C1570h)
000C1557 mov eax,dword ptr [ebp-0D4h]
000C155D mov ecx,dword ptr [a]
000C1560 mov dword ptr [eax],ecx
000C1562 mov edx,dword ptr [ebp-0D4h]
000C1568 mov dword ptr [ebp-0DCh],edx
000C156E jmp foo+5Ah (0C157Ah)
std::operator<<<std::char_traits<char> >:
000C1570 mov dword ptr [ebp-0DCh],0
000C157A mov eax,dword ptr [ebp-0DCh]
000C1580 mov dword ptr [p],eax
}
...
int main()
{
000C1610 push ebp
000C1611 mov ebp,esp
000C1613 sub esp,0E4h
000C1619 push ebx
000C161A push esi
000C161B push edi
000C161C lea edi,[ebp-0E4h]
000C1622 mov ecx,39h
000C1627 mov eax,0CCCCCCCCh
000C162C rep stos dword ptr es:[edi]
int* pn = foo(1);
000C162E push 1
000C1630 call foo (0C124Eh)
000C1635 add esp,4
000C1638 mov dword ptr [pn],eax
if(pn)
000C163B cmp dword ptr [pn],0
000C163F je main+51h (0C1661h)
{
int n = *pn;
000C1641 mov eax,dword ptr [pn]
000C1644 mov ecx,dword ptr [eax]
000C1646 mov dword ptr [n],ecx
delete pn;
000C1649 mov eax,dword ptr [pn]
000C164C mov dword ptr [ebp-0E0h],eax
000C1652 mov ecx,dword ptr [ebp-0E0h]
000C1658 push ecx
000C1659 call operator delete (0C1249h)
000C165E add esp,4
}
}
VS2010编译器在两个示例中都发布了warning 4716。默认情况下,此警告会提升为错误。
答案 7 :(得分:0)
在IBM PC体系结构中从堆栈中弹出值时,不会对存储在那里的旧数据值进行物理破坏。它们只是通过堆栈的操作变得不可用,但仍然保留在同一个存储单元中。
当然,这些数据的先前值将在随后在堆栈上推送新数据时被破坏。
所以可能你很幸运,在你的函数调用期间没有任何东西被添加到堆栈中并返回周围的代码。