C++ 學習筆記之(6)-函數、重載和指針
函數基礎
函數定義包括以下幾個部分
返回類型、函數名字、由0個或多個形參組成的列表以及函數體
局部對象
C++語言中,名字有作用域,對象有生命週期
- 名字的作用域是程序文本的一部分,名字在其中可見
- 對象的生命週期是程序執行過程中該對象存在的一段時間
自動對象
存在於塊執行期間的對象成爲自動對象,即執行到變量定義時創建對象,快末尾銷燬它。比如形參等。
局部靜態對象
在程序的執行路徑第一次經過對象定義語句時初始化,並且知道程序終止才被銷燬,即使所在的函數執行結束。
size_t count_calls()
{
static size_t ctr = 0; // 調用結束後,這個值仍然有效
return ++ctr;
}
int main()
{
for(size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
函數聲明
函數原型:即函數聲明,包含返回類型,函數名和形參類型。
函數的聲明和函數的定義類似,區別是聲明無需函數體,用一個分號替代即可
定義函數的源文件應該把含有函數聲明的頭文件包含進來,編譯器負責驗證函數的定義和聲明是否匹配
參數傳遞
形參的初始化方式和變量的初始化方式一樣
引用傳遞:形參是引用類型,也被叫做傳引用調用,即引用形參是對應實參的別名
- 值傳遞:當實參的值拷貝給形參是,形參和實參是兩個獨立對象,也被稱爲傳值調用
傳值參數
指針形參:指針與非引用類型一樣,執行拷貝操作時,拷貝的是指針的值。兩個指針是不同的指針,但是由於指針可以間接訪問所指對象,所以指向的是統一內容
void reset(int *ip) { *ip = 0; // 改變指針 ip 所指對象的值 ip = 0; // 只改變了 ip 的局部拷貝,實參未被改變 }
傳引用參數
對於引用的操作實際上是作用在引用所引的對象上
void reset(int &i) { i = 0; // 改變了 i 所引對象的值 }
使用引用避免拷貝
如果函數無需改變引用形參的值,最好將其聲明爲常量引用
const 形參和實參
實參初始化形參時會忽略掉頂層const,即形參的頂層const被忽略
void fcn(const int i) { /* fcn 能夠讀取 i, 但是不能向 i 寫值 */ } void fcn(int i) {/* ... */} // 錯誤:重複定義 fcn(int)
C++語言允許函數名字相同,但前提是形參列表應該有明顯區別,由於頂層const被忽略,故上述代碼中傳入兩個函數的參數可以完全一樣,故爲重定義
非常量能夠初始化底層const對象,反之不行
字面值能夠初始化常量引用
函數形參儘量使用常量引用
- 常量:避免函數內修改值,同時可以傳入普通參數和常量參數
- 引用:避免函數對參數進行拷貝,提高效率
知識點
數組形參
數組的兩個性質
- 不允許拷貝數組, 故無法值傳遞
- 使用數組時通常會將其轉換成指針,故實際上傳遞給函數的是指向數組首元素的指針
// 下面三個函數等價,每個函數形參都是 const int*
void print(const int*);
void print(const int[]); // 可以看出,函數的意圖是作用於一個數組
void print(const int[10]); // 維度表示期望數組含有多少元素,但實際不一定
含有可變形參的函數
C++11新標準提供了兩個方法處理不同數量實參的函數
- 若所有實參類型相同,可以傳遞一個名爲
initializer_list
的標準庫類型 - 若實參不同,則可以拜年可變參數模板(16.4節介紹)
initializer_list
若函數的實參數量未知但全部實參類型相同,則可以使用initializer_list
類型的形參, initializer_list
是一種標準庫類型,用於表示某種特定類型的值的數組。其定義在同名頭文件中
和
vector
類似,initializer_list
也是模板類型,定義時,需要說明類型initializer_list
對象中元素必須是常量值,無法改變, 並且放在花括號中void error_msg(initializer_list<string> il) { for(auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " "; cout << endl; } // 調用語句 // expected, actual 是 string 對象 if (expected != actual) error_msg({"functionX", expected, actual}); else error_msg({"functionX", "okay"});
省略符形參
省略符形參是爲了便於C++程序訪問某些特殊的C代碼設置的,這些代碼使用了varargs
的C標準庫功能。
省略符形參只能出現在形參列表的最後一個位置,形式有兩種
// 第一種指定了部分形參的類型,對於這部分參數將進行正常的類型檢查,省略符形參所對應的實參無需類型檢查 void foo(parm_list, ...); void foo(...);
進階
返回類型和return語句
return
語句終止當前正在執行的函數並將控制權返回到調用該函數的地方,return
語句有兩種形式
return;
return expression;
無返回值函數
無返回值的return
語句用在返回類型是void
的函數中,此類函數最後會隱式執行return
。
有返回值函數
return
語句返回值的類型必須與函數的返回類型相同,或能隱式轉換成函數的額返回類型
返回值的方式和初始化一個變量或形參的方式完全一樣
不要反悔局部對象的引用或指針:因爲函數完成後,其存儲空間被釋放,局部變量的引用指向的內存無效
調用返回引用的函數得到左值,其他返回類型得到右值
列表初始化返回值
vector<string> process() { return {"return", "example"}};
返回數組指針
函數不能返回數組,所以可以返回數組指針或引用
Type (*function (parameter_list)) [dimension]
// 例子,使用類型別名 typedef int arrT[10]; // arrT爲類型別名,表示類型是含有 10 個整數的數組 using arrT = int[10]; // arrT的等價聲明 arrT* func(int i); // func 返回一個指向含有 10 個整數的數組的指針 // 例子,沒有使用類型別名 int (*func(int i))[10]; // 準曾理解該聲明含義 func(int i); // 表示調用func函數時需要一個int類型的實參 (*func(int i)); // 意味着我們可以對函數調用的結果執行解引用操作 (*func(int i))[10]; // 表示解引用func的調用將得到一個大小是10的數組 int (*func(int i))[10]; // 表示數組中的元素是int 類型
使用尾置返回類型
// func 接受一個 int 類型的實參,返回一個指針,該指針指向含有 10 個整數的數組 auto func(int i) -> int(*)[10];
使用
decltype
int odd[] = {1, 3, 5, 7, 9}; decltype(odd) *arrPtr(int i); /* 返回一個指向含有 5 個整數的數組的指針,注意 decltype 的結果是數組,不會把數組類型轉換成對應的指針,故需要加 * 符號 */
函數重載
若同一作用域內的幾個函數名字相同但形參列表不同,即爲重載函數
頂層
const
形參等價於無頂層const
形參int lookup(int i); int lookup(const int i); // 重複聲明 int lookup(int); 頂層const int lookup(int *pi); int lookup(int * const pi); // 重複聲明 int lookup(int *); 頂層const
底層
const
可以實現函數重載int lookup(int &); // 函數作用域 int 引用 int lookup(const int&); // 新函數,作用域常量引用 int lookup(int *); // 新函數,作用於指向 int 的指針 int lookup(const int *); // 新函數,作用於指向常量的指針
const_cast
和重載// 參數和返回類型都是 const string 的引用 const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } // 返回類型是普通引用, 參數爲普通變量 string &shorterString(string &s1, string &s2) { // 將普通引用轉換成對對const的引用,然後調用const版本,返回對const的引用,如果不轉換成const,則會根據非常量參數調用非const版本,直至報錯 auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); // 將const引用轉成普通引用返回 return const_cast<string&>(r); }
特殊用途語言特性
默認實參
某個形參被賦予默認值,則其後面的所有形參都必須有默認值
在給定作用域中,一個形參只能被賦予一次默認實參
typedef string::size_type sz; string screen(sz, sz, char = ' '); string screen(sz, sz, char = '*'); // 錯誤:重複聲明 string screen(sz = 24, sz = 80, char); // 正確:添加默認實參
通常,應該在函數聲明中指定默認實參,並將該聲明放在合適的頭文件中。
局部變量不能作爲默認實參,除此之外,只要表達式的類型能轉換成形參類型,該表達式就能作爲默認實參
內聯函數就和constexpr
函數
內聯函數
inline
, 即函數在調用點 內聯地展開內聯說明只是向編譯器發出的請求,編譯器可以選擇忽略這個請求
很多編譯器不支持內聯遞歸函數
constexpr
函數:能用於常量表達式的函數。 函數的返回類型及所有形參的類型都是字面值類型,函數體中必須有且只有一條return
語句constexpr int new_sz() { return 42; } // new_sz() 爲無參數的 constexpr 函數
constexpr
函數被隱式地指定爲內聯函數constexpr
函數體內可以包含其他語句,只要這些語句在運行時不執行任何操作,比如空語句,類型別名以及using
聲明constexpr
函數不一定返回常量表達式// 若參數 arg 爲常量表達式,則 scale(arg) 也是常量表達式 constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
內聯函數和
constexpr
函數可以在程序中多次定義,爲了多個定義保持完全一致,通常定義在頭文件內
調試幫助
assert
預處理宏:一個預處理變量,類似於內聯函數,由預處理器而非編譯器管理assert(expr); // 對 exp r求值,若表達式爲假(即 0 ),assert輸出信息並終止運行;若爲真,則什麼也不做
NDEBUG
:預處理變量,assert
的行爲依賴於次變量的狀態, 若定義了NDEBUG
, 則assert
什麼也不錯,相當於關閉調試。默認沒有定義,此時assert
會執行運行時檢查void print(const int ia[], size_t size) { # ifndef NDEBUG cerr << __func__ << endl // 當前調試函數名 << __FILE__ << endl // 存放文件名的字符串字面值 << __LINE__ << endl // 存放當前行號的整型字面值 << __TIME__ << endl // 存放文件編譯時間的字符串字面值 << __DATE__ << endl; // 存放文件編譯時期的字符串字面值 }
函數匹配
- 候選函數:調用對應的重載函數集,有兩個特徵
- 與被調用函數同名
- 其聲明在調用點可見
- 可行函數:通過考察調用提供的實參,從候選函數中選出能被這組實參調用的函數,也有兩個特徵
- 其形參數量與本次調用提供的實參數量相等
- 每個實參類型與對應形參類型相同,或能轉換成形參類型
- 實參類型與形參類型能夠越接近,匹配的越好
- 若有多個形參匹配,編譯器會一次檢查每個實參以確定最佳匹配函數,若未找到,則會報告二義性錯誤
- 該函數每個實參的匹配都不劣於其他可行函數需要的匹配
- 至少有一個實參的匹配由於其他可行函數提供的匹配
實參類型轉換
爲了確定最佳匹配,編譯器將實參類型到形參類型的轉換分成等級,如下所示
需要類型提升和算數類型轉換的匹配
小整形一般會提升到
int
類型或更大的整數類型void ff(int); void ff(short); ff('a'); // char 提升成 int; 調用 f(int)
所有算數類型轉換的級別都一樣
void manip(long); void manip(float); manip(3.14); // 錯誤:二義性調用,因爲double可以轉換爲long 和 float
函數匹配和const
形參
若重載函數的區別在於用用類型的形參是否引用了const
,或指針類型的形參是否執行const
,則編譯器通過實參是否爲常量決定函數選擇
int lookup(int &); // 參數爲 int 引用
int lookup(const int &); // 參數爲常量引用
const int a;
int b;
lookup(a); // 調用 lookup(const int &)
lookup(b); // 調用 lookup(int &)
函數指針
函數指針即指向函數的指針,函數指針也是指向某種特定類型。函數類型由其返回類型和形參類型決定,與函數名無關。
// 函數類型是 bool (const string&, const string &)
bool lengthCompare(const string &, const string &);
// 聲明指向該函數的指針,只需要用指針替換函數名即可
// pf 指向一個函數,該函數的參數爲兩個 const string引用,返回值是 bool 類型
bool (*pf)(const string &, const string &); // 未初始化
函數名作爲值使用時,自動轉換爲指針
pf = lengthCompare; // 等價於 pf = &lengthCompare; 取地址符可選
可直接使用函數指針調用函數,無需解引用
bool b1 = pf("hello", "goodbye"); // 調用 lengthCompare 函數 bool b2 = (*pf)("hello", "goodbye"); // 等價於上個調用語句
函數指針不存在類型轉換,可爲函數指針賦值
nullptr
或0
,表示函數指針不指向任何函數函數指針可做爲形參,可以使用類型別名簡化形參
// 第三個形參是函數類型,它會自動轉換成指向函數的指針 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); // 等價聲明,顯示地將形參定義成指向函數的指針 void useBigger(const string &s1, const string &s2, book (*pf)(const string &, const string &)); // 自動將函數 lengthCompare 轉換成指向該函數的指針 useBigger(s1, s2, lengthCompare);
函數能返回函數指針,與形參不同,編譯器不會自動將函數返回類型轉換成對應的指針類型。
using F = int(int *, int); // F 是函數類型,不是指針 using PF = int(*)(int *, int); // PF 是指針類型 PF f1(int); // 正確:PF爲函數指針,f1 返回函數指針 F f1(int); // 錯誤:F是函數類型,f1不能返回一個函數 int (*f1(int))(int *, int); // 等價於PF f1(int); auto f1(int) -> int(*)(int *, int); // 使用尾置返回類型方式聲明返回函數指針的函數
結語
- 函數是命名了的計算單元,每個函數都包括返回類型、名字、(可能爲空的)形參列表以及函數體
- C++中,函數可以被重載:同一個敏子可用於定義多個函數,只要形參數量或形參類型不同即可。
函數相關基礎知識已經瞭解,以後要深入學習函數相關知識。