一. 指针数组
指针数组就是存放指针变量的数组,指针数组的本质是数组,而非指针。
1.1 定义和初始化
定义:int* arr[3] //arr是存放整型指针的数组,包含3个元素
初始化:int* arr[3] = { &a, &b, &c },arr中的三个元素分别指向存储a、b、c三个整型变量的内存地址。
指针数组定义并初始化的通用格式:指针类型 数组名[数组包含元素的个数]
指针类型包括:int*、char*、double*、float* 、short* 等等
1.2 指针数组的应用
可通过对指针数组中的元素进行解应用操作,以此打印几个数组中的每个元素。如下代码所示,将整形数组a、b、c首元素地址放入整型数组指针(int* arr[3])中,再对指针数组arr中每个指针元素进行 +整数 运算 和解应用操作,依次打印a、b、c三个数组。
#include<stdio.h>
int main()
{
//定义并初始化a、b、c三个数组
int a[5] = { 1,2,3,4,5 };
int b[5] = { 2,3,4,5,6 };
int c[5] = { 3,4,5,6,7 };
int* arr[3] = { a,b,c }; //将三个数组首元素地址存入指针数组arr中
int i = 0;
int j = 0;
//外循环代表abc三个数组,内循环代表每个数组中的元素
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
//arr[1]、arr[2]、arr[3]分别代表数组abc首元素地址
//arr[i]+j代表每个数组中各个元素的地址
//*(arr[i]+j)可获得数组中的元素
printf("%d ", *(arr[i] + j));
}
printf("\n");
}
return 0;
}
注:数组名在一般情况下表示数组首元素的地址,但在两种特殊情况下例外。
这两种特殊情况是:
(1)sizeof(数组名) —— 计算整个数组的大小
(2)&数组名 —— 取出整个数组的地址(对于整个数组的地址,本文第二节会讲到)
对于上段代码中的语句printf("%d ", *(arr[i] + j)),可以将*(arr[i] + j)写为 arr[i][j],以模拟二维数组的形式来打印每个数组中的元素。计算机识别arr[i][j]的底层逻辑就是*(arr[i] + j)。
对于二维数组,数组名也是数组首元素地址。但要注意与一维数组进行区分,二维数组的首元素是第一行的所有元素的集合(可以将二维数组的每一行理解为一个一维数组)。
二. 数组指针
数组指针存放一个数组的地址。本质为指针,而非数组。
2.1 数组指针的定义和初始化
int arr[10] = {1, 2, 3, 4, 5};
int (*parr)[10] = &arr; //数组指针,存放一个数组的地址数组指针初始化通用格式: 数组元素类型 (*指针变量名) [数组包含元素个数]
注意:在定义数组指针变量时,*一定要和指针变量名一同用括号括起来,如(*parr)的形式,如果不加括号,则指针变量名会首先与后面进行结合,从而错误的将数组指针定义为指针数组。
2.2 对数组名和&数组名的区分
通过对数组的学习,我们了解到,数组名即为数组首元素的地址。如代码2.1所示,分别用%p的格式来打印arr和parr(parr为指向整型数组arr的数组指针),打印结果相同。那么,就引出问题,下面代码中的arr和&arr是等价的吗?答案显然是否定的。
代码2.1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int(*parr)[5] = &arr;
printf("%p\n", arr);
printf("%p\n", parr); //两个printf打印结果相同
return 0;
}
为了辨析arr和&arr的区别,我们对代码2.2进行分析,第二个printf的打印结果比第一个printf大4,第三个printf的打印结果比第一个大printf大20。由此可见,对于arr和&arr,走一步(+1)跨过的步长不同。arr跨过的步长由数组元素的类型来决定(数组元素类型占几个字节的空间arr+1跨过的步长就是多少),&arr跨过的步长由数组arr中的元素类型和其所包含的元素个数共同决定(&arr+1走过的步长为 sizeof(数组元素类型) * 数组元素个数)。在代码2.2中,数组arr中的元素类型为int型,一个int型数据占用4byte的内存空间,arr中包含四个整型元素,故&arr + 1 能跨过的步长为。图2.1为代码2.2的一次运行的结果。
代码2.2
#include<stdio.h>
int main()
{
int arr[4] = { 1,2,3,4 };
printf("%p\n", arr);
printf("%p\n", arr + 1); //打印结果比第一个printf大4
printf("%p\n", &arr + 1); //打印结果比第一个printf大16
return 0;
}
注意:代码2.1和2.2的每次运行结果会有所不同。这是因为在代码运行的过程中,由编译器将数据放入到内存中去,每次运行编译器在内存中的那个区域放置数据程序员是不可知的,每次运行放置的位置也会不一样。
2.3 数组指针的应用
2.3.1 通过数组指针遍历一维数组中的每个元素。
代码演示:
代码2.3
#include<stdio.h>
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 }; //定义并初始化数组
int(*parr)[10] = &arr; //定义指向数组arr的数组指针
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(*parr + i)); //打印10 9 8 7 6 5 4 3 2 1
}
return 0;
}
在代码2.3中,通过printf("%d ", *(*parr + i)) 命令来遍历打印数组中的每个元素。其中:对数组指针解应用(*parr)获得arr,即获得数组首元素地址,*(*parr + i)与*(arr + i)等价,通过解引用获得数组中的每个元素。
2.3.2 通过数组指针传参打印二维数组中的每个元素
代码演示:
代码 2.4
#include<stdio.h>
//定义二维数组打印函数Print
//函数返回类型为void,p是二维数组首元素的地址,row是二维数组的行数,col是列数
void Print(int(*p)[5], int row, int col)
{
int i = 0;
int j = 0;
//外循环为行,内循环为列
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", *((*p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
Print(arr, 3, 5); // 调用二维数组打印函数
return 0;
}
二维数组的数组名也代表数组首元素的地址,二维数组的首元素是数组第一行的元素,二维数组arr可以理解为几个一维数组组成的数组。在代码2.3中,,arr[1]、arr[2]、arr[3]可以分别理解为二维数组第一行、第二行、第三行对应一维数组的数组名。在函数调用时将二维数组数组名作为实参传递给函数,相当于给函数传了个数组指针。
三. 数组传参的方法
在设计函数时,难免会将数组和指针变量作为实参传递给函数,那么,该如何设计函数来确保传参正确呢?
3.1 一维数组传参
提出下面问题,在代码3.1中,哪种传参方式是可行的,那种是不可行的?
#include <stdio.h>
void test(int arr[]) //方法1
{}
void test(int arr[10]) //方法2
{}
void test(int *arr) //方法3
{}
void test2(int *arr[20]) //方法4
{}
void test2(int **arr) //方法5
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
return 0;
}
方法1、2、4、5是可行的,方法3不可行。
解析:要求进行一维数组传参时,定义函数时形式参数有数组形式和指针形式两种写法。方法1和方法2将形式参数写为了数组形式,一维数组在定义时元素个数可以省略,故方法1和方法2均是可行的。方法3、4、5将形式参数写为指针形式,arr2是包含20个整型指针的指针数组,方法3传入整型指针变量,与main函数中arr2的类型冲突,不可行;方法4是直接传入指针数组,可行;方法5将形式参数写为二级指针的形式,可行,详解见图3.1。
3.2 二维数组传参
首先还是先来看一组问题,代码3.2中,那种二维数组传参的方式可行,那种不可行?
void test(int arr[3][5]) //方法1
{}
void test(int arr[][]) //方法2
{}
void test(int arr[][5]) //方法3
{}
void test(int *arr) //方法4
{}
void test(int* arr[5]) //方法5
{}
void test(int (*arr)[5]) //方法6
{}
void test(int **arr) //方法7
{}
int main()
{
int arr[3][5] = {0};
test(arr);
return 0;
}
方法1、3、6是可行的,方法2、4、5、7不可行。
解析:二维数组的数组名是首元素地址,二维数组首元素即第一行元素,因此,二维数组的数组名本质上的类型是数组指针。二维数组在定义时,可以省略行数,但不能省略列数,因此,方法1、3正确,方法2错误。对于方法4,形参arr是整型指针,与主函数中arr的类型相冲突,因此方法4不可行;对于方法5,arr是指针数组,而非数组指针,与主函数中arr类型冲突,不可行;对于方法6,前面提到过二维数组的数组名本质上就是一个数组指针,将形参写为数组指针的形式,显然方法6是可行的;对于方法7,二级指针与二维数组毫无关联,不可行。
3.3 总结
一维数组传参的几种正确形式:
<1> test(int arr[5]) —— 直接传入数组并给出数组中含有几个元素
<2> test(int arr[ ]) —— 直接传入数组,省略数组中含有的元素个数
<3> test(int* arr) —— 形参写为指针形式,传入数组首元素地址
二维数组传参的几种正确形式:
<1> test(int arr[3][5]) —— 直接传入二维数组,行数和列数均不省略
<2> test(int arr[ ][5]) —— 直接传入二维数组,省略行数,不省略列数
<3> test(int (*p)[5]) —— 以数组指针的形式传参
二维数组传参的几种典型错误形式:
<1> test(int arr[ ][ ]) —— 将二维数组的列数省略,二维数组列数不可省略
<2> test(int* arr[5]) —— 传入指针数组
<3> test(int **arr) —— 传入二级指针
全文结束,感谢大家的阅读,敬请批评指正。