在函数调用时,编译器如何处理形参变量和实参变量
当发生函数调用时,需要给形参分配内存,形参变量是实参变量的副本, 在函数调用过程中是形参变量参与整个过程的变化
1. 值传递
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1;
int b = 2;
swap(a, b);
return 0;
}
在函数调用时,编译器为参数a,b准备副本a_, b_, 使a_ = a, b_ = b;这样在整个调用过程中a_ 与b_的content就发生了变化
2. 指针传递
void swap(int *a, int *b)
{
int tmp = *a;
*a =*b;
*b = tmp;
}
int main()
{
int a = 1;
int b = 2;
swap(&a, &b);
return 0;
}
在函数调用时,编译器为参数a address, b address准备副本pa_, pb_, 使pa_ = a address, pb_ = b address;由于在函数调用时swap中变化的是 pa_ content, pb_ content,所以a content, b content 也跟着发生变化
3. 引用传递
void swap(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1;
int b = 2;
swap(a, b);
return 0;
}
在函数调用时,传入的是a, b的别名。在引用传递过程中,虽然被调函数的形参作为局部变 量在栈中开辟了内存空间,但此空间中存放的是主调函数实参的address, 被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
4. 函数的参数为指针
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
int main()
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
return 0;
}
在函数执行时,编译器为参数p准备副本p_,使p_ = p,由于GetMemory函数中只变化了p_,所以p 仍然为NULL,程序crush并且在堆上分配的内存没有被释放
5. 函数的参数为指向指针的指针
void GetMemory(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
int main()
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
return 0;
}
在函数执行时, 编译器为参数p准备副本p_,使p_ = p,由于p_ 此时是指向指针的指针(二级指针), p_中的content为*p_ , GetMemory函数中改变了*p_ , 所以p也跟着变化,程序 可以正常运行,但是在堆上分配的内存没有被释放
6. 函数返回值
(1)普通值做返回值时,直接用eax返回;
(2)指针做返回值时,用eax返回指针值;
(3)引用做返回值时,用eax返回其指向的变量的地址,同指针;
(4)浮点数做返回值时,将返回值存储到浮点数栈的栈顶,也就是ST0做返回值;
(5)数组做返回值时,用eax返回其数组的首地址,同指针;
(6)结构体和类做返回对象时,
若无自定义的拷贝构造函数,
对象大小不大于(<=)4字节,直接用eax返回;
对象大小大于(>)4字节,且不大于(<=)8字节,用eax和edx返回;
若存在自定义的拷贝构造函数或赋值操作符重载,或者无自定义的拷贝构造函数和赋值操作符重载但对象大小大于(>)8字节时,则隐含传入一个返 回对象(可能是临时对象或者接收对象)的地址,在函数内部,对该临时对象的各成员依次赋值(默认拷贝构造),eax返回临时对象的地址。
若返回对象被用作拷贝构造的参数去构造新对象时,直接把新对象的地址做返回对象
若返回对象被用作assignment操作时,需要生成一个临时对象去过渡,该临时对象会被作为返回对象。
7. 用函数返回值来传递动态内存
char *GetMemory(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
int main()
{
char *str = NULL;
str = GetMemory(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
return 0;
}
8. return语句返回指向“栈内存”的指针
char *GetString()
{
char p[] = "hello world";
return p; // 编译器将提出警告
}
int main()
{
char *str = NULL;
str = GetString(); // str 的内容是乱码
cout<< str << endl;
return 0;
}
虽然"hello world"的内存在全局常量区,但是char p[] 占据栈上的一块内存,只不过它拷贝了一份"hello world"给自己,所以在main函数中调用GetString(),来获取那块内存地址会产生乱码,而像如下获取内存地址则是安全的
const char *GetString()
{
const char *p = "hello world";
return p;
}
int main()
{
const char *str = NULL;
str = GetString(); // str 的内容是乱码
cout<< str << endl;
return 0;
}
此时在main函数中获取的内存地址是"hello world"在全局常量区的内存地址
指针和引用的区别
(1)引用不可以为空,但指针可以为空。引用是对象的别名,只有当一个对象真实存在是,才可能有别名,所以,定义一个引用的时候,必须初始化。
因此,如果你有一个变量是用于指向另一个对象,但是它可能为空,这时你应该使用指针;如果变量总是指向一个对象,你的设计不允许这个对象为空,这时你应该使用引用。
(2)引用不可以改变指向,对一个对象"至死不渝";但是指针可以改变指向,而指向其它对象。
虽然引用不可以改变指向,但是可以改变初始化对象的内容。
例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;
而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int num = 100;
int& alias = num;
alias++;
cout << "num = " << num << endl;
cout << "alias = " << alias << endl;
int tmp = 200;
alias = tmp;
alias++;
cout << "num = " << num << endl;
cout << "alias = " << alias << endl;
cout << "tmp = " << tmp << endl;
return 0;
}
对alias的++操作是直接反应到所指变量之上,对引用变量alias重新赋值"alias = tmp",并不会改变alias的指向,它仍然指向的是num,而不是tmp。
所以,对alias进行++操作不会影响到tmp
# g++ quote.cpp -o main
# ./main
num = 101
alias = 101
num = 201
alias = 201
tmp = 200
(3)引用的大小是所指向的变量的大小,因为引用只是一个别名而已;指针是指针本身的大小,是一个无符号整型数,4个字节
(4)引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。
对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。
const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。
(5)函数参数指针传递和引用传递
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形参作为被调函数的局部变量处理,
即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。
值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
引用传递过程中,被调函数的形参也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。
正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,
但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。
而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。
如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
间接寻址是在直接寻址的基础上面建立起来的,也就是直接寻址得到的数据是一个地址,
通过这个地址找到最终的数据,也就是两次寻址,第一次得到的是地址,第二次在通过这个地址获取目标数据
参考 《高质量C++编程》、逆向等