当前位置: 首页 > 科技观察

这些C指针的使用技巧,掌握后可以立马提升到Level

时间:2023-03-19 13:45:47 科技观察

一、前言半个月前写的关于指针底层原理的文章,得到了很多小伙伴的认可(链接:C语言指针-从底层开始,从原理到花哨的技巧,图文代码一一讲解透彻),尤其是刚学过C语言的人,很容易从根本上理解指针是什么,怎么用,这也让我坚信一句话;用心写出的文章,读者一定能感受到!在写这篇文章的时候,我做了一个提纲。题目没写。继续写下去,文章的体量太大了,留个尾巴。今天就来补一下这个尾巴:主要是介绍指针在应用程序编程中经常用到的技巧。如果说上一篇勉强算是“道”的层次,那么这篇就属于“术”的层次了。主要通过8个示例程序来展示C语言应用中指针使用的常用套路,希望能给大家带来一些收获。记得在校园里学C语言的时候,南师大的黄凤亮老师大部分时间都在给我们讲解指针。现在我记得老师说得最清楚的一句话:指针就是地址,地址就是指针!二、八个例子1、开胃菜:修改调用函数中的数据//Swap2intdatavoiddemo1_swap_data(int*a,int*b){inttmp=*a;*a=*b;*b=tmp;}voiddemo1(){inti=1;intj=2;printf("前:i=%d,j=%d\n",i,j);demo1_swap_data(&i,&j);printf("后:i=%d,j=%d\n",i,j);}这段代码不用解释,大家一看就懂。如果解释太多,似乎是在侮辱智商。2、在被调用函数中,分配系统资源代码的目的是:在被调用函数中,从堆区中分配size字节的空间,返回给调用函数中的pData指针。voiddemo2_malloc_heap_error(char*buf,intsize){buf=(char*)malloc(size);printf("buf=0x%x\n",buf);}voiddemo2_malloc_heap_ok(char**buf,intsize){*buf=(char*)malloc(size);printf("*buf=0x%x\n",*buf);}voiddemo2(){intsize=1024;char*pData=NULL;//错误用法demo2_malloc_heap_error(pData,size);printf("&pData=0x%x,pData=0x%x\n",&pData,pData);//正确使用demo2_malloc_heap_ok(&pData,size);printf("&pData=0x%x,pData=0x%x\n",&pData,pData);free(pData);}2.1错误用法刚进入调用函数demo2_malloc_heap_error时,形参buff是一个char*类型的指针,其值等于pData变量的值,也就是说,buff和pData的值相同(都是NULL),内存模型如图:被调用函数中执行malloc语句后,从堆中获取的地址空间area被分配给buf,也就是说指向这个新的地址空间,pData还是NULL,内存模型为如下:从图中可以看出,pData的内存一直为NULL,没有指向任何堆空间。另外,由于形参buf是放在函数的栈区的,当从被调用函数返回时,堆区申请的空间就被泄露了。2.2正确使用在进入被调用函数demo2_malloc_heap_error时,形参buf是一个char*类型的二级指针,也就是说buf中的值是另一个指针变量的地址。本例中,buf中的值为pData指针变量的地址,内存模型如下:在被调用函数中执行malloc语句后,将从堆区获得的地址空间赋值给*buf,因为buf=&pData,所以*buf相当于pData,然后从堆区申请得到的地址空间赋值给pData变量,内存模型如下:从被调用函数返回后,pData正确的获得了一块堆空间,并且不要忘记在使用后主动释放它。3、传递函数指针我们从上一篇文章中知道,函数名本身就代表一个地址,函数体中定义的一系列指令代码都存放在这个地址中,只需在这个地址后面加上一个调用符(括号),输入这个函数来执行。在实际程序中,函数名常常作为函数参数传递。熟悉C++的朋友都知道,在标准库中对容器类数据进行各种算法运算时,可以传入用户自己提供的算法函数(如果不传入函数,标准库会使用默认的)。以下是对int行数组进行排序的示例代码。排序函数demo3_handle_data的最后一个参数是一个函数指针,所以需要传入具体的排序算法函数。示例中可以使用2个候选函数:降序:demo3_algorithm_decend;升序:demo3_algorithm_ascend;typedefintBOOL;#defineFALSE0#defineTRUE1BOOLdemo3_algorithm_decend(inta,intb){returna>b;}BOOLdemo3_algorithm_ascend(inta,intbslot,cmd->state);}elseif(CMD_TYPE_CONTROL_LAMP==pcmd->cmdType){//typecastCmdControlLamp*cmd=pcmd;printf("controllamp.color=0x%x,brightness=%d\n",cmd->color,cmd->brightness);}}voiddemo4(){//命令1:控制一个开关CmdControlSwitchcmd1={CMD_TYPE_CONTROL_SWITCH,1,3,0};demo4_control_device(&cmd1);//命令2:控制一个灯泡CmdControlLampcmd2={CMD_TYPE_CONTROL_LAMP,2,0x112233,90};demo4_control_device(&cmd2);}5.函数指针数组这个例子在上一篇文章中有演示,为了完整起见,这里再贴一遍intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdivide(inta,intb){returna/b;}voiddemo5(){inta=4,b=2;int(*p[4])(int,int);p[0]=add;p[1]=sub;p[2]=mul;p[3]=除法;printf("%d+%d=%d\n",a,b,p[0](a,b));printf("%d-%d=%d\n",a,b,p[1](a,b));printf("%d*%d=%d\n",a,b,p[2](a,b));printf("%d/%d=%d\n",a,b,p[3](a,b));}6.在结构体中使用灵活数组不解释概念,先看一个代码示例://一个结构体,成员变量data是一个指针memorysizeofthestructureintsize=sizeof(ArraryMemberStruct_NotGood);printf("size=%d\n",size);//分配一个结构体指针ArraryMemberStruct_NotGood*ams=(ArraryMemberStruct_NotGood*)malloc(size);ams->num=1;//为结构体ams->data=(char*)malloc(1024)中的数据指针分配空间;strcpy(ams->data,"hello");printf("ams->data=%s\n",ams->data);//打印结构指针和成员变量地址printf("ams=0x%x\n",ams);printf("ams->num=0x%x\n",&ams->num);printf("ams->data=0x%x\n",ams->data);//释放空间free(ams->data);free(ams);}在我的电脑上以上,打印结果如下:可以看到结构体一共有8个字节(int型占4个字节,指针型占4个字节)。结构体中的数据成员是一个指针变量,需要单独申请一块空间才能使用。而且结构体使用后需要先释放数据,再释放结构体指针ams,顺序不能错。这样用是不是有点麻烦?所以,C99标准定义了一个语法:灵活数组成员(flexiblearray),直接上代码(如果下面的代码在编译时遇到警告,请检查编译器对这个语法的理解是否支持)://A结构,成员variableisanarrayofunspecifiedsizetypedefstruct_ArraryMemberStruct_Good_{intnum;chardata[];}ArraryMemberStruct_Good;voiddemo6_good(){//打印结构体的大小intsize=sizeof(ArraryMemberStruct_Good);printf("size=%d\n",size);//为结构体指针分配空间ArraryMemberStruct_Good*ams=(ArraryMemberStruct_Good*)malloc(size+1024);strcpy(ams->data,"hello");printf("ams->data=%s\n",ams->data);//打印结构指针和成员变量的地址printf("ams=0x%x\n",ams);printf("ams->num=0x%x\n",&ams->num);printf("ams->data=0x%x\n",ams->data);//释放空间free(ams);}打印结果如下:以第一个为例,有有以下区别:结构的大小变成了4;为结构指针分配空间时,除了结构本身的大小外,还会申请数据所需的空间;无需单独分配数据空间;释放空间时,直接释放结构体指针即可;是不是更容易使用了?!这就是灵活数组的好处。从语法上讲,灵活数组是指最后一个结构中元素个数未知的数组,也可以理解为长度为0,所以这种结构可以称为变长。前面说过,数组名代表一个地址,是一个不变的地址常量。在结构体中,数组名只是一个符号,只代表一个偏移量,不占用具体的空间。此外,灵活阵列可以是任何类型。下面举个例子给大家体验一下。这种用法在很多通信处理场景中经常可以看到。7.通过指针获取成员变量在结构体中的偏移量这个标题读起来有点费口舌,我们来分解一下:在结构体变量中,可以使用指针操作技术来获取成员变量的地址和距离结构变量的起始地址和它们之间的偏移量。在Linux内核代码中,可以看到很多地方都使用了这种技术。代码如下:#defineoffsetof(TYPE,MEMBER)((size_t)&(((TYPE*??)0)->MEMBER))typedefstruct_OffsetStruct_{inta;intb;intc;}OffsetStruct;voiddemo7(){OffsetStructos;//打印结构变量和成员变量的地址printf("&os=0x%x\n",&os);printf("&os->a=0x%x\n",&os.a);printf("&os->b=0x%x\n",&os.b);printf("&os->c=0x%x\n",&os.c);printf("=====\n");//打印成员变量地址与结构变量起始地址的偏移量printf("offset:a=%d\n",(char*)&os.a-(char*)&os);printf("offset:b=%d\n",(char*)&os.b-(char*)&os);printf("偏移量:c=%d\n",(char*)&os.c-(char*)&os);printf("=====\n");//通过指针强制类型转换获取偏移量printf("offset:a=%d\n",(size_t)&((OffsetStruct*)0)->a);printf("偏移量:b=%d\n",(size_t)&((OffsetStruct*)0)->b);printf("偏移量:c=%d\n",(size_t)&((OffsetStruct*)0)->c);printf("=====\n");//使用宏定义获取成员变量partialshiftprintf("offset:a=%d\n",offsetof(OffsetStruct,a));printf("offset:b=%d\n",offsetof(OffsetStruct,b));printf("offset:c=%d\n",offsetof(OffsetStruct,c));}先看打印结果:前4行的打印信息不用解释,看下面的内存模型就可以理解后面的语句和不用多解释,就是将两个地址的值相减得到结构变量起始地址的偏移量。注意:需要先将地址转成char*类型才能进行减法运算。printf("偏移量:a=%d\n",(char*)&os.a-(char*)&os);下面语句需要理解:printf("offset:a=%d\n",(size_t)&((OffsetStruct*)0)->a);数字0被认为是一个地址,也就是一个指针。正如上一篇文章所解释的,指针代表内存中的一块空间。至于您如何看待这个空间中的数据,由您决定。你只需要告诉编译器,编译器就会按照你的意思去操作数据。.现在我们把0地址中的数据看成一个OffsetStruct结构变量(通过强制转换告诉编译器),这样就得到了一个OffsetStruct结构指针(下图中的绿色横线),进而得到指针变量成员变量a(蓝色横线),然后通过取地址字符&得到a(橙色横线)的地址,最后将这个地址转换为size_t类型(红色横线)。因为结构体指针变量是从地址0开始的,所以成员变量a的地址就是a相对于结构体变量起始地址的偏移量。上面的描述过程如果觉得啰嗦,请结合下图再看一遍:如果上图能看懂,那么最后一条通过宏定义获取偏移量的print语句也就看懂了,无非就是将代码抽象成一个宏定义,方便调用:#defineoffsetof(TYPE,MEMBER)((size_t)&(((TYPE*??)0)->MEMBER))printf("offset:a=%d\n",offsetof(OffsetStruct,a));可能有朋友会问:得到这个offset有什么用?那请看下面的例子8。8.通过结构体中成员变量的指针获取结构体指针的题目也有点啰嗦,直接看代码:typedefstruct_OffsetStruct_{inta;intb;intc;}偏移结构;假设有一个OffsetStruct结构体变量os,我们只知道成员变量c在os中的地址(指针),那么我们要获取变量os的地址(指针),怎么办呢?这就是标题所描述的目的。下面代码中的宏定义container_of也来自于Linux内核(有空多挖掘,可以发现很多好东西)。#definecontainer_of(ptr,type,member)({\consttypeof(((type*)0)->member)*__mptr=(ptr);\(type*)((char*)__mptr-offsetof(type,member));})voiddemo8(){//下面3行只是为了演示typeof关键字的使用intn=1;typeof(n)m=2;//定义同类型变量mprintf("n=%d,m=%d\n",n,m);//定义结构体变量,并初始化OffsetStructos={1,2,3};//打印结构体变量的地址,成员变量的值(供以后验证)printf("&os=0x%x\n",&os);printf("os.a=%d,os.b=%d,os.c=%d\n",os.a,os.b,os.c);printf("=====\n");//假设只知道一个成员变量的地址int*pc=&os.c;OffsetStruct*p=NULL;//根据成员变量的地址,得到结构变量的地址p=container_of(pc,OffsetStruct,c);//打印指针的地址和成员变量的值printf("p=0x%x\n",p);printf("p->a=%d,p->b=%d,p->c=%d\n",p->a,p->b,p->c);}先看打印结果:首先,你mus宏定义不清楚参数类型:ptr:指向成员变量的指针;类型:结构类型;member:成员变量名;这里重点理解宏定义container_of,结合下图,将宏定义拆解说明:宏定义分析中第一条语句:绿色横线:把数字0当成指针,强制执行转换为结构类型类型;蓝色横线:获取结构体指针中的成员变量member;橙色横线:使用typeof关键字,获取成员的类型,然后定义一个该类型的指针变量__mptr;红色横线:将宏参数ptr赋值给__m指针变量;宏定义中第二条语句分析:绿色横线:使用demo7中的offset宏定义,获取成员变量member相对于结构变量起始地址的偏移量,这个成员变量指针刚才已经知道了,它是__mptr;蓝色横线:__mptr的地址从结构变量起始地址的自身偏移量减去,得到结构变量起始地址;橙色横线:最后放this指针(此时为char*类型),强制为结构类型类型的指针;3、掌握了以上8种指针的用法,基本就熟练了,而且工作量有问题。希望大家能够善用指针这个神器,提高程序执行的效率。