一.前言
我们在初阶《指针》初阶C语言-指针-CSDN博客已经讲过了一些基础知识,知道了关于指针的一些概念->
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间
2.指针的大小是固定的4/8个字节(32位平台/64位平台)
3.指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作时候的权限
4.指针的运算
接下来,我们继续探讨指针更深层的秘密->
二.字符指针
指针有一种类型叫做指针类型——char*。char* 的两种常见用法->
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
"abcdef";//这是一个字符串,当它做一个表达式时,它的值是首字符的地址值。
char* ps = "abcdef";//所以可以用字符指针来接收
return 0;
}
"abcdef"也可以看成是一个数组(内存中的一块连续空间),而这个整体可以看成数组名(因为拿到了数组名就相当于拿到了数组首元素地址,这两个是等价的),所以可以做以下操作->
要访问这段空间,可以->
但事实上,这样写是有风险的,因为ps指向的对象是一个常量字符串,它不是变量,是不能被修改的,所以为了不小心对ps解引用使用写权限,程序就会崩溃->
退出码不是0,说明程序不是正常退出。 所以为了安全,我们应该使用const修饰ps->
所以,不论说字符指针指向的是字符串的首元素还是字符串都是正确的,都是能正常访问的。 但是需要注意的是,ps存的是字符串首元素的地址,它并不能把整个字符串存进去,因为有首元素的地址加之字符串在内存空间中是连续的,会误以为,ps存入了整个字符串。
下面有一道面试题,问最后的输出是什么->
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
str1和str2是两个数组,分别代表了内存中的两块不同空间,当if str1 == str2时,数组名比较,比较的是地址是否相同,因为二者是内存种不同的两块空间(虽然存的内容是一样的),所以地址是不一样的。对于str3和str4,它们指向的是常量字符串,所以内存中肯定有一块空间,是用来存放这个常量字符串的,由于,常量字符串是只有可读属性,不具有可写属性,所以是没必要将一份相同的常量字符串保存两份的,因此,str3和str4存的都是字符串首元素的地址,二者相等是没问题的。
三.指针数组
指针数组是数组,由字符数组 - 存放字符的数组、整型数组 - 存放整型的数组,类比出指针数组 - 存放指针的数组,存放的元素在数组中的元素都是指针类型的。
int* arr1[5]; //整型指针的数组
char* arr2[10]; //字符指针的数组
但是指针数组可以用来模拟二维数组,关于这方面的介绍可以看初阶C语言-指针-CSDN博客->
int main()
{
int arr1[5] = { 1, 2, 3 ,4 ,5 };
int arr2[5] = { 2, 3, 4 ,5 ,6 };
int arr3[5] = { 3, 4, 5 ,6 ,7 };
int* arr[3] = { arr1, arr2, arr3 };
return 0;
}
#include <stdio.h>
int main()
{
char* arr[3] = { "张三", "李四", "王五" };
int i = 0;
for (i = 0; i < 3; ++i)
{
printf("%s ", arr[i]);
}
return 0;
}
四.数组指针
数组指针是指针,也可以由字符指针 - 指向字符的指针,整型指针 - 指向整型的指针,浮点型指针 - 指向浮点数的指针类比出数组指针 - 指向数组的指针。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
我们发现,数组的地址和数组的首元素地址是一样的,但他们的区别在于->数组首元素地址+1,走了4字节,数组的地址+1走了40个字节,因为指针的类型决定了指针+1,到底+几个字节,所以能看到,数组首元素的地址是int*,数组的地址是数组指针类型。那我们应该用什么类型的指针来接受&arr呢->
int* p[10] = &arr;
这样写是不可以的,这样写的话,p就是指针数组,p会先跟[]结合形成数组,int*是数组的元素类型,所以应该是*跟p优先结合成指针,指向的类型是int [10]->
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
printf("%p", p);
return 0;
}
语法规定了是这样写,注意不要写成int[10] (*p);关于int(*p)[10],可以理解成指针p指向了一个有10个元素的数组,每个元素的类型是int。
通过报错信息可以发现,在定义数组指针的时候,指向的数组大小必须指定(且是常量),否则编译器会默认为0。
接下来讲解一些数组指针的应用场景->
#include <stdio.h>
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int(*p)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);
}
return 0;
}
(*p)等价于(*&arr),然后*和&相抵消,得到arr,数组名即数组首元素的地址,但这样访问数组明显是不方便的,不常用的,没有int *p = arr方便。
二维数组之前是这样打印的,二维数组传参,形参也是使用二维数组的形式,之前提过,此时数组名(实参)是数组首元素的地址,那在形参就可以写成指针的形式(这也是数组传参的本质),先复习一下一维数组传参->
此时,形参写的也是数组形式,本质上传arr数组名,即数组首元素的地址,那还原回去用指针接收也是没问题的->
再回到刚刚的二维数组传参,二维数组的数组名,它到底是谁的地址?到底用什么类型的指针接收?二维数组的首元素是它的第0行(arr[0]),所以二维数组的数组名是第0行的地址即&arr[0],arr[0]是第0行的数组,所以用成指针做形参,它应该是指向一个数组,应该用数组指针->
#include <stdio.h>
void Print(int (*p)[5], int r, int c)
{
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; 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} };
Print(arr, 3, 5);
return 0;
}
p是一行数组的地址,它+i(步长),走的是i行,对其*拿到第i行首元素的地址,+j,才能访问那一行前j个元素。(也就是说当p是一行数组的地址时,它的下一个元素也是一行数组,只有*p拿到那一行首元素的地址,才能方便访问那一行的元素。同时也应该知道p是一个数组指针,对其解引用,得到的是其首元素的地址,因为p = &arr,*p = arr)。
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思->
int arr[5]; //数组
int *parr1[10]; //指针数组
int (*parr2)[10]; //数组指针
int (*parr3[10])[5]; //存放数组指针的数组
最后一个是parr3先跟[10]结合表示是数组,数组元素是int (*)[5],即数组指针->
五.数组参数、指针参数
写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数应该如何设计呢->
5.1一维数组传参
#include <stdio.h>
#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//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);
return 0;
}
第一个形参的部分写成数组,本质是指针,所以[]可以不用写大小,是对的。
第二个也是对的,实事上,不会真的去创建一个数组接收,大小是没有意义的,写多少都可以。
第三个是一维数组传参的本质,它传递的是数组首元素的地址,所以也是对的。
第四个,形参用数组的形式接收是没问题的。
第五个,传递的是数组首元素的地址,由于数组首元素是指针,所以用二级指针接收指针的地址,没问题。
5.2二维数组传参
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//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);
}
第一个就是数组传参,形参写成数组的形式,是没问题的。
第二个第三个是行可以省略,列不能省略,且列必须是5,因为本质是int(*arr)[5],这里的[5]要和int arr[][5]对应上,列修改成6,那个数组指针指向的数组元素也会是6。
第四个不行的,因为指针类型不一样。
第五个也是不可以的,形参是数组指针都不是指针类型。
第六个是正确的形式。
第七个形参是二级指针,指针类型也没对上。
5.3一级指针传参
#include <stdio.h>
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
一级指针传参,形参写成一级指针即可。
思考:当一个函数的参数部分为一级指针时,函数能接收什么参数呢?
void test(int *p)
{
///...
}
int a = 10;
int* ptr = &a;
int arr[10] = { 0 };
test(&a);
test(ptr);
test(arr);
只要能传一个整型指针类型过去就行。
5.4二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
int* arr[5];
test(pp);
test(&p);
test(arr);
return 0;
}
思考:当函数的参数为二级指针时,可以接受什么参数?
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0;
}
六.函数指针
数组指针 - 指向数组的指针 - 存放的是数组的地址,&数组名就是数组的地址。
函数指针 - 指向函数的指针 - 存放的是函数的地址,那怎么的到函数的地址呢?
数组名是数组首元素地址,那函数名呢?是函数的地址吗?
#include <stdio.h>
int test(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
int (*pf1)(int, int) = test;
int (*pf2)(int, int) = &test;
return 0;
}
->&函数名和函数名都是函数的地址,没什么区别。
假设我要用一个函数指针变量pf1来接收test函数的地址,那我首先是pf1 = test,因为pf1是指针,->(*pf1),函数要函数作用符(),参数列表和返回类型吧->int (*p) (int int) = test。
同时也是没有任何的警告,说明test和&test类型是一致的。
注意:(*pf1)的括号是不可以省略的,如果写成int *pf1(int, int),它就变成了一个函数声明(pf2是函数名,返回类型是int*)。 同时,函数指针的参数只需要标明参数类型(也可以写成(int x, int y)),因为没有调用函数,并没有传参的过程发生。
接下来就是如何利用pf1来调用函数->
*pf1找到函数的地址,调用函数,再开始传参...函数栈帧结束,返回值被寄存器返回回来,被ret接收。 但其实*pf1的*是多余的,写或者不写都可以,甚至想写几个就写几个->
这个和一维数组形参,int arr[100]的这个100就是个摆设,不写或者写成100000都行。但是注意别写成*pf1(2,5),优先级得注意。
阅读两段有趣的代码->
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);
第一个是先看认识的0,0的左边是(void (*)()),里面是函数指针类型,->(类型)0,就是把0强转成函数指针类型,再*解引用,即调用地址0处的函数,至于最右边的()表示调用的函数无参,是因为0被强转成函数指针类型的时候,函数的参数是(),无参。
第二个先看signal,先跟()结合,说明是个函数,然后看()内是只有类型,说明是函数声明,它的参数是int类型和函数指针类型,该类型是void(*)(int),函数的返回类型也是void (*)(int)。
返回类型可以写成void (*) (int) signal(int, void(*)(int))吗?虽然明面上不可以,但是可以类型重命名->
typedef void (*)(int) pfun_t;//这是错误的
typedef void (*pfun_t)(int);//这是对的,理解的时候可以按照上面那一种方式理解,但不能那样写。
void (*pfun_t)(int);
此时,pfun_t是函数指针变量。
typedef void (*pfun_t)(int);
此时,pfun_t是函数指针类型。typedef是类型重定义。
七.函数指针数组
指针数组 - 数组
char* arr1[5]; //字符指针数组 - 存放的是字符指针
int* arr2[5]; //整型指针数组 - 存放的是整型指针
函数指针数组 - 数组
//存放的是函数指针即函数的地址
由之前的函数指针知识可以知道,函数指针类型是 type_t (*)(type_t...)。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int (*pf1)(int, int) = &Add;
int (*pf2)(int, int) = ⋐
int (*pfArr[2])(int, int) = { &Add, &Sub };
return 0;
}
我们知道pf1和pf2的类型都是int (*)(int, int),即他们是同一种类型,即可以用数组来存储,数组类型 + 数组名->int (*pfArr[2])(int, int) = { &Add, &Sub };。
函数指针数组的用途就是:转移表
eg:计算器
#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;
}
void menu()
{
printf("-----------------------------\n");
printf("----- 1.add 2.sub -----\n");
printf("----- 3.mul 4.div -----\n");
printf("----- 0.exit -----\n");
printf("-----------------------------\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("请输入两个操作数:");
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;
}
随着计算器的函数越来越多,这个switch分支语句就越来越多,而且代码有点重复->
这些函数有一些特点,除了函数名不一样,函数返回值和参数是一样的,所以可以改造成->
#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;
}
void menu()
{
printf("-----------------------------\n");
printf("----- 1.add 2.sub -----\n");
printf("----- 3.mul 4.div -----\n");
printf("----- 0.exit -----\n");
printf("-----------------------------\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
//函数指针数组
int (*pfArr[])(int, int) = { NULL, &Add, &Sub, &Mul, &Div };
if (input == 0)
{
printf("退出计算器\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数\n");
scanf("%d%d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else
{
printf("选择错误,请重新选择\n");
}
} while (input);
return 0;
}
后续如果要加功能,直接在菜单上多添加几个选项,在函数指针数组后面接着续函数地址即可。但是这样的缺点是,函数的返回类型和参数必须是一样的,才能放入函数指针数组内。此时我们把这种情况下的函数指针数组叫做转移表。
八.指向函数指针数组的指针
指向函数指针数组的指针是一个指针指向一个数组,数组的元素都是函数指针。
int a = 10;
int b = 20;
int c = 30;
int* arr[] = { &a, &b,&c };//整型指针数组
int* (*p)[3] = &arr;//p是指针,是指向整型指针数组的指针
函数指针数组 - 数组 - 存放的是函数的地址,如刚刚写过的一个->int (*pfArr[])(int, int);,pfArr是函数指针数组,&pfArr,就需要函数指针数组指针来指向它->int (*(*p)[])(int, int) = &pfArr;
这个比较难,不太重要,这个阶段基本用不到。
九.回调函数
回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另一方调用的,用于对该事件或条件进行响应。
我们能有办法把上述四个case语句的函数封装成一个函数吗?这四段代码只有调用的函数不一样,所以我们能够封装成一个calc()函数,参数就传对应函数地址就完成了任务。
void calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数\n");
scanf("%d%d", &x, &y);
ret = pf(x, y);
}
当指针pf调用Add函数时,Add函数就被称为回调函数。这里的实现方是Add函数,我们没有直接调用Add函数,而是在特定的的事件发生(input == 1),由另一方calc函数通过函数指针调用。
再来演示一下qsort函数的使用->
qsort - 是一个库函数,底层使用的是快速排序的方式,对数据进行排序的这个函数可以直接使用,这个函数可以用来排序任意类型的数据。
排序算法有很多,我们当前学过的有冒泡排序,先回忆一下冒泡排序->
#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 + 1] < arr[j])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; ++i)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
int sz = sizeof arr / sizeof arr[0];
bubble_sort(arr, sz);//核心思想是两两相邻元素比较
print_arr(arr, sz);
return 0;
}
这个函数不好的地方在于,参数是写死的,只能排序int类型的数组。而qsort函数是可以排序任意类型的数据的,我们先来看看qsort是如何做到的->
void qsort (void* base,//待排序数组的第一个元素的地址
size_t num,//待排序数组元素个数
size_t size,//待排序数组一个元素的大小
int (*compar)(const void*,const void*)//函数指针- 指向一个函数,这个函数是用来比较两个元素的
);
第四个参数是用来告诉qsort我应该怎么来比较大小的,因为比较不同的数据的大小方式是不一样的。所以你只要提供了比较大小的方法,传给compar即可。
函数指针compar的参数是两个待比较元素的地址。void*的指针不能直接解引用,也不能进行+-整数操作,它的作用是用来存放任意类型数据的地址->
char c = 's';
char* pc = &c;
int* p = &c;//两边数据类型不一样,vs报警告
void* pc_ = &c;//这个不报警告
int a = 20;
pc_ = &a;//不报警告
void*是无具体类型的指针,可以接收任意类型的地址,它就像个垃圾桶一样。
compar的返回值是有要的, 1大于p2时返回大于0的数,等于则返回0,小于则返回<0的数。
int com_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
#include <stdio.h>
#include <stdlib.h>//包含qsort函数的头文件
int com_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; ++i)
{
printf("%d ", arr[i]);
}
}
void test1()
{
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], com_int);
print_arr(arr, sz);
}
int main()
{
test1();
return 0;
}
#include <stdio.h>
#include <stdlib.h>
//测试qsort排序结构体数据
struct Stu
{
char name[20];
int age;
};
//结构体比较大小可以按照年龄比,也可以按照名字比较
com_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void test2()
{
struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 12} };
int sz = sizeof arr / sizeof arr[0];
qsort(arr, sz, sizeof arr[0], com_stu_by_age);
}
int main()
{
test2();
return 0;
}
排序之前是按照数组初始化的顺序,再看排序后的->
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
//结构体按照名字比较
int com_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test3()
{
struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 12} };
int sz = sizeof arr / sizeof arr[0];
qsort(arr, sz, sizeof arr[0], com_stu_by_name);
}
int main()
{
test3();
return 0;
}
排序之后的数组->
qsort之所以能实现这种功能,是因为抽象出了比较方法(函数),你把比较方法传进来就能进行比较了。