文章目錄
-
- 函數基礎
- 參數傳遞
-
- const形參和實參
- 數組形參
- 可變形參
- 傳回類型
-
- 傳回數組指針
- 函數重載
-
- 函數比對
- 特殊用途語言特性
-
- 預設實參
- 内聯函數
- constexpr函數
- 調試幫助
- 函數指針
函數基礎
- 實參傳給形參的規則參見用右值初始化左值。是以,(舉例)如果有一個形參為
,則必須傳入一個能轉換成int
的實參(整型、浮點型)。int
- 在C++中,名字有作用域,對象有生命周期。一旦函數終止,形參就會被銷毀。函數内部的變量也必須被初始化,否則就會被預設初始化為未定義的值。
- 局部靜态對象(
)直到程式終止才會被銷毀。内置類型的局部靜态變量可以被隐式地初始化為0。static
- 在函數的聲明中可以省略形參的名字。但還是建議把名字寫上,便于讀者了解形參含義。含有函數聲明的頭檔案應該被包括到函數的源檔案中。
參數傳遞
- 每次調用函數都會重新建立形參,并用實參初始化形參。當形參是引用類型時,它對應的實參被引用傳遞;當實參的值被拷貝給形參時,實參被值傳遞。
-
在C++中,建議使用引用類型的形參代替指針。void reset(int *ip) { *ip = 0; //改變了ip所指對象的值 ip = 0; //并不改變指針ip的值 } reset(&i); //調用 //如果形參是引用,調用時就隻用寫reset(i)
- 傳引用參數的作用:
- 使用引用避免拷貝。當某種類型不支援拷貝操作、或需要拷貝的對象可能非常大(如
類型)時,應使用引用形參。如果無需改變實參内容,應使用string
。const
- 使用引用形參傳回額外資訊。有時函數需要多個傳回值,引用形參為我們一次傳回多個結果提供了有效的途徑(你懂的)。
- 使用引用避免拷貝。當某種類型不支援拷貝操作、或需要拷貝的對象可能非常大(如
const形參和實參
形參的初始化方式和變量的初始化方式是一樣的,回顧通用的初始化規則有助于了解本節知識。
- 當用實參初始化形參時,會忽略掉頂層
(作用于對象本身的常量屬性)。是以給形參添加頂層const
屬性并不能構成函數重載(因為傳入這兩個函數的實參可以完全一樣:常量或非常量)。const
- 可以使用非常量初始化底層
對象,但反之則不行(引用同理)。const
- 一個普通的引用必須用同類型的對象初始化。不能把普通引用綁定到字面值上,不過常量引用可以(或者就一個
型)。string
- 我們為什麼要把函數不會改變的形參定義為常量引用?除了提醒人們函數内不能改變形參,還有兩個更為隐蔽的原因:
- 不能把
對象、字面值或需要類型轉換的對象傳給普通引用形參:const
string::size_type find_char(string &s, char c, string::size_type &occurs); find_char("Hello world!", 'o', ctr); //錯誤!
- 如果其他函數正确地将其形參定義為常量引用,則(如find_char函數)無法在此類函數中正常使用:
bool is_sentence(const stirng &s) { string::size_type ctr = 0; return find_char(s, '.', ctr) == s.size()-1 && ctr == 1; //錯誤! }
- 不能把
數組形參
-
上述三個函數聲明是等價的。編譯器隻檢查傳入的參數是否是void print(const int *); void print(const int[]); void print(const int[10]);
類型,而不關心數組的次元。數組的大小對函數的調用沒有影響!const int*
- 確定數組不越界的三種技術:
- **使用标記指定數組長度。**适用于C風格字元串(在最後一個字元後面跟着一個空字元)。例如:循環條件為指針不為空。
- **使用标準庫規範。**傳遞指向數組首元素和尾元素的指針:
void print(const int *beg, const int *end) { while (beg != end) cout << *beg++ << endl; //經典寫法 } int j[2] = {0,1}; print(begin(j), end(j)); //調用
- (回顧C程式的寫法)專門定義一個表示數組大小的形參。
當函數不需要對數組元素執行寫操作時,數組形參應該是指向const的指針。
- 數組引用作為形參時,因為次元是類型的一部分,是以必須寫上。
這一用法限制了print函數的可用性:隻能傳遞次元為10的數組。void print(int (&arr)[10]);
- 傳遞多元數組時,數組第二維(以及後面所有次元)的大小都是數組類型的一部分,不能省略。
void print(int (*matrix)[10], int rowSize); void print(int matrix[][10], int rowSize); //matrix都是指向含有10個整數的數組的指針
-
函數是了解C++程式如何向函數傳遞數組的好例子:main
argv[0]為空字元串或程式名字,非使用者輸入;最後一個指針之後的元素值保證為0(舉例:argv[5] = 0; 此時argc等于5)。int main(int argc, char *argv[]) {...}
可變形參
- 如果函數的實參數量未知,但全部實參的數量都相同,可以使用
類型的形參(定義在同名頭檔案中,常用于輸出程式産生的錯誤資訊):initializer_list
void error_msg(initializer_list<string> i1) { for (const auto &elem : i1) //用引用避免拷貝 cout << elem << endl; } //expected和actual是string對象 if (expected != actual) error_msg({"functionX", expected, actual}); //要有花括号來初始化initializer_list<string> else error_msg({"functionX", "okay"}); /* i1.size() * i1.begin() * i1.end() */
- 和
不一樣的是,vector
對象中的元素永遠是常量,我們無法改變它們。initializer_list
- 省略符形參
因該僅僅用于C和C++通用的類型。而且,大多數類型的對象在傳遞給省略符形參時,都無法正确拷貝(是以基本不要用)。...
傳回類型
- 傳回
的函數不要求非得有void
語句,因為在這類函數的最後一行會隐式地執行return
。當然,return
函數如果想在中間位置提前退出,可以使用void
。return
- 對于有傳回值的函數,在含有
語句的循環後面應該也有一條return
語句,沒有的話就是錯誤的;因為循環可能不被執行。return
- **不要傳回局部對象的引用或指針!!!**要確定引用所引的是在函數之前就存在的對象。一旦函數完成,局部對象就會被釋放。
- 調用運算符的優先級和點、箭頭運算符相同,且都滿足左結合律。是以,形如下面的語句是合法的:
auto sz = shorterString(s1, s2).size();
- 函數的傳回類型決定函數調用是否為左值。我們能為傳回類型是非常量引用的函數的結果指派。
- C++新标準允許清單初始化傳回值:
如果函數傳回内置類型,則花括号包圍的清單最多包含一個值。vector<string> process() { ... return {"stage1", "prepared"}; }
- 如果控制到達了
函數的結尾且沒有main
語句,編譯器将隐式地插入一條傳回0的return
語句。return
- 在遞歸調用中,一定有某條路徑是不包含遞歸調用的;否則,函數将不斷地調用它自身直到程式棧空間耗盡為止。
函數不能調用它自己喔。main
傳回數組指針
- 因為數組不能被拷貝,是以函數不能傳回數組,但是函數能傳回數組的指針或引用。最直接的方法是使用類型别名:
typedef int arr[10]; //using arr = int[10]; 等價聲明 arr* func(int i);
- 要想不使用類型别名,必須加上傳回類型的次元:
int (*p)[10] = &arr; //arr是int[10]型 int (*func(int i))[10]; //(*func(int i))意味着我們可以對函數調用的結果解引用 //(*func(int i))[10]表示解引用的結果是一個大小為10的數組
- C++新标準允許使用尾置傳回類型(适用于傳回類型較複雜的函數,如傳回類型是數組的指針或引用):
//在本應出現傳回類型的地方放一個auto auto func(int i) -> int(*)[10];
- 如果我們知道函數傳回的指針指向哪個數組,可以使用
聲明傳回類型:decltype
當數組被用作int odd[] = {1,3,5,7,9}; int even[] = {0,2,4,6,8}; decltype(odd) *func(int i) { return (i%2) ? &odd : &even; } //decltype(odd)是數組類型而不是數組指針,是以還要加上*
關鍵字的參數,或者作為取位址符decltype
、&
等運算符的對象時,數組不會被隐式地轉換成指針。sizzeof
函數重載
- 僅有傳回類型不同,不構成重載。
函數不能重載。main
- 如const形參和實參一節所說,頂層
不影響傳入函數的對象,是以參數的頂層const
屬性不能構成重載。另一方面,如果形參是某種類型的指針或引用,則通過區分其指向的是常量對象還是非常量對象可以實作重載。此時的const
是底層的。const
我們隻能把Record lookup(Phone*); Record lookup(Phone* const); //不構成重載 Record lookup(Phone*); Record lookup(const Phone*); //構成重載
對象或指向const
的指針傳給const
形參。而當我們傳遞一個非常量對象時,編譯器會優先使用非常量版本的函數。const
-
在函數重載中最有用:const_cast
const string &shorertString(const string &s1, const string &s2) { return s1.size()<s2.size() ? s1 : s2; } //常量版本函數(傳入常量,傳回常量) string &shorterString(string &s1, string &s2) { auto &r = shorterString( const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); } //非常量版本函數中,可以調用常量版本,精簡代碼
版本傳回對const
的引用,但這個引用事實上綁定在了非常量實參上,是以我們可以再将其轉換為一個普通的const string
,這顯然是安全的。string&
- **在不同的作用域中無法重載函數名。**通常來說,在局部作用域中聲明函數不是一個好的選擇。
函數比對
- 候選函數:與被調用函數同名、其聲明在調用點可見。可行函數:形參數量等于實參且類型相同(或能轉換)。
- 尋找最佳比對:如果有一個函數,每個實參的比對都不劣于其他可行函數需要的比對,且至少有一個參數優于其他可行函數提供的比對,則比對成功;否則調用具有二義性,是錯誤的。
- 所有類型轉換的級别都一樣。隻有當調用提供
類型的值,才會選擇short
版本的函數(否則選用short
版本)。int
特殊用途語言特性
預設實參
- 一旦某個形參被賦予了預設值,它後面的所有形參都必須有預設值。預設實參負責填補函數調用缺少的尾部實參。
- 當設計含有預設實參的函數時,要合理設定形參順序,盡量讓不怎麼使用預設值的形參出現在前面,而讓經常使用預設值的形參出現在後面。
- 通常,應該在函數聲明中指定預設形參,并将該聲明放在合适的預設形參中。函數的後續聲明隻能為之前沒有預設值的形參添加預設實參。
- 局部變量不能作為預設實參。除此之外,隻要表達式的類型能轉換成形參所需要的類型,該表達式就能作為預設形參:
int wd = 80;
string screen(int sz = wd);
void f2()
{
wd = 100;
}
//函數f2内改變了wd,是以對screen的調用會傳遞這個更新過的值
内聯函數
- 把規模較小的操作定義成函數,有利于確定行為的統一、便于修改、能夠被重複利用。
- 内聯函數(在傳回類型前加上
)能夠在調用點上内聯地展開,避免函數調用的開銷。inline
- 内聯機制用于優化規模較小、流程直接、頻繁調用的函數(往往不支援遞歸)。内聯函數通常定義在頭檔案中。
constexpr函數
-
函數(被隐式地指定為内聯函數)是指能用于常量表達式的函數:constexpr
規定:函數的傳回值類型、所有的形參類型必須是字面值類型;函數體中隻有一條constexpr int new_sz() {return 42;} constexpr int foo = new_sz;
語句。return
- 允許
函數的傳回值并非一個常量。constexpr
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; } //如果arg是常量表達式,scale(arg)也是常量表達式 int arr[scale(2)]; //正确:scale(2)是常量表達式 int i = 2; int arr[scale(i)]; //錯誤:scale(i)不是常量表達式
調試幫助
-
是一種預處理宏。定義在assert
頭檔案中,無需為其提供cassert
聲明。using
assert(expr); //如果表達式為假(即0),assert輸出資訊并終止程式運作 //如果表達式為真,assert什麼也不做
宏常用于檢查“不能發生”的條件。assert
- 如果在
檔案的一開始寫main.c
,可以關閉調試狀态;此時#define NDEBUG
将不執行運作時檢查。此外,assert
也可用于編寫自己的調試代碼:NDEBUG
void print() { #ifdef NDEBUG cerr << __func__ << endl; #endif }
是編譯器定義的一個局部靜态變量,用于存放函數的名字。除此之外,預處理器還定義了4個對程式調試很有幫助的名字:__func__
-
存放檔案名的字元串字面值__FILE__
-
存放目前行号的整形字面值__LINE__
-
存放檔案編譯時間的字元串字面值__TIME__
-
存放檔案編譯日期的字元串字面值__DATE__
if (word.size() < threshold) cerr << "Error:" << __FILE__ << "in function " << __func__ << "at line " << __LINE__ << endl << "Compiled on " << __DATE__ << "at " << __TIME__ << endl;
-
函數指針
- 函數指針指向的是函數而非對象。函數指針指向某種特定類型。函數的類型由它的傳回類型和形參的類型共同決定,與函數名無關。
bool (*pf)(const string &, const string &); //聲明函數指針,隻需要将原本函數名的地方換成指針
- 當我們把函數名當一個值使用時,該函數自動地轉換為指針。此外,我們還能直接使用指向函數的指針調用函數,而無需解引用。
在指向不同函數類型的指針之間,不能互相轉換。不過,我們還是能為函數指針賦pf = lengthCompare; bool b3 = pf("hello", "goodbye");
。nullptr
- 當我們使用重載函數時,編譯器通過函數指針類型判斷選用哪個函數(如:初始化函數指針時)。
- 形參可以是指向函數的指針。雖然看起來是函數類型,實際上卻當成指針使用。
//直接寫 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); //使用類型别名 type bool Func(const string &, const string &); void useBigger(const string &s1, const string &s2, Func);
- 和數組類似,雖然不能傳回一個函數,但是能傳回指向函數的指針。最簡單的辦法依舊是使用類型别名:
和函數形參不一樣,函數傳回值不會自動轉換為指針,我們需要顯式地将傳回類型定為指針。當然,還可以使用尾置傳回類型的方式。using func = int(int *, int); //func是函數類型 using pfunc = int(*)(int *, int); //pfunc是指針類型 func *f1(int); pfunc f1(int); decltype(sumLength) *getFcn(const string &); //sumaLength是一個函數的名字