函数的返回值是如何工作的
How does returning values from a function work?
我最近遇到了一个严重的错误,忘记在函数中返回值。问题是,即使没有返回任何东西,它在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下工作而不崩溃?如何在较低级别上返回值?
在英特尔体系结构上,简单值(整数和指针)通常在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中,它在调试模式下工作,但如果您在Release下编译,它就不会。
以下是在调试下编译的:
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, $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可能就是你所知道的。
从函数返回值的方式取决于体系结构和值的类型。它可以通过寄存器或堆栈来完成。通常在x86体系结构中,如果值是整数类型:char、int或指针,则会在EAX寄存器中返回该值。如果没有指定返回值,则该值是未定义的。这只是你的运气,你的代码有时工作正确。
在IBM PC体系结构中从堆栈中弹出值时,不会对旧值进行物理破坏存储在那里的数据。它们只是通过堆栈的操作变得不可用,但仍然保留在同一个内存单元中。
当然,前面的值这些数据中的一个将在随后向堆栈上推送新数据的过程中被销毁。
因此,您可能很幸运,在函数调用和返回周围代码的过程中,没有向堆栈添加任何内容。
关于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编译器(在调试模式下,在英特尔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编译器在两个示例中都发出警告4716。默认情况下,此警告升级为错误。
- 从python中调用C++函数并获取返回值
- 为什么模板类中的对象不能返回值
- 返回值优化:显式移动还是隐式
- lock_guard是否保护返回值
- 调用CreateProcess()并获取字符串的返回值
- 如何使 windows 命令提示符在C++可执行文件上显示返回值?
- 编译器警告:执行到达值返回函数的末尾而不返回值
- 查找 GCD:并非所有控制路径都返回值
- 在 Arduino 上使用 sscanf 会导致与 const char * 不匹配,并且返回值始终相同,尽管输入值不同
- 将返回值存储在函数指针数组的指针中是如何工作的?
- 如何从 std::thread 返回值
- 将返回值从 exe 传递到 bat,并将其传递给 C# 中的进程
- 方法错误"not all control paths return a value"和方法不返回值
- 找不到使保证返回值优化工作的方法
- 参数传递和返回值如何在程序集级别的 x86 上的 C/C++ 中工作?
- 从程序不工作C++返回浮点值
- 函数的返回值是如何工作的
- cpp字符串find()无法按预期工作-返回大垃圾值
- 矢量擦除无需设置返回值即可工作
- 函数返回值如何工作