程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言返回值深入研究

C語言返回值深入研究

編輯:關於C語言

返回值不是挺簡單的嗎?有什麼好研究的。

其實返回值不簡單,下面就讓我們來看看返回值有什麼好研究的。

在操作系統中以linux為例),每個程序都需要有一個返回值,返回給操作系統.

在shell中,可以利用echo $?查看程序的返回值

可以看到,not_exist不存在,返回2,main.c存在,返回0,一般返回0表示成功,而返回非0表示失敗或者其他意義。

其實這個返回值是存放在eax中的,c規范要求main必須返回int,而int和eax長度是一的(32位系統)。

 

這個匯編程序只有一條指令,將4存到eax,檢測返回值發現是4。

如果你的程序用void main(),有的編譯器會報錯,有的會警告,如果編譯過了,運行時一般沒問題。

  1. int f()  
  2. {  
  3.       return 100;  
  4. }  
  5. void main()  
  6. {  
  7.       f();  

 

函數f把返回值放到eax了,main函數什麼都沒做,所以返回值還是100。

但是我們來看另外一個例子

    //file:haha.c
  1. struct xxx{
  2. int a[50];
  3. };
  4. struct xxx main()
  5. {
  6. struct xxx haha;
  7. return haha;
  8. }  

為什麼會出現段錯誤?我們後面會研究它。

 

我們先把返回值進行分類:

首先是基本類型,void,char,short,long,long long,float,double,指針

然後是結構類型struct。

對於void類型,沒有返回值,不做討論。

char只有1個字節,eax有4個字節,怎麼存?只用低8位al就可以了。下面是示例

  1. //示例1:返回值為char  
  2.  
  3. /*C代碼*/
  4. char f()
    {
            char a = 'a';
            return a;
    }
    int main()
    {
            char b = f();
            return 0;
    }
     
  5.  
  6. /*匯編代碼*/
  7. .file "char.c"
  8. .text
  9. .globl f
  10. f:
  11. pushl %ebp
  12. movl %esp, %ebp
  13. subl $16, %esp
  14. movb $97, -1(%ebp)
  15. movsbl -1(%ebp),%eax //符號擴展
  16. leave
  17. ret
  18. .globl main
  19. main:
  20. leal 4(%esp), %ecx
  21. andl $-16, %esp
  22. pushl -4(%ecx)
  23. pushl %ebp
  24. movl %esp, %ebp
  25. pushl %ecx
  26. subl $16, %esp
  27. call f
  28. movb %al, -5(%ebp)
  29. movl $0, %eax
  30. addl $16, %esp
  31. popl %ecx
  32. popl %ebp
  33. leal -4(%ecx), %esp
  34. ret

從匯編代碼中可以看出,調用完f後,main函數從al中找返回值。

同樣,對於short,int,分別把返回值存放到ax,eax,假如在64位系統裡,那麼long long 返回值是存到rax的,它的長度為64位,在32位系統裡是怎麼存的呢?

在32位系統裡返回64位數,是通過edx和eax聯合實現的,edx存高32位,eax存低32位。

  1. /*示例2:32位系統上返回64位整數*/ 
  2. /*C代碼*/ 
  3. long long f()  
  4. {  
  5.         long long a = 5;  
  6.         return a;  
  7. }  
  8. int main()  
  9. {  
  10.         long long b;  
  11.          b=f();  
  12.         return 0;  
  13. }  
  14. /*匯編代碼*/ 
  15.        .file   "longint.c" 
  16.         .text  
  17. .globl f  
  18. f:  
  19.         pushl   %ebp  
  20.         movl    %esp, %ebp  
  21.         subl    $16, %esp  
  22.         movl    $5, -8(%ebp)  
  23.         movl    $0, -4(%ebp)  
  24.         movl    -8(%ebp), %eax  
  25.         movl    -4(%ebp), %edx  
  26.         leave  
  27.         ret  
  28. .globl main  
  29. main:  
  30.         leal    4(%esp), %ecx  
  31.         andl    $-16, %esp  
  32.         pushl   -4(%ecx)  
  33.         pushl   %ebp  
  34.         movl    %esp, %ebp  
  35.         pushl   %ecx  
  36.         subl    $20, %esp  
  37.         call    f  
  38.         movl    %eax, -16(%ebp)  
  39.         movl    %edx, -12(%ebp)  
  40.         movl    $0, %eax  
  41.         addl    $20, %esp  
  42.         popl    %ecx  
  43.         popl    %ebp  
  44.         leal    -4(%ecx), %esp  
  45.         ret  

對於浮點類型,雖然運算過程中會存放在eax等普通寄存器中,但是作為返回值時,不會用eax,edx等,即使運算結果已經存到了eax中,也要再壓到浮點數寄存器堆棧中,在主調函數中,會認為返回結果存到浮點數寄存器了,當然,如果你要手動優化匯編代碼也是沒問題的。

下面是示例。

  1. /*示例3:返回值為浮點數*  
  2. /*C代碼*/  
  3. float f()  
  4. {  
  5.         return 0.1;  
  6. }  
  7. int main()  
  8. {  
  9.         float a = f();  
  10.         return 0;  
  11. }  
  12. /*匯編代碼*/  
  13.         .file   "float.c"  
  14.         .text  
  15. .globl f  
  16. f:  
  17.         pushl   %ebp  
  18.         movl    %esp, %ebp  
  19.         subl    $4, %esp  
  20.         movl    $0x3dcccccd, %eax  
  21.         movl    %eax, -4(%ebp)  
  22.         flds    -4(%ebp)  //把結果壓到浮點寄存器棧頂  
  23.         leave  
  24.         ret  
  25. .globl main  
  26. main:  
  27.         leal    4(%esp), %ecx  
  28.         andl    $-16, %esp  
  29.         pushl   -4(%ecx)  
  30.         pushl   %ebp  
  31.         movl    %esp, %ebp  
  32.         pushl   %ecx  
  33.         subl    $16, %esp  
  34.         call    f  
  35.         fstps   -8(%ebp) //從浮點寄存器棧頂取數  
  36.         movl    $0, %eax  
  37.         addl    $16, %esp  
  38.         popl    %ecx  
  39.         popl    %ebp  
  40.         leal    -4(%ecx), %esp  
  41.         ret 

關於浮點寄存器及浮點運算指令,可參考:http://www.diybl.com/course/3_program/hb/hbjs/2007124/89946.html

如果返回值為指針?那肯定是用eax(32bit)或者rax(64bit)了。不管是什麼類型的指針,都一樣,我們來看一個奇怪的程序。

  1. /*示例4:返回值為指針*/ 
  2. /*C代碼*/ 
  3. int f()  
  4. {  
  5.         return 5;  
  6. }  
  7. int (*whatisthis()) ()  //這個函數的返回類型是函數指針
  8. {  
  9.         return f;  
  10. }  
  11. int main()  
  12. {  
  13.         int (*a) ();  
  14.         int b;  
  15.         a = whatisthis();  
  16.         b = a();  
  17.         printf("%d\n",b);  
  18.         return 0;  
  19. }  
  20. /*匯編代碼*/ 
  21.         .file   "ret_fun.c" 
  22.         .text  
  23. .globl f  
  24. f:  
  25.         pushl   %ebp  
  26.         movl    %esp, %ebp  
  27.         movl    $5, %eax  
  28.         popl    %ebp  
  29.         ret  
  30.  
  31. .globl whatisthis  
  32. whatisthis:  
  33.         pushl   %ebp  
  34.         movl    %esp, %ebp  
  35.         movl    $f, %eax  
  36.         popl    %ebp  
  37.         ret  
  38.  
  39. .LC0:  
  40.         .string "%d\n" 
  41.         .text  
  42.  
  43. .globl main  
  44. main:  
  45.         leal    4(%esp), %ecx  
  46.         andl    $-16, %esp  
  47.         pushl   -4(%ecx)  
  48.         pushl   %ebp  
  49.         movl    %esp, %ebp  
  50.         pushl   %ecx  
  51.         subl    $36, %esp  
  52.         call    whatisthis  
  53.         movl    %eax, -12(%ebp)  
  54.         movl    -12(%ebp), %eax  
  55.         call    *%eax            
  56.         movl    %eax, -8(%ebp)  
  57.         movl    -8(%ebp), %eax  
  58.         movl    %eax, 4(%esp)  
  59.         movl    $.LC0, (%esp)  
  60.         call    printf  
  61.         movl    $0, %eax  
  62.         addl    $36, %esp  
  63.         popl    %ecx  
  64.         popl    %ebp  
  65.         leal    -4(%ecx), %esp  
  66.         ret  

一個函數的返回值可以是函數指針,定義一個這樣的函數如下:

函數1   int f(int,char)

函數2   返回值為上面函數的類型的指針,假如函數名為g,參數為float

那麼g的定義為     int   (* g(float x)  )    (int,char)

基本類型討論完了,那麼struct類型呢?struct可大可小,怎麼存到寄存器裡呢?

答案是:主調函數會把被賦值對象的地址傳給被調用函數。你可能會說這不是傳引用嗎,其實傳引用傳值什麼的都是浮雲。

還有一個問題就是,對於struct xxx { char a; };這樣的結構也要傳地址嗎?答案是肯定的,gcc是這樣做的,其它編譯器可能不這樣,當然也可以手動修改匯編代碼。

  1. /*示例5:struct只有一個字節*/ 
  2. /*C代碼*/ 
  3. struct xxx{  
  4.         char a;  
  5. };  
  6. struct xxx  f()  
  7. {  
  8.         struct xxx x;  
  9.         x.a = '9';  
  10.         return x;  
  11. }  
  12. int main()  
  13. {  
  14.         struct xxx y = f();  
  15.         return 0;  
  16. }  
  17. /*匯編代碼*/ 
  18.         .file   "struct_char.c" 
  19.         .text  
  20. .globl f  
  21. f:  
  22.         pushl   %ebp  
  23.         movl    %esp, %ebp  
  24.         subl    $16, %esp  
  25.         movl    8(%ebp), %edx //取出地址,放入edx  
  26.         movb    $57, -1(%ebp)    
  27.         movzbl  -1(%ebp), %eax //'9'放到 al  
  28.         movb    %al, (%edx) //將al內容寫到edx指向的地址  
  29.         movl    %edx, %eax  
  30.         leave  
  31.         ret     $4  
  32.  
  33. .globl main  
  34. main:  
  35.         leal    4(%esp), %ecx  
  36.         andl    $-16, %esp  
  37.         pushl   -4(%ecx)  
  38.         pushl   %ebp  
  39.         movl    %esp, %ebp  
  40.         pushl   %ecx  
  41.         subl    $24, %esp  
  42.         leal    -21(%ebp), %eax //地址放到eax  
  43.         movl    %eax, (%esp) //地址壓入棧中  
  44.         call    f   
  45.         subl    $4, %esp    //沒有取返回值的指令了  
  46.         movzbl  -21(%ebp), %eax//因為已經寫到目的地址了  
  47.         movb    %al, -5(%ebp)  
  48.         movl    $0, %eax  
  49.         movl    -4(%ebp), %ecx  
  50.         leave  
  51.         leal    -4(%ecx), %esp  
  52.         ret  

我們再來看個復雜點的例子

  1. /*示例6: struct較大*/ 
  2. /*C代碼*/ 
  3. struct xxx {  
  4.         char a[10];  
  5. };  
  6. struct xxx f(int a)  
  7. {  
  8.         struct xxx t;  
  9.         t.a[9] = 1;  
  10.         return t;  
  11. }  
  12. int main()  
  13. {  
  14.         struct xxx m=f(1);  
  15.         return 0;  
  16. }  
  17. /*匯編代碼*/ 
  18.         .file   "struct.c" 
  19.         .text  
  20. .globl f  
  21. f:  
  22.         pushl   %ebp  
  23.         movl    %esp, %ebp  
  24.         subl    $16, %esp  
  25.         movl    8(%ebp), %edx   //取地址
  26.         movb    $1, -1(%ebp)  
  27.         movl    -10(%ebp), %eax  
  28.         movl    %eax, (%edx)  
  29.         movl    -6(%ebp), %eax  
  30.         movl    %eax, 4(%edx)  
  31.         movzwl  -2(%ebp), %eax  
  32.         movw    %ax, 8(%edx)  
  33.         movl    %edx, %eax  
  34.         leave  
  35.         ret     $4  
  36.  
  37. .globl main  
  38. main:  
  39.         leal    4(%esp), %ecx  
  40.         andl    $-16, %esp  
  41.         pushl   -4(%ecx)  
  42.         pushl   %ebp  
  43.         movl    %esp, %ebp  
  44.         pushl   %ecx  
  45.         subl    $24, %esp  
  46.         leal    -14(%ebp), %eax  
  47.         movl    $1, 4(%esp)      //先壓入參數  
  48.         movl    %eax, (%esp)     //再壓入返回值地址  
  49.         call    f  
  50.         subl    $4, %esp  
  51.         movl    $0, %eax  
  52.         movl    -4(%ebp), %ecx  
  53.         leave  
  54.         leal    -4(%ecx), %esp  
  55.         ret  

進入被調用函數後的堆棧情況

 

它會到假定8(%ebp)處存放著返回值的地址。這也是為什麼main的返回值為struct時會引起段錯誤,main函數認為這個地方存著返回值的地址,實際上這個地方是操作系統寫入的特定值,把這個當作返回值的地址亂寫,肯定會引起段錯誤。

下面這個程序

假如對於struct,有返回值的函數卻不賦值怎麼辦?

比如

  1. struct xxx {  
  2.         char a[10];  
  3. };  
  4. struct xxx f(int a)  
  5. {  
  6.         struct xxx t;  
  7.         t.a[9] = 1;  
  8.         return t;  
  9. }  
  10. int main()  
  11. {  
  12.         f(1);  
  13.         return 0;  
  14. }  

對於上述程序,主調用函數需要開辟垃圾空間作為返回值空間,感興趣的可以驗證下看看。

補充:

gcc支持代碼塊有返回值

比如a = { int b = 2; int c = 3; c-b;} 最終a = 1;

根據我的測試:代碼塊裡必須有除了變量聲明的其他語句,否則不對,不能有return;

另外,只能對基本類型賦值,struct類型不能賦值。

最後的結果是:代碼塊執行結束後,取出eax的值,檢查要賦值的變量類型,如果是char,取al,如果是int,取eax,如果是long long,符號擴展,如果是float或者double,將eax強制轉換成浮點數。

下面代碼可正常運行:

  1. int main()  
  2. {  
  3.         int a;  
  4.         long long a1;  
  5.         double a2;  
  6.         a  = {int b = 5; printf("xxx\n");;};  
  7.         a1  = {int b = 5;int c = 2; 3-4;b-c;};  
  8.         a2  = {int b = 5;int c = 2; 10-8;};  
  9.         printf("%d\n",a);  
  10.         printf("%ld\n",a1);  
  11.         printf("%lf\n",a2);  
  12.         return 0;  
  13. }  

上面代碼中的3-4會被忽略,因為沒有用,而10-8不會被忽略,因為它在代碼塊最後,但是不是執行sub指令,直接movl $2, %eax;

這東西有用嗎?沒用我就不去研究它了,確實用到了,在Linux內核裡,contain_of這個宏用到了上述內容,所以我稍微研究了下。

維基百科講的比較詳細,http://zh.wikipedia.org/wiki/%E5%9D%97_(C%E8%AF%AD%E8%A8%80%E6%89%A9%E5%B1%95)

本文出自 “牛哥的博客” 博客,請務必保留此出處http://nxlhero.blog.51cto.com/962631/703953

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved