C\C++語言7|函數、庫、內聯函數

在程序中,將一段代碼封裝起來,在需要的時候可以直接調用,這些代碼可以完成一定的功能和操作,並且可以操縱參數,這就是函數。函數是程序代碼最主要的組成部分之一,它將完整的程序分為不同的程序塊,有著不同的返回結果。

把相關的語句組合在一起,並且賦予相應的名稱,然後用這種方法來給程序分塊,這種形式的組合就稱之為函數,函數有時候也被稱為例程或者過程。

函數可以看作是由程序員來定義的操作,是劃分程序的各個程序塊,與內置操作符(一種特殊的函數)相同的是,每個函數都會實現一系列的計算,然後(大多數時候)生成一個計算結果。但與操作符不同的是,函數有自己的函數名,而且操作數沒有數量限制。與操作符一樣,函數可以重載(C++),這意味著同樣的函數名可以對應多個不同的函數。

由程序員來編寫完成指定任務的函數是用戶定義的函數。標準函數庫是C提供的可以在任何程序中使用的公共函數,而程序總是從main()函數開始啟動的。

函數由函數名以及一組操作數(形參)類型唯一地表示。函數的操作數,也即形參,在一對圓括號中聲明,形參與形參之間以逗號進行分隔。函數執行的運算在一個稱為函數體的塊語句中定義。每一個函數都必須有一個相關聯的返回類型,定義或者聲明函數時,沒有顯示指定函數的返回類型是不合法的。

在任何編程語言中都會使用函數概念,因為函數結構容易理解,方便調試,函數中的變量都是私有局部變量,是對數據的一種保護,函數之間要通信,需要使用指針或引用。通常,如果不想修改指針或引用對應的數據,通常不把其作為左值,如果想在函數體中更新其值,通常用額外的臨時變量來實現。如果不想改變指針或引用對應的數據,可以用const修飾為只讀。

函數是程序設計語言中最重要的部分,是模塊化設計的主要工具。每一個程序都要用到函數。

即使你自己不定義新的函數, 在每一個完整的C程序中都必須有一個main() 函數。

在C語言中,字符處理、字符串處理和數學計算都是用函數的方式提供的。

函數站在函數設計的角度來看,需要考慮函數的定義,站在函數使用者的角度來看,需要考慮函數的定義和調用;

庫函數在調用前需要#include相應的頭文件。

自定義的函數在調用時需要進行函數原型說明。

函數原型說明與函數首部寫法上需要保持一致,即函數類型、函數名、參數個數和參數順序必須相同。

如果被調函數的定義在主調函數之前,可以不必加聲明。

如果在所有函數定義之前,在函數外部已經做了函數聲明,則在主調函數中無須再作聲明。

1 函數原型與接口、定義與實現

函數包括函數聲明、函數定義、函數調用,如果需要修改函數時,最好不需要三個方面都要修改,只需要修改一個地方而保持另外兩部分的穩定是最好的。如果接口設計良好的話,則只需要更改函數定義即可。

函數原型是為了方便編譯器查看程序中使用的函數是否正確,函數定義描述了函數如何工作。現代編程習慣是把程序要素分為接口部分和實現部分,例如函數原型和函數定義。接口部分描述瞭如何使用一個特性,也就是函數原型所在的;實現部分描述了具體的行為,這正是函數定義所做的。

函數的聲明就類似於變量的聲明:

存儲類型標識符 數據類型標識符 函數名(形式參數列表及類型數據);

我們知道數組名實際上是數組第一個元素在內存中的地址。類似地,函數名實際上是執行這個函數任務在內存中的開始地址。

因為是函數,所以有另外的參數列表。

1.1 庫:接口與實現的分離

函數原型的聲明是為了在多文件的項目中,讓項目的語法符合“多次聲明,一次定義”的要求。

把函數接口是寫給使用者看的,當使用者與實現者分離時,就特別有用。

設計庫的接口:

庫的用戶必須瞭解的內容,包括庫中函數的原型、這些函數用到的符號常量和自定義類型。

接口表現為一個頭文件。

設計庫中的函數的實現:表現為一個源文件。

庫的這種實現方法稱為信息隱藏。

為什麼要使用庫?

庫可以實現代碼重用。某個項目中各個程序員需要共享一組工具函數時可以將這組函數組成一個庫,這些函數的代碼在項目中得到了重用。如果另一個項目中也許要這樣的一組工具函數,那麼這個項目的程序員就不必重新編寫這些函數而可以直接使用這個庫,這樣這組代碼在多個項目中得到了重用。

調用庫函數要包含文件,調用自定義函數也是如此,當然頭文件也可以是包含一些全局常量。

1.2 函數原型聲明和函數定義的區別。

函數原型聲明只是說明了該函數應該如何使用,函數調用時應該給它傳遞哪些數據,函數調用的結果又應該如何使用。函數定義除了給出函數的使用信息外,還需要給出了函數如何實現預期功能,即如何從輸入得到輸出的完整過程。

作為一名程序開發人員,不可能每次編寫都從最底層開發。如要輸入一串字符到輸出設備上,我們需要做的僅是調用printf()函數,至於其參數是怎樣顯示的,我們並不關心。

2 參數傳遞

在聲明函數時,參數列表部分可以不指定參數的名稱,但是必須指定參數的類型。

如果函數具有多個參數,需要為某些參數提供默認值時(C++),要保證默認值參數應位於非默認值參數的右方,否則將導致編譯錯誤。

如果參數的數據類型是指針類型、引用類型或數組類型,則函數是引用傳遞,其他情況下是值傳遞。

函數的參數一般可以看成是函數運行時的輸入。形式參數指出函數調用時應該給它傳遞幾個數據,這些數據是什麼類型的。實際參數是函數某次調用時的真正的輸入數據,是形式參數的初值。

函數調用做了兩件事情:用對應的實參初始化函數的形參(創建變量並賦值),並將控制權轉移給被調用函數。主調函數的執行被掛起,被調函數開始執行。函數的運行以形參的(隱式)定義和初始化開始。

函數的按值傳遞或引用、指針傳遞,區別在於是否在函數體內改變參數的情形下。

函數參數除了數組以外默認是以值來傳遞?為什麼是這樣,為了數據保護的需要,而數組為什麼不是值傳遞而是按址傳遞?因為數組包含的數據如果過多時,特別是當數組元素是對象時,所需要的空間和時間都可能會很大,所以,傳址就有優勢了。

2.1 值傳遞

在值傳遞中,形式參數有自己的存儲空間,實際參數是形式參數的初值。參數傳遞完成後,形式參數和實際參數再無任何關聯。

在“傳值調用”方式下,一個函數只能產生一個返回值,這個返回值是通過return語句傳遞迴主調用函數中的。如果在一個函數中需要產生一個以上的結果值,可以通過使用全局變量的方式。

在C語言中,函數的參數是“按值”傳遞的。假設使用變量num作為參數調用函數test。

test(num);

num的值被複制到一個臨時位置,這個位置被傳遞到test。在這種情況下,test無法訪問原始參數num,因此無法以任何方式更改它。

這是否意味著一個函數永遠不能改變另一個函數中變量的值?它可以,但要做到這一點,它必須能夠訪問變量的地址——存儲變量的內存中的位置(指針或引用)。

2.2 址傳遞

傳址的實質也是傳值,只是其值是一個地址值,函數體內操作的是地址值指向的數據。地址值可以由指針傳遞,也可以由引用來傳遞。

引用的本質是一個由編譯器實現瞭解引用的指針常量,所以使用引用時就如同使用變量一樣,而使用指針的值則需要程序員自己解引用。

值傳遞是因為變量之間沒有關聯。而引用或指針傳遞是因為引用只是變量的別名,而指針是指向某個變量,所以變量之間有關係,這樣在修改函數參數時,沒有關聯的就沒有影響,而有關聯的就有影響。

傳遞一個數組為什麼需要兩個參數?

因為數組傳遞本質上只是傳遞了數組的起始地址,數組中的元素個數需要另一個變量來指出。

編寫一個比較兩個 string 對象長度的函數作為例子。這個函數需要訪問每個 string 對象的 size,但不必修改這些對象。由於 string 對象可能相當長,所以我們希望避免複製操作。使用 const 引用就可避免複製:

 // compare the length of two strings
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}

其每一個形參都是 const string 類型的引用。因為形參是引用,所以不復制實參。又因為形參是 const 引用,所以 isShorter 函數不能使用該引用來修改實參。

值傳遞作為函數的輸入;
指針或引用傳遞作為函數的輸出;

3 函數返回值與指針函數

函數的返回值可以是基本類型,也可以是自定義類型。當然也可以是指針。指針函數,是指返回一個指針的函數:

1) 可以返回賦值是參數指針或引用的變量;
2) 可以返回堆中申請的動態變量;
3) 不要返回局部變量地址(一個原則就是在返回調用結束後其值需要仍然存在)。

函數如何返回一個指針?

1) 參數有一指針pc,在函數內再新建一指針p,p=pc,再返回p;

2) 不考慮參數是否有指針,在函數內用指針p動態申請(malloc或new)一塊內存,再返回p。

函數的返回也可以是一個引用,當返回一個引用時,可以用其做左值,如返回數組元素的下標引用。

4 函數調用

函數調用有三種形式:

1) 把函數調用作為一條語句;

2) 函數調用出現在一個表達式中;

3) 函數調用作為一個函數的實際參數;

函數被調用時,系統為每個形參分配內存單元,也就是相當於一個局部變量的聲明後的初始化。

函數可以通過函數指針被調用,被調用的函數稱為回調函數。

5 函數執行過程

在主程序中計算每個實際參數值。

將實際參數賦給對應的形式參數。在賦值的過程中完成自動類型轉換。

依次執行函數體的每個語句,直到遇見return語句或函數體結束

return後面的表達式的值,如果表達式的值與函數的返回類型不一致,則完成類型的轉換。

用函數的返回值置換函數,繼續主程序的執行

形參:函數定義時定義的參數;實參:函數調用時定義的參數;

6 函數之間的通信和數據共享

參數和返回值(如果參數是指針或引用,則參數即是輸入也是輸出,因為他形成了同函數外數據的修改);

通常,函數調用都有一定的開銷,因為函數的調用過程包括建立調用、傳遞參數、跳轉到函數代碼並返回。使用宏使代碼內聯,可以避免這樣的開銷。

函數之間實現數據共享有以下幾種方式:局部變量、全局變量、類的數據成員、類的靜態成員和友元。如何共享局部變量呢?可以在主調函數和被調函數之間通過參數傳遞來共享。全局變量具有文件作用域,所以作用域中的各個函數都能共享全局變量。類的數據成員具有類作用域,能夠被類的函數成員共享。

函數之間共享數據也就是此函數訪問彼函數的數據主要是通過局部變量(參數傳遞)、全局變量、類的數據成員(數據成員可以被同一個類中的所有函數成員訪問)、類的靜態成員(類的所有對象共享)及友元(某個普通函數或者類的成員函數可以訪問某個類中的私有數據)實現的。

數據的封裝實現了數據的隱藏,讓數據更安全,但是前面講到的通過局部變量、全局變量、類的數據成員、類的靜態成員及友元實現了數據的共享,這樣又降低了數據的安全性。有些數據是需要共享而又不能被改變的,那麼這時候我們就要將其聲明為常量。

因為函數在執行時要依賴於其所在的外部變量。如果將一個函數移到另一個文件中,還要將有關的外部變量及其值一起移過去。但若該外部變量與其他文件的變量同名時,就會出現問題,降低了程序的可靠性和通用性。一般要求把C程序中的函數做成一個封閉體,除了可以通過“實參——形參”的渠道與外界發生聯繫外,沒有其他渠道。

7 遞歸函數

遞歸程序設計:將一個大問題簡化為同樣形式的較小問題。

在一個遞歸求解中,分解的子問題與最初的問題具有一樣的形式

作為處理問題的工具,遞歸技術是一種非常有力的工具。利用遞歸不但可以使得書寫複雜度降低,而且使程序看上去更加美觀

遞歸調用:在一個函數中直接或間接地調用函數本身

必須有遞歸終止的條件

函數決定終止的參數有規略地遞增或遞減

有可對函數的入口進行測試的基本情況。

if (條件)
return (不需要遞歸的簡單答案);
else
return (遞歸調用同一函數);

對於大多數常用的遞歸都有簡單、等價的迭代程序。究竟使用哪一種,憑你的經驗選擇。

迭代程序複雜,但效率高。

遞歸程序邏輯清晰,但往往效率較低。

8 內聯函數

C99還提供另一種方法:內聯函數(inline function)。

C++語言支持函數內聯,其目的是為了提高執行效率。對於任何內聯函數,編譯器在符號表裡放入函數的聲明(包括名字、參數類型、返回值類型)。如果編譯器沒有發現內聯函數存在錯誤,那麼該函數的代碼也被放入符號表裡。在調用內聯函數時,編譯器直接用內聯函數的代碼替換函數調用,於是省去了函數調用的開銷。

在內聯函數內不允許用循環語句和開關語句。如果內聯函數有這些語句,則編譯將該函數視同普通函數那樣產生函數調用代碼,遞歸函數是不能被用來做內聯函數的。內聯函數只適合於只有1~5行的小函數。對一個含有許多語句的大函數,函數調用和返回的開銷相對來說微不足道,所以也沒有必要用內聯函數實現。 內聯函數的定義必須出現在內聯函數第一次被調用之前。

C++ 提供了兩種特殊的函數:內聯函數和成員函數。將函數指定為內聯是建議編譯器在調用點直接把函數代碼展開。內聯函數避免了調用函數的代價。成員函數則是身為類成員的函數。

對於類,如果在聲明的同時即定義了函數,即使沒有使用關鍵字inline,編譯器也將其做內聯函數處理。

9 庫函數

system()會調用shell,而exec()則不會調用shell。system是在單獨的進程中執行命令,執行完畢還會回到程序中;而exec()則直接在進程中執行新的程序,新的程序會把原程序覆蓋,除非調用出錯,否則再也回不到exec()函數後面的代碼。

system()會產生新的pid(生成新的shell),而exec()則不會。

C下面的頭文件用於處理字符函數,如strstr()、strncat()、strncpy()等;

#include "string.h"

在開發應用程序時,通常將函數的聲明放置在頭文件(.h文件)中,將函數的定義放置在源文件中(.cpp文件)。這樣,如果需要在其他文件中訪問函數,只需要引用該函數聲明的頭文件就可以了。

頭文件的內容一般包含一些公用的或常用的宏定義、構造型數據類型的定義以及全局變量的定義等。當源文件需要這些定義時,不必重新定義,只需要把所對應的頭文件包含進來,相當於把頭文件的全部內容插到當前源文件的開頭,合二為一後再進行統一的編譯。

在調用系統庫函數時,要在源文件的開頭把庫函數對應的頭文件包含進來,這樣庫函數才能被正常調用。一般屬於同一類型的庫函數對應一個頭文件。比如:數學類函數的頭文件是math.h,字符類函數的頭文件是ctype.h,字符串類函數的頭文件是string.h,輸入輸出類函數的頭文件是stdio.h。

10 函數指針

定義一個函數指針:

void (*pFunc)(int);

如果要定義多個同一類型的指針,還可以使用typedef定義一種新的函數指針的數據類型:

	typedef void(*PFUNC)(int);

這樣就可以使用這種新的數據類型定義函數指針:

	PFUNC pFunc1;
PFUNC pFunc2;

這些函數指針可以指向多個相同類型的函數。

函數指針可以使用函數名來賦值,如有一func()函數:

	pFunc1 = func(); //前面也可以添加&

用函數指針實現回調函數289/pdf304

回調函數就是函數指針指向的函數。

主調函數使用函數指針作為參數,相當於就是將回調函數嵌入到了主調函數之中。

回調函數可以實現算法的通用性。例如排序算法,你可以定義好算法的通用框架,至於其中核心的算法邏輯,則留待回調函數去完成,用戶可以通過不同的回調函數,輕鬆簡單地實現各種算法,對算法進行自定義。

函數指針數組:

	double (*p[3])()={sin,cos,tan};

11 其它

函數指針加入結構體中可以實現簡單的“方法”。

main()函數對於變量定義的作用域與其它函數是一樣的,唯一不同的是,它是整個程序的唯一入口和出口,不需要原型聲明。是程序與操作系統交互的界面。

全局變量可以在不同函數之間共享數據,但另一方面,函數中因為使用了全局變量,也讓函數的獨立性大大降低了。

函數作用域的概念跟變量的存儲位置和生命期有關。

如果函數嵌套兩層以上,建立用函數的形式解決嵌套層次太多的問題。

C++中的函數可以重載,並有泛型編程的函數模板機制,詳情見後面的文章。

-End-

相關推薦

推薦中...