C语言中的数组与指针学习笔记

在学习C语言过程中不可避免的就遇到了指针这种神奇的东西,在听课和学习的刚开始,我的感觉是:简单,指针就是一个套娃的变量而已。

图1 - 指针:来来来,给你亿点小小的指针震撼

1 - 人畜无害的普通指针

对于接触过一点点如同多功能水果刀的Python,和一点点如同青龙偃月刀的Java的我来说,刚开始接触C这块板砖的时候,我对它的理解还只停留在效率高这个概念上。

普普通通的变量,普普通通的数组。唯一有点奇怪的就是C语言没有字符串这总东西,不过字符串也就是char类型数组嘛,看起来也就是那么回事,下标访问一下呗。

指针就是把储存变量的地址储存到另一个变量里而已。声明并创建一个朴实无华的指针也并不难,无非就是用&取地址符(the address of)获取地址,然后把这个地址储存到一个指针类型的变量里就好了。

int number = 10;
int *p_number = &number;
代码1 - 声明一个指针

而读取指针,则有两种不同的结果,取决于咱是否使用了取值符(the value of)

printf("指针变量值为%p\n", p_number);
printf("指针所指向的值为%d", *p_number);
代码2 - 获取指针的值
指针变量值为0x7fff6cd5773c
指针所指向的值为10
代码2 - 结果

不过尔尔,这有何难,区区指针想让我屈服?指针听罢,微微一笑:“那要不咱上点强度?”

2 - 去内存整点数组

在数组和指针结合之前,两者看起来都是那么善良且无辜。 —— 我

一个普通的数组长得如下

int num_array[] = [1,3,5,7,9];
代码3 - 一个普通的数组

我们输出的时候就使用正常的for循环打印每个元素就好

#include <stdio.h>

int main()
{
    int num_array[] = {1,3,5,7,9};
    int arr_length = sizeof(num_array) / sizeof(num_array[0]);

    for (int i=0; i<arr_length; i++) {
        printf("Element %d: %d\n", i+1, num_array[i]);
    }
 
    return 0;
}
代码4 - 打印数组
Element 1: 1
Element 2: 3
Element 3: 5
Element 4: 7
Element 5: 9
代码4 - 结果

一切看起来都很平常。


指针:“嗨,你知道吗?数组可以通过指针访问哦~”

我:“你在说什么鬼东西?一个数组包含了那么多元素,你指哪啊?”

指针:“我只需要指向数组的第一个元素。数组开辟的内存空间是连续的,所以你要访问其他元素的时候,只需要按照元素类型的大小,依次读取后续的地址就好了。”

我:“我靠,有点道理啊,我试试”


int num_array[] = {1,3,5,7,9};
int *p_num_arr = &num_array;

printf("指针变量值为%p\n", p_number_arr);
printf("指针所指向的值为%d\n", *p_number_arr);
printf("数组首元素值为%d\n", num_arr[0]);
代码5 - 尝试通过指针访问数组
pointer.c: In function ‘main’:
pointer.c:6:22: warning: initialization of ‘int *’ from incompatible pointer type ‘int (*)[5]’
    6 |     int *p_num_arr = &num_array;
      |                      ^
代码5 - 编译!启动!
图片

我↑

图片

指针↑


指针:“小火汁不要捉急,你看你上边代码int *p_num_arr,这个指针保存的变量类型是int *,也就是说这个指针保存的是int类型变量的地址。但是你要往里存的是int [5]啊。你这样,把指针类型改成int (*p_num_arr)[5],这是一个存放有五个int类型元素的数组指针,紫腚行!”

我:“信你一回。”


#include <stdio.h>

int main()
{
    int num_array[] = {1,3,5,7,9};
    int (*p_num_arr)[5] = &num_array;

    printf("指针变量值为%p\n", p_num_arr);
    printf("指针所指向的值为%d\n", *p_num_arr);
    printf("数组首元素值为%d\n", num_array[0]);
    return 0;
}
代码6 - 把数组地址存到数组指针中,而不是普通指针中
pointer.c: In function ‘main’:
pointer.c:9:38: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘int *’ 
    9 |     printf("指针所指向的值为%d\n", *p_num_arr);
      |                             ~^     ~~~~~~~~~~
      |                             |      |
      |                             int    int *
代码6 - 编译!启动!
图片

我↑

图片

指针↑


指针:“其实这事也很好理解嘛,你看p_num_arr是一个int *[5]类型的指针变量,所以你用*p_num_arr访问的时候,其实是访问了整个数组,这就好比你敲了一行printf("%d", num_array)一样,我也不知道你想访问那个元素啊。”

我:“那我还得加下标访问呗?”

指针:“那不必yu~的!”

我:“那我直接num_array加下标就得了,用你干啥?”

指针:“也不是不可以。不过你后面写函数、结构体那些东西的时候,如果传入的参数是数组,编译器老哥全部都会给你搞成指针,到时候给你整疯了可别怪我哦。”

我:“。。。。那我倒要看看,你究竟是个什么东西!”


#include <stdio.h>

int main()
{
    int num_array[] = {1,3,5,7,9};
    int (*p_num_arr)[5] = &num_array;

    // 打印地址
    printf("指针的值为\t\t%p\n", p_num_arr);
    printf("指针存放的数组地址为\t%p\n", *p_num_arr);
    printf("使用&访问数组地址为\t%p\n", &num_array);
    // 指针指向数组第一个元素,也就是数组名
    printf("通过数组名访问地址为\t%p\n", num_array);
    printf("数组首元素地址为\t%p\n", &(num_array[0]));

    return 0;
}
代码7 - 数组地址一探到底
指针的值为                0xff928a08
指针存放的数组地址为        0xff928a08
使用&访问数组地址为        0xff928a08
通过数组名访问地址为        0xff928a08
数组首元素地址为           0xff928a08
代码7 - 结果

所以我们有5种访问数组首元素地址的方法,尽管这五种方法的结果是相同的,他们的类型却并不完全一样。

访问方式 变量类型 说明
num_array int [5] 通过名访问数组时,编译器会进行隐式转换,将数组变量解引用为指针,指向整个数组
&num_array int *[5] 获取数组的地址,也就是直接指向该数组的地址
int (*p_num_arr)[5]
p_num_arr
int *[5] 同上方,因为int (*p_num_arr)[5] = &num_array;
*p_num_arr int [5] p_num_arr进行解引用,也就是*(&num_array),等同于num_array
&(num_array[0]) int * num_array[0]是数组第一个元素,也就是int,这里相当于取一个变量的地址
表1 - 指针与数组

根据上面的数组首元素地址的访问方式,我们就可以得出一大堆使用指针访问数组内元素的方法了。我们来试一试。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int num[] = {1, 3, 5, 7, 9};

    // 使用整个数组指针获取元素
    int(*p_num_arr)[5] = &num;
    // 使用数组首元素指针读取元素
    int *p_num = num;

    // 打印数组第二个元素
    printf("(*p_num_arr)[1] = %d\n", (*p_num_arr)[1]);
    printf("(*(&num))[1] = %d\n", (*(&num))[1]);
    printf("num[1] = %d\n", num[1]);
    printf("p_num[1] = %d\n", p_num[1]);
    printf("*(num+1) = %d\n", *(num + 1));
    printf("*(p_num+1) = %d\n", *(p_num+1));

    return 0;
}

代码8 - 输出数组元素的不同方法
(*p_num_arr)[1] = 3
(*(&num))[1] = 3
num[1] = 3
p_num[1] = 3
*(num+1) = 3
*(p_num+1) = 3
代码8 - 结果

来详细分解一下

访问方式 解析
(*p_num_arr)[1] *p_num_arr指向整个数组的地址,[]对数组进行解引用,使指针指向第一个元素地址,1访问从首元素开始偏移的第一个元素,也就是数组的第二个元素
(*(&num))[1] 可以从两个角度进行理解:
1. &numint *[5]类型,指向整个数组,*对该地址进行解引用,将变为指向,获取了数组本身,也就是int[5],此时指针指向的依旧是数组首元素;而后[1]进行解引用,访问从首元素开始偏移的第一个元素,也就是数组第二个元素的值。
2. 可以理解为*(&num)中的取值符与取地址符互相抵消了,最终还是通过数组名访问了元素,变成了num[1]
num[1] 这个就是普通的通过数组名访问数组元素的方法
p_num[1] 可以从两个方面理解:
1. p_num = num;所以num[1]p_num[1]相同;
2. 编译器会将num隐式解引用为内存地址,[1]会到内从中偏移一个元素并进行读取,相当于*(0xff928a08 + 4 * 1),所以获取的是*(0xff928a0C)
*(num+1) 首先要知道+1并不是指数学运算上的+1,在这里是指偏移元素的个数。
num是数组元素首地址,+1是偏移一个元素,所以*(num+1)是「读取数组首元素偏移一个元素地址中的值」
*(p_num+1) 这个原理和上一个完全相同,p_num = num所以剩下的也是一样的
表2 - 指针与数组元素

我:“数组指针就是通过指针访问数组,而指针的本质是内存地址,所以只要搞明白里边的逻辑也就没什么难度了。还能再给力一点吗?”

指针:“我们来研究一下二维数组。”


首先我们来创建一个二维数组,然后尝试正常输出

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char name[2][20] = {{"Harry Potter"}, {"Draco Malfoy"}};

    for (int i = 0; i < 2; i++) {
        printf("%s\n", name[i]);
    }
    
    return 0;
}
代码9 - 二维数组常规用法
Harry Potter
Draco Malfoy
代码9 - 结果

经过飞速思考,我认为如果将二维数组存入指针,那与一维数组的会很类似,根据获取数组的方式不同,指针类型也会有所区别,我们来验证一下。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char name[2][20] = {{"Harry Potter"}, {"Draco Malfoy"}};
    
    return 0;
}

我们先来简单推测下:

  • name本身是一个char [2][20]类型的数组
  • 如果我想把这个数组保存到一个指针中,也就是&name,那指针类型用也应该是char (*p_name)[2][20]
  • 而如果我想通过数组名的方式将其首元素地址存入指针,也就是指针 = name这种方式,那这个指针的类型应该是其首元素的类型,也就是char [20]

写代码验证一下:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char name[2][20] = {{"Harry Potter"}, {"Draco Malfoy"}};
    char(*p_name_arr)[2][20] = &name;
    char (*p_name)[20] = name;

    printf("p_name_arr \t\t= %p\n", p_name_arr);
    printf("(*p_name_arr)[0] \t= %p\n", (*p_name_arr)[0]);
    printf("&name[0][0] \t\t= %p\n", &name[0][0]);

    printf("p_name \t\t\t= %p\n", p_name);
    printf("*p_name \t\t= %p\n", *p_name);
    printf("name \t\t\t= %p\n", name);
    return 0;
}
代码10 - 二维数组的地址
p_name_arr              = 0xff9060f4
(*p_name_arr)[0]        = 0xff9060f4
&name[0][0]             = 0xff9060f4
p_name                  = 0xff9060f4
*p_name                 = 0xff9060f4
name                    = 0xff9060f4
代码10 - 结果

这其实就是套娃而已,一个数组指针先从char *[2][20]退化为一个char [20]类型的数组,而后通过隐式转换,变为一个char *[20]类型的指针;再然后解引用为一个char类型的字符。

所以最后获得的地址就是这个二维数组第一个元素中的第一个元素的地址。所以说,如果我们尝试输出printf("%c\n", *name);编译肯定会报一个warning,但是输出却应该可以正常输出一个字符H,试试看。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char name[2][20] = {{"Harry Potter"}, {"Draco Malfoy"}};
    char(*p_name_arr)[2][20] = &name;
    char (*p_name)[20] = name;

    printf("%c\n", *name);
    return 0;
}
代码11 - 危险的尝试
4
代码11 - 结果

指针:“你是不是觉得自己很棒棒?你这种自作聪明的行为很容易造成严重的代码事故。”

我:“为啥?”

指针:“首先,写一个规范的代码才是一个程序员应该做的事情,明知道错了还非得写,这就叫自己作死;其次,你这段代码是一个未定义行为(undefined behavior),也就是说编译器看到你这段代码以后想的是:’你摆烂我也摆烂’,输出的结果完全是随机的。你现在调用的是printf还好说,编译器随便找了个内存地址给你打印出来了,万一你修改了某个数据,这个数据是程序中的关键变量,那你就等着吧。”


我们还是按照正常要求输出一下数组的数据吧

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char name[2][20] = {{"Harry Potter"}, {"Draco Malfoy"}};
    char(*p_name_arr)[2][20] = &name;
    char (*p_name)[20] = name;

    printf("(*p_name_arr)[1] = %s\n", (*p_name_arr)[1]);
    printf("*(p_name+1) = %s\n", *(p_name + 1));
    printf("*(name+1) = %s\n", *(name + 1));
    printf("name[1] = %s\n", name[1]);
    printf("p_name[1] = %s\n", p_name[1]);
    return 0;
}
代码12 - 指针输出字符串
(*p_name_arr)[1] = Draco Malfoy
*(p_name+1) = Draco Malfoy
*(name+1) = Draco Malfoy
name[1] = Draco Malfoy
p_name[1] = Draco Malfoy
代码12 - 结果

3 - 数组指针与指针数组

我们不论是在创建指针变量还是在使用指针变量的时候都在变量前加了(),这是因为要提高*这个符号的优先级,我们来看两行相当类似的代码:

int *p_num[10];
int (*p_num)[10];

这两行代码的区别在于:

  1. int *p_num[10]创建了一个数组,这个数组包含了10个int类型的指针。所以它是指针数组
  2. int (*p_num)[10]创建了一个指针,这个指针指向了一个长度为10的int类型数组。所以它是数组指针

数组指针我们上面一直在用,那什么时候使用指针数组呢?我请AI列出了几个常用的使用场景:

  1. 需要动态分配并存储不同大小数组时,指针数组很有用。每个指针可以指向不同大小的动态数组。
  2. 需要在数组中存储指向结构体或对象的指针时,可以使用指针数组。每个元素存储一个对象的指针。
  3. 实现不定长的参数列表,如printf中的va_listva_start,使用指针数组索引每个参数。
  4. 处理多维数组时,通过指针数组可简化多维数组的访问。如二维数组可以定义为int *arr2d[10];
  5. 开发一些容器类数据结构如链表、栈时,通常也会使用指针数组实现。
  6. 一些 lookup table 的实现也采用指针数组,表中的每个元素是一个指针。

[结束]

posted @ 2023-12-16 22:05  JustInCase  阅读(17)  评论(0编辑  收藏  举报