目錄:
我如何寫這個非常簡單的程序?
爲什麼編譯要花這麼長的時間?
爲什麼一個空類的大小不爲0?
我必須在類聲明處賦予數據嗎?
爲什麼成員函數默認不是virtual的?
爲什麼析構函數默認不是virtual的?
爲什麼不能有虛擬構造函數?
爲什麼重載在繼承類中不工作?
我能夠在構造函數中調用一個虛擬函數嗎?
有沒有“指定位置刪除”(placement delete)?
我能防止別人繼承我自己的類嗎?
爲什麼不能爲模板參數定義約束(constraints)?
既然已經有了優秀的qsort()函數,爲什麼還需要一個sort()?
什麼是函數對象(function object)?
我應該如何對付內存泄漏?
我爲什麼在捕獲一個異常之後就不能繼續?
爲什麼C++中沒有相當於realloc()的函數?
如何使用異常?
怎樣從輸入中讀取一個字符串?
爲什麼C++不提供“finally”的構造?
什麼是自動指針(auto_ptr),爲什麼沒有自動數組(auto_array)?
可以混合使用C風格與C++風格的內存分派與重新分配嗎?
我爲什麼必須使用一個造型來轉換*void?
我如何定義一個類內部(in-class)的常量?
爲什麼delete不會將操作數置0?
我能夠寫“void main()”嗎?
爲什麼我不能重載點符號,::,sizeof,等等?
怎樣將一個整型值轉換爲一個字符串?
“int* p”正確還是“int *p”正確?
對於我的代碼,哪一種佈局風格(layout style)是最好的?
我應該將“const”放在類型之前還是之後?
使用宏有什麼問題?
我如何寫這個非常簡單的程序?
特別是在一個學期的開始,我常常收到許多關於編寫一個非常簡單的程序的詢問。這個問題有一個很具代表性的解決方法,那就是(在你的程序中)讀入幾個數字,對它們做一些處理,再把結果輸出。下面是一個這樣做的例子:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
vector<double> v;
double d;
while(cin>>d) v.push_back(d); // 讀入元素
if (!cin.eof()) { // 檢查輸入是否出錯
cerr << "format error/n";
return 1; // 返回一個錯誤
}
cout << "read " << v.size() << " elements/n";
reverse(v.begin(),v.end());
cout << "elements in reverse order:/n";
for (int i = 0; i<v.size(); ++i) cout << v[i] << '/n';
return 0; // 成功返回
}
對這段程序的觀察:
這是一段標準的ISO C++程序,使用了標準庫(standard library)。標準庫工具在命名空間std中聲明,封裝在沒有.h後綴的頭文件中。
如果你要在Windows下編譯它,你需要將它編譯成一個“控制檯程序”(console application)。記得將源文件加上.cpp後綴,否則編譯器可能會以爲它是一段C代碼而不是C++。
是的,main()函數返回一個int值。
讀到一個標準的向量(vector)中,可以避免在隨意確定大小的緩衝中溢出的錯誤。讀到一個數組(array)中,而不產生“簡單錯誤”(silly error),這已經超出了一個新手的能力——如果你做到了,那你已經不是一個新手了。如果你對此表示懷疑,我建議你閱讀我的文章“將標準C++作爲一種新的語言來學習”("Learning Standard C++ as a New Language"),你可以在本人著作列表(my publications list)中下載到它。
!cin.eof()是對流的格式的檢查。事實上,它檢查循環是否終結於發現一個end-of-file(如果不是這樣,那麼意味着輸入沒有按照給定的格式)。更多的說明,請參見你的C++教科書中的“流狀態”(stream state)部分。
vector知道它自己的大小,因此我不需要計算元素的數量。
這段程序沒有包含顯式的內存管理。Vector維護一個內存中的棧,以存放它的元素。當一個vector需要更多的內存時,它會分配一些;當它不再生存時,它會釋放內存。於是,使用者不需要再關心vector中元素的內存分配和釋放問題。
程序在遇到輸入一個“end-of-file”時結束。如果你在UNIX平臺下運行它,“end-of-file”等於鍵盤上的Ctrl+D。如果你在Windows平臺下,那麼由於一個BUG它無法辨別“end-of-file”字符,你可能傾向於使用下面這個稍稍複雜些的版本,它使用一個詞“end”來表示輸入已經結束。
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
int main()
{
vector<double> v;
double d;
while(cin>>d) v.push_back(d); // 讀入一個元素
if (!cin.eof()) { // 檢查輸入是否失敗
cin.clear(); // 清除錯誤狀態
string s;
cin >> s; // 查找結束字符
if (s != "end") {
cerr << "format error/n";
return 1; // 返回錯誤
}
}
cout << "read " << v.size() << " elements/n";
reverse(v.begin(),v.end());
cout << "elements in reverse order:/n";
for (int i = 0; i<v.size(); ++i) cout << v[i] << '/n';
return 0; // 成功返回
}
更多的關於使用標準庫將事情簡化的例子,請參見《C++程序設計語言》中的“漫遊標準庫”("Tour of the Standard Library")一章。
爲什麼編譯要花這麼長的時間?
你的編譯器可能有問題。也許它太老了,也許你安裝它的時候出了錯,也許你用的計算機已經是個古董。在諸如此類的問題上,我無法幫助你。
但是,這也是很可能的:你要編譯的程序設計得非常糟糕,以至於編譯器不得不檢查數以百計的頭文件和數萬行代碼。理論上來說,這是可以避免的。如果這是你購買的庫的設計問題,你對它無計可施(除了換一個更好的庫),但你可以將你自己的代碼組織得更好一些,以求得將修改代碼後的重新編譯工作降到最少。這樣的設計會更好,更有可維護性,因爲它們展示了更好的概念上的分離。
看看這個典型的面向對象的程序例子:
class Shape {
public: // 使用Shapes的用戶的接口
virtual void draw() const;
virtual void rotate(int degrees);
// ...
protected: // common data (for implementers of Shapes)
Point center;
Color col;
// ...
};
class Circle : public Shape {
public:
void draw() const;
void rotate(int) { }
// ...
protected:
int radius;
// ...
};
class Triangle : public Shape {
public:
void draw() const;
void rotate(int);
// ...
protected:
Point a, b, c;
// ...
};
設計思想是,用戶通過Shape的public接口來操縱它們,而派生類(例如Circle和Triangle)的實現部分則共享由protected成員表現的那部分實現(implementation)。
這不是一件容易的事情:確定哪些實現部分是對所有的派生類都有用的,並將之共享出來。因此,與public接口相比,protected成員往往要做多得多的改動。舉例來說,雖然理論上“中心”(center)對所有的圖形都是一個有效的概念,但當你要維護一個三角形的“中心”的時候,是一件非常麻煩的事情——對於三角形,當且僅當它確實被需要的時候,計算這個中心纔是有意義的。
protected成員很可能要依賴於實現部分的細節,而Shape的用戶(譯註:user此處譯爲用戶,指使用Shape類的代碼,下同)卻不見得必須依賴它們。舉例來說,很多(大多數?)使用Shape的代碼在邏輯上是與“顏色”無關的,但是由於Shape中“顏色”這個定義的存在,卻可能需要一堆複雜的頭文件,來結合操作系統的顏色概念。
當protected部分發生了改變時,使用Shape的代碼必須重新編譯——即使只有派生類的實現部分才能夠訪問protected成員。
於是,基類中的“實現相關的信息”(information helpful to implementers)對用戶來說變成了象接口一樣敏感的東西,它的存在導致了實現部分的不穩定,用戶代碼的無謂的重編譯(當實現部分發生改變時),以及將頭文件無節制地包含進用戶代碼中(因爲“實現相關的信息”需要它們)。有時這被稱爲“脆弱的基類問題”(brittle base class problem)。
一個很明顯的解決方案就是,忽略基類中那些象接口一樣被使用的“實現相關的信息”。換句話說,使用接口,純粹的接口。也就是說,用抽象基類的方式來表示接口:
class Shape {
public: //使用Shapes的用戶的接口
virtual void draw() const = 0;
virtual void rotate(int degrees) = 0;
virtual Point center() const = 0;
// ...
// 沒有數據
};
class Circle : public Shape {
public:
void draw() const;
void rotate(int) { }
Point center() const { return center; }
// ...
protected:
Point cent;
Color col;
int radius;
// ...
};
class Triangle : public Shape {
public:
void draw() const;
void rotate(int);
Point center() const;
// ...
protected:
Color col;
Point a, b, c;
// ...
};
現在,用戶代碼與派生類的實現部分的變化之間的關係被隔離了。我曾經見過這種技術使得編譯的時間減少了幾個數量級。
但是,如果確實存在着對所有派生類(或僅僅對某些派生類)都有用的公共信息時怎麼辦呢?可以簡單把這些信息封裝成類,然後從它派生出實現部分的類:
class Shape {
public: //使用Shapes的用戶的接口
virtual void draw() const = 0;
virtual void rotate(int degrees) = 0;
virtual Point center() const = 0;
// ...
// no data
};
struct Common {
Color col;
// ...
};
class Circle : public Shape, protected Common {
public:
void draw() const;
void rotate(int) { }
Point center() const { return center; }
// ...
protected:
Point cent;
int radius;
};
class Triangle : public Shape, protected Common {
public:
void draw() const;
void rotate(int);
Point center() const;
// ...
protected:
Point a, b, c;
};
爲什麼一個空類的大小不爲0?
要清楚,兩個不同的對象的地址也是不同的。基於同樣的理由,new總是返回指向不同對象的指針。
看看:
class Empty { };
void f()
{
Empty a, b;
if (&a == &b) cout << "impossible: report error to compiler supplier";
Empty* p1 = new Empty;
Empty* p2 = new Empty;
if (p1 == p2) cout << "impossible: report error to compiler supplier";
}
有一條有趣的規則:一個空的基類並不一定有分隔字節。
struct X : Empty {
int a;
// ...
};
void f(X* p)
{
void* p1 = p;
void* p2 = &p->a;
if (p1 == p2) cout << "nice: good optimizer";
}
這種優化是允許的,可以被廣泛使用。它允許程序員使用空類以表現一些簡單的概念。現在有些編譯器提供這種“空基類優化”(empty base class optimization)。
我必須在類聲明處賦予數據嗎?
不必須。如果一個接口不需要數據時,無須在作爲接口定義的類中賦予數據。代之以在派生類中給出它們。參見“爲什麼編譯要花這麼長的時間?”。
有時候,你必須在一個類中賦予數據。考慮一下複數類的情況:
template<class Scalar> class complex {
public:
complex() : re(0), im(0) { }
complex(Scalar r) : re(r), im(0) { }
complex(Scalar r, Scalar i) : re(r), im(i) { }
// ...
complex& operator+=(const complex& a)
{ re+=a.re; im+=a.im; return *this; }
// ...
private:
Scalar re, im;
};
設計這種類型的目的是將它當做一個內建(built-in)類型一樣被使用。在聲明處賦值是必須的,以保證如下可能:建立真正的本地對象(genuinely local objects)(比如那些在棧中而不是在堆中分配的對象),或者使某些簡單操作被適當地inline化。對於那些支持內建的複合類型的語言來說,要獲得它們提供的效率,真正的本地對象和inline化都是必要的。
爲什麼成員函數默認不是virtual的?
因爲很多類並不是被設計作爲基類的。例如複數類。
而且,一個包含虛擬函數的類的對象,要佔用更多的空間以實現虛擬函數調用機制——往往是每個對象佔用一個字(word)。這個額外的字是非常可觀的,而且在涉及和其它語言的數據的兼容性時,可能導致麻煩(例如C或Fortran語言)。
要了解更多的設計原理,請參見《C++語言的設計和演變》(The Design and Evolution of C++)。
爲什麼析構函數默認不是virtual的?
因爲很多類並不是被設計作爲基類的。只有類在行爲上是它的派生類的接口時(這些派生類往往在堆中分配,通過指針或引用來訪問),虛擬函數纔有意義。
那麼什麼時候才應該將析構函數定義爲虛擬呢?當類至少擁有一個虛擬函數時。擁有虛擬函數意味着一個類是派生類的接口,在這種情況下,一個派生類的對象可能通過一個基類指針來銷燬。例如:
class Base {
// ...
virtual ~Base();
};
class Derived : public Base {
// ...
~Derived();
};
void f()
{
Base* p = new Derived;
delete p; // 虛擬析構函數保證~Derived函數被調用
}
如果基類的析構函數不是虛擬的,那麼派生類的析構函數將不會被調用——這可能產生糟糕的結果,例如派生類的資源不會被釋放。
爲什麼不能有虛擬構造函數?
虛擬調用是一種能夠在給定信息不完全(given partial information)的情況下工作的機制。特別地,虛擬允許我們調用某個函數,對於這個函數,僅僅知道它的接口,而不知道具體的對象類型。但是要建立一個對象,你必須擁有完全的信息。特別地,你需要知道要建立的對象的具體類型。因此,對構造函數的調用不可能是虛擬的。
當要求建立一個對象時,一種間接的技術常常被當作“虛擬構造函數”來使用。有關例子,請參見《C++程序設計語言》第三版15.6.2.節。
下面這個例子展示一種機制:如何使用一個抽象類來建立一個適當類型的對象。
struct F { // 對象建立函數的接口
virtual A* make_an_A() const = 0;
virtual B* make_a_B() const = 0;
};
void user(const F& fac)
{
A* p = fac.make_an_A(); // 將A作爲合適的類型
B* q = fac.make_a_B(); // 將B作爲合適的類型
// ...
}
struct FX : F {
A* make_an_A() const { return new AX(); } // AX是A的派生
B* make_a_B() const { return new BX(); } // AX是B的派生
};
struct FY : F {
A* make_an_A() const { return new AY(); } // AY是A的派生
B* make_a_B() const { return new BY(); } // BY是B的派生
};
int main()
{
user(FX()); // 此用戶建立AX與BX
user(FY()); // 此用戶建立AY與BY
// ...
}
這是所謂的“工廠模式”(the factory pattern)的一個變形。關鍵在於,user函數與AX或AY這樣的類的信息被完全分離開來了。
爲什麼重載在繼承類中不工作?
這個問題(非常常見)往往出現於這樣的例子中:
#include<iostream>
using namespace std;
class B {
public:
int f(int i) { cout << "f(int): "; return i+1; }
// ...
};
class D : public B {
public:
double f(double d) { cout << "f(double): "; return d+1.3; }
// ...
};
int main()
{
D* pd = new D;
cout << pd->f(2) << '/n';
cout << pd->f(2.3) << '/n';
}
它輸出的結果是:
f(double): 3.3
f(double): 3.6
而不是象有些人猜想的那樣:
f(int): 3
f(double): 3.6
換句話說,在B和D之間並沒有發生重載的解析。編譯器在D的區域內尋找,找到了一個函數double f(double),並執行了它。它永遠不會涉及(被封裝的)B的區域。在C++中,沒有跨越區域的重載——對於這條規則,繼承類也不例外。更多的細節,參見《C++語言的設計和演變》和《C++程序設計語言》。
但是,如果我需要在基類和繼承類之間建立一組重載的f()函數呢?很簡單,使用using聲明:
class D : public B {
public:
using B::f; // make every f from B available
double f(double d) { cout << "f(double): "; return d+1.3; }
// ...
};
進行這個修改之後,輸出結果將是:
f(int): 3
f(double): 3.6
這樣,在B的f()和D的f()之間,重載確實實現了,並且選擇了一個最合適的f()進行調用。
我能夠在構造函數中調用一個虛擬函數嗎?
可以,但是要小心。它可能不象你期望的那樣工作。在構造函數中,虛擬調用機制不起作用,因爲繼承類的重載還沒有發生。對象先從基類被創建,“基類先於繼承類(base before derived)”。
看看這個:
#include<string>
#include<iostream>
using namespace std;
class B {
public:
B(const string& ss) { cout << "B constructor/n"; f(ss); }
virtual void f(const string&) { cout << "B::f/n";}
};
class D : public B {
public:
D(const string & ss) :B(ss) { cout << "D constructor/n";}
void f(const string& ss) { cout << "D::f/n"; s = ss; }
private:
string s;
};
int main()
{
D d("Hello");
}
程序編譯以後會輸出:
B constructor
B::f
D constructor
注意不是D::f。設想一下,如果出於不同的規則,B::B()可以調用D::f()的話,會產生什麼樣的後果:因爲構造函數D::D()還沒有運行,D::f()將會試圖將一個還沒有初始化的字符串s賦予它的參數。結果很可能是導致立即崩潰。
析構函數在“繼承類先於基類”的機制下運行,因此虛擬機制的行爲和構造函數一樣:只有本地定義(local definitions)被使用——不會調用虛擬函數,以免觸及對象中的(現在已經被銷燬的)繼承類的部分。
更多的細節,參見《C++語言的設計和演變》13.2.4.2和《C++程序設計語言》15.4.3。
有人暗示,這只是一條實現時的人爲製造的規則。不是這樣的。事實上,要實現這種不安全的方法倒是非常容易的:在構造函數中直接調用虛擬函數,就象調用其它函數一樣。但是,這樣就意味着,任何虛擬函數都無法編寫了,因爲它們需要依靠基類的固定的創建(invariants established by base classes)。這將會導致一片混亂。
有沒有“指定位置刪除”(placement delete)?
沒有,不過如果你需要的話,可以自己寫一個。
看看這個指定位置創建(placement new),它將對象放進了一系列Arena中;
class Arena {
public:
void* allocate(size_t);
void deallocate(void*);
// ...
};
void* operator new(size_t sz, Arena& a)
{
return a.allocate(sz);
}
Arena a1(some arguments);
Arena a2(some arguments);
這樣實現了之後,我們就可以這麼寫:
X* p1 = new(a1) X;
Y* p2 = new(a1) Y;
Z* p3 = new(a2) Z;
// ...
但是,以後怎樣正確地銷燬這些對象呢?沒有對應於這種“placement new”的內建的“placement delete”,原因是,沒有一種通用的方法可以保證它被正確地使用。在C++的類型系統中,沒有什麼東西可以讓我們確認,p1一定指向一個由Arena類型的a1分派的對象。p1可能指向任何東西分派的任何一塊地方。
然而,有時候程序員是知道的,所以這是一種方法:
template<class T> void destroy(T* p, Arena& a)
{
if (p) {
p->~T(); // explicit destructor call
a.deallocate(p);
}
}
現在我們可以這麼寫:
destroy(p1,a1);
destroy(p2,a2);
destroy(p3,a3);
如果Arena維護了它保存着的對象的線索,你甚至可以自己寫一個析構函數,以避免它發生錯誤。
這也是可能的:定義一對相互匹配的操作符new()和delete(),以維護《C++程序設計語言》15.6中的類繼承體系。參見《C++語言的設計和演變》10.4和《C++程序設計語言》19.4.5。
我能防止別人繼承我自己的類嗎?
可以,但你爲什麼要那麼做呢?這是兩個常見的回答:
效率:避免我的函數被虛擬調用
安全:保證我的類不被用作一個基類(例如,保證我能夠複製對象而不用擔心出事)
根據我的經驗,效率原因往往是不必要的擔心。在C++中,虛擬函數調用是如此之快,以致於它們在一個包含虛擬函數的類中被實際使用時,相比普通的函數調用,根本不會產生值得考慮的運行期開支。注意,僅僅通過指針或引用時,纔會使用虛擬調用機制。當直接通過對象名字調用一個函數時,虛擬函數調用的開支可以被很容易地優化掉。
如果確實有真正的需要,要將一個類封閉起來以防止虛擬調用,那麼可能首先應該問問爲什麼它們是虛擬的。我看見過一些例子,那些性能表現不佳的函數被設置爲虛擬,沒有其他原因,僅僅是因爲“我們習慣這麼幹”。
這個問題的另一個部分,由於邏輯上的原因如何防止類被繼承,有一個解決方案。不幸的是,這個方案並不完美。它建立在這樣一個事實的基礎之上,那就是:大多數的繼承類必須建立一個虛擬的基類。這是一個例子:
class Usable;
class Usable_lock {
friend class Usable;
private:
Usable_lock() {}
Usable_lock(const Usable_lock&) {}
};
class Usable : public virtual Usable_lock {
// ...
public:
Usable();
Usable(char*);
// ...
};
Usable a;
class DD : public Usable { };
DD dd; // 錯誤: DD::DD() 不能訪問
// Usable_lock::Usable_lock()是一個私有成員
(來自《C++語言的設計和演變》11.4.3)
爲什麼不能爲模板參數定義約束(constraints)?
可以的,而且方法非常簡單和通用。
看看這個:
template<class Container>
void draw_all(Container& c)
{
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
如果出現類型錯誤,可能是發生在相當複雜的for_each()調用時。例如,如果容器的元素類型是int,我們將得到一個和for_each()相關的含義模糊的錯誤(因爲不能夠對對一個int值調用Shape::draw的方法)。
爲了提前捕捉這個錯誤,我這樣寫:
template<class Container>
void draw_all(Container& c)
{
Shape* p = c.front(); // accept only containers of Shape*s
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
對於現在的大多數編譯器,中間變量p的初始化將會觸發一個易於瞭解的錯誤。這個竅門在很多語言中都是通用的,而且在所有的標準創建中都必須這樣做。在成品的代碼中,我也許可以這樣寫:
template<class Container>
void draw_all(Container& c)
{
typedef typename Container::value_type T;
Can_copy<T,Shape*>(); // accept containers of only Shape*s
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
這樣就很清楚了,我在建立一個斷言(assertion)。Can_copy模板可以這樣定義:
template<class T1, class T2> struct Can_copy {
static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
Can_copy() { void(*p)(T1,T2) = constraints; }
};
Can_copy(在運行時)檢查T1是否可以被賦值給T2。Can_copy<T,Shape*>檢查T是否是Shape*類型,或者是一個指向由Shape類公共繼承而來的類的對象的指針,或者是被用戶轉換到Shape*類型的某個類型。注意這個定義被精簡到了最小:
一行命名要檢查的約束,和要檢查的類型
一行列出指定的要檢查的約束(constraints()函數)
一行提供觸發檢查的方法(通過構造函數)
注意這個定義有相當合理的性質:
你可以表達一個約束,而不用聲明或複製變量,因此約束的編寫者可以用不着去設想變量如何被初始化,對象是否能夠被複制,被銷燬,以及諸如此類的事情。(當然,約束要檢查這些屬性的情況時例外。)
使用現在的編譯器,不需要爲約束產生代碼
定義和使用約束,不需要使用宏
當約束失敗時,編譯器會給出可接受的錯誤信息,包括“constraints”這個詞(給用戶一個線索),約束的名字,以及導致約束失敗的詳細錯誤(例如“無法用double*初始化Shape*”)。
那麼,在C++語言中,有沒有類似於Can_copy——或者更好——的東西呢?在《C++語言的設計和演變》中,對於在C++中實現這種通用約束的困難進行了
C++風格與技巧(Bjarne Stroustrup著)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.