轻松玩转指针之C指针进阶(1)~
花有重开日,人无再少年!
文章目录
前言
Hello everyone! forever已经很久没更新了,让大家久等了,上一篇文章我们说过了,为了充分拿下指针,我们将指针部分分为初阶和进阶,初阶已经告一段落了,不知道大家是否有初步学习了解到指针呢?
接下来,我们将进行更深层次的指针介绍和学习,当然我也是和大家一样,一起学习,一起分享哦~如果有什么不足之处,请批评指针,当然大家若有什么更好的建议或文章博主也可以评论区向我推荐推荐哈!好啦,废话不多说,进入今天的正题 ~
回顾复习:
- 什么是指针
指针是内存中最小单元的一个编号。也就是地址
我们口头常说的指针,指的是指针变量,用来存放地址的变量叫指针变量
总结:指针就是地址,口头说的指针通常指的是指针变量 - 什么是指针变量
指针变量:我们可以通过&(取地址操作符)取出变量的内存真实地址,把地址可以存放到一个变量中,这个变量就是指针变量。
总结:指针变量,用来存放地址的变量。(存放在指针变量中的值都被当作地址处理),指针的大小在32位机上是4个字节,在64位机上是8个字节。 - 指针存在类型:
指针类型决定了在解引用时候一次能访问几个字节(指针的权限)
指针类型决定了指针向前或向后走一步,走多大距离(单位是字节) - 指针存在运算
指针±整数
指针-指针(前提:两个指针指向同一块空间)得到数组的长度
指针的关系运算(使用指针运算时:注意地址的运算) - 多级指针
正文
一、字符指针
在指针类型中我们知道有一种类型为字符指针类型——char*
,今天我们就来了解了解它 ~
1、字符指针的使用
代码示例解析:
#include int main(){char ch = 'W';char* p = &ch;//以上是一种用法const char* p1 = "abcd";//第二种//*p1='W';因此这种做法是错误的}
这里我们来分析一下,const char p1 = “abcd”;为什么要用const 修饰?
因为 “abcd” 这个字符串存储在只读数据区,因此它只能读取不能被改写,所以要用const修饰,const修饰后就代表不能被修改了。
所以若出现 *p1=‘W’ ; 这种做法,就是错误的
无const修饰的错误定义图:
正确定义图分析:
2、例题解析:
#include int main(){char arr1[] = "abcdef";char arr2[] = "abcdef";//这里是比较两个数组的起始地址,他们是两个数组,因此创建的起始地址不同const char* str1 = "abcde";const char* str2 = "abcde";//这里是比较两个字符串的首元素地址,因为这里字符串相同,并且//const修饰的常变量始终无法被改变,因此,内存中就直接只创建一个abcde,//这两个指针都指向同一地址,即abcde的地址if (arr1 == arr2)printf("arr1 == arr2\n");elseprintf("arr1 != arr2\n");if (str1 == str2)printf("str1 == str2\n");elseprintf("str != str2\n");return 0;}
运行结果:
图解分析:
二、指针数组
指针数组——存放指针的数组(它还是一个数组,只是数组内存放的内容类型是指针)
在《指针》章节我们也简单学习了指针数组:
int* arr[10];//整型指针数组 数组里存放的是int* 类型char* ch1[5];//一级字符指针数组 存放char* 类型char** ch2[6];//二级字符指针数组 存放的是一级指针数组地址的二级指针数组
三、数组指针
1. 数组指针
int* arr[10];//————指针数组int (*p)[10];//————数组指针
解释:
p先和 * 结合,说明p是一个指针变量,然后指针指向一个大小为10个整型的数组,所以p是一个数组指针,指向一个数组的指针叫数组指针。
这里要注意[ ]的优先级要高于*,因此要加上()来保证p先和*结合。
2. &数组名和数组名
先来看看一段简单的代码:
int main(void){int arr[] = { 0,1,2,3 };printf("%p\n", arr);printf("%p\n", &arr);}
运行结果:
呀?怎么回事,从打印结果来看,难道arr和&arr是一样的吗?
答案:当然不是啦!
我们之前有了解学习过arr是数组名,数组名表示首元素地址。
那么来看看下面的分析,让你完全掌握arr和&arr:
代码分析两个的区别:
int main(){ int arr[10] = { 0 }; printf("%p\n", arr);//这里表示数组首元素的地址 printf("%p\n", arr + 1);//这里就是首元素地址+1,即第二个元素地址 printf("%p\n", &(arr[0]));//这里直接是取首元素地址 printf("%p\n", &(arr[0])+1);//第二个元素的地址printf("%p\n", &arr);//这里表示的是取整个数组的地址,而不是数组首元素的地址。printf("%p\n", &arr + 1);//这里数组的地址加1,跳过整个数组的大小,//所以&arr+1直接跳40个字节return 0;}
运行结果图解分析:
3. 数组指针的使用
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
int main(){ char arr[5]; char(*pa)[5] = &arr;//这里是一个数组指针,一个指向数组的指针 int* parr[6];//这里是一个指针数组,一个数组里面全是指针类型 int* (*pp)[6] = &parr;//这里是指向指针数组的数组指针,因为指针数组里的每个元素都是指针 //因此这里指向它的是一个数组指针 //*pp说明它是一个指针,[6]说明它是一个指向数组的指针,而数组中的每个元素类型即就是int*//pp的类型是去掉pp——int* (*)[6]return 0;}
那数组指针有什么用呢?
看看以下代码(不推荐使用,作为理解了解内容)
int main(){//利用指针 int arr[9] = { 1,2,3,4,5,6,7,8,9 }; int* p = arr; for (int i = 0; i < 9; i++) { printf("%d ", *(p + i));//直接利用一个整型指针解决打印数组元素 }printf("\n");int(*pp)[9] = &arr;//定义数组指针for (int i = 0; i < 9; i++){printf("%d ", *((*pp) + i));//利用数组指针访问数组元素,实际上这是多此一举//这样操作反而使得访问便麻烦了,不支持使用这种方法}return 0;}
上面这个代码我们只是为了更好的理解数组指针,当然在真正实战中,不会这样使用数组指针啦!
数组指针通常用于二维数组
看代码:
void print(int(*p)[5], int m, int n){//*p是指向某一行的,每行5个元素//这里直接访问二维数组的第一行for (int i = 0; i < m; i++){for (int j = 0; j < n; j++){printf("%d ", *(*(p + i) + j));//p+i是实现行移动到第i行即第i行的地址//*(p+i)相当于拿到了二维数组的第i行首元素地址,也相当于第i行的数组名//数组名也相当于首元素地址,这里其实也就是是第i行的第一个元素地址//之后再利用+j实现每行上l列的移动访问,再对其解引用,取得值}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;}
运行结果分析:
数组指针和指针数组一起比较分析:
int main(){ int arr[5];//arr是一个整型数组,有5个元素,每个元素是int类型的 int* parr1[10];//parr1是一个数组,数组一共有10个元素,每个元素都是int*类型 //因此这是一个指针数组 int(*parr2)[10];//parr2这里先和*结合,说明parr2是一个指针,该指针 //指向一个数组,该数组有10个元素,每个元素是int类型 因此这是一个数组指针 int(*parr3[10])[5];//parr3先和[]结合,说明parr3是一个数组,数组是10个元素 //数组的每一个元素是一种数组指针,其类型是int (*)[5],该类型的指针//指向的数组有5个int类型的元素。}
图解分析:
四、数组参数和指针参数
1. 一维数组传参
看代码:
void test1(int arr[])//数组传参的时候可以写成数组形式{}void test1(int arr[10])//这里数组得大小可有可无也可以错,无任何影响,因为{} //在形参这里根本就不创建数组void test1(int* arr)//因为test1(arr1)这里arr1是首元素的地址,因此那边传的是地址{} //这里再拿指针接收合情合理,实际上上面写成的是数组的形式其实质上还是指针void test2(int* arr[20])//arr2是一个指针数组,形参这里放一个指针数组来接收它,合情合理{} //看起来也直观,也容易理解void test2(int** arr)//arr2是一个指针数组的首地址,数组里的每个元素都是指针类型,这里定义一个{} //二级指针形式的形参来接收指针数组里的首地址,没任何问题int main(){int arr1[10] = { 0 };int* arr2[20] = { 0 };test1(arr1);test2(arr2);return 0;}
图解分析:
2.二维数组传参
看代码:
/二维数组传参void test1(int arr[3][5])//形参写成数组,没任何问题{}//void tes1t(int arr[][])//虽然形参在内存里面不会真正的创建数组,但这里若写成 //二维数组的形式,最基本要符合二维数组定义的要求最多省略行//{}void test1(int arr[][5])//这里二维数组省略行不省略列的形式,完全符合{}//总结:一个二维数组传参,函数形参的设计只能省略第一个[]中的数//因为一个二维数组可以不知道有多少行,但必须要知道一行有多少个元素//void test1(int* arr)//因为二维数组传过来的是首行的地址,一行又是一个一维数组,//{} //因此,这里应该是一个数组指针—int(*)[5],指针指向一个数组 //数组中每个元素都是int类型//void test1(int* arr[5])//因为传过来的是二维数组的首行地址,因此这里int* arr[5]是数组,不能接收 //{} void test1(int (*arr)[5])//这里正确,刚好一个数组指针接收二维数组中第一行这个一维数组的地址{}//void test1(int arr)//这里定义了一个二级指针,错误不能接收//{}int main(){int arr[3][5] = { 0 };test1(arr);//这里的arr是二维数组首行的地址,将一行看成一个一维数组,因此传过去的是一个数组指针}
图解分析:
3.一级指针传参
当一个函数的参数部分是指针的时候,函数能够接收什么?
看代码:
//一级指针传参void test(int* p){}int main(){ int a = 10; int* ptr = &a;int arr[5] = { 0 };test(&a);//传一个地址过去,指针接收成立test(ptr);//传一个指针变量,指针接收成立test(arr);//传一个数组过去,指针接收成立}
4.二级指针传参
那么当一个函数的参数部分是二级指针的时候,函数又能接收什么参数?
看代码:
//二级指针传参void test(char** p){}int main(){ char ch = 'W'; char* p = &ch;char** pp = &p;char* arr[5];test(&p);//传一个一级指针的地址,用二级指针接收test(pp);//传一个二级指针,二级指针接收test(arr);//传一个指针数组的首元素地址,即一级指针的地址,用二级指针接收}
五、函数指针
函数指针——一个指向函数的指针。
函数指针的定义和介绍
//函数指针的书写和介绍int test(int x, int y){return x + y;}void test1(char* p){}int main(){int arr[5] = { 0 };int (*pa)[5] = &arr;//这里pa是一个数组指针//类比上述数组指针引出函数指针int (*pf)(int, int) = &test;//这里pf是一个函数指针,指向test函数void (*pt)(char*) = &test1;//这里pt是一个函数指针,指向test1函数int sum = (*pf)(10, 20);//test函数的调用printf("%d\n", sum);int (*ps)(int, int) = test;//这里也可以不需要取地址符号int sum1 = (ps)(2, 3);//当上面没要取地址符号的时候,这里的解引用操作符*就可以不需要,可有可无//当然(ps)这个括号也可以不需要,但是如果加上解引用操作符*,会显得逻辑强一点,符合正常代码风格printf("%d\n", sum1);return 0;}
解释说明:
上面int (pf)(int, int) = &test;
这里pf首先和结合说明是一个指针,指针指向一个函数,指向的函数有参数且有两个参数,返回值为int类型。
函数指针示例一分析:
//1、把0强制类型转换为void (*)()类型的函数指针//2、然后对其函数指针解引用,从而调用该函数//3、实际上这串代码作用就是对0地址处的函数进行调用(*( void (*p)())0)();//这是依次函数的调用,对0地址处的函数进行调用//要做好括号的断句,才能更好的理解这串代码
示例二分析:
typedef void(*pfun_t)(int);//pfun_t是一个类型名字int main(){//1、single这是一个函数声明,这个函数的参数有两个,一个是int 类型,一个是函数指针类型//2、该函数指针指向的是一个函数参数int ,返回值为void类型的函数//3、拿掉single ( int,void(*)(int) 之后剩下void (*)(int)也是一个函数指针//4、因此这个指针指向的是一个函数参数为int,返回值为void 的函数 void(*single(int, void(*)(int)))(int); //这里有简化该串代码的方式void(*single(int, pfun_t))(int);//这里利用typedef对其函数名重新定义,从而起到简化代码的作用return 0;}
六、函数指针数组
数组是一个存放相同类型数据的存储空间。
我们已经学习了指针数组,
比如:
int *arr[10];//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();int *parr2[10]();int (*)() parr3[10];
答案是:parr1
parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
函数指针数组的用途:转移表
示例:
使用函数指针数组
在这里插入代码片
//使用函数指针数组写计算器void menu(){printf("*\n");printf("* 0.exit *\n");printf("* 1.add 2.sub *\n");printf("* 3.mul 4.div *\n");printf("*\n");}double add(double x, double y){return x + y;}double sub(double x, double y){return x - y;}double mul(double x, double y){return x * y;}double div(double x, double y){return x / y;}int main(void){double m = 0, n = 0;double (*pf[5])(double, double) = { 0,add,sub,mul,div };//转移表int input = 0;double ret = 0;do {menu();printf("请选择计算类型>: ");scanf("%d", &input);if(input >= 1 && input <= 4){scanf("%lf %lf", &m, &n);if (input == 4 && n == 0)printf("输入值无意义\n");else{ret = (*pf[input])(m, n);printf("ret = %.2lf\n", ret);}}else{printf("输入错误\n");break;}} while (input);}
这道例题是利用函数指针数组,实现简单计算器。
七、指向函数指针数组的指针
指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
那么如何定义呢?
看代码:
void test(const char* str){printf("%s\n", str);}int main(){//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[10])(const char*) = &pfunArr;return 0;}
函数指针数组真正的使用还在下面我们要了解学习的回调函数上面。
八、回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时候,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件进行响应。
1.回调函数示例一:
//回调函数void menu(){printf("——————————————————————————\n");printf("—————1、加法 2、减法—————\n");printf("—————3、乘法 4、除法—————\n");printf("——————— 0、退出 —————————\n");printf("——————————————————————————\n");}double add(double x, double y){return x + y;}double sub(double x, double y){return x - y;}double mul(double x, double y){return x * y;}double div(double x, double y){return x / y;}void clac(double (*fun)(double, double))//定义一个函数指针来接收函数地址{double m = 0, n = 0;printf("请输入两个计算数:");scanf("%lf %lf", &m, &n);double ret= (*fun)(m, n);printf("%.2lf\n", ret);}int main(void){unsigned int input = 0;do{menu();scanf("%d", &input);switch (input){case 1:clac(add);//这里用函数调用函数,实现函数回调break;case 2:clac(sub);break;case 3:clac(mul);break;case 4:clac(div);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");}} while (input);}
根据上面的代码操作,forever 对回调函数简单的理解:回调函数就是定义一个A函数,然后这个A函数的形参是函数指针类型,利用这个函数指针来接收任意所需要的子函数(例如a,b,c……等函数)地址,这样就能将这些子函数当作参数拿到A函数里面使用。
如A(a) 这样的形式就是回调函数。
2. 回调函数示例二:
使用回调函数,模拟实现 qsort(采用冒泡的方式)。
这里我们先来学习了解一下 qsort 函数,并且要深入理解void* 类型指针的使用。
看代码:
void qsort (void* base, //指针 size_t num, //整型,元素个数 size_t size,//整型,一个元素的大小 int (*compar)(const void*,const void*)//函数指针 );int main(){int a = 10;//int* p = &a;//char* p = &a;void* p = &a;////void* 是一种无类型的指针,无具体类型的指针//void* 的指针变量可以存放任意类型的地址//void* 的指针不能直接进行解引用操作 //void* 的指针不能直接进行+-整数 //return;}
qsort 函数排序
qosrt 函数的使用者得实现一个比较函数
#include #include //qsort函数 — 库函数 — 快速排序的方法的实现int cmp_int(const void* e1, const void* e2){return *(int*)e1 - *(int*)e2; //这里e1-e2时候是默认升序排序,反之则降序排序}void print(int arr[], int sz){for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");}//测试qsort排序整型数组void test1(){int arr[] = { 1,2,3,4,5,6,7,8,9 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);print(arr, sz);}int main(void){test1();}
利用冒泡排序模拟实现 qsort 函数快速排序
排序整型数组:
void swap(char* fun1, char* fun2, int size){char temp = *fun1;assert(fun1 && fun2);for (int i = 0; i < size; i++){temp = *fun1;*fun1 = *fun2;*fun2 = temp;fun1++;fun2++;}}int cmp_int(const void* p1, const void* p2){return *((char*)p1) - *((char*)p2);}void bubble_sort(int* arr, int num, int size, int (*cmp)(const void* p1, const void* p2)){assert(arr);for (int i = 0; i < num - 1; i++){for (int j = 0; j < num - i - 1; j++){if ((cmp((char*)arr + j * size, (char*)arr + (j + 1) * size)) > 0){swap((char*)arr + j * size, (char*)arr + (j + 1) * size, size);}}}}void print(int* arr,int sz){for (int i = 0; i < sz; i++){printf("%d ", *(arr + i));}}void test1(){int arr[] = { 5,0,3,6,1,9,8,2 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);print(arr, sz);printf("\n");}int main(void){test1();}
排序结构体:
/利用冒泡排序实现qsort函数快速排序//排序结构体struct Stu{char name[15];int age;float score;};void Swap(char* buf1, char* buf2, int width){assert(buf1 && buf2);for (int i = 0; i < width; i++){int temp = *buf1;*buf1 = *buf2;*buf2 = temp;buf1++;buf2++;}}int cmp_by_name(const void* e1,const void* e2){return (strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name)>0);}void bubble_Stu(void* arr, int sz, int width, int (*cmp)(const void* e1,const void* e2)){assert(arr && cmp);//这里width是用来计算偏移量的for (int i = 0; i < sz - 1; i++){for (int j = 0; j < sz - i - 1; j++){if (cmp((char*)arr + j * width, (char*)arr + (j + 1) * width)>0){//这里无论是何种类型的数据,只不过就是一个元素字节大小不同的问题,所以用width作为其字节大小//然后直接计算没跳过一个元素其字节向后面偏移量的大小Swap((char*)arr + j * width, (char*)arr + (j + 1) * width, width);}}}}void print1(struct Stu arr[], int sz){for (int i = 0; i < sz; i++){printf("%s %d %.2f\n", arr[i].name, arr[i].age, arr[i].score);}}void test1(){struct Stu arr[] = { {"zhangsan",20,98.55},{"lisi",19,100.00},{"wangwu",21,91.23} };int sz = sizeof(arr) / sizeof(arr[0]);bubble_Stu(arr, sz, sizeof(arr[0]), cmp_by_name);print1(arr, sz);printf("\n");}int main(void){test1();}
结语
目前 C 指针进阶相关知识已经介绍完啦~ 当然后面还有走进C指针进阶(2),在(2)这篇文章里面,forever 主要是带大家一起去看很多笔试题目,通过题目分析进一步巩固指针,让我们完全走进指针,轻松玩转指针!
如有不足之处还请大家批评指正哈~
谢谢观看!
再见!
以上代码均可运行,所用编译环境为 vs2019 ,运行时注意加上编译头文件#define _CRT_SECURE_NO_WARNINGS 1