文章目录
二维数组
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
从程序运行可以看出:
- 这是一组指针,还有两个未用到
- 指针占用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.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
最后四行表明
- 行移动,一次是20个字节,刚好就是五个int的字节数
- 列移动,一次是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]
的解引用的过程:
- 二维数组,共2行,3列
- 一维数组名,本质上是列指针,而二维数组名,本质上是行指针,但这些数组名,都是常指针,即,指向固定不变
- 一维数组名,一次解引用,即可获取对应列上的元素
- 二维数组名,二次解引用,才可以获取对应行的对应列上的元素
- 对于一维,在解引用之前,是可以让指向偏移的,但指针不需要移动,只是指向发生偏移
- 对于二维,在解引用一层之前,可以偏移,即【行偏移】,再一次解引用之前,还可以再偏移,即【列偏移】,偏移到指定位置后,再第二层解引用
- 直接取基元素的地址,则偏移一定是以基元素为准,一次一个基元素的内存单位大小
- 本质上:
- 行指针,基元素变为一个一维数组
- 列指针,基元素即为基元素本身
- 基一旦发生变化,移动或是偏移时,指针跨过的内存单位大小就随之而变化
- 行指针,经过一次解引用,就化为列指针,仍然是地址;也就是说,行指针经过两次解引用后,也就是取值,不再是地址;
- 列指针,经过一次解此用,就是取值,不再是地址;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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]
的值
过程解析:
- 行标
a[1]
,标号为1,即第二行,转化为列指针 - 偏移
a[1]+1
,偏移量为1,即第二行第二列,仍然是列指针 - 解引用
*(a[1]+1)
,即取值,取的就是a[1][1]
元素的值
(*a+1)[1]--------表示的是a[0][2]
的值
过程解析:
- 解引用
*a
,由行指针转为列指针,指在首行首列,即第1列 - 偏移
(*a+1)
,指在首行第2列 - 取列标
(*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 来访问二维数组中的每个元素。按照上面的定义:
-
p
指向数组 a 的开头,也即第 0 行;p+1
前进一行,指向第 1 行。 -
*(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
*(p+1)+1
表示第 1 行第 1 个元素的地址。如何理解呢?
*(p+1)
单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
*(*(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型的指针,可以强转为其它任意类型