今天我们来看进阶指针,还没有看过初阶指针的话建议先看看初阶
(3条消息) 初阶指针---从入门到入坟_KLZUQ的博客-CSDN博客
目录
1. 字符指针
2. 指针数组
3. 数组指针
3.1 数组指针的定义
3.2 &数组名VS数组名
3.3 数组指针的使用
4. 数组参数、指针参数
4.1 一维数组传参
4.2 二维数组传参
4.3 一级指针传参
4.4 二级指针传参
5. 函数指针
6. 函数指针数组
7. 指向函数指针数组的指针
8. 回调函数
话不多说,我们来看进阶指针
int main() {
char x = 'a';
char* p = &x;
printf("%c", *p);
}
我们常用的字符指针是这样的,那还有什么方法呢?
int main() {
char* p = "abcdef";
}
我们还可以把常量字符串赋给字符指针,这段代码的意思是把字符串常量“abcdef”的首字符a的地址赋值给指针变量p,在内存里大概是这样的
在这里,我们的代码其实是不标准的,因为我们说后面的字符串是常量字符串,常量是不能被修改的,所以我们需要加上const,在某些编译器下,不加const是会出现警告的
int main() {
const char* p = "abcdef";
}
加上const后,会更加严谨,防止后期有人修改,比如
这样的话程序就崩掉了,加上const就可以防止这样的状况出现
如果我们想要打印这个字符串,使用%s打印即可
为什么不用*p呢?*p是解引用,得到的是a这个字符
字符串打印,我们只需知道其实位置,它就会不断打印,直至\0
接下来我们来看一个例子
int main()
{
char str1[] = "hello";
char str2[] = "hello";
const char* str3 = "hello";
const char* str4 = "hello";
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,是两个数组,他们会在空间里开辟各自的空间来存放字符串,而对于str3来说,hello是常量字符串,常量是无法修改的,所以这个常量字符串会放在一个空间里,再看str4,对于它来说hello也是常量字符串,也是不能修改的,而且这个字符串和str3的是一模一样,那我们就没有必要存储两份,浪费空间,所以str3和str4就共用了一个空间,他们在内存里大概是这样的
(字符常量,一般是放在只读常量区的)这就是为什么会出现这样结果的原因
我们之前介绍过指针数组,是用来存放指针的数组
int main() {
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
char* a[4] = { "hello world","abcedf","你好","12345" };
}
我们来看a4,我们可以把四个字符串常量的首元素地址存放到了数组了
他们在内存里大概是这样的
数组a是连续的,但是4个字符串并不一定是在一块的 ,我们可以把他们打印出来
我们前面说过,字符串常量是不能修改的,所以这时候最严谨的写法应该在数组a前面加上const
我们上面举了一个char类型的例子,接下来我们来看一个int类型的例子
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 arr4[5] = { 4,5,6,7,8 };
int* arr5[4] = { arr1,arr2,arr3,arr4 };
}
他们在内存里大概是这样的
我们把他们打印出来看看
我们学过了指针数组,知道这是数组,那数组指针是什么呢?答案是指针,就和它的名字一样,是指向数组的指针,我们来通过对比看看
int main() {
char ch = 'a';
char* pc = &ch;
int num = 10;
int* pi = #
int arr[10];
int(*pa)[10] = &arr;
}
pa就是一个数组指针,它是指向一个有10个元素数组的指针,而数组的类型是int类型,因为[ ] 的优先级要高于*,所以我们需要给*pa加上( )
这个我在之前文章里有详细介绍,大家可以去看看
(3条消息) 数组传参究竟是怎么一回事?_KLZUQ的博客-CSDN博客_传参数组
我们打印数组的方法多种多样,除了我们平时使用的直接打印,还可以这样打印,那还有没有别的方法呢? 当然有,我们可以这样做
但是这样非常变扭,我们基本不使用,我们接着往下看
我们这样打印出了一个二维数组,*(p+i)相当于让我们找到了对应的一行,而通过[ j ]让我们访问到了对应的元素,我们知道*(p+i)和p[ i ]是等价的,所以我们还可以写成这样
而我们的参数int(*p)[4],就是一个数组指针,我们继续往下看
int main() {
int arr[10];
int(*p)[10] = &arr;
}
p存放的是arr的地址,也就是&arr,所以*p等于*&arr,也就等于arr,所以我们访问数组时(*p)[ i ]和arr[ i ]是一样的,知道了这些,我们来看一个例子
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
arr是整形数组,数组有5个元素,parr1是一个指针数组,数组有10个元素,每个元素是int*类型
parr2是数组指针,该指针指向一个数组,数组有10个元素,每个元素是int类型
parr3是数组,数组有10个元素,数组的每个元素的类型是数组指针,类型为int(*)[5],也就是说parr3是一个存放数组指针的数组
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
我们来看一个例子
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);
}
第一个test,我们数组传参,用数组接收,是ok的,第二个test同理,第三个test我们用指针接收,而数组名表示首元素地址,我们可以放在指针里,是ok的,第一个test2,我们传参为int *arr2[20],用int*arr[20]接收,是ok的,第二个test2,数组名表示首元素地址,而数组为int*类型,int*的地址,就是int**,所以是ok的
我们来看例子
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);
}
对于第一个test,传参接收为3行5列,是ok的,第二个test,列是不能省略的,是错误的,第三个test,行可以省略,是ok的,第四个test,二维数组为3行5列,数组名是首元素地址,是第一行地址,所以是不行的,第五个test,是数组,但又不是二维数组,可以存指针,但又不是指针,是错误的,第六个test,arr是指针,后面[ 5 ] 代表是指针,可以存储5个元素,每个元素是int,相当于多个第四个test,是ok的,第七个test,传过来的是第一行的地址,不可以用二级指针接收,是错误的
我们来看这个例子
void print(int *p, int sz)
{
int i = 0;
for(i=0; i
指针变量传参,用指针变量接收,这和数组是不一样的,这里是正确的
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
我们可以传以下内容
void test(int* p){}
int main() {
int a = 10;
int* p = &a;
int arr[10];
test(arr);
test(p);
test(&a);
}
我们直接看例子
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
二级指针传参,用二级指针传参即可,也可以传一级指针的地址
当函数的参数为二级指针的时候,可以接收什么参数?
void test(int** p){}
int main() {
int** ptr;
int* p;
int* arr[10];
test(ptr);
test(&p);
test(arr);
}
数组指针是指向数组的指针,那么函数指针就是指向函数的指针
我们接着往下看
int Add(int x, int y) {
return x + y;
}
int main() {
int (*pf)(int,int) = &Add;
}
这里的pf,就是一个用来存放函数地址的指针变量,因为pf是指针,所以我们在它前面加上*,函数的参数类型为(int,int),所以我们也加上参数类型,函数返回类型为int,所以我们在前面加上int,注意,这个函数指针的名字为pf,而不是*pf,*只是告诉我们pf是指针而已
&函数名和函数名是一样的,所以我们也可以写成 int (*pf)(int,int) = Add;
当我们拿到函数的地址后,我们可以通过函数的地址来调用函数
pf是指针,所以我们需要解引用,然后我们给它传参,返回类型为int,所以我们需要接收
而Add函数我们也可以直接进行调用,比如 int sum = Add(2,3); 诶,我们发现,这和我们通过pf调用是差不多的,说明Add和pf一样的,那我们就可以直接写成
所以说明我们没有必要按照(*pf)(2,3) 来进行调用,这样只是方便理解,那么这里的*就是一个摆设,既然是摆设,那我们这样写也是无关的
无论写多少颗*,都是无意义的 ,但是如果写了*,那就必须用( )把*和pf括起来,不然是错误的,会变成pf(2,3),然后解引用,对5解引用,这就出现问题了
知道了这些,我们来看两个有趣的例子
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
我们先看代码1,我们从0开始下手,0的前面是括号,而括号里面是void (*)(),这是一个函数指针,不认识的话我们可以这样:void (*p)(),这样是不是就顺眼多了?我们继续看,0的前面是括号,被强制转换为函数指针类型,0是一个地址,而前面有一个*,说明在解引用,然后调用函数,这个函数是无参的,所以后边的括号是空的,这段代码的意思就是把0地址的函数调用了一下,把0看作一个函数的地址,那个函数的参数和返回类型都是void
总结一下,代码1是一次函数调用,先调用0地址处的函数,首先将0强制类型转换为void (*)()的函数指针,然后调用0地址处的函数
我们再看代码2,也是从名字开始下手,signal并没有和*用括号括起来,说明signal是和后边的一起的,说明signal是一个函数名,后边是它的两个参数类型,第一个是int类型,第二个是void(*)(int)类型的函数指针,然后我们把这个拿出去,就变成了
前面的部分也是一个函数指针,我们这样看,就很好理解了,但这种写法是错误的,所以就变成了我们看到的样子,这段代码是一次函数声明,声明函数名字为signal,函数参数有两个,一个为int,一个是函数指针类型,该函数指针可以指向参数类型为int,返回类型为void的函数,signal函数的返回类型也是一个函数指针,类型为void (*)(int),我们可以这样理解
我们看第一行,我们把这样类型的函数指针起名为pf-t,但是这样的写法是错误的,所以变成了第二行的形式,然后我们把signal重新修改一下,就变成了这样,便于我们理解
对于这个指针类型重命名,我们需要把名字放在*旁边
int sum(int x, int y) {
return x + y;
}
int main() {
char* ch[5];//指针数组
int arr[10];
int(*pa)[10] = &arr;//数组指针
int (*pf)(int, int) = ∑//函数指针
int (*pfA[5])(int, int) = { &sum };//函数指针数组
}
我们通过对比,并且从函数指针的基础上进行改造,就可以得到函数指针数组,比如上面的pfA就是一个函数指针数组,数组里面放的是函数的地址,那它有什么作用呢?我们接着往下看
我们先来实现一个计算器,可以实现加减乘除
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 a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
int Mul(int a, int b)
{
return a * b;
}
int Div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
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;
}
这段代码非常简单,这里就不讲解,当我们想要给计算机添加一些功能时,比如a&b,a^b,a|b等等,我们除了要写函数,还要不断在switch语句里增加内容,switch语句就会越来越长,那有没有办法可以优化一下呢?这里就可以使用我们的函数指针数组,我们将代码优化如下
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 a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
int Mul(int a, int b)
{
return a * b;
}
int Div(int a, int b)
{
return a / b;
}
int (*pf[5])(int, int) = { 0,Add,Sub,Mul,Div };//函数指针数组
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if (input == 0) {
printf("退出程序\n");
break;
}
else if (input>=1&&input<=4) {
printf("输入操作数:\n");
scanf("%d %d", &x, &y);
ret = pf[input](x, y);//通过函数指针数组里的函数指针调用函数
printf("%d\n", ret);
}
else {
printf("选择错误\n");
}
} while (input);
return 0;
}
我们用函数指针数组将计算器的功能函数地址存起来,使用时就可以通过下标进行调用(函数指针数组第一个元素为0可以方便下标对应),此时我们就不需要再写那么长的switch语句,并且给计算机添加功能后,我们只需把函数的地址存入数组,再对代码进行小幅度修改即可,这种写法的前提条件是该计算器的所有运算类型都是要符合函数指针类型的计算,比如我们的计算器只能写a?b的功能,我们发现函数指针数组调用时,到了函数指针的地址,然后去调用对应函数,所以函数指针数组也有另一种叫法,叫做转移表
正如数组指针一样,函数指针数组也有对应的指针(指针和数组可以无限套娃)
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
void (*pfun)(const char*) = test;//函数指针pfun
void (*pfunArr[5])(const char* str);//函数指针的数组pfunArr
pfunArr[0] = test;
void (*(*ppfunArr)[5])(const char*) = &pfunArr;//指向函数指针数组pfunArr的指针ppfunArr
return 0;
}
如同之前的方法一样,我们对于这种复杂语句,从名字下手,它的名字和*被括号括起来,说明这是一个指针,紧接着是[ 5 ],说明这是一个数组,可以存放五个元素,将这些拿掉之后,就是数组的类型了,是函数指针,所以这个指针是指向函数指针数组的指针,我们了解即可,不深入研究
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数
的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进
行响应。
我们继续用计算器来举例,在我们最初的计算器里(switch),case语句后有很多重复语句,比如printf的提示,输入操作数的scanf,我们的case有四次,它就出现了四次,这么多重复的代码,使得程序过于冗杂,那我们可以把它优化掉吗?这时我们就可以使用回调函数
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 a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
int Mul(int a, int b)
{
return a * b;
}
int Div(int a, int b)
{
return a / b;
}
void calc(int (*pf)(int,int)) {
int x, y;
int ret = 0;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
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;
}
我们把重复的内容封装成一个函数,再结合我们函数指针的知识,让选择对应数字时实现对应功能,这相当于让calc函数变得通用,我们并没有在我们封装的函数里写Add,Sub等等函数,而是通过函数指针,在特定情况调用对应功能,当我们通过加法的指针调用假加法函数时,此时被调用的加法函数就是回调函数
接着我们来写一个冒泡排序
void bubble(int arr[], int sz) {
int i, j;
for (i = 0; i < sz - 1; i++) {
for (j = 0; j < sz - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main() {
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble(arr, sz);
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
}
我们可以发现,我们的冒泡排序只能对整数进行排序,如果我们想要对浮点数进行排序呢?对字符进行排序呢?总不能每次排序都写一个函数吧,而且这些函数都只有数据类型不同,重复度非常高,那有没有办法可以解决这个问题呢?别急,我们先看看c语言的一个库函数,它叫qsort
这是c语言库提供的一个排序函数,它的底层是使用快速排序,我们来看看这个函数怎么用吧
我们可以看到,这个函数有四个参数, 我们发现最后一个参数是一个函数指针,现在是不是感觉我们学习过的东西都很实用了呢?
base是目标数组的起始位置,num是数组元素个数,width是宽度,一个元素几个字节,最后一个参数是比较函数,我们怎么比较,影响排序的结果,这里其实就使用到了回调函数,前三种参数我们都知道,让我们来实现比较函数吧
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
首先我们先看函数参数,既然是回调函数,那么我们实现的函数参数就应该和系统规定的是一样的,参数类型为const void*,返回类型为int,那void*是什么呢?
我们写了可以用int*类型的指针接收int数据的地址,但是不可以接收float类型的地址
但是我们使用void*类型指针,不仅可以接收float类型,还可以接收int类型,我们可以把void*理解为一个垃圾桶,什么都可以仍进去,但是坏处是放进去后无法直接使用
void*指针是不能直接解引用,它不知道自己是什么类型,同样的,它也不能自增或者自减
void*的好处在于,当我们不知道传什么地址的时候,可以使用void*,任何类型都可以接收,坏处是无法直接使用,那该怎么解决呢?所以我们使用了类型转换,我们知道我们要实现比较函数,我们要对整形数组进行排序,所以我们给cmp_int传进去的肯定是整形数据,所以我们把e1和e2强制类型转换为int,然后解引用
这个函数的返回值为,当第一个元素小于第二个元素,返回<0的数,相等返回0,第一个元素大于第二个元素返回>0的数,所以我们的cmp直接相减然后返回,此时的cmp就是比较函数,然后我们把比较函数传给qsort,qsort的内部就会根据我们的比较函数进行排序
既然我们说了void*是什么都可以接收的,那么qsort函数也是什么都可以比较的,接着我们来对结构体进行排序
typedef struct stu {
char name[20];
int age;
}stu;
int main() {
stu s[3] = { { "张三",33 },{ "lisi",18 },{ "wangwu",60 } };
}
我们对这三个结构体有sqort函数进行排序,要比较这三个学生类型,我们比较也要有个规则,我们按照年龄和名字依次比较
int cmp_age(const void* e1, const void* e2) {
return ((stu*)e1)->age - ((stu*)e2)->age;
}
int main() {
stu s[3] = { { "张三",33 },{ "lisi",18 },{ "wangwu",60 } };
qsort(s, 3, sizeof(s[0]), cmp_age);
}
我们先把e1和e2强转为stu*类型,接着用->指向他们的年龄,相减然后返回,我们来看看是不是真的可以对结构体进行排序
通过监视窗口,我们发现果然将结构体数组按照年龄进行排序,那接着我们来实现按照名字排序(字典序)
int cmp_name(const void* e1, const void* e2) {
return strcmp(((stu*)e1)->name, ((stu*)e2)->name);
}
int main() {
stu s[3] = { { "张三",33 },{ "lisi",18 },{ "wangwu",60 } };
qsort(s, 3, sizeof(s[0]), cmp_name);
return 0;
}
名字比较是字符串比较,要使用strcmp函数比较,我们来看看结果
是正确的,接下来,既然我们可以做到这么多了,那不妨来点更厉害的,我们来模拟实现qsort函数,让我们的冒泡排序,也可以对任意数据类型进行排序
首先我们要设计函数参数,既然我们要对任意类型的数据进行排序,那我们的数组参数类型就不能写成固定的,而是要写成void*类型,第二个参数我们需要知道元素个数,第三个参数是宽度,为什么需要宽度呢?如果没有宽度,我们只知道我们的数组其实位置,但不知道数组里的一个元素占多少个字节,不知道数组的元素类型,所以我们需要一个宽度,最后一个参数,我们的排序函数,有可能对整形数组排序,也可能对结构体进行排序,所以我们的排序大体思路不变,但是比较方法和元素位置交换方法要进行改变
void bubble(void* base,size_t sz, size_t width,int (*cmp)(const void* e1, const void* e2))
我们的函数参数这样设计,size_t是无符号整形,我们的宽度和元素个数无论如何都不可能为负数(写int也没错,我们只是追求和qsort一样),所以我们这样设计,比较函数也是因为数据类型不知道,所以设计为void*类型,接着就是函数体了
我们对于冒泡函数里面的if语句,当我们进行排序时,比如对arr[3]={9,8,7},我们要知道9的地址和8的地址,但我们不知道数组是什么类型,没法使用下标,所以我们要通过偏移量来进行计算,但我们参数为void*类型,无法直接使用,所以我们要强制类型转换,但是我们要转换为什么类型呢?转换为整形吗?那样就写死了,我们上边知道了宽度,宽度的单位是字节,我们又知道了数组的起始位置,所以我们可以使用char*指针,乘以宽度,就是下一个元素的位置了,所以这里我们转换为char*的指针
if (cmp((char*)base+j*width,(char*)base+(j+1)*width) > 0)
我们再次对比冒泡函数,我们对比的元素是位置为j和j+1的元素,所以最后我们再给宽度乘以j和j+1,进去if后,就是交换了,接着我们来完成交换,交换元素,我们也不知道我们交换元素的类型,比如两个结构体,都是10个字节,我们不知道要开辟多大的空间,但是我们可以一个字节一个字节的进行交换,我们可以用一个字节的空间,先交换第一对字节,再交换第二对,以此类推
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width)
我们的交换用Swap函数来实现,我们传入两个元素的起始位置,然后传入宽度
void Swap(char* buf1, char* buf2, int width) {
int i;
for (i = 0; i < width; i++) {
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
既然我们都强转为了char*类型,那我们的Swap函数参数就直接用char*接收,然后使用循环进行替换即可
void bubble(void* base,size_t sz, size_t width,int (*cmp)(const void* e1, const void* e2)) {
int i, j;
for (i = 0; i < sz - 1; i++) {
for (j = 0; j < sz - i - 1; j++) {
if (cmp((char*)base+j*width,(char*)base+(j+1)*width) > 0) {
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
这就是我们用冒泡排序模拟的qsort函数,可以对任意数据进行排序
比如我们对整形数组排序
对结构体进行排序一样可以
以上即为全部内容,希望大家可以有所收获
如有错误,还请指正