指针和数组
数组的元素在内存里是一个挨一个排着的。所以在 C 里,指针和数组是近亲。大多数时候,数组的名字就当作它第一个元素的地址来用:
1#include <stdio.h>2 3int main(void) {4 int scores[4] = {70, 85, 92, 64};5 int *p = scores;6 printf("%d\n", *p);7 printf("%d\n", p == &scores[0]);8 return 0;9}
70 1
int *p = scores; 没写 &,也不用写:这里的 scores 就是 &scores[0]。点单步,能看到 p 的箭头指着数组的第一个格子。
p + 1 是下一个元素
指向数组的指针加 1,得到的是下一个元素的地址,不是下一个字节。一个 int 占 4 个字节,所以地址其实大了 4;这个 C 会按指针的类型自己算。
1#include <stdio.h>2 3int main(void) {4 int scores[4] = {70, 85, 92, 64};5 int *p = scores;6 printf("%d\n", *(p + 1));7 printf("%d\n", *(p + 3));8 p = p + 2;9 printf("%d\n", *p);10 return 0;11}
85 64 92
点单步,看第 8 行:箭头往右挪了两格,指着 92。*(p + 1) 的括号不能少:*p + 1 是第一个元素加 1,也就是 71。
p[i] 就是 *(p + i)
在 C 里,方括号是「先加,再顺着找过去」的简写。a[i] 的意思就是 *(a + i)。数组名可以这么用,指针也可以:
| 写法 | 意思 |
|---|---|
a[0] | *a |
a[i] | *(a + i) |
&a[i] | a + i |
1#include <stdio.h>2 3int main(void) {4 double prices[3] = {2.50, 4.00, 1.25};5 double *p = prices;6 printf("%.2f %.2f\n", prices[2], *(prices + 2));7 printf("%.2f %.2f\n", p[1], *(p + 1));8 p[0] = 3.00;9 printf("%.2f\n", prices[0]);10 return 0;11}
1.25 1.25 4.00 4.00 3.00
所以指向数组的指针,用起来和数组一模一样。下标从 0 开始,也是这个原因:下标是离开头走几步,第一个元素离开头 0 步。
用指针走数组
不用 i 数,也可以让指针从第一个元素走到最后一个。scores + 4 是紧跟在最后一个元素后面的位置;p 一到那里就停,那里的东西不能读。
1#include <stdio.h>2 3int main(void) {4 int scores[4] = {70, 85, 92, 64};5 int total = 0;6 for (int *p = scores; p < scores + 4; p++) {7 total += *p;8 }9 printf("总分:%d\n", total);10 return 0;11}
总分:311
点单步,看 p 的箭头每转一圈往右挪一格,total 跟着变大。p++ 让指针挪到下一个元素。
这段程序显示什么?
1#include <stdio.h>2 3int main(void) {4 int steps[5] = {10, 20, 30, 40, 50};5 int *p = steps + 1;6 p++;7 printf("%d %d\n", *p, p[1]);8 return 0;9}
steps + 1 指着 20,p++ 再往后挪到 30,所以 *p 是 30。p[1] 是从 p 现在的位置再往后一个:40。
数组传给函数
把数组传给函数,数组并不会复制一份,函数拿到的是第一个元素的地址。所以函数能改外面的数组;也所以作为参数,int a[] 和 int *a 是一个意思:
1#include <stdio.h>2 3void add_to_all(int *a, int count, int amount) {4 for (int i = 0; i < count; i++) {5 a[i] += amount;6 }7}8 9int main(void) {10 int prices[3] = {10, 25, 40};11 add_to_all(prices, 3, 5);12 printf("%d %d %d\n", prices[0], prices[1], prices[2]);13 return 0;14}
15 30 45
点单步:在 add_to_all 里,a 是一条箭头,指回 main 的数组,变的是 main 框里的格子。
函数只拿到一个地址,所以它不知道数组有多长。在函数里用 sizeof,得到的是指针的大小,不是数组的大小:
1#include <stdio.h>2 3void show_size(int a[]) {4 printf("函数里:%zu\n", sizeof a);5}6 7int main(void) {8 int prices[5] = {10, 25, 40, 8, 12};9 printf("main 里:%zu\n", sizeof prices);10 show_size(prices);11 return 0;12}
main.c: In function 'show_size':
main.c:4:36: warning: 'sizeof' on array function parameter 'a' will return size of 'int *' [-Wsizeof-array-argument]
4 | printf("函数里:%zu\n", sizeof a);
| ^
main.c:3:20: note: declared here
3 | void show_size(int a[]) {
| ~~~~^~~
main 里:20
函数里:8
在 main 里,prices 是 5 个 int,20 个字节;在函数里,a 是一个指针,8 个字节。gcc 还专门给了一条警告。所以要像 add_to_all 那样,把个数另外当参数传进去。
字符串为什么不能用 == 比
字符串是 char 数组,它的名字也是第一个字符的地址。第 14 课说过要讲:为什么字都一样,typed == saved 还是假。现在能看明白了:== 比的是两个地址,而两个数组永远在两个不同的地方。
1#include <stdio.h>2#include <string.h>3 4int main(void) {5 char saved[] = "secret";6 char typed[] = "secret";7 char *same = saved;8 printf("typed == saved:%d\n", typed == saved);9 printf("same == saved:%d\n", same == saved);10 printf("strcmp:%d\n", strcmp(typed, saved));11 return 0;12}
typed == saved:0 same == saved:1 strcmp:0
same 和 saved 指着同一个数组,所以相等。strcmp 顺着两个地址找过去,一个字符一个字符地比,比文字就该用它。
用指针走字符串,和走数组一样,只是不数个数,碰到 '\0' 就停:
1#include <stdio.h>2 3int main(void) {4 char word[] = "banana";5 int count = 0;6 for (char *p = word; *p != '\0'; p++) {7 if (*p == 'a') {8 count++;9 }10 }11 printf("%d\n", count);12 return 0;13}
3
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
用指针求和
✓让一个指针走过 steps,把步数加起来(不要用 steps[i]),显示 总数:25000。
总数:25000
1#include <stdio.h>2 3int main(void) {4 int steps[5] = {4000, 6000, 3000, 7000, 5000};5 int total = 0;6 7 printf("总数:%d\n", total);8 return 0;9}
数及格的人
✓count_pass 要数出 60 分及以上的有几个,可它在函数里用 sizeof 算个数,算错了。改成多一个参数 count,让程序显示 及格:4。
及格:4
1#include <stdio.h>2 3int count_pass(int scores[]) {4 int count = sizeof scores / sizeof scores[0];5 int pass = 0;6 for (int i = 0; i < count; i++) {7 if (scores[i] >= 60) {8 pass++;9 }10 }11 return pass;12}13 14int main(void) {15 int scores[6] = {58, 72, 91, 45, 60, 83};16 printf("及格:%d\n", count_pass(scores));17 return 0;18}
自己写 strlen
✓写一个函数 my_strlen:用指针沿着字符串往后走,走到 '\0' 为止,返回走过了几个字符。程序要显示 8。
8
1#include <stdio.h>2 3int my_strlen(char *s) {4 5}6 7int main(void) {8 printf("%d\n", my_strlen("keyboard"));9 return 0;10}
这一课学会了
- 数组名就当作第一个元素的地址用:
int *p = scores;。 p + 1是下一个元素的地址;C 按类型的大小往后挪。a[i]就是*(a + i),数组和指针都一样。- 可以用
p++走数组,走到紧跟在最后面的位置就停;字符串走到'\0'停。 - 数组传给函数传的是地址:函数能改它,但在函数里
sizeof得到的是指针的大小,所以个数要另外传。 - 两个字符串用
==比的是地址;比文字用strcmp。