Как работает возврат значений из функции?

недавно у меня была серьезная ошибка, когда я забыл вернуть значение в функции. Проблема заключалась в том, что, хотя ничего не было возвращено, он отлично работал под 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 ответов


в архитектуре Intel простые значения (целые числа и указатели) обычно возвращаются в eax зарегистрироваться. Этот регистр (среди прочих) также используется как временное хранилище при перемещении значений в памяти и как операнд при вычислениях. Поэтому любое значение, оставшееся в этом регистре, рассматривается как возвращаемое значение, и в вашем случае оно оказалось именно тем, что вы хотели вернуть.


вероятно, по счастью, " a " осталось в регистре, который используется для возврата результатов одного указателя, что-то в этом роде.

вызывающие / соглашения и возвращаемые результаты функций зависят от архитектуры, поэтому неудивительно, что ваш код работает на Windows/Linux, но не на Mac.


существует два основных способа для компилятора вернуть значение:

  1. поставь значение Регистрация перед возвращением, и
  2. попросите вызывающего абонента передать блок памяти стека для возвращаемого значения и записать значение в этот блок [подробнее]

#1 обычно используется со всем, что вписывается в регистр; #2 для всего остального (большие структуры, массивы и т. д.).

в вашем случае компилятор использует #1 для возвращения new и для возвращения вашей функции. В Linux и Windows компилятор не выполнял никаких операций искажения значений в регистре с возвращаемым значением между записью его в переменную указателя и возвращением из вашей функции; на Mac это было сделано. Отсюда и разница в результатах, которые вы видите: в первом случае левую-над стоимостью в обратном регистре произошло со-внутри со значением, которое вы хотел в любом случае вернуться.


во-первых, вам нужно немного изменить ваш пример, чтобы получить его для компиляции. Функция должна иметь по крайней мере путь выполнения, возвращающий значение.

A* getA(){
    if(false)
        return NULL;
    A* p = new A(1,2,3);
//  return p;
}

в-третьих, в Windows он работает в режиме отладки, но если вы компилируете под выпуском, это не так.

в разделе 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.

так вот почему это работает.


как упоминал 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, , 5 # 5 is the first param
addi $a1, , 4 # 4 is the second param
jal add
# $v0 now contains 9

обратите внимание, что в отличие от C, нет явного требования, что $v0 содержит возвращаемое значение, просто ожидание. Итак, что произойдет, если вы на самом деле ничего не нажимаете на $v0? Ну, $v0, направленную всегда некоторые значение, поэтому значение будет тем, что было последним.

Примечание: этот пост делает некоторые упрощения. Кроме того, вы компьютер, вероятно не работает пом... Но, надеюсь, пример верен, и если вы изучили сборку в университете, MIPS может быть тем, что вы знаете в любом случае.


способ возврата значения из функции зависит от архитектуры и типа значения. Это можно сделать через регистры или через стек. Обычно в архитектуре x86 значение возвращается в регистре EAX, если это интегральный тип: char, int или указатель. Если возвращаемое значение не указано, оно не определено. Это только ваша удача, что ваш код иногда работал правильно.


при извлечении значений из стека в архитектуре IBM PC нет физического уничтожения старых значений данных, хранящихся там. Они просто становятся недоступными благодаря работе стека, но по-прежнему остаются в той же ячейке памяти.

конечно, предыдущие значения этих данных будут уничтожены во время последующего нажатия новых данных в стеке.

Так что, вероятно, вам просто повезло, и ничего не добавляется в стек во время вашей функции вызовите и возвратите окружающий код.


Что касается следующего утверждения из проекта стандарта C++ n3242, пункт 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 (в режиме отладки на 32-разрядной машине Intel) генерирует следующую сборку:

#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 проблемы компилятора предупреждение 4716 в обоих примерах. По умолчанию это предупреждение повышается до ошибки.