C++之父Bjarne stroustrup曾經說過:不需要了解所有的c++細節,也能夠寫出好的c++程序;不應該注重語言方面的特征,而應該注重軟件設計技術本身。很顯然,我的這篇文章,與這兩句話背道而馳:).的確,我們程序員,不應該把精力放在c++本身語言的特征上,而是應該思考軟件設計技術本身。那么,在我們需要提高對c++理解的同時,是不是我們從下面幾個方面為著眼點1) 從編譯原理的角度2) 從技術需求的角度3) 從軟件設計技術的角度從以上的幾個角度,來重新審視c++一些晦澀語法,或許,我們能從中獲益。在這里,我要說的是,我們不單單是要記住這些c++語言特性怎么樣的使用,而是應該知道這些語言特性背后隱藏的故事,以便于我們更深層次地理解c++,理解軟件設計。一、子類通過函數名字隱藏父類函數。如下例:
當我們編譯pd->f(10)操作時,編譯器報錯。按照我們常規的理解是:父類的函數void f(int x)與子類的函數void f(double*pd),由于參數類型不同,其函數簽名也是不一樣的,按照這樣的邏輯,在這個類繼承體系中,這兩個函數完全應該是互不隱藏的,我們完全可以認為是符合overloaded規則的兩個函數。 但是,在c++里,子類通過函數名字隱藏父類函數,而不是通過函數簽名!c++給出的解釋也是合理的:試想一種情況:你使用了別人寫的類庫,繼承其中的某個類,寫了你自己的子類。如上面的例子,你的子類就是Derived,而類庫中的父類就是Base.當你根本不知道在父類中還有這樣一個f(int x)函數時,在調用子類Derived的f函數時,你犯了錯誤,參數類型傳成了int類型(或者不是你犯的錯誤,編譯器幫你自動轉化為int類型),結果是:程序可以正常運行,但是,執行的結果卻不是你所期望的,是f(int x)調用,而不是你自己的實現:f(double* pd)調用! 這就是c++為什么通過函數名字隱藏父類函數的原因。 說到這里,我們需要補充幾句:雖然c++在語言層面上給我們提供了這樣的保證,但是,子類hide父類的函數,這是一個非常不好的設計。從OO的角度出發,應該講求的是Liskov Substitution Principle。即:suntypes must be substitutable fro their base types.很顯然,當hide行為發生時,從接口的角度來講,子類與父類是不能互為替代的。父類的protected or public的方法,應該很自然地由其所有子類所繼承,而不是被隱藏。隱藏行為的發生,相當于在這套繼承體系中開的一個后門。很顯然,C++幫助我們自動隱藏了父類的方法,但是,作為程序開發的我們,應該意識到這一點,也應該避免這樣的設計。二、c++的per-class allocator語法規則 在D&E of C++一書中,Stroustrup給出了幾點c++提供per-class allocator的理由,這些理由也是我們使用class level的allocator的原因,所以,有必要我們總結一下:第一、許多程序應用,需要在運行的過程中,大量地Create和Delete對象。這些對象,諸如:tree nodes,linked list nodes,messages等等。如果在傳統的heap完成這些對象的創建,銷毀,由于大量的內存申請,釋放,勢必會造成內存碎片。這種情況下,我們需要對內存分配進行細粒度的控制。第二、一些應用需要長時間跑在內存受限的裝置上,這也需要我們對內存分配進行細粒度的控制,而不是無限制地分配,釋放。主要基于以上的兩點,c++提供了per-class allocator語言支持。如下例:
new操作符函數負責對象X的內存分配。對這樣一個語法規則,我們好奇的是,為什么聲明了一個我們從來都不使用的參數size_t sz.我們的使用語法如下: X* px = new X;C++也給出了解釋:per-class allocator機制將適用整個類的繼承體系。例如:
對于子類Y,其內存分配函數也是X::operator new()。但是,在這里,內存分配的大小,不應該是sizeof(X),而是sizeof(Y).問題的關鍵在這里:C++通過提供多余的參數size_t sz,而給開發者提供了更大的靈活性,也即:per-class allocator是面向類的繼承體系的內存管理機制,而不單單是面向單個類。三、Koenig Lookup機制。 大家對Andrew Koenig應該很熟悉,c++大牛,是AT&T公司Shannon實驗室大規模編程研究部門中的成員,同時他也是C++標準委員會的項目編輯。他擁有超過30年的編程經驗,其中有15年的C++使用經驗。 Koenig Lookup,就是以Andrew Koenig命名的查找規則。在看這個定義之前,我們先弄清楚函數所在的域的分類,一般來講,分為:1:類域(函數作為某個類的成員函數(靜態或非靜態))2:名字空間域3:全局域(即C++默認的namespace) 而Koenig Lookup機制,就是當編譯器對無限定域的函數調用進行名字查找時,除了當前名字空間域以外,也會把函數參數類型所處的名字空間加入查找的范圍。如下例:
如上的代碼,使用operator<<操作符函數,打印對象的狀態,但是函數ostream& operator<<(ostream& out, const MyArg& myArg) 的定義域是處于名字空間Koenig中,為什么編譯器在解析main函數(全局域)里面的operator<<調用時,它能夠正確定位到Koenig名字空間里面的operator<<?這是因為根據Koenig查找規則,編譯器需要把參數類型MyArg所在的名字空間Koenig也加入對ostream& operator<<(ostream& out, const MyArg& myArg) 調用的名字查找范圍中。 如果沒有Koenig查找規則,我們就無法直接寫cout<<myArg;,而是需要寫類似Koenig::operator<<(std::cout, myArg); 這樣的代碼(使用完全限定名)。這樣的結果是,即不直觀也不方便。
其實在C++里,提供了很多類似于Koenig查找規則的機制,以保證程序語法上的簡潔,明了。例如:許多的操作符函數,COPY構造函數。而這些,也是我們寫出專業的C++程序的基本。未完待續:)
Powered by: C++博客 Copyright © 愛上龍卷風