规则

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

我总结出下面这些规则:

  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. Python小白的数学建模课-A3.12 个新冠疫情数模竞赛赛题与点评

    新冠疫情深刻和全面地影响着社会和生活,已经成为数学建模竞赛的背景帝. 本文收集了与新冠疫情相关的的数学建模竞赛赛题,供大家参考,欢迎收藏关注. 『Python小白的数学建模课 @ Youcans』带你 ...

  2. nvGRAPH原理概述

    nvGRAPH原理概述 nvGRAPH的API参考分析. 简介 数据分析是高性能计算的不断增长的应用.许多高级数据分析问题可以称为图形问题.反过来,当今许多常见的图形问题也可以称为稀疏线性代数.这是N ...

  3. 理想的GVS智能照明体验,就在汕头迎宾花园酒店

    汕头,依海而生,海在城中央是汕头特色. 汕头湾将汕头分为南北两岸,造就绝美市区海岸线,一碧万顷的海湾,焕然一新的海港,在市区就能直接看海. 在北山湾,动可结伴冲浪,静可观海吹风,动静都是一种快乐. 当 ...

  4. 孟老板 BaseAdapter封装(五) ListAdapter

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  5. python应用_异常处理

    我们把可能发生错误的语句放在try模块里,用except来处理异常. 参考学习链接: https://www.cnblogs.com/OliverQin/p/12222619.html 异常处理的完整 ...

  6. 深入解读Redis分布式锁

    之前码甲哥写了两篇有关线程安全的文章: 你管这叫线程安全? .NET八股文:线程同步技术解读 分布式锁是"线程同步"的延续 最近首度应用"分布式锁",现在想想, ...

  7. Mybatis数据连接池的配置---增删改查(以及遇见的问题)

    1.首先创建项目和各个文件,如图所示: 2.配置相关数据库连接 在jdbc.properties中加入 1 db.driver=com.mysql.jdbc.Driver 2 db.url=jdbc: ...

  8. 【题解】PIZZA 贪心

    题目描述 Michael请N个朋友吃馅饼,但是每个朋友吃且仅吃一个馅饼的1/4.1/2或3/4.请你编程求出Michael至少需要买多少个馅饼. 输入输出格式 输入格式: 输入文件的第一行是整数N:接 ...

  9. ES系列(七):多节点任务的分发与收集实现

    我们知道,当我们对es发起search请求或其他操作时,往往都是随机选择一个coordinator发起请求.而这请求,可能是该节点能处理,也可能是该节点不能处理的,也可能是需要多节点共同处理的,可以说 ...

  10. Kubernetes Pod中容器的Liveness、Readiness和Startup探针

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 探针的作用 在Kubernetes的容器生命周期管理中,有三种探针,首先要知道,这探针是属于容器的,而不是Pod: 存 ...