C语言指针从理解到深入(2)

1、函数返回指针
函数返回指针常用在管理堆内存上,比如malloc、calloc函数就是返回指针,而我们自己实现的函数如果在函数内分配了内存,希望跳出函数后还可以管理这片内存,那么在函数外必须要
获取这片内存的地址,那么就是通过返回指针来实现。下面通过代码片段来说明这样的应用
struct Note {
    struct Note *next;
    int data;
};/*数据结构中单链表常用的节点定义*/
struct Note *LIST_CreateNote(int data){
    struct Note *note = (struct Note*)malloc(sizeof(struct Note));
    note->next = NULL;
    note->data = data;
    return note;
}
该函数在内部malloc了一个节点的内存空间,然后返回节点的指针,在函数外只要接收函数返回值就可以管理这个节点,函数调用如下
struct Note *note1 = LIST_CreateNote(10);
printf("data=%d\n", note1->data); /*输出data=10, 说明note1是函数内部分配的内存*/
但是如果函数局部变量的的指针往往是危险的,比如把LIST_CreateNote的定义改为
struct Note *LIST_CreateNote(int data){
    struct Note note;
    note.next = NULL;
    note.data = data;
    return &note;
}
上面的定义同样返回了一个节点的指针,但是可惜的是note在由系统管理的栈空间,note是个局部变量,当退出这个函数的时候这片内存被系统回收,而函数返回了这个地址,外部程序通 过这个指针访问到一片被系统回收的内存并且可能系统已经又分配他用了,这是个危险的错误,说他危险是因为这样的错误可能不会被编译器发现。

2、函数值传递机制
首先复习一下值传递机制,所谓值传递机制就是拷贝实参的值给形参(实例),比如下面的函数
int CALC_Add(int a, int b){
    return a+b;
}
调用的时候;
int a = 10, b = 11;
int c = CALC_Add(a, b);
我把上面两句代码展开为基本等效的伪代码
int a = 10, b = 11;
int c;
{
    int _a = a;
    int _b = b;
    {/*函数开始执行的地方*/
        c = _a + _b;
    }
}
上面的伪代码虽然不能100%真实反应实际的函数调用过程,但是值传递这个过程却是很清晰的,参数其实是函数内部的局部变量,在函数执行{}前把实参的值赋给局部变量,当然拷贝的过 程不是程序员可控的,而是由编译器把C转换为汇编的时候添加这样的赋值语句。 值传递在两种情况下不合适。第一:当传递数组、结构体等大体积的数据给函数内部时,实参拷贝给局部变量的过程效率低。第二:函数内部不能通过形参修改实参,比如不能通过函数参 数a修改函数外的a,参数a和外部的a同名只是个巧合,从伪代码可以看出,它们是两个不同的变量。

3、指针传递
指针传递源于解决4最后提出的两种情况。 先定义一个结构体作为后面的例子使用
struct Time{
    int h;int min; int sec;
    int other[1024];
}
现在需要实现一个函数打印结构体的成员值,先想到的方法就是把成员分别作为参数传入函数内打印
void TIME_ShowTime(int h, char min, char sec){
    printf("%d:%d:%d\n", h, min, sec);
}
无疑上面函数是完全符合要求的,虽然感觉参数多了一点但是还能接受,但是当结构体的定义增加一个成员比如ms,我们也想把它打印出来, 改函数的时候非常糟糕,我们必须到函数声明 和调用的地方增加这个参数,于是想到了为何不直接把整个结构体传进去,重新实现的函数如下
void TIME_ShowTime(struct Time time){
    printf("%d:%d:%d\n", time.h, tim.min, tim.sec);
}
该函数清爽了很多,而且增加结构体要打印的成员时只要改实现就可以了,函数参数完全不变。通过上面讨论的值传递机制,这种方法要把1027(sizeof(note))个字节拷贝 进函数内部,之所以会产生这么大的拷贝是因为这种传递不可避免地把不需要用到的成员传了进来,所以其效率之低可想而知,当然在结构体不大的情况下这种方法完全可行。 想要继续优化上面的函数就该指针传递出场了,指针传递是把函数需要外面的值的指针传进去,然后函数里面再通过指针找到值的所在,完成所谓的传递(函数的调用没有把值传进去 ,除非自己通过指针拷贝值进来),下面还是看实现
void TIME_ShowTime(struct Time *time){
    printf("%d:%d:%d\n", time->h, tim->min, tim->sec);
}
要知道任何类型的指针大小都是4字节(32位平台),指针传递存在一个拷贝指针的行为,但显然它和传一个int型参数的消耗是一致的,和拷贝整个结构体变量的消耗比。拷贝一个指针的消 耗简直可以忽略不计。指针传递还有一个比值传递消耗资源的地方,就是数据的访问,上面例子中time->h, tim->min, tim->sec是间接访问,time.h, tim.min, tim.sec是直接访问,间接 访问显然要更消耗资源,假设间接是直接的两倍,我们依然可以忽略这样的消耗差异,因为要访问的数据实在太少了。
下面又要实现一个新函数,在函数内部设置结构体变量成员的值,要实现该功能除了指针传递外实际上别无他法,下面看实现
void TIME_SetTime(struct Time *dst, int h, int min, int sec){
    dst->h = h;dst->min = min; dst->sec = sec;
}
函数形参dst虽然是个局部变量,但是调用时它指向了函数外部的内存,修改dst成员实际上就是修改函数外的内存,比如下面的调用
struct time;
TIME_SetTime(&time, 1, 2, 3);
函数里面修改dst成员其实就是修改time成员。到这里可以明白指针传递使得函数内部可读可写外部数据。
再回过头来看TIME_ShowTime函数的定义发现并不安全,因为该函数只是一个显示函数,无需修改函数外部的数据,但是显然函数体内可以有修改外部数据的能力,所以该函数用const禁止 通过参数指针time写操作会更好,所以TIME_ShowTime函数的可修改如下
void TIME_ShowTime(const struct Time *time){
    printf("%d:%d:%d\n", time->h, tim->min, tim->sec);
}
特别函数参数是字符串指针时,用const修饰是一个好习惯。

4、往函数内部传递数组
下面对2的加法函数适当修改,让他可以计算一个数组的元素的和
int CALC_AddArray(int []array, unsigned int number){
    int sum = 0; while(number){sum += array[--number];} return sum;
}
调用的实例:
int array[3] = {1,2,3};int sum = CALC_AddArray(array, sizeof(array)/sizeof(int));
在第一篇已经提到过,函数名array其实等效为指针 int *const array。实际上编译器总是会把数组的参数转换为指针参数,也就是想传array整个数组进去是不可能的。所以
CALC_AddArray也可以这么等效地定义
int CALC_AddArray(int *array, unsigned int number){
    int sum = 0; while(number){sum += array[--number];} return sum;
}
在函数体内不会觉察到两种声明的区别,于是讨论传递数组问题就变成了上面讨论的指针传递机制问题。下面是一个完整的例子,注意两次sizeof的结果差异
#include <stdio.h>
void _main(int argc, char argv[20][20]){
    printf("_size=%d\n", sizeof(argv));/*输出:_size=4 也就是指针的大小*/
    while(argc--){
        printf("argv[%d]%s\n", argc, *argv++);    
    }
}
int main(){
    char argv[20][20] = {"床前明月光", "疑是地上霜" , "举头望明月", "低头思故乡"};
    printf("size=%d\n", sizeof(argv));/*输出:size=400 也就是整个二维数组的大小*/
    _main(4, argv);
}
下面对_main的4种声明做一个对比(函数的实现忽略):
void _main(int argc, char argv[20][20]);/*argv在函数内存转换为指针数组char (*)[20]*/
void _main(int argc, char *argv[20]);/*argv在函数内转换为二级指针char**类型*/
void _main(int argc, char *argv[]);/*argv在函数内转换为二级指针char**类型*/
void _main(int argc, char **argv); /*保持为char **类型*/
要知道第一种声明和第2到4种声明完全不能等同,而第2到4种编译器等同处理,但是第2种明确告诉调用者,我希望要一个char *[20]的指针数组为实参,所以对于调用者来说第2种和3到4 种是有区别的,第3和第4种不管从那个角度看都是等同的,用哪种完全取决于程序员喜好。其实不管是几维的数组,数组的第一个参数会丢失第二个及以上(如果有)的参数不会被丢失;二 维数组转换为指针数组,数组指针(指针是一级的)转换为二级指针...

5、void *
void翻译为无类型, void* 自然可叫无类型指针,前一篇文章有定义指针是存储类型描述+地址,其实void*类型就是少了类型描述这块,就简单地相当于地址,为了能让地址也归类到指针 下面,就把“无类型”作为存储类型描述,即void *指针=无类型+地址,也够绕口令的。 void *是一些编程技巧实现的前提。懂得这些技巧的前提是了解void *的特点。
第一:任何类型的指针都可以强转为void*指针,所以任何类型的指针可以赋值给void *指针变量,比如
int a = 10; 
void *p1 = &a;/*隐式强转*/
void *p2 = (void *)&a;/*显式强转*/
第二:void *指针可以转换为其他任何类型的指针,比如
int a = 10;
void *p1 = &a;
int *p2 = (int *)p1; /*显式强转*/ 
int *p3 = p1; /*隐式强转:注意一些编译器不支持这种隐式强转比如vc,但是gcc却是支持的,使用隐式强转指针为void*类型使得代码的可移植性变差*/ 
第三: 不可通过void *指针访问指针指向的内存,也不可用++、--来改变void *指针变量自身。比如
int array[2] = {1,2};
void *p1 = &array;
p1[0] = 10; /*编译无法通过*/
p1++; /*编译无法通过*/
上面的两个操作错误的原因很简单,void *指针没有描述他指向的内存是如何存储、存储空间多大的信息,自然不知道如何通过它往内存里面放和读数据,也不知道++和--要偏移多少个字 节。 了解了上面的void *指针的特点,下面看一下他的应用,同样以一个加法函数为例
void *CALC_Add_UseVoidx(void *out, const void * a, const void *b, int type){
    if(a==NULL || b==NULL || a<0) return NULL;
    switch(type){
    case 0:{ /*两个整数相加*/
        *(int *)out = *(int *)a + *(int *)b;
        break;
        }
    case 1: {/*两个字符串'相加'*/
        unsigned int alen = strlen((char *)a);
        unsigned int blen = strlen((char *)b);
        memcpy(out, a, alen);
        memcpy(((char *)out)+alen, b, blen);
        ((char *)out)[alen+blen] = '\0';
        break;
        } 
    default:out = NULL;
    }
    return out;
}
上面的逻辑不重要,重要的是这个函数显示了使用void*指针做函数参数使得同一个函数可处理的数据范围变宽了(比如上面的函数如果声明为int类型就只能做数学加法,声明为void *类 型后可做数学运算也可做字符串‘相加’),或者返回值void *指针,返回的数据可随意转换为其他类型。但是滥用就没有意义了,一般在内存分配、内存移动这方面用得比较多,比如系 统函数malloc、calloc、memcpy、memmov等常用void *。就拿上面的内存拷贝memcpy函数来说把,原型是
void *memcpy(void *dst, void *src, unsigned int n);
利用void*的特点。他可拷贝字符串,数组,结构体等等,所有类型数据间的拷贝通吃(dst和src的地址不能重叠)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dz2015

哈哈 菜鸟来混一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值