「C 是值传递还是引用传递?」这道题的标准答案只有一个词,但需要一章来理解它:值传递,一切皆值传递,没有例外。函数收到的永远是实参的复印件,地址也不过是「复印件了一个门牌号」。理解了这一点,swap 为什么要传地址、scanf 为什么带 &、出参模式怎么写、以及「返回局部变量地址」为什么是死刑,全部是同一条原理的推论。原文小节编号错成 9.x,一并修正。
void bad_swap(int a, int b) { // 收到 x、y 的复印件
int t = a; a = b; b = t; // 折腾复印件,原件无恙
}
void swap(int *a, int *b) { // 收到的是「门牌号」的复印件
int t = *a; *a = *b; *b = t; // 顺着门牌号改原件
}
int x = 1, y = 2;
swap(&x, &y); // 传门牌号
bad_swap 里折腾的是复印件,函数返回即销毁;swap 收到的是地址的复印件,虽然地址本身也是复印件,顺着这个地址能摸到原件。这就是 C 的全部传递模型,Java 里「基本类型按值、对象按引用」的讨论,翻译过来就是「有的复印件是值本身,有的复印件是地址」。scanf("%d", &x) 那个让你初学时困惑的 & ,现在看顺理成章:它要往里写,当然要门牌号。
收益一:出参(改调用者的东西)。第 7 章讲过心法「要改谁传谁的地址」,这里补它的多返回值应用。C 函数只能有一个返回值,多个结果靠出参:
int divmod(int a, int b, int *rem) { // 商走返回值,余数走出参
*rem = a % b;
return a / b;
}
int rem;
int q = divmod(17, 5, &rem); // q = 3, rem = 2
glibc 的例子俯拾皆是:strtod 的第二个参数带出「解析到哪了」,pthread_create 的第一个参数带出新线程句柄。出参的命名习惯(rem、endptr、out_前缀)和「入参在前出参在后」的排序,是可读性的隐形基建。
收益二:省拷贝。结构体按值传递是整块复印(几十上百字节照搬),传指针只复印 8 字节门牌:
typedef struct { char name[32]; int scores[100]; } Student;
void grade(Student *s); // 8 字节,顺路可写
void view(const Student *s); // 8 字节,只读承诺(第 8 章)
只读场景务必加 const:view 版既能接变量也能接只读数据,还向读者声明「我不动你的数据」。这两个收益经常同时兑现:传指针又快又能改,何乐不为,唯一要警惕的是第 12 章(别名)里「两个指针指向同一结构」引发的意外。
返回局部变量的地址是死刑(第 2 章从汇编层看过全程):
char *bad(void) {
char s[64] = "hello";
return s; // s 的栈帧随返回作废,这个地址是凶宅
}
白名单三种,可以背:
// 白一:静态存储期的对象(static、全局、字符串字面量)
char *ok1(void) { return "hello"; } // 字面量在只读段,永生
const char *ok1b(void) { return "hi"; } // 更严谨的签名(反正不许改)
// 白二:堆上分配,所有权随返回值移交给调用者
char *ok2(void) {
char *s = malloc(64);
if (!s) return NULL;
strcpy(s, "hello");
return s; // 调用者负责 free,文档里写明!
}
// 白三:调用者传进来的缓冲区,原样返回(方便链式调用)
char *ok3(char *buf, size_t n) { snprintf(buf, n, "hello"); return buf; }
白二是标准库最爱的模式(strdup、strtok 的返回),它的代价是「所有权移交」必须靠文档和自觉:返回堆指针的函数,注释里不写清楚谁释放,三个月后一定有人泄漏。黑名单三种:局部数组地址、局部结构体地址、以及指向局部 static 之外、还指望「下次调用还在」的任何栈内对象。编译器的 -Wreturn-local-addr 会拦第一种,拦不住的是「返回参数指针的减法结果」这类自作聪明。
p = malloc(...) 改的是指针复印件(第 7 章的 bad_alloc),要改指针本身还得二级指针。规则没有第二层例外。int parse_int(const char *s, int *out)(字符串转整数,失败返回 0 且不动 out,成功返回 1 并写 out),处理前导空白、正负号、非数字截断,不许用 atoi;写 6 组测试覆盖边界。