一.指针的计算
在了解指针的计算之前,我们需要明确一个知识点:指针的类型到底是什么意思?
之前我们学习过指针的定义格式为:
数据类型* 变量名
那么这个数据类型到底有什么用呢?
实际上,这是用于得到获取字节数据的个数的(如int类型的字节为4个)
1.指针中的加减法:
对指针变量进行加法运算表示指针往后(前)移动了n步,这个“步”是什么意思呢?
其实就是步长——指针移动一次走了多少个字节
举例:
#include<stdio.h>
int main() {int a;int* p = &a;// 你会发现这两个结果的差值为四个字节,即一个int类型数据所占的空间,这也就是“走了一步” printf("%p\n", p);printf("%p", p + 1);
}
2.指针的运算是否有意义
①:有意义的操作:
指针跟整数进行加减操作(每次向前(后)移动n个步长)
指针跟指针进行减操作(计算间隔步长)
②:无意义的操作:
指针跟整数进行乘除操作
没有意义的原因是:此时指针的指向不明
指针跟指针进行加,乘,除也是同样没有意义的,因为这样的操作表示不了任何的意义
二.指向不明的指针
野指针的概念:指针指向的空间未分配
悬空指针:指针指向的空间已分配,但是被释放了
像这两类指针我们要尽量避免使用,防止出错
三.没有类型的指针
即——void类型的指针
void* 变量名
特点:
无法获取数据,同时也无法计算,但是可以接收任意地址
事实上,不同类型的指针之间是不能相互赋值的,void类型的指针的作用就是打破这一点
缺点:
void类型的指针无法获取变量里面的数据,也不能进行加减的计算
使用void类型的指针完成一个能够交换所有类型的变量的函数:
#include<stdio.h>
// p1和p2分别为两个我们要交换的数据的地址,step_length表示的是该种数据类型所占的字节数
void swap(void* p1, void* p2, int step_length) {//转换为char类型的原因是char只占一个字节,正好能够便于我们一个字节一个字节的完成交换 char* pc1 = p1;char* pc2 = p2; for (int i = 0; i < step_length; i++) {char temp = *pc1;*pc1 = *pc2;*pc2 = temp;// 移动到下个字节进行交换 pc1++;pc2++;}
}int main() {int a = 10;int b = 20;int step_length = sizeof(int);swap(&a, &b, step_length);printf("a=%d,b=%d", a, b);
}
四.二级指针和多级指针
1.二级指针
(1).概念:
指向一级指针地址的指针
(2).二级指针的格式:
//跟定义一级指针只有一点小小的差别
数据类型** 指针名
//定义几级指针就加几个*
这里的指针数据类型跟指向空间中(一级指针中的)数据的类型是保持一致的
(3).二级指针的简单定义:
#include<stdio.h>
int main() {int test = 10;// 定义一级指针int* p1 = &test;//定义二级指针int** p2 = &p1;
}
(4).二级指针的作用:
①:二级指针用于操作一级指针记录的地址
简单举例:
#include<stdio.h>
int main() {int x = 10;int y = 20;// 定义一级指针int* p1 = &x;//定义二级指针int** p2 = &p1;printf("%p\n", &x);printf("%p\n", p1);//使用二级指针修改一级指针中指向的地址为y的地址 *p2 = &y;printf("%p\n", &y);printf("%p\n", p1);
}
②:利用二级指针获取变量中记录的数据
#include<stdio.h>
int main() {int test = 10;int* p1= &test;int** p2 = &p1;//使用二级指针获取数据printf("%d", **p2);
}
五.数组和指针
1.数组指针的基本用法:
(1).数组指针的概念:
指向数组的指针
(2).数组指针的作用:
方便地操作数组中的各种数据
#include<stdio.h>
int main() {int ans[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};//实际上获取的是数组的首地址 int* p = ans;int len = sizeof(ans) / sizeof(int);//使用循环遍历数组for(int i = 0; i < len; i++) {//这里依次向后遍历使用的是步长的概念 printf("%d\n", *(p + i));}
}
2.数组指针的细节:
(1).数组变量参与计算的时候会退化为第一个元素的指针,这也是上方代码中
int* p = ans;
这一行代码不用写&ans的原因(写了&ans反而会报错)
(2).特殊情况
①:在我们使用sizeof对数组长度进行运算的时候,数组变量不会退化,还是该个数组的整体
②:我们使用&+数组变量获取地址的时候也不会退化,还是表示该个数组的整体
那么这第二种特殊情况会导致什么结果呢?总所周知,我们使用循环遍历数组时使用的是步长的概念,遍历一个数就往后走一步,但是如果我们使用的是&+数组变量的形式获取地址,这一步的步长代表的就是整个数组的长度(举例:如果是一个长度为10的int类型的数组,那么这样的话一步就是40个字节,这就是所谓的获取整个数组的概念)
六.利用索引遍历二维数组
1.二维数组的概念:
把多个小数组放到一个大的数组当中
2.二维数组的定义格式:
第一种定义格式:
//可以看作一个m行n列的格子图
数据类型 arr[m][n] =
{{1, 2, 3, ..., n},{1, 2, 3, ..., n},{1, 2, 3, ..., n},...{1, 2, 3, ..., n},
};
m表示二维数组的长度,n表示放在二维数组中的一维数组的长度
第一种定义格式的遍历方式(索引):
#include<stdio.h>
int main() {//第一种定义方式的索引遍历方式 int ans[3][3] = {{1, 2, 3},{4, 5, 6},{7, 8, 9}};//第一层索引——计算二维数组中有多少个一维数组 int len1 = sizeof(ans) / sizeof(ans[0]);//第二层索引——计算一维数组中有多少个元素 int len2 = sizeof(ans[0]) / sizeof(ans[0][0]);for (int i = 0; i < len1; i++) {for (int j = 0; j < len2; j++) {printf("%d", ans[i][j]);}printf("\n"); }
}
第一种定义格式的遍历方式(指针):
#include<stdio.h>
int main() {//第一种定义方式的指针遍历方式 int ans[3][3] = {{1, 2, 3},{4, 5, 6},{7, 8, 9}};int(*p)[3] = ans;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {printf("%d", *(*p + j));}printf("\n");//移动二维数组的指针,否则就会一直打印第一个一维数组 p++;}
}
第二种定义格式:
定义的方式:先把所有的一维数组定义完毕,再放到二维数组当中
第二种定义方式的索引遍历方式:
#include<stdio.h>
int main() {//第二种定义方式的索引遍历方式int arr1[] = {1, 2, 3};int arr2[] = {4, 5, 6, 7};int arr3[] = {8, 9, 10, 11, 12};//在上方计算二层索引int index1= sizeof(arr1) / sizeof(int);int index2= sizeof(arr2) / sizeof(int);int index3= sizeof(arr3) / sizeof(int);//定义一个装有所有数组长度的数组int indexArr[] = {index1, index2, index3}; //把这三个一维数组放到二维数组当中//这里为什么使用指针类型,因为我们在这里存入一维数组时其实存入的是他们的首地址,所以我们要保证数组的数据类型跟内部存储的元素的类型保持一致 int* ans[] = {arr1, arr2, arr3}; //计算第一层索引int len1 = sizeof(ans) / sizeof(ans[0]);for(int i = 0; i < len1; i++) {
// //计算第二层索引
// int len2 = sizeof(ans[i]) / sizeof(ans[i][0]);
// 如果这么写的话是会出错的,因为此时二维数组中的一维数组已经退化为第一个元素的指针,计算出的长度为8,最后计算出的第二层索引的长度全为2
// 所以我们使用上方存储着正确的数组长度的数组来获取每个一维数组的长度 for (int j = 0; j < indexArr[i]; j++) {printf("%d ", ans[i][j]);}printf("\n");}
}
第二种定义方式的指针遍历方式:
#include<stdio.h>
int main() {//第二种定义方式的指针遍历方式int arr1[] = {1, 2, 3};int arr2[] = {4, 5, 6};int arr3[] = {8, 9, 10}; int* arr[] = {arr1, arr2, arr3};//获取指向二维数组的指针 int** p = arr;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {printf("%d ", *(*p + j));}printf("\n");//移动指向二维数组的指针 p++;}
}
注意:
数组指针和指针数组对比:
数组指针:指向数组的指针,作用:方便地操作数组中的各种数据
指针数组:存放指针的数组,作用:用于存放指针
七. 函数指针
1.格式:
返回值类型(*指针名) (形参列表)
2.作用:
利用函数指针可以动态地调动函数
3.举例:
#include<stdio.h>
int add(int x, int y) {return x + y;
}int mul(int x, int y) {return x * y;
}int main() {//设置函数指针 int (*p1)(int, int) = add;int (*p2)(int, int) = mul;//利用函数指针去调用函数printf("%d\n", p1(10, 20));printf("%d\n", p2(10, 20));
}
4.函数指针的应用场景:
做一道例题:定义加减乘除四个函数,用户键盘录入三个数字,前两个数字表示参与计算的数字,第三个数字表示调用的函数
#include<stdio.h>
int add(int x, int y) {return x + y;
}int sub(int x, int y) {return x - y;
}int mul(int x, int y) {return x * y;
}int div(int x, int y) {return x / y;
}int main() {//定义一个指针数组去装四个函数的指针int (*conclusion[4])(int, int) = {add, sub, mul, div};printf("请录入两个数字参与计算\n");int x, y;scanf("%d%d", &x, &y);printf("请录入一个数字表示要进行的计算\n");int choice;scanf("%d", &choice);//调用函数printf("最后的结果为%d", conclusion[choice - 1](x, y));
}
在这道题中我们就能看出,使用函数指针要方便得多
但是我们要注意一点——只有形参完全相同而且返回值也一样地函数才能放到同一个函数指针数组当中