《C++ Primer》學習筆記(六):C++模組設計——函式

我是管小亮 :)發表於2020-01-19

專欄C++學習筆記

《C++ Primer》學習筆記/習題答案 總目錄

——————————————————————————————————————————————————————

?? Cpp-Prime5 + Cpp-Primer-Plus6 原始碼和課後題

C++模組設計——函式

1、函式基礎

函式是一個命名了的程式碼塊,通過呼叫函式執行相應的程式碼。函式可以有0個或多個引數,而且(通常)會產生一個結果。可以過載函式,也就是說,同一個名字可以對應幾個不同的函式。

一個典型的 函式(function) 定義包括:返回型別(return type)函式名字、由0個或多個形式引數(parameter,簡稱形參)組成的 列表函式體(function body)。函式執行的操作在語句塊中說明,即函式體。

n的階乘是從1到n所有數字的乘積,程式如下:

// val的階乘是val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1)
int fact(int val)
{
    int ret = 1;    	// 區域性變數,用於儲存計算結果
    while (val > 1)
    	ret *= val--;   // 把ret和val的來積賦給ret,然後將val減1
    return ret;     	// 返回結果
}

程式通過 呼叫運算子(call operator) 來執行函式。呼叫運算子的形式之一是一對圓括號 (),作用於一個表示式,該表示式是函式或者指向函式的指標;圓括號內是一個用逗號隔開的 實際引數(argument,簡稱實參) 列表,用來初始化函式形參。呼叫表示式的型別就是函式的返回型別。

int main()
{
    int j = fact(5);    // j equals 120, i.e., the result of fact(5)
    cout << "5! is " << j << endl;
    return 0;
}

函式呼叫完成兩項工作:

  • 用實參初始化對應的形參;
  • 將控制權從主調函式轉移給被調函式。

此時,主調函式(calling function) 的執行被暫時中斷,被調函式(called function) 開始執行。

return 語句結束函式的執行過程,完成兩項工作:

  • 返回 return 語句中的值(如果有的話)。
  • 將控制權從被調函式轉移回主調函式,函式的返回值用於初始化呼叫表示式的結果,之後繼續完成呼叫所在的表示式的剩餘部分。

實參是形參的初始值,兩者的順序和型別必須一一對應,相應的數量也要一致。

函式的形參列表可以為空,但是不能省略。

void f1() { /* ... */ }      // 隱式地定義空形參列表
void f2(void) { /* ... */ }  // 顯式地定義空形參列表

形參列表中的形參通常用逗號隔開,每個形參都是含有一個宣告符的宣告,即使兩個形參型別一樣,也必須把兩個型別宣告都寫出來。

int f3(int v1, v2) { /* ... */ }      // 錯誤
int f4(int v1, int v2) { /* ... */ }  // 正確

函式的任意兩個形參不能同名,而且函式最外層作用域中的區域性變數也不能使用與函式形參一樣的名字。形參的名字是可選的,但是由於無法使用未命名的形參,所以形參一般都應該有個名字,即使某個形參不被函式使用,也必須為它提供一個實參。

函式的返回型別不能是陣列型別或者函式型別,但可以是指向陣列或函式的指標。

1)區域性物件

在C++語言中,名字有作用域,物件有 生命週期(lifetime)

  • 名字的作用域是程式文字的一部分, 名字在其中可見;
  • 物件的生命週期是程式執行過程中該物件存在的一段時間。

形參和函式體內定義的變數統稱為 區域性變數(local variable),僅在函式的作用域內可見。

只存在於塊執行期間的物件稱為 自動物件(automatic object),當塊的執行結束後,塊中建立的自動物件的值就變成未定義的了。形參是一種自動物件,在函式開始時為形參申請儲存空間,因為形參定義在函式體作用域之內,所以一旦函式終止,形參也就被銷燬。

區域性靜態物件(local static object) 在程式的執行路徑第一次經過物件定義語句時初始化,並且直到程式結束才被銷燬,物件所在的函式結束執行並不會對它產生影響。在變數型別前新增關鍵字 static 可以定義區域性靜態物件。

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;
}

如果區域性靜態物件沒有顯式的初始值,它將執行值初始化,內建型別的區域性靜態變數初始化為0 。

2)函式宣告

和其他名字一樣,函式的名字也必須在使用之前宣告。和變數類似,函式只能定義一次,但可以宣告多次,函式宣告也叫做 函式原型(function prototype)。函式的宣告和函式的定義非常類似,唯一的區別是函式宣告無須函式體,用一個分號
替代即可。

函式應該在標頭檔案中宣告,在原始檔中定義。定義函式的原始檔應該包含含有函式宣告的標頭檔案,編譯器負責驗證函式的定
義和宣告是否匹配。

3)分離式編譯

分離式編譯(separate compilation) 允許我們把程式按照邏輯關係分割到幾個檔案中去,每個檔案獨立編譯。這一過程通常會產生字尾名是 .obj(Windows) 或 .o(UNIX) 的檔案,該檔案包含 物件程式碼(object code)。之後編譯器把物件檔案 連結(link) 在一起形成可執行檔案。

2、引數傳遞

每次呼叫函式時都會重新建立它的形參,並用傳入的實參對形參進行初始化。形參初始化的機理與變數初始化一樣。

形參的型別決定了形參和實參互動的方式:

  • 當形參是引用型別時,它對應的實參被 引用傳遞(passed by reference),函式被 傳引用呼叫(called by reference)。引用形參是它對應實參的別名。
  • 當形參不是引用型別時,形參和實參是兩個相互獨立的物件,實參的值會被拷貝給形參(值傳遞,passed by value),函式被 傳值呼叫(called by value)

1)傳值引數

如果形參不是引用型別,則函式對形參做的所有操作都不會影響實參。

使用指標型別的形參可以訪問或修改函式外部的物件。

#include <iostream>
using namespace std;
int main()
{
	int n = 0, i = 42;
	int *p = &n, *q = &i; // p指向n; q指向i
	*p = 42;			  // n的值改變; p不變
	p = q;				  // p現在指向了i; 但是i和n的值都不變
	
	cout << "n:" << n << endl;
	cout << "i:" << i << endl;
	system("pause");
	return 0;
}

在這裡插入圖片描述
指標形參的行為與之類似:

// 該函式接受一個指標,然後將指標所指的位置為0
void reset(int *ip) {
	*ip = 0;					// 改變指標ip所指物件的值
	ip = 0;						// 只改變了ip的區域性拷貝,實參未被改變
}

呼叫 reset 函式之後, 實參所指的物件被置為0,但是實參本身並沒有改變:

int i = 42;
reset(&i);						// 改變i的值而非i的地址
cout << "i = " << i << endl;	// 輸出i = 0

在這裡插入圖片描述
如果想在函式體內訪問或修改函式外部的物件,建議使用引用形參代替指標形參。

2)傳引用引數

通過使用引用形參,函式可以改變實參的值。

#include <iostream>
using namespace std;

int main()
{
	int n = 0, i = 42;
	int &r = n; 		// r繫結了n(即r是n的另一個名字)
	r = 42;				// 現在n的值是42
	r = i;				// 現在n的值和i相同
	cout << "n = " << n << endl;
	system("pause");
	return 0;
}

在這裡插入圖片描述
引用形參的行為與之類似:

// 該函式接受一個int物件的引用,然後將物件的位置為0
void reset(int &i)  			// i是傳給reset函式的物件的另一個名字
{
    i = 0;  					// 改變了i所引物件的值
}

引用形參繫結初始化它的物件,即引用直接傳入物件而無須傳遞物件的地址。

int j = 42;
reset(j);						// j採用傳引用方式,它的值被改變
cout << "j = " << j << endl;	// 輸出j = 0

在這裡插入圖片描述
使用引用形參可以避免拷貝操作,拷貝大的類型別物件或容器物件比較低效,另外有的類型別(如IO型別)根本就不支援拷貝操作,這時只能通過引用形參訪問該型別的物件。

除了內建型別、函式物件和標準庫迭代器外,其他型別的引數建議以引用方式傳遞。

如果函式無須改變引用形參的值,最好將其宣告為常量引用。

一個函式只能返回一個值,然而有時函式需要同時返回多個值,這個時候可以使用引用形參讓函式返回額外資訊。

3)const形參和實參

關於 const,寫了一個部落格——【C++100問】深入理解理解頂層const和底層const,如果有必要,可以寫一個這個系列的文章。

當形參有頂層 const 時,傳遞給它常量物件或非常量物件都是可以的。

void fcn(const int i) { /* fcn能夠讀取i,但是不能向i寫值 */ }

呼叫 fcn 函式時,既可以傳入 const int 也可以傳入 int。忽略掉形參的頂層 const 可能產生意想不到的結果:

void fcn(const int i) { /* fcn能夠讀取i,但是不能向i寫值 */ }
void fcn(int i) { /* ... */ } 	// 錯誤:重複定義了fcn(int)

可以使用非常量物件初始化一個底層 const 形參,但是反過來不行。

把函式不會改變的形參定義成普通引用會極大地限制函式所能接受的實參型別,同時也會給別人一種誤導,即函式可以修改實參的值。此外,使用引用而非常量引用也會極大地限制函式所能接受的實參型別。

4)陣列形參

陣列的兩個特殊性質對定義和使用作用在陣列上的函式有影響,分別是:不允許拷貝陣列以及使用陣列時(通常)會將其轉換成指標。

  • 因為不能拷貝陣列,所以無法以值傳遞的方式使用陣列引數,但是可以把形參寫成類似陣列的形式。
// 儘管形式不同,但這三個print函式是等價的
// 每個函式都有一個const int*型別的形參
void print(const int*);
void print(const int[]);    // 可以看出來,函式的意圖是作用於一個陣列
void print(const int[10]);  // 這裡的維度表示我們期望陣列含有多少元素,實際不一定
  • 因為陣列會被轉換成指標,所以當我們傳遞給函式一個陣列時,實際上傳遞的是指向陣列首元素的指標。
int i = 0, j[2] = {0, 1};
print(&i); 					// 正確:&i的型別是int*
print(j); 					// 正確: j轉換成int*並指向j[0]

因為陣列是以指標的形式傳遞給函式的,所以一開始函式並不知道陣列的確切尺寸,呼叫者應該為此提供一些額外資訊。管理指標形參有三種常用的技術:

  • 要求陣列本身包含一個結束標記;
  • 傳遞指向陣列首元素和尾後元素的指標;
  • 專門定義一個表示陣列大小的形參。

以陣列作為形參的函式必須確保使用陣列時不會越界。

如果函式不需要對陣列元素執行寫操作,應該把陣列形參定義成指向 const 的指標。只有當函式確實要改變元素值的時候,才把形參定義成指向非常量的指標。

形參可以是陣列的引用,但此時維度是形參型別的一部分,函式只能作用於指定大小的陣列。

//正確: 形參是陣列的引用,維度是型別的一部分
void print(int (&arr) [10]) {
	for (auto elem : arr)
		cout << elem << endl;
}

// &arr兩端的括號必不可少:
// f(int &arr[10]) 		// 錯誤:將arr宣告成了引用的陣列
// f(int (&arr)[10]) 	// 正確:arr是具有10個整數的整型陣列的引用

將多維陣列傳遞給函式時,真正傳遞的是指向陣列首元素的指標,陣列第二維(以及後面所有維度)的大小是陣列型別的一部分,不能省略。

// matrix指向陣列的首元素,該陣列的元素是由10個整數構成的陣列
void print(int (*matrix)[10], int rowSize) { /* ... */ }

// *matrix兩端的括號必不可少:
int *matrix[10]; 		// 10個指標構成的陣列
int (*matrix)[10]; 		// 指向含有10個整數的陣列的指標

// 等價定義
void print(int matrix[][10], int rowSize) { /* ... */ }

5)main:處理命令列選項

可以在命令列中向 main 函式傳遞引數,形式如下:

int main(int argc, char *argv[]) { /*...*/ }
int main(int argc, char **argv) { /*...*/ }
  • 第一個形參 argc 表示陣列中字串的數量;
  • 第二個形參 argv 是一個陣列,陣列元素是指向C風格字串的指標。

當實參傳遞給 main 函式後,argv 的第一個元素指向程式的名字或者一個空字串,接下來的元素依次傳遞命令列提供的實參,最後一個指標之後的元素值保證為0。

當使用argv中的實參時,一定要記得可選的實參從argv[1]開始;argv[0]儲存程式的名字,而非使用者輸入。

Visual Studio2013 中可以設定 main 函式除錯引數:

在這裡插入圖片描述

6)含有可變形參的函式

C++11新標準提供了兩種主要方法處理實引數量不定的函式:

  • 如果實參型別相同,可以使用 initializer_list 標準庫型別;

  • 如果實參型別不同,可以定義可變引數模板。

  • C++還可以使用省略符形參傳遞可變數量的實參,但這種功能一般只用在與C函式交換的介面程式中。

initializer_list 是一種標準庫型別,定義在標頭檔案 initializer_list 中,表示某種特定型別的值的陣列。

initializer_list 提供的操作:

在這裡插入圖片描述

  • vector 一樣,initializer_list 也是一種模板型別,定義物件時,必須說明列表中所含元素的型別
  • vector 不一樣的是,initializer_list 物件中的元素永遠是常量值,無法改變

使用如下的形式編寫輸出錯誤資訊的函式,使其可以作用於可變數量的實參:

void error_msg(initializer_list<string> il)
{
    for (auto beg = il.begin(); beg != il.end(); ++beg)
    	cout << *beg << " " ;
    cout << endl;
}

拷貝或賦值一個 initializer_list 物件不會拷貝列表中的元素,拷貝後,原始列表和副本共享元素。initializer_list 物件中的元素永遠是常量值。如果想向 initializer_list 形參傳遞一個值的序列,則必須把序列放在一對花括號內。

// expected和actual是string物件
if (expected != actual)
    error_msg({"functionX", expected, actual});
else
    error_msg({"functionX", "okay"});

因為 initializer_list 包含 beginend 成員,所以可以使用範圍 for 迴圈處理其中的元素。

省略符形參是為了便於C++程式訪問某些特殊的C程式碼而設定的,這些程式碼使用了名為 varargs 的C標準庫功能。通常,省略符形參不應該用於其他目的。

省略符形參應該僅僅用於C和C++通用的型別,大多數類型別的物件在傳遞給省略符形參時都無法正確拷貝。

void foo(parm_list, ...);
void foo(...);

3、返回型別和return語句

return 語句有兩種形式,作用是終止當前正在執行的函式並返回到呼叫該函式的地方。

return;
return expression;

1)無返回值函式

沒有返回值的 return 語句只能用在返回型別是 void 的函式中。返回 void 的函式可以省略 return 語句,因為在這類函式的最後一條語句後面會隱式地執行 return

通常情況下,如果 void 函式想在其中間位置提前退出,可以使用 return 語句,return 的這種用法有點類似於用 break 語句退出迴圈。

void swap (int &vl , int &v2) {
	// 如果兩個值是相等的,則不需要交換,直接退出
	if (v1 == v2)
		return;
	// 如果程式執行到了這裡,說明還需要繼續完成某些功能
	int tmp = v2;
	v2 = v1;
	v1 = tmp;
	// 此處無須顯式的return語句
}

一個返回型別是 void 的函式也能使用 return 語句的第二種形式,不過此時 return 語句的 expression 必須是另一個返回 void 的函式,強行令 void 函式返回其他型別的表示式將產生編譯錯誤。

2)有返回值函式

return 語句的第二種形式提供了函式的結果。只要函式的返回型別不是 void,該函式內的每條 return 語句就必須返回一個值,並且返回值的型別必須與函式的返回型別相同,或者能隱式地轉換成函式的返回型別(main 函式例外)。

  • return 語句沒有返回值是錯誤的,編譯器能檢測到這個錯誤。
  • 在含有 return 語句的迴圈後面應該也有一條 return 語句,否則程式就是錯誤的,但很多編譯器無法發現此錯誤。

函式返回一個值的方式和初始化一個變數或形參的方式完全一樣:返回的值用於初始化呼叫點的一個臨時量,該臨時量就是函式呼叫的結果。如果函式返回引用型別,則該引用僅僅是它所引用物件的一個別名。

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

函式不應該返回區域性物件的指標或引用,因為一旦函式完成,區域性物件將被釋放,指標將指向一個不存在的物件。


如果函式返回指標、引用或類的物件,則可以使用函式呼叫的結果訪問結果物件的成員。

呼叫一個返回引用的函式會得到左值,其他返回型別得到右值。

C++11規定,函式可以返回用花括號包圍的值的列表。同其他返回型別一樣,列表也用於初始化表示函式呼叫結果的臨時量。如果列表為空,臨時量執行值初始化;否則,返回的值由函式的返回型別決定。

  • 如果函式返回內建型別,則列表內最多包含一個值,且該值所佔空間不應該大於目標型別的空間。

  • 如果函式返回類型別,由類本身定義初始值如何使用。

    vector<string> process()
    {
        // . . .
        // expected和actual是string物件
        if (expected.empty())
            return {};  								// 返回一個vector物件
        else if (expected == actual)
            return {"functionX", "okay"};  // 返回列表初始化的vector物件
        else
            return {"functionX", expected, actual};
    }
    

如果函式的返回型別不是 void,那麼它必須返回一個值,除了,main 函式可以沒有 return 語句直接結束。如果控制流到達了 main 函式的結尾處並且沒有return語句,編譯器會隱式地插入一條返回0的 return 語句。

main 函式的返回值可以看作是狀態指示器。返回0表示執行成功,返回其他值表示執行失敗,其中非0值的具體含義依機器而定。

為了使 main 函式的返回值與機器無關,標頭檔案 cstdlib 定義了 EXIT_SUCCESSEXIT_FAILURE 這兩個預處理變數,分別表示執行成功和失敗。

int main()
{
    if (some_failure)
        return EXIT_FAILURE; // 定義在cstdlib標頭檔案中
    else
        return EXIT_SUCCESS; // 定義在cstdlib標頭檔案中
}

建議 使用預處理變數 EXIT_SUCCESSEXIT_FAILURE 表示 main 函式的執行結果。

如果一個函式呼叫了它自身,不管這種呼叫是直接的還是間接的,都稱該函式為 遞迴函式(recursive function)

// 計算val!,即1 * 2 * 3 . . . * val
int factorial(int val)
{
    if (val > 1)
        return factorial(val-1) * val;
    return 1;
}

在遞迴函式中,一定有某條路徑是不包含遞迴呼叫的,否則函式會一直遞迴下去,直到程式棧空間耗盡為止。

相對於迴圈迭代,遞迴的效率較低,但在某些情況下使用遞迴可以增加程式碼的可讀性。

  • 迴圈迭代適合處理線性問題(如連結串列,每個節點有唯一前驅、唯一後繼),
  • 而遞迴適合處理非線性問題(如樹,每個節點的前驅、後繼不唯一)。

main函式不能呼叫它自身。

3)返回陣列指標

因為陣列不能被拷貝,所以函式不能返回陣列,但可以返回陣列的指標或引用。

返回陣列指標的函式形式如下:

Type (*function(parameter_list))[dimension]    

其中 Type 表示元素型別,dimension 表示陣列大小,(*function (parameter_list)) 兩端的括號必須存在,如果沒有這對括號,函式的返回型別將是指標的陣列。

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型別

C++11允許使用 尾置返回型別(trailing return type) 簡化複雜函式宣告。尾置返回型別跟在形參列表後面,並以一個 -> 符號開頭。為了表示函式真正的返回型別在形參列表之後,需要在本應出現返回型別的地方新增 auto 關鍵字。

// func接受一個int型別的實參,返回一個指標,該指標指向含有10個整數的陣列
auto func(int i) -> int(*)[10];

任何函式的定義都能使用尾置返回型別,但是這種形式更適用於返回型別比較複雜的函式。

如果我們知道函式返回的指標將指向哪個陣列,就可以使用 decltype 關鍵字宣告返回型別。但 decltype 並不會把陣列型別轉換成指標型別,所以還要在函式宣告中新增一個 * 符號。

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// 返回一個指標,該指標指向含有5個整數的陣列
decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even;  // 返回一個指向陣列的指標
}

4、函式過載

同一作用域內的幾個名字相同但形參列表不同的函式叫做 過載函式

void print(const char *cp);
void print(const int *beg, const int *end); 
void print (const int ia[), size_t ze);

函式的名字僅僅是讓編譯器知道它呼叫的是哪個函式,而函式過載可在一定程度上減輕程式設計師起名字、記名字的負擔。

main 函式不能過載。

不允許兩個函式除了返回型別以外的其他所有要素都相同。

Record lookup(const Account&); 
bool lookup(const Account&); 	// 錯誤:與上一個函式相比只有返回型別不同

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

Record lookup(Phone);
Record lookup(const Phone);  // 重複宣告瞭Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const); // 重複宣告瞭Record lookup(Phone*)

如果形參是某種型別的指標或引用,則通過區分其指向的物件是常量還是非常量可以實現函式過載,此時的const是底層的。

// 對於接受引用或指標的函式來說,物件是常量還是非常量對應的形參不同
// 定義了4個獨立的過載函式
Record lookup(Account&);        // 函式作用於Account的引用
Record lookup(const Account&);  // 新函式,作用於常量引用

Record lookup(Account*);        // 新函式,作用於指向Account的指標
Record lookup(const Account*);  // 新函式,作用於指向常量的指標

當我們傳遞給過載函式一個非常量物件或者指向非常量物件的指標時,編譯器會優先選用非常量版本的函式。

const_cast 可以用於函式的過載。

當函式的實參是常量時,返回的結果仍然是常量的引用。

// 比較兩個string物件的長度,返回較短的那個引用
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);
}

函式匹配(function matching) 也叫做 過載確定(overload resolution),是指編譯器將函式呼叫與一組過載函式中的某一個進行關聯的過程。

編譯器首先將呼叫的實參與過載集合中每一個函式的形參進行比較,然後根據比較的結果決定到底呼叫是哪個函式。

呼叫過載函式時有三種可能的結果:

  • 編譯器找到一個與實參 最佳匹配(best match) 的函式,並生成呼叫該函式的程式碼。
  • 編譯器找不到任何一個函式與實參匹配,發出 無匹配(no match) 的錯誤資訊。
  • 有一個以上的函式與實參匹配,但每一個都不是明顯的最佳選擇,此時編譯器發出 二義性呼叫(ambiguous call) 的錯誤資訊。

1)過載與作用域

一般來說,將函式宣告直 於區域性作用域內不是一個明智的選擇。

在不同的作用域中無法過載函式名。一旦在當前作用域內找到了所需的名字,編譯器就會忽略掉外層作用域中的同名實體。

string read();
void print(const string &);
void print(double);     // 過載print函式
void fooBar(int ival)
{
    bool read = false;  // 新作用域:隱藏了外層的read
    string s = read();  // 錯誤:read是一個布林值,而非函式
    // 不好的習慣:通常來說,在區域性作用域中宣告函式不是一個好的選擇
    void print(int);    // 新作用域:隱藏了之前的print
    print("Value: ");   // 錯誤:print(const string &)被隱藏掉了
    print(ival);    	// 正確:當前print(int)可見
    print(3.14);    	// 正確:呼叫print(int); print(doub1e)被隱藏掉了
}

在C++中,名字查詢發生在型別檢查之前。

5、特殊用途語言特性

  • 預設實參
  • 行內函數
  • constexpr 函式

1)預設實參

預設實參 作為形參的初始值出現在形參列表中。可以為一個或多個形參定義預設值,不過一旦某個形參被賦予了預設值,它後面的所有形參都必須有預設值。

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');

呼叫含有預設實參的函式時,可以包含該實參,也可以省略該實參。

如果想使用預設實參,只要在呼叫函式的時候省略該實參即可。

雖然多次宣告同一個函式是合法的,但是在給定的作用域中一個形參只能被賦予一次預設實參。函式的後續宣告只能為之前那些沒有預設值的形參新增預設實參,而且該形參右側的所有形參必須都有預設值。

// 表示高度和寬度的形參沒有預設位
string screen(sz, sz, char = ' ');

string screen(sz, sz, char = '*');      // 錯誤:重複宣告
string screen(sz = 24, sz = 80, char);  // 正確:新增預設實參

預設實參只能出現在函式宣告和定義其中一處。通常應該在函式宣告中指定預設實參,並將宣告放在合適的標頭檔案中。

區域性變數不能作為函式的預設實參。

用作預設實參的名字在函式宣告所在的作用域內解析,但名字的求值過程發生在函式呼叫時。

// wd、def和ht的宣告必須出現在函式之外
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, '*')
}

2)行內函數和constexpr函式

行內函數會在每個呼叫點上“內聯地”展開,省去函式呼叫所需的一系列工作。

定義行內函數時需要在函式的返回型別前新增關鍵字 inline

// 內聯版本:尋找兩個string物件中較短的那個
inline const string &
shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

在函式宣告和定義中都能使用關鍵字 inline,但是建議只在函式定義時使用。

一般來說,內聯機制適用於優化規模較小、流程直接、呼叫頻繁的函式。行內函數中不允許有迴圈語句和 switch 語句,否則函式會被編譯為普通函式。


constexpr 函式是指能用於常量表示式的函式。constexpr 函式的返回型別及所有形參的型別都得是字面值型別。

另外C++11標準要求 constexpr 函式體中必須有且只有一條 return 語句,但是此限制在C++14標準中被刪除。

constexpr 函式的返回值可以不是一個常量。

// 如果arg是常量表示式,則scale(arg)也是常量表示式
constexpr size_t scale(size_t cnt) 
{ 
    return new_sz() * cnt; 
}

scale 的實參是常量表示式時,它的返回值也是常量表示式;反之則不然:

int arr[scale(2)];  // 正確:scale(2)是常量表示式
int i = 2;          // i不是常量表示式
int a2[scale(i)];   // 錯誤:scale(i)不是常量表示式

constexpr 函式不一定返回常量表示式。

和其他函式不同,行內函數和 constexpr 函式可以在程式中多次定義。因為在編譯過程中,編譯器需要函式的定義來隨時展開函式,僅有函式的宣告是不夠的。對於某個給定的行內函數或 constexpr 函式,它的多個定義必須完全一致。因此行內函數和 constexpr 函式通常定義在標頭檔案中。

3)除錯幫助

1_assert 預處理巨集

assert 是一種預處理巨集。所謂預處理巨集其實是一個預處理變數,它的行為有點類似於行內函數。

assert (expr);

首先對 expr 求值,如果表示式為假(即0),assert 輸出資訊並終止程式的執行;如果
表示式為真(即非0),assert什麼也不做。

2_NDE8UG 預處理變數

assert 的行為依賴於於一個名為 NDEBUG 的預處理變數的狀態。

  • 如果定義了 NDEBUG,則 assert 什麼也不做;
  • 預設狀態下沒有定義 NDEBUG,此時 assert 將執行執行時檢查。

可以使用 #define 語句定義 NDEBUG,從而關閉除錯狀態。

常用的幾個對於程式除錯很有用的名字:

變數名稱 內容
__func__ 輸出當前除錯的函式的名稱
__FILE__ 存放檔名的字串字面值
__LINE__ 存放當前行號的整型字面值
__TIME__ 存放檔案編譯時間的字串字面值
__DATE__ 存放檔案編譯日期的字串字面值

6、函式匹配

函式實參型別與形參型別越接近,它們匹配得越好。

過載函式集中的函式稱為 候選函式(candidate function)

  • 一是與被呼叫的函式同名;
  • 二是其宣告在呼叫點可見。

可行函式(viable function)

  • 一是形引數量與函式呼叫所提供的實引數量相等;
  • 二是每個實參的型別與對應的形參型別相同,或者能轉換成形參的型別。

如果沒找到可行函式,編譯器講報告無匹配函式的錯誤。

呼叫過載函式時應該儘量避免強制型別轉換。

1)實參型別轉換

為了確定最佳匹配,編譯器將~參型別到形參型別的轉換戈 分成兒個等級 具體卡11
如下所示:

  1. 精確匹配 包括以下情況:
    • 實參型別和形參型別相同
    • 實參從陣列型別或函式型別轉換成對應的指標型別
    • 向實參新增頂層 const 或者從實參中刪除頂層 const
  2. 通過 const 轉換實現的匹配
  3. 通過型別提升實現的匹配
  4. 通過算術型別轉換或指標轉換實現的匹配
  5. 通過類型別轉換實現的匹配

所有算術型別轉換的級別都一樣。

如果過載函式的區別在於它們的引用或指標型別的形參是否含有底層 const,或者指標型別是否指向 const,則呼叫發生時編譯器通過實參是否是常量來決定函式的版本。

Record lookup(Account&);    	// 函式的引數是Account的引用
Record lookup(const Account&);  // 函式的引數是一個常量引用
const Account a;
Account b;
lookup(a);  					// 呼叫lookup(const Account&)
lookup(b);  					// 呼叫lookup(Account&)

7、函式指標

要想宣告一個可以指向某種函式的指標,只需要用指標替換函式名稱即可。

// 比較兩個string物件的長度
bool lengthCompare(const string &, const string &);
// pf指向一個函式,該函式的引數是兩個const string的引用,返回值是bool型別
bool (*pf)(const string &, const string &); // uninitialized

*pf 兩端的括號必不可少!!!如果不寫這對括號,則 pf 是一個返回值為 bool 指標的函式:

// 宣告一個名為pf的函式,該函式返回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");    // 另一個等價的呼叫

對於過載函式,上下文必須清晰地界定到底應該選用了哪個函式,編譯器通過指標型別決定函式版本,指標型別必須與過載函式中的某一個精確匹配。

void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)

void (*pf2)(int) = ff; 		// 錯誤:沒有任何一個ff與該形參列表匹配
double (*pf3)(int*) = ff; 	// 錯誤:ff和pf3的返回型別不匹配

可以把函式的形參定義成指向函式的指標。呼叫時允許直接把函式名當作實參使用,它會自動轉換成指標。

// 第三個形參是函式型別,它會自動地轉換成指向函式的指標
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 作用於函式時,返回的是函式型別,而不是函式指標型別。

函式可以返回指向函式的指標。但返回型別不會像函式型別的形參一樣自動地轉換成指標,必須顯式地將其指定為指標型別,即加上 *

參考文章

  • 《C++ Primer》

相關文章