- 任意兩個形參都不能同名,而且函式最外層作用域中的區域變數也不能使用與函式形參一樣的名字(形參就相當于該函式的區域變數),
- 形參名是可選的,但是由于我們無法使用未命名的形參,所以形參一般都應該有個名字,某類形參通常不命名以表示在函式體內不會使用它,不管怎樣,是否設定未命名的形參并不影響呼叫時提供的實引數量,即使某個形參不被函式使用,也必須為它提供一個實參,
- 在C++語言中,名字有作用域,物件有生命周期,
- 名字的作用域是程式文本的一部分,名字在其中可見,
- 物件的生命周期是程式執行程序中該物件存在的一段時間,
- 一個陳述句塊構成一個新的作用域,
- 形參和函式體內部定義的變數統稱為區域變數,其僅在該函式的作用域內可見,同時區域變數還會隱藏在外層作用域中同名的其他所有宣告,
- 我們把只存在于執行期間的物件稱為自動物件,當塊的執行結束后,塊中創建的自動物件的值就變成未定義的了,
- 區域靜態物件在程式的執行路徑第一次經過物件定義陳述句時初始化,并且直到程式終止才被銷毀,在此期間即使物件所在的函式結束執行也不會對它有影響,
- 如果區域靜態變數沒有顯式的初始值,它將執行值初始化,內置型別的區域靜態變數初始化為0,
- 如果函式無需改變參考形參的值,最好將其宣告為常量參考,
- 和其他初始化程序一樣,當用實參初始化形參時會忽略掉頂層const(從呼叫的角度來看),換句話說,形參的頂層const被忽略掉了,當形參有頂層const時,傳給它常量物件或者非常量物件都是可以的,注意:
void fcn (const int i) { /* fcn 能夠讀取i,但是不能向i寫值 */ } void fcn (int i) { /*...*/ } // 錯誤:重復定義了fcn (int) // 在C++語言中,允許我們定義若干具有相同名字的函式,不過前提是不同函式的形參串列應該有明顯的區別,因為頂層const被忽略掉了,所以在上面的代碼中傳入兩個fcn函式的引數可以完全一樣,因此第二個fcn是錯誤的,盡管形式上有差異,但實際上它的形參和第一個fcn的形參沒什么不同, - 陣列形參:
// 盡管形式不同,但這三個print函式是等價的 // 每個函式都有一個const int*型別的形參 void print (const int*); void print (const int[]); void print (const int[10]); // 這里的維度沒有用, - 給陣列形參指定長度:
// 1. 使用標記指定陣列長度: void print (const char *cp) // C風格字串以'\0'結尾 { if (cp) while (cp) cout << *cp++; } // 2. 使用標準庫規范: void print (const int *beg, const int *end) // 傳遞指向陣列首元素和尾后元素的指標 { // 輸出beg到end之間(不含end)的所有元素 while (beg != end) cout << *beg++ << endl; // 輸出當前元素并將指標向前移動一個位置 } // 3. 顯式傳遞一個表示陣列大小的形參: void print (const int ia[], size_t size) // 專門定義一個表示陣列大小的形參 { for (size_t i = 0; i != size; ++i) { cout << ia[i] << endl; } } - 當函式不需要對陣列元素執行寫操作的時候,陣列形參應該是指向const的指標,只有當函式確實要改變元素值的時候,才把形參定義成指向非常量的指標,
- 形參也可以是陣列的參考,
// 正確:形參是陣列的參考,維度是型別的一部分 void print (int (&arr)[10]) // 這里的維度是必須的 // &arr兩端的括號必不可少 { for (auto elem : arr) cout << elem << endl; } - 多維陣列的形參:
// 兩者等價 void print (int (*matrix)[10], int rowSize) { /*...*/ } void print (int matrix[][10], int rowSize) { /*...*/ } - main:處理命令列選項
int main (int argc, char *argv[]) { ... },最后一個指標之后的元素值保證為0, - 如果函式的實引數量未知但是全部實參的型別都相同,我們可以使用initializer_list型別的形參,initializer_list物件中的元素永遠是常量值,我們無法改變initializer_list物件中元素的值:
void error_msg (ErrCode e, initializer_list<string> il) { cout << e.msg() << ": "; for (const auto &elem : il) // 因為initializer_list包含begin和end成員,所以我們可以使用范圍for回圈處理其中的元素,也可以寫成for (auto beg = il.begin(); beg != il.end(); ++beg) cout << elem << " "; cout << endl; } if (expected != actual) error_msg (ErrCode(42), {"functionX", expected, actual}); // expected和actual是string物件 else error_msg (ErrCode(0), {"functionX", "okay"});
| 操作 | 解釋 |
|---|---|
| initializer_list |
默認初始化:T型別元素的空串列 |
| initializer_list |
lst的元素數量和初始值一樣多;lst的元素是對應初始值的副本;串列中的元素是const |
| lst2 (lst)或lst2 = lst | 拷貝或賦值一個initializer_list物件不會拷貝串列的元素;拷貝后,原始串列和副本共享元素(畢竟是常量值) |
| lst.size() | 串列中的元素數量 |
| lst.begin() | 回傳指向lst中首元素的指標 |
| lst.end() | 回傳指向lst中尾元素下一位置的指標 |
- 省略符形參(C語言特性,非C++):大多數型別別的物件在傳遞給省略符形參時都無法正確拷貝,
void foo (parm_list, ...);、void foo (...);第一種形式指定了foo函式的部分形參的型別,對應于這些形參的實參將會執行正常的型別檢查,省略符形參所對應的實參無需型別檢查,在第一種形式中,形參宣告后面的逗號是可選的, - 一個回傳型別是void的函式也能使用return陳述句的第二種形式(
return expression;),不過此時return陳述句的expression必須是另一個回傳void的函式,強行令void函式回傳其它型別的運算式將產生編譯錯誤, - 在含有return陳述句的回圈后面應該也有一條return陳述句,如果沒有的話該程式就是錯誤的,控制流可能尚未回傳任何值就結束了函式的執行,很多編譯器都無法發現此類錯誤,
const string &shorterString (const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; }其中形參和回傳型別都是const string的參考,不管是呼叫函式還是回傳結果都不會真正拷貝string物件,- 不要回傳區域物件的參考或指標(回傳時區域變數可能已銷毀),要想確保回傳值安全,我們不妨提問:參考所引的是在函式之前已經存在的哪個的物件?
- 呼叫一個回傳參考的函式得到左值,其他回傳型別得到右值,可以像使用其它左值那樣來使用回傳參考的函式的呼叫,特別是,我們能為回傳型別是非常量參考的函式的結果賦值,
- 串列初始化回傳值:如果函式回傳的是內置型別,則花括號包圍的串列最多包含一個值,而且該值所占空間不應該大于目標型別的空間(所以
int a = {3.14};是錯的),如果函式回傳的是型別別,由類本身定義初始值如何使用(可以看作用花括號內容初始化一個待回傳的臨時變數,該變數型別就是回傳值型別),vector<string> process() { // ... // expected和actual是string物件 if (expected.empty()) return {}; // 回傳一個空vector物件 else if (expected == actual) return {"functionX", "okay"}; // 回傳串列初始化的vector物件 else return {"functionX", expected, actual}; } // 猜猜func的回傳值會是什么? vector<string> func(void) { return {10, "hi"}; } - 如果控制到達了main函式的結尾處而且沒有return陳述句,編譯器將隱式地插入一潭訓傳0的return陳述句,
- 宣告一個回傳陣列指標的函式:
// 1. C風格 int (*func(int i))[10]; // 2. 使用尾置回傳型別 auto func(int i) -> int(*)[10] // 3. 使用decltype int odd[] = {1,3,5,7,9}; int even[] = {0,2,4,6,8}; decltype(odd) *arrPtr(int i) // decltype并不負責把陣列型別轉換成對應的指標,所以decltype的結果是個陣列 { return (i % 2) ? &odd : &even; // 回傳一個指向陣列的指標 } - main函式不能多載
- 對于多載的函式來說,它們應該在形引數量或形參型別上有所不同(省略形參名字或僅僅改變變數名不算),不允許兩個函式除了回傳型別外其他所有的要素都相同,一個擁有頂層const的形參無法和另一個沒有頂層const的形參區分開來(底層const可以),
- 當我們傳遞一個非常量物件或者指向非常量物件的指標時,編譯器會優先選用非常量版本的函式,
- const_cast和多載:
const string &shorterString(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); } - 函式匹配是指一個程序,在這個程序中我們把函式呼叫與一組多載函式中的某一個關聯起來,函式匹配也叫做多載確定,
- 當呼叫多載函式時有三種可能的結果:
- 編譯器找到一個與實參最佳匹配的函式,并生成呼叫該函式的代碼,
- 找不到任何一個函式與呼叫的實參匹配,此時編譯器發出無匹配的錯誤資訊,
- 有多于一個函式可以匹配,但是每一個都不是明顯的最佳選擇,此時也將發生錯誤,稱為二義性呼叫,
- 如果我們在內層作用域中宣告名字,它將隱藏外層作用域中宣告的同名物體(不論是函式名,還是變數名),在不同的作用域中無法多載函式名(內層函式會隱藏同名的外層函式),
- 在C++語言中,名字查找發生在型別檢查之前,
- 函式宣告放在同一個作用域中,則它將成為另一種多載形式,
- 默認實參:一旦某個形參被賦予了默認值,它后面的所有形參都必須有默認值,合理設定形參的順序,盡量讓不怎么使用默認值的形參出現在前面,而讓那些經常使用默認值的形參出現在后面,
typedef string::size_type sz; string screen(sz ht = 24, sz wid = 80, char backgrnd = ' '); - 如果我們想使用默認實參,只要在呼叫函式的時候省略該實參就可以了,函式呼叫時實參按其位置決議,默認實參負責填補函式呼叫缺少的尾部實參(靠右側位置),
- 多次宣告同一個函式也是合法的,在給定的作用域中一個形參只能被賦予一次默認實參,函式的后續宣告只能為之前那些沒有默認值的形參添加默認實參,而且該形參右側的所有形參必須都有默認值,
typedef string::size_type sz; string screen(sz, sz, char = ' '); string screen(sz, sz, char = '*'); // 錯誤:重復宣告 string screen(sz = 24, sz = 80, char); // 正確:添加默認實參 // 默認實參不在形參串列的結尾 // 第一句和第三句如果調換順序則錯誤 // 如果第三句再加上char = ' '就是重定義 // VS2019報錯但可以運行 - 函式定義和函式宣告不能同時指定默認實參,通常,應該在函式宣告中指定默認實參,并將該宣告放在合適的頭檔案中,
- 區域變數不能作為默認實參,除此之外,只要運算式的型別能轉換成形參所需的型別,該運算式就能作為默認實參,
// wd、def和ht的宣告必須出現在函式之外 typedef string::size_type sz; sz wd = 80; char def = ' '; sz ht(); string screen(sz = ht(), sz = wd, char = def); string window = screen(); // 呼叫screen(ht(), 80, ' ') - 用作默認實參的名字在函式宣告所在的作用域內決議,而這些名字的求值程序發生在函式呼叫時,
void f2() { def = '*'; // 改變默認實參的值 sz wd = 100; // 隱藏了外層定義的wd,但是沒有改變默認值 window = screen(); // 呼叫screen(ht(), 80, '*') } - 行內說明(inline)只是向編譯器發出的一個請求,編譯器可以選擇忽略這個請求,一般來說,行內機制用于優化規模較小、流程直接、頻繁呼叫的函式,很多編譯器都不支持行內遞回函式,而且一個75行的函式也不大可能在呼叫點行內地展開,
- constexpr函式定義的幾項約定:函式的回傳型別及所有形參的型別都得是字面值型別;而且函式體中必須有且只有一條return陳述句,
constexpr int new_sz() { return 42; } constexpr int foo = new_sz(); // 正確:foo是一個常量運算式 - 為了能在編譯程序中隨時展開,constexpr函式被隱式地指定為行內函式,
- constexpr函式體內也可以包含其他陳述句,只要這些陳述句在運行時不執行任何操作就行,例如,constexpr函式中可以有空陳述句、型別別名以及using宣告,我們允許constexpr函式的回傳值并非一個常量,當scale的實參是常量運算式時,它的回傳值也是常量運算式;反之則不然,(記住constexpr在編譯階段得出計算結果就行)constexpr函式不一定回傳常量運算式
// 如果arg是常量運算式,則scale(arg)也是常量運算式 constexpr size_t scale(size_t cnt) { return new_sz() * cnt; } int arr[scale(2)]; // 正確:scale(2)是常量運算式 int i=2; // i不是常量運算式 int a2[scale(i)]; // 錯誤:scale(i)不是常量運算式 - 行內函式和constexpr函式可以在程式中多次定義,但它的多個定義必須完全一致,基于這個原因,行內函式和constexpr函式通常定義在頭檔案中,
- assert是一種預處理宏
assert(expr);,首先對expr求值,如果運算式為假(即0),assert輸出資訊并終止程式的執行,如果運算式為真(即非0),assert什么也不做,assert宏定義在cassert頭檔案中, - 我們可以使用一個#define陳述句定義NDEBUG(一定要定義在
#include <cassert>之前),從而關閉除錯狀態,(命令列:CC -D NDEBUG main.C相當于在main.C檔案的一開始寫#define NDEBUG) - 除錯幫助:
#ifndef NDEBUG // __func__是編譯器定義的一個區域靜態變數,用于存放函式的名字 cerr << __func__ << ": array size is " << size << endl; #endif
| 名稱 | 解釋 |
|---|---|
__func__ |
輸出當前除錯的函式的名字 |
__FILE__ |
存放檔案名的字串字面值 |
__LINE__ |
存放當前行號的整型字面值 |
__TIME__ |
存放檔案編譯時間的字串字面值 |
__DATE__ |
存放檔案編譯日期的字串字面值 |
- 函式匹配的第一步是選定本次呼叫對應的多載函式集,集合中的函式稱為候選函式,候選函式具備兩個特征:一是與被呼叫的函式同名,二是其宣告在呼叫點可見,
- 第二步考察本次呼叫提供的實參,然后從候選函式中選出能被這組實參呼叫的函式,這些新選出的函式稱為可行函式,可行函式也有兩個特征:一是其形引數量與本次呼叫提供的實引數量相等,二是每個實參的型別與對應的形參型別相同,或者能轉換成形參的型別,(如果函式含有默認實參,則我們在呼叫該函式時傳入的實引數量可能少于它實際使用的實引數量,)
- 接下來,編譯器依次檢查每個實參以確定哪個函式是最佳匹配,如果有且只有一個函式滿足以下列條件,則匹配成功:
- 該函式每個實參的匹配都不劣于其他可行函式需要的匹配,
- 至少有一個實參的匹配優于其他可行函式提供的匹配,
- 呼叫多載函式時應盡量避免強制型別轉換,如果在實際應用中確實需要強制型別轉換,則說明我們設計的形參集合不合理,
- 實參型別轉換:
- 精確匹配
- 實參型別和形參型別相同
- 實參從陣列型別或函式型別轉換成對應的指標型別
- 向實參添加頂層const或者從實參中洗掉頂層const
- 通過const轉換實作的匹配
- 通過型別提升實作的匹配
- 通過算術型別轉換或指標轉換實作的匹配
- 通過型別別轉換實作的匹配,
- 精確匹配
- 分析函式呼叫前,我們應該知道小整型一般都會提升到int型別或更大的整數型別,有時候,即使實參是一個很小的整數值,也會直接將它提升成int型別,
void ff(int); void ff(short); ff('a'); // char提升成int;呼叫ff(int); - 所有算術型別轉換的級別都一樣,例如:從int向unsigned int的轉換并不比從int向double的轉換級別高,例如:
void manip(long); void manip(float); manip(3.14); // 錯誤:二義性呼叫 // 字面值3.14的型別是double,它既能轉換成long也能轉換成float,因為存在兩種可能的算術型別轉換,所以該呼叫具有二義性, - 函式的型別由它的回傳型別和形參型別共同決定,與函式名無關,
- 當我們把函式名作為一個值使用時,該函式自動地轉換成指標,此外,我們還能直接使用指向函式的指標呼叫該函式,無須提前解參考指標,注意,在指向不同函式型別的指標間不存在轉換規則,但是和往常一樣,我們可以為函式指標賦一個nullptr或者值為0的整型常量運算式,表示該指標沒有指向任何一個函式,
// pf指向一個函式,該函式的引數是兩個const string的參考,回傳值是bool型別, bool (*pf)(const string &, const string &); pf = lengthCompare; // pf指向名為lengthCompare的函式 pf = &lengthCompare; // 等價的賦值陳述句:取地址符是可選的 bool b1 = pf("hello", "goodbye"); // 呼叫lengthCompare的函式 bool b2 = (*pf)("hello", "goodbye"); // 一個等價的呼叫 bool b3 = lengthCompare("hello", "goodbye"); // 另一個等價的呼叫 string::size_type sumLength(const string &, const string &); bool cstringCompare(const char *, const char *); pf = 0; // 正確:pf不指向任何函式 pf = sumLength; // 錯誤:回傳型別不匹配 - 編譯器通過指標型別決定選用哪個函式,指標型別必須與多載函式中的某一個精確匹配,
- 和陣列類似,雖然不能定義函式型別的形參,但是形參可以是指向函式的指標,此時,形參看起來是函式型別,實際上確是當成指標使用,我們可以直接把函式作為實參使用,此時它會自動轉換成指標,
// 第三個形參是函式型別,它會自動的轉換成指向函式的指標 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); // 等價的宣告:顯式地將形參定義成指向函式的指標 void useBigger(const string &s1, const string &s2, bool(*pf)(const string &, const string &)); // 自動將函式lengthCompare轉換成指向該函式的指標 useBigger(s1, s2, lengthCompare); - 型別別名和decltype能讓我們簡化使用了函式指標的代碼,decltype回傳函式型別,此時不會將函式型別自動轉換成指標型別,
// Func和Func2是函式型別 typedef bool Func(const string&, const string&); typedef decltype(lengthCompare) Func2; // 等價的型別 // FuncP和FuncP2是指向函式的指標 typedef bool(*FuncP)(const string&, const string&); typedef decltype(lengthCompare) *FuncP2; // 等價的型別 // useBigger的等價宣告,其中使用了型別別名,這兩個宣告陳述句宣告的是同一個函式, void useBigger(const string&, const string&, Func); // 編譯器自動地將Func表示的函式型別轉換成指標 void useBigger(const string&, const string&, FuncP2); - 和陣列類似,雖然不能回傳一個函式,但是能回傳指向函式型別的指標,然而,我們必須把回傳型別寫成指標形式,編譯器不會自動地將函式回傳型別當成對應的指標型別處理,
using F = int(int*, int); // F是函式型別,不是指標 using PF = int(*)(int*, int); // PF是指標型別 // 1. 使用型別別名宣告f1 PF f1(int); // 正確:PF是指向函式的指標,f1回傳指向函式的指標 F f1(int); // 錯誤:F是函式型別,f1不能回傳一個函式 F *f1(int); // 正確:顯示地指定回傳型別是指向函式的指標 // 2. 直接宣告f1 int (*f1(int))(int*, int); // 3. 使用尾置回傳型別的方式宣告f1 auto f1(int) -> int (*)(int*, int); // 4. 使用auto和decltype用于函式指標型別 string::size_type sumLength(const string&, const string&); string::size_type largerLength(const string&, const string&); // 牢記當我們將decltype作用于某個函式時,它回傳函式型別而非指標型別, decltype(sumLength) *getFcn(const string &); // 使用decltype(sumLength)和decltype(largerLength)一樣 - 實參(argument),形參(parameter)
- 隱藏名字:某個作用域內宣告的名字會隱藏掉外層作用域中宣告的同名物體(不僅限于變數的名字),
- 行內函式:請求編譯器在可能的情況下在呼叫點展開函式,行內函式可以避免常見的函式呼叫開銷,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/251333.html
標籤:C++
