Skip to content

LV020-指针与数组

一、指针与一维数组

1. 数组的指针

数组元素的地址是指数组元素在内存中的起始地址,可以由各个元素加上取地址符号 & 组成,而数组名就代表了数组的起始地址。

数组指针 就是指向 数组起始地址 的指针,其本质就是指针,一维数组名可以看做是一维数组的指针(但是一维数组名不能像指针变量那样做自 ++ 和 -- 运算)

2. 数组元素的表示

我们直接来看实例:

c
#include <stdio.h>
int main(int argc, char *argv[])
{
    int a[3]  = {0, 1, 2};
    int *p = a;
    int i;
    printf("a[i]\t p[i]\t *(a+i)\t *(p+i)\t|");
    printf("\t   &a[i]\t    &p[i]\t     a+i\t     p+i \n");
    printf("---------------------------------------------------");
    printf("---------------------------------------------------\n");
    for(i = 0; i < 3; i++)
    {
        printf(" %d\t   %d\t    %d\t    %d\t|", a[i], p[i], *(a+i), *(p+i));
        printf("\t%p\t%p\t%p\t%p\n", &a[i], &p[i], a+i, p+i);
    }
 
    return 0;
}

image-20260211111431070

从上边实例可以看出:

  • a [i],p [i],*(p+i),*(a+i)四者等价,均可表示数组元素;

  • &a [i],&p [i],p+i, a+i 四者等价,均可表示相应数组元素地址;

元素表示 元素地址表示
a [i] ⇔ p [i] ⇔ *(p+i) ⇔ *(a+i)&a [i] ⇔ &p [i] ⇔ p+i ⇔ a+i
a [0] p [0] *(p+0)*(a+0)&a [0] &p [0] p+0 a+0
a [1] p [1] *(p+1)*(a+1)&a [1] &p [1] p+1 a+1
a [2] p [2] *(p+2)*(a+2)&a [2] &p [2] p+2 a+2
(1)指针变量和数组在访问数组中元素时,一定条件下其使用方法具有相同的形式,因为 **指针变量和数组名都是地址量**。

(2)指针变量和数组的指针(或叫数组名)在 本质上不同指针变量是地址变量,而 数组的指针是地址常量。例如 p++, p-- (正确); a++, a--(错误) 。

二、指针与二维数组

1. 数组名

我们可以直接将数组名作为指针进行遍历,但是需要注意,数组名不能做 ++ 和 -- 操作。

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[2][3]  = {{0, 1, 2}, {3, 4, 5}};
    int i, j;
    for(i = 0;i < 2; i++)
    {
        for(j = 0;j < 3; j++)
        {
            printf("a[%d][%d]=%d,&a[%d][%d]=%p | ",i, j, a[i][j], i, j, &a[i][j]); // 数组访问方式
            // 数组名转为指针,但要注意不能自加自减
            printf("*(a+%d)=0x%x, *(a+%d)+%d=0x%x, ", i, *(a + i), i, j, *(a + i) + j); 
            printf("*(*(a+%d)+%d)=%d\n", i, j, *(*(a + i) + j));
        }
    }
    return 0;
}

在 C 语言中,二维数组在内存中以 行优先顺序 连续存储。也就是说,数组元素按行依次排列,地址上排列顺序为 &a[0][0]&a[0][1]&a[0][2]&a[1][0]&a[1][1]&a[1][2]。地址差为 sizeof(int)(通常 4 字节),因此数组在内存中是一个扁平化的连续块。我们编译并执行上面的程序,会得到:

image-20260211114251639

*( *(a + i) + j ) 是通过指针访问二维数组的方式(其实这里是吧数组名当做了行指针,后面会详细学习),可以来看一下每部分都是什么:

  • a 是二维数组名,在表达式中会退化为指向第一行的指针(类型为 int (*)[3],即指向包含 3 个整数的数组的指针)。
  • a + i:指针算术运算,a 指向第一行,所以 a + i 指向第 i 行(步长为一行的大小,即 3 * sizeof(int))。
  • *(a + i):解引用得到第 i 行的首地址(类型退化为 int*,指向该行第一个元素)。
  • *(a + i) + j:在行内偏移 j 个元素(步长为 sizeof(int)),指向第 i 行第 j 列。
  • *(*(a + i) + j):最终解引用得到元素值。

2. 列指针遍历

2.1 一个实例

在 C 语言中 二维数组的元素连续存储,按行优先存储,所以自然可以使用一级指针来进行访问。

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[2][3]  = {{0, 1, 2}, {3, 4, 5}};
    int *p = a[0]; /* 不能直接写 int *p = a; 这样类型不匹配,会有警告 */
    int i, j;
    for(i = 0;i < 2; i++)
    {
        for(j = 0;j < 3; j++)
        {
            printf("a[%d][%d]=%d,&a[%d][%d]=%p | ",i, j, a[i][j], i, j, &a[i][j]);
            printf("*(p+%d)=%d,p+%d=%p\n",i * 3 + j, *(p + i * 3 + j), i * 3 + j, p + i * 3 + j);
        }
    }
    return 0;
}

image-20260211111634253

从例子中可以看出,一级指针 p 移动了 6 个数,到达 p + 5 时,将 a [2][3] 中的 6 个数访问完毕,相当于移动了 6 列,因此一级指针 p 也叫作 列指针。上面我们不能直接用写成 int *p = a; 因为 a 是二维数组名,在表达式中会退化为指向第一行的指针(类型为 int (*)[3],即指向包含 3 个整数的数组的指针),这样的话与 p 的指针类型不匹配了。

image-20260211114450260

2.2 列指针的定义

c
int *p = a[0];  /* 列指针的定义法 */ 
int *p = &a[0][0];

2. 行指针遍历

2.1 什么是行指针

在学习二维数组时,我们就知道二维数组可以看做是多个一维数组,对于 a[2][3] 来说,有如下表格中理解:

行名(代表了地址) 每行元素
a a [0] &a [0][0] a [0][0] a [0][1] a [0][2]
a + 1 a [1] &a [1][0] a [1][0] a [1][1] a [1][2]

在指针与一维数组中我们知道 a[i], p[i], *(p+i),*(a+i) 四者等价;&a[i], &p[i], p+i, a+i 四者等价,所以类似的有:

行名(代表了地址) 每行元素地址
第 1 行 a a [0] ⇔ *(a + 0)&a [0][0] a [0][0] a [0][1] a [0][2]
第 2 行 a + 1 a [1] ⇔ *(a + 1)&a [1][0] a [1][0] a [1][1] a [1][2]

所以,第 2 行第 2 列的元素就可以表示为:

a [1][1] ⇔ *(&a [1][0] + 1) ⇔ *(a [1] + 1) ⇔ *(*(a + 1) + 1)

上边的 a [0]、a [1] 代表了行地址,但是与一维数组名一样,是地址常量。指针变量存储的是地址,那么存储行地址的指针变量,被称之为 行指针

2.2 定义与赋值

2.2.1 行指针的定义
c
<存储类型> <数据类型> (*<指针变量名>)[表达式] ;

(1)存储类型指的是 auto, register, static, extern ,若省略,则默认为 auto 。

(2)赋值时要注意定义为行指针的指针变量才可以直接将数组名赋值给指针变量,否则会有警告。

例如:

c
int a[2][3]; 
int *p = a;       /* 会报警告*/
int (*p)[3] = a;  /* 正确方式*/

我们来分析一下这里的int (*p)[3]

  • 首先从 p 处开始,有()的存在,所以先与 * 结合,这说明 p 是一个指针;
  • 然后再与 [] 结合(与"()"这步可以忽略,只是为了改变优先级), 说明指针所指向的内容是一个数组,这里中括号里面是3,说明p指向了一个含有3个元素的数组;
  • 最后再与 int 结合, 说明数组里的元素是整型的。

所以 p 是一个指向由整型数据组成的数组的指针,这个语句的含义就是,定义了一个指针变量 p ,它可以指向一个含有 3 个 int 类型元素的数组。这里的p++会将会直接移动3个int类型的数据,指向下一个包含3个int类型数据的数组。

Tips:

(1)添加打印就会发现这种指针p中存放的是一个地址,且和*p的值相等,虽然他们在数值上相等,但是他们的类型和含义完全不同。

表达式类型含义64位机器下的大小++运算
pint (*)[3]指向整个数组的指针8p以数组为单位运算(p++将会直接跳过12字节)
*pint[3]int*指向数组首元素的指针12*p以元素为单位运算((*p)++跳过4字节)

p指向整个数组,且它是一个指针变量,*p指向包含了3个元素的数组,大小为3个元素整体的大小,就像是一维数组的数组名。

(2)是二级指针吗?感觉有点像。答案是:不是二级指针,它是一个数组指针

类型声明含义示例图示
数组指针int (*p)[3]指向数组的指针int arr[3] = {1, 2, 3};
int (*p)[3] = &arr; // p 指向 arr 整个数组
p ──────► arr[0] arr[1] arr[2]
(int(*)[3]) (int) (int) (int)
二级指针int **p指向指针的指针int a = 10;
int *ptr = &a;
int **pp = &ptr; // 二级指针:pp 指向 ptr 本身
pp ──────► ptr ──────►a
(int**) (int*) (int)
2.2.2 怎么赋值
  • 定义时初始化
c
// 一维数组
int arr[3] = {10, 20, 30};  // 定义一个包含3个int的数组
int (*p)[3] = &arr;         // p 指向数组 arr
printf("第一个元素: %d\n", (*p)[0]);  // 输出 10(*p 解引用得到数组,然后取索引0)

// 动态分配的数组
int (*p)[3] = malloc(sizeof(int[3])); // 动态分配一个包含3个int的数组
printf("第一个元素: %d\n", (*p)[0]);  // 输出 10(*p 解引用得到数组,然后取索引0)

// 二维数组
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 2行3列的二维数组
int (*p)[3] = matrix; // p 指向二维数组的第一行(类型为 int(*)[3])
printf("元素 [%d][%d] = %d\n", i, j, p[i][j]); // p[i] 是第i行的数组
  • 先定义后初始化
c
// 一维数组
int arr[3] = {10, 20, 30};  // 定义一个包含3个int的数组
int (*p)[3]; // 先定义指针
p = &arr;    // 后赋值(指向数组的地址)
printf("第一个元素: %d\n", (*p)[0]);  // 输出 10(*p 解引用得到数组,然后取索引0)

// 动态分配的数组
int (*p)[3];
p = malloc(sizeof(int[3])); // 动态分配一个包含3个int的数组
printf("第一个元素: %d\n", (*p)[0]);  // 输出 10(*p 解引用得到数组,然后取索引0)

// 二维数组
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 2行3列的二维数组
int (*p)[3];
p = &matrix[1]; // p 指向二维数组的第2行
printf("matrix[1][0] = %d\n", (*p)[0]);  // 输出 4

2.3 怎么使用行指针

若有 int a[2][3]; int (*p)[3] = a; 则有

a [i][j] ⇔ *(&a [i][0] + j) ⇔ *(a [i] + j) ⇔ *(*(a + i) + j)
p [i][j] ⇔ *(&p [i][0] + j) ⇔ *(p [i] + j) ⇔ *(*(p + i) + j)
#### 2.4 使用实例
2.4.1 示例demo
c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[2][3] = {{0, 1, 2}, {3, 4, 5}};
    int (*p)[3] = a;  /* 行指针:指向包含3个int的数组 */
    int i, j;
    printf("a=%p, &a=%p, a+1=%p\n", a, &a, a+1);
    printf("a[0]=%p, &a[0]=%p\n", a[0], &a[0]);
    printf("a[1]=%p, &a[1]=%p\n", a[1], &a[1]);
    printf("p=%p, &p=%p\n", p, &p);
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("a[%d][%d]=%d, &a[%d][%d]=%p | ", i, j, a[i][j], i, j, &a[i][j]);
            printf("*(*(p+%d)+%d)=%d, *(p+%d)+%d=%p\n", i, j, *(*(p + i) + j), i, j, *(p + i) + j);
        }
    }
    
    return 0;
}

将会得到以下输出信息:

image-20260211150123994

我们把相关的地址画出来:

image-20260211160947845

2.4.1 *(*(p + i) + j)分析

我们来分析一下这个*(*(p + i) + j)

  • p + i —— 行指针算术运算。p + 0指向第0行(等价于 &a[0]);p + 1指向第1行(等价于 &a[1])。行指针 p 的步长是一行的大小(3个int = 12字节)
md
假设 p = 0x1000:

p      → 0x1000 (指向第0行)
p + 1  → 0x100C (指向第1行,因为 0x1000 + 12)
  • *(p + i) —— 解引用行指针
操作类型含义
*(p + i)int [3]int *第i行的数组名,退化为指向首元素的指针

解引用后得到什么?

md
*(p + 0)  → a[0]  → &a[0][0]  (第0行首元素地址)
*(p + 1)  → a[1]  → &a[1][0]  (第1行首元素地址)

==> *(p + i) 等价于 a[i]
  • *(p + i) + j —— 指针算术运算
操作类型含义
*(p + i)int *第i行首元素地址
*(p + i) + jint *第i行第j列元素的地址

int * 类型的步长是 1个int的大小(4字节):

md
假设 *(p+0) = 0x1000:

*(p+0) + 0  → 0x1000  (a[0][0]的地址)
*(p+0) + 1  → 0x1004  (a[0][1]的地址)
*(p+0) + 2  → 0x1008  (a[0][2]的地址)
  • *(*(p + i) + j) —— 最终解引用
操作类型含义
*(p + i) + jint *第i行第j列元素的地址
*(*(p + i) + j)int第i行第j列元素的值
md
*(*(p + i) + j)  <==>  a[i][j]  <==>  p[i][j]
2.4.3 完整等价链
md
*(*(p + i) + j)

  *(p + i)     →  a[i]         (第i行的数组名)

  *(p + i) + j →  &a[i][j]     (第i行第j列的地址)

*(*(p + i) + j) →  a[i][j]     (第i行第j列的值)
2.4.4 小结
md
                    p (int (*)[3])


┌──────────────────────────────────────────────────────┐
│  第0行           │  第1行                             │
│  ┌──┬──┬──┐     │  ┌──┬──┬──┐                        │
│  │0 │1 │2 │     │  │3 │4 │5 │                        │
│  └──┴──┴──┘     │  └──┴──┴──┘                        │
│     ▲           │     ▲                              │
│     │           │     │                              │
│  *(p+0)         │  *(p+1)                            │
│  = a[0]         │  = a[1]                            │
│     │           │     │                              │
│     │  +j ──────┴─────┘                              │
│     │           │                                    │
│  *(p+0)+j = &a[0][j]                                 │
│     │                                                │
│     ▼                                                │
*(*(p+0)+j) = a[0][j]                               │
└──────────────────────────────────────────────────────┘

3. 行指针与列指针

对于如下定义的二维数组:

c
int a[2][3] = {{0, 1, 2}, {3, 4,5 }};

有如下说明

指针类型 表示形式 说明
行指针 a 或 a + 0 指向第 0 行
a + 1 指向第 1 行
列指针 a [0] 是一维数组的名称,也是整个数组的首地址。
第 0 行第 1 个元素(a [0][0])的地址。
a [0] + 1 第 0 行第 2 个元素(a [0][1])的地址
a [0] + 2 第 0 行第 3 个元素(a [0][2])的地址
a [1] 第 1 行第 1 个元素(a [1][0])的地址
a [1] + 1 第 1 行第 2 个元素(a [1][1])的地址
a [1] + 2 第 1 行第 3 个元素(a [1][2])的地址

可以将 列指针理解为行指针的具体元素,行指针理解为列指针的地址。 那么他们的具体关系就可以这样表示:

*行指针 ⇒ 列指针
&列指针 ⇒ 行指针

一般来说我们都是定义行指针,再转换为列指针进而访问数组具体元素,所以这里只写一个行指针转换为列指针的列子,列指针转行指针应该属于一个逆运算,但是意义好像不大,也基本没有遇到过。

行指针⇒ 列指针 列指针等价表示 指向内容 指向内容的等价表示 数组表示
a ⇒ *a a [0] *a [0] *(* a) a [0][0]
a+1 ⇒ *(a+1) a [1] *a [1] *(*(a+1)) a [1][0]

三、指针数组

1. 怎么定义

1.1 基本语法

指针数组 是指由若干个具有 相同存储类型和数据类型指针变量 构成的 集合,也就是说这个数组中的每个元素都是同类型的指针变量。一般声明形式:

c
<存储类型>  <数据类型>  *<指针数组名>[<指针数组大小>];

指针数组名 表示该指针数组的 起始地址

1.2 定义示例

c
int *pa[2]; /* 定义了一个长度为 2 的 int 型指针数组*/
char *pb[6];/* 定义了一个长度为 6 的 char 型指针数组*/

但是其实写成下边这种形式可能会更好理解一些,看个人吧。

c
int * pa[2]; /* 定义了一个长度为 2 的 int 型指针数组*/
char * pb[6];/* 定义了一个长度为 6 的 char 型指针数组*/

可以这样理解,对于 int * pa [2]; 来说,是声明了一个 int 类型的一维数组 pa [2] ,它包含了两个元素,每个元素都是一个 int 类型的指针。

2. 怎么初始化?

初始化写法 等价写法(赋值)
int a = 10;
int b = 20;
int * p [2] = {&a, &b};
int a = 10;
int b = 20;
int * p [2];
p [0] = &a;
p [1] = &b;

3. 内存分布

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 10;
    int b = 20;
    int * p[2] = {&a, &b};
	
    printf("a=%d, &a=%p\n", a, &a);
    printf("b=%d, &b=%p\n", b, &b);
    printf("&p=%p\n", &p);
    printf("  p=%p,    *p=%p,     **p=%d, p[0]=%p, &p[0]=%p, *p[0]=%d\n", p, *p, **p, p[0], &p[0], *p[0]);
    printf("p+1=%p,*(p+1)=%p, **(p+1)=%d, p[1]=%p, &p[1]=%p, *p[1]=%d\n", (p+1), *(p+1), **(p+1), p[1], &p[1], *p[1]);

    return 0;
}

image-20260211170906996

由程序结果可知变量在内存中存储如下图所示:

image-20260211171037520

指针变量所占空间是固定的,在 32 位系统中就是 4 个字节,而在 64 位系统中则是 8 个字节,由于这里是 64 位系统,所以指针数组名加 1 ,就会移动 8 个字节。

4. 指针数组的数组名

对于指针数组的数组名,它代表了指针数组的起始地址。由于数组元素已经是指针了,而数组名又是数组首元素的地址,因此指针数组名就是指针数组中 首个指针元素的地址,所以指针数组名是一个 多级指针,具体来说就是一个二级指针。

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 10;
    int b = 20;
    int * p[2] = {&a, &b};
    int * *q;

    q = p;
    printf("sizeof(p)=%ld,sizeof(q)=%ld\n",sizeof(p), sizeof(q));
    printf("q=%p,&q=%p,&q[0]=%p,&q[1]=%p\n", q, &q, &p[0],&q[1]);
    printf("p=%p,&p=%p,&p[0]=%p,&p[1]=%p\n", p, &p, &p[0],&p[1]);
    printf("a=%d,&a=%p,p[0]=%p,*p[0]=%d,**p=%d\n", a, &a, p[0], *p[0], **p);
    printf("b=%d,&b=%p,p[1]=%p,*p[1]=%d,**(p+1)=%d\n", b, &b, p[1], *p[1], **(p+1));

    return 0;
}

image-20260211165116029

【注意】

(1)从另一个方面理解,在一维数组中我们知道 a [i], p [i], *(p+i),*(a+i)四者等价,所以直接将 p[i] 替换为 *(p+i) 那么也可以得到上边的结果。

(2)通过 sizeof(指针数组名) 可以求得指针数组占据的总空间,而每个元素所占空间与系统相关。

5. 指针数组与二维数组

考虑到二维数组可以通过行指针来访问,这样的话通过指针数组来存储二维数组的行指针,那么这样就可以通过指针数组遍历二维数组了。

image-20220204204857551
  • 通过指针数组遍历二维数组
c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a [2][3] = {{0, 1, 2}, {3, 4, 5}};
    int * p[2] = {a[0], a[1]};
    int i, j;
    for(i = 0; i < 2; i++)
    {
        for(j = 0; j < 3; j++)
        {
            printf("a[%d][%d]=%d,p[%d][%d]=%d,", i, j, a[i][j], i, j, p[i][j]);
            printf("*(p[%d] + %d)=%d,*(*(p + %d) + %d)=%d\n", i, j, *(p[i] + j), i, j, *(*(p + i) + j));
        }
    }
    
    return 0;
}

image-20260211171152293