指针和函数
第 8 课里,函数改不了外面的变量:它拿到的只是值的副本。学了指针,就有办法了:把变量的地址交给函数,函数顺着地址就能找回原来那个变量。
1#include <stdio.h>2 3void add_bonus(int *score) {4 *score = *score + 10;5}6 7int main(void) {8 int score = 80;9 add_bonus(&score);10 printf("main 里:%d\n", score);11 return 0;12}
main 里:90
这一次 main 里的 score 真的变成了 90。参数是 int *score,一个指针;调用时传的是 &score,一个地址。
副本换不了
交换两个变量要三行,外加一个临时变量。把这三行放进函数里,却好像什么也没做:
1#include <stdio.h>2 3void swap(int a, int b) {4 int tmp = a;5 a = b;6 b = tmp;7}8 9int main(void) {10 int x = 1;11 int y = 2;12 swap(x, y);13 printf("x = %d, y = %d\n", x, y);14 return 0;15}
x = 1, y = 2
点单步,看两个栈帧。swap 的框里有它自己的 a 和 b,是 1 和 2 的副本。它们确实换过来了,可只在 swap 的框里。函数一结束,这个框就没了,main 框里的 x 和 y 从来没变过。
传地址
把参数改成指针,调用时传 &x 和 &y。在函数里,*a 就是 main 的 x,*b 就是 main 的 y:
1#include <stdio.h>2 3void swap(int *a, int *b) {4 int tmp = *a;5 *a = *b;6 *b = tmp;7}8 9int main(void) {10 int x = 1;11 int y = 2;12 swap(&x, &y);13 printf("x = %d, y = %d\n", x, y);14 return 0;15}
x = 2, y = 1
再点单步。这一次箭头从 swap 的框伸到 main 的框里:a 指着 x,b 指着 y。走到第 5、6 行,变的是 main 框里的值。tmp 是普通的 int,不带星号。
其实这里 C 还是传值:swap 拿到的是两个地址的副本。可地址抄一份,指的还是同一个地方。
这段程序显示什么?
1#include <stdio.h>2 3void twice(int *n) {4 *n = *n * 2;5}6 7int main(void) {8 int steps = 3;9 twice(&steps);10 twice(&steps);11 printf("%d\n", steps);12 return 0;13}
每调用一次,都通过指针把 main 的 steps 翻一倍:3 变 6,6 变 12。要是参数是普通的 int,显示的就是 3。
交回两个结果
return 只能交回一个值。函数有两个答案的时候,可以让调用的地方把两个变量的地址传进来,函数把结果填进去:
1#include <stdio.h>2 3void split_time(int total, int *hours, int *minutes) {4 *hours = total / 60;5 *minutes = total % 60;6}7 8int main(void) {9 int h;10 int m;11 split_time(135, &h, &m);12 printf("%d 小时 %d 分钟\n", h, m);13 split_time(50, &h, &m);14 printf("%d 小时 %d 分钟\n", h, m);15 return 0;16}
2 小时 15 分钟 0 小时 50 分钟
h 和 m 一开始没有值,这没关系:split_time 只往里写,不读。用指针参数交回结果,在 C 里很常见。名字起清楚,一看就知道哪些是给函数的,哪些是函数交回来的。
再看一个:走一遍数组,同时找出最小值和最大值:
1#include <stdio.h>2 3void min_max(int a[], int count, int *low, int *high) {4 *low = a[0];5 *high = a[0];6 for (int i = 1; i < count; i++) {7 if (a[i] < *low) {8 *low = a[i];9 }10 if (a[i] > *high) {11 *high = a[i];12 }13 }14}15 16int main(void) {17 int temps[5] = {18, 23, 15, 27, 21};18 int low;19 int high;20 min_max(temps, 5, &low, &high);21 printf("最低 %d 度,最高 %d 度\n", low, high);22 return 0;23}
最低 15 度,最高 27 度
数组写成 int a[] 传进去,旁边跟着它的个数。这里到底传了什么,第 17 课讲。
scanf 为什么要 &
现在第 3 课 scanf("%d", &age) 里的 & 就说得通了。scanf 也是一个函数。要是传 age,它拿到的只是一个值的副本,没法把读到的数放进你的变量。所以要传地址,它顺着地址写进去,和 split_time 一样。
1#include <stdio.h>2 3int main(void) {4 int age;5 int *p = &age;6 scanf("%d", p);7 printf("明年你 %d 岁\n", age + 1);8 return 0;9}
明年你 37 岁
p 里已经存着 age 的地址,所以写 p 和写 &age 一样。printf 不用 &:它只读值,副本就够了。
忘了写 &
参数是指针,就得传地址。下面漏了 &:
1#include <stdio.h>2 3void swap(int *a, int *b) {4 int tmp = *a;5 *a = *b;6 *b = tmp;7}8 9int main(void) {10 int x = 0;11 int y = 7;12 swap(x, y);13 printf("x = %d, y = %d\n", x, y);14 return 0;15}
main.c: In function 'main': main.c:12:10: warning: passing argument 1 of 'swap' makes pointer from integer without a cast [-Wint-conversion] 12 | swap(x, y); | ^ | | | int main.c:3:16: note: expected 'int *' but argument is of type 'int' 3 | void swap(int *a, int *b) { | ~~~~~^ main.c:12:13: warning: passing argument 2 of 'swap' makes pointer from integer without a cast [-Wint-conversion] 12 | swap(x, y); | ^ | | | int main.c:3:24: note: expected 'int *' but argument is of type 'int' 3 | void swap(int *a, int *b) { | ~~~~~^ load of null pointer
发生了什么: 停在第 4 行:这个指针是 NULL(空指针),不能通过它读写。
makes pointer from integer 的意思是:这里要一个指针(地址),给的却是 int。这个 gcc 只给了警告(gcc 14 起就是错误),程序照样运行:x 里的值被当成了地址。x 是 0,地址 0 就是 NULL,所以第 4 行顺着 a 找过去,什么也找不到。我们的引擎在这里停下。点「看看是怎么出错的」看一看。改法是写成 swap(&x, &y)。
另一个常见的错是函数里漏了星号:写成 a = b;,只是在 swap 的框里把一个地址抄到另一个上面,main 什么也看不到。
小技巧: 什么时候参数用指针?函数只要读一个值,就传值;要改外面的变量,或者要交回不止一个结果,就传地址。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
存钱
✓把 deposit 写完:给 balance 指向的余额加上 amount。程序要显示 余额:150。
余额:150
1#include <stdio.h>2 3void deposit(int *balance, int amount) {4 5}6 7int main(void) {8 int balance = 100;9 deposit(&balance, 50);10 printf("余额:%d\n", balance);11 return 0;12}
元和分
✓写一个函数 split_price,通过两个指针把以分为单位的价格拆成元和分。split_price(1295, &d, &c) 让 d 变成 12,c 变成 95。程序要显示 12 元 95 分。
12 元 95 分
1#include <stdio.h>2 3// 在这里写 split_price4 5int main(void) {6 int d;7 int c;8 split_price(1295, &d, &c);9 printf("%d 元 %d 分\n", d, c);10 return 0;11}
小的放前面
✓写一个函数 sort2,参数是两个 int 变量的地址;如果第一个比第二个大,就交换它们,让小的在前面。程序要先显示 3 8,再显示 4 9。
3 8 4 9
1#include <stdio.h>2 3// 在这里写 sort24 5int main(void) {6 int a = 8;7 int b = 3;8 sort2(&a, &b);9 printf("%d %d\n", a, b);10 int c = 4;11 int d = 9;12 sort2(&c, &d);13 printf("%d %d\n", c, d);14 return 0;15}
这一课学会了
- 函数拿到的是实参的副本,改参数影响不到外面的变量。
- 改成传地址(
swap(&x, &y),参数写int *a),函数就能通过*a改原来的变量。 - 单步看得很清楚:箭头从被调用的函数的框,伸到调用它的那个框里的变量。
return只交回一个值;用指针参数可以交回好几个结果。scanf要&也是这个道理:它得知道变量在哪里,才能把值放进去。- 要指针的地方传了
int,编译器会警告(gcc 14 起是错误),程序也会出事:别忘了&。