C語言中函式的返回值

東垂小夫 發表於 2021-07-21

規則

除區域性變數的記憶體地址不能作為函式的返回值外,其他型別的區域性變數都能作為函式的返回值。

我總結出下面這些規則:

  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 <[email protected]>
   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 <[email protected]>
   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. 最好為函式的指標型別區域性變數手工分配記憶體空間,否則,會出現詭異的錯誤。