规则

除局部变量的内存地址不能作为函数的返回值外,其他类型的局部变量都能作为函数的返回值。

我总结出下面这些规则:

  1. intchar等数据类型的局部变量可以作为函数返回值。
  2. 在函数中声明的指针可以作为函数返回值。指针可以是执行int等数据类型的指针,也可以是指向结构体的指针。
  3. 在函数中声明的结构体也可以作为函数返回值。
  4. 在函数中声明的数组不能作为函数返回值。
  5. 函数中的局部变量的内存地址不能作为函数返回值。

代码

对上面的每条规则列举一段代码,然后观察执行结果。

int类型局部变量

int f2()
{
int a = 54;
return a;
}

指针类型局部变量

int *f()
{
int *a = malloc(sizeof(int));
*a = 54;
return a;
}
struct person *f6()
{
struct person *p1 = malloc(sizeof(struct person));
//struct person *p1;
//*p1 = {2};
p1->age = 2;
strcpy(p1->name, "Jim");
return p1;
}

结构体局部变量

struct person f5()
{
struct person p1 = {2, "Jim"};
return p1;
}

数组局部变量

int *f4()
{
int a[2] = {1,2};
// warning: function returns address of local variable [-Wreturn-local-addr]
return a;
}

局部变量的内存地址

int *f3()
{
int a = 54;
// warning: function returns address of local variable [-Wreturn-local-addr]
return &a;
}

main

#include <stdio.h>
#include <string.h>
#include <stdlib.h> struct person{
int age;
char name[20];
}; int *f();
int f2();
int *f3();
int *f4();
struct person f5();
struct person *f6(); int main(int argc, char **argv)
{
int *t = f();
printf("t = %p\n", t);
printf("*t = %d\n", *t);
int t2 = f2();
printf("t2 = %d\n", t2);
int *t3 = f3();
printf("t3 = %p\n", t3);
int *t4 = f4();
printf("t4 = %p\n", t4);
struct person p1 = f5();
printf("p1.age = %d\n", p1.age);
struct person *p2 = f6();
printf("p2->age = %d\n", p2->age);
return 0;
}

执行结果是:

t = 0x836f1a0
*t = 54
t2 = 54
t3 = (nil)
t4 = (nil)
p1.age = 2
p2->age = 2

t3、t4的值是(nil),说明局部变量的内存地址和数组类型的局部变量并不能作为函数返回值。

原因

为什么会这样?

内存地址和数组

局部变量的内存地址指向的是函数栈中的一个元素A,当函数执行结束后,函数的栈会被清空。无论在A中存储了什么数据,当函数执行结束后,A中的数据都不存在了。虽然仍然可以用A的内存地址访问A内存,但是A中的数据没有了。

所以,在函数执行完后,再访问函数栈,是没有任何意义的。

数组类型的局部变量作为返回值,实质也是“局部变量的内存地址作为返回值”的变种。在函数f4中,返回数据是aa是数组名,同时也是数组的内存地址,即,是一个局部变量的内存地址。

其他

除局部变量的内存地址和数组外,其他类型的局部变量为什么能够作为函数返回值?

直接从上面那些函数对应的汇编代码找原因吧。

汇编函数常识

先简单介绍一些汇编函数的常识。

  1. eax寄存器中最后的值是函数的返回值。
  2. 如果函数有三个参数,从右到左一次是p3、p2、p1,进入函数后,函数栈的元素从高地址到低地址应该是:p3、p2、p1、eip、旧ebp。
  3. 函数的局部变量存储在ebp-N位置。

只详细解释f函数的汇编代码,其他函数的汇编代码可以模仿对f的解释自己去理解。

f

(gdb) disas f
Dump of assembler code for function f:
0x080485be <+0>: push %ebp
0x080485bf <+1>: mov %esp,%ebp
0x080485c1 <+3>: sub $0x18,%esp
0x080485c4 <+6>: sub $0xc,%esp
0x080485c7 <+9>: push $0x4
0x080485c9 <+11>: call 0x8048380 <malloc@plt>
0x080485ce <+16>: add $0x10,%esp
0x080485d1 <+19>: mov %eax,-0xc(%ebp)
0x080485d4 <+22>: mov -0xc(%ebp),%eax
0x080485d7 <+25>: movl $0x36,(%eax)
0x080485dd <+31>: mov -0xc(%ebp),%eax
0x080485e0 <+34>: leave
0x080485e1 <+35>: ret
End of assembler dump.

寄存器eax中的值是函数的返回值。

mov -0xc(%ebp),%eax,把-0xc(%ebp)中的值作为函数的返回值。

那么,-0xc(%ebp)中的值是什么呢?

   0x080485d4 <+22>:	mov    -0xc(%ebp),%eax
0x080485d7 <+25>: movl $0x36,(%eax)

让我们一起理解上面的两条语句:

  1. 第1条语句,把-0xc(%ebp)中的数据复制到eax中。
  2. -0xc(%ebp)中是由malloc分配的4个字节的内存空间的第1个字节的内存地址M。
  3. mov -0xc(%ebp),%eax的意思是,把malloc分配的4个字节的内存空间的第1个字节的内存地址M复制到eax中。
  4. movl $0x36,(%eax),把54存储到M指向的内存空间中。

现在能回答mov -0xc(%ebp),%eax中的-0xc(%ebp)中的值是什么了。是M。

M指向的内存中的数据在函数执行结束后有没有被清除?我从汇编代码中也没有找到答案。然而,结合整个程序的执行结果,我认为,M指向的内存应该不属于本函数的栈空间。因为,在函数执行结束后,仍然能从M中获取在函数中存储的数据。

f2

(gdb) disas f2
Dump of assembler code for function f2:
0x080485e2 <+0>: push %ebp
0x080485e3 <+1>: mov %esp,%ebp
0x080485e5 <+3>: sub $0x10,%esp
0x080485e8 <+6>: movl $0x36,-0x4(%ebp)
0x080485ef <+13>: mov -0x4(%ebp),%eax
0x080485f2 <+16>: leave
0x080485f3 <+17>: ret
End of assembler dump.

f3

(gdb) disas f3
Dump of assembler code for function f3:
0x080485f4 <+0>: push %ebp
0x080485f5 <+1>: mov %esp,%ebp
0x080485f7 <+3>: sub $0x10,%esp
0x080485fa <+6>: movl $0x36,-0x4(%ebp)
0x08048601 <+13>: mov $0x0,%eax
0x08048606 <+18>: leave
0x08048607 <+19>: ret
End of assembler dump.

f4

(gdb) disas f4
Dump of assembler code for function f4:
0x08048608 <+0>: push %ebp
0x08048609 <+1>: mov %esp,%ebp
0x0804860b <+3>: sub $0x10,%esp
0x0804860e <+6>: movl $0x1,-0x8(%ebp)
0x08048615 <+13>: movl $0x2,-0x4(%ebp)
0x0804861c <+20>: mov $0x0,%eax
0x08048621 <+25>: leave
0x08048622 <+26>: ret
End of assembler dump.

f5

(gdb) disas f5
Dump of assembler code for function f5:
0x08048623 <+0>: push %ebp
0x08048624 <+1>: mov %esp,%ebp
0x08048626 <+3>: sub $0x20,%esp
0x08048629 <+6>: movl $0x2,-0x18(%ebp)
0x08048630 <+13>: movl $0x6d694a,-0x14(%ebp)
0x08048637 <+20>: movl $0x0,-0x10(%ebp)
0x0804863e <+27>: movl $0x0,-0xc(%ebp)
0x08048645 <+34>: movl $0x0,-0x8(%ebp)
0x0804864c <+41>: movl $0x0,-0x4(%ebp)
0x08048653 <+48>: mov 0x8(%ebp),%eax
0x08048656 <+51>: mov -0x18(%ebp),%edx
0x08048659 <+54>: mov %edx,(%eax)
0x0804865b <+56>: mov -0x14(%ebp),%edx
0x0804865e <+59>: mov %edx,0x4(%eax)
0x08048661 <+62>: mov -0x10(%ebp),%edx
0x08048664 <+65>: mov %edx,0x8(%eax)
0x08048667 <+68>: mov -0xc(%ebp),%edx
0x0804866a <+71>: mov %edx,0xc(%eax)
0x0804866d <+74>: mov -0x8(%ebp),%edx
0x08048670 <+77>: mov %edx,0x10(%eax)
0x08048673 <+80>: mov -0x4(%ebp),%edx
0x08048676 <+83>: mov %edx,0x14(%eax)
0x08048679 <+86>: mov 0x8(%ebp),%eax
0x0804867c <+89>: leave
0x0804867d <+90>: ret $0x4
End of assembler dump.
  1. movl $0x6d694a,-0x14(%ebp),把Jim存储到-0x14(%ebp)指向的栈空间。
  2. mov -0x18(%ebp),%edx,把struct person p1的内存地址复制到edx中。
  3. mov 0x8(%ebp),%eax,从这条指令可以看出:
    1. 0x8(%ebp)中存储着struct person p1占据的内存空间的首地址。
    2. 0x8(%ebp)是什么?f5没有参数,0x8(%ebp)不是参数的内存地址,而是由系统自动为p1分配了一块内存。

回过头再看前面的语句。

  1. movl $0x2,-0x18(%ebp),把2存储到-0x18(%ebp)指向的内存中。

  2. ; 把struct person p1占据的内存的地址复制到eax中。
    mov 0x8(%ebp),%eax
    ; 把-0x18(%ebp)中的数据,也就是2复制到edx中。
    mov -0x18(%ebp),%edx
    ; 把2复制到struct person p1中。
    mov %edx,(%eax)
    ; 上面的所有语句的功能是把p1的age成员设置为2。
  3. ; 把p1的成员name设置成Jim。
    movl $0x6d694a,-0x14(%ebp)
    mov -0x14(%ebp),%edx
    mov %edx,0x4(%eax)
  4. # 这些语句为struct person的两个成员准备数据,把即将赋值给两个成员的值存储在栈中中。
    # 第二个成员char name[20]占用20个字节,
    # 0x18-0x15:4个;0x14-0x11:4个;0x10-0xd:4个;0xc-0x9:4个;0x8-0x5:4个;0x4-0x0:4个。
    #
    0x08048629 <+6>: movl $0x2,-0x18(%ebp)
    0x08048630 <+13>: movl $0x6d694a,-0x14(%ebp)
    0x08048637 <+20>: movl $0x0,-0x10(%ebp)
    0x0804863e <+27>: movl $0x0,-0xc(%ebp)
    0x08048645 <+34>: movl $0x0,-0x8(%ebp)
    0x0804864c <+41>: movl $0x0,-0x4(%ebp)

f6

(gdb) disas f6
Dump of assembler code for function f6:
0x08048680 <+0>: push %ebp
0x08048681 <+1>: mov %esp,%ebp
0x08048683 <+3>: sub $0x18,%esp
0x08048686 <+6>: sub $0xc,%esp
0x08048689 <+9>: push $0x18
0x0804868b <+11>: call 0x8048380 <malloc@plt>
0x08048690 <+16>: add $0x10,%esp
0x08048693 <+19>: mov %eax,-0xc(%ebp)
0x08048696 <+22>: mov -0xc(%ebp),%eax
0x08048699 <+25>: movl $0x2,(%eax)
0x0804869f <+31>: mov -0xc(%ebp),%eax
0x080486a2 <+34>: add $0x4,%eax
0x080486a5 <+37>: movl $0x6d694a,(%eax)
0x080486ab <+43>: mov -0xc(%ebp),%eax
0x080486ae <+46>: leave
0x080486af <+47>: ret
End of assembler dump.

结论

观察上面的汇编的代码,我得出两个结论:

  1. 如果函数的返回值不是人为设置成0,函数对应的汇编代码却把eax的值设置成0,那么,可以认为,这个函数的返回值有问题。
  2. 函数的指针类型局部变量指向的内存空间并不在函数的栈中。
  3. 最好为函数的指针类型局部变量手工分配内存空间,否则,会出现诡异的错误。

C语言中函数的返回值的更多相关文章

  1. [日常] Go语言圣经-函数多返回值习题

    Go语言圣经-函数多返回值1.在Go中,一个函数可以返回多个值2.许多标准库中的函数返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息3.如果一个函数将所有的返回值都显示的变量名,那么该 ...

  2. C++中函数的返回值

    原文 [ 函数的返回值用于初始化在调用函数处创建的临时对象.在求解表达式时,如果需要一个地方储存其运算结果,编译器会创建一个没有命名的对象,这就是 临时对象.temporary object ] -- ...

  3. python中函数的返回值

    函数返回值(一) <1>“返回值”介绍 现实生活中的场景: 我给儿子10块钱,让他给我买包烟.这个例子中,10块钱是我给儿子的,就相当于调用函数时传递到参数,让儿子买烟这个事情最终的目标是 ...

  4. js 中 函数的返回值问题

    var result=''; function searchByStationName( address ) { // map.clearOverlays();//清空原来的标注 var keywor ...

  5. SpringMvc中函数的返回值是什么?

    返回值可以有很多类型,有String, ModelAndView.ModelAndView类把视图和数据都合并的一起的,但一般用String比较好.

  6. C语言中函数的传入值与传出值

    看到一个函数的原型后,怎么样一眼看出来哪个参数做输入哪个做输出? 函数传参如果传的是普通变量(不是指针)那肯定是输入型参数: 如果传指针就有 2 种可能性了,为了区别,经常的做法是: 如果这个参数是做 ...

  7. Swift2.0语言教程之函数的返回值与函数类型

    Swift2.0语言教程之函数的返回值与函数类型 Swift2.0中函数的返回值 根据是否具有返回值,函数可以分为无返回值函数和有返回值函数.以下将会对这两种函数类型进行讲解. Swift2.0中具有 ...

  8. C语言中函数返回字符串的4中方法

    C语言中函数返回字符串的4中方法 函数的构成部分:返回类型.函数名称.参数.函数主体 参数:函数调用时传入的参数称为实参,函数定义时出现的参数为形参 形参的作用在于接收实参传入的值,形参和函数内部的其 ...

  9. JavaScript 在函数中使用Ajax获取的值作为函数的返回值

    解决:JavaScript 在函数中使用Ajax获取的值作为函数的返回值,结果无法获取到返回值 原因:ajax默认使用异步方式,要将异步改为同步方式 案例:通过区域ID,获取该区域下所有的学校 var ...

随机推荐

  1. Relay张量集成

    Relay张量集成 Introduction NVIDIA TensorRT是一个用于优化深度学习推理的库.这种集成将尽可能多地减轻从中继到TensorRT的算子,在NVIDIA GPU上提供性能提升 ...

  2. Windows下Qt VS 打包程序 到他人电脑安装运行出现的问题

    1.可能缺程序依赖的Qt动态库   ------>    使用Qt自带的windeployqt进入安装程序所在的文件夹内进行自动配置 将程序安装在C盘之外的盘,这样可以方便windeployqt ...

  3. ES6中的变量结构赋值

    小编的上一篇文章更新了es6中关于变量定义的问题,这篇文章继续来一些实用的干货,关于数组.对象的赋值问题.特别是在前后端合作项目的时候,对后端数据的拆分,还有就是对于函数的默认值的惰性赋值问题.看完下 ...

  4. JAVA复习题(一)基础知识

    类的构造方法描述正确的是( )A. 类中的构造方法不能省略B. 构造方法必须与类同名,但方法不能与class同名C. 构造方法在一个对象被new时执行D. 一个类只能有一个构造方法我的答案:C正确答案 ...

  5. Python 5种方法实现单例模式

    基本介绍 一个对象只允许被一次创建,一个类只能创建一个对象,并且提供一个全局访问点. 单例模式应该是应用最广泛,实现最简单的一种创建型模式. 特点:全局唯一,允许更改 优缺点 优点: 避免对资源的多重 ...

  6. RobotFramework常用断言关键字

    变量或者关键字内容判断关键字 1.内容包含或者不包含:should contain . should not contain 与should contain x times *** Test Case ...

  7. 怎么停掉或关闭运行的npm run dev

    可以直接Ctrl+C就会出现 输入是就可以了

  8. 『动善时』JMeter基础 — 51、使用JMeter测试WebService接口

    目录 1.什么是WebService 2.WebService和SOAP的关系 3.什么是WSDL 4.测试WebService接口前的准备 (1)如何判断是WebService接口 (2)如何获取W ...

  9. Java常见面试题 非常实用【个人经验】

    必收藏的Java面试题 目录 Java 面试题 一.容器部分 二.多线程部分 三.SpringMvc部分 四.Mybatis部分 五.MySQL部分 六.Redis部分 七.RabbitMQ部分 八. ...

  10. kafka简单介绍

    Kafka是分布式发布-订阅消息系统.它最初由LinkedIn公司开发,之后成为Apache项目的一部分.Kafka是一个分布式的,可划分的,冗余备份的持久性的日志服务.它主要用于处理活跃的流式数据. ...