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