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;  // a 是一个左值
int& ref = a; // 引用必须绑定到左值

右值:右值是一个临时对象,它没有持久的存储位置。右值通常出现在表达式的右边,不能出现在赋值运算符的左边。

1
int b = 10 + 20;  // 10 + 20 是一个右值

右值引用:右值引用是C++11引入的一个新特性,它通过&&声明。右值引用允许函数区分左值和右值,从而实现不同的行为。

1
int&& rref = 10 + 20;  // 绑定到右值

移动构造函数和移动赋值函数

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); // 初始化为0
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); // 创建一个大小为10的Buffer
buf1.print(); // 输出缓冲区内容 Buffer size: 10, Data: 0 0 0 0 0 0 0 0 0 0

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(); // 输出移动后的buf1状态 Buffer size: 0, Data: nullptr
buf2.print(); // 输出移动后的buf2状态 Buffer size: 10, Data: 0 0 0 0 0 0 0 0 0 0

std::cout << "\nCreating buf3 with size 5" << std::endl;
Buffer buf3(5); // 创建一个大小为5的Buffer
buf3.print(); // 输出缓冲区内容 Buffer size: 5, Data: 0 0 0 0 0


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(); // 输出移动后的buf2状态 Buffer size: 0, Data: nullptr
buf3.print(); // 输出移动后的buf3状态 Buffer size: 10, Data: 0 0 0 0 0 0 0 0 0 0

return 0;
}

C++的返回值

1
2
3
4
5
Buffer createBuffer(size_t size) {
Buffer temp(size);
// 对temp进行一些操作
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不会。


本站由 yyb 使用 Stellar 1.29.1 主题创建。 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。