規則
除局部變量的內存地址不能作為函數的返回值外,其他類型的局部變量都能作為函數的返回值。
我總結出下面這些規則:
int
、char
等數據類型的局部變量可以作為函數返回值。- 在函數中聲明的指針可以作為函數返回值。指針可以是執行
int
等數據類型的指針,也可以是指向結構體的指針。 - 在函數中聲明的結構體也可以作為函數返回值。
- 在函數中聲明的數組不能作為函數返回值。
- 函數中的局部變量的內存地址不能作為函數返回值。
代碼
對上面的每條規則列舉一段代碼,然后觀察執行結果。
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
中,返回數據是a
。a
是數組名,同時也是數組的內存地址,即,是一個局部變量的內存地址。
其他
除局部變量的內存地址和數組外,其他類型的局部變量為什么能夠作為函數返回值?
直接從上面那些函數對應的匯編代碼找原因吧。
匯編函數常識
先簡單介紹一些匯編函數的常識。
- eax寄存器中最后的值是函數的返回值。
- 如果函數有三個參數,從右到左一次是p3、p2、p1,進入函數后,函數棧的元素從高地址到低地址應該是:p3、p2、p1、eip、舊ebp。
- 函數的局部變量存儲在
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條語句,把
-0xc(%ebp)
中的數據復制到eax中。 -0xc(%ebp)
中是由malloc分配的4個字節的內存空間的第1個字節的內存地址M。mov -0xc(%ebp),%eax
的意思是,把malloc分配的4個字節的內存空間的第1個字節的內存地址M復制到eax中。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.
movl $0x6d694a,-0x14(%ebp)
,把Jim
存儲到-0x14(%ebp)
指向的棧空間。mov -0x18(%ebp),%edx
,把struct person p1
的內存地址復制到edx中。mov 0x8(%ebp),%eax
,從這條指令可以看出:0x8(%ebp)
中存儲着struct person p1
占據的內存空間的首地址。0x8(%ebp)
是什么?f5沒有參數,0x8(%ebp)
不是參數的內存地址,而是由系統自動為p1分配了一塊內存。
回過頭再看前面的語句。
-
movl $0x2,-0x18(%ebp)
,把2存儲到-0x18(%ebp)
指向的內存中。 -
; 把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。
-
; 把p1的成員name設置成Jim。 movl $0x6d694a,-0x14(%ebp) mov -0x14(%ebp),%edx mov %edx,0x4(%eax)
-
# 這些語句為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.
結論
觀察上面的匯編的代碼,我得出兩個結論:
- 如果函數的返回值不是人為設置成0,函數對應的匯編代碼卻把eax的值設置成0,那么,可以認為,這個函數的返回值有問題。
- 函數的指針類型局部變量指向的內存空間並不在函數的棧中。
- 最好為函數的指針類型局部變量手工分配內存空間,否則,會出現詭異的錯誤。