C cdecl调用约定
简单类型(x86-64) int类型的返回值存储在RAX(指针与其类似) 对于float类型的返回值,存储在ST0寄存器(浮点栈的栈顶)。 对于double类型的返回值,存储在XMM0寄存器中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int add (int a, int b) { return a + b; } int result = add(1 ,2 );add: push ebp mov ebp, esp mov eax, [ebp + 8 ] ; 获取参数a add eax, [ebp + 12 ] ; 加上参数b pop ebp ret push 2 ; 将参数b压入栈 push 1 ; 将参数a压入栈 call add ; 调用函数 add esp, 8 ; 调用者清理栈(清理两个参数,每个参数4 字节)
结构体 小结构体(可以放入寄存器) 对于较小的结构体(例如,大小不超过寄存器大小的结构体),返回值可以直接通过寄存器返回。例如,在x86-64架构中,如果结构体大小不超过16字节,可以使用RAX和RDX寄存器返回。
大结构体(不能放入寄存器) 对于较大的结构体(例如,大小超过寄存器大小的结构体),C语言通常会通过隐式指针返回结构体。编译器会将一个指向返回结构体的指针作为第一个参数传递给函数,函数将结果存储到该指针指向的内存中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 Avatar 在cdecl调用约定下,返回结构体的方式会根据结构体的大小和复杂性有所不同。以下是详细的说明: 1. 小结构体(可以放入寄存器)对于较小的结构体(例如,大小不超过寄存器大小的结构体),返回值可以直接通过寄存器返回。例如,在x86-64 架构中,如果结构体大小不超过16 字节,可以使用RAX和RDX寄存器返回。 示例代码: c 复制 struct Point { int x; int y; }; struct Point getPoint (int x, int y) { struct Point p ; p.x = x; p.y = y; return p; } 对应的汇编代码(x86-64 架构): assembly 复制 getPoint: mov eax, edi ; 将参数x (第一个参数在rdi) 移动到eax mov edx, esi ; 将参数y (第二个参数在rsi) 移动到edx mov dword ptr [rsp-8 ], eax ; 将x存储到局部变量 mov dword ptr [rsp-4 ], edx ; 将y存储到局部变量 mov eax, [rsp-8 ] ; 将x加载到rax mov edx, [rsp-4 ] ; 将y加载到rdx ret 在调用getPoint函数时,返回的结构体会存储在寄存器中: x存储在RAX寄存器 y存储在RDX寄存器 2. 大结构体(不能放入寄存器)对于较大的结构体(例如,大小超过寄存器大小的结构体),C语言通常会通过隐式指针返回结构体。编译器会将一个指向返回结构体的指针作为第一个参数传递给函数,函数将结果存储到该指针指向的内存中。(如果没有复杂操作,会直接将值存储到调用者开好的空间里,如果有,会在函数里创建临时结构体马,然后将临时结构体复制到调用者开好的空间里) ```C struct BigStruct { int a; int b; int c; int d; }; struct BigStruct getBigStruct (int a, int b, int c, int d) { struct BigStruct s ; s.a = a; s.b = b; s.c = c; s.d = d; return s; } struct BigStruct result ;result = getBigStruct(1 , 2 , 3 , 4 ); getBigStruct: push ebp mov ebp, esp mov eax, [ebp + 8 ] ; 获取隐式指针(指向返回结构体的指针) mov ecx, [ebp + 12 ] ; 获取参数a mov [eax], ecx ; 将a存储到返回结构体中 mov ecx, [ebp + 16 ] ; 获取参数b mov [eax + 4 ], ecx ; 将b存储到返回结构体中 mov ecx, [ebp + 20 ] ; 获取参数c mov [eax + 8 ], ecx ; 将c存储到返回结构体中 mov ecx, [ebp + 24 ] ; 获取参数d mov [eax + 12 ], ecx ; 将d存储到返回结构体中 pop ebp ret sub esp, 16 ; 为返回结构体分配空间 lea eax, [esp] ; 获取返回结构体的地址 push eax ; 将返回结构体的地址作为第一个参数压入栈 push 4 ; 将参数d压入栈 push 3 ; 将参数c压入栈 push 2 ; 将参数b压入栈 push 1 ; 将参数a压入栈 call getBigStruct ; 调用函数 add esp, 20 ; 清理栈(5 个参数,每个4 字节)
C++ 移动语义和左右值
左值:左值是一个有名字的变量,它有一个持久的存储位置。左值可以出现在赋值运算符的左边。
1 2 int a = 10 ; int & ref = a;
右值:右值是一个临时对象,它没有持久的存储位置。右值通常出现在表达式的右边,不能出现在赋值运算符的左边。
右值引用:右值引用是C++11引入的一个新特性,它通过&&声明。右值引用允许函数区分左值和右值,从而实现不同的行为。
移动构造函数和移动赋值函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #include <iostream> #include <cstring> class Buffer {private : char * data; size_t size; public : Buffer (size_t s = 0 ) : size (s), data (new char [s]) { std::memset (data, 0 , size); std::cout << "Buffer created with size " << size << std::endl; } Buffer (const Buffer& other) : size (other.size), data (new char [other.size]) { std::memcpy (data, other.data, size); std::cout << "Buffer copied with size " << size << std::endl; } Buffer (Buffer&& other) noexcept : size (other.size), data (other.data) { other.size = 0 ; other.data = nullptr ; std::cout << "Buffer moved with size " << size << std::endl; } Buffer& operator =(const Buffer& other) { if (this != &other) { delete [] data; size = other.size; data = new char [size]; std::memcpy (data, other.data, size); std::cout << "Buffer assigned with size " << size << std::endl; } return *this ; } Buffer& operator =(Buffer&& other) noexcept { if (this != &other) { delete [] data; size = other.size; data = other.data; other.size = 0 ; other.data = nullptr ; std::cout << "Buffer moved-assigned with size " << size << std::endl; } return *this ; } ~Buffer () { delete [] data; std::cout << "Buffer destroyed with size " << size << std::endl; } void print () const { if (data) { std::cout << "Buffer size: " << size << ", Data: " ; for (size_t i = 0 ; i < size; ++i) { std::cout << static_cast <int >(data[i]) << " " ; } std::cout << std::endl; } else { std::cout << "Buffer size: 0, Data: nullptr" << std::endl; } } }; int main () { std::cout << "Creating buf1 with size 10" << std::endl; Buffer buf1 (10 ) ; buf1. print (); std::cout << "\nUsing move constructor to create buf2 from buf1" << std::endl; Buffer buf2 = std::move (buf1); std::cout << "After moving buf1 to buf2:" << std::endl; buf1. print (); buf2. print (); std::cout << "\nCreating buf3 with size 5" << std::endl; Buffer buf3 (5 ) ; buf3. print (); std::cout << "\nUsing move assignment to assign buf2 to buf3" << std::endl; buf3 = std::move (buf2); std::cout << "After moving buf2 to buf3:" << std::endl; buf2. print (); buf3. print (); return 0 ; }
C++的返回值 1 2 3 4 5 Buffer createBuffer (size_t size) { Buffer temp (size) ; return temp; }
在createBuffer函数中,创建了一个临时对象temp。 当temp被返回时,C++17标准保证了返回值优化(RVO),即编译器会直接将temp的资源移动到调用者的变量buf中,而不会进行拷贝或额外的移动操作。 如果编译器没有启用RVO(例如在某些旧的编译器或特定情况下),移动构造函数会被调用,将temp的资源移动到buf中。
1.有移动构造函数 如果类定义了移动构造函数,编译器会优先使用移动构造函数来优化返回值的传递。移动构造函数会将临时对象的资源直接“移动”到目标对象中,避免了不必要的拷贝。
2.没有移动构造函数,但有拷贝构造函数 如果类没有定义移动构造函数,但定义了拷贝构造函数,编译器会使用拷贝构造函数来创建返回值。这意味着会创建一个临时对象的副本。
3.既没有移动构造函数,也没有拷贝构造函数 如果类既没有定义移动构造函数,也没有定义拷贝构造函数,编译器会尝试使用默认的移动构造函数或默认的拷贝构造函数。如果这些默认构造函数也无法生成(例如,类中有不可移动或不可拷贝的成员),编译器会报错。(如果无法生成默认拷贝,则也无法生成默认移动,比如unique_ptr,只支持默认拷贝,不支持默认移动,可以是一个有拷贝构造无移动构造的类放到一个类或容器里)
4. 特殊情况:返回值优化(RVO)和命名返回值优化(NRVO) 即使类没有显式定义移动构造函数,现代C++编译器通常会启用返回值优化(RVO)或命名返回值优化(NRVO)。这些优化可以完全避免临时对象的拷贝或移动,直接将资源构造到目标对象中。 RVO:优化的是函数返回的临时对象。编译器直接将临时对象构造到目标对象中。适用场景:函数返回一个临时对象,如return Buffer(10);。 NRVO:优化的是函数中被命名的返回值。编译器直接将命名的返回值构造到目标对象中。如 Buffer result(10); return result; 其最终效果都相当于被调用者在调用者开辟的返回值的位置直接生成,相当与C的隐式指针返回结构体。
总结 也就是说,如果使用移动构造和拷贝构造的返回值,返回时会调用析构函数,但是RVO和NRVO不会。