Post

【C++】二维数组的行指针和列指针

在C++中,数组与指针有密切的关系。任何通过数组和下标实现的表达式可等价地通过指针和偏移量实现。下面首先介绍一维数组与指针的关系,之后介绍二维数组的行指针和列指针。

1.一维数组和指针

定义一个长度为3的一维数组a

1
int a[3] = {1, 2, 3};

假设其首元素地址为0x9F36FAE0,则数组元素在内存中的示意图如下图所示:

一维数组内存示意图

1.1 指向数组元素的指针

指向数组元素的指针声明和初始化方式如下:

1
int* p = a;  // 等价于int* p = &a[0];

这里声明了一个指向数组a的首元素的指针p

由于在C++中数组名就是数组首元素的地址,因此可以直接使用数组名来初始化指针,也可以显式地取数组元素的地址(如&a[0])。

一维数组-指向数组元素的指针

对于数组a和指向其首元素的指针p,有以下等价形式:

含义等价形式类型
首元素的值a[0]*ap[0]*pint1
首元素的地址&a[0]a&p[0]pint*0x9F36FAE0
第i个元素的值a[i]*(a+i)p[i]*(p+i)int3 (i = 2)
第i个元素的地址&a[i]a+i&p[i]p+iint*0x9F36FAE8 (i = 2)

★根据C++地址算术运算的定义,无论a是数组名还是指针,都有

  • a[i]等价于*(a+i)
  • &a[i]等价于a+i

1.2 指向数组的指针

指向数组的指针声明和初始化方式如下:

1
int (*q)[3] = &a;

这里声明了一个指向数组a 本身的指针q,因此*q等价于a

注意:这里的圆括号不能省略,否则int *q[3]声明的是“有3个元素的整型指针数组”,而不是“指向有3个元素的整型数组的指针”。

一维数组-指向数组的指针

对于数组a和指向其本身的指针q,有以下等价形式:

含义等价形式类型
首元素的值a[0]*a(*q)[0]**qint1
首元素的地址&a[0]a&(*q)[0]*qint*0x9F36FAE0
第i个元素的值a[i]*(a+i)(*q)[i]*(*q+i)int3 (i = 2)
第i个元素的地址&a[i]a+i&(*q)[i]*q+iint*0x9F36FAE8 (i = 2)
数组的地址&aqint(*)[3]0x9F36FAE0

注意:&a在数值上等于a,但二者的类型不同,如下表所示:

指针指针类型元素类型sizeof元素指针值
aint[3]int40x9F36FAE0
a+1int[3]int40x9F36FAE4
pint*int40x9F36FAE0
p+1int*int40x9F36FAE4
&aint(*)[3]int[3]120x9F36FAE0
&a+1int(*)[3]int[3]120x9F36FAEC
qint(*)[3]int[3]120x9F36FAE0
q+1int(*)[3]int[3]120x9F36FAEC

&aq是指向数组的指针,因此&a+1q+1直接到达数组末尾的下一个位置。

1.3 验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <cstdio>

using namespace std;

int main() {
    int a[3] = {1, 2, 3};
    int* p = a;
    int (*q)[3] = &a;

    printf("第i个元素的值:\n");
    printf("%s %10s %10s %10s %10s %10s %10s\n", "i", "a[i]", "*(a+i)", "p[i]", "*(p+i)", "(*q)[i]", "*(*q+i)");
    for (int i = 0; i < 3; ++i)
        printf("%d %10d %10d %10d %10d %10d %10d\n", i, a[i], *(a + i), p[i], *(p + i), (*q)[i], *(*q + i));
    putchar('\n');

    printf("第i个元素的地址:\n");
    printf("%s %10s %10s %10s %10s %10s %10s\n", "i", "&a[i]", "a+i", "&p[i]", "p+i", "&(*q)[i]", "*q+i");
    for (int i = 0; i < 3; ++i)
        printf("%d %10p %10p %10p %10p %10p %10p\n", i, &a[i], a + i, &p[i], p + i, &(*q)[i], *q + i);
    putchar('\n');

    printf("指针的值:\n");
    printf("%10s %10s %10s\n", "指针", "类型", "值");
    printf("%10s %10s %10p\n", "a", "int[3]", a);
    printf("%10s %10s %10p\n", "a+1", "int[3]", a + 1);
    printf("%10s %10s %10p\n", "p", "int*", p);
    printf("%10s %10s %10p\n", "p+1", "int*", p + 1);
    printf("%10s %10s %10p\n", "&a", "int(*)[3]", &a);
    printf("%10s %10s %10p\n", "&a+1", "int(*)[3]", &a + 1);
    printf("%10s %10s %10p\n", "q", "int(*)[3]", q);
    printf("%10s %10s %10p\n", "q+1", "int(*)[3]", q + 1);
    return 0;
}

输出如下(指针的具体值会随每次运行变化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
第i个元素的值:
i       a[i]     *(a+i)       p[i]     *(p+i)    (*q)[i]    *(*q+i)
0          1          1          1          1          1          1
1          2          2          2          2          2          2
2          3          3          3          3          3          3

第i个元素的地址:
i      &a[i]        a+i      &p[i]        p+i   &(*q)[i]       *q+i
0   9F36FAE0   9F36FAE0   9F36FAE0   9F36FAE0   9F36FAE0   9F36FAE0
1   9F36FAE4   9F36FAE4   9F36FAE4   9F36FAE4   9F36FAE4   9F36FAE4
2   9F36FAE8   9F36FAE8   9F36FAE8   9F36FAE8   9F36FAE8   9F36FAE8

指针的值:
      指针       类型         值
         a     int[3]   9F36FAE0
       a+1     int[3]   9F36FAE4
         p       int*   9F36FAE0
       p+1       int*   9F36FAE4
        &a  int(*)[3]   9F36FAE0
      &a+1  int(*)[3]   9F36FAEC
         q  int(*)[3]   9F36FAE0
       q+1  int(*)[3]   9F36FAEC

2.二维数组和指针

定义一个大小为2×3的二维数组a

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

假设其首元素地址为0x19AFFC40。

二维数组在概念上可理解为矩阵:

二维数组矩阵示意图

但二维数组在内存中的实际存储形式是按行的顺序依次存储,如下图所示:

二维数组内存示意图

可以看出,二维数组在内存中是以类似于一维数组的形式存储的,元素a[i][j]的偏移量可使用公式i*列数+j计算。

虽然二维数组并不是以矩阵的形式存储的,但二维数组的每一行(每个元素)就是一个一维数组,在这一点上与矩阵的概念是一致的。

2.1 地址运算

由于二维数组就是一维数组的数组,元素类型是一维数组,数组名仍然是首元素的地址。在这里“首元素”就是二维数组的首行,因此a是首行的地址。使用数组名a可进行如下的地址运算。

2.1.1 行

含义等价形式类型数组元素
首行a[0]*aint[3]{1, 2, 3}0x19AFFC40
首行的地址&a[0]aint(*)[3] 0x19AFFC40
第i行a[i]*(a+i)int[3]{4, 5, 6} (i = 1)0x19AFFC4C (i = 1)
第i行的地址&a[i]a+iint(*)[3] 0x19AFFC4C (i = 1)

由此可以看出,即使数组元素的类型变成了一维数组,1.1节最后的结论仍然成立。

2.1.2 数组元素

含义等价形式类型
首行首列元素的值a[0][0]*a[0](*a)[0]**aint1
首行首列元素的地址&a[0][0]a[0]*aint*0x19AFFC40
第i行首列元素的值a[i][0]*a[i](*(a+i))[0]**(a+i)int4 (i = 1)
第i行首列元素的地址&a[i][0]a[i]*(a+i)int*0x19AFFC4C (i = 1)
第i行第j列元素的值a[i][j]*(a[i]+j)(*(a+i))[j]*(*(a+i)+j)int6 (i = 1, j = 2)
第i行第j列元素的地址&a[i][j]a[i]+j*(a+i)+jint*0x19AFFC54 (i = 1, j = 2)

注意:&a[0][0]a[0]&a[0]a&a(上面未出现)在数值上均相等,但类型不同,如下表所示:

指针指针类型元素类型sizeof元素指针值
&a[0][0]int*int40x19AFFC40
&a[0][0]+1int*int40x19AFFC44
a[0]int[3]int40x19AFFC40
a[0]+1int[3]int40x19AFFC44
&a[0]int(*)[3]int120x19AFFC40
&a[0]+1int(*)[3]int120x19AFFC4C
aint[2][3]int[3]120x19AFFC40
a+1int[2][3]int[3]120x19AFFC4C
&aint(*)[2][3]int[2][3]240x19AFFC40
&a+1int(*)[2][3]int[2][3]240x19AFFC58

2.2 行指针

二维数组行指针的声明和初始化方式如下:

1
int (*q)[3] = a;  // 等价于int (*q)[3] = &a[0];

这里声明了一个指向二维数组a首行的指针q,因此*q等价于a[0],如下图所示:

二维数组-行指针

从语法上可以看出,行指针本质上就是指向一维数组的指针。二维数组a的首行a[0]就是一个长度为3的一维数组,因此可以使用其地址&a[0]来初始化行指针q。另一方面,因为数组名就是首元素的地址,因此a等价于&a[0]

对于二维数组a和指向其首行的指针q,有以下等价形式:

含义等价形式类型数组元素
首行a[0]q[0]*qint[3]{1, 2, 3}0x19AFFC40
首行的地址&a[0]a&q[0]qint(*)[3] 0x19AFFC40
第i行a[i]q[i]*(q+i)int[3]{4, 5, 6} (i = 1)0x19AFFC4C (i = 1)
第i行的地址&a[i]&q[i]q+iint(*)[3] 0x19AFFC4C (i = 1)

与数组元素相关的表达形式这里不再列举,直接将a替换为q即可。

2.3 列指针

二维数组列指针的声明和初始化方式如下:

1
int* p = &a[0][0];

这里声明了一个指向二维数组a首行首列元素的指针p,如下图所示:

二维数组-列指针

可以看出,列指针就是指向数组元素的普通指针。通过列指针可以将二维数组当作一维数组、利用偏移量计算公式来访问数组元素。

对于二维数组a和指向其首行首列元素的指针p,有以下等价形式:

含义等价形式类型
首行首列元素的值a[0][0]p[0]*pint1
首行首列元素的地址&a[0][0]&p[0]pint*0x19AFFC40
第i行首列元素的值a[i][0]p[i*3]*(p+i*3)int4 (i = 1)
第i行首列元素的地址&a[i][0]&p[i*3]p+i*3int*0x19AFFC4C (i = 1)
第i行第j列元素的值a[i][j]p[i*3+j]*(p+i*3+j)int6 (i = 1, j = 2)
第i行第j列元素的地址&a[i][j]&p[i*3+j]p+i*3+jint*0x19AFFC54 (i = 1, j = 2)

注:

  • 使用列指针时不存在“行”的概念,因为对于列指针来说,二维数组a相当于长度为6的一维数组。
  • 通过列指针访问元素时(p[i*3+j])用到了列数3,而通过行指针或者数组名+下标访问元素时(a[i][j])却不需要,好像编译器能够“自动”知道列数。这是因为数组的声明类型int a[2][3]中包含了列数信息——这就是为什么在定义二维数组或者声明函数参数时可以省略行数,但必须指定列数的原因,即可以声明为int a[][3],但不能是int a[][]

2.4 验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <cstdio>

using namespace std;

int main() {
    int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
    int (*q)[3] = a;    // 行指针
    int* p = &a[0][0];  // 列指针

    printf("第i行的地址\n");
    printf("%s %10s %10s %10s %10s\n", "i", "&a[i]", "a+i", "&q[i]", "q+i");
    for (int i = 0; i < 2; ++i)
        printf("%d %10p %10p %10p %10p\n", i, &a[i], a + i, &q[i], q + i);
    putchar('\n');

    printf("第i行第j列元素的值\n");
    printf("%s %s %10s %10s %10s %10s\n", "i", "j", "a[i][j]", "*(*(a+i)+j)", "p[i*3+j]", "*(p+i*3+j)");
    for (int i = 0; i < 2; ++i)
        for (int j = 0; j < 3; ++j)
            printf("%d %d %10d %10d %10d %10d\n", i, j, a[i][j], *(*(a + i) + j), p[i * 3 + j], *(p + i * 3 + j));
    putchar('\n');

    printf("第i行第j列元素的地址\n");
    printf("%s %s %10s %10s %10s %10s\n", "i", "j", "&a[i][j]", "*(a+i)+j", "&p[i*3+j]", "p+i*3+j");
    for (int i = 0; i < 2; ++i)
        for (int j = 0; j < 3; ++j)
            printf("%d %d %10p %10p %10p %10p\n", i, j, &a[i][j], *(a + i) + j, &p[i * 3 + j], p + i * 3 + j);
    putchar('\n');

    printf("指针的值:\n");
    printf("%10s %15s %10s\n", "指针", "类型", "值");
    printf("%10s %15s %10p\n", "&a[0][0]", "int*", &a[0][0]);
    printf("%10s %15s %10p\n", "&a[0][0]+1", "int*", &a[0][0] + 1);
    printf("%10s %15s %10p\n", "a[0]", "int[3]", a[0]);
    printf("%10s %15s %10p\n", "a[0]+1", "int[3]", a[0] + 1);
    printf("%10s %15s %10p\n", "&a[0]", "int(*)[3]", &a[0]);
    printf("%10s %15s %10p\n", "&a[0]+1", "int(*)[3]", &a[0] + 1);
    printf("%10s %15s %10p\n", "a", "int[2][3]", a);
    printf("%10s %15s %10p\n", "a+1", "int[2][3]", a + 1);
    printf("%10s %15s %10p\n", "&a", "int(*)[2][3]", &a);
    printf("%10s %15s %10p\n", "&a+1", "int(*)[2][3]", &a + 1);
    return 0;
}

输出如下(指针的具体值会随每次运行变化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
第i行的地址
i      &a[i]        a+i      &q[i]        q+i
0   19AFFC40   19AFFC40   19AFFC40   19AFFC40
1   19AFFC4C   19AFFC4C   19AFFC4C   19AFFC4C

第i行第j列元素的值
i j    a[i][j] *(*(a+i)+j)   p[i*3+j] *(p+i*3+j)
0 0          1          1          1          1
0 1          2          2          2          2
0 2          3          3          3          3
1 0          4          4          4          4
1 1          5          5          5          5
1 2          6          6          6          6

第i行第j列元素的地址
i j   &a[i][j]   *(a+i)+j  &p[i*3+j]    p+i*3+j
0 0   19AFFC40   19AFFC40   19AFFC40   19AFFC40
0 1   19AFFC44   19AFFC44   19AFFC44   19AFFC44
0 2   19AFFC48   19AFFC48   19AFFC48   19AFFC48
1 0   19AFFC4C   19AFFC4C   19AFFC4C   19AFFC4C
1 1   19AFFC50   19AFFC50   19AFFC50   19AFFC50
1 2   19AFFC54   19AFFC54   19AFFC54   19AFFC54

指针的值:
      指针            类型         值
  &a[0][0]            int*   19AFFC40
&a[0][0]+1            int*   19AFFC44
      a[0]          int[3]   19AFFC40
    a[0]+1          int[3]   19AFFC44
     &a[0]       int(*)[3]   19AFFC40
   &a[0]+1       int(*)[3]   19AFFC4C
         a       int[2][3]   19AFFC40
       a+1       int[2][3]   19AFFC4C
        &a    int(*)[2][3]   19AFFC40
      &a+1    int(*)[2][3]   19AFFC58

3.n维数组和指针

行指针和列指针的概念可以推广到n维数组:

  • n维数组的行指针就是指向n-1维数组的指针。
  • n维数组的列指针就是指向数组元素的普通指针。

例如:

1
2
3
4
5
6
int a[2][3][4] = {
    { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} },
    { {13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24} }
};
int (*q)[3][4] = a;
int* p = &a[0][0][0];

这段代码声明了一个三维数组a以及指向其“首行”的行指针q和指向其首元素的列指针p,则a[i][j][k]分别等价于q[i][j][k]p[((i*3)+j)*4+k]

4.应用

下面的程序分别使用行指针和列指针遍历二维数组,并展示了如何将行指针和列指针传递给函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>

using namespace std;

void print_2d_array(int a[][3], int m, int n);
void print_2d_array_row_pointer(int (*q)[3], int m, int n);
void print_2d_array_col_pointer(int* p, int m, int n);

int main() {
    int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
    int (*q)[3] = a;
    int* p = &a[0][0];

    print_2d_array(a, 2, 3);
    print_2d_array_row_pointer(q, 2, 3);
    print_2d_array_col_pointer(p, 2, 3);
    return 0;
}

void print_2d_array(int a[][3], int m, int n) {
    cout << "直接打印二维数组:" << endl;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j)
            cout << a[i][j] << ' ';
        cout << endl;
    }
}

void print_2d_array_row_pointer(int (*q)[3], int m, int n) {
    cout << "使用行指针打印二维数组:" << endl;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j)
            cout << q[i][j] << ' ';
        cout << endl;
    }
}

void print_2d_array_col_pointer(int* p, int m, int n) {
    cout << "使用列指针打印二维数组:" << endl;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j)
            cout << p[i * n + j] << ' ';
        cout << endl;
    }
}

5.参考

二维数组与指针(详解)

This post is licensed under CC BY 4.0 by the author.