《C++ Primer》學習筆記(六):函數
函數基礎
一個典型的 函數(function) 定義包括:返回類型(return type)、函數名字、由0個或多個形式參數(parameter,簡稱形參)組成的 列表 和 函數體(function body)。函數執行的操作在語句塊中說明,即函數體。
局部對象
形參和函數體內定義的變量統稱爲 局部變量(local variable),僅在函數的作用域內可見。同時局部變量還會隱藏在外層作用域中同名的其他所有聲明。
只存在於塊執行期間的對象稱爲** 自動對象(automatic object)**,當塊的執行結束後,塊中創建的自動對象的值就變成未定義的了。形參是一種自動對象,在函數開始時爲形參申請存儲空間,因爲形參定義在函數體作用域之內,所以一旦函數終止,形參也就被銷燬。
局部靜態對象(local static object) 在程序的執行路徑第一次經過對象定義語句時初始化,並且直到程序結束才被銷燬,對象所在的函數結束執行並不會對它產生影響。在變量類型前添加關鍵字 static 可以定義局部靜態對象。如果局部靜態對象沒有顯式的初始值,它將執行值初始化,內置類型的局部靜態變量初始化爲0 。
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;
}
函數聲明
函數必須在使用之前聲明,函數只能定義一次,但可以聲明多次,函數聲明也叫做** 函數原型(function prototype)**。函數的聲明和函數的定義非常類似,唯一的區別是函數聲明無須函數體,用一個分號替代即可。
建議在頭文件中進行函數聲明,這樣就能確保同一函數的所有聲明保持一致。定義函數的源文件應該把含有函數聲明的頭文件包含進來,由編譯器負責檢查驗證函數的定義和聲明是否匹配。
分離式編譯
分離式編譯(separate compilation) 允許我們把程序按照邏輯關係分割到幾個文件中去,每個文件獨立編譯。這一過程通常會產生後綴名是 .obj(Windows)或 .o(UNIX) 的文件,該文件包含對象代碼(object code)。之後編譯器把對象文件 鏈接(link) 在一起形成可執行文件。
參數傳遞
如果形參是引用類型,它將綁定到對應的實參上(傳引用調用)。否則,將實參的值拷貝後賦給形參(傳值調用)。
使用引用形參可以避免拷貝操作,拷貝大的類類型對象或容器對象比較低效,另外有的類類型(如IO類型)根本就不支持拷貝操作,這時只能通過引用形參訪問該類型的對象。除了內置類型、函數對象和標準庫迭代器外,其他類型的參數建議以引用方式傳遞。如果函數無須改變引用形參的值,最好將其聲明爲常量引用。
數組形參
數組的兩個特殊性質對定義和使用作用在數組上的函數有影響,分別是:不允許拷貝數組以及使用數組時(通常)會將其轉換成指針。
因爲不能拷貝數組,所以無法以值傳遞的方式使用數組參數,但是可以把形參寫成類似數組的形式。
// 儘管形式不同,但這三個print函數是等價的
// 每個函數都有一個const int*類型的形參
//編譯器處理該函數調用時,只檢查傳入參數是否爲const int*類型
void print(const int*);
void print(const int[]); // 可以看出來,函數的意圖是作用於一個數組
void print(const int[10]); // 這裏的維度表示我們期望數組含有多少元素,實際不一定
因爲數組會被轉換成指針,所以當我們傳遞給函數一個數組時,實際上傳遞的是指向數組首元素的指針。
int num = 0, array[2] = {0, 1};
print(&num); // 正確:&num的類型是int*
print(array); // 正確: array轉換成int*並指向array[0]
因爲數組是以指針的形式傳遞給函數的,所以一開始函數並不知道數組的確切尺寸,調用者應該爲此提供一些額外信息。管理指針形參有三種常用的技術:
- 要求數組本身包含一個結束標記;
- 傳遞指向數組首元素和尾後元素的指針;
- 專門定義一個表示數組大小的形參。
形參可以是數組的引用,但此時維度是形參類型的一部分,函數只能作用於指定大小的數組。
//正確: 形參是數組的引用,維度是類型的一部分
void print(int (&arr) [10]) {
for (auto elem : arr)
cout << elem << endl;
}
// &arr兩端的括號必不可少:
// f(int &arr[10]) // 錯誤:將arr聲明成了引用的數組
// f(int (&arr)[10]) // 正確:arr是具有10個整數的整型數組的引用
int i = 0, j[2] = {0, 1};
int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(&i); //錯誤:實參不是含有10個整數的數組
print(j); //錯誤:實參不是含有10個整數的數組
print(k); //正確:實參是含有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) { /* ... */ }
main:處理命令行選項
int main(int argc, char *argv[]) { /*...*/ }
int main(int argc, char **argv) { /*...*/ }
第一個形參 argc
表示數組中字符串的數量;第二個形參 argv
是一個二維數組,數組元素是指向C風格字符串的指針。
當實參傳遞給 main
函數後,argv
的第一個元素指向程序的名字或者一個空字符串,接下來的元素依次傳遞命令行提供的實參,最後一個指針之後的元素值保證爲0。
含有可變形參的函數
爲了編寫能處理不同數量實參的函數,C++11提供了兩種主要方法:
- 如果所有的實參類型相同,可以傳遞一個名爲
initializer_list
的標準庫類型。 - 如果實參類型不同,可以編寫可變參數模板。
C++還有一種特殊的形參類型(即省略符),可以用它傳遞可變數量的實參。不過這種用法一般只用於與C函數交互的接口程序。省略符形參是爲了便於C++程序訪問某些特殊的C代碼而設置的,這些代碼使用了名爲 varargs
的C標準庫功能。通常,省略符形參不應該用於其他目的。省略符形參應該僅僅用於C和C++通用的類型,大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝。
void foo(parm_list, ...);
void foo(...);
initializer_list
是一種標準庫類型,定義在頭文件 initializer_list
中,表示某種特定類型的值的數組。
initializer_list
是一種模板類型,定義initializer_list
對象時,必須說明列表中所含元素的類型。initializer_list
中的元素永遠是常量值,無法改變initializer_list
對象中元素的值。
可以使用如下的形式編寫輸出錯誤信息的函數,使其可以作用於可變數量的實參:
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
如果想向initializer_list
形參傳遞一個值的序列,則必須把序列放在花括號內。
// expected和actual是string對象
if (expected != actual)
error_msg({"functionX", expected, actual});
else
error_msg({"functionX", "okay"});
含有initializer_list
形參的函數也可以同時擁有其他形參。例如調試系統可以有個名爲ErrCode
的類用來表示不同類型的錯誤。
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() <<":";
for(const auto &item : il)
cout << item << " ";
cout << endl;
}
返回數組指針
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];
任何返回類型都可以使用** 尾置返回類型(trailing return type)**,但是一般只有比較複雜的返回類型纔會使用。
如果我們知道函數返回的指針將指向哪個數組,就可以使用 decltype 關鍵字聲明返回類型。但decltype
並不會把數組類型轉換成指針類型,所以decltype
的結果是一個數組,想要返回指針還要在函數聲明中添加一個 *
符號。
函數重載
同一作用域內的幾個名字相同但形參列表不同的函數叫做重載函數。當調用這些函數時,編譯器會根據傳遞的實參類型推斷想要的是哪個函數。
void print(const char *cp);
void print(const int *beg, const int *end);
void print (const int ia[), size_t ze);
重載函數在形參數量或者參數類型上應該有所不同,僅僅是返回值類型不同是不能構成重載的。
頂層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
對象,只能調用常量版本的函數;而對於非常量,編譯器會優先調用非常量版本的函數。
調用重載函數時有三種可能的結果:
- 編譯器找到一個與實參 *最佳匹配(best match) *的函數,並生成調用該函數的代碼。
- 編譯器找不到任何一個函數與實參匹配,發出 無匹配(no match) 的錯誤信息。
- 有一個以上的函數與實參匹配,但每一個都不是明顯的最佳選擇,此時編譯器發出 二義性調用(ambiguous call) 的錯誤信息。
特殊用途語言特性
默認實參
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
string window;
window = screen();//等價於screen(24, 80, ' ')
window = screen(66); //等價於screen(66, 80, ' ')
window = screen(66, 256); //等價於screen(66, 256, ' ')
window = screen(66, 256, '#');
注意:一旦某個形參被賦予了默認值,它後面的所有形參必須有默認值。
雖然多次聲明同一個函數是合法的,但是在給定的作用域中一個形參只能被賦予一次默認實參。函數的後續聲明只能爲之前那些沒有默認值的形參添加默認實參,而且該形參右側的所有形參必須都有默認值。
// 表示高度和寬度的形參沒有默認位
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 錯誤:重複聲明
string screen(sz = 24, sz = 80, char); // 正確:添加默認實參
默認實參只能出現在函數聲明和定義其中一處。通常應該在函數聲明中指定默認實參,並將聲明放在合適的頭文件中。
內聯函數
內聯函數會在每個調用點上“內聯地”展開,可避免函數調用的開銷。
在函數的返回類型前加上關鍵字inline
就可以將函數聲明爲內聯函數了。
// 內聯版本:尋找兩個string對象中較短的那個
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
內斂說明只是向編譯器發起一個建議,編譯器可以忽略這個請求。
一般來說,內聯機制用於優化規模較小、流程直接、頻繁調用的函數。
constexpr函數
constexpr
函數是指能用於常量表達式的函數。constexpr
函數的返回類型及所有形參的類型都得是字面值類型。
C++11標準要求 constexpr
函數體中必須有且只有一條 return
語句,但是此限制在C++14標準中被刪除。
constexpr
函數的返回值可以不是一個常量。
// 如果arg是常量表達式,則scale(arg)也是常量表達式
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt;
}
當 scale
的實參是常量表達式時,它的返回值也是常量表達式;如果我們用一個非常量表達式調用scale
函數,比如int
類型的對象i
,則返回值是一個非常量表達式。當把scale
函數用在需要常量表達式的上下文中時,由編譯器負責檢查函數的結果是否符合要求。如果恰好不是常量表達式,編譯器將發出錯誤信息。
int arr[scale(2)]; // 正確:scale(2)是常量表達式
int i = 2; // i不是常量表達式
int a2[scale(i)]; // 錯誤:scale(i)不是常量表達式
constexpr
函數不一定返回常量表達式。
和其他函數不同,內聯函數和 constexpr
函數可以在程序中多次定義。因爲在編譯過程中,編譯器需要函數的定義來隨時展開函數,僅有函數的聲明是不夠的。對於某個給定的內聯函數或 constexpr
函數,它的多個定義必須完全一致。因此內聯函數和 constexpr
函數通常定義在頭文件中。
調試幫助
程序可以包含一些用於調試的代碼,但是這些代碼僅在開發程序時使用。當應用程序編寫完成準備發佈時,要先屏蔽掉調試代碼。這些方法用到兩項預處理功能:assert
和NDEBUG
。
assert預處理宏
assert
是一種預處理宏。assert
宏定義在cassert
頭文件中。由於預處理名字由預處理器而非編譯器管理,因此我們可以直接使用預處理名字而無須提供using
聲明。也就是說,我們應該直接使用assert
而不是std::assert
,也不需要爲assert
提供using
聲明。
assert (expr);
首先對 expr
求值,如果表達式爲假(即0),assert
輸出信息並終止程序的執行;如果表達式爲真(即非0),assert
什麼也不做。
NDEBUG 預處理變量
assert
的行爲依賴於於一個名爲 NDEBUG
的預處理變量的狀態。如果定義了 NDEBUG
,則assert
什麼也不做;默認狀態下沒有定義 NDEBUG
,此時 assert
將執行運行時檢查。
可以使用 #define
語句定義 NDEBUG
,從而關閉調試狀態。
除了用於assert
外,也可以使用NDEBUG
編寫自己的條件調試代碼:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
//__func__是編譯器定義的一個局部靜態變量,用於存放函數的名字
cerr << __func__ << ": array size is "<< size << endl;
#endif
}
//...
下表常用的幾個對於程序調試很有用的名字:
變量名稱 | 內容 |
---|---|
func | 存放當前調試函數的名字 |
FILE | 存放文件名的字符串字面值 |
LINE | 存放當前行號的整型字面值 |
TIME | 存放文件編譯時間的字符串字面值 |
DATE | 存放文件編譯日期的字符串字面值 |
函數匹配
調用重載函數時應儘量避免強制類型轉換。如果在實際使用中確實需要強制類型轉換,則說明我們設計的形參集合不合理。
爲了確定最佳匹配,編譯器將實參類型到形參類型的轉換劃分成幾個等級:
精確匹配 包括以下情況:
- 實參類型和形參類型相同,包括下面幾種情況:
- 實參從數組類型或函數類型轉換成對應的指針類型
- 向實參添加頂層
const
或者從實參中刪除頂層const
- 通過
const
轉換實現的匹配
- 通過類型提升實現的匹配
- 通過算術類型轉換或指針轉換實現的匹配
- 通過類類型轉換實現的匹配
如果重載函數的區別在於它們的引用或指針類型的形參是否含有底層 const
,或者指針類型是否指向const
,則調用發生時編譯器通過實參是否是常量來決定函數的版本。
Record lookup(Account&); // 函數的參數是Account的引用
Record lookup(const Account&); // 函數的參數是一個常量引用
const Account a;
Account b;
lookup(a); // 調用lookup(const Account&)
lookup(b); // 調用lookup(Account&)
函數指針
// 比較兩個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 作用於函數時,返回的是函數類型,而不是函數指針類型。因此我們需要顯式的加上*
以表明我們需要返回指針,而非函數本身。
string::size_type sumLength(const string&, const string&);
string::size_type largerLegth(const string&, const string&);
//根據形參的取值,getFcn函數返回指向sumLength或者largerLength的指針
decltype(sumLength) *getFcn(const string &);
練習
- 編寫一個函數,它的參數是
initializer_list<int>
類型的對象,函數的功能是計算列表中所有元素的和。
#include <iostream>
#include <initializer_list>
using namespace std;
int list_sum(initializer_list<int> li){
int sum = 0;
for(auto item: li)
sum += item;
return sum;
}
int main(){
cout << "the sum of {1,2,3,4,5} is "
<< list_sum({ 1, 2, 3, 4, 5 }) << endl;
system("pause");
return 0;
}
- 編寫一個遞歸函數,輸出
vector
對象的內容。
#include <iostream>
#include <vector>
using namespace std;
void print_v(const vector<int> &vi, unsigned int index){
if(!vi.empty() && index < vi.size()){
cout << vi[index] << endl;
print_v(vi, index+1);
}
}
int main()
{
vector<int> v{ 1, 2, 3, 4, 5, 6};
print_v(v, 0);
system("pause");
return 0;
}
- 在調用 factorial 函數時,爲什麼我們傳入的值是
val-1
而非val--
?
int factorial(int val){
if (val < 1)
return factorial(val-1) * val;
return 1;
}
如果把傳入的值 val-1
改成 val--
,則變量的遞減操作與讀取變量值的操作共存於同一條表達式中,這時有可能產生未定義的值。
- 編寫一個函數聲明,使其返回數組的引用並且該數組包含10個
string
對象。
string (&f())[10]; //方法一
auto f()->string(&)[10]; //方法二 使用尾置返回類型(trailing return type)
string str_array10[10];
decltype(str_array10)& f(); //方法三 使用decltype
f()
表示調用 f
函數無須任何實參,(&f())
表示函數的返回結果是一個引用,(&f())[10]
表示引用的對象是一個維度爲10的數組,string (&f())[10]
表示數組內的元素是string
對象。
- 說明在下面的每組聲明中第二條語句是何含義。
如果有非法的聲明,請指出來。
(a) int calc(int, int);
int calc(const int, const int);
(b) int get();
double get();
(c) int *reset(int *);
double *reset(double *);
( a )的第二個聲明是非法的,因爲頂層const
不能構成重載。
( b )的第二個聲明也是非法的,因爲 返回值不能構成重載。
(c)是合法的,構成重載。
- 下面的哪個聲明是錯誤的?爲什麼?
(a) int ff(int a, int b = 0, int c = 0);
(b) char *init(int ht = 24, int wd, char bckgrnd);
(a)是合法的,(b)是錯誤的。因爲C++規定一旦某個形參被賦予了默認實參,則它後面的所有形參都必須有默認實參。
- 能把
isShorter
函數定義成constexpr
函數嗎?
如果能,將它改寫成constxpre
函數;如果不能,說明原因。
constexpr
函數是指能用於常量表達式的函數,constexpr
函數的返回類型和所有形參的類型都得是字面值類型,而且函數體中必須有且只有一條return
語句。
顯然isShorter
函數不符合 constexpr
函數的要求,它雖然只有一條 return
語句,但是返回的結果調用了標準庫 string
類的 size()
函數和 <
比較符,無法構成常量表達式,因此不能改寫成 constexpr
函數。
- 編寫函數的聲明,令其接受兩個
int
形參並返回類型也是int
;然後聲明一個vector
對象,令其元素是指向該函數的指針。
int f(int, int);
vector<decltype(f)* > vf;
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int func1(int a, int b){
return a + b;
}
int func2(int a, int b){
return a - b;
}
int func3(int a, int b){
return a * b;
}
int func4(int a, int b){
return a / b;
}
int main()
{
int i = 5, j = 10;
decltype(func1) *p1 = func1, *p2 = func2, *p3 = func3, *p4 = func4;
vector<decltype(func1)*>vF = { p1, p2, p3, p4 };
// 遍歷vector中的每個元素,依次調用四則運算函數
for (auto p : vF){
cout << p(i, j) <<" ";
}
system("pause");
return 0;
}