文章目录
一.字符指针
int main()
{
char ch = 'a';
char* pc = &a;
return 0;
}
这是字符指针最普通的应用,但是字符指针只有这么大的能耐吗?
我们再来看:
int main()
{
const char* pstr = "hello word.";
printf("%s\n", pstr);
return 0;
}
在这里我们是把整个字符串传给指针pstr吗?当然不是,一个字符指针怎么可能把整个字符串放进去。在这里我们是把一个常量字符串的首字符地址传给了字符指针pstr.
因为"hello word."是常量字符串,常量字符串是不允许被修改的。
如果将const来修饰char* pstr的时候,说明此时不允许*pstr被改变。
如果是char* const pstr的话,说明此时pstr不可以被修改
也就是const只修饰它右边的那一部分
我们再看一看有关字符指针的题目:
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是数组,str3,str4是字符串。我刚才也说过了,字符串是不允许改变的,但是数组是可以的呀。既然字符串是不允许修改的,说明在内存中,计算机只保存一份字符串即可。所以说不管你定义多少个字符指针,只要它们指向的字符串是一样的,这些指针存放的地址都是一样的,所以str3 == str4.
数组就不一样了,因为数组里的内容是可以修改的,每个数组都有一块自己独立的内存空间,所以数组首元素的地址是不可能一样的。
二.指针传参
2.1一级指针传参
void print(int *p, int sz)
//此时p拿到的地址还是arr首元素的地址,直接用就行
{
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;
}
一级指针传递过去,函数接收时用一级指针接收即可,没什么好讲的。
如果一个函数参数是一级指针的话,函数可以接收什么类型的参数呢?
- 一级指针
- 一维数组数组名
- 一个元素的地址
2.2二级指针传参
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;
}
二级指针也没有太多花里胡哨的东西,用二级指针接收即可
如果一个函数参数是二级指针的话,函数可以接收什么类型的参数呢?
- 二级指针
- 一级指针的地址
- 指针数组的数组名(指针数组就是一个数组,里面的元素是指针,指针数组的数组名是首元素的地址,即一个指针的地址,也就是二级指针)。
三.函数指针
3.1函数指针的定义
我们定义的每种类型的变量都有它自己的地址,数组也有,结构体也有。那函数呢?函数是不是也有一个地址呢?
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们能不能把函数名当成数组名那样来使用呢?我们来看结果:
我们发现这也能打印出来结果,而且函数名和&函数名结果是一样的。那函数名和&函数名会不会和数组类似,两个地址虽然相同,但是意义不一样?答案是否定的。函数并没有这么麻烦,在这里函数名就等于&函数名,没有区别。
现在我们再来看看怎么把函数地址赋值给指针的:
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*p1)(int x, int y) = add;
int (*p2)(int, int) = add;
return 0;
}
首先p1先于*结合说明p是指针,把 * p1拿出来之后,剩下的是int (int x,int y)是个函数,这个函数的返回类型是Int,两个参数是类型也是int.
x,y也可以省略不写。
3.2函数指针的应用
int main()
{
int (*p)(int, int) = add;
//既然p是指针,那对p解引用就可以找到这个函数
printf("%d\n", (*p)(2, 3));
return 0;
}
在以前用函数调用的时候一般会写add(2,3);我们发现在定义指针p的时候,是把add的地址传给了指针p。是不是可以说p和add其实是等价的,既然可以写add(2,3);是不是也可以写成p(2,3)?
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = add;
printf("%d\n", p(2, 3));
return 0;
}
我们来看结果:
发现确实可以,所以说在调用的时候 * 加不加都无所谓
四.一些奇葩题目
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
能不能分析两个代码都有什么用?
代码1:
我们通过分析可以把括号都分一下:
我们先看黑框框里的部分,我们刚已经接触过了函数指针,可以一眼看出来它是一个函数指针类型,因为它没有指针名称,所以它只可能是个类型。
那一个括号,里面包含了一个类型。这代表什么呢?想想我们之前接触过的,其实这是一个强制类型转换。0是一个int型的变量,我们这里就是把0强制类型转换成一个函数指针类型那也就是把0当成一个函数的地址。
随后对这个0解引用,就是把0这个地址处的函数解引用,参数的话就什么都没传。
代码2:
同理我们先把这个代码简单分一下:
先看signal(int, void(*)(int)),这就是一个函数,函数名是signal,参数一个是int类型,另一个是void( * )(int)类型的。
既然这是个函数,那肯定要知道返回类型,和参数类型。参数类型我们已经知道了,那返回类型呢?现在我们把这个函数拿出来,看看剩下的是什么?
剩下的是void(*)(int);
现在整个框架就已经知道了,这个函数
函数名:signal
函数参数类型:int,void( * )(int)
函数返回值:void( * )(int)
但是我们这样写代码是不是比较冗余?
我们进行一个这样的定义:
typedef void(*pfun_t)(int);
这个重定义的意思就是把类型void(*)(int)重新命名为pfun_t.
千万不要这样写:
typedef void ( * )(int) pfun_t
虽然我们之前接触过的其他重命名写法和这个差不多,但是语法规定要typedef void(*pfun_t)(int);这样写。我们也不要问为什么。
现在我们就可以把刚才的代码换一下了。
因为参数和返回值类型都是void ( * )(int),我们把这个都换成pfun_t
pfun_t signal(int,pfun_t);
五.函数指针数组
既然我们学完了函数指针,我们现在就开始来套娃了
函数指针数组:本质上是数组,里面的元素是函数指针类型。
但是我们该如何表达出这个数组呢?
我们看看之前用到过的数组:
int arr1[10];
char arr2[10];
float arr3[10];
首先我们要把这个数组名写出来,还要把数组里元素的内容也要写出来。函数指针数组也同样这样做。
int main()
{
//我们假设这个数组里的函数指针类型是int (*)(int)
//然后我们定义一个数组,把这个数组放进去就可以了
int (*par[10])(int);
return 0;
}
如果不理解的话,我们倒着在分析一波
首先par和[]先结合,说明它是一个数组,然后我们把par[]取出来,剩下的就是这个数组里元素的类型。
学完函数指针数组后,它有什么用呢?
假如我们有很多函数,这些函数的参数类型,返回值类型都是一样的。这样我们就可以把这些函数名写到一个数组里。然后我们再后来用这些函数时,就直接通过数组下标就可以找到。
我来写个例子:(做一个简易计算器)
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()
{
printf("1.add 2.sub\n");
printf("1.mul 2.div\n");
//输入需要进行的计算
int input = 0;
printf("请输入->");
scanf("%d", &input);
//我们定义一个函数指针数组
//因为我们输入数字1才是add函数,数组元素又是从0开始的
//所以我们在数组的首元素放上0
int (*ret[5])(int, int) = { 0,add,sub,mul,div };
//输入需要操作的两个数
printf("请输入两个数->");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
int output = 0;
output = ret[input](x, y);
printf("%d\n", output);
return 0;
}
主要看这两行代码:
int (*ret[5])(int, int) = { 0,add,sub,mul,div };
output = ret[input](x, y);
第一行就是定义一个函数指针数组,里面的后四个元素就是各个函数的函数名。
第二行就可以通过输入的数字来寻找调用哪个函数。
六.指向函数指针数组的指针
我们继续来套娃。
既然有数组,这个数组就有地址,就可以用指针接收。我们就了解一下向函数指针数组的指针是怎么定义的就行。主要是为了巩固一下之前学的那些。我们就用计算器的那个数组来举例子。
int (*(*pp)[5])(int, int);
首先既然它是指针肯定是先和 * 结合。然后指向的是数组所以后面要跟[]。现在就变成了(*pp)[5]
现在我们还剩数组里的元素类型没有写,元素类型是int ( * )(int,int)
最后我们把(pp)[5]放到int ( * )(int,int)就行
最后结果就是int ((*pp)[5])(int, int);
七.qsort函数
7.1qsort函数的用法
排序你所给的元素数组,数组里的元素可以是任意的。
7.2qsort函数的结构
void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
参数:
base:你所需要排序的数组的地址
num:数组里元素的个数
width:每个元素的大小(字节)
int (__cdecl *compare )(const void *elem1, const void *elem2 ):这是一个需要你自己写的关于比较两个元素的函数
返回值:void
头文件;<stdlib.h>
7.3qsort函数的使用
int com_int(const void* e1, const void* e2)
{
return *((int*)e1) - *((int*)e2);
}
int main()
{
int arr[10] = { 1,3,5,7,9,2,4,6,8,10 };
qsort(arr, 10, 4, com_int);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
com_int函数是自己定义的,而且返回值必须是Int,参数也必须是const void*类型。qsort函数具体怎么用我们写的这个函数,我们现在不用操心。
com_int函数:
qsort函数要求我们写的这个函数
如果参数1<参数2返回一个小于0的值
如果参数1>参数2返回一个大于0的值
如果参数1=参数2返回一个0
因为我们函数的两个参数是void类型的,不能直接使用,所以要**强制转化类型为int类型**的。直接返回两个参数相减的结果即可
当然我们这里只是比较了两个Int类型值用到的函数,如果我们要比较字符串,结构体这样的类型则要重新写一个函数
最后我们再来看看刚才代码的结果:
qsort函数帮我们把数组里的元素从小到大排序好了。如果你希望是从大到小排序,你在我们写的com_int函数代码里把两个参数交换位置即可。
八.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
我们通过模拟qsort函数来理解回调函数。
int com_int(const void* e1, const void* e2)
{
return *((int*)e1) - *((int*)e2);
}
//void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
void my_qsort(void* base, size_t num, size_t size, int (*p)(const void* e1, const void* e2))
{
//我们通过冒泡排序来进行排序
int i = 0;
for (i = 0; i < num - 1; i++)
//总共num-1趟
{
int j = 0;
for (j = 0; j < (num - 1 - i); j++)
//每趟需要比较的次数
{
//比较第j和第j+1数,如果小于不变,如果大于则换位置
//可以通过函数com_int来比较
//第j个元素可以用(char*)base + j*size
//第j+1个元素可以用(char*)base + (j+1)*size
if (p((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
//比较第j个元素和第j+1个元素
{
//换位置,和交换两个杯子里的水一样
char tmp = 0;
tmp = *((char*)base + j * size);
*((char*)base + j * size) = *((char*)base + (j + 1) * size);
*((char*)base + (j + 1) * size) = tmp;
}
}
}
}
在这里,你们可能有几点问题不明白:
冒泡排序:
我们先进行一趟排序:第一个元素和第二个元素比较,如果大于不动,小于换位置
第一趟排序我们共进行了4组比较,也就是元素个数-1次
我们发现每一趟排序都能找到一个最大的数放到最后面,此时第二趟排序的时候,就不用和最后一个数比较了。所以每加一趟,要比较的组数都-1.
我们又发现最后一趟比较的时候只用比较第一个元素和第二个元素就行。通过推算,我们最多要进行元素个数-1趟
我们通过这些写两个for循环就行
为什么?
第j个元素可以用(char*)base + jsize
第j+1个元素可以用(char)base + (j+1)*size
在qsort函数的用法时我就讲过,qsort函数可以排序任意类型的数组,所以我们在传参的时候压根不知道传入数组的类型。
因为我们知道数组每个元素的大小,所以我们通过1元素大小就可以找到一个元素了。换句话解释,假如数组里元素的类型是int,我们把数组强制类型转换成char类型,因为数组+1只能往后找一个字节,但是我们一下子往后找四个字节,是不是就可以一下找一个整型了?
所以第j个元素可以用(char*)base + j*size
在这里qsort就是回调函数
但是回调函数比普通函数调用好在哪里呢?
我们知道qsort函数是什么类型都能排序,听万能的。假如我们不用回调函数,就是把qsort参数的指针那块去掉。我们要用到比较int类型的函数时,就在qsort函数里加上调用和这个有关的函数,如果比较结构体,就跑qsort函数内部,把相应函数填上。也就是说如过qsort不是回调函数,我们每比较一个类型,还要把qsort内部代码找出来,然后添加相应函数。这是不是很麻烦,如果qsort是回调函数的话,就直接在qsort函数参数部分把所用到的函数地址填过去就行。