十.指针进阶(对指针的深度理解)
创始人
2024-05-07 10:19:25
0

目录

一. 字符指针

1.字符指针的定义

2.字符指针的用法

3.字符指针练习

二. 数组指针

1.指针数组的定义

2.指针数组的用法

三. 指针数组

1.数组指针的定义

2.数组名和&数组名的区别

3.数组指针的用法

4.练习

四. 数组传参和指针传参

1.一维数组传参

2.二维数组传参

3.一级指针传参

4.二级指针传参

 五. 函数指针

1.函数指针的定义

2.取函数指针的地址

3.函数指针的用法

4.练习

六. 函数指针数组

1.函数指针数组的定义

2.函数指针数组的用法

七. 指向函数指针数组的指针

1.指向函数指针数组的指针的定义

2.指针总结

八. 回调函数

1.回调函数的概念

2.回调函数的例子

3.qsort函数

4.模拟实现冒泡排序版qsort函数


前言:

指针的主题,我们在初级阶段的《六.初阶指针_殿下p的博客-CSDN博客》章节已经接触过了,我们知道了指针的概念:

1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
4. 指针的运算。

这个章节,我们继续探讨指针的高级主题。


一. 字符指针

1.字符指针的定义

定义:字符指针,常量字符串,存储时仅存储一份(为了节约内存)

char *pa="string";

2.字符指针的用法

用法:

int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}

 对于指向字符串的字符指针:

int main()
{char* pstr = "hello world";printf("%s\n", pstr);return 0;
}//打印结果为:hello world

上面代码 char* pstr = " hello world "  特别容易让人以为是把 hello world 放在字符指针 pstr 里了,但是本质上是把字符串 hello world 首字符的地址放到了 pstr 中。

3.字符指针练习

下面代码将输出什么结果呢?

int main()
{char str1[] = "abcdef";char str2[] = "abcdef";const char* str3 = "abcdef";const char* str4 = "abcdef";if (str1 == str2)printf("str1 == str2\n");elseprintf("str1 != str2\n");if (str3 == str4)printf("str3 == str4\n");elseprintf("str3 != str4\n");return 0;
}

运行结果:

这是因为:

(1)str1和str2是数组,在内存中开辟两块内存空间,这两块内存空间的起始地址不相同,这两个值自然不一样。

(2)“abcdef”是常量字符串,本身不可以被修改,在内存中这个常量字符串只开辟一块内存空间,str3和str4是两个字符指针,这两个指针都指向该字符串的首字符地址,所以str3和str4相等。

 

二. 数组指针

1.指针数组的定义

定义:指针数组是数组,数组中存放的是指针(地址)

int arr1[10];    //整型数组
char arr2[5];    //字符数组
int *parr1[10];  //存放整形指针的数组--指针数组
char *parr2[5];  //存放字符指针的数组--指针数组

 注:[] 优先级高于*,会先与 p 结合成为一个数组,再由 int* 说明这是一个整型指针数组。

2.指针数组的用法

#include int main()
{int arr1[] = {1, 2, 3, 4, 5};int arr2[] = {2, 3, 4, 5, 6};int arr3[] = {3, 4, 5, 6, 7};int* p[] = { arr1, arr2, arr3 }; // 首元素地址int i = 0;for(i=0; i<3; i++) {int j = 0;for(j=0; j<5; j++) {printf("%d ", *(p[i] + j)); // p[i]表示遍历指针数组内的每一个指针//*(p[i]+j)则表示每一个指针向后移动j个元素所指向的元素// == p[i][j] }printf("\n");}return 0;
}

 

 如下图所示:

三. 指针数组

1.数组指针的定义

定义:数组指针是指针,是指向数组的指针。

整形指针 - 是指向整型的指针

字符指针 - 是指向字符的指针

数组指针 - 是指向数组的指针

int arr[n];
int (*p)[n]=&arr;  //数组指针

前面提到过,[ ]的优先级高于*,所以加上括号,p先和*结合,说明p是一个指针变量,然后指向的是一个大小为n个整型的数组,所以p是一个指针,指向一个数组,叫数组指针。

2.数组名和&数组名的区别

先观察如下代码:

int main()
{int arr[10] = {0};printf("%p\n", arr);printf("%p\n", &arr);return 0;
}

 

 发现它们的地址是一样的,但其实:

 arr和&arr的值一样,但含义却不一样:

int main()
{int arr[10] = { 0 };int* p1 = arr;int(*p2)[10] = &arr;printf("%p\n", p1);printf("%p\n", p1 + 1);printf("%p\n", p2);printf("%p\n", p2 + 1);return 0;
}

 

我们发现arr+1跳过一个整形,而&arr+1跳过一个数组。

这是因为,arr表示数组首元素的地址,&arr表示整个数组的地址。

总结:

数组名是数组首元素的地址,但是有 2 个 例外:

①  sizeof ( 数组名 )  - 数组名表示整个数组,计算的是整个数组的大小,单位是字节。

②  &数组名 - 数组名表示整个数组,取出的是整个数组的地址。

3.数组指针的用法

二维数组以上常使用数组指针:

void print1 (int arr[3][5], int row, int col)
{int i = 0;int j = 0;for(i=0; i{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};// print1(arr, 3, 5);print2(arr, 3, 5); // arr数组名,表示数组首元素的地址return 0;
}

4.练习

分析下以下代码的含义:

int arr[5];                                                   
int* parr1[10];                                                             
int (*parr2)[10];                                                        
int (*parr3[10])[5];

解析:

//arr是一个有5个元素的整型数组
int arr[5];             //parr1是一个指针数组,数组有10个元素,每个元素都是int*的指针                                      
int* parr1[10];                                           //parr2是一个数组指针,该指针指向一个数组,数组有10个元素,每个元素都是int型                  
int (*parr2)[10];                                                       //parr3是一个数组指针数组,该数组存放10个数组指针,
//每个数组指针能够指向一个数组,数组有5个元素,每个元素的类型为int型 
int (*parr3[10])[5];


四. 数组传参和指针传参

1.一维数组传参

判断下面函数的形参是否合理:

void test(int arr[]) //ok?
{}
void test(int arr[10]) //ok?
{}
void test(int *arr) //ok?
{}
void test(int *arr[]) //ok?
{}
void test2(int *arr[20]) //ok?
{}
void test2(int **arr) //ok?
{}int main()
{int arr[10] = {0};int* arr2[20] = {0};test(arr);test2(arr2);
}

 答:以上都合理

2.二维数组传参

判断以下函数的参数是否合理:

void test(int arr[3][5]) //0k?
{}
void test(int arr[][5]) //0k?
{}
void test(int arr[3][]) //ok?
{}
void test(int arr[][]) //ok?
{}void test(int* arr) //ok?
{}
void test(int* arr[5]) //ok?
{}
void test(int(*arr)[5]) //ok?
{}
void test(int** arr) //ok?
{}int main()
{int arr[3][5] = {0};test(arr); // 二维数组传参return 0;
} 

 答:只有第一,第二和第七个合理,其他都不行,理由如下:

//直接数组传参
void test(int arr[3][5])//可以 
{}//数组传参,行可以省略
void test(int arr[][5]) //可以
{}//数组传参,列不可以省略
void test(int arr[3][]) //不可以
{}//同上
void test(int arr[][]) //不可以
{}//数组名表示数组首元素的地址,二维数组的首元素为第一行一维数组的地址,
//需要一个指向数组的数组指针来接受,一个一阶指针接受不下
void test(int* arr) //不可以
{}//参数部分为指针数组,不是指针,无法接受二维数组的首元素地址
void test(int* arr[5]) //不可以
{}//数组指针,该指针指向的数组有5个元素,可以接受二位数组的首地址
void test(int(*arr)[5]) //可以
{}//二维数组名表示第一行数组的地址,二级指针容纳不下
void test(int** arr) //不可以
{}int main()
{int arr[3][5] = {0};test(arr); // 二维数组传参return 0;
} 

3.一级指针传参

例:

void print(int* ptr, int sz) // 一级指针传参,用一级指针接收
{int i = 0;for(i=0; i

那么问题来了,当函数参数为一级指针时,可以接收什么样的参数呢?

如下所示:

void test1(int* p)
{}int main()
{int a = 10;int* pa = &a;test1(&a); test1(pa); return 0;
}

4.二级指针传参

例:

void test(int** ptr)
{printf("num = %d\n", **ptr);
}int main()
{int n = 10;int* p = &n;int** pp = &p;test(pp);test(&p); // 取p指针的地址,依然是个二级指针return 0;
}

还是那个问题,当函数的参数为二级指针时,可以接收什么样的参数呢?

如下所示:

void test(int **p) // 二级指针
{;
}int main()
{int *ptr;int** pp = &ptr;test(&ptr); // 传一级指针变量的地址 test(pp); // 传二级指针变量 int* arr[10]; //指针数组test(arr); // 传存放一级指针的数组,因为arr是首元素地址,元素类型为int*  return 0;
}

 
五. 函数指针

1.函数指针的定义

定义:指向函数的指针,存放函数地址的指针

int Add(int x, int y)
{return x + y;
}int main()
{int (*pf)(int, int) = &Add; //pf是一个函数指针return 0;
}

int ( * pf ) (int ,int ) = & Add;

解释:*先于pf结合,表示pf是一个指针,然后pf指向一个函数,括号内表示该函数有两个参数,参数类型都为int ,对于函数来说还有返回类型,最前面的int表示函数的返回类型

2.取函数指针的地址

函数也是有地址的,取函数地址可以通过 &函数名 或者 函数名 实现。

但是,要注意:

  •  函数名  ==  &函数名  (两者的含义是一样的,都表示函数的地址)
  •  数组名  !=  &数组名

如下所示:

int Add(int x, int y)
{return x + y;
}int main()
{// 函数指针 - 存放函数地址的指针// &函数名  - 取到的就是函数的地址printf("%p\n", &Add);printf("%p\n", Add);return 0;
}

 地址是一样的:

 

 

3.函数指针的用法

例:

int Add(int x, int y)
{return x + y;
}int main()
{int (*pf)(int, int) = &Add; //创建函数指针,指向Add函数int ret = (*pf)(3, 5); // 对 pf 进行解引用操作,找到它所指向的函数,然后对其传参printf("%d\n", ret);return 0;
}

那么能不能把(*)pf(3,5)写成*pf(3,5)呢?

答案是不可以的,因为这么写会导致*对函数返回值进行解引用,所以星号一定要用括号括起来。

当然也可以选择不写*,因为前文提到过,函数名和&函数名都表示函数的地址

int Add(int x, int y)
{return x + y;
}int main()
{int (*pf)(int, int) = &Add;// int ret = Add(3, 5);int ret = pf(3, 5);printf("%d\n", ret);return 0;
}

 结果是一样的,说明(*pf)前的*加不加都可以。

4.练习

(1)例一:

(*(void (*)())0)();

 解析:这段代码的作用其实是调用 0 地址处的函数,该函数无参,返回类型是 void

 如下图所示:

 (2)例二:

void (*signal(int, void(*)(int)))(int);

 解析:这段代码是对函数的声明

如下图所示:

1.signal先与()结合,说明signal是函数

2.signal函数的第一个参数类型是int,第二个参数类型是函数指针,该函数指针,指向一个参数为int,返回类型是void的函数。

3.signal函数的返回类型也是一个函数指针,该函数指针,指向一个参数为int,返回类型为void的函数。

上面的函数声明看上去过于冗杂,一眼让人难以察觉这段代码的真正含义,我们可以做如下简化:

int main()
{void (* signal(int, void(*)(int)) )(int);typedef void(*pfun_t)(int); // 对void(*)(int)的函数指针类型重命名为pfun_tpfun_t signal(int, pfun_t); // 和上面的写法完全等价return 0;
}

 用typedef对重复出现的函数指针进行重命名,这样该函数声明就一目了然了。


六. 函数指针数组

1.函数指针数组的定义

定义:如果要把函数的地址存到一个数组中,那这个数组就叫函数指针数组。

int Add(int x, int y) {return x + y;
}int Sub(int x, int y) {return x - y;
}int main()
{int (*pf)(int, int) = Add;int (*pf2)(int, int) = Sub;int (*pfArr[2])(int, int) = {Add, Sub}; //函数指针数组,数组元素为函数的地址return 0;
}

2.函数指针数组的用法

引例:实现一个计算器,可以进行简单的加减乘除运算。

代码1:

include void menu()
{printf("*****************************\n");printf("**    1. add     2. sub    **\n");printf("**    3. mul     4. div    **\n");printf("**         0. exit         **\n");printf("*****************************\n");
}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 input = 0;do {menu();int x = 0;int y = 0;int ret = 0;printf("请选择:> ");scanf("%d", &input);switch(input) {case 1:printf("请输入2个操作数:> ");scanf("%d %d", &x, &y);ret = Add(x, y);printf("ret = %d\n", ret);break;case 2:printf("请输入2个操作数:> ");scanf("%d %d", &x, &y);ret = Div(x, y);printf("ret = %d\n", ret);break;case 3:printf("请输入2个操作数:> ");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("请输入2个操作数:> ");scanf("%d %d", &x, &y);ret = Div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("重新选择\n");break;}} while(input);return 0;
}

 运行结果:

当前代码看着主要功能都实现了,但是还有很多可以优化的地方:

 

  • 当前代码冗余,存在大量重复出现的语句。
  • 添加计算器的功能(比如 a & b,a^b.....)时每加一个功能都要写一段case,每写一段case都会使代码显得很长,很没技术含量,能否更方便地增加?

这时候使用函数指针数组就会方便很多:

#include void menu()
{printf("*****************************\n");printf("**    1. add     2. sub    **\n");printf("**    3. mul     4. div    **\n");printf("**         0. exit         **\n");printf("*****************************\n");
}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 input = 0;do {menu();// pfArr 就是函数指针数组int (*pfArr[5])(int, int) = {NULL, Add, Sub, Mul, Div};int x = 0;int y = 0;int ret = 0;printf("请选择:> ");scanf("%d", &input);if(input >= 1 && input <= 4) {printf("请输入2个操作数:> ");scanf("%d %d", &x, &y);ret = (pfArr[input])(x, y);printf("ret = %d\n", ret);  }else if(input == 0) {printf("退出程序\n");break;} else {printf("选择错误\n");}} while(input);return 0;
}

这就是函数指针数组的应用。接收一个下标,通过下标找到数组里的某个元素,这个元素如果恰好是一个函数的地址,就会去调用那个函数。它做到了一个 "跳板" 的作用,所以我们通常称这种数组叫做 转移表 。


七. 指向函数指针数组的指针

1.指向函数指针数组的指针的定义

定义:指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素是函数指针。

int Add(int x, int y) {return x + y;
}int main()
{int arr[10] = {0};int (*p)[10] = &arr; // 取出数组的地址int (*pfArr[4])(int, int); // pfArr是一个数组 - 函数指针的数组int (* (*ppfArr)[4])(int, int) = &pfArr;// ppfArr 是一个指针,指针指向的数组有4个元素// 每个元素的类型是一个函数指针 int(*)(int, int)return 0;
}

2.指针总结

void add(int,int); //函数int arr[10];  //数组int *prr[10];  //指针数组int (*pa)[10]; //数组指针int (*padd)(int,int)=add;  //函数指针int (*parr[10])(int,int);  //函数指针数组int (*(*pparr)[10])(int,int)=&parr;  //指向函数指针的数组


八. 回调函数

1.回调函数的概念

回调函数是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时候,我们就称之为回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的时间或条件发生时由另外的一方调用的,用于该事件或条件进行响应。

2.回调函数的例子

用上面switch版本的计算器为例:

#include void menu()
{printf("*****************************\n");printf("**    1. add     2. sub    **\n");printf("**    3. mul     4. div    **\n");printf("**         0. exit         **\n");printf("*****************************\n");
}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;
}void Calc(int (*pf)(int, int))
{int x = 0;int y = 0;printf("请输入2个操作数:>");scanf("%d %d", &x, &y);printf("%d\n", pf(x, y));
}int main()
{int input = 0;do {    menu();printf("请选择:>");scanf("%d", &input);switch(input) {case 1:Calc(Add);break;case 2:Calc(Sub);break;case 3:Calc(Mul);break;case 4:Calc(Div);break;case 0:printf("退出\n");break;default:printf("选择错误\n");break;}} while(input);return 0;
}

 解析:

3.qsort函数

定义:qsort 函数是C语言编译器函数库自带的排序函数( 需引入头文件 stdlib.h )

#include void qsort(void* base,   //待排序数组的首元素size_t num,   //待排序数组的元素个数size_t size,  //待排序数组的每个元素的大小,单位是字节int (*compar)(const void*, const void*)  //可以实现 比较待排序数据大小的 函数);

 让我们回顾下冒泡排序:

#include 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-1-i; j++) {if(arr[j] > arr[j + 1]) {// 交换int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++) {printf("%d ", arr[i]);}printf("\n");
}int main()
{int arr[10] = {9,8,7,6,5,4,3,2,1,0};int sz = sizeof(arr) / sizeof(arr[0]);print_arr(arr, sz);bubble_sort(arr, sz);print_arr(arr, sz);return 0;
}

会发现,这个冒泡排序只能实现整形数据的排序,当我们想要对字符串或者结构体排序时,这个冒泡排序就显得有些寒酸了,而qsort函数可以帮助我们实现任意数据类型的排序:

qsort 整型数据排序(升序):

#include 
#include /*
void qsort (void* base,size_t num,size_t size,int (*cmp_int)(const void* e1, const void* e2));
*/int cmp_int(const void* e1, const void* e2)
{// 升序: e1 - e2return *(int*)e1 - *(int*)e2;
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++) {printf("%d ", arr[i]);}printf("\n");
}void int_sort()
{int arr[] = {9,8,7,6,5,4,3,2,1,0};int sz = sizeof(arr) / sizeof(arr[0]);// 排序(分别填上四个参数)qsort(arr, sz, sizeof(arr[0]), cmp_int);// 打印print_arr(arr, sz);
}int main()
{int_sort();return 0;
}

运行结果为:0 1 2 3 4 5 6 7 8 9

qsort 对结构体排序:

#include 
#include 
#include struct Stu
{char name[20];int age;
};/*
void qsort (void* base,size_t num,size_t size,int (*cmp_int)(const void* e1, const void* e2));
*///按照年龄来排序
int cmp_struct_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}//按照名字,也就是字符串来排序
int cmp_struct_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}void struct_sort()
{// 使用qsort函数排序结构体数据struct Stu s[3] = { {"Ashe", 39},{"Hanzo", 38},{"Ana", 60}};int sz = sizeof(s) / sizeof(s[0]);// 按照年龄排序qsort(s, sz, sizeof(s[0]), cmp_struct_age);// 按照名字来排序qsort(s, sz, sizeof(s[0]), cmp_struct_name);
}int main()
{struct_sort();return 0;
}

运行结果:

按照年龄排序:

按照姓名(字符串)排序:

4.模拟实现冒泡排序版qsort函数

模拟qsort实现一个冒泡排序版本的通用排序算法:

#include 
#include struct Stu 
{char name[20];char age;
};// 模仿qsort实现一个冒泡排序的通用算法
void Swap(char*buf1, char*buf2, int width) {int i = 0;for(i=0; i 0) {//交换Swap((char*)base+j*width, (char*)base+(j+1)*width, width);     }}}
}int cmp_struct_age(const void* e1, const void* e2) {return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_struct_name(const void* e1, const void* e2) {return strcmp( ((struct Stu*)e1)->name, ((struct Stu*)e2)->name );
}
void struct_sort()
{// 使用qsort排序结构体数据struct Stu s[] = {"Ashe", 39, "Hanzo", 38, "Ana", 60};int sz = sizeof(s) / sizeof(s[0]);// 按照年龄排序bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_age);// 按照名字排序bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_name);
}void print_arr(int arr[], int sz) 
{int i = 0;for(i=0; i

 


本篇到此结束,码文不易,还请多多支持哦!

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...