1. 程式人生 > 實用技巧 >C++ Primer 第六章 函式

C++ Primer 第六章 函式

第六章 函式

6.1 函式基礎

6.1.1 函式定義

  • 一個函式的定義包含以下幾部分
    • 返回型別
    • 函式名字
    • 0個或多個形參組成的列表
    • 函式體
  • 我們使用呼叫運算子(call operator)來執行函式,呼叫運算子的形式是一對圓括號,他作用於一個表示式,該表示式的值是函式或者指向函式的指標;圓括號之內是一個用逗號隔開的實參列表,我們用實參初始化函式的形參.呼叫表示式的型別就是函式的返回型別

6.1.2 函式呼叫

  • 函式呼叫完成兩步工作
    1. 用實參(隱式)定義並初始化它的形參
    2. 將控制權轉移給被調函式

6.1.3 形參和實參

  • 實參是形參的初始值,第一個實參初始化第一個形參,第二個實參初始化第二個形參....
  • 實參和形參存在對應關係,但是沒有規定實參的求值順序(運算子的運算順序沒有規定)
  • 實參的數量必須和形參相同,型別必須匹配

6.1.4 形參列表

  • 形參列表可以為空,但是不能省略
    • void f1() {} //隱式定義空形參列表
    • void f2(void) {} //顯式定義空形參列表
  • 形參名是可選的,但是我們無法使用未命名的形參,有個別形參不會被用到,可以採用不命名的方式以表示在函式中不會使用它

6.1.5 函式的返回型別

  • 大多數型別都能作為函式的返回型別
  • 函式的返回型別不能是陣列型別或者函式型別,可以是指向陣列的指標

6.1.6 區域性靜態物件

  • 為了令區域性變數的生命週期貫穿函式呼叫及之後的時間,可以將區域性變數定義成static型別從而獲得這樣的物件
  • 區域性靜態物件在程式執行路徑第一次經過物件定義語句時初始化,直到程式終止才被銷燬

6.1.7 函式宣告

  • 和變數一樣,函式的名字也必須在使用之前宣告,函式只能定義一次,但可以宣告多次.
  • 如果一個函式永遠也不會被我們用到,那麼它可以只有宣告沒有定義
  • 因為函式的宣告不包含函式體,所以也不需要形參的名字,事實上函式的宣告經常省略形參的名字,但是寫上形參的名字有助於使用者理解函式的功能
  • 函式宣告也被稱作函式原型
  • 建議函式在標頭檔案中宣告而在原始檔中定義

6.1.8 分離式編譯

  • 隨著程式越來越複雜,我們希望把函式存在一個檔案裡,使用這些函式的程式碼存在其他原始檔中,為了允許編寫程式時按照邏輯關係劃分,C++支援分離式編譯,允許我們把程式分割到幾個檔案中去,每個檔案獨立編譯
  • e.g. 假設fact函式定義位於fact.cc,宣告位於fact.h,顯然fact.cc需要包含fact.h,我們需要在factmain.cc中建立main函式,呼叫fact函式,為了生成可執行檔案,我們需要告訴編譯器我們用到的程式碼在哪裡
    • CC factmain.cc fact.cc # generates factmain.exe or a.out
    • CC factmain.cc fact.cc -o main # generates main or main.exe
    • 其中,CC是編譯器的名字,$是系統提示符,#是命令列下的註釋語句,接下來執行可執行檔案,就會執行我們定義的main函式
    • 如果我們修改了其中一個原始檔,只需要重新編譯改動了的檔案,大多數編譯器提供了分離式編譯每個檔案的機制,這一過程會產生一個字尾名為.obj(windows)或者.o(unix)的檔案,字尾名的含義是該檔案包含物件程式碼(object code)
    • 接下來編譯器負責把物件檔案連結在一起形成可執行檔案,編譯的過程如下
    • CC -c factmain.cc #generates factmain.o
    • CC -c fact.cc #generates fact.o
    • CC factmain.o fact.o #generates factmain.exe or a.out
    • CC factmain.o fact.o main #generates main or main.exe

6.2 引數傳遞

  • 每次呼叫函式會重新建立它的形參,並且用傳入的實參對形參進行初始化
  • 如果形參是引用型別,則繫結到實參上,否則將實參的值拷貝給形參
    • 當形參是引用型別時,我們說實參被引用傳遞或者函式被傳引用呼叫
    • 實參被拷貝時,我們說實參被值傳遞或者傳值呼叫

6.2.1 指標形參

  • 指標的行為和其他非引用型別一樣,執行指標拷貝操作時,拷貝的是指標的值,拷貝之後兩個指標是不同的指標,因為指標使我們可以間接地訪問它所指的物件,所以我們可以通過指標修改它所指的物件的值。
  • tips,C語言中常常使用指標型別的形參修改函式外部的物件,C++中建議使用引用型別的形參替代指標

6.2.2 傳引用引數

  • 對引用的操作實際上是作用在引用所引的物件上
  • 使用引用避免拷貝,因為拷貝大的類型別物件或者容器物件比較低效,有些類型別甚至不支援拷貝,這種情況下函式只能通過引用形參訪問該型別的物件
//e.g. 我們準備編寫一個函式比較兩個長字串的長度,需要避免直接拷貝他們,這時候使用引用形參會比較明知,並且把他們定義為常量的引用
bool strcmp(const string &s1, const str &s2) {
  return s1.size() < s2.size();
}
  • 如上,當函式無需修改形參的值的時候最好使用常量引用

6.2.3 引用引數返回額外資訊

  • 一個函式只能返回一個值,但是當函式需要返回多個值的時候,可以採用引用形參,將需要返回的值通過引用傳遞進入函式

6.2.4 const形參和實參

  • 用實參初始化形參時會忽略掉頂層const,當形參有頂層const時,傳給它常量物件或者非常量物件都可以
void fun(const int i); //fun可以讀取i但是不能向i寫值
void fun(int i); //錯誤:重複定義
  • 因為頂層const被忽略了,所以上面兩個函式不構成過載,在使用的時候看來都是void fun(int i);

6.2.5 指標或引用形參參與cosnt

  • 和變數初始化一樣,我們可以使用非常量初始化一個底層const物件,但是反過來不行。同時一個普通的引用必須用同類型的物件初始化。
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); //呼叫形參型別是int*的reset函式
reset(&ci);//錯誤:不能用const int*初始化int*

reset(i);  //呼叫形參是int&的reset函式
reset(ci); //錯誤:不能把int&繫結到const物件上
reset(42); //錯誤:不能把int&繫結到字面值上
reset(ctr);//錯誤:型別不匹配
  • 如上,想要呼叫引用版本的reset,只能使用int型別物件,不能使用字面值,求值結果、需要轉換的物件或者const int
  • 想要呼叫指標版本的reset,只能使用int*
  • 但是如果函式的引用形參是常量引用,我們就可以採用字面值初始化常量引用

6.2.6 常量引用

  • 把函式不會改變的形參定義為普通引用是常見的錯誤,會讓呼叫者覺得這個函式可以修改它實參的值
  • 使用非常量引用也會限制函式所能接受的實參型別,我們不能把const物件、字面值或者需要轉換型別的物件傳遞給普通的引用傳參
//e.g. 錯誤示範
//我們本該將函式的第一個形參定義為const string&
string::size_type find_char(string &s, char c, string::size_type &occ);

//這種情況下,下面的這種呼叫就變得不可行了,因為我們第一個引數傳入了字面量值
find_char("hello world", "o", ctr); 

//更加難以察覺的是下面這種情況,const物件也是不能作為實參傳入的
bool fun(const string &s) {
  return find_char(s, '.', 0) == s.size() - 1;
}
//我們或許可以修改fun的傳參,但是沒有從根本上解決問題,正確的方法還是修改find_char的形參

6.2.7 陣列形參

陣列有兩個特殊性質會對我們定義和使用作用在陣列的函式有影響

  • 不允許拷貝陣列
  • 使用陣列時(通常)會將其轉換為指標

因此,我們無法用值傳遞的方式使用陣列引數,當我們為函式傳遞一個數組時,實際上傳遞的是指向陣列首元素的指標

//這三個print實際上是等價的,每個函式都是一個const int*的形參,編譯器在處理函式呼叫的時候,只檢查函式的傳參是不是const int*型別
void print(const int*);
void print(const int []); //函式的意圖是作用於一個數組
void print(const int [10]);  //表示我們期望陣列有多少元素,但是實際不一定

當然,以陣列作為形參的函式也必須保證使用的時候陣列不會越界
因為傳遞的只有首元素的指標,呼叫者還需要提供一些額外的資訊告訴函式確切的陣列的尺寸,有三種常用的方法

  1. 使用標記指定陣列長度:例如C風格字串中的結尾往往有一個空字元表示字串的停止,但是這種方法在int*這種所有取值都合法的資料下就不太有效
  2. 使用標準庫規範:傳遞指向陣列首元素和尾後元素的指標
//e.g.
void print(const int *beg, const int *end) {
  while(beg != end) cout << *beg++ << endl;
}
int j[2] = {0, 1};
print(begine(j), end(j));
  1. 顯示傳遞一個表示陣列大小的形參

當函式不需要對陣列元素執行寫操作的時候,陣列形參應該是指向const的指標,只有函式需要修改元素值的時候,才會把形參定義為非常量指標

6.2.8 陣列引用形參

  • C++允許將變數定義為對陣列的引用,同樣,形參也可以是陣列的引用
//arr是具有10個整數的整型陣列的引用
//括號必不可少,否則int &arr[10]變成了引用的陣列
void print(int (&arr)[10]) {
  for(auto elem:arr) {
    cout << elem << endl;
  }
}
//但是這一用法無形中限制了print函式的可用性
int i = 0, j[2] = {0, 1};
print(&i);//錯誤,實參不是含有10個整數的陣列
print(j); //錯誤,實參不是含有10個整數的陣列

6.2.9 傳遞多維陣列

  • C++中實際上沒有真正的多維陣列,所謂的多維陣列其實是陣列的陣列
  • 當多維陣列傳遞給函式時,真正傳遞的是指向陣列首元素的指標,因為處理的是陣列的陣列,所以首元素本身就是一個數組,指標就是一個指向陣列的指標。第二維(以及後面所有的維度)的大小都是陣列型別的一部分,不能省略
//matrix指向陣列首元素,陣列的元素是10個整數構成的陣列
void print(int (*matrix)[10], int rowSize);
//matrix的括號必不可少,否則變成10個指標構成的陣列

//上述宣告等價於
void print(int matrix[][10], int rowSize);

6.2.10 main:處理命令列選項

int main(argc, char *argv[]) {}

  • 第一個形參argc表示陣列中字串的數量
  • 第二個形參是陣列,當實參傳給main函式之後,argv的第一個元素指向程式的名字或者一個空字串,接下來的元素依次是傳遞命令列提供的實參。最後一個指標之後的元素值保證為0
  • tips:當使用argv中的實參時,一定要記得可選的實參從argv[1]開始,argv[0]用於儲存程式的名字而非使用者輸入

6.2.11 含有可變形參的函式

有時我們無法提前預知應該向函式傳遞幾個實參,為了能編寫處理不同數量實參的函式,C++11提供了兩種主要方法

  • 如果所有的實參型別相同,可以傳遞一個名為initializer_list的標準庫型別
  • 如果實參的型別不同,可以編寫一種特殊的函式,也就是所謂的可變引數模板
  • C++還有一種特殊形參型別(省略符),可以用它傳遞可變數量的實參,不過這種功能一般只用於與C函式互動的介面程式

initializer_list形參
如果函式的實引數量未知但是全部實參的型別都相同,可以使用initializer_list型別的形參,這是一種標準庫型別,用於表示某種特定型別的值的陣列

initializer_list<T> lst; //預設初始化,T型別元素的空列表
initializer_list<T> lst{a,b,c..}; //lst的元素數量和初始值一樣多,lst的元素是對應初始值的副本,列表中的元素是const
lst2(lst); 
lst2 = lst;//拷貝或者賦值一個initializer_list物件不會拷貝列表中的元素,拷貝後原始列表和副本共享元素
lst.size(); //列表中元素數量
lst.begin(); 
lst.end(); //列表的首指標和尾後指標
  • initializer_list也是一種模板型別,定義物件的時候必須說明列表中所含的元素的型別
  • initializer_list物件中的元素永遠是常量值,我們無法改變物件中元素的值
//e.g.
void error_msg(ErrCode e, initializer_list<string> il) {
  cout << e.msg() << endl;
  for (auto beg = il.begin(); beg != il.end(); ++beg) {
      cout << *beg << " ";
  }
  cout << endl;
}
//使用
if (excepted != actual) 
    error_msg(ErrCode(42),{"functionX", excepted, actual});
else
    error_msg(ErrCode(0),{"functionX","okey"});

省略符形參

  • 省略符形參是為了便於C++程式訪問某些特殊的C程式碼而設定的,這些程式碼使用了名為varargs的C標準庫的功能
  • 特別值得注意的是,大多數類型別的物件在傳遞給省略符形參時都無法正確拷貝
//省略符形參只能出現在形參列表的最後一個位置,他的形式只有兩種
void fun(parm_list, ...); //指定了部分形參型別,形參後的逗號是可選的
void fun(...);

6.3 返回型別和return語句

6.3.1 不能返回區域性物件的引用或者指標

//嚴重錯誤:試圖返回區域性物件的引用
const string &manip() {
    string ret;
    if(!ret.empty()) {
        return ret; //錯誤:返回區域性物件的引用
    } else {
        return "Empty"; //錯誤:Empty是一個區域性臨時量
    }
}

如上,兩條return都將返回一個未定義的值,試圖使用manip的返回值會引發未定義的行為,這兩條語句都指向了不再可用的記憶體空間

6.3.2 返回引用左值

函式的返回型別決定函式呼叫是否是左值

  • 呼叫一個返回引用的函式得到左值,其他返回型別得到右值
    可以像使用其他左值那樣來使用返回引用的函式的呼叫,特別是我們能為返回型別是非常量引用的函式的結果賦值
char &get_val(string &str, string::size_type ix) {
    return str[ix];
}
int main() {
    string s("a value");
    cout << s << endl;
    get_val(s,0) = 'A'; //將s[0]改為A
    cout << s << endl;
    return 0;
}
  • 當然,如果返回型別是常量引用,我們不能給呼叫的結果賦值

6.3.3 列表初始化返回值

C++11 新標準規定,函式可以返回花括號包圍的值的列表,類似於返回其他結果,此處的列表也用來表示函式返回的臨時量進行初始化。

vector<string> process() {
    //...
    if (excepted.empty()) {
        return {};
    } else if (excepted == actual) {
        return {"functionX","okey"};
    } else {
        return {"functionX", excepted, actual};
    }
}

6.3.4 main的返回值

  • 對於其他函式來說,如果函式的返回型別不是void,那麼他必須返回一個值,但是main函式例外,如果main函式沒有return語句直接結束,編譯器會隱式的插入一條返回0的return語句
  • main函式的返回值可以看作是狀態指示器,返回0表示執行成功,其他返回值表示執行失敗,其中非0值的具體含義由機器而定。
  • 為了使返回值與機器無關,cstdlib標頭檔案定義了兩個預處理變數,我們可以用這兩個變量表示成功與失敗
int main() {
    if(some_failure) return EXIT_FAILESS;
    else return EXIT_SUCCESS;
}
  • 因為他們是預處理變數,所以既不能在前面加std::,也不能再using宣告中出現
  • main函式不能呼叫他自己

6.3.5 返回陣列指標

  • 因為陣列不能被拷貝,所以函式不能返回陣列,但是函式可以返回陣列的指標或引用。
  • 從語法上來說,想要定義一個返回陣列的指標或引用比較繁瑣,但是有方法可以簡化這一任務,比如類型別名
typedef int arrT[10]; //arrT是一個類型別名,表示含有10個整數的陣列
using arrT = int[10]; //arrT的等價宣告
arrT* func(int i);    //返回一個指向10個整數的陣列的指標

6.3.6 宣告一個返回陣列指標的函式

  • 如果想在省宣告func時不用類型別名,必須記住被定義的名字後面陣列的維度
int arr[10];        //arr是含有10個整形的陣列
int *p1[10];        //p1是含有10個指標的陣列
int (*p2)[10] = &arr; //p2是一個指標,指向含有10個整數的陣列
  • 如果我們想定義一個返回陣列指標的函式,則陣列的維度必須跟在函式名字之後
  • 函式的形參列表也跟在函式名字後面且形參列表應該先與陣列的維度,因此返回陣列指標的函式形式如下所示
    • Type (*function (parameter_list))[dimension]
    • Type表示元素型別,dimension表示陣列的大小,(function(parameter_list))*兩端的括號必須存在,否則函式的返回型別將會是指標的陣列
//e.g.
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型別

6.3.7 使用尾置返回型別

  • 任何函式的定義都能使用尾置返回,但是這種形式對於返回型別比較複雜的函式最有效,比如返回型別是陣列的指標或者陣列的引用。
  • 尾置返回型別跟在形參列表後面並以一個->符號開頭,為了表示函式真正的返回型別跟在形參列表之後,在本該出現在返回型別的地方放置一個auto
  • auto func(int i) -> int (*i)[10];
  • 我們可以清楚的看到func含稅返回的是一個指標,並且指向了含有10個整數的陣列

6.3.8 使用decltype

如果我們知道函式返回的指標將指向哪個陣列,就可以使用decltype關鍵字宣告返回型別

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
//返回一個指標,指向含有5個整數的陣列
decltype(odd) *arrPtr(int i) {
    return (i & 1)? &odd : &even;
}
  • tips:decltype並不負責把陣列型別轉換成對應的指標,所以decltype的結果是個陣列

6.4 函式過載

  • 過載函式:同一作用域內的幾個函式名字相同但形參列表不同,當呼叫這些函式時,編譯器會根據傳遞的實參型別推斷想要的函式
  • main函式不能過載
  • 不允許兩個函式除了返回型別之外的其他所有要素都相同

6.4.1 過載和const形參

頂層const不影響傳入函式的物件,一個擁有頂層const的形參無法和另一個沒有頂層const的形參區分開來

int solve(int);
int solve(const int ); //重複宣告int solve(int)

int solve(int*);
int solve(int* const); //重複宣告int solve(int*)

6.4.2 const_cast和過載

const_cast可以改變底層const,可以很好的和過載函式搭配

//e.g. 我們現在需要一個比較字串長度的函式,並且返回較短的那個的引用
const string &shorterString(const string &s1, const string &s2) {
    return s1.size() < s2.size() ? s1 : s2;
} 
//為了保證可以呼叫shorterString("hello","hi"); 這樣的形式,我們將string設定為const引用,但是因此也返回了const string, 但它原本可能是一個非常量
//對於非常量的字串,我們需要過載它
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去掉了,顯然這樣是安全的

6.4.3 呼叫過載的函式

函式匹配是指一個過程,在這個過程中我們把函式呼叫與一組過載函式中的某一個關聯起來,也叫做函式確定
呼叫過載函式可能有三中可能的結果

  • 編譯器找到一個與實參最佳匹配的函式,並呼叫函式程式碼
  • 找不到任何一個函式與實參匹配,發出無匹配的錯誤資訊
  • 有多於一個函式可以匹配,但是每一個都不是最佳選擇,此時也會發生錯誤,稱為二義性呼叫

6.4.4 過載與作用域

首先:將函式宣告置於區域性域不是一個明智的選擇

  • 如果我們在內部作用域中宣告名字,他將隱藏外層作用域中宣告的同名實體。在不同的作用域中無法過載函式名
string read();
void print(const string&);
void print(double);
void func(int val) {
    bool read = false;
    string s = read(); //錯誤,read是bool
    void print(int);
    print("value: "); //錯誤,print(const string&)被隱藏了
    print(val); //正確
    print(3.14); //正確,但呼叫的是print(int)
}

6.5 特殊用途語言特性

6.5.1 預設實參

  • 預設實參作為初始值出現在形參列表中
  • 一旦某個形參被賦予了預設值,它後面的所有形參都必須有預設值(只能省略尾部實參)
//e.g. 預設實參
string screen(size_t weight = 24, size_t width = 80, char background = ' ');
string window;
window = screen();
window = screen(66);
window = screen(66,256);
window = screen(66,256,'#');

window = screen(, ,'#'); //錯誤,只能省略尾部實參
window = screen('#'); //正確,但呼叫的是screen('#',80,' ');
  • 給定作用域中一個形參只能被賦予一次預設實參,也就是函式的後續宣告只能為之前沒有預設值的形參新增預設實參,而且該函式右側的所有形參都必須有預設值。
//e.g.
string screen(size_t, size_t, char = ' '); //宣告最後一個為預設實參
string screen(size_t, size_t, char = '*'); //錯誤:重複宣告
string screen(size_t, size_t = 60, char);  //正確:新增預設實參

區域性變數不能作為預設實參。除此之外只要表示式的型別能轉換成形參需要的型別,表示式就能作為預設實參。

//wd,def,ht宣告必須在函式之外
size_t wd = 80;
char def = ' ';
size_t ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen();//呼叫screen(ht(), 80, ' ');
//表示式求值的過程發生在函式呼叫時
void f2() {
     def = '*';       //改變def
     size_t wd = 100; //隱藏外部wd
     window = screen(); //依然呼叫screen(ht(), 80, ' ');
}

6.5.2 行內函數

在大多數機器上,一次函式呼叫包含著一系列工作:呼叫前儲存暫存器,返回時恢復,可能需要拷貝實參,程式轉向一個新的位置繼續執行

  • 可以通過行內函數避免函式呼叫的開銷
  • 把函式指定為行內函數,就是在他的每個呼叫點上“內聯地”展開

當我們把shorterString(s1,s2)指定為行內函數,cout << shorterString(s1,s2) << endl會被編譯器展開為cout << (s1.size() < s2.size() ? s1 : s2) << endl;,從而消除執行函式時候的開銷

  • 改寫的方法是在函式名前加上inline
  • 內聯說明是向編譯器發出一個請求,編譯器可以選擇忽略這個請求
  • 內聯機制適用於優化規模較小,流程直接,頻繁呼叫的函式

6.5.3 constexpr函式

constexpr函式是指能用於常量表達式的函式,定義constexpr函式的方法與其他函式類似,但是需要遵守幾項約定

  • 函式的返回型別及所有形參的型別都得是字面值型別
  • 函式體必須有且只有一條return語句
constexpr int new_size() {
    return 42;
}
constexpr int foo = new_size();
  • 編譯器在初始化的時候能驗證new_size返回的是常量表達式,因此可以用來初始化constexpr型別的foo
  • 初始化的時候,編譯器把對constexpr函式的呼叫替換成結果值。為了能在編譯的時候隨時展開,constexpr函式被隱式的指定為行內函數
  • constexpr函式體可以包含其他語句,只要這些語句在執行時不執行任何操作就行
  • 允許constexpr返回值不是一個常量
constexpr size_t scale(size_t cnt) {
  return new_size() * cnt;
}
int arr[scale(2)]; //正確,scale(2)是一個常量表達式
int i = 2;
int a2[scale(i)]; //錯誤,i不是常量表達式
  • constexpr不一定返回常量表達式
  • 行內函數和constexpr函式允許在程式中被定義多次,但是多個定義必須完全一致

6.5.4 除錯幫助

程式可以包含一些用於除錯的程式碼,但是這些程式碼只在開發程式的時候使用,當程式完成釋出的時候要遮蔽掉這些程式碼,這種方法用到兩項預處理功能assertNDEBUG

  • assert預處理巨集
    • 所謂預處理巨集其實是一個預處理變數,行為類似於行內函數
    • assert巨集使用一個表示式作為條件: assert(expr)
      • 如果表示式為假(0),assert輸出資訊並終止程式執行
      • 如果表示式為真,則什麼也不做
    • assert常用於檢查不能發生的條件
  • NDEBUG預處理變數
    • assert行為依賴於名為NDEBUG的預處理變數的狀態
      • 如果定義了NDEBUG,則assert什麼也不做
      • 預設狀態下沒有定義,assert將執行執行時檢查
    • 可以使用define語句定義NDEBUG從而關閉除錯狀態
    • 很多編譯器也提供了命令列選項讓我們定義預處理變數
      • CC -D NDEBUG main.c等價於在main.c檔案的一開始寫#define DEBUG
    • 定義NDEFBUG避免各種檢查的開銷,assert只能作為除錯程式的輔助手段,不能代替真正執行時候的邏輯檢查,也不能代替程式應該包含的錯誤檢查
    • 除了assert,NDEBUG也可以用來編寫自己的條件除錯程式碼,如果NDEBUG未定義,將執行#ifndef和#endif之間的程式碼,如果定義了,這些程式碼會被忽略
void print() {
#ifndef NDEBUG 
    //__func__是編譯器定義的區域性靜態變數,用來存放函式的名字
    cout << __func__ << endl;
#endif
}

如上,除了__func__之外,還有4個編譯器定義的對於程式除錯有用的名字

  • FILE 存放檔名的字串字面值
  • LINE 存放當前行號的整形字面值
  • TIME 存放檔案編譯時間的字串字面值
  • DATA 存放檔案編譯日期的字串字面值

6.6 函式匹配

大多數情況下我們容易確定某次呼叫應該選用哪個過載函式,但是當幾個過載函式的形引數量相同並且某些形參的型別可以由其它型別轉換得來的時候,這項工作就不那麼容易了

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); //呼叫void f(double, double);

6.6.1 確定候選函式和可行函式

第一步:選定本次呼叫對應的過載函式集,集合中的函式稱為候選函式,候選函式有兩個特徵

  • 與被呼叫的函式同名
  • 宣告在呼叫點可見

第二步:考察呼叫提供的實參,從候選函式選出能被這組實參呼叫的函式,這些函式稱為可行函式,可行函式也有兩個特徵

  • 形引數量與本次呼叫提供的實引數量相等
  • 每個實參的型別與對應的形參型別相同,或者能轉換成形參的型別

6.6.2 尋找最佳匹配

第三步:從可行的函式中選擇與本次呼叫最匹配的函式,逐一檢查函式呼叫提供的實參,尋找形參型別與實參型別最匹配的可行函式。基本思想是:實參型別與形參型別越接近,他們匹配的越好

6.6.3 含有多個形參的函式匹配

當實參的數量含有兩個或者更多時,函式匹配就比較複雜了。編譯器選擇那些形引數量滿足要求且實參型別和形參型別能夠匹配的函式,如果檢查了所有實參後沒有任何一個函式脫穎而出,則該呼叫是錯誤的,編譯器將報告二義性呼叫資訊

  • tips:呼叫過載函式時應該避免強制型別轉換。如果實際應用中確實需要強制型別轉換,則說明我們設計的形參集合不合理。

6.6.4 實參型別轉換

為了確定最佳匹配,編譯器將實參型別到形參型別的轉換劃分為幾個等級,具體排序如下

  1. 精確匹配
    • 實參型別和形參型別相同
    • 實參從陣列型別或函式型別轉換為對應指標型別
    • 向實參新增頂層const或者從實參中刪除頂層const
  2. 通過const轉換實現的匹配
  3. 通過型別提升實現的匹配 (bool -> int)
  4. 通過算術型別匹配或指標轉換實現的匹配
  5. 通過類型別轉換實現的匹配

所有算術型別轉換的級別都一樣,從int向unsigned的轉換不比向double的轉換級別高

void func(float);
void func(long);
func(3.14); //錯誤:二義性呼叫

6.6.5 函式匹配和const實參

如果過載函式的區別在於引用型別的實參是否引用了const或者指標型別的形參是否指向const,則呼叫發生的時候編譯器通過實參是否是常量決定選擇哪個函式

Record lookup(Account&);
Record lookup(const Account&);
const Account a;
Account b;
lookup(a); //呼叫lookup(const Account&);
lookup(b); //呼叫lookup(Account&);

6.7 函式指標

函式指標指向的是函式而不是物件,和其他指標一樣,函式指標指向某種特定型別。函式的型別由他的返回值和形參型別共同決定,和函式名無關

bool lengthCmp(const string &,const string &);
//pf指向一個函式,函式的引數是兩個const string&,返回值是bool
bool (*pf)(const string &,const string &); //未初始化
//pf兩邊括號必不可少,否則變為一個返回值為bool*的函式

6.7.1 使用函式指標

pf = lengthCmp; //pf指向名為lengthCmp的函式
pf = &lengthCmp; //等價的語句,取地址符可選

bool b1 = pf("hello","goodbye"); //呼叫lengthCmp函式
bool b2 = (*pf)("hello","goodbye"); //等價呼叫
bool b3 = lengthCmp("hello","goodbye"); //等價呼叫

指向不同函式型別的指標之間不存在轉換規則,但是可以給函式指標賦nullptr或者0表示沒有指向任何一個函式

size_type sumLength(const string&, const string&);
bool cstringCmp(const char*, const char*);
pf = 0;
pf = sumLength; //錯誤:返回型別不匹配
pf = cstringCmp;//錯誤:形參型別不匹配
pf = lengthCmp; //正確

過載函式的指標

void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int ) = ff; //pf1指向ff
void (*pf2)(int) = ff; //錯誤,沒有任何一個ff形參列表和pf2匹配
double (*pf3)(int*) = ff; //錯誤,pf3返回型別和ff不匹配

函式指標形參

  • 和陣列類似,雖然不能定義函式型別的形參,但是形參可以是指向函式的指標,此時形參看起來是函式型別,實際上是當成形參使用
//第三個形參是函式型別,它會自動轉換為指向函式的指標
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 &));
//使用
useBigger(s1, s2, lengthCmp);//lengthCmp自動轉換為指向函式的指標

可以通過decltype讓我們簡化使用函式指標的程式碼

typedef bool Func(const string&, const string&);
typedef decltype(lengthCmp) Func2; //等價的型別
//useBigger等價宣告
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, Func2);

雖然不能返回函式,但是可以返回指向函式的指標,和往常一樣,最簡單的方法是使用類型別名

using F = int(int*, int);  //F是函式型別
using PF = int(*)(int*, int);  //PF是指標型別
PF f1(int);   //正確,PF是指向函式的指標,f1返回指向函式的指標
F f1(int);    //錯誤,F是函式型別
F *f1(int);   //正確,顯示的指定返回型別為指向函式的指標