?
“經(jīng)典的用戶定義算術(shù)類型”是complex:
class complex {
double re, im; // 表征數(shù)據(jù):兩個double
public:
complex(double r, double i) :re{r}, im{i} {} // 用兩個標量構(gòu)造complex
complex(double r) :re{r}, im{0} {} // 用一個標量構(gòu)造complex
complex() :re{0}, im{0} {} // complex的默認值:{0,0}
double real() const { return re; }
void real(double d) { re=d; }
double imag() const { return im; }
void imag(double d) { im=d; }
complex& operator+=(complex z) {
re+=z.re; // 加至re和im
im+=z.im;
return *this; // 返回結(jié)果
}
complex& operator-=(complex z) {
re-=z.re;
im-=z.im;
return *this;
}
complex& operator*=(complex); // 定義在類外某處
complex& operator/=(complex); // 定義在類外某處
};
很多有用的運算無需直接訪問complex的表征數(shù)據(jù),因此可以與類定義分開:
complex operator+(complex a, complex b) { return a+=b; }
complex operator-(complex a, complex b) { return a-=b; }
complex operator-(complex a) { return {-a.real(), -a.imag()}; } // 一元負號
complex operator*(complex a, complex b) { return a*=b; }
complex operator/(complex a, complex b) { return a/=b; }
bool operator==(complex a, complex b) { // 相等
return a.real()==b.real() && a.imag()==b.imag();
}
bool operator!=(complex a, complex b) { // 不等
return !(a==b);
}
complex sqrt(complex); // 定義在別處
// ...
complex類可以這樣用:
void f(complex z) {
complex a {2.3}; // 從 2.3 構(gòu)造出 {2.3,0.0}
complex b {1/a};
complex c {a+z*complex{1,2.3}};
// ...
if (c != b)
c = -(b/a)+2*b;
}
編譯器會把涉及complex數(shù)值的運算符轉(zhuǎn)換成相應(yīng)的函數(shù)調(diào)用。 例如:c!=b對應(yīng)operator!=(c,b),1/a對應(yīng)operator/(complex{1},a)。
用戶定義的運算符(“重載運算符(overloaded operator)”) 應(yīng)該謹慎并且遵循約定俗成的規(guī)則使用。
?
我們自定義的Vector有個致命的缺陷:它用new給元素分配空間,卻從未釋放它們。這就不太妙了,因為盡管C++定義了垃圾回收接口,但卻不能確保有個垃圾回收器把未使用的內(nèi)存供新對象使用。某些情況下你無法使用垃圾回收器,更常見的情形是:出于邏輯和性能原因,你傾向于更精細地控制銷毀行為。我們需要一個機制確保把構(gòu)造函數(shù)分配的內(nèi)存釋放掉;這個機制就是析構(gòu)函數(shù)(destructor):
class Vector {
public:
Vector(int s) :elem{new double[s]}, sz{s} { // 構(gòu)造函數(shù):申請資源
for (int i=0; i!=s; ++i) // 初始化元素
elem[i]=0;
}
~Vector() { delete[] elem; } // 析構(gòu)函數(shù):釋放資源
double& operator[](int i);
int size() const;
private:
double* elem; // elem指向一個數(shù)組,該數(shù)組承載sz個double
int sz;
};
析構(gòu)函數(shù)的名稱是取補運算符~后跟類名;它跟構(gòu)造函數(shù)互補。Vector的構(gòu)造函數(shù)用new運算符在自由存儲區(qū) (也叫堆(heap)或動態(tài)存儲區(qū)(dynamic store))里分配了一些內(nèi)存。析構(gòu)函數(shù)去清理——使用delete[]運算符釋放那塊內(nèi)存。普通delete刪除單個對象,delete[]刪除數(shù)組。
這些操作都不會干涉到Vector用戶。 用戶僅僅創(chuàng)建并使用Vector,就像對內(nèi)置類型一樣。例如:
void fct(int n) {
Vector v(n);
// ... 使用 v ...
{
Vector v2(2*n);
// ... 使用 v 和 v2 ...
}
// ... 使用 v ...
}// v 在此被銷毀
像int和char這些內(nèi)置類型一樣,Vector遵循相同的命名、作用域、內(nèi)存分配、生命期等一系列規(guī)則。此處的Vector版本為簡化而略掉了錯誤處理。
構(gòu)造函數(shù)/析構(gòu)函數(shù) 這對組合是很多優(yōu)雅技術(shù)的根基。確切的說,它是C++大多數(shù)資源管理技術(shù)的根基??紤]如下的Vector圖示:

構(gòu)造函數(shù)分配這些元素并初始化Vector響應(yīng)的成員變量。析構(gòu)函數(shù)釋放這些元素。這個數(shù)據(jù)操控器模型(handle-to-data model)常見于數(shù)據(jù)管理,管理那些容量在對象生命期內(nèi)可能變化的數(shù)據(jù)。這個構(gòu)造函數(shù)申請資源、析構(gòu)函數(shù)釋放資源的技術(shù)叫做 資源請求即初始化(Resource Acquisition Is Initialization)或者RAII,為我們消滅“裸的new操作”,就是說,避免在常規(guī)代碼中進行內(nèi)存分配,將其隱匿于抽象良好的實現(xiàn)中。與之類似,“裸的delete操作”也該竭力避免。避免裸new和裸delete,能大大降低代碼出錯的幾率,也更容易避免資源泄漏。
?
容器的作用是承載元素,因此很明顯需要便利的方法把元素放入容器。 可以創(chuàng)建元素數(shù)量適宜的Vector,然后給這些元素賦值,但還有些更優(yōu)雅的方式。 此處介紹其中頗受青睞的兩種:
- 初始化列表構(gòu)造函數(shù)(initializer-list constructor):用一個元素列表進行初始化。
- push_back():在序列的末尾(之后)添加一個新元素。
它們可以這樣聲明:
class Vector {
public:
Vector(std::initializer_list<double>);// 用一個double列表初始化
// ...
void push_back(double);// 在末尾新增元素,把容量加一
// ...
};
在輸入任意數(shù)量元素的時候,push_back()很有用,例如:
Vector read(istream& is) {
Vector v;
for (double d; is>>d; ) // read floating-point values into d
v.push_back(d); // add d to v return v;
}
Vector v = read(cin);// 此處未對Vector的元素進行復(fù)制
用于定義初始化列表構(gòu)造函數(shù)的std::initializer_list是個標準庫中的類型, 編譯器對它有所了解:當我們用{}列表,比如{1,2,3,4}的時候, 編譯器會為程序創(chuàng)建一個initializer_list對象。 因此,可以這樣寫:
Vector v1 = {1,2,3,4,5}; // v1有5個元素
Vector v2 = {1.23, 3.45, 6.7, 8}; // v2有4個元素
Vector的初始化列表構(gòu)造函數(shù)可能長這樣:
Vector::Vector(std::initializer_list<double> lst) // 用列表初始化
:elem{new double[lst.size()]}, sz{static_cast<int>(lst.size())}
{
copy(lst.begin(),lst.end(),elem); // 從lst復(fù)制到elem(§12.6)
}
很遺憾,標準庫為容量和下標選擇了unsigned整數(shù), 所以我需要用丑陋的static_cast把初始化列表的容量顯式轉(zhuǎn)換成int。
static_cast不對它轉(zhuǎn)換的值進行檢查;它相信程序員能運用得當。 可它也總有走眼的時候,所以如果吃不準,檢查一下值。 應(yīng)該盡可能避免顯式類型轉(zhuǎn)換 (通常也叫強制類型轉(zhuǎn)換(cast),用來提醒你它可能會把東西弄壞)。 盡量把不帶檢查的類型轉(zhuǎn)換限制在系統(tǒng)底層。它們極易出錯。
還有兩種類型轉(zhuǎn)換分別是:reinterpret_cast,它簡單地把對象按一連串字節(jié)對待;const_cast用于“轉(zhuǎn)掉const限制”。對類型系統(tǒng)的審慎運用以及設(shè)計良好的庫,都有助于在頂層軟件中消除不帶檢查的類型轉(zhuǎn)換。
?
complex和Vector這些被稱為實體類型,因為表征數(shù)據(jù)是它們定義的一部分。 因此,它們與內(nèi)置類型相仿。相反,抽象類型是把用戶和實現(xiàn)細節(jié)隔絕開的類型。為此,要把接口和表征數(shù)據(jù)解耦,并且要摒棄純局部變量。既然對抽象類的表征數(shù)據(jù)(甚至其容量)一無所知,就只能把它的對象分配在自由存儲區(qū),并通過引用或指針訪問它們。
首先,我們定義Container類的接口,它將被設(shè)計成Vector更抽象的版本:
class Container {
public:
virtual double& operator[](int) = 0; // 純虛函數(shù)
virtual int size() const = 0; // const 成員函數(shù)
virtual ~Container() {} // 析構(gòu)函數(shù)
};
該類是一個用于描述后續(xù)容器的純接口。virtual這個詞的意思是“后續(xù)可能在從此類派生的類中被重新定義”。用virtual聲明的函數(shù)自然而然的被稱為虛函數(shù)。從Container派生的類要為Container接口提供實現(xiàn)。古怪的=0語法意思是:此函數(shù)是純虛的;就是說,某些繼承自Container的類必須定義該函數(shù)。因此,根本無法直接為Container類型定義對象。例如:
Container c; // 報錯:抽象類沒有自己的對象
Container* p = new Vector_container(10); // OK:Container作為接口使用
Container只能用做作接口,服務(wù)于那些給operator和size()函數(shù)提供了實現(xiàn)的類。帶有虛函數(shù)的類被稱為抽象類。
Container可以這樣用:
void use(Container& c) {
const int sz = c.size();
for (int i=0; i!=sz; ++i)
cout << c[i] << '\n';
}
請注意use()在使用Container接口時對其實現(xiàn)細節(jié)一無所知。它用到size()和[ ],卻完全不知道為它們提供實現(xiàn)的類型是什么。為諸多其它類定義接口的類通常被稱為多態(tài)類型。
正如常見的抽象類,Container也沒有構(gòu)造函數(shù)。畢竟它不需要初始化數(shù)據(jù)。另一方面,Container有一個析構(gòu)函數(shù),并且還是virtual的,以便讓Container的派生類去實現(xiàn)它。這對于抽象類也是常見的,因為它們往往通過引用或指針進行操作,而借助指針銷毀Container對象的人根本不了解具體用到了哪些資源。
抽象類Container僅僅定義接口,沒有實現(xiàn)。想讓它發(fā)揮作用,就需要弄一個容器去實現(xiàn)它接口規(guī)定的那些函數(shù)。為此,可以使用一個實體類Vector:
class Vector_container : public Container { // Vector_container 實現(xiàn)了 Container
public:
Vector_container(int s) : v(s) { } // s個元素的Vector
~Vector_container() {}
double& operator[](int i) override { return v[i]; }
int size() const override { return v.size(); }
private:
Vector v;
};
:public可以讀作“派生自”或者“是……的子類型”。 我們說Vector_container派生自Container, 并且Container是Vector_container的基類。 還有術(shù)語把Vector_container和Container分別稱為 子類和親類。 我們說派生類繼承了其基類的成員,所以這種基類和派生類的關(guān)系通常被稱為繼承。
我們這里的operator和size()覆蓋(override)了基類Container中對應(yīng)的成員。這里明確使用override表達了這個意向。這里的override可以省略,但是明確使用它,可以讓編譯器查錯,比如函數(shù)名拼寫錯誤,或者virtual函數(shù)和被其覆蓋的函數(shù)之間的細微類型差異等等。在較大的類體系中明確使用override格外有用,否則就難以搞清楚覆蓋關(guān)系。
這里的析構(gòu)函數(shù)(~Vector_container()) 覆蓋了基類的析構(gòu)函數(shù)(~ Container())。請注意,其成員的析構(gòu)函數(shù)(~Vector) 被該類的析構(gòu)函數(shù)(~Vector_container())隱式調(diào)用了。
對于use(Container&)這類函數(shù),使用Container時不必了解其實現(xiàn)細節(jié),其它函數(shù)要創(chuàng)建具體對象供它操作的。例如:
void g() {
Vector_container vc(10);// 十個元素的Vector
// ... 填充 vc ...
use(vc);
}
由于use()只了解Container接口而非Vector_container,它就可以對Container的其它實現(xiàn)同樣有效。例如:
class List_container : public Container { // List_container implements Container
public:
List_container() { } // empty List
List_container(initializer_list<double> il) : ld{il} { }
~List_container() {}
double& operator[](int i) override;
int size() const override { return ld.size(); }
private:
std::list<double> ld; // double類型的(標準庫)列表 (§11.3)
};
double& List_container::operator[](int i) {
for (auto& x : ld) {
if (i==0)
return x;
--i;
}
throw out_of_range{"List container"};
}
?
void use(Container& c) {
const int sz = c.size();
for (int i=0; i!=sz; ++i)
cout << c[i] << '\n';
}
void g() {
Vector_container vc(10);
use(vc);
}
void h() {
List_container lc = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
use(lc);
}
use()中的c[i]調(diào)用是怎么解析到對應(yīng)的operator呢?當h()調(diào)用use()時,List_container的operator必須被調(diào)用。g()調(diào)用use()時,Vector_container的operator必須被調(diào)用。
想要實現(xiàn)這種解析,Container對象必須包含某種信息,以便在運行時找到正確的待調(diào)用函數(shù)。常見的實現(xiàn)技術(shù)是:編譯器把虛函數(shù)的名稱轉(zhuǎn)換成一個指向函數(shù)指針表的索引。這個表格通常被稱為虛函數(shù)表(virtual function table),或者簡稱vtbl。每個帶有虛函數(shù)的類都有自己的vtbl以確認其虛函數(shù)。這可以圖示如下:

vtbl中的函數(shù)能夠正確地使用其對象,即便調(diào)用者對該對象的容量以及數(shù)據(jù)布局全都一無所知。調(diào)用者的實現(xiàn)僅需要知道某個Container中指向vtbl指針的位置以及每個待用虛函數(shù)的索引。虛函數(shù)調(diào)用機制幾乎能做到與“常規(guī)函數(shù)調(diào)用”機制同樣高效(性能差別不到25%)。 其空間消耗是帶有虛函數(shù)的類的每個對象一個指針,再加上每個類一個vtbl。
基類的析構(gòu)函數(shù)往往需要定義為虛函數(shù),如果一個基類指針指向一個 派生類對象,當調(diào)用delete操作符時,只會調(diào)用基類的析構(gòu)函數(shù)而不會調(diào)用派生類的析構(gòu)函數(shù)。如:
class ClxBase {// 基類
public:
ClxBase() {}
virtual ~ClxBase() {
cout << "Output from the destructor of class ClxBase!\n";
}
virtual void DoSomething() {
cout << "Do something in class ClxBase!\n";
}
}
class ClxDerived : public ClxBase {// 派生類
public:
ClxDerived() {}
~ClxDerived() {
cout << "Output from the destructor of class ClxDerived!\n";
}
void DoSomething() {
cout << "Do something in class ClxDerived!\n";
}
}
int main() {
ClxBase *p = new ClxDerived;// 有多態(tài)
// 當基類是虛函數(shù)時,基類的指針將表現(xiàn)為派生類的行為(非虛函數(shù)將表現(xiàn)為基類行為)
p->DoSomething();
delete p;
return 0;
}
// 運行結(jié)果
Do something in class ClxDerived!
Output from the destructor of class ClxDerived!
Output from the destructor of class ClxBase!
?

class Shape {
public:
virtual Point center() const =0; // 純虛函數(shù)
virtual void move(Point to) =0;
virtual void draw() const = 0; // 在“畫布”上繪制
virtual void rotate(int angle) = 0;
virtual ~Shape() {} // 析構(gòu)函數(shù)
// ...
};
根據(jù)這個定義,可以寫一個通用的函數(shù)操縱一個vector,其中的元素是指向圖形的指針:
void rotate_all(vector<Shape*>& v, int angle) {// 把v的元素旋轉(zhuǎn)給定角度
for (auto p : v)
p->rotate(angle);
}
要定義特定的圖形,必須指明它是個Shape,定義它特有的屬性(包括其虛函數(shù)):
class Circle : public Shape {
public:
Circle(Point p, int rad); // 構(gòu)造函數(shù)
Point center() const override {
return x;
}
void move(Point to) override {
x = to;
}
void draw() const override;
void rotate(int) override {} // 優(yōu)美且簡潔的算法
private:
Point x; // 圓心
int r; // 半徑
};
截至目前,Shape和Circle的例子跟Container相比還沒有什么亮點, 請接著往下看:
class Smiley : public Circle { // 用圓圈作為笑臉的基類
public:
Smiley(Point p, int rad) : Circle{p,rad}, mouth{nullptr} { }
~Smiley() {
delete mouth;
for (auto p : eyes)
delete p;
}
void move(Point to) override;
void draw() const override;
void rotate(int) override;
void add_eye(Shape* s) {
eyes.push_back(s);
}
void set_mouth(Shape* s);
virtual void wink(int i); // 讓第i只眼做“飛眼”
// ...
private:
vector<Shape*> eyes; // 一般是兩只眼睛
Shape* mouth;
};
現(xiàn)在,可以利用Smiley的基類和成員函數(shù)draw()的調(diào)用來定義Smiley::draw()了:
void Smiley::draw() const {
Circle::draw();
for (auto p : eyes)
p->draw();
mouth->draw();
}
請注意,Smiley把它的眼睛保存在一個標準庫的vector里, 并且會在析構(gòu)函數(shù)中把它們銷毀。 Shape的析構(gòu)函數(shù)是virtual的,而Smiley又覆蓋了它。 虛析構(gòu)函數(shù)對于抽象類來說是必須的,因為操控派生類的對象通常是借助抽象基類提供的接口進行的。具體地說,它可能是通過其基類的指針被銷毀的。然后,虛函數(shù)調(diào)用機制確保正確析構(gòu)函數(shù)被調(diào)用。該析構(gòu)函數(shù)則會隱式調(diào)用其基類和成員變量的析構(gòu)函數(shù)。
在這個簡化過的例子中,把眼睛和嘴巴準確放置到代表臉的圓圈中,是程序員的的任務(wù)。
在以派生方式定義一個新類時,我們可以添加新的 成員變量 或/和 運算。 這帶來了極佳的靈活性,同時又給邏輯混亂和不良設(shè)計提供了溫床。
?
類的層次結(jié)構(gòu)有兩個益處:
- 接口繼承(interface inheritance):派生類對象可以用在任何基類對象勝任的位置。就是說,基類充當了派生類的接口。Container和Shape這兩個類就是例子。這種類通常是抽象類。
- 實現(xiàn)繼承(implementation inheritance):基類的函數(shù)和數(shù)據(jù)直接就是派生類實現(xiàn)的一部分。 Smiley對Circle的構(gòu)造函數(shù)、Circle::draw()的調(diào)用就是這方面的例子。這種基類通常具有成員變量和構(gòu)造函數(shù)。
enum class Kind { circle, triangle, smiley };
Shape* read_shape(istream& is) { // 從輸入流is讀取圖形描述
// ... 從 is 讀取圖形概要信息,找到其類型(Kind) k ...
switch (k) {
case Kind::circle:
// 把圓圈的數(shù)據(jù) {Point,int} 讀取到p和r
return new Circle{p,r};
case Kind::triangle:
// 把三角形的數(shù)據(jù) {Point,Point,Point} 讀取到p1、p2、和p3
return new Triangle{p1,p2,p3};
case Kind::smiley:
// 把笑臉的數(shù)據(jù) {Point,int,Shape,Shape,Shape} 讀取到p、r、e1、e2 和 m
Smiley* ps = new Smiley{p,r};
ps->add_eye(e1);
ps->add_eye(e2);
ps->set_mouth(m);
return ps;
}
}
某個程序可以這樣使用此圖形讀取器:
void user() {
std::vector<Shape*> v;
while (cin)
v.push_back(read_shape(cin));
draw_all(v); // 為每個元素調(diào)用 draw()
rotate_all(v,45); // 為每個元素調(diào)用 rotate(45)
for (auto p : v) // 別忘了銷毀元素(指向的對象)
delete p;
}
顯而易見,這個例子被簡化過了——尤其是錯誤處理相關(guān)的內(nèi)容—— 但它清晰地表明了,user()函數(shù)對其所操縱圖形的類型一無所知。 user()的代碼僅需要編譯一次,在程序加入新的Shape之后可以繼續(xù)使用。
請留意,沒有任何圖形的指針流向了user()之外,因此user()就要負責回收它們。 這實用運算符delete完成,且嚴重依賴Shape的虛析構(gòu)函數(shù)。 因為這個析構(gòu)函數(shù)是虛的,delete調(diào)用的是距基類最遠的派生類里的那個。 這至關(guān)重要,因為可能獲取了各式各樣有待釋放的資源(比如文件執(zhí)柄、鎖及輸出流)。在本例中,Smiley要刪除其eyes和mouth的對象。刪完這些之后,它又去調(diào)用Circle的析構(gòu)函數(shù)。對象的構(gòu)建通過構(gòu)造函數(shù)“自下而上”(從基類開始), 而銷毀通過虛構(gòu)函數(shù)“從頂?shù)降住保◤呐缮愰_始)。
read_shape()函數(shù)返回Shape*,以便我們對所有Shape一視同仁。 但是,如果我們想調(diào)用某個派生類特有的函數(shù), 比方說Smiley里的wink(),該怎么辦呢? 我們可以用dynamic_cast運算符問這個問題 “這個Shape對象是Smiley類型的嗎?”:
Shape* ps {read_shape(cin)};
if (Smiley* p = dynamic_cast<Smiley*>(ps)) { // ... ps指向一個 Smiley 嗎? ...
// ... 是 Smiley;用它
} else {
// ... 不是 Smiley,其它處理 ...
}
在運行時,如果dynamic_cast的參數(shù)(此處是ps)指向的對象不是期望的類型 (此處是Smiley)或其派生類,dynamic_cast就返回nullptr。如果其它類型不可接受,我們就直接把dynamic_cast用于引用類型。 如果該對象不是期望的類型,dynamic_cast拋出一個bad_cast異常:
Shape* ps {read_shape(cin)};
Smiley& r {dynamic_cast<Smiley&>(*ps)}; // 某處可以捕捉到 std::bad_cast
當“轉(zhuǎn)換目標不屬于所需的類”需要報錯時,就把dynamic_cast用于引用類型;如果“轉(zhuǎn)換目標不屬于所需的類”可接受,就把dynamic_cast用于指針類型。
?
經(jīng)驗豐富的程序員可能注意到了我有三個紕漏:
- 寫Smiley的程序員可能忘記delete指向mouth的指針;
- read_shape()的用戶可能忘記delete返回的指針;
- Shape指針容器的所有者可能忘記delete它們指向的對象。
從這個意義上講,指向自由存儲區(qū)中對象的指針是危險的: “直白老舊的指針(plain old pointer)”不該用于表示所有權(quán)。例如:
void user(int x) {
Shape* p = new Circle{Point{0,0},10};
// ...
if (x<0) throw Bad_x{}; // 資源泄漏潛在危險
if (x==0) return; // 資源泄漏潛在危險
// ...
delete p;
}
除非x為正數(shù),否則就會導(dǎo)致資源泄漏。 把new的結(jié)果賦值給“裸指針”就是自找麻煩。
這類問題有一個簡單的解決方案:在需要釋放操作時, 使用標準庫的unique_ptr而非“裸指針”:
class Smiley : public Circle {
// ...
private:
vector<unique_ptr<Shape>> eyes; // 一般是兩只眼睛
unique_ptr<Shape> mouth;
};
這是一個示例,展示簡潔、通用、高效的資源管理技術(shù)。
這個修改有個良性的副作用:我們不需要再為Smiley定義析構(gòu)函數(shù)了。 編譯器會隱式生成一個,以便將vector中的unique_ptr銷毀。 使用unique_ptr的代碼跟用裸指針的代碼在效率方面完全一致。
重新審視read_shape()的使用:
unique_ptr<Shape> read_shape(istream& is) {// 從輸入流is讀取圖形描述
// ... 從 is 讀取圖形概要信息,找到其類型(Kind) k ...
switch (k) {
case Kind::circle:
// 把圓圈的數(shù)據(jù) {Point,int} 讀取到p和r
return unique_ptr<Shape>{new Circle{p,r}};
// ...
}
void user() {
vector<unique_ptr<Shape>> v;
while (cin)
v.push_back(read_shape(cin));
draw_all(v); // 為每個元素調(diào)用 draw()
rotate_all(v,45); // 為每個元素調(diào)用 rotate(45)
} // 所有 Shape 都隱式銷毀了
現(xiàn)在每個對象都被一個unique_ptr持有,當不再需要這個unique_ptr,也就是它離開作用域的時候,就會銷毀持有的對象。
想讓unique_ptr版本的user()正常運作, 就需要能夠接受vector<unique_ptr<Shape>>版本的 draw_all()和rotate_all()。
忠告
- [1] 用代碼直接表達意圖;
- [2] 實體類型是最簡單的類。情況許可的時候, 請優(yōu)先用實體類,而非更復(fù)雜的類或者普通的數(shù)據(jù)結(jié)構(gòu);
- [3] 用實體類去表示簡單的概念;
- [4] 對于性能要求嚴苛的組件,優(yōu)先用實體類,而不是選擇類層次;
- [5] 定義構(gòu)造函數(shù)去處理對象的初始化;
- [6] 只在一個函數(shù)需要直接訪問類的表征數(shù)據(jù)時,把它定義為成員函數(shù);
- [7] 自定義運算符的主要用途應(yīng)該是模擬傳統(tǒng)運算;
- [8] 為對稱運算符使用非成員函數(shù);
- [9] 把不修改對象狀態(tài)的成員函數(shù)定義為
const; - [10] 如果構(gòu)造函數(shù)申請了資源,這個類就需要虛構(gòu)函數(shù)去釋放這個資源;
- [11] 避免使用“裸的”
new和delete操作; - [12] 利用資源操控器和 RAII 去管理資源;
- [13] 如果類是容器,請給它定義一個初始化列表構(gòu)造函數(shù);
- [14] 需要接口和實現(xiàn)完全分離的時候,請用抽象類作為接口;
- [15] 請通過指針和引用訪問多態(tài)對象;
- [16] 抽象類通常不需要構(gòu)造函數(shù);
- [17] 對于與生俱來就具有層次結(jié)構(gòu)的概念,請使用類層次結(jié)構(gòu)表示它們;
- [18] 帶有虛函數(shù)的類,應(yīng)該定義虛析構(gòu)函數(shù);
- [19] 在較大的類層次中,顯式用
override進行覆蓋; - [20] 設(shè)計類層次的時候,要分清實現(xiàn)繼承和接口繼承;
- [21] 在不可避免要在類層次中進行辨別的時候,使用
dynamic_cast; - [22] 當“轉(zhuǎn)換目標不屬于所需的類”需要報錯時,就把
dynamic_cast用于引用類型; - [23] 如果“轉(zhuǎn)換目標不屬于所需的類”可接受,就把
dynamic_cast用于指針類型; - [24] 對于通過
new創(chuàng)建的對象,用unique_ptr和shared_ptr避免忘記delete。