函数的返回值如何工作?

时间:2012-03-11 09:09:48

标签: c++ stack

我最近遇到了一个严重的错误,我忘了在函数中返回一个值。问题是,即使没有返回任何内容,它在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下运行而不会崩溃?如何在较低级别上返回值?

8 个答案:

答案 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. 让调用者为返回值传递一堆堆栈内存,并将值写入该块[more info]
  3. #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体系结构中从堆栈中弹出值时,不会对存储在那里的旧数据值进行物理破坏。它们只是通过堆栈的操作变得不可用,但仍然保留在同一个存储单元中。

当然,这些数据的先前值将在随后在堆栈上推送新数据时被破坏。

所以可能你很幸运,在你的函数调用期间没有任何东西被添加到堆栈中并返回周围的代码。