6.5.1 默認實參
- 將反復出現的數值稱為函數的默認實參,調用含有默認實參的時候可以包含該實參也可以不包含
- 比如程序打開頁面會有一個默認的寬高,如果用戶不喜歡也允許用戶自由指定與默認數值不同的數值,具體例子如下圖所示
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80,char backgrnd = ' ');
- 默認實參作為形參的初始化數值出現在形參列表中,可以為一個或者多個形參定義成默認數值,但是,一旦某個形參被賦予了默認數值,這個形參之后的數值也必須有默認數值。
使用默認實參調用函數
- 如果使用默認的實參,調用函數的時候省略掉該實參就可以了。
- 如上面提出的screen函數,有三個變量,因此可以使用0、1、2、3個實參調用該函數
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80,char backgrnd = ' ');
int main(){string window;window = screen();window = screen(1);window = screen(1,2);window = screen(1,2,'&');
}
- window = screen(,,'&');//錯誤,只可以忽略掉尾部的實參,即左邊的必須補齊定義,只有右邊的才可以缺省
- 因此,在實際使用的場景中,需要設計形參的順序,將不怎么使用默認形參的變量放在前面
默認實參聲明
- 對于函數的聲明來講,通常是將其放在頭文件中,并且每個函數聲明一次,但是多次聲明也是合法的。
- 但是,在給定的作用域中,一個形參只可以賦予一次默認實參。即,函數的后續聲明只能為之前那些沒有默認值的形參添加默認的實參,而且這個形參的左側的所有的形參都有默認值。
默認實參初始值
- 局部變量不可以作為默認的實參,只要表達式的類型可以轉化為形參所需要的類型,該表達式就可以作為默認的實參。
typedef string::size_type sz;
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,'*')
}
- 在函數f2的內部改變了def的值,所以對于screen的調用將會傳遞這個更新過的數值。
- 對于wd,內部聲明的局部變量會隱藏外部的wd,但是該局部變量并不會傳遞給screen的默認的參數
6.5.2 內聯函數和constexpr函數
定義函數的好處
- 使用函數可以確保行為的統一,每次相關的操作都按照同樣的方式進行
- 如果需要修改計算的過程,顯然修改函數比先找到等價的表達式所有出現地方再逐一修改更加方便
- 函數可以被其他應用重復調用,省去了重新編寫開發的代價
壞處
- 調用函數會比使用表達式慢,因為使用函數要涉及到一系列的工作:調用前保存寄存器、并且在返回的是會恢復;需要拷貝實參,程序需要在不同的新的位置上繼續執行。
內聯函數可以避免函數的調用所帶來的開銷
- 將函數指定為內聯函數,通常是將它在每一個調用點上“內聯地”展開,將會減少函數運行使得開銷。
- 在函數的前面加上關鍵字inline就可以將其聲明稱為內聯函數
inline const string &shorterString(const string &s1,const string &s2){return s1.size() < s2.size() ? s1 : s2;
}
- 內聯說明只是向編譯器發出一個請求,編譯器可以選擇忽略這個請求
- 內聯機制一般用于優化規模較小,流程直接、頻繁調用的函數,很多編譯器都不支持內聯遞歸函數,如果程序很大的話,也不大可能在調用點內聯的展開
constexpr函數
- constexpr是指可以用于常量表達式的函數。
- 其函數的返回類型以及所有的形參的類型都得是字面值類型,而且函數體中必須有且只有一個return語句。
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //正確,foo是一個常量的表達式
- 把new_sz定義成無參數的constexpr函數,因為編譯器能在程序編譯的時候驗證new_sz函數返回的是常量的表達式,所以可以使用new_sz來初始化foo函數。
- 執行該初始化任務的時候,編譯器把constexpr函數的調用替換成其結果數值。為了能在編譯的過程中隨時展開,constexpr會被隱式轉變為內聯函數。
- constexpr函數體內也可以包含其他語句,只要這些語句在運行的時候不執行任何操作就可以。例如,constexpr函數內可以有空的語句、類型別名以及using聲明。
- 允許constexpr函數的返回值并非一個常量。
//如果arg是常量的表達式,則scale(arg)也是常量的表達式
constexpr size_t scale(size_t cnt){return new_sz() * cnt;//當scale的實參是常量表達式的時候,返回的數值也是常量的表達式;反之不然;
}
int main(){int arr[scale(2)];//正確,scale(2)是常量的表達式int i = 2;int a2[scale(i)];//錯誤,scale(i)不是常量的表達式
}
把內聯函數和constexpr函數放在頭文件中
- 和其他函數不一樣,內聯函數和constexpr函數可以在程序中多次定義。畢竟,編譯器要想展開函數僅僅有函數的聲明是不夠的,還需要函數的定義。但是對于某個給定的內聯函數或者constexpr函數來說,他的多個定義必須完全一致,因此通常將他們定義在頭文件中。
6.5.3 調試幫助
- C++有些時候會用到一些類似于頭文件保護的技術,從而可以有選擇的執行代碼。
- 基本思想是,程序包含一些用于調試的代碼,但是這些代碼只會在開發程序的時候使用,當程序完準備上線的時候,需要先屏蔽代碼,這就用到了兩項預先處理的功能:assert和NDEBUG
assert預處理宏
- 預處理宏就是一個預處理變量,其行為類似于內聯函數
- assert使用一個表達式作為它的條件 assert(expr);? 首先對于expr進行求值,如果表達式為假,assert會輸出信息并且終止程序的執行。如果表達式為真,assert什么也不做
- assert的頭文件會定義在cassert文件中,因為預處理名字是由預處理器而不是編譯管理,因此可以直接使用預處理名字而不需要提供using聲明。即直接使用assert,而不是std::assert,也不需要為assert提供using的聲明。
- 和預處理的變量一樣,宏名字在程序內部必須唯一。但是在實際編程的時候,即使沒有包含這個頭文件,也不要使用assert這個名字進行任何相關的變量、函數、其他實體等的命名,因為,很多頭文件都會包含這個cassert,有可能通過其他途徑包含在程序中。
- assert宏常常用于檢查“不能發生的條件”。例如,一個對于輸入文本進行操作的語句對于輸入的長度有很大的要求,必須大于某一個指定的閾值。這個使用,程序可能會包含一個如下所示的語句? assert(word.size() > threshold );
- assert都是一種調試程序的輔助手段,但是他不可以替代運行時候的邏輯檢查,也不可以替代程序本身應該包含的錯誤檢查
NDEBUG預處理變量
- assert的行為依賴于一個名為NDEBUG的預處理變量的狀態。如果定義了NDEBUG,那么assert就什么都不做。默認情況下是沒有定義這個NDEBUG的,因此assert會執行運行檢查。
- 使用#define NDEBUG 或者使用命令行進行程序編譯的時候,使用 CC -D NDEBUG main.c 這兩者之間是等效的
- 同理,也可以使用NDEBUG來編寫自己的條件調試的代碼,如果NDEBUG沒有定義,那么將會執行#ifdef和#endif之間的代碼;如果定義了NDEBUG,那么這些代碼將會被忽略掉
- #define NDEBUG
#ifdef NDEBUG// __func__是編譯器定義的一個局部靜態變量,用于存放函數的名字cerr << __func__ << ": array size is " << size << endl;
#endif
- __func__是一個const char的一個靜態數組,用于存放函數的名字