C语言中函数的返回值

规则

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

我总结出下面这些规则:

  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. 最好为函数的指针类型局部变量手工分配内存空间,否则,会出现诡异的错误。
posted @ 2021-07-21 17:06  东小夫  阅读(2765)  评论(0编辑  收藏  举报