C語言學習篇(16)-----函數組成詳解

我們都知道函數是函數名, 函數主體(用{}括起來的代碼塊),以及參數,返回值組成。那你知道這些部分有哪些講究和注意的地方嘛,今天我們就好好研究下函數。

C語言學習篇(16)-----函數組成詳解

函數組成詳解


函數名的實質

函數名是一個符號,表示整個函數體(代碼段)的起始地址,其實質就是一個指針常量,在編譯器編譯成功時,就分配了固定地址。通過以下例子來感受下函數名是一個指針:

<code>#include <stdio.h>

typedef int (*add)(int a, int b);

int test(int a, int b)
{
\treturn (a+b);
}


int main(void)
{
\t
\tadd add1 = test;
\t
\tint c = add1(3,5);
\t
\tprintf("c = %d.\\n", c);
\t
\treturn 0;
}\t/<stdio.h>/<code>

分析:

  1. 首先我們用typedef自定義了一個函數指針類型add,該函數返回值是int ,參數為兩個int類型。
  2. 我們又定義了test函數,細心的小夥伴可以發現,test的定義與1)步驟typedef定義的函數指針完全一致, 返回值都是int, 形參都是2個int類型。
  3. 我們在main主函數中定義了add函數指針類型的add1,並add1 = test,其就是讓add1指向test函數
  4. 我們使用add1來間接調用test:add1(3,5),看下是否能否成功?運行結果是不是我們要的8?

運行結果:

C語言學習篇(16)-----函數組成詳解

函數指針調用函數

可以看到我們成功使用了函數指針來調用了函數,並得到了我們預想的結果。 接下來我們通過反彙編手段,看看彙編後的代碼是咋樣的吧。通過以下命令:

C語言學習篇(16)-----函數組成詳解

使用objdump反彙編

其中,gcc func.c -c 是讓編譯器只編譯不鏈接,得到func.o目標文件(關於編譯過程不熟悉的小夥伴,可以關注我,後續我會找一節詳細講解), 然後再通過gcc編譯工具鏈中的objdump反彙編指令,將func.o文件反彙編得到func.i文件, 我們通過notepad++打開查看些反彙編的內容:

<code>
func.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <test>:
0:\t55 \tpush %rbp
1:\t48 89 e5 \tmov %rsp,%rbp
4:\t89 7d fc \tmov %edi,-0x4(%rbp)
7:\t89 75 f8 \tmov %esi,-0x8(%rbp)
a:\t8b 55 fc \tmov -0x4(%rbp),%edx
d:\t8b 45 f8 \tmov -0x8(%rbp),%eax
10:\t01 d0 \tadd %edx,%eax
12:\t5d \tpop %rbp
13:\tc3 \tretq

0000000000000014 <main>:
14:\t55 \tpush %rbp
15:\t48 89 e5 \tmov %rsp,%rbp
18:\t48 83 ec 10 \tsub $0x10,%rsp
1c:\t48 c7 45 f8 00 00 00 \tmovq $0x0,-0x8(%rbp)
23:\t00
24:\t48 8b 45 f8 \tmov -0x8(%rbp),%rax
28:\tbe 05 00 00 00 \tmov $0x5,%esi
2d:\tbf 03 00 00 00 \tmov $0x3,%edi
32:\tff d0 \tcallq *%rax
34:\t89 45 f4 \tmov %eax,-0xc(%rbp)
37:\t8b 45 f4 \tmov -0xc(%rbp),%eax
3a:\t89 c6 \tmov %eax,%esi
3c:\tbf 00 00 00 00 \tmov $0x0,%edi
41:\tb8 00 00 00 00 \tmov $0x0,%eax
46:\te8 00 00 00 00 \tcallq 4b <main>
4b:\tb8 00 00 00 00 \tmov $0x0,%eax
50:\tc9 \tleaveq
51:\tc3 \tretq
/<main>/<main>/<test>/<code>

這裡我們忽略彙編指令,只需要看到test和main函數其實在只是個符號,而符號的實質是地址(當然這裡test符號對應地址0000000000000000,並不是最終地址,因為我們還沒鏈接),通過反彙編內容,我們更加深了函數名的理解:本質就是函數起始地址!

輸入型參數與輸出型參數

首先需要說明的是:什麼是輸入型參數,什麼是輸出型參數?

輸入型參數:是指我們想外部傳遞給函數進行加工的數據

輸出型參數:是函數處理完後,想向外輸出一些加工後的數據或是結果

通過以下簡單例子感受下輸入輸出型參數吧:

<code>#include <stdio.h>

void test(int a, int b, int *c)
{
\t*c = a + b;
}


int main(void)
{
\tint c = 0;
\t
\ttest(3,5, &c);
\t
\tprintf("c = %d.\\n", c);
\t
\treturn 0;
}
/<stdio.h>/<code>

分析:我們定義了test函數,其中a, b就是輸入型參數, 而int *c 就是輸出型參數,函數的本意是將a和b的加和後賦值給c(注意這裡為什麼不是寫成int c,而是int *c,如果不清楚的小夥伴,可以參看我上一篇文章函數傳參詳解

),然後打印在main函數中打印出c的值,運行結果:

C語言學習篇(16)-----函數組成詳解

輸入輸出型參數

OK,為了便於更好的理解輸入型和輸出型參數的區別,通常我們會結合const一起使用,我們將上面的test函數定義改成如下:

<code>void test(const int a, const int b, int *c)
{
\t*c = a + b;
}/<code>

可以看到我們分別在int a, int b前面加了const修飾

,以此來表明形參a,b是在函數體內是不允許被改變的,讓自己和未來調用該函數的人一目瞭然,這2個參數是輸入型參數,這也是一個很良好的編程習慣!

函數的返回值

函數在運行結束時如果需要返回運行結果,我們通常使用return關鍵字,如return 0或return -1等等,

當然我們也可以返回數據。這裡有人就會疑問返回值和輸出型參數都能返回數據,那有啥區別呢?我什麼情況下用返回值,什麼情況使用輸出型返回數據呢?

其實這兩種方式都可以返回函數的結果或數據,都是OK,沒問題的。但是在複雜函數處理中,函數返回的數據並不可靠,比如在函數執行過程中,已經不滿足條件,退出了函數執行,這時候返回的數據可能就不是正確的數據。 因此推薦的做法:利用返回值返回函數執行情況,用輸出型參數返回函數加工處理的數據。具體做法:先判斷函數返回值是否正常,比如return 0表示函數正常執行退出,rerurn -1表示函數異常退出, 若是正常情況下, 再使用輸出參數中的數據,反之,丟棄輸出型參數的數據。

通過以下例子感受下:

<code>#include <stdio.h>

int test(const int a, const int b, int *c)
{
\tif(a > 100)
\t\treturn -1;
\tif(b > 100)
\t\treturn -2;
\t
\t*c = a + b;
\t
\treturn 0;
}


int main(void)
{
\tint c = 0, d = 0;
\t
\tint ret = test(3, 5, &c);
\tif(ret == 0)
\t{
\t\td = c;
\t}
\telse if(ret == -1)
\t{
\t\tprintf("參數1不符號條件!\\n");
\t}
\telse if(ret == -2)
\t{
\t\tprintf("參數2不符號條件!\\n");
\t}
\t\t
\tprintf("d = %d.\\n", d);
\t
\treturn 0;
}/<stdio.h>/<code>

例子介紹: 我們定義的test函數,其作用是將100以內的兩數相加, 如果輸入型參數a, b不滿足條件,則直接退出,並分別返回各自的錯誤碼, 若滿足條件,我們將輸出型參數c的值賦值給變量d,並打印出來。

使用test(3, 5, c)測試, 運行結果:

C語言學習篇(16)-----函數組成詳解

test(3, 5, c)測試結果

test(101, 5, c)測試, 運行結果:

C語言學習篇(16)-----函數組成詳解

test(101, 5 , c)測試結果

test(3, 101, c)測試, 運行結果:

C語言學習篇(16)-----函數組成詳解

test(3, 101, c)測試結果

好了,今天的分享到此,感興趣的小夥伴的評論留下你的看法或是感興趣的知識點,之後我會挑出來逐一分析並分享~


分享到:


相關文章: