首页 > 学院 > 开发设计 > 正文

函数返回值与不同存储位置字符串的“坑”

2019-11-14 10:05:37
字体:
来源:转载
供稿:网友

通常,我们采用两种方式从一个函数中(被调用者),将某个有用信息传递到另外一个函数中(调用者)。分别是传参方式与返回值方式①传参方式:传参数我们传递的是地址(或者值,以传地址为重点),地址作为传出参数,其思想是调用者将自己的栈空间给被调用者使用,然后在被调用者结束时调用者授权其使用的栈空间并不会被回收,而调用者就可以从自己的栈空间中取得有用的值;②返回值方式: 被调用者在结束是向调用者返回一个值或者是一个不会被回收的内存地址(或者值)。当然我们也可以使用定义局部变量的方式解决传参与返回值的麻烦的步骤,但是一有所得,必有所失,全局变量由于其共享性而不是那么的安全。

1、传参与返回值各自的演示:

我们以简单的求和([1,n]所有整数之和),实现主函数获取求和函数的处理结果的例子来做演示:

#include<stdio.h>void add_para(int * sum, int n){ itn i; for(i=1; i<=n; i++) *sum += i; }int add_ret(int n){ return (n*(n+)/2); //返回值}int main(void){ int n = 0, sum = 0; scanf("%d",&n); add_para(&sum, n);//传参方式 PRintf("sum = %d/n", sum); sum = 0;//clear sum = add_ret(n); printf("sum = %d/n",sum);//返回值方式 return 0;}

这里写图片描述 当然,我们也可以采用参数与返回值双重返回的方式来传递处理结果:

int * add_para_ret(int * sum, int n){//传参方式 itn i; for(i=1; i<=n; i++) *sum += i; return &sum; //返回地址方式}

那么今天我们就重点说说返回值(地址)与传参的那些问题。

2、返回值的重点:地址返回与值返回

在返回值时,我们经常会返回一个变量的地址,该地址在函数被调函数结束后不会被回收。因为在采用返回值(狭义),而不是一个值的有效地址的时候,返回其实并不起作用(或者说是返回失效)。当然返回地址也会失效,比如局部变量的栈空间在返回给主函数(调用者)时其返回是无效的。 举个简单的例子(以上例为母本):

/*地址返回的误区*/#include<stdio.h>int * add_ret(int n){ int sum = (n*(n+)/2); return &sum; //返回局部栈空间地址}void add_para(int * sum, int n){ itn i; for(i=1; i<=n; i++) *sum += i; }int main(void){ int n = 0,* sum = 0; scanf("%d",&n); sum = add_ret(n);//计算结果是5050 add_para(sum,n);//计算结果是5050的两倍,当然如果不是那也不意外,因为sum指向的地址被来就已经不属于它,被别人修改也很正常 printf("sum = %d/n",*sum);//输出不是正确结果 return 0;}

这里写图片描述

我们发现会有警告:函数返回局部变量地址。 并且打印的并不是我们想要的值,虽然这个结果是我们人为造成的(我们用main中的sum接收了一个不可控的局部变量地址,之前的计算结果5050还存在,导致最终结果是预期的两倍),只需要在add_para()函数中将sum重新初始化,结果就会正确;但是很多情况下即使在add_para()中将sum中重新初始化为0,对于返回的局部变量地址(这块地址在返回后并不属于调用方,已被回收只是还没重新利用,故原有内内容还在)我们不能人为控制。如果在main()输出sum中内容之前系统用已经用回收的地址干了其他事,那结果就那没有规律可循了。所以我们在返回地址时,绝对不能返回局部变量地址(栈空间)。

3、字符串地址返回与传参的“坑”:

这块才是今天的重头戏,昨天晚上我写了一段代码,出现了段错误提示,代码如下:

#include<stdio.h>#include<errno.h>#include<pthread.h>#include<string.h>#include<stdlib.h>void * task(void *p){ printf("%s/n",(char *)p); p = "hello"; strcpy(p,"hello"); //char st[] = "hello";//st指向栈区,返回的是无效的指针,不能使用 //return st; return p;}int main(void){ char str[] = "abcde"; pthread_t tid; pthread_create(&tid, NULL, task, str); char * retval = "world"; int ret = pthread_join(tid, (void **)&retval); if(ret){ printf("pthread_join error %s/n",strerror(ret)); exit(EXIT_FAILURE); } printf("str = %s/n",str);//str由strcpy(p,"hello");语句改变 printf("res = %s/n",retval);//retval由p="hello";语句改变 return 0;}

这里str采用传参的方式改变字符串内容,retval采用返回值的方式改变字符串内容,按理说str与retval输出应该都是”hello”,结果却总是那么出人意料让人惊喜。。 这里写图片描述

编译运行之后,我们发现只有第一个printf()输出了个”abcde”,而后面接着是段错误。那么首先我想到的就是返回了局部变量地址,但是并非如此:参数指针p指向的地址并非本函数中的栈空间,pthread_join(tid, (void **)&retval);语句将main()函数中的地址传给了被调函数(子线程),返回是不会出错的。而当我仔细观察了这两句语句之后:

p = "hello";strcpy(p,"hello");

我发现了端倪所在:p本来指向主线程传给其的主线程中的地址,单单就每个语句来看:①p = “hello”;这一语句改变了p的指向(改为指向字符常量区),返回p是没有问题的,②strcpy(p,”hello”);只负责复制不负责修改指向,就像传参一样。但是strcpy(p,”hello”);放在了p = “hello”;的后面就引发了段错误(strcpy(p,”hello”);放在了p = “hello”;的前面就不会引发),问题在于第一句修改p的指向,“hello”在只读常量区,也就是说其之不能修改,而后面紧接着就用strcpy修改其值,导致了段错误。当我们稍作修改如下,问题立马解决:

/*修改strcpy(p,"hello");与p = "hello";的顺序*/#include<stdio.h>#include<errno.h>#include<pthread.h>#include<string.h>#include<stdlib.h>void * task(void *p){ printf("%s/n",(char *)p); //p = "hello";//修改地址为只读常量区地址 strcpy(p,"hello");//没有修改地址,原有地址在main函数中 p = "hello"; /* *注意:第八行与第九行不能同时执行,必须注释掉一个 *但是当调换8、9行顺序后可以同时执行(即9、10行一起执行) *这是因为"hello"在只读区 *p="hello"是修改p的地址,strcpy是修改p的指向的地址中的值 *8、9行的代码顺序试图改变只读区的值,引发段错误而退出 *而9、10行的顺序不会去修改只读区的值 * */ //char st[] = "hello";//st指向栈区,返回的是无效的指针,不能使用 return p; //return st;}int main(void){ char str[] = "abcde"; pthread_t tid; pthread_create(&tid, NULL, task, str); char * retval = "world"; int ret = pthread_join(tid, (void **)&retval); if(ret){ printf("pthread_join error %s/n",strerror(ret)); exit(EXIT_FAILURE); } printf("str = %s/n",str);//str由strcpy(p,"hello");语句改变 printf("res = %s/n",retval);//retval由p="hello";语句改变 return 0;}

结果如下(正确结果):

这里写图片描述

4、总结:

关于函数/线程的返回值: ①可以以指针作返回值,但是不能直接以数组作返回类型; ②可以返回局部变量,但是不能返回局部变量的指针; ③加了static的局部变量指针可以返回(加static就不再是局部变量); ④函数/线程的返回值必须是一个有效的指针。 最后再加一点:对于返回值不仅要注意栈空间,也要注意全局区、堆区。也就是说对于按址返回的方式,一定要注意在函数执行中其地址是否改变,其地址是否被允许改变,其地址的指向是否被允许改变。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表