2、函数参数传递:形参与实参,值传递与地址传递的底层区别,参数压栈顺序

好,咱们今天聊一个老生常谈、但很多人其实没真正吃透的话题——函数参数传递。

我面试过不少自称“精通C语言”的工程师,结果一问参数传递,就开始含糊其辞。说白了,这块要是搞不清楚,后面指针、数组、结构体传参全都会踩坑。今天我就把形参、实参、值传递、地址传递,还有参数压栈顺序,一次性给你讲透。

2.1 形参与实参:名字背后的本质

先看个最简单的例子:

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y);
    printf("x=%d, y=%d\n", x, y);  // 输出:x=10, y=20
    return 0;
}

很多人第一次看到这个结果都懵了——明明在swap里交换了,怎么外面没变?

原因很简单:形参是实参的副本

这里的 xy 是实参,ab 是形参。当调用 swap(x, y) 时,C语言会做一件事:把 x 和 y 的值复制一份,塞给 a 和 b。从此以后,a 和 b 就有了自己的内存空间,跟 x、y 再无瓜葛。

我当年刚学C语言时,也在这上面栽过跟头。调试了半天,发现swap函数里确实交换了,可一回到main函数,值又变回去了。后来才明白——我交换的只是两个“替身”,真身纹丝未动。

核心结论:形参是实参的拷贝。对形参的修改,不会影响实参。

2.2 值传递 vs 地址传递:底层到底发生了什么?

好,那怎么才能让swap真正交换两个变量的值?用指针:

void swap_ptr(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    swap_ptr(&x, &y);
    printf("x=%d, y=%d\n", x, y);  // 输出:x=20, y=10
    return 0;
}

这次成功了。为什么?

咱们从底层看看区别。

值传递时:

  • 实参 x=10y=20
  • 形参 a 得到一份拷贝:a=10
  • 形参 b 得到一份拷贝:b=20
  • a 和 b 有自己的内存地址,跟 x、y 完全无关

地址传递时:

  • 实参 &x(x的地址,假设是0x1000),&y(y的地址,假设是0x1004)
  • 形参 a 得到一份拷贝:a=0x1000
  • 形参 b 得到一份拷贝:b=0x1004
  • 虽然 a 和 b 也是拷贝,但拷贝的是地址!通过 *a 就能操作地址0x1000里的内容——也就是 x 本身

你想想看,地址传递本质上还是值传递——传递的是“地址这个值”。只不过这个值指向了原始变量,所以能间接修改它。

我的个人习惯:在嵌入式开发中,如果参数是基本类型(int、char等)且不需要修改,我倾向于用值传递。如果参数是结构体或数组,或者需要修改原值,就用指针传递。这样代码意图更清晰。

2.3 参数压栈顺序:编译器的小秘密

好,接下来聊一个稍微进阶的话题——参数压栈顺序。

先看这段代码:

int func(int a, int b, int c) {
    printf("a=%d, b=%d, c=%d\n", a, b, c);
    return a + b + c;
}

int main() {
    int i = 1;
    func(i, ++i, i++);
    return 0;
}

这段代码在不同编译器下,输出可能不一样。为什么?因为参数求值顺序和压栈顺序是两回事

在x86架构的C调用约定(cdecl)下,参数是从右向左压栈的。也就是说:

  1. 先计算最右边的参数 i++,压栈
  2. 再计算中间的 ++i,压栈
  3. 最后计算最左边的 i,压栈

但注意!求值顺序是未定义行为。C标准没有规定函数参数的求值顺序,不同编译器可能不同。所以上面那段代码,你永远别指望它有什么确定的结果。

我曾经在一个项目里见过这样的代码:

// 别这么写!
send_data(get_sensor_value(0), get_sensor_value(1), get_sensor_value(2));

三个 get_sensor_value 的调用顺序不确定,导致发送的数据顺序时而正确、时而错误。排查了整整两天才找到原因。从那以后,我定了个规矩:函数参数里,绝不允许出现带副作用的表达式

避坑指南:永远不要依赖函数参数的求值顺序。如果参数之间有依赖关系,先算好再传进去。

2.4 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

函数参数传递核心知识体系 函数参数传递 形参 vs 实参 形参是实参的副本 值传递 传递值的拷贝,不影响原变量 地址传递 传递地址的拷贝,可间接修改 底层:栈上分配形参空间 底层:传递指针(地址值) 参数压栈顺序(从右向左) 注意:求值顺序是未定义行为! 核心原则:避免参数中的副作用表达式

2.5 实战建议

说了这么多,最后给几条实战建议:

  • 能用值传递就用值传递——代码更安全,不容易出现意外修改
  • 大结构体用指针传递——避免栈空间浪费,嵌入式开发中尤其重要
  • const指针保护只读参数——比如 void func(const int *p),既高效又安全
  • 永远不要在参数里写 i++、++i 这种表达式——你永远不知道编译器会怎么处理

嗯,这一章就到这儿。参数传递这块,说白了就是“拷贝”两个字——值拷贝还是地址拷贝,搞清楚了,后面指针、数组、函数指针什么的,就一通百通了。


公众号:蓝海资料掘金营,微信deep3321