Effictive C++知識點復習
1、盡量以const、enum、inline替換#define或者寧可以編譯器替換預處理器
eg:#define NUM_RATIO 1.653
由於NUM_RATIO在編譯器開始處理源碼之前都被預處理器移走,因而當常量在編譯時出錯,只會提示到1.653.
對於程序員並不知道1.653在哪個文件中存放。故追蹤會浪費時間。即所使用的名稱並未進入記號表中。
解決方法:用一個常量替換上面的宏
const double NumRatio = 1.653;
註意:兩個常量定義時的寫法
2、若在頭文件定義一個常量的字符串(char*),則必須寫兩次const
const char* const authorName = "uestc";
3、class專屬常量,為了將常量的作用域限制於class內,必須讓它成為class的一個成員。為了確保此常量只有一份實體,必須讓他成為一個static成員
class GamePlayer
{
private:
static const int NumTurns = 5;//只是聲明並沒有定義
int score[NumTurns];//使用常量
};
有一個問題出現,若是要對NumTurns取地址,必須聲明且定義。或者編譯器要求必須有定義,則此時就必須對該變量進行定義處理。
const int GamePlayer::NumTurns;由於聲明已獲得初值,因此定義時不可以再設初值。
另外一個問題,若編譯器要求不能在一個類中對static進行設定初值。即"in-class初值設定"也只允許對整數常量進行。則此時只能將初值放在定義式。
class GamePlayer
{
private:
static const int NumTurns;//只是聲明並沒有定義
int score[NumTurns];//使用常量
};
const int GamePlayer::NumTurns=5;
上述出現了問題,就是NumTurns要求必須在編譯時期得到其值。則改用enum方法。
class GamePlayer
{
private:
enum{NumTurns = 5};//只是聲明並沒有定義
int score[NumTurns];//使用常量
};
此時就不能對NumTurns 取地址。
如果不想讓別人獲得一個指針或者引用指向你的某個整數常量,就可以使用enum.
4、當用實現宏,看起來像函數,但不會招致函數調用帶來的開銷。
#define MAX(a,b) f((a)>(b)?(a):(b))
你必須為宏中的所有實參加上小括號。
int a = 5, b = 0;
MAX(++a, b);
調用MAX之前,a的遞增次數取決於“它被拿來和誰比較”
template<typename T>
inline void MAX(const T&a, const T&b)
{
f(a>b?a:b);
}
總結:
····對於單純常量,最好以const對象或enums替換#defines
····對於形似函數的宏,最好改用inline函數替換#define
----------------------------------------------------------
1、盡可能使用const
聲明指針為const ,即T* const指針。
聲明指針所指向的東西是const,則const T*
例如:vector
const std::vector<int>::iterator iter = vec.begin();//如同T* const
*iter = 10; //改變iter所指物,沒問題
++iter; //錯誤, iter是const
std::vector<int>::const_iterator cIter = vec.begin();//cIter的作用是const T*
*cIter = 10;//*cIter是const
++cIter;//沒問題,改變cIter
2、const成員函數
將成員函數作為const修飾,目的就是為了確認該成員函數可作用於const對象身上。
重要的原因:
可以知道哪個函數可以改動對象內容而哪個函數不行;
使操作對象成為可能。
3、如果兩個成員函數只是常量型不同,可以被重載
4、const成員函數不可以更改對象任何non-static成員變量
mutable釋放掉non-static成員變量的const特性,可能會被const成員函數修改。
5、可以使用non-const版本的成員函數調用const版本,使用const_cast去除const即可。
但是不可以用const版本調用non-const版本。
---------------------------------------------------
1、確定對象被使用前已先被初始化
2、對象的成員變量的初始化發生在構造函數本體之前。
而在構造函數內部,只是對成員變量進行賦值的。
3、 構造函數的成員初始值列替換賦值動作,此時對成員變量而言是初始化操作。
默認構造函數則是先為成員變量進行設初值,然後立刻再對它賦予新值。
成員初始值列的效率大於默認構造函數。(內置類型效率是一樣的)
4、成員初始化次序總是以其聲明次序被初始化。
5、若成員變量是const或者引用類型,他們就一定需要初值,不能被賦值。
6、static對象包括global對象,定義在namespace作用域內的對象,在classes內,在函數內,以及在file作用域內被聲明為static的對象。
non-logical static對象是除了在函數內的對象都是。
函數內的對象叫做logical static對象。
7、編譯單元--是指產出單一目標文件的那些源碼。
如果某一個編譯單元內的某個non-local static對象的初始化動作使用了另一編譯單元內的某個non-local static 對象,它所用到的這個對象可能尚未初始化。
定義於不同編譯單元內的non-local static 對象的初始化次序並無明確定義。
解決方法:將每個non-local static對象搬到自己的專屬函數內(該對象在函數內聲明為static)。這樣做,C++保證,函數內的local static對象會在該函數被調用期間首次遇上該對象的定義式時被初始化。
8、這些函數內涵static對象在多線程系統中帶有不確定性。運用返回引用函數防止初始化次序問題。
9、請為內置型對象進行手工初始化,因為C++不保證初始化他們。
10、為免除跨編譯單元之初始化次序問題,請以local static 對象替換non-local static對象。
-----------------了解C++默默編寫並調用那些函數------------
1、什麽時候空類不再時空類呢?當C++處理之後。
編譯器會自動生成
拷貝構造函數
賦值構造函數
析構函數
默認構造函數(沒有聲明任何構造函數之前)
這些函數都是public且inline
只有這些函數在被需要才會被編譯器創建出來。
2、在一個內含引用成員的class以及內含const成員的class,必須定義自己的拷貝賦值操作符。
3、若基類將拷貝賦值操作符聲明為private,編譯器將拒絕為其派生類生成一個copy賦值操作符。因為編譯器為派生類生成的拷貝賦值操作符中需要處理base class 成分。
------------------不想使用編譯器自動生成的函數------------
對於拷貝函數和賦值函數,將其聲明為私有private且必須不能對其進行定義。
但是類成員函數和類友元函數還是可以調用private函數,此時連接器會發生錯誤,進而轉移到編譯期也是可能的。因而若不想使用編譯器自動生成的函數,就將函數聲明為私有,且不對其進行定義。
通常並不是在該類本身做私有操作,而是在該類的基類做相應的操作,然後做private繼承。(這樣做的原因,是在該類做拷貝和賦值操作時,編譯器會試圖生成一個拷貝構造函數和賦值構造函數,此時這些函數會嘗試調用基類的對應的構造函數,基類會拒絕)
-----------為多態基類聲明virtual析構函數----------------
了解工廠函數,返回一個基類指針,指向新生成的派生類對象。
什麽時候會用到工廠函數?
當客戶只關心使用的時間,而不想關心時間如何計算的細節,此時就可以使用工廠函數,返回的指針是一個基類指針,指向新生成派生類對象。工廠函數的返回對象必須是動態分配的對象,即存儲在heap中。主要還要記得用delete 父類指針;
eg:
父類指針* getTimeKeeper()
{
return new 子類對象;
}
由於父類指針指向子類對象,但是要delete 父類指針,而new 的是子類對象,故此時基類必須有virtual 析構函數。
若是一個non-virtual 析構函數,實際執行通常發生的是派生類對象的成分沒被銷毀。此時交造成了資源泄露,局部銷毀對象行為。
但是,並不是所有的析構函數都是virtual,當一個類不企圖做基類時,不要將其析構函數作為一個virtual,因為會占用空間。
當想創建一個抽象類,但手上沒有任何純虛函數,且抽象類又常做基類,基類中常要含有一個虛析構函數。故此時就要有一個純虛析構函數。註意你必須為純虛析構函數提供一份定義。
析構函數的運作方式:最深層派生的那個類其析構函數最先被調用,然後是其每一個基類的析構函數。
------------別讓異常逃離析構函數----------------------
析構函數突出異常會導致程序過早結束或者發生不明確行為的風險。
第一種情況,通過調用abort完成;搶先制止不明確行為於死地
A::~A()
{
try
{
db.close();//此時若拋出異常;
}
catch(...)
{
std::abort();//強制結束程序;
}
}
第二種情況,吞下異常
A::~A()
{
try
{
db.close();//此時若拋出異常;
}
catch(...)
{
制作運轉記錄;//吞下異常
}
}
最好的方法,就是將close()責任從析構函數轉移到對象上。
class A
{
public:
void close()
{
db.close();
close = true;
}
~A()
{
if (!close)
{
try
{
db.close();//將異常問題轉移到對象來處理。
}
catch(...)
{
制作運轉記錄;
}
}
}
};
---------------絕不在構造和析構過程中調用virtual函數--------
基類構造期間virtual 函數絕不會下降到派生類。即在base class 構造期間,virtual 函數不是vitual 函數。
class A
{
public:
A();
virtual void Copy() const = 0;
};
A::A()
{
...
Copy();//此時會出現無法連接,因為連接器找不到必要的A::copy實現的代碼。
}
class B:public A
{
virtual void Copy() const;
}
B b;//首先會調用A的構造函數。其次再調用B 的構造函數。即B對象內的基類成分會在派生類自身成分被構造之前先構造。而A的構造函數中會調用virtual函數copy。這時候調用的copy是A類中的版本,不是B類中的版本。
這就是基類構造期間virtual 函數絕不會下降到派生類。
原因是:在派生類對象的基類構造期間,對象類型時基類,而不是派生類。不只virtual函數會被編譯器解析至基類,若使用運行期類型信息(dynamic_cast 和 typeid),也會把對象視為基類類型。
同理:也適用於析構函數。
註意:不僅要確定構造函數和析構函數都沒有virtual函數,而他們調用的所有函數也都服從同一約束。
eg:
class A
{
public:
A()
{
init();//錯誤。不僅要保證構造函數沒有虛函數,而且構造函數所調用的函數內也不能有虛函數。
}
virtual void Copy() const = 0;
private:
void init()
{
...
Copy();
}
};
總結;無法使用虛函數從基類向下調用,在構造期間,你可以,讓派生類將必要的構造信息向上傳遞到基類的構造函數。
------------令opertator=返回一個引用*this-----------
為了實現連鎖賦值,則賦值操作符必須返回一個引用指向操作符的左側實參。
實現自我賦值----
class A
{
private:
char* m_data;
};
A& A::operator=(const A& a)
{
if (this == &a)
{
return *this;
}
delete[] m_data;
m_data = NULL;
m_data = new char[strlen(a.m_data)+1];
strcpy(m_data, a.m_data);
return *this;
}
考慮異常安全性問題
A& A::operator=(const A&a)
{
char* tmp = m_data;
tmp = new char[strlen(a.m_data)+1];
delete[] tmp;
return *this;
}
----------復制對象時勿忘其每一個成分-------------
註意為派生類寫拷貝函數時,必須要復制其基類成分,那些成分往往時private,所以你無法直接訪問他們,你應該讓派生類的拷貝函數調用相應的基類函數。
class A
{
A(const A& rhs)
{
a = rhs.a;
}
private:
int rhs;
};
class B:public A
{
B(const B& lhs):A(lhs),b(lhs.b);
{
}
private:
int b;
};
不要嘗試以某個拷貝函數實現另一個拷貝函數。而應該將共同機能放進第三個函數中,並由兩個拷貝函數共同調用。
--------------------以對象管理資源----------------
將資源放在對象內,保證析構函數會自動調用確保資源被釋放。
例如auto_ptr是個類指針對象。其析構函數會自動對其所指對象調用delete.
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
}
涉及2個知識點:
獲得資源後立即放進管理對象內----調用工廠函數之後,返回的資源被當作其管理者auto_ptr的初值。
管理對象運用析構函數確保資源被釋放---一旦對象被銷毀,其析構函數自然會被自動調用,於是資源被釋放。
註意:auto_ptr銷毀時會自動刪除它所指的對象,因而不要讓多個auto_ptr同時指向同一對象。
解決方案:使用引用計數的智慧指針,即shared_ptr。
盡量不要將動態分配用在auto_ptr 和 shared_ptr;
不要 shared_ptr<int> spi(new int[1024]);因為這樣必須使用delete spi,而不是delete[] spi;
------------在資源管理類中小心拷貝行為------------
···通常是禁止復制。
···對底層資源祭出引用計數法--將資源的引用計數遞增。shared_ptr就是這樣的。
(因為shared_ptr允許指定所謂的刪除器,那是一個函數或者函數對象,當引用次數為0時便被調用)
···或者復制底部資源
···轉移底部資源的擁有權
------------在資源管理類中提供對原始資源的訪問------------
std::auto_ptr<Investment> pInv(createInvestment());
假設你希望以某個函數處理Investment對象。
int daysHeld(const Investment* pi);
這樣調用
daysHeld(pInv);錯誤。因為daysHeld需要的是Investment* 指針。
但傳給它的卻是auto_ptr<Investment>對象
此時需要一個函數將RAII class對象轉換為原始資源,即Investment*資源。
即shared_ptr 和 auto_ptr都提供了一個get成員函數。用來執行顯示轉換,也就是它會返回智能指針內部的原始指針。
即更改為
int days = daysHeld(pInv.get());此時正確。顯示轉化原始指針。
shared_ptr 和 auto_ptr 都重載了指針取值操作符,因而他們允許隱式轉換到底部原始指針。
std::auto_ptr<Investment> pInv(createInvestment());
bool taxable = !(pInv->idTaxFree());隱式轉換為原始指針,然後調用operator->訪問資源。
----------成對使用new 和delete時要采取相同形式----------
new操作符
··內存被分配出來;
··針對內存會有一個(或更多)構造函數被調用。
delete操作符
··會由一個(或更多)析構函數被調用;
··內存才會被釋放。
typedef string AddressLines[4];
string* pa1 == new AddressLines;//和ew string[4]一樣。
deletep[] pal;
因而最好盡量不要對數組形式做typedef 動作。
-------以獨立語句將newed對象置入智能指針-------------
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
processWidget決定對其動態分配得來的Widget運用智能指針。
當調用processWidget
processWidget(new Widget, priority());
不能通過編譯。shared_ptr構造函數需要一個原始指針,但該構造函數是個explicit構造函數,無法進行隱式轉換。
更改為:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
但是這樣會出現內存泄漏;
函數參數需要做以下三件事:
·執行new Widget;
·調用priority();
·調用tr1::shared_ptr構造函數
當調用priority()出現異常,則會發生內存泄露。
---以獨立語句將newed對象存儲於置入智能指針內。否則會有異常拋出。
避免這類問題,使用分離語句,分別寫出
(1)創建Widge,
(2)將它置入一個智能指針內,然後
再把那個智能指針傳給processWidget
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
----讓接口容易被正確使用,不易被誤用---------------
示例1:
struct Month{
Month(int mon):val(mon){}
int val
};
struct Day{
Month(int day):val(day){}
int val
};
struct Year{
Month(int year):val(year){}
int val
};
接口就會設計成這樣:
Data(Month , Day, Year);
那麽調用的時候可能就需要這樣去調用:
Data(Month(month), Day(day), Year(year));
示例2:
Investment * createInvestment();
//使用原始指針可能會使得用戶飯錯誤的概率大大增加,這裏嘗試使用一個智能指針來返回更好!
shared_ptr<Investment> createInvestment()
{
...
return shared_ptr<Investment>(new Investment(tmpIvst));
}
一般返回智能指針應該給他指定一個更好的刪除器。
對於一個指針在一個Dll中被賦值,但是另一個Dll中卻被delete掉,而
對於shared_ptr就不會有這種問題,其刪除器是在new的時候就指定好了的刪除器,所以不會出現像上述那樣的跨越Dll的new/delete情況。
---------設計class相當於設計一個內置類型---------
1. 新的type應該如何創建與銷毀
2. 對象的初始化與賦值應該有什麽樣的區別
3. 新type的對象如果被pass-by-value,有什麽影響?
4. 什麽事新type的合法值
5. 新的type需要什麽樣的轉換
6. 什麽樣的操作符和函數對於這個type而言是合理的
7. 什麽樣的標準函數應該駁回
8. 誰應該取用新的type成員
9. 什麽事新type的未聲明接口
10. 新的type應該如何的進行一般化
11. 這個新的type的建立是一定的嗎?
-----------------以按常引用傳遞替換按值傳遞----------------
按值傳遞---通常都以實際實參的副本為初值,而調用端所獲得也是函數返回值的一個復件。這些復件也是由對象的拷貝否早函數產生,故會帶來額外的開銷。
按引用傳遞,沒有任何構造函數或析構函數被調用。因為沒有任何新對象被創建。同時也避免了切割問題。當一個派生類對象以按值方式傳遞一個基類對象,基類的拷貝構造函數會被調用,而此時派生類對象的特化性質都被切割掉,僅僅保留了一個基類對象。而解決切割的問題就是以按引用方式傳遞。因為引用往往以指針實現出來。
一般而言,按值傳遞的對象是內置類型和STL叠代器和函數對象。其他的盡可能按照引用方式傳遞。
-----------必須返回對象時,別忘返回其引用---------------
返回引用,可以減少返回時候拷貝帶來的開銷操作;
1.在函數中使用new在heap上創建對象而不是在stack上創建對象:
Rational & operator(const Rational & lhs, const Rational & rhs)
{
Retional result = new Rational(lhs.numirator + rhs.numirator, lhs.denumirator * rhs.denumirator);
return result;
}
但是如果這麽做的話誰又來保證客戶可以安然無恙的將資源合理的釋放呢。
2.在函數內部用static對象來取代臨時變量,像下面這樣:
const Rational & operator*(const Rational & lhs, const Rational & rhs)
{
static Rational result;
Rational = Ratioanl(lhs.numirator * rhs.numirator, lhs.denumirator * rhs.denumirator);
return result;
}
首先,加入static可定會對多線程的情況帶來一些麻煩。
Rational a, b, c, d;
3.if(a * b == c * d);
//相當於if(operator*(a, b) == operator*(c, d))
顯然,這兩個operator*返回的static變量肯定是指向同一個的,那麽這個if必定等於1。其實正常的情況就是返回一個value就可以了,雖然可能帶來效率上的損失(只是可能,現代的編譯器有的都能消除這種情況帶來的損失),這種損失也可以使用std::move來加以彌補。
總結:不能返回指針或引用指向一個棧對象,或返回引用指向一個堆分配對象,或返回指針或引用指向一個局部靜態對象而有可能同時需要多個這樣的對象。
----------將成員變量聲明為private-------------------
1.protected成員變量的封裝性並非高於public變量。
成員變量的封裝性與成員變量的內容改變時所破壞的代碼數量成反比。因為一個public成員變量改變,會導致所有使用過它的代碼都會發生破壞。而protected 成員變量改變,會導致所有使用它的派生類都會被破壞。
因此從封裝的角度來看,其實只有兩種訪問權限:private(提供封裝)和其他(不提供封裝)
2.將成員變量聲明為private,這可賦予客戶訪問數據的一致性,可細微劃分訪問控制,提供class充分實現彈性。
------------寧以non-number、non-friend替換member函數-------
1.
class webBroswer
{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
2.
member函數
class WebBrowser
{
public:
...
void clearEverything();
};
3.
non-member函數
void clearBrowser(WebBrowser & wb)
{
wb.clearCache();
wb.clearHistory();
wb.clearCookie();
}
比較,是member函數還是non-member函數好呢?
面向對象的誤解問題,數據以及操作數據的那些函數應該被捆綁在一塊。意味著member函數是較好的選擇。
面向對象的守則要求是數據盡可能被封裝,根源member帶來的封裝性比non-member函數要低。
封裝的原因---使我們能夠改變事物而只影響有限客戶。
但是越多東西被封裝,就越少人可以看到它,因而就有越大的彈性去變化它。因為我們的改變僅僅直接影響看到改變那些東西的能力也就越大。
為什麽是non-member 和 non-friend具有較大的封裝性呢?
因為它並不增加能夠訪問class 內的private成分的函數數量。但是這個non-menber可以是另一個class 的member。
C++為了保證具有較強的封裝性,通常將一個non-member函數且將其位於類所在的同一個命名空間內,
namespace A
{
class A{};
void ClearA(A& a);//non-member函數,這樣的便利函數可以在一個namespace裏面聲明多個。
}
註意namespace 和 classes的區別:
前者可以跨越多個源碼文件,而後者不能。
---------若素有參數都需類型轉換,請采用non-member函數-----
實例:
class Rational
{
public:
const Rational operator*(const Rational& rhs)const;
....;
};
Rational onehalf(1, 2);
result = onehalf*2;正確;
result = 2*onehalf;錯誤;
等價於
result = onehalf.operator*(2);正確;
result = 2.operator*(onehalf);錯誤;
通過結論,只有當參數被位於參數列內,這個參數才是隱式類型轉換的合格參與者。
解決方法:支持混合式算術運算。讓operator*成為一個non-member函數。
class Rational
{
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numberator()*rhs.numberator(), lhs.denominator()*rhs.denominator());
}
result = onehalf*2;正確;
result = 2*onehalf;正確;
------------考慮寫出一個不拋出異常的swap函數-----------
1.標準庫的swap算法
namespace std
{
template<typename T>
void swap(T & a, T & b)
{
T tmp = a;
a = b;
b = tmp;
}
}
2.如何完成高效的swap操作,比如拷貝兩個對象。
class WidgetImpl
{
public:
...
private:
int a, b, c;
std::vector<double> v;
...
};
class Widget
{
pubcic:
.....
private:
WidgteImpl * pImpl;
};
如何高效交換WidgteImp1類對象
第一種,將函數特例化-----針對的是class而言;
class Widget
{
public:
...
void swap(Widget & other)
{
using std::swap;
swap(pUmpl, other.pUmpl);
}
...
};
namespace std
{
template<>
void swap<Widget>(Widget & a, Widget & b)
{
a.swap(b);
}
}
第二種做法:
當Widget和WidgetImp1都是class templates而非classes
即
template<typename T>
class WidgetImpl
{
...
};
template<typename T>
class Widget
{
...
};
此時不能在對swap特例化;而是做偏特化.錯誤。C++編譯器只允許對類做偏特化。
namespace std
{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);----錯誤錯誤錯誤
}
}
對於函數偏特化模板,通常為它添加一個重載版本
namespace std
{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
std規定,可以全特化std內的templates,但不可以添加新的template到std裏。
故第二種方法,只能這樣做
namespace WidgetStuff
{
template<typename T>
class Widget
{
...
};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
C++名稱查找法則,先global作用域或者T所在命名空間內任何T專屬的swap;然後是T所在的命名空間,然後std::swap。
考慮寫出一個不拋出異常的swap函數總結:在你的class 或 template所在的命名空間內提供一個non-menber swap,並令它調用上述的swap。
-----------盡可能延後變量定義式的出現時間-----------
盡量少做轉型動作
1. const_cast:常量性的轉除。
2. dynamic_cast:安全的向derived --class進行轉型,可能會帶來很高的開銷
3. reinterpret_cast:低級轉型,例如可講pointer轉成int,不建議使用
4. static_cast: 強迫隱式轉換,例如int to const int,int to double, 但是const int 到 int 只有const_cast能做到。
-----避免返回handles指向對象的內部成分------------
class Point
{
public:
point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData
{
Point ulhc; //左上角
Point lrhc; //右下角
};
class Rectangle
{
...
private:
shared_ptr<RectData> pData;
};
class Rectangle
{
public:
...
Point & upperLeft() const {return pData->ulhc; }
Point & lowerRight() const{return pData->lrhc; }
...
};
這樣只是能通過編譯,但是設計確是錯誤的,在成員函數被聲明為const的情況下返回了一個內部成員的引用,這樣使得ulhc 以及 lrhc對象都可以被更改。但是二者都是private的,實際上二者都是不應該被改變的。
返回句柄的對象都會造成內部狀態暴露在外面。容易改變內部狀態或者造成空懸的指針。
解決方法:就是在成員函數返回的handle前加上const。
---------為異常安全努力是值得的-------------
異常安全性包括:
不泄露任何資源;
不允許數據敗壞;
-------透徹了解inlining的裏裏外外------------
inlining的優點:
1.可以消除函數調用帶來的開銷;編譯器對這種非函數代碼可以做出優化策略;
2.若inline函數本體很小的話,那麽inline函數展開的代碼大小可能比調用非inline函數展開的代碼更小,這樣就可以提高緩存的集中率,從而提高函數的效率。
inlining的缺點:
肯定會導致程序代碼的大小更加的龐大,這樣會帶來代碼膨脹,造成損害,首先就是會導致計算機額外的換頁行為,降低指令告訴緩存的集中率。這就會帶來效率的損失。
標準庫中的template::max實際上就是一個inline函數,但是並非所有的template版本的函數都是inline函數。 如果聲明某個模板的時候認為其所有的具體化都應該是模板,那麽就應該將模板聲明成為inline的形式。
第一種實例:
inline void f(){...}
void (*pf)() = f;
...
f();
pf();
上面的f()可能會被正確的inline,但是下面的pf並不一定,因為函數要提供地址給pf,那麽一般是應該實現出inline函數的一個outline版本提供給函數指針的。
第二種實例:
class Base
{
public:
private:
std::string;
};
class Derived : public Base
{
public:
Derived(){}//它不能是inline。因為derived構造函數至少一定會陸續調用其成員變量和base class兩者的構造函數。而那些調用會影響編譯器是否對此空函數inlining.
...
private:
std::string dm1, dm2, dm3;
};
而且inline函數是無法鏈接的,這樣當函數有改動的時候,會導致整個模塊的重新鏈接, 可能造成較為恐怖的開銷。
inline 函數若被改變,則必須重新編譯;
non-inline一旦有任何修改,只需重新連接。
-----------------將文件間的編譯依存關系降至最低-------------
避免大量依賴性編譯的解決方案是:在頭文件中用class聲明外來類,用指針或引用代替變量的聲明,在CPP文件中包含外來類的頭文件。
1.
假設有三個類ComplexClass, SimpleClass1和SimpleClass2,采用頭文件將類的聲明與類的實現分開,這樣共對應於6個文件,分別是ComplexClass.h,ComplexClass.cpp,SimpleClass1.h,SimpleClass1.cpp,SimpleClass2.h,SimpleClass2.cpp。
ComplexClass復合兩個BaseClass,SimpleClass1與SimpleClass2之間是獨立的,ComplexClass的.h是這樣寫的:
#ifndef COMPLESS_CLASS_H
#define COMPLESS_CLASS_H
#include “SimpleClass1.h”
#include “SimpleClass2.h”
class ComplexClass
{
SimpleClass1 xx;
SimpleClass2 xxx;
};
…
#endif /*COMPLESS _CLASS_H*/
問題1:當SimpleClass1.h發生了變化,比如添加了一個新的成員變量。則此時SimpleClass1.cpp必須重新編譯。由於SimpleClass2因為與SimpleClass1是獨立的,所以SimpleClass2是不需要重編的。那麽現在的問題是,ComplexClass需要重編嗎?
引出話題1:
答案是“是”,因為ComplexClass的頭文件裏面包含了SimpleClass1.h(使用了SimpleClass1作為成員對象的類),而且所有使用ComplexClass類的對象的文件,都需要重新編譯!
再次引出問題:
如果把ComplexClass裏面的#include “SimpleClass1.h”給去掉,當然就不會重編ComplexClass了,但問題是也不能通過編譯了,因為ComplexClass裏面聲明了SimpleClass1的對象xx。那如果把#include “SimpleClass1.h”換成類的聲明class SimpleClass1,會怎麽樣呢?能通過編譯嗎?
答案是“否”,因為編譯器需要知道ComplexClass成員變量SimpleClass1對象的大小,而這些信息僅由class SimpleClass1是不夠的,但如果SimpleClass1作為一個函數的形參,或者是函數返回值,用class SimpleClass1聲明就夠了。如:
1 // ComplexClass.h
2 class SimpleClass1;
3 …
4 SimpleClass1 GetSimpleClass1() const;
5 …
但如果換成指針呢?像這樣:
// ComplexClass.h
#include “SimpleClass2.h”
class SimpleClass1;
class ComplexClass:
{
SimpleClass1* xx;
SimpleClass2 xxx;
};
這樣能通過編譯嗎?
答案是“是”,因為編譯器視所有指針為一個字長(在32位機器上是4字節),因此class SimpleClass1的聲明是夠用了。但如果要想使用SimpleClass1的方法,還是要包含SimpleClass1.h,但那是ComplexClass.cpp做的,因為ComplexClass.h只負責類變量和方法的聲明。
問題2:
那麽還有一個問題,如果使用SimpleClass1*代替SimpleClass1後,SimpleClass1.h變了,ComplexClass需要重編嗎?
先看Case2。
回到最初的假定上(成員變量不是指針),現在SimpleClass1.cpp發生了變化,比如改變了一個成員函數的實現邏輯(換了一種排序算法等),但SimpleClass1.h沒有變,那麽SimpleClass1一定會重編,SimpleClass2因為獨立性不需要重編,那麽現在的問題是,ComplexClass需要重編嗎?
答案是“否”,因為編譯器重編的條件是發現一個變量的類型或者大小跟之前的不一樣了,但現在SimpleClass1的接口並沒有任務變化,只是改變了實現的細節,所以編譯器不會重編。
問題3:
Case 3:
// ComplexClass.h
#include “SimpleClass2.h”
class SimpleClass1;
class ComplexClass
{
SimpleClass1* xx;
SimpleClass2 xxx;
};
// ComplexClass.cpp
void ComplexClass::Fun()
{
SimpleClass1->FunMethod();
}
答案是“否”,因為這裏用到了SimpleClass1的具體的方法,所以需要包含SimpleClass1的頭文件,但這個包含的行為已經從ComplexClass裏面拿掉了(換成了class SimpleClass1),所以不能通過編譯。
解決問題:
只要在ComplexClass.cpp裏面加上#include “SimpleClass1.h”就可以了。
這樣做是為了什麽?假設這時候SimpleClass1.h發生了變化,會有怎樣的結果呢?
SimpleClass1自身一定會重編,SimpleClass2當然還是不用重編的,ComplexClass.cpp因為包含了SimpleClass1.h,所以需要重編,但換來的好處就是所有用到ComplexClass的其他地方,它們所在的文件不用重編了!因為ComplexClass的頭文件沒有變化,接口沒有改變!
總結:對於C++類而言,如果它的頭文件變了,那麽所有這個類的對象所在的文件都要重編,但如果它的實現文件(cpp文件)變了,而頭文件沒有變(對外的接口不變),那麽所有這個類的對象所在的文件都不會因之而重編。
---再理解Handles classes
#include <string>
#include "MyDate.h"
#include "MyAddress.h"
class Person
{
private:
string name;
MyDate birthday;
MyAddress address;
public:
// fallows functions
// ...
};
在MyDate.h裏面寫好日期類相關的成員變量與方法,而在MyAddress.h裏面寫好地址類相關的成員變量與方法。但如果此後要往MyDate類或者MyAddresss類添加成員變量,那麽不僅僅所有用到MyDate或者MyAddress對象的文件需要重新編譯,而且所有用到Person對象的文件也需要重編譯,一個小改動竟然會牽涉到這麽多的地方!
第一種是采用Handler Classes(用指針指向真正實現的方法),就是就是.h裏面不包含類的自定義頭文件,用“class 類名”的聲明方式進行代替(也要把相應的成員變量替換成指針或引用的形式),在.cpp文件裏面包含類的自定義頭文件去實現具體的方法。
更改如下:
// Person.h
#include <string>
using namespace std;
class PersonImp;
class Person
{
private:
//string Name;
//MyDate Birthday;
//MyAddress Address;
PersonImp* MemberImp;
public:
string GetName() const;
string GetBirthday() const;
string GetAddress() const;
// follows functions
// ...
};
// Person.cpp
#include "PersonImp.h"
#include "Person.h"
string Person::GetName() const
{
return MemberImp->GetName();
}
string Person::GetBirthday() const
{
return MemberImp->GetBirthday();
}
string Person::GetAddress() const
{
return MemberImp->GetAddress();
}
在Person.h裏面並沒有使用MyDate*和MyAddress*,而是用了PersonImp*,由PersonImp裏面包含MyDate與MyAddress,這樣做的好處就是方便統一化管理,它要求PersonImp裏面的方法與Person的方法是一致的。以後Person添加成員變量,可以直接在PersonImp中進行添加了,從而起到了隔離和隱藏的作用,因為客戶端代碼大量使用的將是Person,而不必關心PersonImp,用於幕後實現的PersonImp只面向於軟件開發者而不是使用者。
第二種是Interface Classes(抽象基類)。
Interface Classes則是利用繼承關系和多態的特性,在父類裏面只包含成員方法(成員函數),而沒有成員變量,
// Person.h
#include <string>
using namespace std;
class MyAddress;
class MyDate;
class RealPerson;
class Person
{
public:
virtual string GetName() const = 0;
virtual string GetBirthday() const = 0;
virtual string GetAddress() const = 0;
virtual ~Person(){}
};
// RealPerson.h
#include "Person.h"
#include "MyAddress.h"
#include "MyDate.h"
class RealPerson: public Person
{
private:
string Name;
MyAddress Address;
MyDate Birthday;
public:
RealPerson(string name, const MyAddress& addr, const MyDate& date):Name(name), Address(addr), Birthday(date){}
virtual string GetName() const;
virtual string GetAddress() const;
virtual string GetBirthday() const;
};
在RealPerson.cpp裏面去實現GetName()等方法。
子類的頭文件變化了,則子類一定會重編,所有用到子類頭文件的文件也要重編。為了防止重編,應該盡量少用子類的對象。利用多態的特性,可以使用父類的指針。
Person* p = new RealPerson(xxx),然後p->GetName()實際上是調用了子類的GetName()方法。
問題引出:
new RealPerson()這句話一寫,就需要RealPerson的構造函數,那麽RealPerson的頭文件就要暴露了,這樣可不行。還是只能用Person的方法。
解決問題:
// Person.h
static Person* CreatePerson(string name, const MyAddress& addr, const MyDate& date);
這個方法是靜態的(沒有虛特性),它被父類和所有子類共有,可以在子類中去實現它:
// RealPerson.cpp
#include “Person.h”
Person* Person::CreatePerson(string name, const MyAddress& addr, const MyDate& date)
{
return new RealPerson(name, addr, date);
}
這樣在客戶端代碼裏面,可以這樣寫:
// Main.h
class MyAddress;
class MyDate;
void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date);
// Main.cpp
#include "Person.h"
#include “MyAddress.h”;
#include “MyDate.h”;
void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date)
{
Person* p = Person::CreatePerson(name, addr, date);
…
}
總結:
Handler classes與Interface classes解除了接口和實現之間的耦合關系,從而降低文件間的編譯依存性。減少編譯依存性的關鍵在於保持.h文件不變化,具體地說,是保持被大量使用的類的.h文件不變化,
1. 支持“編譯依存性最小化”的一般構想是:相依於聲明式,不要相依於定義式,基於此構想的兩個手段是Handler classes和Interface classes。
2. 程序庫頭文件應該以“完全且僅有聲明式”的形式存在,這種做法不論是否涉及templates都適用。
註意:重寫只能適用於實例方法,不能用於靜態方法。多余靜態方法,只能隱藏(形式上被重寫,但是不符合多態的特性),重寫是用來實現多態性的,只有實例方法可以實現多態,而靜態方法無法實現多態。
----------確定你的public繼承繼承塑模出is-a關系-----------------
在所有public繼承的背後,一定要保證父類的所有特性子類都可以滿足(父類能飛,子類一定可以飛),抽象起來說,就是在可以使用父類的地方,都一定可以使用子類去替換。
class Bird
{
public:
virtual void fly(){cout << "it can fly." << endl;}
};
class Penguin: public Bird
{
// fly()被繼承過來了,可以覆寫一個企鵝的fly()方法,也可以直接用父類的
};
int main()
{
Penguin p;
p.fly(); // 問題是企鵝並不會飛!
}
企鵝是鳥,但是企鵝不能飛,該怎麽解決這個問題呢?
方法1:
在Penguin的fly()方法裏面拋出異常,一旦調用了p.fly(),那麽就會在運行時捕捉到這個異常。
方法2:
去掉Bird的fly()方法,在中間加上一層FlyingBird類(有fly()方法)與NotFlyingBird類(沒有fly()方法),然後讓企鵝繼承與NotFlyingBird類。
方法3:
保留所有Bird一定會有的共性(比如生蛋和孵化),去掉Bird的fly()方法,只在其他可以飛的鳥的子類裏面單獨寫這個方法
一句話:
is-a關系:----任何父類可以出現的地方,子類一定可以替代這個父類,只有當替換使軟件功能不受影響時,父類才可以真正被復用。通俗地說,是“子類可以擴展父類的功能,但不能改變父類原有的功能”。
“public繼承”意味著is-a。適用於base classes身上的每一件事情一定也適用於derived classes身上,因為每一個derived class對象也都是一個base class對象。
------------避免遮掩繼承而來的名稱-----------------
實例1
//例1:普通變量遮掩
int i = 3;
int main()
{
int i = 4;
cout << i << endl; // 輸出4
}
這是一個局部變量遮掩全局變量的例子,編譯器在查找名字時,優先查找的是局部變量名,找到了就不會再找,所以不會有warning,不會有error,只會是這個結果。
實例2
//例2:成員變量遮掩
class Base
{
public:
int x;
Base(int _x):x(_x){}
};
class Derived: public Base
{
public:
int x;
Derived(int _x):Base(_x),x(_x + 1){}
};
int main()
{
Derived d(3);
cout << d.x << endl; //輸出4
}
因為定義的是子類的對象,所以會優先查找子類獨有的作用域,這裏已經找到了x,所以不會再查找父類的作用域,因此輸出的是4,如果子類裏沒有另行聲明x成員變量,那麽才會去查找父類的作用域。那麽這種情況下如果想要訪問父類的x,怎麽辦呢?
利用Base::x,可以使查找指定為父類的作用域,這樣就能返回父類的x的值了。
實例3:
//例3:函數的遮掩
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual PureVirtualFunction() = 0;
};
class Derived: public Base
{
public:
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
int main()
{
Derived d;
d.CommonFunction(); // Derived::CommonFunction()
d.VirtualFunction(); // Derived::VirtualFunction()
d.PureVirtualFunction(); // Derived::PureVirtualFunction()
return 0;
}
與變量遮掩類似,函數名的查找也是先從子類獨有的作用域開始查找的,一旦找到,就不再繼續找下去了。這裏無論是普通函數,虛函數,還是純虛函數,結果都是輸出子類的函數調用。
實例4:
//例4:重載函數的遮掩
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
void virtual PureVirtualFunction() = 0;
};
class Derived: public Base
{
public:
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
int main()
{
Derived d;
d.VirtualFunction(3); // 代碼編譯不通過,並不是調用父類的成員函數
return 0;
}
C++的確是支持重載的,編譯器在發現函數重載時,會去尋找相同函數名中最為匹配的一個函數(從形參個數,形參類型兩個方面考慮,與返回值沒有關系),如果大家的匹配程度都差不多,那麽編譯器會報歧義的錯。
因為編譯器先查找子類中獨有的域,一單發現了完全相同的函數名,就不再往父類查找,在核查函數參數時,發現了沒有帶整型形參,所以直接報編譯錯了。
只有去掉子類的virtualFunction(),才會找到父類的VirtualFunction(int).
解決上述問題:
一種采用using 聲明;
class Derived: public Base
{
public:
using Base::VirtualFunction; // 第一級查找也要包括Base::VirtualFunction
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
用了using,實際上是告訴編譯器,把父類的那個函數也納入第一批查找範圍裏面,這樣就能發現匹配得更好的重載函數了。
--------------------
一種是定義轉交函數;采用這種做法,需要在子類中再定義一個帶int參的同名函數,在這個函數裏面用Base進行作用域的指定,從而調用到父類的同名函數。
class Derived: public Base
1 { 2 public: 3 using Base::VirtualFunction; 4 void CommonFunction(){cout << "Derived::CommonFunction()" << endl;} 5 void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;} 6 void virtual PureVirtualFunction(int x){cout << "Derived::PureVirtualFunction()" << endl;} 7 void virtual VirtualFunction(int x){Base::VirtualFunction(x)};//轉交函 8 };
總結:
1. derived classses內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如此。
2. 為了讓被遮掩的名稱再見天日,可使用using聲明式或轉交函數(forwarding functions)。
-----------區分接口繼承和實現繼承------------------
只要能理解三句話即可,
第一句話是:
純虛函數只繼承接口;
第二句話是:
虛函數既繼承接口,也提供了一份默認實現;
第三句話是:
普通函數既繼承接口,也強制繼承實現。
解析:
純虛函數有一個“等於0”的聲明,具體實現一般放在派生中(但基類也可以有具體實現),所在的類(稱之為虛基類)是不能定義對象的,派生類中仍然也可以不實現這個純虛函數,交由派生類的派生類實現,總之直到有一個派生類將之實現,才可以由這個派生類定義出它的對象。
虛函數則必須有實現,否則會報鏈接錯誤。虛函數可以在基類和多個派生類中提供不同的版本,利用多態性質,在程序運行時動態決定執行哪一個版本的虛函數(機制是編譯器生成的虛表)。virtual關鍵字在基類中必須顯式指明,在派生類中不必指明,即使不寫,也會被編譯器認可為virtual函數,virtual函數存在的類可以定義實例對象。
普通函數則是將接口與實現都繼承下來了,如果在派生類中重定義普通函數,將會出現名稱的遮蓋
普通函數所代表的意義是不變性淩駕與特異性,所以它絕不該在派生類中被重新定義。
註意:
書上提倡用純虛函數去替代虛函數,因為虛函數提供了一個默認的實現,如果派生類的想要的行為與這個虛函數不一致,而又恰好忘記去覆蓋虛函數,就會出現問題。但純虛函數不會,因為它從語法上限定派生類必須要去實現它,否則將無法定義派生類的對象。
同時,因為純虛函數也是可以有默認實現的(但是它從語法上強調派生類必須重定義之,否則不能定義對象),所以完全可以替換虛函數。
總結:
1. 接口繼承和實現繼承不同。在public繼承之下,derived class總是繼承base class的接口;
2. pure virtual函數只具體指定接口繼承;
3. impure virtual函數具體指定接口繼承和缺省實現繼承;(可以重定義實現,則此時實現就會不再繼承,但是接口仍然會繼承)
4. non-virutal函數具體指定接口繼承以及強制性實現繼承。(不可以重定義在派生類)
----------考慮virtual函數意外的其他選擇--------------
Effictive C++知識點復習