函數(shù)是一個命了名的代碼塊,我們通過調(diào)用函數(shù)執(zhí)行相應(yīng)的代碼。函數(shù)可以有零個或者多個參數(shù),而且(通常)會產(chǎn)生一個結(jié)果。可以重載函數(shù),也就是說,同一個名字可以對應(yīng)幾個不同的函數(shù)。
6.1 函數(shù)基礎(chǔ)
典型的函數(shù)定義包含以下部分:
- 返回類型。
- 函數(shù)名。
- 由零個或者多個形參組成的列表。
- 函數(shù)體。
每個形參使用逗號隔開,形參的列表位于一對圓括號內(nèi)。函數(shù)執(zhí)行的操作在語句塊中,這個語句塊被稱為函數(shù)體。
通過調(diào)用運(yùn)算符來執(zhí)行函數(shù)。調(diào)用運(yùn)算符的形式是一對圓括號,該符號作用于一對表達(dá)式,該表達(dá)式是函數(shù)或者指向函數(shù)的指針:圓括號里面是一個用逗號隔開的實(shí)參列表,我們使用實(shí)參初始化函數(shù)的形參。調(diào)用表達(dá)式的類型就是函數(shù)的返回類型。
編寫函數(shù)
調(diào)用函數(shù)
函數(shù)的調(diào)用完成兩項(xiàng)工作:首先用實(shí)參初始化函數(shù)對應(yīng)的形參,然后將控制權(quán)轉(zhuǎn)移給被調(diào)用函數(shù)。這時主調(diào)函數(shù)的執(zhí)行被暫時中斷,被調(diào)函數(shù)開始執(zhí)行。
return語句完成兩項(xiàng)工作:返回return語句中的值(如果有的話),將控制權(quán)從被調(diào)函數(shù)轉(zhuǎn)移回主調(diào)函數(shù)。函數(shù)的返回值用于初始化調(diào)用表達(dá)式的結(jié)果,之后繼續(xù)完成調(diào)用所在的表達(dá)式的剩余部分。
形參和實(shí)參
實(shí)參是形參的初始值。實(shí)參初始化位置對應(yīng)的形參。盡管這種對應(yīng)關(guān)系存在,但是并沒有規(guī)定實(shí)參的求值順序。編譯器能以任意可行的順序?qū)?shí)參求值。
實(shí)參的類型必須與對應(yīng)的形參類型匹配。函數(shù)有幾個形參,我們就必須提供相同數(shù)量的實(shí)參。
函數(shù)的形參列表
函數(shù)的形參列表可以為空,但是不能省略。要想定義一個不帶形參的函數(shù),最常用的辦法就是書寫一個空的形參列表,當(dāng)然也可以使用關(guān)鍵字void表示函數(shù)沒有形參。
每個形參都必須含有一個類型聲明符。
任意兩個形參都不能同名,而且函數(shù)最外層作用域中的局部變量也不能使用與函數(shù)形參一樣的名字。
形參名是可選的,但是由于我們無法使用未命名的形參,所以形參必須起名。
函數(shù)返回類型
返回類型void表示函數(shù)不返回任何值。函數(shù)的返回類型不能是數(shù)組類型或者函數(shù)類型,但是可以是指向函數(shù)或者數(shù)組的指針。
6.1節(jié)練習(xí)
練習(xí) 6.1:實(shí)參和形參的區(qū)別是什么?
形參是在函數(shù)定義時寫在函數(shù)名之后的列表,表明該函數(shù)接受幾個什么類型的參數(shù)。實(shí)參是函數(shù)調(diào)用時函數(shù)用戶寫在調(diào)用運(yùn)算符中的值,是形參的初始值。
練習(xí) 6.2:請指出下列函數(shù)哪個有錯誤,為什么?應(yīng)該如何修改這些錯誤呢?
// a:
int f() {
std::string s;
// ...
return s;
}
// 返回值的類型和函數(shù)返回類型不匹配
// 修改后:
std::string f() {
std::string s;
// ...
return s;
}
// b:
f2(int i) { /* ... */ }
// 缺少返回類型,如果沒有返回值,那么返回類型為void
// 修改后:
void f2(int i) { /* ... */ }
// c:
int calc(int v1, int v2) /* ... */ }
// 函數(shù)體花括號不成對
// 修改后:
int calc(int v1, int v2) { /* ... */ }
// d:
double square(double x) return x * x;
// 函數(shù)體必須使用花括號括起來
// 修改后:
double square(double x) { return x * x; }
練習(xí) 6.3:編寫你自己的fact函數(shù),上機(jī)檢查是否正確。
int fact(int n)
{
int ret = 1;
for (int i = 1; i <= n; ++i) ret *= i;
return ret;
}
練習(xí) 6.4:編寫一個與用戶交互的函數(shù),要求用戶輸入一個數(shù)字,計(jì)算生成該數(shù)字的階乘。在main函數(shù)中調(diào)用該函數(shù)。
#include <iostream>
void user_fact();
int main(int argc, char const *argv[])
{
user_fact();
return 0;
}
void user_fact()
{
std::cout << "Enter a positive integer: ";
int input_number;
std::cin >> input_number;
int res = 1;
for (int i = 1; i <= input_number; ++i) res *= i;
std::cout << "The result is: " << res << std::endl;
}
練習(xí) 6.5:編寫一個函數(shù)輸出其實(shí)參的絕對值。
#include <iostream>
void abs(double val)
{
if (val >= 0) std::cout << val << std::end;
else std::cout << -val < std::endl;
}
6.1.1 局部對象
名字有作用域,對象有生命周期。
- 名字的作用域是程序文本的一部分,名字在其中可見。
- 對象的生命周期是程序執(zhí)行過程中該對象存在的一段時間。
函數(shù)體是一個語句塊。塊構(gòu)成一個新的作用域,我們可以在其中定義變量。形參和函數(shù)體內(nèi)部定義的變量統(tǒng)稱為局部變量。它們僅僅在函數(shù)的作用域內(nèi)可見,同時局部變量還會覆蓋在外層作用域中同名的其他所有同名的聲明。
在所有函數(shù)體之外定義的對象存在于程序的整個執(zhí)行過程之中。此類對象在程序啟動時被創(chuàng)建,直到程序結(jié)束才會被銷毀。局部變量的生命周期依賴于定義的方式。
自動對象
自動對象指的是:只存在于塊執(zhí)行期間的對象。當(dāng)塊的執(zhí)行結(jié)束后,塊中創(chuàng)建的自動對象的值就變成未定義的了。
形參就是一種自動對象。函數(shù)開始時為形參申請存儲空間,因?yàn)樾螀⒍x在函數(shù)體作用域之內(nèi),所以一旦函數(shù)終止,形參也就被銷毀。
使用傳遞給函數(shù)的實(shí)參初始化形參對應(yīng)的自動對象。對于局部變量,也就是聲明定義在函數(shù)體內(nèi)的變量來說分兩種情況:如果變量定義本身含有初始值,就用這個初始值進(jìn)行初始化;否則,如果變量定義本身不含初始值,執(zhí)行默認(rèn)初始化。這里表明:內(nèi)置類型的未初始化局部變量將產(chǎn)生未定義的值。
局部靜態(tài)對象
局部靜態(tài)對象在程序的執(zhí)行路徑第一次經(jīng)過對象定義語句時初始化,直到程序終止才被銷毀。在此期間即使對象所在的函數(shù)結(jié)束執(zhí)行也不會對它產(chǎn)生影響。
如果局部靜態(tài)變量沒有顯式的初始值,它將執(zhí)行值初始化,內(nèi)置類型的局部靜態(tài)變量初始化為0。
6.1.1節(jié)練習(xí)
練習(xí) 6.6:說明形參、局部變量以及局部靜態(tài)變量的區(qū)別。編寫一個函數(shù),同時用到這三種形式。
形參是一種自動對象,必須由調(diào)用者提供相對應(yīng)的實(shí)參進(jìn)行初始化,如果不能提供足夠數(shù)量的實(shí)參,那么編譯器會報(bào)錯。局部變量是聲明在塊內(nèi)部的變量,生命周期是變量聲明到塊作用域結(jié)束,內(nèi)置類型變量如果不提供初始值,將執(zhí)行值初始化,也是一種未定義的行為。局部靜態(tài)變量存在于整個程序執(zhí)行過程之中,也就是說一旦聲明,直到程序結(jié)束之前都是存在的,塊的執(zhí)行完成與否不影響其生命周期。
int func(int val)
{
static int times_of_calling = 0;
int local_var = 10;
}
練習(xí) 6.7:編寫一個函數(shù),當(dāng)它第一次被調(diào)用時返回0,以后每次被調(diào)用返回值加1。
int func()
{
static int ret = -1;
ret += 1;
return ret;
}
6.1.2 函數(shù)聲明
函數(shù)的名字必須在使用前聲明。函數(shù)只能定義一次,但是可以聲明多次。如果一個函數(shù)永遠(yuǎn)不會被用到,那么它可以只有聲明沒有定義。
函數(shù)的聲明無須函數(shù)體,使用一個分號代替即可。
函數(shù)的聲明不包含函數(shù)體,所以也就無須形參的名字。當(dāng)然,我們建議寫出形參的名字,它可以幫助使用者或者程序員更好的理解函數(shù)的功能。
函數(shù)的三要素(返回類型,函數(shù)名,形參類型)描述了函數(shù)的接口,說明了調(diào)用該函數(shù)所需的全部信息。函數(shù)聲明也稱作函數(shù)原型。
在頭文件中進(jìn)行函數(shù)聲明
我們建議變量和函數(shù)在頭文件中聲明,在源文件中定義。
含有函數(shù)聲明的頭文件應(yīng)該被包含到定義函數(shù)的源文件中。
6.1.2節(jié)練習(xí)
練習(xí) 6.8:編寫一個名為Chapter6.h的頭文件,令其包含6.1節(jié)練習(xí)(第184頁)中的函數(shù)聲明。
// Chapter6.h
int f();
void f2(int i);
int calc(int v1, int v2);
double square(double x);
int fact(int n);
int user_fact();
double abs(double val);
6.1.3 分離式編譯
C++語言支持所謂的分離式編譯。分離式編譯允許我們把程序分割到幾個文件中去,每個文件獨(dú)立編譯。
編譯和鏈接多個源文件
6.1.3節(jié)練習(xí)
練習(xí) 6.9:編寫你自己的fact.cc和factMain.cc,這兩個文件都應(yīng)該包含上一小節(jié)的練習(xí)中編寫的Chapter6.h頭文件。通過這些文件,理解你的編譯器是如何支持分離式編譯的。
6.2 參數(shù)傳遞
每次調(diào)用函數(shù)時都會重新創(chuàng)建它的形參,并用傳入的實(shí)參對形參進(jìn)行初始化。形參的初始化機(jī)理和變量初始化一樣。
形參的類型決定了形參和實(shí)參交互的方式。如果形參是引用類型,它將綁定到對應(yīng)的實(shí)參上;否則,將實(shí)參的值拷貝后賦給形參。
當(dāng)形參是引用類型時,我們說它對應(yīng)的實(shí)參被引用傳遞或者函數(shù)被傳引用調(diào)用。引用形參也是它綁定的對象的別名;也就是說,引用形參是它對應(yīng)的實(shí)參的別名。
當(dāng)實(shí)參的值被拷貝給形參時,形參和實(shí)參是兩個相互獨(dú)立的對象。這樣的實(shí)參被值傳遞或者函數(shù)被傳值調(diào)用。
6.2.1 傳值參數(shù)
當(dāng)初始化一個非引用類型的變量時,初始值被拷貝給變量。此時,對變量的改動不會影響初始值。如果發(fā)生的是值傳遞,函數(shù)對形參做的所有操作都不會影響實(shí)參。
指針形參
指針的行為和其他非引用類型一樣。當(dāng)執(zhí)行指針拷貝操作時,拷貝的是指針的值。拷貝之后,兩個指針是不同的指針。因?yàn)橹羔樖刮覀兛梢蚤g接訪問它所指向的對象。所以通過指針可以修改它所指向?qū)ο蟮闹怠?strong>指針的傳遞本質(zhì)上還是值傳遞。
在C++中,一般情況下建議使用引用類型的形參來代替指針傳遞。
6.2.1節(jié)練習(xí)
練習(xí) 6.10:編寫一個函數(shù),使用指針形參交換兩個整數(shù)的值。在代碼中調(diào)用該函數(shù)并輸出交換后的結(jié)果,以此驗(yàn)證函數(shù)的正確性。
#include <iostream>
void swap(int* vp1, int* vp2)
{
int temp = *vp1;
*vp1 = *vp2;
*vp2 = temp;
}
int main(int argc, char const *argv[])
{
int val1 = 10;
int val2 = 15;
std::cout << val1 << " " << val2 << std::endl;
swap(&val1, &val2);
std::cout << val1 << " " << val2 << std::endl;
return 0;
}
6.2.2 傳引用參數(shù)
對于引用的操作,實(shí)際上是作用在“引用”所引用的對象上。所以通過引用形參,允許函數(shù)改變一個或者多個實(shí)參的值。引用形參綁定傳入的實(shí)參,而非拷貝實(shí)參。
使用引用避免拷貝
拷貝大的類類型對象或者容器對象非常低效,甚至有的類型根本不支持拷貝操作。當(dāng)某種類型不支持拷貝操作時,函數(shù)只能通過引用形參訪問該類的對象。
如果函數(shù)無須改變引用形參的值,最好將其聲明為常量引用。
使用引用形參返回額外信息
一個函數(shù)只能有一個返回類型,一般情況下只能返回一個值。但是有時需要返回多個值,一種方法是定義一個新的數(shù)據(jù)類型,讓它包含多個數(shù)據(jù)成員。還有一種就是傳入引用,在函數(shù)中修改傳入的實(shí)參。
6.2.2節(jié)練習(xí)
練習(xí) 6.11:編寫并驗(yàn)證你自己的reset函數(shù),使其作用于引用類型的參數(shù)。
void reset(int& val) { val = 0; }
練習(xí) 6.12:改寫6.2.1節(jié)中練習(xí)6.10(第188頁)的程序,使用引用而非指針交換兩個整數(shù)的值。你覺得哪種方法更易于使用呢?為什么?
void swap(int& v1, int& v2)
{
int temp = v1;
v1 = v2;
v2 = temp;
}
顯然引用版本的值交換函數(shù)更易于使用,指針版本的交換過程中還需要涉及解引用。
練習(xí) 6.13:假設(shè)T是某種類型的名字,說明以下兩個函數(shù)聲明的區(qū)別:一個是void f(T)
,另一個是void f(T&)
。
第一個是T類型的值傳遞。第二個是T類型的引用傳遞。
練習(xí) 6.14:舉一個形參應(yīng)該是引用類型的例子,再舉一個形參不能是引用類型的例子。
當(dāng)傳遞標(biāo)準(zhǔn)輸入輸出流對象的時候必須設(shè)置成引用類型,因?yàn)閕ostream類不支持拷貝操作;當(dāng)需要將一棵二叉樹遍歷然后將節(jié)點(diǎn)存入容器時,引用類型不能當(dāng)作對象存在容器里,那么就必須使用指針。
練習(xí) 6.15:說明find_char函數(shù)中的三個形參為什么是現(xiàn)在的類型,特別說明為什么s是常量引用而occurs是普通引用?為什么s和occurs是引用類型而c不是?如果令s是普通引用會發(fā)生什么情況?如果令occurs是常量引用會發(fā)生什么情況?
// 返回s中c第一次出現(xiàn)的位置索引
// 引用形參occurs負(fù)責(zé)統(tǒng)計(jì)c出現(xiàn)的總次數(shù)
using index = std::string::size_type;
index find_char(const std::string& s, char c, int& occurs)
{
auto ret = s.size();
occurs = 0;
for (index i = 0; i != s.size(); ++i) {
if (s[i] == c) {
if (ret == s.size()) { ret = i; }
occurs += 1;
}
}
return ret;
}
字符串s因?yàn)椴恍枰薷乃允褂胏onst進(jìn)行修飾;并且因?yàn)槭菑?fù)雜類型所以使用引用傳遞來減少開銷。
字符c不需要任何的修改且是內(nèi)置類型所以使用普通的值傳遞。
計(jì)數(shù)器occurs因?yàn)樾枰薷乃詡鬟f引用,這里和書上不同的是我們使用int來計(jì)數(shù)。
如果令s為普通引用我們就無法傳入字符串字面值進(jìn)行轉(zhuǎn)換;如果令occurs為常量引用會導(dǎo)致occurs無法修改導(dǎo)致計(jì)數(shù)失敗。
6.2.3 const形參和實(shí)參
這里我們主要強(qiáng)調(diào)的是頂層和底層const的區(qū)別。頂層const作用于當(dāng)前對象本身。當(dāng)使用實(shí)參初始化形參時會忽略掉頂層const。換句話說,形參的頂層const被忽略掉了。<u>當(dāng)形參有頂層const時,傳給它常量對象或者非常量對象都是可以的</u>。
C++中,頂層const會被編譯器忽略掉,所以僅僅在頂層const上存在差異的函數(shù)重載是錯誤的。
指針或者引用形參與const
形參的初始化方式和變量的初始化方式是一樣的。我們可以使用非常量初始化一個底層const對象,然而反之不行。一個普通的引用必須使用同類型的對象初始化。
盡量使用常量引用
把函數(shù)不會改變的形參定義成普通引用是一種比較常見的錯誤,這么做帶給函數(shù)的調(diào)用者是一種誤導(dǎo),即函數(shù)可以修改它的實(shí)參的值。我們不能把const對象、字面值或者需要類型轉(zhuǎn)換的對象傳遞給普通的引用形參。
6.2.3節(jié)練習(xí)
練習(xí) 6.16:下面的這個函數(shù)雖然合法,但是不算特別有用。指出它的局限性并設(shè)法改善。
bool is_empty(std::string& s) { return s.empty(); }
這個函數(shù)只能傳入普通的string對象,const對象和字符串字面值都是無法進(jìn)行傳遞的,應(yīng)該修改為:
bool is_empty(const std::string& s) { return s.empty(); }
練習(xí) 6.17:編寫一個函數(shù),判斷string對象中是否有大寫字母。編寫另一個函數(shù),把string對象全都改成小寫形式。在這兩個函數(shù)中你使用的形參類型相同嗎?為什么?
bool is_contains_upper(const std::string& s)
{
for (char ch : s) {
if (ch >= 'A' && ch <= 'Z') return true;
}
retrn false;
}
void to_lower(std::string& s)
{
for (char& ch : s) { tolower(ch); }
}
這兩個函數(shù)的形參列表不一樣,第一個函數(shù)的形參需要設(shè)定為const,這樣就可以傳遞常量對象和字面值。第二個需要修改字符串中的內(nèi)容,于是應(yīng)當(dāng)設(shè)置成為非常量引用。
練習(xí) 6.18:為下面的函數(shù)編寫函數(shù)聲明,從給定的名字中推測函數(shù)具備的功能。
bool compare(const matrix& lhs, const matrix& rhs);
// 比較兩個矩陣是否相同,相同返回true,否則返回false
vector<int>::iterator change_val(int val, vector<int>::iterator targ);
// 傳入一個迭代器和一個值,將該迭代器的內(nèi)容改為傳入的值之后返回當(dāng)前的迭代器
練習(xí) 6.19:假定有如下聲明,判斷哪個調(diào)用合法,哪個調(diào)用不合法。對于不合法的函數(shù)調(diào)用,說明原因。
#include <string>
double calc(double);
int count(const std::string&, char);
int sum(vector<int>::iterator, vector<int>::iterator, int);
int main(int argc, char const *argv[])
{
std::vector<int> vec(10);
// a:
calc(23.4, 55.1); // 錯誤,傳入了太多的實(shí)參
calc(23.4);
// b:
count("abcda", 'a'); // 正確
// c:
calc(66); // 正確
// d:
sum(vec.begin(), vec.end(), 3.8); // 正確
return 0;
}
練習(xí) 6.20:引用形參什么時候應(yīng)該是常量引用?如果形參應(yīng)該是常量引用,而我們將其設(shè)為了普通引用,將會發(fā)生什么情況?
當(dāng)確定函數(shù)不會改變形參綁定的實(shí)參時,就應(yīng)當(dāng)設(shè)為常量引用。如果形參應(yīng)當(dāng)是常量引用,而我們將其設(shè)為了普通引用,首先會對用戶產(chǎn)生誤導(dǎo);其次無法使用常量對象進(jìn)行調(diào)用。
6.2.4 數(shù)組形參
數(shù)組的兩個特殊性質(zhì)對我們定義和使用作用在數(shù)組上的函數(shù)有影響,這兩個性質(zhì)分別是:不允許拷貝數(shù)組;使用數(shù)組時會將其轉(zhuǎn)換為指針。
因?yàn)椴荒芸截悢?shù)組,所以我們無法以值傳遞的方式使用數(shù)組參數(shù)。因?yàn)閿?shù)組會被轉(zhuǎn)換為指針,所以當(dāng)我們?yōu)楹瘮?shù)傳遞一個數(shù)組時,實(shí)際上傳遞的是指向數(shù)組首元素的指針。
盡管不能以值傳遞的方式傳遞數(shù)組,但是我們可以把形參寫成類似數(shù)組的形式:
// 盡管形式不同,但這三個print函數(shù)是等價(jià)的
// 每個函數(shù)都有一個const int*類型的形參
void print(const int*);
void print(const int[]); // 作用于一個數(shù)組
void print(const int[10]); // 這里的維度僅僅是一個期望,實(shí)際有多少不一定
如果我們傳給print函數(shù)的是一個數(shù)組,則實(shí)參自動地轉(zhuǎn)換成指向數(shù)組首元素的指針,數(shù)組的大小對函數(shù)的調(diào)用沒有影響。
和其他使用數(shù)組的代碼一樣,以數(shù)組作為形參的函數(shù)也必須確保使用數(shù)組時不會越界。
因?yàn)閿?shù)組是以指針的形式傳遞給函數(shù)的,所以函數(shù)無法得知數(shù)組的確切尺寸,所以調(diào)用者也應(yīng)當(dāng)提供數(shù)組盡頭信息。管理指針形參有三種常用的技術(shù):
- 使用標(biāo)記指定數(shù)組的長度:數(shù)組本身包含一個結(jié)束標(biāo)記。
- 使用標(biāo)準(zhǔn)庫規(guī)范:傳遞指向數(shù)組首元素和尾后元素的指針。
- 顯式傳遞一個表示數(shù)組大小的形參:額外增加一個數(shù)組長度參數(shù)。
數(shù)組形參和const
當(dāng)函數(shù)不需要對數(shù)組元素執(zhí)行寫操作的時候,數(shù)組形參應(yīng)該是指向const的指針。
數(shù)組引用形參
C++允許將變量定義成為數(shù)組的引用。形參也可以是數(shù)組的引用。這時,引用形參綁定到對應(yīng)的實(shí)參上,也就是綁定到數(shù)組上:
// 形參是數(shù)組的引用,維度是類型的一部分
void print(int (&arr)[10]) // 這里的括號不可少,而且引用中的維度是有意義的
{
for (int elem : arr)
std::cout << elem << " ";
std::cout << std::endl;
}
傳遞多維數(shù)組
所謂的多維數(shù)組其實(shí)是數(shù)組的數(shù)組。
當(dāng)多維數(shù)組傳遞給函數(shù)時,真正傳遞的是指向數(shù)組首元素的指針,但是這里進(jìn)行傳遞時一定要給出數(shù)組維度的第二維:數(shù)組第二維(以及后面的所有維度)的大小都是數(shù)組類型的一部分,不能省略。
void print(int mat[][10], int row_size) { /* ... */ }
6.2.4節(jié)練習(xí)
練習(xí) 6.21:編寫一個函數(shù),令其接受兩個參數(shù):一個是int型的數(shù),另一個是int指針。桉樹比較int值和指針?biāo)傅闹担祷剌^大的那一個。在該函數(shù)中指針的類型應(yīng)該是什么?
int func(int val, const int* int_ptr)
{
if (val > *int_ptr) return val;
else return *int_ptr;
}
練習(xí) 6.22:編寫一個函數(shù),令其交換兩個int指針。
void swap_ptr(int*& ptr1, int*& ptr2)
{
int* temp = ptr1;
ptr1 = ptr2;
ptr2 = temp;
}
練習(xí) 6.23:參考本節(jié)介紹的幾個print函數(shù),根據(jù)理解編寫你自己的版本。依次調(diào)用每個函數(shù)使其輸入下面定義的i和j:
int i = 0, j[2] = {0, 1};
#include <iostream>
void print(int val);
void print(int arr[], int len);
int main(int argc, char const *argv[])
{
int i = 0, j[2] = {0, 1};
print(i);
print(j, 2);
return 0;
}
void print(int val)
{
std::cout << val << std::endl;
}
void print(int arr[], int len)
{
for (int i = 0; i < len; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
練習(xí) 6.24:描述下面這個函數(shù)的行為。如果代碼中存在問題,請指出并改正。
void print(const int ia[10])
{
for (size_t i = 0; i != 10; ++i)
std::cout << ia[i] << std::endl;
}
因?yàn)槲覀儾荒軐?shù)組進(jìn)行值傳遞,所以當(dāng)我們向一個函數(shù)傳遞數(shù)組時,我們實(shí)際上傳遞的是一個指向數(shù)組首元素的指針。
在這個問題中,const int ia[10]
實(shí)際上等價(jià)于const int*
,并且其中的維度是無關(guān)緊要的。我們可以傳遞const int ia[3]
或者const int ia[255]
。如果我們的確想傳遞一個長度為10的數(shù)組,我們可以傳遞它的引用:
void print10(const int (&arr)[10]) { ... }
6.2.5 main:處理命令行選項(xiàng)
當(dāng)使用argv中的參數(shù)時,一定要記得可選的實(shí)參是從argv[1]開始;argv[0]保存程序的名字,而非用戶輸入。
6.2.5節(jié)練習(xí)
練習(xí) 6.25:編寫一個main函數(shù),令其接受兩個實(shí)參。把實(shí)參的內(nèi)容連成一個string對象并且輸出出來。
#include <iostream>
#include <string>
int main(int argc, char const *argv[])
{
std::string s;
for (int i = 1; i < 3; ++i) {
s += argv[i];
}
std::cout << s << std::endl;
return 0;
}
練習(xí) 6.26:編寫一個程序,使其接受本節(jié)所示的選項(xiàng);輸出傳遞給main函數(shù)的實(shí)參的內(nèi)容。
#include <iostream>
int main(int argc, char const *argv[])
{
for (int i = 0; i < argc; ++i) {
std::cout << argv[i] << " ";
}
std::cout << std::endl;
return 0;
}
6.2.6 含有可變形參的函數(shù)
為了編寫能處理不同數(shù)量實(shí)參的函數(shù),C++11標(biāo)準(zhǔn)提供了兩種主要的方法:
- 如果所有實(shí)參類型類型相同,傳遞一個名為
initializer_list
的標(biāo)準(zhǔn)庫類型。 - 如果實(shí)參的類型不同,編寫可變參數(shù)模板。
C++還有一種特殊的形參類型(省略符),可以用它傳遞可變數(shù)量的實(shí)參。
initializer_list形參
如果函數(shù)的實(shí)參數(shù)量未知但是全部實(shí)參的類型都相同,我們可以使用initializer_list類型的形參。initializer_list是一種標(biāo)準(zhǔn)庫類型,用于表示某種特定類型的數(shù)組。initializer_list類型定義在同名頭文件中。
該容器可以使用迭代器進(jìn)行操作。
省略符形參
省略符形參是為了便于C++程序訪問某些特殊的C代碼而設(shè)置的。省略符形參應(yīng)該僅僅用于C和C++通用的類型。
6.2.6節(jié)練習(xí)
練習(xí) 6.27:編寫一個函數(shù),它的參數(shù)是initializer_list<int>
類型的對象,函數(shù)的功能是計(jì)算列表中所有元素的和。
int sum(std::initializer_list<int> vals)
{
int sum = 0;
for (int num : vals) {
sum += num;
}
return sum;
}
練習(xí) 6.28:在error_msg函數(shù)的第二個版本中包含ErrCode類型的參數(shù),其中循環(huán)內(nèi)的elem是什么類型?
elem的類型是const std::string&
。
練習(xí) 6.29:在范圍for循環(huán)中使用initializer_list對象時,應(yīng)該將循環(huán)控制變量聲明成引用類型嗎?為什么?
如果在循環(huán)中不涉及任何的修改,那么就應(yīng)該將控制變量聲明為引用類型。
6.3 返回類型和return語句
return語句終止當(dāng)前正在執(zhí)行的函數(shù)并將控制權(quán)返回到調(diào)用該函數(shù)的地方。注意這里首先終止當(dāng)前函數(shù),然后將控制權(quán)轉(zhuǎn)移回調(diào)用當(dāng)前函數(shù)的地方,有兩步。
6.3.1 無返回值函數(shù)
沒有返回值的return語句只能用在返回類型是void的函數(shù)中。返回void的函數(shù)并要求非得是return語句,因?yàn)樵谶@類函數(shù)的最后會隱式的執(zhí)行return。
void函數(shù)如果想在它的中間位置提前退出,可以使用return語句。
強(qiáng)行令void函數(shù)返回其他類型的表達(dá)式會產(chǎn)生編譯錯誤。
6.3.2 有返回值函數(shù)
只要函數(shù)的返回類型不是void,則該函數(shù)內(nèi)的每條return語句必須返回一個值。return語句返回值的類型必須與函數(shù)的返回類型相同,或者能隱式的轉(zhuǎn)換成函數(shù)的返回類型。
在含有return語句的循環(huán)后面應(yīng)該也有一條循環(huán)語句,如果沒有的話該程序就是錯誤的。
值是如何被返回的
返回的值用于初始化調(diào)用點(diǎn)的接收變量,該接收變量是函數(shù)調(diào)用的結(jié)果。
不要返回局部對象的引用或者指針
函數(shù)執(zhí)行完畢之后,它所占用的儲存空間也隨之被釋放掉。因此,函數(shù)終止意味著局部變量的引用將指向不再有效的內(nèi)存區(qū)域。
返回類類型的函數(shù)和調(diào)用運(yùn)算符
調(diào)用運(yùn)算符的優(yōu)先級與點(diǎn)運(yùn)算符和箭頭運(yùn)算符相同,并且符合左結(jié)合律。
引用返回左值
函數(shù)的返回類型決定函數(shù)調(diào)用是否是左值。調(diào)用一個返回引用的函數(shù)得到左值,其他返回類型為右值。
列表初始化返回值
C++11新標(biāo)準(zhǔn)規(guī)定,函數(shù)可以返回花括號包圍的值的列表。這里注意,返回類型是可以被列表初始化的類型。
如果函數(shù)返回的是內(nèi)置類型,則花括號包圍的列表最多包含一個值。如果函數(shù)返回的是類類型,由類本身定義初始值如何使用。
主函數(shù)main的返回值
允許main函數(shù)沒有return語句直接結(jié)束。如果控制到達(dá)了main函數(shù)的結(jié)尾處而且沒有return語句,編譯器將隱式的插入一條返回0的return語句。
main函數(shù)的返回值可以是看作狀態(tài)指示器。返回0表示執(zhí)行成功,返回其他值表示執(zhí)行失敗。
遞歸
如果一個函數(shù)調(diào)用了它自身,不管這種調(diào)用是直接的還是間接的,都稱該函數(shù)為遞歸函數(shù)。
在遞歸函數(shù)中,一定有某條路徑是不包含遞歸調(diào)用的。無窮遞歸的情況被稱為遞歸循環(huán)。
main函數(shù)不能調(diào)用它自己。
6.3.2節(jié)練習(xí)
練習(xí) 6.30:編譯第200頁的str_subrange函數(shù),看看你的編譯器是如何處理函數(shù)中得錯誤的。
#include <iostream>
bool str_subrange(const std::string& s1, const std::string& s2)
{
int sz1 = s1.size();
int sz2 = s2.size();
if (sz1 == sz2) return s1 == s2;
int min_sz = sz1 < sz2 ? sz1 : sz2;
for (int i = 0; i < min_sz; ++i) {
if (s1[i] != s2[i]) return;
}
}
// a.cc: In function 'bool str_subrange(const string&, const string&)':
// a.cc:10:29: error: return-statement with no value, in function returning 'bool' [-fpermissive]
// if (s1[i] != s2[i]) return;
// ^~~~~~
練習(xí) 6.31:什么情況下返回的引用無效?什么情況下返回常量的引用無效?
返回局部變量的引用無效;返回局部變量的常量引用無效。
練習(xí) 6.32:下面的函數(shù)合法嗎?如果合法,說明其功能,如果不合法,修改其中的錯誤并解釋原因。
int& get(int* arry, int index) { return arry[index]; }
int main(int argc, char const *argv[])
{
int ia[10]; // 聲明一個維度為10的數(shù)組
for (int i = 0; i != 10; ++i) get(ia, i) = i; // 循環(huán)調(diào)用函數(shù)來設(shè)定每個元素的值
return 0;
}
練習(xí) 6.33:編寫一個遞歸函數(shù),輸出vector對象的內(nèi)容。
void factorial_print(std::vector<int> v, int index)
{
if (index == v.size()) {
return;
} else {
std::cout << v[index] << " ";
factorial_print(v, index + 1);
}
}
練習(xí) 6.34:如果factorial函數(shù)的停止條件如下所示,將發(fā)生什么情況?if (val != 0)
函數(shù)依然正常運(yùn)作。
練習(xí) 6.35:在調(diào)用factorial函數(shù)時,為什么我們傳入的值是val - 1而非val--?
因?yàn)楫?dāng)傳入val--時,相當(dāng)于先傳值再減1,而且減掉的1是在當(dāng)前層內(nèi),結(jié)果就會發(fā)生錯誤。
6.3.3 返回?cái)?shù)組指針
函數(shù)可以返回?cái)?shù)組的指針或者引用。
聲明一個返回?cái)?shù)組指針的函數(shù)
要想在聲明func時不使用類型別名,我們必須牢記被定義的名字后面數(shù)組的維度:
int arr[10]; // arr是一個含有10個整數(shù)的數(shù)組
int* arr[10]; // arr是一個含有10個指針的數(shù)組
int (*arr)[10] // arr是一個含有10個整數(shù)的數(shù)組的指針
當(dāng)需要返回一個數(shù)組的時候,我們需要將函數(shù)聲明成如下的形式:
int (*func(int i))[10];
可以按照以下的順序來逐層理解該聲明的含義:
-
func(int i)
表示調(diào)用函數(shù)時需要一個int類型的形參。 -
(*func(int i))
意味著我們可以對函數(shù)調(diào)用的結(jié)果執(zhí)行解引用操作。 -
(*func(int i))[10]
表示解引用func的調(diào)用將得到一個大小是10的數(shù)組。 -
int (*func(int i))[10]
表示數(shù)組中的元素是int類型。
使用尾置返回類型
C++11中有一種簡明的方法,就是尾置返回類型。任何函數(shù)的定義都能使用尾置返回,但是這種形式對于返回類型比較復(fù)雜的函數(shù)最有效。
位置返回類型跟在形參列表后并以一個->
符號開頭。為了表示函數(shù)真正的返回類型跟在形參列表之后,我們在本應(yīng)該出現(xiàn)返回類型的地方放置一個auto:
// func接受一個int類型的實(shí)參,返回一個指針,該指針指向含有10個整數(shù)的數(shù)組
auto func(int i) -> int(*)[10];
使用decltype
如果我們知道函數(shù)返回的指針將指向哪個數(shù)組,就可以使用decltype關(guān)鍵字聲明返回類型。
6.3.3節(jié)練習(xí)
練習(xí) 6.36:編寫一個函數(shù)的聲明,使其返回?cái)?shù)組的引用并且該數(shù)組包含10個string對象。不要使用尾置返回類型、decltype或者類型別名。
std::string (&func())[10];
練習(xí) 6.37:為上一題的函數(shù)再寫三個聲明,一個使用類型別名,另一個使用位置返回類型,最后一個使用decltype關(guān)鍵字。你覺得哪種形式最好?為什么?
using string_array_refer = std::string(&)[10];
auto func() -> std::string(&)[10];
std::string arr[10];
decltype(arr)& func();
個人認(rèn)為第一種方式比較好。
練習(xí) 6.38:修改arrPtr函數(shù),使其返回?cái)?shù)組的引用。
using arr = std::string[10];
arr& arrPtr(int i) { return i % 2 ? odd : even; }
6.4 函數(shù)重載
如果同一個作用域內(nèi)的幾個函數(shù)名字相同但是形參列表不同,我們稱之為重載函數(shù)。當(dāng)調(diào)用函數(shù)時,編譯器會根據(jù)傳遞的實(shí)參來推斷想要的時哪個函數(shù)。
main函數(shù)不能重載。
定義重載函數(shù)
對于重載函數(shù)來說,它們應(yīng)該在形參數(shù)量或者形參類型上有所不同。
不允許兩個函數(shù)除了返回類型外其他所有的要素都相同。
判斷兩個形參的類型是否相同
函數(shù)聲明中,編譯器會忽略掉頂層const。
重載和const形參
頂層const不影響傳入函數(shù)的對象。一個擁有頂層const的形參無法和另一個沒有頂層const的形參區(qū)分開來。
如果形參是某種類型的指針或者引用,則通過區(qū)分其指向的是常量對象還是非常量對象可以實(shí)現(xiàn)函數(shù)重載,此時的const是底層的。
// 對于接受引用或者指針的函數(shù)來說,對象是常量還是非常量對應(yīng)的形參不同
// 定義了4個獨(dú)立的重載函數(shù)
Record lookup(Account& rhs); // 函數(shù)作用于Account的引用
Record lookup(const Account& rhs); // 新函數(shù),作用于常量引用
Record lookup(Account* rhs); // 新函數(shù),作用于指向Account的指針
Record lookup(const Account* rhs); // 新函數(shù),作用于指向常量的指針,底層const
編譯器可以通過實(shí)參是否是常量來推斷應(yīng)該調(diào)用哪個函數(shù)。因?yàn)閏onst不能轉(zhuǎn)換成其他類型,所以我們只能把const對象或者指向const的指針傳遞給const形參。相反的,因?yàn)榉浅A靠梢赞D(zhuǎn)換成const,所以上面的4個函數(shù)都能作用于非常量對象或者指向非常量對象的指針。當(dāng)我們傳遞一個非常量對象或者指向非常量對象的指針時,編譯器會優(yōu)先選用非常量版本的函數(shù)。
何時不應(yīng)該重載函數(shù)
盡管函數(shù)重載能在一定程度上減輕我們?yōu)楹瘮?shù)起名字、記名字的負(fù)擔(dān),但是最好只重載那些確實(shí)非常相似的操作。
const_cast和重載
調(diào)用重載的函數(shù)
函數(shù)匹配是指一個過程,在這個過程中我們把函數(shù)調(diào)用與一組重載函數(shù)中的某一個關(guān)聯(lián)起來,函數(shù)匹配也叫做重載確定。編譯器首先將調(diào)用的實(shí)參與重載集合中每一個函數(shù)的形參進(jìn)行比較,然后根據(jù)比較的結(jié)果決定到底調(diào)用哪個函數(shù)。
現(xiàn)在我們需要掌握的是,當(dāng)調(diào)用重載函數(shù)時有三種可能的結(jié)果:
- 編譯器找到一個與實(shí)參最佳匹配的函數(shù),并生成調(diào)用該函數(shù)的代碼。
- 找不到任何一個函數(shù)與調(diào)用的實(shí)參匹配,此時編譯器發(fā)出無匹配的錯誤信息。
- 有多于一個函數(shù)可以匹配,但是每一個都不是明顯的最佳選擇。此時也將發(fā)生錯誤,稱為二義性調(diào)用。
6.4節(jié)練習(xí)
練習(xí) 6.39:說明在下面的每組聲明中第二條聲明語句是何含義。如果有非法的聲明,請指出來。
// a:
int calc(int, int);
int calc(const int, const int);
// 編譯器會忽略頂層const,重復(fù)聲明
// b:
int get();
double get();
// 函數(shù)重載不允許僅僅在返回類型上有區(qū)別,重復(fù)聲明
// c:
int* reset(int*);
double* reset(double*);
// 重載成功
6.4.1 重載與作用域
如果我們在內(nèi)層作用域中聲明名字,它將隱藏外層作用域中聲明的同名實(shí)體。在不同的作用域中無法重載函數(shù)名。
在C++中,名字查找發(fā)生在類型檢查之前。即:先找到名字,再檢查類型。
6.5 特殊用途語言特性
默認(rèn)實(shí)參、內(nèi)聯(lián)函數(shù)和constexpr函數(shù),以及再程序調(diào)試過程中常用的一些功能。
6.5.1 默認(rèn)實(shí)參
我們可以為一個或者多個形參都定義默認(rèn)值,一旦某個形參被賦予了默認(rèn)值,它右邊的所有形參都必須默認(rèn)值。
使用默認(rèn)實(shí)參調(diào)用函數(shù)
如果我們想使用默認(rèn)實(shí)參,只要在調(diào)用函數(shù)的時候省略該實(shí)參就可以了。
函數(shù)調(diào)用時實(shí)參按位置解析,默認(rèn)實(shí)參負(fù)責(zé)填補(bǔ)函數(shù)調(diào)用缺少的尾部實(shí)參(右側(cè)位置)。
當(dāng)設(shè)計(jì)含有默認(rèn)實(shí)參的函數(shù)時,其中一項(xiàng)任務(wù)是合理設(shè)置形參的順序,盡量讓不怎么使用默認(rèn)值的形參出現(xiàn)在前面,而讓那些經(jīng)常使用默認(rèn)值的形參出現(xiàn)在后面(左邊的沒有默認(rèn)值可以,但是有默認(rèn)值的右邊不能沒有默認(rèn)值)。
默認(rèn)實(shí)參聲明
在給定的作用域中一個形參只能被賦予一次默認(rèn)實(shí)參。換句話說,函數(shù)的后續(xù)聲明只能為之前那些沒有默認(rèn)值的形參添加默認(rèn)實(shí)參,而且該形參右側(cè)的所有形參必須都有默認(rèn)值。
通常,應(yīng)該在函數(shù)聲明中指定默認(rèn)實(shí)參,并將該聲明放在合適的頭文件中。
默認(rèn)實(shí)參初始值
局部變量不能作為默認(rèn)實(shí)參。除此之外,只要表達(dá)式的類型能轉(zhuǎn)換成形參所需的類型,該表達(dá)式就能作為默認(rèn)實(shí)參。
用作默認(rèn)實(shí)參的名字在函數(shù)生命所在的作用域內(nèi)解析,而這些名字的求值過程發(fā)生在函數(shù)調(diào)用時。
6.5.1節(jié)練習(xí)
練習(xí) 6.40:下面的哪個聲明是錯誤的?為什么?
int ff(int a, int b = 0, int c = 0); // 正確
char* init(int ht = 24, int wd, char bakgrnd); // 錯誤:第一個默認(rèn)值形參的右邊的形參必須有默認(rèn)值
練習(xí) 6.41:下面的哪個調(diào)用是非法的?為什么?哪個調(diào)用雖然合法但顯然與程序員的初衷不符?為什么?
char* init(int ht, int wd = 80, char bakgrnd = ' ');
// a:
init(); // 非法,沒有足夠的實(shí)參初始化形參
// b:
init(24, 10); // 合法
// c:
init(14, '*'); // 合法,但是結(jié)果為:init(14, '*', ''); 其中'*'被轉(zhuǎn)換成整數(shù)
練習(xí) 6.42:給make_plural函數(shù)的第二個形參賦予默認(rèn)實(shí)參's',利用新版本的函數(shù)輸出單詞success和failure的單數(shù)和復(fù)數(shù)形式。
#include <iostream>
#include <string>
std::string make_plural(size_t ctr, const std::string& word, const std::string& ending = "s")
{
return (ctr > 1) ? word + ending : word;
}
int main(int argc, char const *argv[])
{
std::cout << make_plural(1, "success") << std::endl;
std::cout << make_plural(10, "success", "es") << std::endl;
std::cout << make_plural(1, "failure") << std::endl;
std::cout << make_plural(10, "failure") << std::endl;
return 0;
}
6.5.2 內(nèi)聯(lián)函數(shù)和constexpr函數(shù)
調(diào)用函數(shù)一般比求等價(jià)表達(dá)式的值要慢一些。在大多數(shù)機(jī)器上,一次函數(shù)調(diào)用其實(shí)包含著一系列工作:調(diào)用前要先保存寄存器,并在返回時恢復(fù);可能需要拷貝實(shí)參;程序轉(zhuǎn)向一個新的位置繼續(xù)執(zhí)行。
內(nèi)聯(lián)函數(shù)可避免函數(shù)調(diào)用的開銷
將函數(shù)指定為內(nèi)聯(lián)函數(shù),通常就是將它在每個調(diào)用點(diǎn)上“內(nèi)聯(lián)的”展開。從而消除函數(shù)運(yùn)行時的開銷。
在函數(shù)的返回類型前面加上關(guān)鍵字inline,這樣就可以將它聲明成內(nèi)聯(lián)函數(shù)了。
內(nèi)聯(lián)說明只是向編譯器發(fā)出的一個請求,編譯器可以選擇忽略這個請求。
constexpr函數(shù)
constexpr函數(shù)是指能用于常量表達(dá)式的函數(shù)。定義constexpr函數(shù)的方法與其他函數(shù)類似,不過要遵循幾項(xiàng)約定:
- 函數(shù)的返回值以及所有形參的類型都得是字面值類型。
- 函數(shù)體中必須有且僅有一條return語句。
constexpr int new_size() { return 42; }
constexpr int foo = new_size();
constexpr函數(shù)被隱式的指定為內(nèi)聯(lián)函數(shù)。
把內(nèi)聯(lián)函數(shù)和constexpr函數(shù)放在頭文件內(nèi)
內(nèi)聯(lián)函數(shù)和constexpr函數(shù)可以在程序中多次定義。
6.5.2節(jié)練習(xí)
練習(xí) 6.43:你會把下面的哪個聲明和定義放在頭文件中?哪個放在源文件中?為什么?
inline bool eq(const BigInt&, const BigInt&) { } // 頭文件,因?yàn)橛袃?nèi)聯(lián)關(guān)鍵字
void putValues(int* arr, int size); // 頭文件,僅僅是一條函數(shù)的聲明
練習(xí) 6.44:將6.2.2節(jié)(第189頁)的isShorter函數(shù)改寫成內(nèi)聯(lián)函數(shù)。
inline bool isShorter(const std::string& s1, const std::string& s2) { return s1.size() < s2.size(); }
練習(xí) 6.45:回顧在前面的練習(xí)中編寫的那些函數(shù),它們應(yīng)該是內(nèi)聯(lián)函數(shù)嗎?如果是,將他們改寫成內(nèi)聯(lián)函數(shù);如果不是,說明原因。
是否內(nèi)聯(lián)遵循一個準(zhǔn)則,一般復(fù)雜程度不超過兩行的程序可以比較方便的內(nèi)聯(lián)展開,只要超過兩行就不建議內(nèi)聯(lián)。
練習(xí) 6.46:能把isShorter函數(shù)定義成constexpr函數(shù)嗎?如果能,將它改寫成constexpr函數(shù);如果不能,說明原因。
不能,因?yàn)閏onstexpr函數(shù)要求返回類型是字面值類型。
6.5.3 調(diào)試幫助
當(dāng)應(yīng)用程序編寫完成準(zhǔn)備發(fā)布時,要先屏蔽調(diào)試代碼。這種方法用到兩項(xiàng)預(yù)處理功能:assert和NDEBUG。
assert預(yù)處理宏
assert是一種預(yù)處理宏。所謂預(yù)處理宏其實(shí)是一個預(yù)處理變量,它的行為有點(diǎn)類似內(nèi)聯(lián)函數(shù)。assert宏使用一個表達(dá)式作為其條件:assert(expr);
。
首先對expr求值,如果表達(dá)式為假,assert輸出信息并終止程序。如果表達(dá)式為真,assert什么也不做。
NDEBUG預(yù)處理變量
assert的行為依賴于一個名為NDEBUG的預(yù)處理變量的狀態(tài):如果定義了NDEBUG,則assert什么也不做。默認(rèn)狀態(tài)下沒有定義NDEBUG,此時assert將執(zhí)行運(yùn)行檢查。
我們可以使用#define語句定義NDEBUG,從而關(guān)閉調(diào)試狀態(tài)。
6.5.3節(jié)練習(xí)
練習(xí) 6.47:改寫6.3.2節(jié)(第205頁)練習(xí)中使用遞歸輸出vector內(nèi)容的程序,使其有條件的輸出與執(zhí)行過程有關(guān)的信息。例如,每次調(diào)用時輸出vector對象的大小。分別在打開和關(guān)閉調(diào)試器的情況下編譯并執(zhí)行這個程序。
#include <iostream>
#include <vector>
void factorial_print(std::vector<int> v, int index)
{
#ifndef NDEBUG
std::cout << __func__ << ": vector size is " << v.size() << " ";
#endif
if (index == v.size()) {
return;
} else {
std::cout << v[index] << std::endl;
factorial_print(v, index + 1);
}
}
int main(int argc, char const *argv[])
{
std::vector<int> v = {4,3,6,4,5,7,8,2,4,6};
factorial_print(v, 0);
return 0;
}
練習(xí) 6.48:說明下面這個循環(huán)的含義,他對assert的使用合理嗎?
std::string s;
while (std::cin >> s && s != sought) { }
assert(std::cin);
這個程序想表達(dá)的是,只要輸入合法,或者輸入的內(nèi)容和sought不相同,就向字符串s中不停的輸入。有兩種情況,首先輸入合法但是內(nèi)容等于sought,退出循環(huán),然后assert語句什么都不做。其次輸入不合法,退出循環(huán),然后std::cin判定為false,終止程序。所以是合理的。
6.6 函數(shù)匹配
確定候選函數(shù)和可行函數(shù)
函數(shù)匹配的第一步是選定本次調(diào)用對應(yīng)的重載函數(shù)集,集合中的函數(shù)稱為候選函數(shù)。候選函數(shù)具備兩個特征:
- 與被調(diào)用的函數(shù)同名。
- 其聲明在調(diào)用點(diǎn)可見。
第二步是考察本次調(diào)用提供的實(shí)參,然后從候選函數(shù)中選出能被這組實(shí)參調(diào)用的函數(shù),這些新選出的函數(shù)稱為可行函數(shù)。可行函數(shù)也有兩個特征:
- 形參數(shù)量與本次調(diào)用提供的實(shí)參數(shù)量相等。
- 每個實(shí)參的類型與對應(yīng)的形參類型相同,或者能轉(zhuǎn)換成形參的類型。
如果沒有找到可行函數(shù),編譯器將報(bào)告無匹配函數(shù)的錯誤。
尋找最佳匹配(如果有的話)
函數(shù)匹配的第三步是從可行函數(shù)中選擇與本次調(diào)用最匹配的函數(shù)。它的基本思想是:實(shí)參類型與形參類型越接近,它們匹配的越好。
含有多個形參的函數(shù)匹配
選擇可行函數(shù)的方法和只有一個實(shí)參時一樣,編譯器選擇那些形參數(shù)量滿足要求且實(shí)參類型和形參類型能夠匹配的函數(shù)。接下來,編譯器依次檢查每個實(shí)參以確定哪個函數(shù)是最佳匹配。如果有且僅有一個函數(shù)滿足條件,則匹配成功:
- 該函數(shù)每個實(shí)參的匹配都不劣于其他可行函數(shù)需要的匹配。
- 至少有一個實(shí)參的匹配優(yōu)于其他可行函數(shù)提供的匹配。
如果在檢查了所有實(shí)參之后沒有選中任何一個函數(shù),則該調(diào)用是錯誤的。
調(diào)用重載時應(yīng)盡量避免強(qiáng)制類型轉(zhuǎn)換。如果在實(shí)際應(yīng)用中確實(shí)需要強(qiáng)制類型轉(zhuǎn)換,則說明我們設(shè)計(jì)的形參集合不合理。
6.6節(jié)練習(xí)
練習(xí) 6.49:什么是候選函數(shù)?什么是可行函數(shù)?
候選函數(shù):和被調(diào)用函數(shù)同名;聲明在調(diào)用點(diǎn)可見。
可行函數(shù):形參數(shù)量與本次調(diào)用所提供的實(shí)參數(shù)量相等;每個實(shí)參類型與形參類型匹配。
練習(xí) 6.50:已知有第217頁對函數(shù)f的聲明,對于下面的每一個調(diào)用列出可行函數(shù)。其中哪個函數(shù)是最佳匹配?如果調(diào)用不合法,是因?yàn)闆]有可匹配的函數(shù)還是因?yàn)檎{(diào)用具有二義性?
void f(); // 1
void f(int); // 2
void f(int, int); // 3
void f(double, double = 3.14); // 4
f(2.56, 42); // 4
f(42); // 2
f(42, 0); // 3
f(2.56, 3.14) // 4
練習(xí) 6.51:編寫函數(shù)f的4個版本,令其各輸出一條可以區(qū)分的消息。驗(yàn)證上一個練習(xí)的答案,如果你回答錯了,反復(fù)研究本節(jié)的內(nèi)容直到你弄清自己錯在何處。
void f(); // 1
void f(int); // 2
void f(int, int); // 3
void f(double, double = 3.14); // 4
f(2.56, 42); // 這個調(diào)用其實(shí)是二義性,因?yàn)?和4都涉及一個類型轉(zhuǎn)換
6.6.1 實(shí)參類型轉(zhuǎn)換
-
精確匹配,包括以下情況:
- 實(shí)參類型和形參類型相同。
- 實(shí)參從數(shù)組類型或函數(shù)類型轉(zhuǎn)換成對應(yīng)的指針類型。
- 向?qū)崊⑻砑禹攲觕onst或者從實(shí)參中刪除頂層const。
- 通過const轉(zhuǎn)換實(shí)現(xiàn)的匹配。
- 通過類型提升實(shí)現(xiàn)的匹配。
- 通過算術(shù)類型轉(zhuǎn)換或者指針轉(zhuǎn)換實(shí)現(xiàn)的匹配。
- 通過類類型轉(zhuǎn)換實(shí)現(xiàn)的匹配。
需要類型提升和算術(shù)類型轉(zhuǎn)換的匹配
內(nèi)置類型的提升和轉(zhuǎn)換可能在函數(shù)匹配時產(chǎn)生意想不到的結(jié)果。
小整型一般都會提升到int類型或者更大的整數(shù)類型。
所有算術(shù)類型轉(zhuǎn)換的級別都一樣。
參數(shù)匹配和const實(shí)參
如果重載函數(shù)的區(qū)別在于它們的引用類型的形參是否引用了const,或者指針類型的形參是否指向const,則當(dāng)調(diào)用發(fā)生時編譯器通過實(shí)參是否是常量來決定選擇哪個函數(shù)。
6.6.1節(jié)練習(xí)
練習(xí) 6.52:已知有以下聲明:
void manip(int, int);
double dobj;
請指出下列調(diào)用中每個類型轉(zhuǎn)換的等級。
// a:
manip('a', 'z'); // 第3級
// b:
manip(55.4, dobj); // 第3級
練習(xí) 6.53:說明下列每組聲明中的第二條語句會產(chǎn)生什么影響,并指出哪些不合法(如果有的話)。
int calc(int&, int&);
int calc(const int&, const int&); // 合法,傳入常量引用
int calc(char* char*);
int calc(const char*, const char*); // 合法,傳入底層const指針
int calc(char*, char*);
int calc(char* const, char* const); // 不合法,編譯器會忽略掉頂層const,重復(fù)聲明
6.7 函數(shù)指針
函數(shù)指針指向的是函數(shù)而非對象。函數(shù)指針指向某種特定的類型。函數(shù)的類型由它的返回類型和形參共同決定。
要想聲明一個可以指向函數(shù)的指針,只需要用指針替換函數(shù)名即可。
使用函數(shù)指針
當(dāng)我們把函數(shù)名作為一個值使用時,該函數(shù)自動的轉(zhuǎn)換成指針,也就是說,取地址符是可選的。
我們可以直接使用指向函數(shù)的指針調(diào)用該函數(shù),無須提前解引用指針。
重載函數(shù)的指針
重載函數(shù)的指針需要在指針聲明時確定形參列表,這樣就可以進(jìn)行精確重載了。
函數(shù)指針形參
返回指向函數(shù)的指針
int (*func(int))(int*, int);
func函數(shù)的形參列表是int
,返回類型是int(*)(int*, int*)
:一個指向函數(shù)的指針。
將auto和decltype用于函數(shù)指針類型
6.7節(jié)練習(xí)
練習(xí) 6.54:編寫函數(shù)的聲明,令其接受兩個int形參并且返回類型也是int;然后聲明一個vector對象,令其元素是指向該函數(shù)的指針。
int func(int, int);
std::vector<int(*)(int, int)> func_ptr_vec;
練習(xí) 6.55:編寫4個函數(shù),分別對兩個int值執(zhí)行加、減、乘、除運(yùn)算;在上一題創(chuàng)建的vector對象中保存指向這些函數(shù)的指針。
int add(int n1, int n2) { return n1 + n2; }
int minus(int n1, int n2) { return n1 - n2; }
int multiply(int n1, int n2) { return n1 * n2; }
int divide(int n1, int n2) { return n1 / n2; }
std::vector<int(*)(int, int)> func_ptr_vec = {add, minus, multiply, divide};
練習(xí) 6.56:調(diào)用上述vector對象中的每個元素并輸出結(jié)果。
#include <iostream>
#include <vector>
int add(int n1, int n2) { return n1 + n2; }
int minus(int n1, int n2) { return n1 - n2; }
int multiply(int n1, int n2) { return n1 * n2; }
int divide(int n1, int n2) { return n1 / n2; }
int main(int argc, char const *argv[])
{
std::vector<int(*)(int, int)> func_ptr_vec = {add, minus, multiply, divide};
int v1 = 18;
int v2 = 6;
for (auto func : func_ptr_vec) {
std::cout << func(v1, v2) << std::endl;
}
return 0;
}
// 24
// 12
// 108
// 3