Kodewick
第 17 课,共 30 课目录

指针和数组

第 17 课,共 30 课 · 第 3 部分 · 指针 · 约 15 分钟

数组的元素在内存里是一个挨一个排着的。所以在 C 里,指针和数组是近亲。大多数时候,数组的名字就当作它第一个元素的地址来用:

main.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 会按指针的类型自己算。

main.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
main.c
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 一到那里就停,那里的东西不能读。

main.c
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}

数组传给函数

把数组传给函数,数组并不会复制一份,函数拿到的是第一个元素的地址。所以函数能改外面的数组;也所以作为参数,int a[] 和 int *a 是一个意思:

main.c
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,得到的是指针的大小,不是数组的大小:

main.c
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 还是假。现在能看明白了:== 比的是两个地址,而两个数组永远在两个不同的地方。

main.c
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' 就停:

main.c
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

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

用指针求和

让一个指针走过 steps,把步数加起来(不要用 steps[i]),显示 总数:25000。

要显示
总数:25000
exercise1.c
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

数及格的人

count_pass 要数出 60 分及以上的有几个,可它在函数里用 sizeof 算个数,算错了。改成多一个参数 count,让程序显示 及格:4。

要显示
及格:4
exercise2.c
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

自己写 strlen

写一个函数 my_strlen:用指针沿着字符串往后走,走到 '\0' 为止,返回走过了几个字符。程序要显示 8。

要显示
8
exercise3.c
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 数组名就当作第一个元素的地址用:int *p = scores;。
  • p + 1 是下一个元素的地址;C 按类型的大小往后挪。
  • a[i] 就是 *(a + i),数组和指针都一样。
  • 可以用 p++ 走数组,走到紧跟在最后面的位置就停;字符串走到 '\0' 停。
  • 数组传给函数传的是地址:函数能改它,但在函数里 sizeof 得到的是指针的大小,所以个数要另外传。
  • 两个字符串用 == 比的是地址;比文字用 strcmp。