文章目录
- 深入理解指针
- 一、冒泡排序
- 二、二级指针
- 三、指针数组
- 3.1 指针数组模拟二维数组
- 四、字符指针变量
- 五、数组指针变量
- 5.1 数组指针变量是什么?
- 5.2 数组指针变量的初始化
- 六、二维数组传参的本质
深入理解指针
指针系列回顾:
【C语言基础】:深入理解指针(一)
【C语言基础】:深入理解指针(二)
一、冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较。
#include<stdio.h>void bubble_sort(int arr[], int sz)
{int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;for (j = 0; j < sz - i - 1; j++){if (arr[j] > arr[j + 1]){int tmp = 0;tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}int main()
{int arr[] = { 10,9,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}
可以看到,这段代码对arr数组进行了排序,但这个代码还有一些缺陷,那就是无论数组内部的元素是否有序,他都会循环9次,这样肯定是不合理的,要进行优化一下。
我们在bubble_sort函数的第一层循环里面定义一个变量flag,进入第二层循环就修改flag的值,第二层循环结束时给变量flag来个判断,如果变量flag没有发生改变,说明没有进入第二层循环,也就是说这时数组里的元素是有序的,就会直接跳出第一层循环。另外,我们还可以在最外面定义一个全局变量用来计数,每进行一次元素交换就会自增1。
#include<stdio.h>
int count = 0;
bubble_sort(int arr[], int sz) //参数接收数组元素个数
{int i = 0;for (i = 0; i < sz - 1; i++){int flag = 1; //假设这⼀趟已经有序了int j = 0;for (j = 0; j < sz - i - 1; j++){count++;if (arr[j] > arr[j + 1]){flag = 0; //发⽣交换就说明,⽆序int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}if (flag == 1) //这⼀趟没交换就说明已经有序,后续⽆序排序了break;}
}int main()
{int arr[] = { 0,9,8,6,5,3,1,2,4,7 };int sz = sizeof(arr) / sizeof(arr[0]); // 求出数组中的元素个数bubble_sort(arr, sz);int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");printf("%d\n", count);return 0;
}
运行之后可以发现,代码的运行效率明显提升了,理论来说上述数组元素在未优化的代码下运行是要进行45次交换的,现在明显次数减少了。
二、二级指针
通过前面的知识我们知道,指针变量也是一个变量,只要是一个变量就会有地址,那么,指针变量的地址放在哪里呢?
我们把存放地址的指针称为二级指针,也就是指针的指针
#include<stdio.h>
int main()
{int a = 10;int* pa = &a;int** ppa = &pa;return 0;
}
而三级指针就是存放二级指针变量的地址,四级指针、五级指针以此类推…
对于二级指针的运算有:
- *ppa通过对ppa进行解引用,就可以找到pa,*ppa其实访问的就是pa。
int b = 20;
*ppa = &b; // 等价于pa = &b
- **pa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到的就是a。
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
三、指针数组
首先我们要弄懂一个问题:指针数组到底是一个指针还是一个数组?
这里我们可以类比一下:
整数数组:就是一个存放整型的数组。
字符数组:就是一个存放字符的数组。
那指针数组呢?没错,就是存放指针的数组。
指针数组里的每个元素都是用来存放地址(指针)的。
如下图:
通过这张图可以看到,指针数组里的每一个元素都是一个指针,而这里面的每一个指针又可以指向一块区域。
3.1 指针数组模拟二维数组
#include<stdio.h>
int main()
{int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//数组名是数组首元素的地址,类型是int*的,就可以存放在parr数组中int* parr[] = { arr1, arr2, arr3 };int i = 0;int j = 0;for (i = 0; i < 3; i++){for (j = 0; j < 5; j++){printf("%d ", parr[i][j]);}printf("\n");}return 0;
}
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。
注意:这只是利用指针数组模拟的二维数组,并非是真的二维数组,因为每一行的地址并非连续的。
四、字符指针变量
在指针的类型中我们知道有一种指针类型为字辅指针char*
一般使用:
#include<stdio.h>
int main()
{char ch = 'w';char* pc = &ch;*pc = 'w';return 0;
}
还有一种使用方式如下:
#include<stdio.h>
int main()
{const char* pc = "hello world";printf("%s\n", pc);//这里是把一个字符串放到pc指针变量里了吗?return 0;
}
注意:这里const char* pc = “hello world”; 特别容易让人认为是把字符串hello world放到字符指针pc里了,但其实本质上是字符串hello world 首字符的地址放到了pc中。
上面的代码的意思是把一个常量字符串的首字符h的地址存放到指针变量pc中。
下面是一道和字符串相关的题,我们可以学一下:
#include<stdio.h>
int main()
{char str1[] = "hello world";char str2[] = "hello world";const char* str3 = "hello world";const char* str4 = "hello world";if (str1 == str2)printf("str1 and str2 are same\n"); // 序号1elseprintf("str1 and str2 are not same\n"); // 序号2if (str3 == str4)printf("str3 and str4 are same\n"); // 序号3elseprintf("str3 and str4 are not same\n"); // 序号4return 0;
}
运行之后我们会发现,打印的是序号2和序号3的语句,这是为什么呢?
这里的str3和str4指向的是同一个常量字符串。C/C++会把常量字符串存储到一个单独的内存区域,当几个指针指向同一个字符串的时候,它们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
五、数组指针变量
5.1 数组指针变量是什么?
上面在学指针数组的时候我们知道,指针数组是数组元素为指针的数组。那么数组指针是什么呢?
上面学习指针数组的时候类比了一下,那么这里不妨也来类比一下:
- 整型指针变量:int * pint; 存放的是整型变量的地址,能够指向整形数据的指针。
- 浮点型指针变量:float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那么数组指针就是存放数组的地址,能够指向数组数据的指针变量。
我们判断一下下面那个是数组指针
int *p1[10];
int (*p2)[10];
int * p1[10]; 是一个指针数组,是 10 个整型指针的数组。
int ( * p2)[10]; 是数组指针,表示一个指向包含 10 个整型数据的数组的指针。因为 p2 是一个指针,所以 p2 是一个指针类型。
数组指针变量
int (*p2)[10];
这里p会先和 * 号结合,说明p是一个指针变量,然后指向的是一个大小为10个整型数据大小的数组,所以p是一个指针,指向一个数组,叫数组指针。
注意:[]的优先级是高于 * 号的,所以必须要加上()来保证 p会先和 * 结合。
5.2 数组指针变量的初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?其实就是我们之前学的 &数组名。
int main()
{int arr[10] = { 0 };&arr; //得到的就是数组的地址return 0;
}
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int main()
{int arr[10] = { 0 };int(*pa)[10] = &arr;return 0;
}
在调试的过程中我们也可以看到 &arr 和pa的类型是完全一致的。
数组指针类型解析:
int (*p) [10] = &arr;| | || | || | | p指向数组的元素个数| p是数组指针变量名p指向的数组的元素类型
六、二维数组传参的本质
有了数组指针的理解,我们就能够讲⼀下二维数组传参的本质了。
过去我们有一个二维数组的需要传参给⼀个函数的时候,我们是这样写的:
#include<stdio.h>void test(int a[3][5], int r, int c)
{int i = 0;int j = 0;for (i = 0; i < r; i++){for (j = 0; j < c; j++){printf("%d ", a[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} };test(arr, 3, 5);return 0;
}
这里实参是二维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
首先我们再次理解⼀下二维数组,二维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的首元素就是第一行,是个⼀维数组。
如下图:
所以,根据数组名是数组首元素的地址这个规则,⼆维数组的数组名表示的就是第一行的地址,是⼀维数组的地址。根据上面的例子,第一行的⼀维数组的类型就是 int [5] ,所以第一行的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第一行这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:
#include<stdio.h>void test(int(*p)[5], int r, int c)
{int i = 0;int j = 0;for (i = 0; i < r; i++){for (j = 0; j < c; 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} };test(arr, 3, 5);return 0;
}
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。