一、指针访问数组
1.理解数组名
之前我们使用指针访问数组时,使用了如下方法:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = &arr[0];
这里我们使用&arr[0]来得到数组首元素的地址。不过,数组首元素还有其他表示方法:数组名。
在c语言中,除了以下两种情况,数组名都表示首元素地址:
1.sizeof(数组名)。sizeof中单独放数组名时,数组名表示的是整个数组,sizeof计算的就是整个数组的大小。
2.&数组名。此时数组名表示的也是整个数组,取地址取出的是整个数组的地址(虽然值与首元素地址相同,但是两者类型有区别)
2.使用数组名访问数组元素
接下来,我们尝试使用数组名来访问数组元素:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < sz; i++) { printf("%d ", *(arr + i));//数组名表示首元素地址 } return 0; }
由于数组名就是首元素地址,我们就可以对其进行加减操作,访问数组中的元素。
我们还可以这么写:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr;//将首元素地址赋值给指针变量p for (int i = 0; i < sz; i++) { printf("%d ", *(p + i)); } return 0; }
运行结果:
以上代码中,我们将数组名传给了指针变量p,程序仍然能打印出数组元素,说明数组名arr等价于&arr[0],也等价于p。
我们都知道,可以使用arr[i]的方法表示数组元素,既然arr等价于p,那么我们是否可以用p[i]来访问呢?
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr; for (int i = 0; i < sz; i++) { printf("%d ", p[i]); } return 0; }
运行结果:
可以看到,效果也是一样的。实际上,p[i] 和 *(p+i)是等价的。使用下标引用操作符的时候,编译器也是通过找到首元素地址和偏移量并且执行解引用操作来实现数组元素的访问。
二、一维数组传参
接着我们来讨论一下一维数组传参的本质。我们都知道,数组名表示首元素地址,那在数组传参的时候,传入数组首元素的地址就可以对数组进行操作了。但是由于函数接收的只是一个地址,无法知道数组的具体大小,可能会造成数组越界的情况,所以再需要传入一个变量来表示数组的元素个数。
我们来写一个函数,实现打印数组的功能:
int print(int arr[], int sz)//数组形式的形参 int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz);//传入数组的首元素地址和元素个数 return 0; }
既然数组名是数组首元素的地址,那么我们也可以将形参写成指针形式:
int print(int *arr, int sz)//指针形式的形参 int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); return 0; }
运行结果:
三、二级指针
我们之前学习的指针,例如int*,char*,float*等等,它们都是一级指针。而二级指针又是什么呢?首先看一段代码:
int main() { int a = 10; int* pa = &a; return 0; }
这里使用指针变量p来接收a的地址。既然p是一个变量,那么它本身也有地址。它的地址如何存放呢?这里就需要使用二级指针:
int main() { int a = 10; int* pa = &a; int** ppa = &pa;//这里的ppa就是一个二级指针变量 return 0; }
如果想要通过二级指针ppa来访问变量a,就需要两次解引用操作,首先找到pa,然后找到a。例如:
int main() { int a = 10; int* pa = &a; int** ppa = &pa; **ppa = 30;//两次解引用操作 printf("%d\n", a); return 0; }
可以看到,a的值被成功修改成了30。
当然,二级指针变量也是变量,它也有地址,可以存放在三级指针变量中。不过更高级指针往往在一些复杂程序中出现,我们目前还不会接触到。
四、指针数组
1.指针数组的概念
我们都知道,数组就是一组相同类型元素的集合。例如整形数组中存放的都是整形变量,字符数组中存放的都是字符型变量。同理,指针数组就是存放指针变量的数组,数组中每一个元素的类型是指针类型。
和定义整形数组的方式类似,定义整形指针数组的方式是:
int *arr[10];
这表示一个整形指针数组,数组中存放10个元素。每个元素都是int*类型的指针变量。
2.指针数组的使用
当我们需要创建多个同类型的指针变量时,我们就可以创建一个指针数组。例如:
int main() { int a = 0; int b = 0; int c = 0; int* parr[3] = { &a,&b,&c };//三个指针变量分别指向a,b,c for (int i = 0; i < 3; i++) { *parr[i] = i + 1;//通过指针修改变量的值 } printf("a=%d b=%d c=%d\n",a, b, c); return 0; }
运行结果:
这里我们定义了一个指针数组,数组中分别存放a,b,c的地址,然后通过指针来修改a,b,c的值。
3.使用指针数组模拟二维数组
接下来,我们来一个关于指针数组的高级操作:模拟二维数组。我们知道二维数组可以看成是一维数组的数组,那么我们就可以使用指针数组来分别存放每一个一维数组首元素的地址来模拟出一个二维数组。
代码实现:
int main() { //创建三个一维数组 int arr1[5] = { 1,2,3,4,5 }; int arr2[5] = { 2,3,4,5,6 }; int arr3[5] = { 3,4,5,6,7 }; //将三个数组的首元素地址赋给指针数组 int* parr[3] = { arr1,arr2,arr3 }; //访问模拟的二维数组 for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { printf("%d ", parr[i][j]); } printf("\n"); } return 0; }
运行结果:
这里的“parr[i]”表示解引用操作找到了指针数组的某一个元素,也就是整形数组的首元素地址。“arr[i][j]”表示通过整形数组的首元素地址,解引用操作找到了整形元素。需要注意的是:这只是使用指针数组模拟出的二维数组,并不是真正的二维数组,因为三个整形数组之间不一定是连续存放的。
五、数组指针
1.数组指针的概念
之前我们学习了指针数组,接下来我们探讨一下数组指针。要注意:这两者可是完全不同的概念,前者本质是数组,后者本质是指针。顾名思义,数组指针就是指向数组的指针,它存放的是整个数组的地址。数组指针的定义方式:
int (*p)[10];
从定义方式上来看,它就是在指针数组的基础上使用括号将变量名和 * 号结合起来了,它表示p是一个指针变量。这里的 int 表示它指向的数组的元素类型,后边的 [10] 表示的是它指向的数组有十个元素。
这里有一个知识补充:[](下标引用操作符)的优先级要高于 *(解引用操作符),所以加括号才有意义。
2.数组指针初始化
既然数组指针存放的是整个数组的地址,那么初始化时将整个数组的地址赋值给数组指针变量就好了。&数组名得到的就是整个数组的地址。
int main() { int arr[5] = { 0 }; int(*p)[5] = &arr; return 0; }
3.数组指针类型的理解
首先观察以下程序:
int main() { int arr[10] = { 0 }; int(*p1)[10] = &arr; int* p2 = arr; printf("%p\n", p1); printf("%p\n", p2); return 0; }
在这里,我们打印了数组的地址和数组首元素的地址。可以先猜猜运行结果是什么。。
运行结果:
结果表明,这两者的地址是一样的。这样就会有人想:数组的地址难道就是首元素的地址吗?其实两者是有区别的。我们观察一下调试窗口:
可以看到,两者的值虽然相同,但是它们的类型是不同的。数组首元素的地址类型是int* ,而数组地址的类型是 int[10]* ,也就是数组指针类型。结合我们之前学过的指针知识,不同的指针类型决定了它访问变量时的权限和步长。
六、二维数组传参
在学习了数组指针之后,我们就可以探讨二维数组传参的本质了。我们首先编写一个程序,以函数的方式实现二维数组的打印:
void print(int arr[3][3], int row, int col) { int i = 0; int j = 0; for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } int main() { int arr[3][3] = { 1,2,3,4,5,6,7,8,9 }; print(arr, 3, 3); return 0; }
运行结果:
这里我们传入了二维数组的数组名,行数和列数,函数的形参也写成了数组形式,那么如何理解二维数组的数组名呢?
既然数组名是数组首元素地址,那么二维数组也不例外。
二维数组的数组名代表二维数组的首元素的地址,也就是第一行的地址。由于第一行是一个一维数组,所以它就是这个一维数组的地址。
既然传入的是一个一维数组的地址,那么我们也可以用数组指针的形式表示形参:
void print(int(*arr)[3], int row, int col) { int i = 0; int j = 0; for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } int main() { int arr[3][3] = { 1,2,3,4,5,6,7,8,9 }; print(arr, 3, 3); return 0; }
当然,我们在访问二维数组元素的时候,还可以这么写:
*(*(arr+i)+j);
以上语句是什么意思呢?首先,arr是第一行的地址,给它加上i,就相当于跳过了i行。之后使用 * 对这一行的地址进行解引用操作,得到这一行(也就是这一行首元素的地址)。给这个地址再加上j,就相当于跳过了j个元素,最后再对这个元素的地址进行解引用操作,就得到了这个元素的值。
以上的操作可能感觉比较复杂,但是本质上也就是通过地址来访问元素而已。当然,它与arr[i][j]是等价的。这里要特别注意:以上的形式是把指针解引用了两次,你可能会认为它是一个二级指针,实际上它跟二级指针是没有关系的,不要将它与二级指针混淆。
总结
这篇文章我们主要探讨了指针与一维数组、二维数组之间的关系。后续博主还会继续和大家探索指针的更多知识。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤