规则

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

我总结出下面这些规则:

  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. 行人检测与重识别!SOTA算法

    行人检测与重识别!SOTA算法 A Simple Baseline for Multi-Object Tracking, Yifu Zhang, Chunyu Wang, Xinggang Wang, ...

  2. 深度学习框架集成平台C++ Guide指南

    深度学习框架集成平台C++ Guide指南 这个指南详细地介绍了神经网络C++的API,并介绍了许多不同的方法来处理模型. 提示 所有框架运行时接口都是相同的,因此本指南适用于所有受支持框架(包括Te ...

  3. JAVA 进行图片中文字识别(准确度高)!!!

    OCR 识别文字项目 该项目 可以进行两种方式进行身份证识别 1. 使用百度接口 1.1 application-dev.yml配置 ocr: # 使用baiduOcr 需要有Ocr服务器 使用百度需 ...

  4. NX二次开发】Block UI 选择特征

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  5. NX二次开发-曲线或边分析函数

    UF_EVAL_is_arc   判断是圆形曲线或边UF_EVAL_ask_arc 圆形曲线或边分析,得到曲线或边的信息 类似的函数还有以下这些: UF_EVAL_is_ellipse // 椭圆UF ...

  6. Zab协议 (史上最全)

    文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...

  7. noConflict冲突处理机制

    最近接手了一个古早项目,用的backbone,于是正好学习一下早期MVC框架的源码. 这篇主要写冲突处理机制,源码其实就一个函数,代码也很短.原理也很好理解,总结起来就是:每执行一次noConflic ...

  8. 重新整理 .net core 实践篇—————Mediator实践[二十八]

    前言 简单整理一下Mediator. 正文 Mediator 名字是中介者的意思. 那么它和中介者模式有什么关系呢?前面整理设计模式的时候,并没有去介绍具体的中介者模式的代码实现. 如下: https ...

  9. vs2008中安装dev之后输入代码会输入代码段但是报错,可能解决方法

    vs2008工具栏DevExpress→Options 取消勾选这个

  10. jwt-在asp.net core中的使用jwt

    JWT学习文章: 第一篇:JWT原理 第二篇:JWT原理实现代码 第三篇:在asp.net core中的使用JWT 前两篇文章中我写了jwt的原理,并且也用原理实现了jwt的验证.如果要看前两篇文章, ...