C语言的数组和指针

在 C 语言中,数组和指针是两个关系密切但也不容易弄清楚的两个概念。本文主要基于一维和二维数组来展示他们的关系,其中关于二维数组的内容也可以适用到多维。

本文使用 gdb 作为交互式的实验工具,在文章的实验展示中以 (gdb) 开头的部分是输入,没有该前缀的是输出。文章内容和实验中都以整型(int)数组为例,同样可以适用到其他类型。

一维数组

在 C 语言中定义数组是比较容易的,可以使用 int arr[5] = {1, 2, 3, 4, 5}; 来定义一个长度为 5,其中的数据为从 1 到 5 的数组。

一维数组的内存结构

C 语言的数组在内存中是一块连续存放的区域,按照顺序存放其中的元素。前面例子中的数组 arr 在内存中的表示如下:

arr:|1|2|3|4|5|

通过下标去访问的时候,就会取出对应位置的数据比如 arr[2] 就是 3。

指向一维数组的指针

总体来说,指向数组的指针类型就是数组降一维后的类型。比如指向一维数组的指针类型就是 int*

int* p = arr; 这里 p 就指向了 arr 的起始位置,这个声明也表示着 p 指向的内容是一个整型。可以使用 * 来解引用取得该内存位置的值,也可以通过指针运算来让指针指向后续的位置。

// 测试代码
int main() {
    int arr[5] = {1,2,3,4,5};
    int (*p) = arr;
    return 0;
}
# 实验结果
(gdb) print p
$1 = (int *) 0x7ff7bfeff3f0
(gdb) print *p
$2 = 1
(gdb) print *(p+2)  # 从 p 的位置向后移动 2 个元素(int)
$3 = 3

二维数组

二维数组的内存结构

计算机中的内存地址是一维的形式,地址从低向高增长,在数组也同是一维的情况下只需要顺序排列。不过在数组增加到二维以后就涉及到了如何将超过一维的数据存放在一维的空间里。比较直接的方法就是将多维的数组拉平,也用一维的形式来储存。

比如定义了一个二维数组 int arr1[2][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 0}}; ,这个二维数组中有 10 个元素,在内存中这 10 个元素其实就是顺序连续存放下来的,在内存中开起来就像是一个一维的长度为 10 的数组。

arr1: |1|2|3|4|5|6|7|8|9|0|

也就是说,通过int arr1[2][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 0}}; 这种方式定义的二维数组,在内存中与 int arr2[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; 的一维数组是相同的。

行优先和列优先

上一节中提到多维数组在内存中其实就是一个连续的一维数组的形式,不过在介绍时有一个预设前提,就是将二维数组放在一维内存中时是按照行优先的规则进行的。也就是 {1, 2, 3, 4, 5}在前,{6, 7, 8, 9, 0}在后。

与此相对的就是列优先,就是按照列的顺序来存放,对于 arr1 来说,如果按照列优先来存放,那么在内存中就是 |1|6|2|7|3|8|4|9|5|0| 这样的顺序了。

C 语言中的数组是按照行优先的规则存放在内存中的。

指向二维数组的指针

指向二维数组的指针声明方法为 int (*p1)[5] = arr1; 其中的关键是 p1 后面指定了长度。这个p1 指针可以理解为是“指向了一个长度为 5 的整型数组的指针”。当然按照这个理解更好的写法应该是 int[5] *p = arr1; 不过 C 语言编译器不支持这种写法,所以还是老老实实按照标准来写代码。

既然 p1 指向了一个长度为 5 的数组,那么对 p1 进行的指针运算就会按照数组的方式进行。比如 p1 + 1 指向的是 arr1 中下一个长度为 5 的数组。也就是说对 p1 进行的运算都是行级别的运算,那么如果想取到某列的数据要怎么办呢?可以在行级别的运算结束后再解引用后对列的位置进行运算,下面通过例子加强下认识。

int main() {
    int arr1[2][5] = {
        {1,2,3,4,5}, {6,7,8,9,0}
    };

    int (*p1)[5] = arr1;
    return 0;
}
(gdb) print p1
$1 = (int (*)[5]) 0x7ff7bfeff3d0
(gdb) print *p1
$2 = {1, 2, 3, 4, 5}
(gdb) print p1+1
$3 = (int (*)[5]) 0x7ff7bfeff3e4  # 与 $1 中打印出来的地址进行比较,可以发现地址增长了 0x14 也就是 20 个字节,正好是 5 个 int 的内存大小
(gdb) print *(p1+1)
$4 = {6, 7, 8, 9, 0}
(gdb) print *(*(p1+1)+2)  # p1+1 是一个指向长度为 5 的数组的指针,*(p1+1) 解了一次引用就变成指向这个长度为 5 的数组的第一个元素的指针,再加 2 就是这一行第 3 个元素的指针了
$5 = 8

更进一步

我们现在已经明白即使是 arr1 这种多维的数组,在内存中也是以连续的、一维的一串整数形式保存的。那么我们能不能直接定义一个整型的指针,按照内存中的顺序去遍历呢?

比如可以有 int* p2 = arr1; for (int i = 0; i < 10; ++i) *(p2+i);这种写法吗?

实际上是不可以的,上面的写法会导致编译报错,不过这也体现了 C 语言的安全性,因为标准的写法中没有丢失维度信息,比如通过 p1 的定义我们可以知道指向的是长度为 5 的数组,而通过 p2 则毫无头绪。

不过,既然是做实验那么就使用下不正规的方式吧。在 C 语言中,void* 类型的指针没有携带任何类型信息,可以在任何类型间进行转换,因此通过 void* 的指针进行过渡可以得到类似的效果。

首先改造下上面的代码,增加一个 void* 的指针 p2 指向 arr1后进行实验。

int main() {
    int arr1[2][5] = {
        {1,2,3,4,5}, {6,7,8,9,0}
    };

    int (*p1)[5] = arr1;
    void* p2 = arr1;
    return 0;
}
(gdb) print p2
$6 = (void *) 0x7ff7bfeff3d0  # 与 p1 的地址相同,因为都指向一个位置
(gdb) print *(int*)p2  # 将 p2 强制转换成了指向 int 的指针,可以成功读取该位置的内容
$7 = 1
(gdb) print *((int*)p2+1)  # 转换成 int* 的指针后进行指针运算,加 1 就是指向下一个 int 数据
$8 = 2
(gdb) print *((int*)p2+8)  # 虽然 arr1 一行只有 5 个元素,但是由于在内存中是顺序一维的排列,所以可以通过指针运算跨行来取到对应位置的值
$9 = 9

数组变量

最后,数组变量本身也是可以像指针一样进行运算和解引用的。比如要访问 arr 中的第二个元素,不但可以使用 arr[1] ,也可以使用 *(arr+1) 的形式。

要访问 arr1 中第一行第二个元素,不但可以使用 arr1[0][1],也可以使用 *(*(arr1)+1) 的形式。

参考文章

http://c.biancheng.net/view/368.html

https://www.geeksforgeeks.org/how-does-c-allocate-memory-of-data-items-in-a-multidimensional-array/

https://stackoverflow.com/questions/2565039/how-are-multi-dimensional-arrays-formatted-in-memory

posted @ 2023-04-03 01:53  RedAppleJuice  阅读(141)  评论(0)    收藏  举报