1.定义一个指针变量指向数组首地址
1、知识点:
- 数组的首地址是其下标为0的元素的地址。
- 在C语言中,数组的名字(不包括函数形参中的数组名,它是一个假数组,实际上是一个地址,形参数组并不占据实际内存单元)代表数组的首地址。
2、两种方式:
- 将数组第0个元素的地址赋值给指针变量
int a[10] = {1,2,7,9,11,6,3,5,9,10}; int *p = &a[0];
- 将数组的名字赋值给指针变量
int a[10] = {1,2,7,9,11,6,3,5,9,10}; int *p = a;
2.通过指针偏移间接访问数组的每一个元素
1、知识点:
- 回顾前一节课程中的“为啥要定义指针变量的类型?”,指针变量+1并不是说指针的值(地址编号)加1,而是地址偏移了它所指向的类型所占据的内存字节数。
- 定义指针时,要保证数组的类型和指针的类型一致,比如int arr[3]; 必须用int *p; 匹配
2、标准写法:指针偏移后取星花
#include <stdio.h>
int main(int argc, char const *argv[])
{
int arr[3] = {1,2,3};
int *p;
p = arr;
/* 笨方法 */
printf("数组的首元素是:%d\n", *p);
printf("1元素是:%d\n", *(p+1));
printf("2元素是:%d\n", *(p+2));
/* 用指针遍历 */
for(int i=0; i<3; i++){
printf("address:0x%p, %d元素是:%d\n", p+i, i, *(p+i));
}
return 0;
}
3、下标法和指针法访问的效率对比:说法不明确,二者效率差距可能微乎其微
- 数组下标对数组元素进行访问时,开销较大,占用的硬件资源会更多一些。但优点是可读性好。
- 指针法对数组元素进行访问时,更有效率。但缺点是容易出错。
4、见怪不怪的做法:
- *p++:涉及到运算符的优先级,但是存在缺点(因为对p进行了修改,下次再用容易造成数组越界)
#include <stdio.h> int main(int argc, char const *argv[]) { int arr[3] = {1,2,3}; int *p; p = arr; for(int i=0; i<3; i++){ printf("%d\n", *p++); //见怪不怪的写法 } return 0; } /* 或者这么写 for(int i=0; i<3; i++){ printf("%d\n", *p); p++; } */
- *p++:强调初始化条件,每次都让p回到数组头部。
#include <stdio.h> int main(int argc, char const *argv[]) { int arr[3] = {1,2,3}; int *p; int i; for(i=0, p=arr; i<3; i++){ //每次让p回到头部 printf("%d\n", *p++); } return 0; }
- 指针下标法:把指针当做数组名,然后下标法访问,这种方法比较直观
#include <stdio.h> int main(int argc, char const *argv[]) { int arr[3] = {1,2,3}; int *p = arr; int i; for(i=0; i<3; i++){ printf("%d\n", p[i]); //见怪不怪的写法 } return 0; }
- 数组名当成指针来用 *(arr+i):
#include <stdio.h> int main(int argc, char const *argv[]) { int arr[3] = {1,2,3}; int i; for(i=0; i<3; i++){ printf("%d ", *(arr+i)); //见怪不怪的写法 } return 0; }
5、数组名是指针吗?可以用arr++吗?arr有地址吗?:
- 知识点1:数组名是地址常量。我们早就知道不能修改常量的值,所以用*arr++来访问地址元素的做法是错的。
int arr[3] = {1,2,3}; int i; for(i=0; i<3; i++){ printf("%d ", *arr++); //编译不通过,指针常量 }
- 知识点2:对数组名用sizeof运算符计算出来的是数组占用的内存空间,而对指针变量p用sizeof运算符计算出来的是指针占据的空间,且在64位操作系统中,任何类型的指针大小都是8个字节,也就是统一用8个字节表示1个地址。所以数组名不是指针,只是一个地址常量。
#include <stdio.h> int main(int argc, char const *argv[]) { int arr[3] = {1,2,3}; int *p = arr; printf("sizeOfArr is %d\n", sizeof(arr)); //3*4=12 printf("sizeOfP is %d\n", sizeof(p)); //os用8个字节表示一个地址 printf("sizeOf int* is %d\n", sizeof(int *)); //os用8个字节表示一个地址 printf("sizeOf char* is %d\n", sizeof(char *)); //os用8个字节表示一个地址 return 0; }
- 知识点3:说出来可能有点奇怪,由于数组名不是变量,所以数组名arr没有地址,对它取地址是无效操作,&arr 的值 和 arr的值是一样的,数值上都是&arr[0] 。
#include <stdio.h> int main(int argc, char const *argv[]) { int arr[3] = {1,2,3}; printf("add:%p\n", arr); //等同于&arr[0] printf("add:%p\n", &arr); //数值上等于&arr[0],没人这么用 printf("add:%p\n", *(&arr)); //数值上等于&arr[0],没人这么用 return 0; }
3.习题(函数形参接收数组名)
1、代码心得:
- 有了指针之后,我们在用数组作为函数的参数的时候,既然已经知道了数组的名字就是一个地址常量,那么不要再用假数组的方式定义形参了,要用指针来保存这个地址常量。
- 函数的形参如果是指针,我们要养成一个命名习惯:int *parr,前面加个p代表指针变量parr保存的是arr的地址。
- 函数中,为了符合人的阅读习惯,我们一般用指针下标法来间接访问数组元素。
- 题外话:当我们写scanf语句时,忘记加&来取地址了,然后程序运行时卡死,又不报错,这时候傻傻的我们不知道问题出在哪里,怎么办?按照以下步骤:用gbd软件调试程序,发现那些不报错的死机程序。(关于gdb,来自有道:你会用到它做很多事.GNU调试器(GDB)是Linux程序员使用的最多的调试工具.你可以用GDB来单步调试你的代码,设置断点和查看变量的值)
- 在编译时,后面加个-g选项,也就是在命令提示窗口中写“gcc test.c -g”,它的功能是让程序变得可调试。
-
- 然后用在运行可执行程序a.exe时,前面加个gdb选项,也就是在命令提示窗口写“gdb a.exe”,按下enter后程序进入待运行状态。
- 输入r,是run的意思,让程序跑起来。
- 遇到问题,会显示,比方说:“Segmentation fault”,段错误的意思。
- 输入q,是quit的意思,退出程序。
习题1:封装数组初始化的API、数组遍历的API
- 思路:
f1. 封装初始化数组的API:void initArry(int *parr, int len); 形参分别是保存数组arr地址的指针变量parr、数组arr的长度 f1.1 for循环,代表数组下标的循环变量i从0开始,<len 时,进入循环 f1.1.1 输入数组元素的值,用指针下标法来间接访问数组元素 f2. 封装打印数组的API:void printArry(int *parr, int len); 形参分别是保存数组arr地址的指针变量parr、数组arr的长度 f2.1 for循环,代表数组下标的循环变量i从0开始,<len 时,进入循环 f2.1.1 打印数组元素的值,用指针下标法来间接访问数组元素 f2.2 换行 1. 定义一个数组arr 2. 用sizeof运算符获取数组arr的长度len 3. 用API1. 初始化数组arr 4. 用API2. 打印数组arr
- 代码:
#include <stdio.h> void initArry(int *parr, int len); void printArry(int *parr, int len); int main(int argc, char const *argv[]) { int arr[5]; int len = sizeof(arr) / sizeof(arr[0]); initArry(arr, len); //指针名是一个地址常量 printArry(arr, len); return 0; } void initArry(int *parr, int len) { int i; for(i=0; i<len; i++){ printf("请输入第%i个元素的数值:\n", i+1); scanf("%d", &parr[i]); //指针下标法来间接访问数组元素 } } void printArry(int *parr, int len) { int i; for(i=0; i<len; i++){ printf("%d ", *(parr+i)); //指针偏移取星花的方法来间接访问数组元素 } putchar('\n'); }
习题2:封装选择排序法的函数
- 思路:回顾选择排序法
- 代码:
void selectionSort_SmallToLarge(int *parr, int len) { int i, j; //分别是外、内层循环变量 int temp; //用于交换的临时变量 int sword; //尚方宝剑 for (i=0; i<len-1; i++){ sword = i; //暂时把尚方宝剑交给第i个人 for (j=i+1; j<len; j++){ //j每次都从i+1开始,因为一轮开始时尚方宝剑在第i个人手中 if (parr[sword] < parr[j]){ //尚方宝剑在parr[sword]位置,让parr[j]轮流去挑战,大的夺走sword sword = j; } } if (sword != i){ //判断尚方宝剑是否还在第i个人手中 temp = parr[i]; parr[i] = parr[sword]; parr[sword] = temp; } } }
习题3:封装逆序存放数组元素的函数
- 思路:for循环的双循环变量法、数组长度不管是奇数还是偶数,都是相同思路。
f1. 封装翻转数组的API:void reverseArry(int *parr, int len); 形参分别是保存数组arr地址的指针变量parr、数组arr的长度 f1.1 for循环,代表数组头部的循环变量head从0开始,代表数组尾部的循环变量tail从len-1开始,当 head<tail 时,进入循环 f1.1 用临时变量head交换第head个数组元素和第tail个数组元素,用指针下标法来实现间接访问数组元素 f1.2 改变循环变量head和tail: head++; tail--; 1. 定义一个数组arr并初始化 2. 用sizeof运算符获取arr的长度 3. 用API1. 翻转数组arr 4. 打印数组元素,验证
- 代码:
#include <stdio.h> void reverseArry(int *parr, int len); int main(int argc, char const *argv[]) { int i; int arr[5] = {66,88,33,44,22}; int len; len = sizeof(arr) / sizeof(arr[0]); reverseArry(arr, len); for(i=0; i<len; i++){ printf("%d ", arr[i]); } return 0; } void reverseArry(int *parr, int len) { int head, tail; int temp; for(head=0,tail=len-1; head<tail; head++,tail--){ temp = parr[head]; parr[head] = parr[tail]; parr[tail] = temp; } }