C语言指针操作(五)通过指针引用多维数组

多维数组的地址,通过指针引用多维数组详解。

通过指针引用一维数组可以参考这篇文章:

C语言指针操作(三)通过指针引用数组

目录


 指针变量可以指向一维数组中的元素,也可以指向多维数组中的元素;但在概念和使用方法上,多维数组的指针比一维数组的指针要复杂一些。

一、多维数组的地址

1.1引入

先定义一个二维数组

int a[3][4] = { {1,3,5,7},{9,11,13,15},{17,19,21,23} };

a 是二维数组名,a 数组包含 3 行,即 3 个行元素:a[0],a[1],a[2]。而每一个行元素又是一个一维数组,它包含 4 个元素(即 4 个列元素)。例如, a[0] 所代表的一维数组又包含 4 个元素:a[0][0],a[0][1],a[0][2],a[0][3]。可以认为二维数组是 “数组的数组”,即二维数组 a 是由 3 个一维数组所组成的。

从二维数组的角度来看,a 代表二维数组首元素的地址,现在的首元素不是一个简单的整型元素,而是由 4 个整型元素所组成的一维数组,因此 a 代表的是首行(即序号为 0 的行)的起始地址。a+1代表序号为 1 的行的起始地址。如果二维数组的首行的起始地址为2000,一个整型数据占 4 个字节,则 a+1 的值应该是 2000+4×4=2016 (因为第 0 行有 4 个整型数据)。a+1 指向a[1],或者说,a+1 的值是 a[1] 的起始地址。a+2 代表 a[2] 的起始地址,它的值是2032。

a[0],a[1],a[2] 既然是一维数组名,从前面已知,数组名代表数组首元素地址,因此 a[0] 代表一维数组 a[0] 中第 0 列元素的地址,即 &a[0][0]。同理,a[1] 的值是 &a[1][0],a[2] 的值是 &a[2][0]。

a[0] 是一维数组名,该一维数组中序号为 1 的元素的地址显然应该用 a[0]+1 来表示。此时 “a[0]+1” 中的 1 代表 1 个列元素的字节数,即4个字节。a[0] 的值是2000,a[0]+1 的值是2004(而不是2016)。这是因为现在是在一维数组范围内讨论问题,正如有一个一维数组x,x+1 是其第 1 个元素x[1] 的地址一样。a[0]+0,a[0]+1,a[0]+2,a[0]+3 分别是a[0][0],a[0][1],a[0][2],a[0][3] 元素的地址(即 &a[0][0],&a[0][1],&a[0][2],&[0][3])。

从上一篇文章可知,a[0] 和 *(a+0) 等价,a[1] 和 *(a+1) 等价,a[i] 和 *(a+i) 等价。

因此,a[0]+1和 *(a+0)+1 都是 &a[0][1](即地址都为2004)。a[1]+2 和 *(a+1)+2 的值都是 &a[1][2](即地址为2024)。请注意不要将*(a+1)+2 错写成 *(a+1+2),后者变成 *(a+3) 了,相当于 a[3] 。进一步分析,欲得到 a[0][1] 的值,用地址法怎么表示呢?既然 a[0]+1 和 *(a+0)+1 是 a[0][1] 的地址,那么,*(a[o]+1) 就是 a[0][1] 的值。同理,*(*(a+0)+1) 或 *(*a+1) 也是 a[0][1] 的值。*(a[i]+j) 或 *(*(a+i)+j)是 a[i][j] 的值。务请记住 *(a+i) 和 a[i] 是等价的。

注意:a[i] 从形式上看是 a 数组中序号为 i 的元素。如果a是一维数组名,则 a[i] 代表 a 数组序号为 i 的元素的存储单元。a[i] 是一个有确定地址的存储单元。但如果 a 是二维数组,则 a[i] 是一维数组名,它只是一个地址,并不代表一个存储单元,也不代表存储单元中的值(如同一维数组名只是一个指针常量一样)。a,a+i,a[i],* (a+i),* (a+i)+j,a[i]+j 都是地址。而 *(a[i]+j) 和 *(*(a+i)+j) 是二维数组元素 a[i][j] 的值。

1.2地址举例说明

表现形式含义
a二维数组名,指向行,即一维数组a[0],即0行的起始地址2000
a[0]一维数组名,指向列,即整型常量a[0][0],即0行0列元素的地址2000
*(a+0),*a0 行 0 列元素的地址2000
a+1,&a[1]第 1 行 (a[1]) 的起始地址2016
a[1],*(a+1)第 1 行 0 列元素 a[1][0] 的地址2016
a[1]+2,*(a+1)+2,&a[1][2]第 1 行 2 列元素 a[1][2] 的地址2024
*(a[1]+2),*(*(a+1)+2),a[1][2]第 1 行 2 列元素 a[1][2] 的值13

a+1 是二维数组 a 中序号为 1 的行的起始地址(序号从 0 起算),而 *(a+1) 并不是 a+1 单元的内容(值),因为 a+1 并不是一个数组元素的地址,也就谈不上存储单元的内容了。*(a+1) 就是 a[1],而a[1] 是一维数组名,所以也是地址,它指向a[1][0]。a[1]和 *(a+1) 都是二维数组元素 a[1][0] 的地址的不同的表示形式。

1.3地址类型详解

C 语言的地址信息中既包含位置信息(如内存编号2000),还包含它所指向的数据的类型信息。现在a[0] 是一维数组名,它是一维数组中起始元素的地址,a 是二维数组名,它是二维数组的首行起始地址,二者的纯地址是相同的;即2000,但它们的基类型不同,即它们指向的数据的类型不同,前者是整型数据,后者是一维数组。

如果用一个指针变量 pt 来指向一维数组,应当这样定义:

int (*pt)[4];

int(*pt)[4];表示 pt 指向由 4 个整型元素组成的一维数组,此时指针变量 pt 的基类型是由 4 个整型元素组成的一维数组。

a+1 与 a[0]+1 是不同的,a+1 是序号为 1 的行的起始地址,a+1 指向序号为 1 的行,而 *(a+1)或 a[1] 或 a[1]+0 都指向 1 行 0 列元素,二者地址虽相同,但指向的数据类型不同。a 和 a[0] 的值虽然相同(等于2000),但是由于指针的基类型不同,a 指向一维数组a[0],而 a[0] 指向列元素a[0][0]。因此,对不同的指针进行加 1 的运算,得到的结果是不同的。

注意:

二维数组名(如 a )是指向行(一维数组)的。因此 a+1 中的 “1” 代表一行中全部元素所占的字节数。一维数组名(如 a[0],a[1] )是指向列元素的。a[0]+1 中的 1 代表一个 a 元素所占的字节数。在指向行的指针前面加一个 *,就转换为指向列的指针。例如,a 和 a+1 是指向行的指针,在它们前面加一个 * 就是 *a和 *(a+1),它们就成为指向列的指针,分别指向 a 数组 0 行 0 列的元素和 1 行 0 列的元素。反之,在指向列的指针前面加 &,就成为指向行的指针。例如 a[0] 是指向 0 行 0 列元素的指针,在它前面加一个&,得 &a[0],由于 a[0] 与 *(a+0)等价,因此 &a[0] 与 &*a 等价,也就是与 a 等价,它指向二维数组的 0 行。

注意:

不要把 &a[i] 简单地理解为 a[i] 元素的存储单元的地址,因为并不存在 a[i] 这样一个实际的数据存储单元。它只是一种地址的计算方法,能得到第 i 行的起始地址,&a[i] 和 a[i] 的值是一样的,但它们的基类型是不同的。&a[i] 或 a+i 指向行,而 a[i] 或 *(a+i) 指向列。当列下标 j 为 0 时,&a[i] 和 a[i] (即 a[i]+j )值相等,即它们的纯地址相同,但应注意它们所指向的对象的类型是不同的,即指针的基类型是不同的。*(a+i) 只是 a[i] 的另一种表示形式,不要简单地认为 *(a+i) 是 “ a+i 所指单元中的内容 ” 。在一维数组中 a+i 所指的是一个数组元素的存储单元,在该单元中有具体值。而对二维数组,a+i 不是指向具体存储单元而是指向行(即指向一维数组)。在二维数组中,a+i,a[i],*(a+i),&a[i],&a[i][0] 的值相等,即它们都代表同一地址,但基类型不同。

1.4实例说明

#include<stdio.h>
int main()
{
	int a[3][4] = { {1,3,5,7},{9,11,13,15},{17,19,21,23} };
	printf("%p,%p\n", a, *a);			//0行起始地址和0行0列元素的地址
	printf("%p,%p\n", a[0], *(a + 0));	//0行0列元素的起始地址
	printf("%p,%p\n", &a[0], &a[0][0]);	//0行起始地址和0行0列元素的地址
	printf("%p,%p\n", a[1], a + 1);		//1行0列元素的地址和1行起始地址
	printf("%p,%p\n", &a[1][0], *(a + 1) + 0);	//1行0列元素的地址
	printf("%p,%p\n", a[2], *(a + 2));			//2行0列元素的地址
	printf("%p,%p\n", &a[2], a + 2);			//2行起始地址
	printf("%d,%d\n", a[1][0], *(*(a + 1) + 0));//1行0列元素的值
	printf("%d,%d\n", *a[2], *(*(a + 2) + 0));	//2行0列元素的值
}

程序分析:

数组 a 的起始地址是 00EFFC74 (16进制) 。上面是在 VS2019 环境下的一次运行记录。在不同的计算机、不同的编译环境、不同的时间运行以上程序时,由于分配内存情况不同,所显示的地址可能是不同的。但是上面显示的地址是有共同规律的,如上面显示 0 行起始地址和 0 行 0 列元素地址为 00EFFC74,前 3 行显示的地址是相同的。第 4,5 行是 1 行 0 列元素地址和 1 行起始地址,它的值应当比上面显示的 0 行起始地址和 0 行 0 列元素地址大 16 个字节(一行有 4 个元素,每个元素 4 个字节),00EFFC84 和 00EFFC74 之差是16。同样,第 6,7 行是 2 行 0 列元素地址和 ⒉行起始地址,它的值应当比 1 行起始地址和 1 行 0 列元素地址大 16 个字节,00EFFC94 和 00EFFC84 之差是16。最后两行显示的是 a[1][0] 和 a[2][0] 的值。

二、指向多维数组元素的指针变量

2.1指向数组元素的指针变量

实例:有一个3×4的二维数组,要求用指向元素的指针变量输出二维数组各元素的值。

#include<stdio.h>
int main()
{
	int a[3][4] = { 1,3,5,7,9,11,13,15,17,19,21,23 };
	int* p;		//p是int* 型的指针
	for (p = a[0]; p < a[0] + 12; p++)	//使p依次指向下一个元素
	{
		if (p != a[0] && ((p - a[0]) % 4 == 0)) printf("\n");	//p移动4次后换行
		printf("%4d ", *p);		//输出p所指向元素的值
	}
}

运行结果:

程序分析:

p 是一个 int* 型(指向整型数据)的指针变量,它可以指向一般的整型变量,也可以指向整型的数组元素。每次使 p 值加 1,使 p 指向下一元素,比较简单。如果要输出某个指定的数值元素(例如 a[1][2] ),则应事先计算该元素在数组中的相对位置(即相对于数组起始位置的相对位移量)。计算 a[i][j] 在数组中的相对位置的计算公式为:i * m + j

其中,m 为二维数组的列数(二维数组大小为 n×m )。例如,对上述 3×4 的二维数组,它的 2 行 3 列元素 a[2][3] 对 a[0][0] 的相对位移量为 2×4+3=11 元素。如果一个元素占 4 个字节,则 a[2][3] 对 a[0][0]的地址差为 11×4=44 字节。若开始时指针变量 p 指向 a[0][0],a[i][j] 的地址为 “&a[0][0]+(i*m+j)” 或 “p+(i*m+j)” 。a[2][3] 的地址是 (p+2*4+3),即 (p+11)。a[2][3] 的值为 *(p+11)。

从图中可以看到在 a[i][j] 元素之前有 i 行元素(每行有 m 个元素),在 a[i][j] 所在行,a[i][j] 的前面还有 j 个元素,因此 a[i][j] 之前共有 i×m+j 个元素。例如,a[2][3]的前面有两行,共 2×4=8 个元素,在它本行内还有 3 个元素在它前面,故共有 8+3=11 个元素在它之前,可用 p+11 表示其相对位置。

2.2指向由 m 个元素组成的一维数组的指针变量

上面实例的指针变量 p 是用 “int* p;” 定义的,它是指向整型数据的,p+1 所指向的元素是 p 所指向的列元素的下一元素(按在内存中存储的下一个整型元素)。可以改用另一方法,使 p 不是指向整型变量,而是指向一个包含 m 个元素的一维数组。这时,如果 p 先指向 a[0](即 p=&a[0] ),则 p+1 不是指向 a[0][1],而是指向 a[1],p 的增值以一维数组的长度为单位。

实例:输出二维数组任一行任一列元素的值

#include<stdio.h>
int main()
{
	int a[3][4] = { 1,3,5,7,9,11,13,15,17,19,21,23 };
	int(*p)[4] = a;		//p是指向包含4个整型元素的一维数组的指针变量
	int i, j;
	printf("请输入要输出元素的行列号:");
	scanf_s("%d,%d", &i, &j);		//获得所需要的行列号
	printf("a[%d,%d] = %d", i, j, *(*(p + i) + j));	//输出a[i][j]的值
}

运行结果:

程序分析:

程序第 4 行中 “int(*p)[4];” 表示定义 p 为一个指针变量,它指向包含 4 个整型元素的一维数组。注意,*p 两侧的括号不可缺少,如果写成 *p[4],由于方括号 [ ] 运算级别高,因此 p 先与 [4] 结合,p[4] 是定义数组的形式,然后再与前面的 * 结合,*p[4] 就是指针数组。

int(*p)[4]详解:

表示 (*p) 有 4 个元素,每个元素为整型。也就是 p 所指的对象是有 4 个整型元素的数组,即 p 是指向一维数组的指针。应该记住,此时 p 只能指向一个包含 4 个元素的一维数组,不能指向一维数组中的某一元素。p 的值是该一维数组的起始地址。虽然这个地址(指纯地址)与该一维数组首元素的地址相同,但它们的基类型是不同的。不要混淆。

实例:

#include<stdio.h>
int main()
{
	int a[4] = { 1,3,5,7 };
	int(*p)[4];			//p是指向包含4个整型元素的一维数组的指针变量
	p = &a;				//使p指向一维数组
	printf("%d", (*p)[3]);	//输出a[3]
}

注意第 6 行不应写成 “p=a;” ,因为这样写表示 p 的值是 &a[0],指向首元素a[0]。“p=&a;” 表示 p 指向一维数组(行),(*p)[3] 是 p 所指向的行中列为 3 的元素。

注意:

要注意指针变量的类型,从 “int (*p)[4];” 可以看到,p 的类型不是 int* 型,而是 int(*)[4] 型,p 被定义为指向一维整型数组的指针变量,一维数组有 4 个元素,因此 p 的基类型是一维数组,其长度是16字节。

三、用指向数组元素的指针作函数参数

一维数组名可以作为函数参数,多维数组名也可作函数参数。用指针变量作形参,以接受实参数组名传递来的地址。可以有两种方法:

①用指向变量的指针变量

②用指向一维数组的指针变量

3.1举例说明1

实例:3 个学生,4 门课,计算总平均分数以及第 n 个学生的成绩

#include<stdio.h>
int main()
{
	void average(float* p, int n);		//average函数声明
	void search(float(*p)[4], int n);	//search函数声明
	float score[3][4] = { {66,85,79,94},{88,98,76,84},{98,88,72,83} };
	average(*score, 12);	//求平均分
	search(score, 2);		//求序号为2的学生的成绩
	return 0;
}

void average(float* p, int n)
{
	float* q = p;	//指针变量为int*型
	float sum = 0, avg;
	for (; q < p + n; q++)
		sum += *q;		//求和
	avg = sum / n;		//求平均值
	printf("average = %5.2f\n", avg);
}

void search(float(*p)[4], int n)
{
	printf("第%d位同学的成绩为:", n);
	for (int i = 0; i < 4; i++)
		printf("%5.2f ", *(*(p + n) + i));	//指针变量为int (*p)[4]
	printf("\n");
}

运行结果:

程序分析:

在 main 函数中,先调用 average 函数以求总平均值。在函数 average 中形参 p 被声明为 float* 类型(指向 float 型变量)的指针变量。它的基类型为 float 型,实参用 *score,即 score[0],也就是&score[0][0],即 score[0][0] 的地址。把 score[0][0] 的地址传给 p,使 p 指向score[0][0]。然后在average 函数中使 p 先后指向二维数组的各个元素,p 每加 1 就改为指向 score 数组的下一个元素。形参 n 代表需要求平均值的元素的个数,实参 12 表示要求 12 个元素值的平均值。

函数search的形参 p 的类型是 float(*)[4],它不是指向整型变量的指针变量,而是指向包含 4 个元素的一维数组的指针变量。函数调用开始时,将实参 score 的值(代表该数组 0 行起始地址)传给p,使 p 也指向 score[0]。p+n 是 score[n] 的起始地址,*(p+n)+i 是score [n][i] 的地址,*(*(p+n)+i) 是 score[n][i] 的值。现在实参传给形参 n 的值是 2,即想找序号为 2 的学生的成绩。

调用 search 函数时,实参是 score(二维数组名,代表该数组中 0 行起始地址)传给 p,使 p 也指向score[0]。p+n 是 score[n] 的起始地址,*(p+n)+i 是 score[n][i] 的地址,*(*(p+n)+i) 是 score[n][i] 的值。现在 n=2,i 由 0 变到 3,for 循环输出 score[2][0] 到 score[2][3] 的值。

注意:

实参与形参如果是指针类型,应当注意它们的基类型必须一致。不应把 int* 型的指针(即数组元素的地址)传给 int(*)[4] 型(指向一维数组)的指针变量,反之亦然。

3.2举例说明2

在3.1实例的基础上,查找有一门以上课程不及格的学生,输出他们的全部成绩

#include<stdio.h>
int main()
{
	void search(float(*p)[4], int n);	//search函数声明
	float score[3][4] = { {66,85,55,94},{88,98,43,84},{98,88,72,83} };
	search(score, 3);		//查找存在不及格同学的成绩
	return 0;
}

void search(float(*p)[4], int n)
{
	for (int i = 0; i < n; i++)		//遍历每位同学
	{
		int  flag = 0;	//默认此同学不存在挂科情况
		for (int j = 0; j < 4; j++)
			if (*(*(p + i) + j) < 60)
			{
				flag = 1;	//遍历一个同学的每科成绩,如果存在小于60分的,将flag标记为1
			}
		if (flag == 1)	//如果flag==1,则此同学存在挂科,输出他的所有成绩
		{
			printf("第%d位同学存在挂科现象:", i + 1);
			for (int j = 0; j < 4; j++)
				printf("%5.2f ", *(*(p + i) + j));
			printf("\n");
		}
	}
}

运行结果:

程序分析:

实参 score 和形参 p 的类型是相同的。在调用 search 函数时,p 得到实参 score 的值,即 score[0] 的起始地址,也就是说 p 也指向 score 数组的第 1 行。然后 p 先后指向各行(每行包括该学生几门课的成绩)。p+i 是 score 数组第 i 行的起始地址,*(p+i) 是 score[i][0] 元素的地址,即 &score[i][0],*(p+i)+j 是 score[i][j] 的地址,即 &score[i][j] ,search函数中的 *(*(p+i)+j)就是 score[i][j]。先后检查各学生每门课的成绩,如有不及格的就记录下来。

在函数 search 中,变量 flag 用来表示有无不及格的课程。若flag 的值为 1 表示有不及格的课程,若 flag 的值为 0 表示没有不及格的课程。开始时先使 flag=0,若发现某一学生有一门不及格,就使 flag 变为 1。最后用 if 语句检查 flag,如为 1,则表示该学生有不及格的记录,输出该学生全部课程成绩。变量 i 代表学生号,j 代表课程号。score[i][j] 是序号为 i 的学生第 i 门课的成绩。

通过指针变量存取数组元素速度快,程序简明。用指针变量作形参,所处理的数组大小可以改变。因此数组与指针常常是紧密联系的,使用熟练的话可以使程序质量提高,编写程序方便灵活。

C语言指针操作系列文章:

C语言指针操作(一)地址,指针,指针变量是什么

C语言指针操作(二)指针变量作为函数参数

C语言指针操作(三)通过指针引用数组

C语言指针操作(四)用数组名作函数参数

C语言指针操作(五)通过指针引用多维数组

C语言指针操作(六)通过指针引用字符串

C语言指针操作(七)指向函数的指针

C语言指针操作(八)返回指针值的函数

C语言指针操作(九)指针数组和多重指针

C语言指针操作(十)动态内存分配与指向它的指针变量

C语言指针操作(十一)有关指针的小结

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
上一篇 2023年12月20日
下一篇 2023年12月20日

相关推荐