C/C++之函数
Tips:
?1. 本人當初學習C/C++的記錄。
?2. 資源很多都是來自網上的,如有版權請及時告知!
?3. 可能會有些錯誤。如果看到,希望能指出,以此共勉!
??說到函數,第一個要說的就是main函數,他是C/C++標準規定的!main的形式如下:
/* 形式一*/ int main(int argc, char *argv[]) {statements;return 0; } /* 形式二*/ int main(void) {statements;return 0; } /* 形式三*/ void main() {statements;return; } /* 形式四*/ main() {statements;return; }??上面這些形式,是比較常見的幾種main函數的形式,下面來詳細解釋一下。關于main函數,最詳細的說明當然是C/C++的標準文檔,以下內容是對標準文檔的記錄。ANSI C89和C90、C99和C11以及C++標準各個版本之間沒有實質變化,只有一些細節上的東西有些區別。
C 標準
??首先是最后一種:這在C89標準中允許出現,Brian W. Kernighan 和Dennis M. Ritchie 的經典巨著 The C programming Language(《C 程序設計語言》)用的就是main( )這種形式。這主要是因為C語言誕生初期,只有一種類型,那就是int,沒有char、long、float等。既然只有一種類型,那么就可以不寫,后來的C標準為了兼容以前的代碼于是規定:不明確標明返回值的,默認返回值為int,也就是說 main()等同于int main(),而不是等同于void main()。但是,該條規則,從C99開始,就不在支持了!
??在C99中,第5.1.2.2.1 Program startup節,指出了main函數的定義:
??The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters:
int main(void) { /* ... */ }
or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):
int main(int argc, char *argv[]) { /* ... */ }
or equivalent;9) or in some other implementation-defined manner.
9) Thus, int can be replaced by a typedef name defined as int, or the type of argv can be written as char ** argv, and so on
如果main函數的末尾沒寫return語句,C99 規定編譯器要自動在生成的目標文件中加入 return 0
??第三種:關于第三種,返回值為void的,C++之父 Bjarne Stroustrup 在他的主頁上的 FAQ 中明確地寫著 The definition void main( ) { /* …… */ } is not and never has been C++, nor has it even been C。至于出現的原因,可能是由于在 C 和 C++ 中,不接收任何參數也不返回任何信息的函數原型為“void foo(void);”。但是,這條規則并不適用于main函數。事實上,void main()在C仍然可以是符合標準(conforming)的擴展,為什么呢?因為or in some other implementation-defined manner這句的存在。(ISO C/C++中,根據對環境的要求,分為兩類:一類是獨立實現(freestanding implementation),另一類是宿主實現(hosted implementation),不同的實現環境是有區別的,更詳細的就只能看標準了。)
C++標準
??在C++98中,第3.6.1 Main function節,指出了main函數的定義:
??An implementation shall not predefine the main function. This function shall not be overloaded. It shall have a return type of type int, but otherwise its type is implementationdefined.
All implementations shall allow both of the following definitions of main:
int main() { /* ... */ }
and
int main(int argc, char* argv[]) { /* ... */ }
- main 函數的返回值類型也必須是int。如果main函數的末尾沒寫return語句,C++98 規定編譯器要自動在生成的目標文件中加入 return 0
全局main禁止被使用。因此不像C,C++中main無法遞歸調用。&::main也是錯誤的
再來說說函數返回值:
??C語言的(void)或函數定義中的()表示不接受任何參數,相當于C++的(),也和C++的(void)等價。C語言的()在函數定義外表示接受任何參數,相當于C++的(…)。所以在C語言中,聲明時最好不要省略(void)中的void,要是省略就不是預期想要的函數原型了。在定義中可以使用(),如int main(){},同int main(void){}。但若要保證聲明和定義通用,只用(void)表示函數沒有參數。
??而C++中,不接受任何參數的參數列表寫成(void)是不必要的。C++中函數參數空著和寫void等效。C語言中,main函數省略返回值相當于返回int,但是在C++中,不再主張這樣用!
- 關于注釋,/**/是C風格的注釋;// 是C++風格注釋。現在,C及C++同時支持這兩種注釋風格
- 關于頭文件,xx.h是C和C++舊風格;C++新風格沒有擴展名(iostream),原來舊的C頭文件以c加原文件名組成(cmath)
- 在舊的C標準中,變量的定義必須在函數開始的位置(C99支持隨時定義),而C++中,習慣做法是在使用的地方定義!
- C++中,可以使用連續賦值(a = b = c = 10 這個語句從右向左執行),C中不允許這樣做
- C++和C一樣,不允許嵌套定義函數(可以嵌套聲明),每個函數都必須是獨立的!
函數定義
??函數分為有返回值和沒有返回值兩大類。
void functionName(parameterlist) // 無返回值 { statements; return; // 可以省略 }typeName functionName(parameterlist) // 有返回值 { statements; return value; // value的類型為typeName類型 }??C和C++對于返回值類型有一定的限制:不能是數組。但可以是其他任何類型:整型、浮點型、指針、結構、對象。(但是,數組可以作為結構或對象的組成部分被返回)。通常,函數將返回值放到寄存器或者內存中進行返回。
函數原型(聲明)
C++函數原型與ANSI C函數原型
?ANSI C借鑒了C++中函數原型,但是兩者是有區別的。其中,最大的區別就是:
??1. 為了與C兼容,ANSI C中的函數原型是可選的,但是,在C++中是必不可少的。
??2. 在C++中形參列表為空與寫void是等效的——意味著函數沒有參數。在ANSI C中,形參為空意味著不指明參數——意味著將在函數定義中給出形參列表。
??3. 在C++中,不指明形參列表時,需要使用省略號 … 。例如:void func(…); 通常,只有在需要與可變參數的C函數(如:printf)交互時,才需要這樣做。
4.
為什么需要函數原型(聲明)
??原型提供了函數到編譯器的接口。原型告訴編譯器函數有什么類型的返回值、參數類型和數量。方便編譯器捕捉錯誤。關于編譯器為什么不直接取cpp文件找函數定義,原因之一就是這樣效率太低;再一個,函數的定義可能不在一個文件中或者定義在一個動態庫中。
函數的形參和返回值
??參數的形參和返回值可以是基本類型、結構體變量、類變量、指針變量、引用等,變量的傳遞方式可以分為值傳遞和指針傳遞。需要重點說明的是引用型形參以及引用型返回值。
函數的返回值
返回值的隱式轉換
??C和C++中同樣:如果函數的實際返回值和聲明的類型不一致,那么實際返回值將被自動轉換為函數聲明的返回類型。(超出范圍,將可能出錯)
VC編譯器會給出如上圖所示的警告!
返回值為引用的函數(僅C++)
類型標識符 &函數名(形參列表及類型說明) {//函數體 }??好處:在內存中不產生被返回值的副本;(注意:正是因為這點原因,所以返回一個局部變量的引用是不可取的。因為隨著該局部變量生存期的結束,相應的引用也會失效,產生runtime error!
注意:
1. 不能返回局部變量的引用。這條可以參照Effective C++[1]的Item 31。主要原因是局部變量會在函數返回后被銷毀,因此被返回的引用就成為了”無所指”的引用,程序會進入未知狀態。
2. 不能返回函數內部new分配的內存的引用。 這條可以參照Effective C++[1]的Item 31。雖然不存在局部變量的被動銷毀問題,可對于這種情況(返回函數內部new分配內存的引用),又面臨其它尷尬局面。例如,被函數返回的引用只是作為一個臨時變量出現,而沒有被賦予一個實際的變量,那么這個引用所指向的空間(由new分配)就無法釋放,造成memory leak。
3. 可以返回類成員的引用,但最好是const。 這條原則可以參照Effective C++[1]的Item 30。主要原因是當對象的屬性是與某種業務規則(business rule)相關聯的時候,其賦值常常與某些其它屬性或者對象的狀態有關,因此有必要將賦值操作封裝在一個業務規則當中。如果其它對象可以獲得該屬性的非常量引用(或指針),那么對該屬性的單純賦值就會破壞業務規則的完整性。
4. 流操作符重載返回值聲明為“引用”的作用:
流操作符<<和>>,這兩個操作符常常希望被連續使用,例如:cout <<”hello” << endl; 因此這兩個操作符的返回值應該是一個仍然支持這兩個操作符的流引用。可選的其它方案包括:返回一個流對象和返回一個流對象指針。但是對于返回一個流對象,程序必須重新(拷貝)構造一個新的流對象,也就是說,連續的兩個<<操作符實際上是針對不同對象的!這無法讓人接受。對于返回一個流指針則不能連續使用<<操作符。 因此,返回一個流對象引用是惟一選擇。這個唯一選擇很關鍵,它說明了引用的重要性以及無可替代性。 賦值操作符 = 和流操作符一樣,是可以連續使用的,例如:x = j = 10;或者(x=10)=100;賦值操作符的返回值必須是一個左值,以便可以被繼續賦值。因此引用成了這個操作符的惟一返回值選擇。
5. 在另外的一些操作符中,卻千萬不能返回引用:+-*/ 四則運算符。它們不能返回引用,Effective C++[1]的Item23詳細的討論了這個問題。主要原因是這四個操作符沒有side effect,因此,它們必須構造一個對象作為返回值,可選的方案包括:返回一個對象、返回一個局部變量的引用,返回一個new分配的對象的引用、返回一 個靜態對象引用。根據前面提到的引用作為返回值的三個規則,第2、3兩個方案都被否決了。靜態對象的引用又因為((a+b) == (c+d))會永遠為true而導致錯誤。所以可選的只剩下返回一個對象了。
形參
形參的隱式轉換
??C和C++中同樣:將一個表達式作為實參傳遞給函數調用,此時形參和實參類型不一致:目標轉換類型為形參的類型
同4.1.1的返回值情況,編譯器會給出警告!
引用型形參和臨時變量(僅C++)
??如果實參和引用形參類型不匹配,C++將產生臨時變量,當且僅當引用形參為const時。如果形參為const 引用,當出現以下兩種情況時,C++將產生臨時變量:
實參類型不正確,但是可以轉化為正確的類型
如果變量為基本類型,盡量不要使用引用形參,而是使用按值傳遞的方式;當數據比較大(類、結構體)時,使用引用傳遞較好。
形參的sizeof問題(C和C++同樣)
函數形參在使用sizeof()取大小時,有許多需要注意的地方。
參數及返回值的傳遞問題
??C語言函數參數和返回值的傳遞方式有:值傳遞和指針傳遞兩種,C++函數又增加了一種:引用傳遞。
??值傳遞:形參是實參的拷貝,改變形參的值并不會影響外部實參的值。從被調用函數的角度來說,值傳遞是單向的(實參->形參),參數的值只能傳入,不能傳出。當函數內部需要修改參數,并且不希望這個改變影響調用者時,采用值傳遞。
??指針傳遞:形參為指向實參地址的指針,當對形參的指向操作時,就相當于對實參本身進行的操作
??(C++)引用傳遞:形參相當于是實參的“別名”,對形參的操作其實就是對實參的操作,在引用傳遞過程中,被調函數的形式參數雖然也作為局部變量在棧中開辟了內存空間,但是這時存放的是由主調函數放進來的實參變量的地址。被調函數對形參的任何操作都被處理成間接尋址,即通過棧中存放的地址訪問主調函數中的實參變量。正因為如此,被調函數對形參做的任何操作都影響了主調函數中的實參變量。
??實際上,這里的指針傳遞可以歸為值傳遞,下面給出解釋。在Pascal語言中,形參有兩種:值形參和變量形參,前者是值傳遞方式,后者是指針傳遞方式。在C語言中,只有“值形參”而無“變量形參”,全部采用值傳遞方式。C++把引用型變量作為函數形參,就彌補了這個不足。
從編譯的角度來闡述它們之間的區別:
??程序在編譯時分別將指針和引用添加到符號表上,符號表上記錄的是變量名及變量所對應地址。指針變量在符號表上對應的地址值為指針變量的地址值,而引用在符號表上對應的地址值為引用對象的地址值。符號表生成后就不會再改,因此指針可以改變其指向的對象(指針變量中的值可以改),而引用對象則不能修改。
參數傳遞內存問題
??函數調用過程中,必然存在著內存的開辟與釋放。處理不好往往就會造成內存泄露,甚至程序崩潰。
上圖中,用到了指針的引用。注意指針的引用的寫法。
- 千萬別返回指向“棧內存”的指針、引用、包括數組,因為該內存在函數結束時自動消亡了,返回的指針是個野指針了。
- 可以返回指向堆的指針和引用,但是不推薦這樣做。上圖中,返回malloc分配的內存,因為這是在堆上分配的,如果程序員不去釋放,將一直持續的程序結束,由操作系統回收,第二個函數,返回棧上的變量,是錯誤的!可以用函數返回值來傳遞動態內存,只能返回在堆上分配的內存、全局變量內存、靜態變量
指針函數與函數指針
??辨別指針函數與函數指針最簡單的方式就是看函數名前面的指針*號有沒有被括號包含,如果被包含就是函數指針,反之則是指針函數。
指針函數就是返回類型是某一類型的指針的函數,形式如下:
類型標識符 *函數名(參數表);
函數指針就是指向函數的指針變量,即本質是一個指針變量。形式如下:
類型說明符 (*函數名)(參數); 當然 也可以定義函數指針數組: 類型說明符 (*函數名[ ])(參數);
例如:我們定義一個函數指針數組:
定義和調用方式,如下圖:
地址跳轉(多出現在嵌入式C中)
再此之前,先看下面的例子:
??按照&運算符本來的意義,它要求其操作數是一個對象,但函數名不是對象(函數是一個對象),本來&func是非法的,但很久以前有些編譯器已經允許這樣做,c/c++標準的制定者出于對象的概念已經有所發展的緣故,也承認了&func的合法性。
??因此,對于func和&func可以這樣理解:func是函數的首地址,它的類型是void(),&func表示一個指向函數void func(void)這個對象的地址,它的類型是void(*)(),因此func和&func所代表的地址值是一樣的,但類型不一樣。func是一個函數,&func表達式的值是一個指針!
??這時,我們來看一道嵌入式筆試題:想讓程序跳轉到絕對地址0x100000處執行,該如何做?
??答案有兩種:(*(void(*)(void))0x100000)(); // 和上面的(*funcPtr)();是不是很像
或者((void(*)(void))0x100000)(); // 和上面的(funcPtr)(); 是不是很像
??首先,來看(void()(void),這就好比一個類型,就像int是指向int的指針,那么(void(*)(void) 就是指向 void func(void)這樣函數的函數指針。這樣,就回到了我們的函數指針上。
??將地址0x100000轉換時,其當然是沒有參數和返回值的,因此:(void()(void))0x100000 // 這就和(int)a類似了。
??與此同時,(void()(void)是指向函數的指針,轉換后自然帶有函數特性,因此:(void()(void))0x100000 就是個函數指針
??最后,我們調用這個函數指針:就是出現了上面兩種答案了。
??可能有人會問,上面函數指針調用中還有個funcPtr(),這是不是就意味著,(void(*)(void))0x100000() 這也是正確的呢?實際上,這是不對的。
??在很多嵌入式代碼中,我們常看到如下代碼:
void(*reset)(void) = (void(*)(void))0;
??其中,void(reset)(void)就是函數指針定義,(void()(void))0是強制類型轉換操作,將數值“0”強制轉換為函數指針地址“0”。通過調用reset()函數,程序就會跳轉到程序執行的“0”地址處重新執行。在一些其他高級單片機Bootloader中,如NBoot、UBoot、EBoot,經常通過這些Bootloader進行下載程序,然后通過函數指針跳轉到要執行程序的地址處。
??對于(*(void(*)(void))(0x30700000))();可以這樣理解:首先(void()(void))是一個強制類型轉換符,他將后面的0x30700000這個無符號整數強制轉化為一個函數指針,該函數指針所指向的函數入口參數為void,返回值也是void。如果到這步你看懂了,那么設(void()(void))(0x30700000)為fp;那么上面的表達式就可以簡化為(*fp)();
函數遞歸調用
??C++函數和C一樣,允許函數調用自己。(與C不同的是,C++不允許main調用自己)。遞歸函數必須要有結束條件,否者將一直調用下去。
內聯函數(C99添加)
??內聯函數是C++為了提高程序執行速度而做的改進(節省時間但是消耗空間),常規函數與內聯函數的區別主要在于C++編譯器如何將他們組合到程序中。使用內聯函數特性必須符合以下條件之一:
- 在函數聲明中加上關鍵字inline
- 在函數定義中加上關鍵字inline
??C++內聯函數用來取代C語言中的宏函數。內聯函數的參數傳遞方式為:值傳遞;宏函數則是文字替換。
C++函數重載
什么是函數重載
??函數重載是指在同一作用域內,可以有一組具有相同函數名,不同參數列表的函數,這組函數被稱為重載函數。重載函數通常用來命名一組功能相似的函數,這樣做減少了函數名的數量,避免了名字空間的污染,對于程序的可讀性有很大的好處。
為什么需要函數重載
- 如果沒有函數重載機制,如在C中,你必須要這樣去做:為這個print函數取不同的名字,如print_int、print_string。這里還只是兩個的情況,如果是很多個的話,就需要為實現同一個功能的函數取很多個名字,如加入打印long型、char*、各種類型的數組等等。這樣做很不友好!
- 類的構造函數跟類名相同,也就是說:構造函數都同名。如果沒有函數重載機制,要想實例化不同的對象,那是相當的麻煩!
- 操作符重載,本質上就是函數重載,它大大豐富了已有操作符的含義,方便使用,如+可用于連接字符串等!
如何實現函數重載
??函數重載的關鍵是函數的參數列表,如果參數的個數不同或者類型不同就可實現重載。注意引用形參和普通形參不能重載。注意:編譯器將類型引用和類型視作相同類型,不能重載。
C++函數重寫、重載、隱藏的區別
重寫(override)
?覆蓋也稱重寫,英文override,重寫(覆蓋)的規則:
作用:
??基類指針和引用在調用對應方法時,根據所指對象類型實現動態綁定。
重載(overload)
??overload是重載,一般是用于在一個類內實現若干重載的方法,這些方法的名稱相同而參數形式不同。重載的規則:
作用:
??同一方法,根據傳遞消息的不同(類型或個數),產生不同的動作(相同方法名,實現不同)。
隱藏
總結
- 上一篇: C/C++之常用关键字
- 下一篇: C/C++之 C++ String(字符