?

優(yōu)秀的程序組織方法是: 把程序當(dāng)作一組明確定義過依賴關(guān)系的模塊,利用語(yǔ)言特性表達(dá)這種模塊化的邏輯關(guān)系,再把這種關(guān)系通過文件以實(shí)體形式暴露出來(lái),以實(shí)現(xiàn)高效的分離編譯。
除了函數(shù)、類以及枚舉,C++還提供一個(gè)叫命名空間(namespace)的機(jī)制, 用來(lái)表示某些聲明相互依托,以便這些名稱不會(huì)跟其它名稱發(fā)生沖突。 比如說(shuō),我想弄個(gè)復(fù)數(shù)類型:
namespace My_code {
class complex {
// ...
};
complex sqrt(complex);
// ...
int main();
}
int My_code::main() {
complex z {1,2};
auto z2 = sqrt(z);
std::cout << '{' << z2.real() << ',' << z2.imag() << "}\n";
// ...
}
int main() {
return My_code::main();
}
void my_code(vector<int>& x, vector<int>& y) {
using std::swap; // 使用標(biāo)準(zhǔn)庫(kù)中的 swap
// ...
swap(x,y); // std::swap()
other::swap(x,y); // 別的 swap()
// ...
}
?
假設(shè)我們想從越界訪問的錯(cuò)誤中恢復(fù)運(yùn)行,解決方案是:實(shí)現(xiàn)Vector的人檢測(cè)越界訪問的企圖,然后把它告知用戶。然后用戶就可以采取適當(dāng)?shù)拇胧?。舉例來(lái)說(shuō):Vector::operator能檢測(cè)越界訪問的企圖,并拋出out_of_range異常:
double& Vector::operator[](int i) {
if (i<0 || size()<=i)
throw out_of_range{"Vector::operator[]"};
return elem[i];
}
throw把控制權(quán)轉(zhuǎn)給處理out_of_range異常的代碼,這個(gè)代碼位于某些函數(shù)里,而這些函數(shù)直接或間接地調(diào)用了Vector::operator[]。 要做到這些,編譯器就得展開函數(shù)調(diào)用堆棧,以便回退到調(diào)用者的代碼環(huán)境。就是說(shuō),異常處理機(jī)制將根據(jù)需要退出作用域和函數(shù),以便回退到有意處理該類異常的調(diào)用者,必要時(shí)沿途調(diào)用析構(gòu)函數(shù)。例如:
void f(Vector& v) {
// ...
try { // 此處的異常將被下面定義的代碼處理
v[v.size()] = 7;// 試圖訪問v到末尾之后
} catch (out_of_range& err) { // 壞菜了:out_of_range錯(cuò)誤
// ... 處理越界錯(cuò)誤 ...
cerr << err.what() << '\n';
}
// ...
}
我們把有意異常處理的代碼放到try-代碼塊里。 給v[v.size()]賦值的企圖不會(huì)得逞。 因此,會(huì)進(jìn)入catch-子句,里面包含處理out_of_range異常的代碼。 out_of_range異常定義在標(biāo)準(zhǔn)庫(kù)中(<stdexcept>里), 并且實(shí)際上已經(jīng)被標(biāo)準(zhǔn)庫(kù)里某些容器的訪問函數(shù)用到了。
我以引用方式捕捉此異常以避免復(fù)制,并使用what()函數(shù)打印錯(cuò)誤信息, 這個(gè)信息是在throw-位置放進(jìn)異常里的。
使用異常處理機(jī)制可以讓錯(cuò)誤處理更簡(jiǎn)潔、更系統(tǒng)化,也更具可讀性。 要確保這一點(diǎn),就別濫用try-語(yǔ)句。 以簡(jiǎn)潔和系統(tǒng)化方式實(shí)現(xiàn)錯(cuò)誤處理的主要技術(shù)(被稱為 資源請(qǐng)求即初始化(Resource Acquisition Is Initialization; RAII))。RAII的大體思路是:讓類的構(gòu)造函數(shù)獲取正常運(yùn)作所需的全部資源,然后讓析構(gòu)函數(shù)釋放全部資源,這樣資源的釋放就可以有保障地隱式執(zhí)行。
如果一個(gè)函數(shù)絕對(duì)不應(yīng)該拋出異常,可以用noexcept聲明它。例如:
void user(int sz) noexcept {
Vector v(sz);
iota(&v[0],&v[sz],1); // 用1,2,3,4...填充v
// ...
}
萬(wàn)一user()還是拋出了異常,就會(huì)立即調(diào)用std::terminate()以終止程序。
既然operator運(yùn)算符要操作Vector類型的對(duì)象,那么,如果Vector的成員不具備“合理的”值,這個(gè)運(yùn)算就毫無(wú)意義。確切的說(shuō),我們指出了“elem指向承載sz個(gè)元素的數(shù)組”,但僅僅止步于注釋中。這種為類聲稱某個(gè)假設(shè)為真的語(yǔ)句被稱為 類的不變式,簡(jiǎn)稱不變式。 為類制定不變式(以確保成員函數(shù)有的放矢)的職責(zé)歸構(gòu)造函數(shù),而成員函數(shù)運(yùn)行完成之后,要確保不變式依然成立。不巧的是,我們Vector的構(gòu)造函數(shù)有點(diǎn)虎頭蛇尾了。它出色地為Vector的成員變量完成了初始化,卻沒留意傳入的參數(shù)是否合理??紤]一下這個(gè):
Vector v(-27);
基本上,這就要出事了。
更靠譜的定義是這樣的:
Vector::Vector(int s) {
if (s<0)
throw length_error{"Vector constructor: negative size"};
elem = new double[s];
sz = s;
}
我使用標(biāo)準(zhǔn)庫(kù)里的length_error異常報(bào)告“元素?cái)?shù)量不是正整數(shù)”的問題, 因?yàn)闃?biāo)準(zhǔn)庫(kù)也用這個(gè)異常報(bào)告這類問題。 如果new運(yùn)算符沒找到可分配的內(nèi)存,將拋出std::bad_alloc。 我們可以這么寫:
void test() {
try {
Vector v(-27);
} catch (std::length_error& err) {
// 處理容量為負(fù)數(shù)的情況
} catch (std::bad_alloc& err) {
// 處理內(nèi)存耗盡的問題
}
}
一般來(lái)說(shuō),函數(shù)在捕獲異常之后就已經(jīng)沒法搞定待處理的任務(wù)了。 然后,異?!疤幚怼本鸵馕吨拖薅鹊木植抠Y源清理,然后重新拋出該異常。例如:
void test() {
try {
Vector v(-27);
} catch (std::length_error&) {// 處理一下,然后重新拋出
cerr << "test failed: length error\n";
throw;// 重新拋出
} catch (std::bad_alloc&) {// 糟!這程序沒法處理內(nèi)存耗盡的問題
std::terminate();// 終止程序
}
}
目前只能依賴權(quán)宜之計(jì),例如用命令行里的宏控制運(yùn)行時(shí)檢查:
double& Vector::operator[](int i) {
if (RANGE_CHECK && (i<0 || size()<=i))
throw out_of_range{"Vector::operator[]"};
return elem[i];
}
標(biāo)準(zhǔn)庫(kù)提供了調(diào)試用的宏assert(),以確保某個(gè)條件在運(yùn)行時(shí)成立。例如:
void f(const char* p) {
assert(p!=nullptr); // p絕不能是nullptr
// ...
}
如果這個(gè)assert()條件在“調(diào)試模式”不成立,程序?qū)⒔K止。 如果不在調(diào)試模式,assert()就不做檢查。 這個(gè)方法忒糙還不靈活,不過通常也湊合夠用了。
?
異常給運(yùn)行時(shí)發(fā)現(xiàn)的問題報(bào)錯(cuò)。如果能在編譯時(shí)發(fā)現(xiàn)錯(cuò)誤就該大力推廣。 對(duì)于絕大多數(shù)類型系統(tǒng)、區(qū)分用戶定義類型的接口那些語(yǔ)言特性而言,這就是意義所在。 最起碼,我們可以對(duì)編譯期已知的大多數(shù)屬性進(jìn)行基本檢查, 以編譯器錯(cuò)誤信息的形式匯報(bào)不滿足需求的情況。例如:
static_assert(4<=sizeof(int), "integers are too small");// 檢查整數(shù)容量
在不滿足4<=sizeof(int)時(shí),這段代碼輸出integers are too small; 就是說(shuō)在系統(tǒng)里的int不足4個(gè)字節(jié)時(shí)。 我們把這種陳述預(yù)期的語(yǔ)句稱為斷言(assertion)。
static_assert機(jī)制可用于任意——可以通過常量表達(dá)式表示的——情形。例如:
constexpr double C = 299792.458; // km/s
void f(double speed) {
constexpr double local_max = 160.0/(60*60); // 160 km/h == 160.0/(60*60) km/s
static_assert(speed<C,"can't go that fast"); // 錯(cuò)誤:speed必須是常量
static_assert(local_max<C,"can't go that fast"); // OK
// ...
}
如果A不為true,那么static_assert(A,S)就會(huì)把S作為編譯器錯(cuò)誤信息輸出。 如果不想輸出特定信息就把S留空,編譯器會(huì)采用默認(rèn)信息:
static_assert(4<=sizeof(int)); // 采用默認(rèn)信息
默認(rèn)信息的內(nèi)容通常是static_assert所在的源碼位置,外加一個(gè)表示斷言謂詞的字母。
靜態(tài)斷言static_assert最重要的用途體現(xiàn)在泛型編程中對(duì)用作參數(shù)的類型有特定要求時(shí)。
?
把信息從程序的一個(gè)位置向另一個(gè)位置傳遞,最主要且推薦的方法是通過函數(shù)調(diào)用。 執(zhí)行功能所需的信息作為參數(shù)傳入函數(shù),生成的結(jié)果以返回值形式傳出。例如:
int sum(const vector<int>& v) {
int s = 0;
for (const int i : v)
s += i;
return s;
}
vector fib = {1,2,3,5,8,13,21};
int x = sum(fib); // x變成53
既然函數(shù)信息的傳入和傳出如此重要,就不難想見它們有多種方式。主要涉及:
- 該對(duì)象是被復(fù)制還是被共享?
- 如果該對(duì)象被共享,是否可變?
- 如果對(duì)象轉(zhuǎn)移了,是否要留下一個(gè)“空對(duì)象”?
在sum()例子中,作為結(jié)果的int是以復(fù)制方式傳出sum()的,但對(duì)于可能容量巨大的vector,讓它以復(fù)制方式進(jìn)入sum()將會(huì)低效且毫無(wú)意義, 所以參數(shù)以引用方式傳入。sum()無(wú)需修改其參數(shù),這種不可變更性通過將vector聲明為const來(lái)標(biāo)示,因此vector通過const-引用傳遞。
完成計(jì)算之后,需要把結(jié)果弄出函數(shù)并交回給調(diào)用者。 跟參數(shù)一樣,值返回也默認(rèn)采用復(fù)制方式,并且這對(duì)于較小的對(duì)象很完美。 只有在把不屬于函數(shù)局部作用域的東西轉(zhuǎn)給調(diào)用者時(shí),才通過“傳引用”返回。例如:
class Vector {
public:
// ...
double& operator[](int i) { return elem[i]; } // 返回對(duì)第i個(gè)元素的引用
private:
double* elem; // elem指向一個(gè)數(shù)組,該數(shù)組承載sz個(gè)double
// ...
};
Vector的第i個(gè)元素的存在不依賴于取下標(biāo)運(yùn)算符,因此可以返回對(duì)它的引用。
另一方面,在函數(shù)的返回操作結(jié)束后,局部變量就消失了,所以不能返回指向它的指針或引用:
int& bad() {
int x;
// ...
return x; // 糟糕:返回了指向局部變量x的引用
}
返回引用或者較“小”類型的值很高效,但是要把大量信息傳出函數(shù)該怎么辦呢? 考慮這個(gè)示例:
Matrix operator+(const Matrix& x, const Matrix& y) {
Matrix res;
// ... 對(duì)所有 res[i,j], res[i,j] = x[i,j]+y[i,j] ...
return res;
}
Matrix m1, m2;
// ...
Matrix m3 = m1+m2; // 沒有復(fù)制
即便對(duì)時(shí)下的硬件而言,Matrix可能都非常大,而且復(fù)制的代價(jià)高昂。 因此我們不進(jìn)行復(fù)制,而是為Matrix定義一個(gè)轉(zhuǎn)移構(gòu)造函數(shù)(move constructor),從而以低廉的代價(jià)把Matrix從operator+()傳出。 此處不需要抱殘守缺地使用手動(dòng)內(nèi)存管理:
Matrix* add(const Matrix& x, const Matrix& y) { // 復(fù)雜且易錯(cuò)的20世紀(jì)風(fēng)格
Matrix* p = new Matrix;
// ... 對(duì)所有的 *p[i,j], *p[i,j] = x[i,j]+y[i,j] ...
return p;
}
Matrix m1, m2;
// ...
Matrix* m3 = add(m1,m2); // 僅復(fù)制一個(gè)指針
// ...
delete m3; // 這個(gè)操作太容易忘記
很遺憾,通過指針返回大型對(duì)象在老式代碼里很常見,而且是個(gè)難以捕獲錯(cuò)誤的主要根源。 別寫這樣的代碼。 注意,operator+()跟add()同樣高效,但是定義簡(jiǎn)單、易于使用還不易出錯(cuò)。
函數(shù)的返回類型可以從返回值本身推斷出來(lái)。例如:
auto mul(int i, double d) { return i*d; } // 此處的“auto”意思是“推斷返回類型”
這很方便,尤其對(duì)于泛型函數(shù)(函數(shù)模板(function template))以及 lambda表達(dá)式來(lái)說(shuō),但是請(qǐng)謹(jǐn)慎采用,因?yàn)橥茖?dǎo)出來(lái)的類型會(huì)讓接口不穩(wěn)定:對(duì)函數(shù)(或lambda表達(dá)式)內(nèi)容的修改,會(huì)改變返回類型。
?
函數(shù)只能返回單獨(dú)的一個(gè)值,但這個(gè)值可以是包含多個(gè)成員的類對(duì)象。 這使得我們得以高效地返回多個(gè)值。例如:
struct Entry {
string name;
int value;
};
Entry read_entry(istream& is) { // 很菜的讀取函數(shù)
string s;
int i;
is >> s >> i;
return {s,i};
}
auto e = read_entry(cin);
cout << "{ " << e.name << " , " << e.value << " }\n";
此處的{s,i}被用于構(gòu)建Entry類型的返回值。 與之類似,可以把Entry的成員“拆包”到本地變量里:
auto [n,v] = read_entry(is);
cout << "{ " << n << " , " << v << " }\n";
auto [n,v]這句聲明了兩個(gè)局部變量n和v, 它們的類型從read_entry()的返回類型推導(dǎo)出來(lái)。 這個(gè)給類對(duì)象成員命名的機(jī)制叫結(jié)構(gòu)化綁定(structured binding)。
考慮以下示例:
map<string,int> m;
// ... 填充 m ...
for (const auto [key,value] : m)
cout << "{" << key "," << value << "}\n";
按慣例,可以用const和&限定auto,例如:
void incr(map<string,int>& m) { // 為m的每個(gè)元素自增1
for (auto& [key,value] : m)
++value;
}
在結(jié)構(gòu)化綁定用于不包含私有數(shù)據(jù)的類時(shí),綁定方式顯而易見: 綁定行為定義的名稱數(shù)量必須跟類里面的非靜態(tài)數(shù)據(jù)成員數(shù)量相同, 綁定行為中的引入名稱按次序?qū)?yīng)成員變量。 與顯式使用復(fù)合對(duì)象相比,這種代碼的質(zhì)量并無(wú)差異; 使用結(jié)構(gòu)化綁定的主旨在于恰如其分地表達(dá)意圖。
通過成員函數(shù)訪問類對(duì)象的操作同樣可行。例如:
complex<double> z = {1,2};
auto [re,im] = z+2;// re=3, im=2
忠告
[1] 注意區(qū)分聲明(用做接口)和定義(用做實(shí)現(xiàn));
[2] 使用頭文件代表接口,以強(qiáng)調(diào)邏輯結(jié)構(gòu);
[3] 在實(shí)現(xiàn)函數(shù)的源文件里#include它的頭文件;
[4] 不要在頭文件里定義非內(nèi)聯(lián)函數(shù);
[5] (在支持module的地方)以module替代頭文件;
[6] 使用命名空間表達(dá)邏輯結(jié)構(gòu);
[7] 為代碼遷移、基本類庫(kù)(比如std),或在局部作用域內(nèi)使用using-指令;
[8] 別把using-指令放在頭文件里;
[9] 在無(wú)法搞定待處理的任務(wù)時(shí),拋出異常指出這種情況;
[10] 僅在錯(cuò)誤處理的情形下使用異常;
[11] 在直接調(diào)用者該處理某個(gè)錯(cuò)誤的時(shí)候,采用錯(cuò)誤碼;
[12] 在錯(cuò)誤將要穿過大量函數(shù)調(diào)用層級(jí)的時(shí)候,拋出異常;
[13] 拿不定該用異常還是錯(cuò)誤碼的時(shí)候,用異常;
[14] 在設(shè)計(jì)早期就確定錯(cuò)誤處理的策略;
[15] 使用能夠反映設(shè)計(jì)意圖的用戶定義類型作為異常(而非內(nèi)置類型);
[16] 不要在每個(gè)函數(shù)中都捕捉所有異常;
[17] 用 RAII 代替try-代碼塊;
[18] 如果函數(shù)不該拋出異常,用noexcept聲明它;
[19] 讓構(gòu)造函數(shù)建立不變式,如果做不到就拋出異常;
[20] 圍繞不變式設(shè)計(jì)錯(cuò)誤處理策略;
[21] 能在編譯期檢查的就在編譯期檢查;
[22] “小”值傳值,“大”值傳引用;
[23] 盡可能用常(const)引用而非普通引用;
[24] 使用函數(shù)返回值進(jìn)行返回(而不要用傳出參數(shù));
[25] 別濫用返回類型推斷;
[26] 別濫用結(jié)構(gòu)化綁定;使用具名返回類型可以讓文檔更清晰。