-
通過調用運算符
()
調用函數 -
函數的調用完成兩項工作:
- 用實參初始化函數對應的形參
- 將控制權轉移給被調用函數:主調函數的執行被暫時中斷,被調函數開始執行
-
盡管實參與形參存在對應關系,但是并沒有規定實參的求值順序。編譯器能以任意可行的順序對實參求值
-
任意兩個形參都不能同名,形式參數名是可選的,但是由于我們無法使用未命名的形式參數,所以形式參數一般都應該有一個名字
-
函數的返回類型不能是數組類型或者函數類型,但可以是指向數組或者函數的指針
-
函數體必須是大括號包圍的!
-
普通局部變量對應的對象是自動對象:當函數的控制路徑經過變量定義語句時創建該對象,當到達定義所在的塊末尾時銷毀它。
-
局部靜態對象:在程序的執行路徑第一次經過對象定義時對它初始化,并且直到程序終止時才被銷毀,在此期間即使對象所在的函數執行結束也不會對他有影響。如果局部靜態變量沒有顯式的初始值,則執行值初始化(內置類型會初始化為0)
-
函數只能定義一次,但是可以聲名多次,唯一的區別是函數的聲明不需要函數體,用一個分號替代即可(因此經常省略形式參數的名字,但是寫上名字也有利于理解函數的功能),函數聲明也稱作函數原型
-
形式參數初始化的機理和變量初始化一樣
-
拷貝大的類類型對象或者容器對象比較低效,甚至有的類型(包括IO類型在內)根本不支持拷貝操作。因此函數只能通過引用形式參數訪問該類型的對象,如果函數無須改變引用形式參數的值,最好將其聲明為常量引用
-
熟悉C的程序員常常使用指針類型的形參訪問函數外部的對象,在C++中最好還是使用引用類型的形式參數代替指針
-
對于有可能是臨時參數的形式參數,我們不應該使用引用(因為無法引用到常量上)
-
函數重載要求同名函數的形式參數列表應該有明顯的區別,因此如果僅僅是
const
的不同則不能進行重載(應該要求形式參數類型不同) -
形式參數的初始化方式和變量的初始化方式是一樣的:我們可以使用非常量初始化一個底層
const
對象,但是反過來不行,同時一個普通的引用必須使用同類型的對象初始化(詳細同第二章指針和引用部分) -
盡量使用常量引用
- 給函數調用者傳遞正確的信息
- 使用非常量引用會極大地限制函數所能接受的實際參數類型:我們不能把
const
對象、字面值對象或者需要類型轉換的對象傳遞給普通的引用參數
-
數組形式參數
-
數組的兩個特點:
- 不允許拷貝數組
- 使用數組時通常會將其轉換成指針
-
以下三種聲明方式是等價的:
void print(const int*); void print(const int[]); void print(const int[10]);
-
因為我們不清楚數組的實際大小,因此在使用過程中必須通過一定的方式判斷是否越界
-
使用標記指定數組長度:例如C風格的字符串,最后一個一定是一個
\0
,我們可以判斷是否為\0
來判斷是否到達末尾 -
使用標準庫規范:
void print(const int *beg, const int *end) {while(beg != end) {cout << *beg++ << endl;} } int arr[] = {0, 1, 2}; print(begin(arr), end(arr)); //#include<iterator>
-
顯式傳遞一個表示數組大小的形式參數
-
同常量引用,當函數不需要對數組元素執行讀寫操作的時候,數組形式參數應該是指向
const
的指針
-
-
數組引用參數
void print(int (&arr)[10]) {for (auto item : arr) {cout << item << endl;} }
對于數組的引用詳細可以看第三章關于數組部分的筆記
-
傳遞多維數組:C++語言中實際上沒有真正的多維數組,所謂的多維數組其實是數組的數組。數組第二維(以及后面所有的維度)的大小都是數組類型的一部分,不能省略
void print(int (*matrix)[10], int rowSize); void print(int matrix[][10], int rowSize);
上面兩種聲明是完全等價的
-
-
命令行選項可以通過兩個(可選的)形式參數傳遞給
main
函數:int main(int argc, char *argv[]); int main(int argc, char **argv);
- 第二個形式參數是一個數組,它的元素指向C風格字符串的指針,第一個形式參數
argc
表示數組中字符串的數量 argv
的第一個元素指向程序的名字或者一個空字符串,接下來的元素依次傳遞命令行提供的參數,最后一個指針的元素值保證為0
- 第二個形式參數是一個數組,它的元素指向C風格字符串的指針,第一個形式參數
-
為了編寫能處理不同數量實際參數的函數,C++11標準提供了幾種方法:
-
如果函數的實際參數數量未知但是類型相同,我們可以使用
initializer_list
類型的形式參數(需要#include<initializer_list
)initializer_list<T> lst; //默認初始化,T類型元素的空列表 initializer_list<T> lst{a,b,c...}; //lst的元素是對應初始值的副本,列表中的元素是const lst2(lst); //等價與lst2 = lst ,賦值,不會拷貝列表中的元素,原始列表和副本共享元素 lst.size() //列表中的元素數量 lst.begin() lst.end()
initialzer_list
對象中的元素永遠是常量值void err_msg(initializer_list<string> il) {for (auto beg = il.begin(); beg != il.end(); ++beg)cout << *beg << " "; //也可以通過范圍for循環訪問65cout << endl; } err_msg({"A", "B", "C"}); err_msg({"A", "B"});
-
使用可變參數模板
-
-
返回
void
的函數不要求非得有return
語句,因為在這類函數的最后一句會隱式地執行return
,一個返回類型是void
的函數也能使用return expression
,不過此時return
語句的expression
必須是另一個返回void
的函數,強行令void
函數返回其他類型將產生編譯錯誤 -
有返回值函數
bool str_subrange(const string &str1, const string &str2) {auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();for (decltype(size) i = 0; i < size; ++i) {if (str1[i] != str2[i])return false;}return true; }
-
返回一個值的方式和初始化一個變量或形式參數的方式完全一樣:返回的值用于初始化調用點的一個臨時量,這個臨時量就是函數調用的結果
-
不要返回局部對象的引用或者指針:函數完成后,它所占用的存儲空間也隨之被釋放掉。因此,函數終止意味著局部變量的引用(或指針)將指向不再有效的內存區域
-
調用一個返回引用的函數得到左值,其他返回類型得到右值
-
C++11新標準規定,函數可以返回花括號包圍的值的列表。類似于其他返回結果,此處的列表也用來對表示函數返回的臨時量進行初始化
-
我們允許
main
函數沒有return
語句直接結束,如果控制到達了main
函數的結尾處而且沒有return
語句,編譯器將隱式地插入一條返回0的return
語句。在cstdlib
頭文件中定義了兩個預處理變量,我們用這兩個變量分別表示成功與失敗int main() {if (some_failure) {return EXIT_FAILURE; } else {return EXIT_SUCCESS;} }
-
從語法上來說,想要定義一個返回數組的指針或引用的函數比較繁瑣,但是使用類型別名可以簡化這一任務
typedef int arrT[10]; //using arrT = int[10]; arrT* func(int i); //返回一個指向含有10個整數的數組的指針 int (*func(int i))[10]; //等價于上面的聲明
我們還可以使用尾置返回類型使得上面的聲明變得清晰:
auto func(int i) -> int(*)[10];
如果我們知道函數返回的指針指向哪個(類別)的數組,我們還可以使用
decltype
關鍵字聲明返回類型int arr[] = {0, 1, 2, 3, 4}; decltype(arr) *arrPtr(int i) {return &arr; }
-
如果同一作用域內的幾個函數名字相同但是形式參數列表(形式參數數量或形式參數類型)不同,我們稱之為重載函數。
main
函數不能重載。需要注意的是,函數的重載和返回類型關系不大 -
頂層
const
不影響傳入函數的對象,一個擁有頂層const
的形式參數無法和另一個沒有頂層const
的形式參數區分開來。但是底層const
是會影響函數的重載的,當傳入的對象是常量時,會選擇帶有底層cosnt
的函數版本,如果傳遞一個非常量對象,編譯器會優先選用非常量版本的函數 -
最好只重載那些確實非常相似的操作
-
我們也可以使用
const_cast
實現const
到非const
的轉換:const string &func(const string &s1, const string &s2) {return s1.size() < s2.size() ? s1 : s2; } string &func(string &s1, string &s2) {return const_cast<string&>(func(const_cast<const string&>(s1), const_cast<const string&)(s2)); }
-
當調用重載函數時的結果:
- 編譯器找到一個與實際參數最佳匹配的函數
- 找不到任何一個函數匹配,發出無匹配的錯誤
- 有多于一個函數可以匹配,但是沒一個都不是明顯的最佳選擇,此時也將發生錯誤,稱為二義性調用
-
如果我們在內層作用域中聲明名字,它將隱藏外層作用域中聲明的同名實體。在不同的作用域中無法重載函數名
-
一旦某個形式參數被賦予了默認值,它后面所有形式參數都必須有默認值。當設計含有默認實際參數的函數時,其中一項任務是合理設置形式參數順序,盡量讓不怎么使用默認值的形式參數出現在前面
-
通常,應該在函數聲明中指定默認實際參數,并將聲明放在合適的頭文件中。局部變量不能作為默認實際參數
-
用作默認實際參數的名字在函數聲明所在的作用域內解析,而這些名字的求值過程發生在函數調用時。比如函數
A
某個默認實際參數的值是一個函數B
調用的返回值,則該函數調用B
會在A
被調用的時候調用#include <iostream>using namespace std;string A = "global A"; string B = "global B";const string &func() {return const_cast<const string&>(B); }int main() {ios::sync_with_stdio(false);void test(const string &a = A, const string &b = func());string A = "local A"; //local varibale cannot be default value::A += " has been changed";B = "local B";test();return 0; }void test(const string &a, const string &b) {cout << a << endl;cout << b << endl; }
運行結果:
global A has been changed local B
-
將一些簡單但需要多次重復的函數定義為內聯函數的好處:
- 有利于閱讀理解
- 可以被重復利用,使得代碼簡潔
- 需要修改時只用修改一個地方
-
在函數前面加上
inline
便可以將函數生命為內聯函數。內聯說明只是向編譯器發出一個請求,編譯器可以選擇忽略這個請求。一般來說,內聯機制用于優化規模較小、流程直接、頻繁調用的函數。很多編譯器都不支持內聯遞歸函數 -
constexpr
函數是指能夠用于常量表達式的函數- 函數的返回類型以及所有形式參數的類型都必須是字面值類型,而且函數體中必須有且只有一條
return
語句 constexpr
函數被隱式地指定為內聯函數- 允許
constexpr
函數的返回值不是一個常量,如果參數非常量表達式導致最后返回值不是常量表達式則在需要常量的地方調用會報錯
- 函數的返回類型以及所有形式參數的類型都必須是字面值類型,而且函數體中必須有且只有一條
-
內聯(
inline
)函數和constexpr
函數可以在程序中多次定義,但是多個定義必須一致。基于這個原因,內聯函數和constexpr
函數通常定義在頭文件中 -
程序可以包含一些用于調試的代碼,但是這些代碼只在開發程序時使用。當應用程序編寫完成準備發布時,要先屏蔽掉調試代碼
assert (expr)
預處理宏:首先對expr
求值,如果表達式為假(0),assert
輸出信息并終止程序的執行,如果為真,則什么也不做- 需要頭文件
cassert
,因為是供預處理器處理,所以無需提供using
聲明 assert
宏常用于檢查“不能發生”的條件,即程序的運行是建立在assert
的條件成立的情況下
- 需要頭文件
assert
的行為依賴于一個名為NDEBUG
的預處理變量的狀態,如果定義了NDEBUG
,則assert
什么也不做。默認情況下沒有定義NDEBUG
,此時assert
將執行檢查。如果想要關閉assert
檢查:- 在程序開頭加上
#define NDEBUG
- 或在編譯的時候加上
-D NDEBUG
參數
- 在程序開頭加上
assert(word.size() >= threshold); //等價寫法: #ifndef NDEBUG if (word.size() < threshold)cerr << "Error: " << __FILE__<< " : in function " << __func__<< " at line " << __LINE__ << endl<< " Compiled on " << __DATE__<< " at " << __TIME__ << endl<< " Word read was \"" << word<< "\": Length too short" << endl; #endif
-
函數匹配
- 選定候選函數:
- 與被調用函數同名
- 其聲明在調用點可見
- 選定可行函數:
- 形式參數和實際參數數量一直
- 類型符合(相同或可以進行轉換)
- 尋找最佳匹配
- 該函數的每個實際參數的匹配不劣于其他可行函數需要的匹配
- 至少有一個實際參數的匹配優于其他可行函數提供的匹配
- 如果最終確定了一個函數,則匹配成功,如果最后匹配出多個函數,則匹配失敗,報告二義性錯誤
- 選定候選函數:
-
為了確定最佳匹配,編譯器將實際參數類型到形式參數類型的轉換分成了幾個等級:
- 精確匹配
- 實際參數類型和形式參數類型相同
- 實際參數從數組類型或函數類型轉換成相應的指針類型
- 向實際參數添加或者刪除頂層
const
- 通過
const
轉換實現的匹配 - 通過類型提升實現的匹配(小整數類型會自動變成
int
,如果放不下再變成unsigned int
) - 通過算數類型轉換或指針轉換實現的匹配
- 通過類類型轉換實現的匹配
- 精確匹配
-
想要聲明一個指向函數的指針,只需要用指針替換函數名即可:
bool func(const string &, const string &); bool (*pf)(const string &, const string &);
當我們把函數名作為一個值使用時,該函數自動地轉換成指針
pf = func; //等價于 pf = &func; //&是可選的
我們可以直接使用指向函數的指針調用該函數,無需提前解引用指針
//等價的三種調用方法 bool b1 = pf("A", "B"); bool b2 = (*pf)("A", "B"); bool b3 = func("A", "B");
-
在指向不同函數類型的指針之間不存在轉換規則,但是我們可以為函數指針賦一個
nullptr
或者0 -
當我們使用重載函數為指針賦值時,上下文必須清晰地界定到底應該選用哪個函數
-
我們可以定義函數指針作為形式參數
void work(bool pf(const string &, const string &)); //看起來是函數類型,實際上會自動轉換成指針 //等價于 void work(bool (*pf)(const string &, const string &));
我們同樣可以使用
typedef
和decltype
簡化操作typedef bool funcT(const string &, const string &); //funcT是函數類型 typedef decltype(func) funcT2; //同上 typedef bool (*funcTP)(const string &, const string &); //funcTP是函數指針 typedef decltype(func) *funcTP2; //同上 void work(funcT); //同之前定義,函數類型會自動轉換成指針類型 void work(funcTP); //同之前定義
-
編譯器不會自動地將函數返回類型當成對應的指針類型進行處理
using F = int(int *, int); using FP = int(*)(int *, int); //以下四種方式是等價的 FP f1(int); F *f1(int); int (*f1(int))(int *, int); auto f1(int) -> int (*)(int *, int);
如果使用
decltype
指定返回函數指針類型記得decltype(func)
如果func
是一個函數則得到的是函數類型,還需要加上*
decltype(func) *getFunc(const string &);