20201123-C语言补充-二维数组-再谈二维数组与指针

二维数组

1 一维数组中的指针数组数组的指针区别

1.1 指针数组

int *p[N];

读解,此时,[]优先级比*高,先结合进行运算,即首先,它成为了一个数组。

  • 它的大小就是N,即可以存放N个元素

但是什么元素呢?

  • int *这玩艺儿一看就眼熟了,这不就是整型指针吗?是的,就是整型指针
  • 也就是说,元素是整型指针
  • 故叫指针数组

说白了,它仍然是数组。就这里的这个定义来看,它和普通的整型数组不一样。因为,它里面只能装==指向整型变量的指针==。

1.2 数组的指针,即行指针

前面的叫指针数组,那么这个数组的指针能不能简称为数组指针呢?我个人觉得不可以。

但确实也有人学着学着,就这么叫了。

先看具体的一个示例

int (*p)[N];

大家都知道,()的优先级最高,那么,它里面的表达式优先运算,则计算结果就是:

  • *p是指针,再往前看,就是int,也就是说,是指针的基类型是整型

  • []是数组的标识,前面是指针,然后又做成了数组,这就不好理解了

    • 数组内有N个元素,基类型肯定也是整型
    • 普通情况,()这个位置里,应该是一个变量名,而这时,变成了一个指针
    • 所以,它是一个数组的指针,含义是定义了一个指向N个元素的一维数组的指针

1.3 指针可以指向什么?

int a;		// 变量===本质上就是一个单位的整型内存空间
int *p;		// 变量的指针
p = &a;		// 让指针指向对应的内存空间


int arr[N];		// 数组
int (*p)[N];	// 数组的指针
p = &arr;		// 指针指向数组对应的内存空间【的首地址】

可以明确地看到,什么样的指针,指向什么样的内存空间,必须对应,才可以完成指向,否则,编译出错。

1.4 再看指针数组

#include<stdio.h>
#define N 5
int main(void)
{
    
    
    int *p[N];	// 定义指针数组,也就是说,这是定义了一组指针
    int a = 10;
    int b = 20;
    int c = 30;

    // 一个个完成指针数组里的元素的赋值,指针赋值,即完成指向初始化
    p[0] = &a;
    p[1] = &b;
    p[2] = &c;
    
    printf("变量a = %d, 指针pa = 0X%p, 指针解引用*pa = %d\n", a, p[0], *p[0]);
    printf("变量b = %d, 指针pb = 0X%p, 指针解引用*pb = %d\n", b, p[1], *p[1]);
    printf("变量c = %d, 指针pc = 0X%p, 指针解引用*pc = %d\n", c, p[2], *p[2]);

    return 0;
}

运行结果如下:

PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) {
    
     gcc 'arr_point01.c' -o 'arr_point01.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) {
    
     &'.\arr_point01' }
变量a = 10, 指针pa = 0X000000000061FE44, 指针解引用*pa = 10
变量b = 20, 指针pb = 0X000000000061FE48, 指针解引用*pb = 20
变量c = 30, 指针pc = 0X000000000061FE4C, 指针解引用*pc = 30

从程序运行可以看出:

  1. 这是一组指针,还有两个未用到
  2. 指针占用4个字节的内存大小,顺序排列
  3. 指针完成指向时,要取普通变量的地址
  4. 指针解引用可以直接取值

基本知识点:

  • &为取地址
  • *为取值,即指针解引用

1.5 数组的指针之赋值

#include<stdio.h>

#define N 5

int main(void)
{
    
    
    int (*p)[N];        // 这是一个数组的指针,只有指向有N个元素的数组【本质上,是一个行指针】
    int a[N] = {
    
    1, 2, 3, 4, 5};
    int i;

    p = &a;             // a是数组名,本身也是地址,即数组首地址,但这个指针是数组的指针,就要直接取数组的地址

    printf("查看内地址:\n");
    printf("\n指针本身的地址:0X%p", &p);
    printf("\n指针指向的地址:0X%p", p);
    printf("\n数组的首地址:0X%p", a);
    printf("\n数组的首元素的地址:0X%p", &a[0]);
    printf("\n数组的地址:0X%p", &a);
    
    printf("\n指针解引用值:%d", *p[0]);

    a[0] = 11;
    *p[0] = 111;    // 刚好首行首列,操作有效
    *p[1] = 22;     // 没有这一行,操作无效
    *p[3] = 33;     // 没有这一行,操作无效
    printf("\n打印数组里的值:");
    for ( i = 0; i < N; i++)
    {
    
    
        printf("%d ", a[i]);
    }

    // 使用行指针时,先取行,再取行上的列,注意解引用的顺序
    *(*(p)+0) = 112;      // 首行0列  
    *(*(p)+1) = 22;       // 首行1列  
    *(*(p)+2) = 33;       // 首行2列  

    printf("\n再打印数组里的值:");
    for ( i = 0; i < N; i++)
    {
    
    
        printf("%d ", a[i]);
    }
    

    return 0;
}

运行结果如下:

PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) {
    
     gcc 'arr_point02.c' -o 'arr_point02.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) {
    
     &'.\arr_point02' }
查看内地址:

指针本身的地址:0X000000000061FE08
指针指向的地址:0X000000000061FE10
数组的首地址:0X000000000061FE10
数组的首元素的地址:0X000000000061FE10
数组的地址:0X000000000061FE10
指针解引用值:1
打印数组里的值:111 2 3 4 5
再打印数组里的值:112 22 33 4 5

从程序运行可以看出:

  1. 数组的首地址、首个元素的地址、数组的地址,都是同一个地址
  2. 指针本身是要占用内存空间的,它是指针变量
  3. 指针完成指向后,可以存取该地址单元【基类型空间大小】
  4. 数组的指针指向一个数组,这个数组,就是一个单元
  5. 数组的指针,第一次解引用,就是取行,再一次解引用,就是取列
  6. 所以,数组的指针,可以直接和二维数组对应

1.6 进一步观察行指针,即数组的指针的移动

#include<stdio.h>

#define N 5

int main(void)
{
    
    
    int (*p)[N];        // 这是一个数组的指针,只有指向有N个元素的数组【本质上,是一个行指针】
    int a[N] = {
    
    1, 2, 3, 4, 5};
    int i;

    p = &a;             // a是数组名,本身也是地址,即数组首地址,但这个指针是数组的指针,就要直接取数组的地址

    printf("查看内地址:\n");
    printf("\n指针本身的地址:0X%p", &p);
    printf("\n指针指向的地址:0X%p", p);
    printf("\n数组的首地址:0X%p", a);
    printf("\n数组的首元素的地址:0X%p", &a[0]);
    printf("\n数组的地址:0X%p", &a);
    
    printf("\n指针解引用值:%d", *p[0]);

    a[0] = 11;
    *p[0] = 111;    // 刚好首行首列,操作有效
    *p[1] = 22;     // 没有这一行,操作无效
    *p[3] = 33;     // 没有这一行,操作无效
    printf("\n打印数组里的值:");
    for ( i = 0; i < N; i++)
    {
    
    
        printf("%d ", a[i]);
    }

    // 使用行指针时,先取行,再取行上的列,注意解引用的顺序
    *(*(p)+0) = 112;      // 首行0列  
    *(*(p)+1) = 22;       // 首行1列  
    *(*(p)+2) = 33;       // 首行2列  

    printf("\n再打印数组里的值:");
    for ( i = 0; i < N; i++)
    {
    
    
        printf("%d ", a[i]);
    }

    printf("\n数组的内存大小:%d字节【十六进制】", (int)sizeof(a));
    printf("\n完成指向后的p指针所指的地址:0X%p", p);
    printf("\n完成指向后的p指针移动【行移动】所指的地址:0X%p", p + 1);
    printf("\n完成指向后的p指针移动【行移动】所指的地址:0X%p", p + 2);
    printf("\n完成指向后的p指针移动【列移动】所指的地址:0X%p", *(p));
    printf("\n完成指向后的p指针移动【列移动】所指的地址:0X%p", *(p) + 1);

    return 0;
}

查看最后的运行结果

PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) {
    
     gcc 'arr_point02.c' -o 'arr_point02.exe' -Wall -g
-O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) {
    
     &'.\arr_point02' }
查看内地址:

指针本身的地址:0X000000000061FE08
指针指向的地址:0X000000000061FE10
数组的首地址:0X000000000061FE10
数组的首元素的地址:0X000000000061FE10
数组的地址:0X000000000061FE10
指针解引用值:1
打印数组里的值:111 2 3 4 5
再打印数组里的值:112 22 33 4 5
数组的内存大小:20字节【十六进制】
完成指向后的p指针所指的地址:0X000000000061FE10
完成指向后的p指针移动【行移动】所指的地址:0X000000000061FE24
完成指向后的p指针移动【行移动】所指的地址:0X000000000061FE38
完成指向后的p指针移动【列移动】所指的地址:0X000000000061FE10
完成指向后的p指针移动【列移动】所指的地址:0X000000000061FE14

最后四行表明

  1. 行移动,一次是20个字节,刚好就是五个int的字节数
  2. 列移动,一次是4个字节,即一个int的字节数

2 对于数组的地址

2.1 概念

  • 数组的地址
  • 数组的首地址
  • 数组元素的地址,首元素的地址
  • 二维数组a[m][n]
    • 数组首地址
    • 首行地址
    • 首行首列地址
    • 第一个元素【仍然是数组】的地址,即a[0]的地址
    • 第一个数据元素的地址,即a[0][0]的地址

一维数组

int a[5];

a表示的是数组的首地址,a等价于&a[0]

二维数组

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

a表示的整个数组的首地址,a[0]表示的是第一行的首地址,这两者者在数值上是一样的,但含义不同(或者说类型不同),数组名a是对于整个数组,a[0]是对于第一行

从上面的示例运行结果来看,有些地址就是同一个地址

在用数组的地址进行赋值的时候,虽然三者值相同,但是三者不可随意混用(以int a[2][2]为例)

a--------是int (*)[2]型

a[0]-----是int *型

对于a[0]&a[0][0],两个类型都是int *型的,所以下述两种赋值方法等价

第一种:

int a[2][2] = {1, 2, 3, 4};
int *p;
p = a[0];

第二种:

int a[2][2] = {1, 2, 3, 4};
int *p;
p = &a[0][0];

对于int a[2][2]来说,如果将a[0]改为&a[0],那么&a[0]和a的类型相同,都为int (*)[2]类型,下面以int a[5][5]为例,列出了二维数组的元素在不同方式表达下的不同类型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BW6lOvME-1607311364560)(20201120-C语言-二维数组.assets/image-20201120145832572.png)]

也可以用一维指针数组来保存二维数组中某个元素的地址

int a[2][2] = {
    
    1, 2, 3, 4};
int *p[2];
p[0] = &a[0][0];
printf("%d", *p[0]);

3 二维数组的解引用

以二维数组

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

为例(第一维是行,第二维是列)

第一种:*(*a+1)--------等价于a[0][1],因为*的优先级比+高,所以先解引用,进入第二维在第二维里面地址+1,再次解引用得到元素值

第二种:*(*(a+1))------等价于a[1][0],比上面第一种多加了一个括号,括号优先级最高,先+1移动地址(注意是在第一维里面移动即行上的移动),然后解引用进入第二维,再解引用得到元素的值

第三种:*(&a[0][0]+1)-----等价于a[0][1],这里使用了&取地址符【注意,这里取出来的是变量元素即基元素的地址,地址就是指针,如果指针移动,将以它为基准,一次移动一个基元素内存大小,本质上,也就是列上的移动】,将原本表示第一个元素的a[0][0]返回到第二个维度,然后第二维地址+1,再解引用得到元素的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbekSuFT-1607311364564)(20201120-C语言-二维数组.assets/image-20201120150728181.png)]

对于a[2][3]的解引用的过程:

  1. 二维数组,共2行,3列
  2. 一维数组名,本质上是列指针,而二维数组名,本质上是行指针,但这些数组名,都是常指针,即,指向固定不变
  3. 一维数组名,一次解引用,即可获取对应列上的元素
  4. 二维数组名,二次解引用,才可以获取对应行的对应列上的元素
  5. 对于一维,在解引用之前,是可以让指向偏移的,但指针不需要移动,只是指向发生偏移
  6. 对于二维,在解引用一层之前,可以偏移,即【行偏移】,再一次解引用之前,还可以再偏移,即【列偏移】,偏移到指定位置后,再第二层解引用
  7. 直接取基元素的地址,则偏移一定是以基元素为准,一次一个基元素的内存单位大小
  8. 本质上:
  • 行指针,基元素变为一个一维数组
  • 列指针,基元素即为基元素本身
  • 基一旦发生变化,移动或是偏移时,指针跨过的内存单位大小就随之而变化
  1. 行指针,经过一次解引用,就化为列指针,仍然是地址;也就是说,行指针经过两次解引用后,也就是取值,不再是地址;
  2. 列指针,经过一次解此用,就是取值,不再是地址;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V8vTBpkk-1607311364570)(20201120-C语言-二维数组.assets/image-20201120150824653.png)]

示例分析

#include <stdio.h>

int main(void)
{
    
    
    int a[2][3] = {
    
    1, 2, 3, 4, 5, 6};

    printf("%d***%d\n", *(a[1]), (*a)[0]);
    printf("%d***%d\n", *(a[1]+1), (*a+1)[0]);
    printf("%d***%d\n", *(a[1]+1), (*a+1)[1]);

    return 0;
}

运行结果如下:

PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) {
    
     gcc 'arr_point03.c' -o 'arr_point03.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) {
    
     &'.\arr_point03' }
arr_point03.c: In function 'main':
arr_point03.c:5:19: warning: missing braces around initializer [-Wmissing-braces]
     int a[2][3] = {
    
    1, 2, 3, 4, 5, 6};
                   ^
                    {
    
          } {
    
          }
4***1
5***2
5***3

虽然有警告,也说明内存是线性,仍然可以一次贯穿了来完成赋值

  • 二维可以用一维的方式来初始化
  • 先移动,再解引用,还是先解引用,再移动,程序员自己要明白清楚
  • 数组的下标操作,和解引用有相同的效果,但下标是可以直接定位到相应的行或是列上的
    • 行标定位行
    • 列标才定位列
    • 对于二维数组,带行标的,肯定还是列指针,还可以做偏移,还可以再解引用
    • 对于一维数组,只能带列标,带上列标,即取值
    • 对于行指针,解引用一次后,就成了一维数组上的列指针,再带上列标,即取值

4 解引用和下标

4.1 下标是数组说法,解引用是指针说法

*(a[1]+1)--------表示的是a[1][1]的值

过程解析:

  1. 行标a[1],标号为1,即第二行,转化为列指针
  2. 偏移a[1]+1,偏移量为1,即第二行第二列,仍然是列指针
  3. 解引用*(a[1]+1),即取值,取的就是a[1][1]元素的值

(*a+1)[1]--------表示的是a[0][2]的值

过程解析:

  1. 解引用*a,由行指针转为列指针,指在首行首列,即第1列
  2. 偏移(*a+1),指在首行第2列
  3. 取列标(*a+1)[1],仍然是一个数组,如果是列标为[0]即当前所指位置,而这时,列标号为1,即偏移一个基元素,也就是取首行第二列的下一个元素,即首行第三列的元素,即a[0][2]元素

4.2 为了方便理解,再一次详细描述一下

先退回一维数组,以

int a[5];

来说,a表示的数组a的首地址,a[2]表示在a的基础上移动2个地址(注意a的类型是int *型的),再解引用得到元素的值,意思是a[2]

实际上包含了两步

  • 第一步地址移动
  • 第二步解引用得到元素的值(注意第二步,有点隐式转换的意思,经常被人忽略)

现在来解释上面的二维数组就容易多了

  • 先来看第一个*(a[1]+1)

    • a[1]代表第二行的首地址,注意这里的维度已经是第二维度了
    • 然后括号优先第二维地址+1
    • 最后解引用得到元素的值
  • 再看第二个(*a+1)[1],这里提一句,因为[]的优先级是比高的所以这里的括号不能去掉

    • 第一步先解引用进入第二维度(*优先级高于+)
    • 然后第二维地址+1
    • 然后再在当前基础上再移动一次地址,只要不是[0],就会发生位置偏移
    • 最后下标取值
    • 得到元素的值,这里可能有点绕,换个说法就是[1]是在当前维度进行移动,然后解引用(“当前维度”有点不太严谨,为了方便理解先将就这么用了)

a[2][1]来说一共有四步

  • 其中包含了两次地址移动,两次解引用
  • 执行顺序是:
    • 地址移动->解引用->地址移动->解引用
    • (这里提一句,[]的结合性是左结合的,所以在移动的时候先移动行(第一维)再移动列(第二维))

详细步骤:

  • 第一步:在当前维度地址+2,因为a的维度是第一维,所以是第一维地址+2,即行+2
  • 第二步:解引用进入第二维度
  • 第三步:在当前维度地址+1,因为这时已经进入第二维,所以第二维地址+1,即列+1
  • 第四步:解引用得到元素的值

5 理解指针数组的本质==内存空间的分配和使用

概括的说,指针其实就是可变数组的首地址,说是可变数组,是指其包含内容的数量的可变的,并且是可动态申请和释放的,从而充分节约宝贵的内存资源。我一向喜欢一维数组,除非万不得已,我一般是不用二维数组的,多维的则更是很少涉足了。因为一维简单,容易理解,而用指针指向的多维数组就具有相当的复杂性了,也因此更具有讨论的必要。

本质上,就是为了更好地使用和操纵内存

5.1 三个二维数组的比较

int **Ptr;
int *Ptr[5];
int (*Ptr)[5];

三例都是整数的二维数组,都可以用形如 Ptr[0][0] 的方式访问其内容;但它们的差别却是很大的。

5.2 从四个方面对它们进行讨论

5.2.1 内容:

​ 它们本身都是指针,它们的最终内容都是整数。注意这里说的是最终内容,而不是中间内容,比如你写 Ptr[ 0 ],对于三者来说,其内容都是一个整数指针,即 int *Ptr[1][1] 这样的形式才是其最终内容。

5.2.2 意义:

​ (1)、int **Ptr 表示指向"一群"指向整数的指针的指针。【可以认为是指针数组,只能指向指针,这些被指向的指针是整型指针】
​ (2)、int *Ptr[5] 表示指向 5 个指向整数的指针的指针。【就是5个指针,成了一组】
​ (3)、int (*Ptr)[5] 表示指向"一群"指向 5 个整数数组的指针的指针。【即数组的指针,只能指向数组,不能指向整型元素】

5.2.3 所占空间:

​ (1)、int **Ptr 和 (3)、int (*Ptr)[5] 一样,在32位平台里,都是4字节,即一个指针。但 (2)、int *Ptr[5] 不同,它是 5 个指针,它占5 * 4 = 20个字节的内存空间。

5.2.4 用法:

​ (1)、int **Ptr

​ 因为是指针的指针,需要两次内存分配才能使用其最终内容。首先,Ptr = (int **)new int *[5];这样分配好了以后,它和(2)的
意义相同了;然后要分别对 5 个指针进行内存分配,例如:Ptr[0] = new int[20];它表示为第 0 个指针分配 20 个整数,分配好以后, Ptr[0] 为指向 20 个整数的数组。这时可以使用下标用法 Ptr[0][0]Ptr[0][19]了。
​ 如果没有第一次内存分配,该 Ptr 是个"野"指针,是不能使用的,如果没有第二次内存分配,则 Ptr[0] 等也是个"野"指针,也是不能用的。当然,用它指向某个已经定义的地址则是允许的,那是另外的用法(类似于"借鸡生蛋"的做法),这里不作讨论(下同)。
​ (2)、int *Ptr[5]
​ 这样定义的话,编译器已经为它分配了 5 个指针的空间,这相当于(1)中的第一次内存分配。根据对(1)的讨论可知,显然要对其进行一次内存分配的。否则就是"野"指针。
​ (3)、int (*Ptr)[5]
​ 这种定义我觉得很费解,不是不懂,而是觉得理解起来特别吃力,也许是我不太习惯这样的定义吧。怎么描述它呢?它的意义是"一群"指针,每个指针都是指向一个 5 个整数的数组。如果想分配 k 个指针,这样写:

Ptr = (int(*)[5]) new int[sizeof(int)*5*k]

这是一次性的内存分配。分配好以后,Ptr 指向一片连续的地址空间,其中 Ptr[0] 指向第 0 个 5 个整数数组的首地址,Ptr[1] 指向第1 个 5 个整数数组的首地址。

综上所述,我觉得可以这样理解它们:
int ** Ptr <==> int Ptr[ x ][ y ];
int *Ptr[ 5 ] <==> int Ptr[ 5 ][ x ];
int ( *Ptr )[ 5 ] <==> int Ptr[ x ][ 5 ];
这里 x 和 y 是表示若干的意思。

6 指针数组(数组每个元素都是指针)详解

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。

指针数组的定义形式一般为:

dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为:

dataType *(arrayName[length]);

括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:

#include <stdio.h>
int main()
{
    
        
    int a = 16, b = 932, c = 100;    //定义一个指针数组    
    int *arr[3] = {
    
    &a, &b, &c};	//也可以不指定长度,直接写作 int *arr[]    
    
    //定义一个指向指针数组的指针    
    int **parr = arr;    
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);    
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));    
    
    return 0;
}

运行结果:
16, 932, 100
16, 932, 100

arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。

第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。

第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

指针数组还可以和字符串数组结合使用,请看下面的例子:

#include <stdio.h>
int main()
{
    
        
    char *str[3] = {
    
            
        "www.cuit.edu.cn",        
        "数学学院学习C语言",        
        "C Language"    
    };    
    
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);    
    
    return 0;
}

运行结果:
www.cuit.edu.cn
数学学院学习C语言
C Language

需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。

为了便于理解,可以将上面的字符串数组改成下面的形式,它们都是等价的。

#include <stdio.h>
int main()
{
    
    
    char *str0 = "www.cuit.edu.cn";
    char *str1 = "数学学院学习C语言";
    char *str2 = "C Language";
    char *str[3] = {
    
    str0, str1, str2};

    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);    
    
    return 0;
}

7 二维数组的内存理解

7.1 基本概念理解

int a[3][4] = {
    
     {
    
    0, 1, 2, 3}, {
    
    4, 5, 6, 7}, {
    
    8, 9, 10, 11} };

从概念上理解,a 的分布像一个矩阵:

0   1   2   3
4   5   6   7
8   9  10  11

但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UeLCBcZf-1607311364577)(20201120-C语言-二维数组.assets/image-20201120160552706.png)]

C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]a[0][1]a[0][2]a[0][3]

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8V4NRlep-1607311364582)(20201120-C语言-二维数组.assets/image-20201120160640958.png)]

为了更好的理解指针和二维数组的关系,我们先来定义一个指向 a 的指针变量 p:

int (*p)[4] = a;		// 典型应用:行指针指向二维数组

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。

[ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

数组名 a 在表达式中也会被转换为和 p 等价的指针!

下面我们就来探索一下如何使用指针 p 来访问二维数组中的每个元素。按照上面的定义:

  1. p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。

  2. *(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素,下面的运行结果有力地证明了这一点:

#include <stdio.h>
int main(){
    
    
    int a[3][4] = {
    
     {
    
    0, 1, 2, 3}, {
    
    4, 5, 6, 7}, {
    
    8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));

    return 0;
}

运行结果:
16

  1. *(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢?

*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

  1. *(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。

根据上面的结论,可以很容易推出以下的等价关系:

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

【实例】使用指针遍历二维数组

#include <stdio.h>
int main(){
    
    
    int a[3][4]={
    
    0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
    
    
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j));
        printf("\n");
    }

    return 0;
}

运行结果:

 0   1   2   3
 4   5   6   7
 8   9  10  11

7.1 指针数组和二维数组指针的区别

指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:

int *(p1[5]);  //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5];  //二维数组指针,不能去掉括号

指针数组和二维数组指针有着本质上的区别:

  • 指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。
  • 二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。
  • 所有的指针,占用的内存空间大小是一样的
  • 但指向的内容是由它的基类型决定的
  • 所以void型的指针,可以强转为其它任意类型

猜你喜欢

转载自blog.csdn.net/matrixbbs/article/details/110801574