(一)函数指针
在前边的blog中,已经整理归纳了数组和初级指针,接下来,我来继续整理高级指针和如何正确使用指针。
我们说过,指针数组是一个数组,每个元素是指针;数组指针是个指针,指向的是数组。所以:
函数指针就是指向函数的指针。我们先看以下代码:
<pre name="code" class="cpp">void fun() { } int main() { printf("%p",fun); printf("%p",&fun); printf("%p",fun+1); printf("%p",&fun+1); fun(); (*fun)(); }
我们来分析以下这个程序运行出来的结果是什么。这个程序如果你拿到自己的编译环境下测试,是编译不通过的。函
数名被编译之后其实就是一个地址,所以最后两句代码作用一样。对函数名加1的操作编译不通过。
注意区分一下两句代码:
void *p(int,char);//这是一个函数,返回值类型void*
void(*q)(int,char);//定义了函数指针变量q,q指向的函数有两个参数,int和char,返回值void
我们之前说过,给定数组指针int (*parr)[5];该数组指针的类型是int (*)[5].同样类比,函数指针变量的类型是什么(针
对上边例子的函数指针)?它的类型是void (*)(int,char);所以我们可以将上式理解为:void(*)(int,char) q;这样子是不是
是不是好理解一点呢?只是编译器不同意你这么写~~~知道这个很重要。
下边我们来看一下《c陷阱与缺陷》里的这个例子:
(*(void(*)())0)()这个表示的是什么??
void(*)(),这是函数指针类型;
(void(*)())0,将0强制转化成函数指针类型
(*(void(*)())0),取0开始的一段内存里边的内容,其内容就是保存在首地址为0的一段区域内的函数;
(*(void(*)())0)(),这就是对这个函数的调用。
再看下边一个例子:
(*(char **(*)(char**,char**))0)(char**,char **)表示什么呢??
char **(*)(char**,char**)这是一个函数指针类型;
*(char **(*)(char**,char**))0,将0强制转化成函数指针类型,并解引用找到该内存的内容,即就是一个函数。
这个函数的参数有两个,char**,char**。
继续下边一个例子:void (*signal(int,void(*)(int)))(int);
分析:signal是一个函数,有两个参数,int型和void(*)(int),返回值是一个函数指针,该函数指针指向的函数参数为int
型,返回值为void。
由于void(*)(int)的多次出现,我们可以使用typedef关键字简化,如下:
typedef void(*)(int) p;//我们可以这样理解,但是不可以这样对编译器~~
typedef void(*pfun)(int);
所以根据上边的分析,原式子就可以简化写为:pfun signal(int,pfun);
函数指针数组:这是一个数组,数组里的每一个元素都是指向函数的指针。
void (*parr[3])(int);这就是函数指针数组,如果看不出来我们这样来:void (*)(int) parr[3];这样就可以理解了,只是编
译器不同意我们这样~~
函数指针数组指针:原型举例:void (*(*parr)[3])(int) ;可以理解为:void
(*)(int) (*parr)[3];
写了这么多,貌似我们还没有了解函数指针可以用来干什么。其实,函数指针可以用于很多地方。当我们需要对一组
整形数排序时,我们可以用最基本的冒泡排序(虽然效率不是很高)来实现,可是,如果我要求你排序结构体时,你
是不是又要写一个函数去被调用,我们发现,这两个函数基本是一样的,所以,我们可以直接用回调函数来实现~~
下边我们就用回调函数实现排序各种类型的数~~
<pre name="code" class="cpp">int compare(const void *elem1, const void *elem2) { return *(const int *)elem1 - *(const int *)elem2; } void sort(void *base, unsigned int num, unsigned int byte, int(*cmp)(const void *elem1, const void *elem2)) { char *pbase = (char *)base; int flag = 0; int i = 0; int j = 0; int k = 0; const void *p1 = NULL; const void *p2 = NULL; for (i = 0;i < num - 1;i++) { flag = 0; for (j = 0;j < num - 1 - i;j++) { p1 = (const void *)(pbase + j*byte); p2 = (const void *)(pbase + (j + 1)*byte); int ret = cmp(p1, p2); if (ret > 0) { for (k = 0;k < byte;k++) {//交换秘诀 pbase[j*byte + k] = pbase[j*byte + k] + pbase[(j + 1)*byte + k]; pbase[(j + 1)*byte + k] = pbase[j*byte + k] - pbase[(j + 1)*byte + k]; pbase[j*byte + k] = pbase[j*byte + k] - pbase[(j + 1)*byte + k]; } flag = 1; } if (flag == 0) break; } } } int main() { int(*cmp)(const void *elem1, const void *elem2) = compare; int arr[4] = { 6,7,4,2 }; int i = 0; sort(arr,4,4,cmp); for (i = 0;i < 4;i++) { printf("%d ",arr[i]); } system("pause"); return 0; }
交换两个数有多种方法,创建临时变量,想必大家都知道~而不创建临时变量的交换,我们来归纳一下:
比如,交换整形数a和b:
方法一:创建临时变量;
方法二:利用加减的方法;
代码:
a = a+b; b = a-b; a = a-b;
方法三:利用乘除的方法;
a = a *b; b = a/b; a= a/b;
方法四:异或的方法:
int a= 3;//011 int b = 5;//101 a = a^b;//110 b=a^b;//011 a = a^b;//101
在《剑指offer》这本书中又看到这样一道题:将一个数组中的所有奇数调到所有偶数之前。我们用指针或者数组的方
法,都可以实现~下边给出指针的方法~~
void swap_odd_even(int arr[], int n) { if (n <= 1) return; int *pstart = arr; int *pend = arr + n - 1; int tmp = 0; while (pstart < pend) { while (pstart < pend && *pstart % 2 == 1)//如果是奇数,继续向后遍历 { pstart++; } while (pstart < pend && *pend % 2 == 0)//如果是偶数,就向前遍历 { pend--; } tmp = *pstart; *pstart = *pend; *pend = tmp; } } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,0}; int i = 0; swap_odd_even(arr,10); for (i = 0;i < 10;i++) { printf("%d ",arr[i]); } system("pause"); return 0; }
看了上述代码之后,会不会有人觉得奇偶交换之后指针没有变动呢~~告诉你吧,交换之后质变指针指向奇数,右边指
针指向偶数,两个while循环依然可以执行向后遍历~~
但是,但是,如果要求我们将能被3整除的放在数组左边,不能被3整除的放在数组右边,我们是不是又要写出一个函
数呢??不用的,我们可以使用回调函数来实现~
int ope(const void *elem) { return *(const int *)elem % 3; } void swap_odd_even(int arr[], int n, void(*p)(const void *)) { if (n <= 1) return; int *pstart = arr; int *pend = arr + n - 1; int tmp = 0; while (pstart < pend) { while (pstart < pend && ope((const void *)pstart))//如果是不能整除的数,继续向后遍历 { pstart++; } while (pstart < pend && ope((const void *)pend) == 0)//如果是可以整除,就向前遍历 { pend--; } tmp = *pstart; *pstart = *pend; *pend = tmp; } } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,0}; int i = 0; void(*p)(const void *) = ope; swap_odd_even(arr,10,p); for (i = 0;i < 10;i++) { printf("%d ",arr[i]); } system("pause"); return 0; }
我们只需要加上一个小小的操作函数判断一下就可以了~~
(二)指针使用过程中的易错点和难点:
1.空指针:前边模拟实现memmove,memcpy等函数时,我们就使用了空指针,希望她可以接收任意类型的数,我们
也说过,空指针不能自加,也不能解引用,但是只是在windows下,那么我们来看看在gcc环境下的情况:
代码如图:
运行通过~~~
2.野指针:野指针就是对内容不明确的指针进行解引用~
int *p; *p =10;
p就是一个野指针,我们并不知道p里边存放着哪个变量的地址,所以解引用时,你懂~~再看下边一个例子:
struct student { char *name; int score; }stu,*pstu; int main() { strcpy(sty.name,"yaoyao"); stu.score = 100; return 0; }
乍一看这段代码没有任何问题,其实,我们只知道指针变量name被分配了4字节(32位系统),并不知道里边的内容
,所以复制时无效。解决办法:给那么指针动态分配空间,这里就不给出代码了,可以自己实现~
注意:函数入口时,一定要对指针变量是否为NULL进行判断,我们可以通过assert断言实现。需要注意的是,这个宏
只能在debug版本使用,release版本并不可以。原因在于:assert的一切作用就是尽可能的在调试函数的时候吧错误
定位,而不是等到release之后。assert是定位错误,不是排除错误。当程序被完整地测试完毕后,可以在预编译中通
过定义NDEBUG来消除所有的断言。assert宏所在头文件是,assert.h。
3.内存泄漏:没有释放向系统申请的内存,举例:
<pre name="code" class="cpp"><pre name="code" class="cpp">void getmemory(char *p,int num) { p = (char *)malloc(num * sizeof(char)); } int main() { char *str = NULL; getmemory(str,10); strcpy(str,"hello"); free(str); return 0; }
这个程序运行后会崩溃。在main函数中将参数str传进getmemory函数,此时p = NULL,而这个p是在getmemory的栈
帧中,str在main函数栈帧中,在函数中动态分配内存赋给p,改变了p,并不能改变str。如何更正使得程序运行通过
呢?给出两种方法:
1.将str的地址传过去。
2.将分配好的内存首地址返回。下边给出一种更正方法的代码:
char *getmemory(int num) { char *p = (char *)malloc(num * sizeof(char)); return p; } int main() { //char *str = NULL; char *ret = getmemory(10); strcpy(ret,"hello"); free(ret); ret = NULL; return 0; }
注意:malloc和free要成对,就像上边那个代码,虽然成对,但是跟没成对一样。free可以释放空指针,释放完后要置
空。
不可返回局部变量的地址。上边代码返回的是局部变量,不是局部变量的地址。
看下边的例子:
int* fun() { int tmp = 10; return &tmp; } int main() { int *pret = fun(); printf("%d",*pret); return 0; }
这段代码或许可以运行通过,输出结果是10,那是因为fun函数的那块内存还在(没有被别人占用),要是被占用
了,程序就会运行出错,因为出了那个函数,那块空间就已经不属于那个函数,返回的地址肯定是不对的。
指针很重要,使用好指针更重要~~
文章难免存在不足,希望各位朋友指出~~