文章目錄
- 01.讓自己習慣C++
- 2.構造/析構/賦值
- 05.瞭解C++默默編寫並調用哪些函數
- 06.若不適用編譯器自動生成的函數,就該明確拒絕
- 07.爲多態基類聲明virtual析構函數
- 08.別讓異常逃離析構函數
- 09.絕不在構造和析構中調用virtual
- 10. operator返回一個 reference to *this
- 11.在operator中處理"自我賦值"
- 12.複製對象時勿忘其每一個成分
- 3.資源管理
- 4.設計與聲明
- 18.讓接口容易被正確使用,不易被勿用
- 19.設計class猶如設計type
- 20.寧以pass-by-reference-to-const替換pass-by-value
- 21.必須返回對象時,別妄想返回reference
- 22.將成員變量聲明爲private
- 23.寧以non-member、non-friend替換member函數
- 24.若所有參數皆需類型轉換,請以此採用non-member函數
- 25.考慮寫出一個不拋出異常的swap函數
- 5.實現
- 26.儘可能延後變量定義式的出現時間
- 27.儘量少做類型轉換動作
- 28.避免返回handles指向對象內部成分
- 29.爲異常安全而努力是值得的
- 30.透徹瞭解inling的裏裏外外
- 31.將文件的編譯依賴關係降到最低
- 6.繼承與面向對象設計
- 32.確定你的public繼承塑模出is-a關係
- 33.避免遮掩繼承而來的名稱
- 34.區分接口繼承和實現繼承
- 35.考慮virtual函數以外的其他選擇
- 36.絕不重新定義繼承而來的non-virtual
- 37.絕不重新定義繼承而來的non-virtual function
- 38.通過複合塑模出has-a或根據某物實現出
- 39.明智而審慎地使用private繼承
- 40.明智而審慎地使用多重繼承
- 7.模板與泛型編程
- 41.瞭解隱式接口和編譯期多態
- 42.瞭解tyename的雙重含義
- 43.學習模板化基類內名稱
- 44將與參與無關的代碼抽離templates
- 45.運用成員函數模板接受所有兼容類型
- 46.需要類型轉換時請爲模板定義非成員函數
- 47.請使用 traits classes表現類型信息
- 48.認識template元編程
- 8.定製new和delete
- 9.雜項討論
- 53.不要忽略編譯期的警告
01.讓自己習慣C++
01.視C++爲一個語言聯邦
C++支持多重泛型編程語言,支持面向過程、面向對象、函數形式、泛型形式、元編程形式。
內容:
- C: 區塊、語句、預處理、內置數據類型、數組、指針等。
- 帶有類的C: ,classes(構造函數和析構函數),封裝、集成、多態、虛函數等等
- template C++:TMP,模板元編程
- STL:容器、迭代器、算法以及函數對象
C++是這四個次語言的組成
02 儘量以const,enum,inline 替換#define
寧可編譯器代替預處理
1.報錯的時候會指向宏的內容,預處理會將宏進行替換
技巧:
1.常量字符串
const char* const authorName = "xxxxx";
//聲明不變字符串
const std::string authorName("Scott Meyers");
2.class專屬常量,爲了作用域
class GamePlayer{
private :
static const int NumTurns = 9;//定義該常量
int socres[NumTurns] ; //使用常量
}
3.需要取地址需要定義式
const int GamePlayer::NumTurns;由於聲明以賦值,所以無需繼續賦值
原因:
1.寧願編譯器替換預處理
2.使用宏定義,編譯器可能盲目的替換,沒有常量產生的代碼小
3.可以控制其域
4.enum比較像#define,因爲取地址的時候也違法
5.宏的括號,及算法優先級的煩惱
要點:
- 對於單純常量,最好以const對象替換#defines
- 對於形式函數的宏,最好用inline代替宏
03.儘可能使用const
const星號左邊:被指物體常量
const星號右邊:指針自身常量
- 對單純遍歷,最好以const對象或enums替換#deines
- 對於形式函數的宏,最好用inline替換爲#define
04.確定對象被使用前已被初始化
- 變量都要初始化。
- STL默認已經幫我們初始化好了。
- 構造函數賦值的行爲不能稱之爲初始化,也不建議這麼寫
- 推薦使用初始化成員列表方式初始化
- 基類多構造函數,可以把數值初始化放到一個私有函數中
static分爲函數內static loical
static全局non-local
- non-loacl static對象聲明如果 依賴另一個non-local static對象
這樣不能明確順序是不好的,可以使用單例模式來解決這個問題
記住:
–爲內置型對象進行手工初始化,因爲C++不保證初始化他們
– 使用初始化列表初始化,不要在構造函數本體內使用賦值操作
– 爲免除“跨單元之初始化次序”,以local static 對象替換non-local static對象
2.構造/析構/賦值
05.瞭解C++默默編寫並調用哪些函數
- 默認有構造函數,析構函數,拷貝構造函數
06.若不適用編譯器自動生成的函數,就該明確拒絕
- 不適用拷貝構造
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有聲明而沒有定義
HomeForSale& operator=(const HomeForSale&);
- 爲了駁回編譯器自動(暗自)提供的機能,可以將相應的成員函數聲明爲private並且不予實現。使用像Uncopyable這樣的base class也是一種做法。(就是使用繼承的方法,讓父類私有化拷貝構造函數)
07.爲多態基類聲明virtual析構函數
-
帶多態性質的基類應該聲明一個虛析構函數,如果類帶有任何虛函數,它就應該擁有一個虛析構函數
-
類的設計目的如果不是作爲基類使用,或者不是爲了具備多態性,就不應該聲明虛析構函數。(浪費內存)
– 32位計算中將佔用64bits~96bits
– 64位佔用 64~128bits
08.別讓異常逃離析構函數
-
析構函數絕對不要吐出異常,如果一個析構函數調用的函數可能拋出異常,
析構函數應該捕捉任何異常,然後吞下它們或者結束程序。 -
如果客戶需要對某個操作函數運行期間拋出的異常做出反應,
那麼class應該提供一個普通函數(而非在析構函數中)執行該操作。
09.絕不在構造和析構中調用virtual
eg:構造中父類調用了虛函數的print,會打印父類print
析構函數中,先析構子類後析構父類,所以調用虛函數,也會打印父類print函數
C++ 構造和析構禁止調用virtual
10. operator返回一個 reference to *this
int x,y,z;
x=y=z=15
將上述轉換爲下:
x=(y=(z=15))
推薦代碼如下:
適用於+=,-=,*=,等等
Widget& operator=(const Widget&rhs)
{
...
return *this
...
}
11.在operator中處理"自我賦值"
可能導致指針被提前釋放的問題
//刪除this.pb意味着rhs.pb也可能是個野指針,代碼會產生問題
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*(rhs.pb));
return *this;
}
解決方案
Widget& operator=(const Widget&rhs)
{
if(this == &rhs)
{
return *this
}
delete pb;
pb = new Bitmap(*(rhs.pb))
return *this;
}
- 確保當對象自我賦值時 operaotr=有良好的行爲,處理好源對象和目標對象的處理順序,以及 copy and swap
- 確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象是,其還是正確。
12.複製對象時勿忘其每一個成分
eg:
class Defau {
public:
int i = 10;
};
class A {
public:
A() = default;
A(const A&rhs):name(rhs.name),u(rhs.u){}
A& operator = (const A&rhs) {
this->name = rhs.name;
this->u = rhs.u;
return *this;
}
string name;
Defau u;
};
class B : public A {
public:
B():prio(3){}
int prio;
B(const B&ur):prio(ur.prio),A(ur){}
B& operator =(const B&ur) {
prio = ur.prio;
A::operator=(ur);
return *this;
}
};
int main() {
B b;
b.u.i = 15;
b.prio = 6;
b.name = "dewdqw";
B c(b);
cout << c.u.i << endl;
cout << c.prio << endl;
cout << c.name;
}
Copying函數應該確保複製“對象內的所有成員變量”及“所有base class成分”。
不要嘗試以某個copying函數實現另一個copying函數。應該將共同機能放進第三個函數中,並由兩個copying函數共同調用。
3.資源管理
13.以對象管理資源
– 使用 auto_ptr /share_ptr
-
爲防止資源泄漏,請使用RAII對象,它們在構造函數中獲得資源並在析構函數中釋放資源。
-
兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr,前者通常是較佳選擇,因爲其copy行爲比較直觀。若選擇auto_ptr,複製動作會使它(被指物)指向null。
14.在資源管理類中小心coping行爲
RAII(資源獲得即是初始化)
-
複製RAII對象必須一併複製它所管理的資源,所以資源copying行爲決定RAII對象的copying行爲
-
普遍而常見的RAII class copying行爲是:抑制copying,施行引用計數法(shared_ptr思想),或者是轉移底部資源(auto_ptr思想)
15.在資源管理類中提供對原始資源的訪問
-
1、APIs往往要求訪問原始資源( raw resources),所以每一個 RAII class 應該提供一個“取得其所管理之資源”的辦法。
-
2、對原始資源的訪問可能經由顯式轉換或隱式轉換。一般而言顯式轉換比較安全, 但隱式轉換對客戶比較方便。
eg:顯示轉換
#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class myClass
{
public :
myClass()
{
cout<<"myClass()"<<endl;
}
~myClass()
{
cout<<"~myClass()"<<endl;
}
void printFunc()
{
cout<<"printFunc()"<<endl;
}
int getType()const
{
return type;
}
private:
int type;
};
myClass* creatMyClass()
{
myClass *my=new myClass();
return my;
}
void issue(myClass *my) {
delete my;
}
int _tmain(int argc, _TCHAR* argv[])
{
auto_ptr<myClass> apMy(creatMyClass());
myClass* myC=apMy.get();//////auto_ptr 給我們提供的函數,用來訪問原始資源
myC->printFunc();//////調用了myClass的方法
return 0;
}
eg:隱式轉換
通過函數把share_ptr/auto_ptr,轉出一個目標值
16.成對使用new和delete時要採取 相同形式
- 1.如果在new表達式中使用[],必須在相應的delete表達式中使用[]。如果
在new表達式中不使用[],一定不要再相應的delete表達式中使用[]。 - 2.new一個對象會有兩個行爲,第一個是內存被分配出來,第二是針對此內存會有一個
或多個構造函數被調用。 - 3.delete一個對象也會有兩個行爲,第一個是針對此內存會有一個或多個析構函數被調用,
17.以獨立語句將newed對象置入智能指針
以獨立語句將newed對象存儲於(置入)智能指針內。如果不這樣做,一旦異常拋出
有可能導致難以察覺的資源泄漏。
processWidget(std::trl::shared_ptr<Widget> (new Widget), property());
①執行new Widget
②調用proprety()函數
③調用tr1::shared_ptr的構造函數
那麼proprety()可以在第一個調用,可以第二個調用,也可以第三個調用。
如果函數異常了,new Widget的指針沒放入share_ptr,這樣就泄漏了.
4.設計與聲明
18.讓接口容易被正確使用,不易被勿用
struct Day {
explicit Day(int d) : val(d) { }
int val;
};
struct Month {
explicit Month(int m) : val(m) { }
int val;
};
struct Year {
explicit Year(int y) : val(y) { }
int val;
};
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); //錯誤的類型!
Date d(Day(30), Month(3), Year(1995)); //錯誤的類型!
Date d(Month(3), Day(30), Year(1995)); //正確了!
eg:createInvestment使它返回一個tr1::shared_ptr並夾帶getRidOfInvestment函數作爲刪除器
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
retVal = ...; //令retVal指向正確的對象
return retVal;
}
tr1::shared_ptr的一個優秀的性質是:
它會自動的使用它的“每個指針專屬的刪除器”
這樣的性質消除了一個潛在的可能錯誤:
“cross-DLL problem”
這個問題發生於“對象在動態鏈接庫(DLL)中被創建,但卻在另一個DLL內被delete銷燬”。在一些平臺上,這一類“跨DLL的new/delete成對運用”會導致運行期間錯誤。
然而tr1::shared_ptr就沒有這個問題,因爲它默認的刪除器是來自“tr1::shared_ptr誕生所在的那個那個DLL”的delete。舉個例子來說,如果Stock派生自Investment,而createInvestment的實現如下:
std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
返回的那個tr1::shared_ptr可被傳遞給任何其他的DLLs,無需在意“cross-DLL problem”。這個指向Stock的tr1::shared_ptr會追蹤記錄“當Stock的引用次數變成0時該調用的那個DLL’s delete”。
- 1,好的接口很容易被正確使用,不容易被誤用。應該在實現的所有接口中努力達成這些性質。
- 2,“促進正常使用”的辦法包括接口的一致性,以及與內置類型的行爲兼容。
- 3,“阻止誤用”的辦法包括建立新類型、限制類型上的操作,束縛對象值,以及消除用戶的資源管理責任。
- 4,tr1::shared_ptr支持定製型刪除器(custom deleter)。這可防範DLL問題,可被用來自動解鎖互斥鎖(mutexes)等等。
19.設計class猶如設計type
靈魂十二問啊!!
-
1.新的type對象該如何被創建銷燬? 這會影響到class的構造函數和析構函數以及內存的分配函數和釋放函數的設計。
-
2.對象初始化和對象賦值應該有什麼差別不要混淆初始化和賦值,這將決定構造函數和賦值操作符的行爲。
-
3.新type對象如果被值傳遞,這將意味着什麼? copy構造函數用來定義一個type的值傳遞應該如何實現。
-
4.什麼是新type的合法值? 對於成員變量而言,只有有些數值集是有效的。
-
5.你的新type需要配合繼承圖系嗎?
-
繼承:受到基類的束縛
-
被繼承:影響你所聲明的函數——尤其是析構函數——是否爲virtual。
-
6. 你的新type需要什麼樣的轉換?
-
7.什麼樣的操作符核函數對此新的type是合理的? 這個問題將決定你的class聲明哪些函數
-
8.什麼樣的標準函數應該被駁回 這些正是你必須聲明爲private的
-
9.誰改取用新type的成員? 這些幫助決定那些成員爲public,哪些爲protedted,哪些爲private。也幫助決定哪一個class和/或function應該是友元,以及將他們嵌套到另一個之內是否合理。
-
10.什麼是type的“未聲明接口”? 他對效率、異常安全性以及資源運用提供何種保證?你講在這些方面提供的保證將爲你的class實現代碼加上相應的約束條件
-
11.你的type有多麼一般化 或許你並非定義了一個新type,而是定義了一整個types家族。如果果真如此,你就不應該定義一個新的class,而是定義一個新的class template(模板類)
-
12.你真的需要一個新type嗎? 如果只是定義一個新的子類以便爲既有的class添加機能,說不定單純定義一個或多個non-number函數或者templates更能夠達到目標。
20.寧以pass-by-reference-to-const替換pass-by-value
儘量傳引用,效率高。不需要修改的 引用就給個const
不包含STL的迭代器和函數對象
- 1,儘量以pass-by-reference-to-const替換pass-by-value。前者通常比較高效,並可以避免切割問題(slicing problem)。
- 2,以上的規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value往往比較適當。
21.必須返回對象時,別妄想返回reference
該錯誤嘗試過了,不在解釋。新手易犯錯誤
絕不要返回pointer或reference指向一個local stack對象,或返回reference指向一個heap-allocated對象,或返回pointer指向一個local static對象而有可能同時需要多個這樣的對象。
22.將成員變量聲明爲private
- 1,切記將成員變量聲明爲private。這可以賦予客戶訪問數據的一致性、可細微劃分訪問控制、允諾約束條件獲得保證,並提供class作者以充分的實現彈性。
- 2,protected並不比public根據封裝性。
一旦你將一個成員變量聲明爲public或protected而用戶開始使用它,就很難改變這個變量所涉及的一切!總結說來,其實只有兩種訪問權限:
private(提供封裝)
其他(不提供任何封裝)
23.寧以non-member、non-friend替換member函數
封裝性優先於類的內聚邏輯
採用namespace可以對內聚性進行良好的折中。
“愈多東西被封裝,愈少人可以看到它,而愈少人看到它,我們就有愈大的彈性去改變它,因爲我們的改變僅僅影響看到改變的那些人或事物”
code:
void ClearWebBrowser(WebBrower& w)
{
w.ClearCach();
w.ClearHistory();
w.RemoveCookies();
}
- 儘量用non-member non-friend函數替換member函數。可以增加封裝性、包裹彈性(packaging flexibility)、和機能擴充性。
24.若所有參數皆需類型轉換,請以此採用non-member函數
https://www.bbsmax.com/A/lk5aEoE051/
不太理解
25.考慮寫出一個不拋出異常的swap函數
- todo
5.實現
26.儘可能延後變量定義式的出現時間
- 儘可能延後變量定義式。這樣做可增加程序清晰度和改善效率。
- 用到變量在定義就行,防止沒有必要的生命成本
27.儘量少做類型轉換動作
C風格的轉型動作:
(T)expression
函數風格的轉型動作:
T(expression)
C++ 提供四種新類型轉換
- const_cast (expression)
-
- 通常是將對象的長良性消除,是唯一有此能力的C++ style轉型操作符
//1. 指針指向類
const A *pca1 = new A;
A *pa2 = const_cast<A*>(pca1); //常量對象轉換爲非常量對象
pa2->m_iNum = 200; //fine
//2. 指針指向基本類型
const int ica = 100;
int * ia = const_cast<int *>(&ica);
*ia = 200;
3.常量引用轉爲非常量引用
A a0;
const A &a1 = a0;
A a2 = const_cast<A&>(a1); //常量引用轉爲非常量引用
//常量對象被轉換成非常量對象時出錯
const A ca;
A a = const_cast<A>(ca); //不允許
const int i = 100;
int j = const_cast<int>(i); //不允許
const int a = 1;//允許
int * b = const_cast<int*>(&a);
*b = 2;
const int a = 1;//允許
int & b = const_cast<int&>(a);
b = 2;
網上看到的一個比較好的代碼
const string& shorter(const string& s1, const string& s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
string& shorter(string& s1, string& s2) {
//重載調用到上一個函數,它已經寫好了比較的邏輯
auto &r = shorter(const_cast<const string&>(s1), const_cast<const string&>(s2));
//auto等號右邊爲引用,類型會忽略掉引用
return const_cast<string&>(r);
}
- dynamic_cast(expression)
-
- 是將一個基類對象指針(或引用)轉換到繼承類指針,dynamic_cast會根據基類指針是否真正指向繼承類指針來做相應處理。(基類轉子類)
-
- 前提條件:當我們將dynamic_cast用於某種類型的指針或引用時,只有該類型含有虛函數時,才能進行這種轉換。否則,編譯器會報錯。
dynamic_cast運算符的調用形式如下所示:
- 前提條件:當我們將dynamic_cast用於某種類型的指針或引用時,只有該類型含有虛函數時,才能進行這種轉換。否則,編譯器會報錯。
dynamic_cast<type*>(e) //e是指針
dynamic_cast<type&>(e) //e是左值
dynamic_cast<type&&>(e)//e是右值
e能成功轉換爲type*類型的情況有三種:
-
1)e的類型是目標type的公有派生類:派生類向基類轉換一定會成功。
-
2)e的類型是目標type的基類,當e是指針指向派生類對象,或者基類引用引用派生類對象時,類型轉換纔會成功,當e指向基類對象,試圖轉換爲派生類對象時,轉換失敗。
-
3)e的類型就是type的類型時,一定會轉換成功。
如果一條dynamic_cast語句的轉換目標是指針類型並且轉換失敗了,會返回一個空指針,則判斷條件爲0,即爲false;如果轉換成功,指針爲非空,則判斷條件爲非零,即true。
補充:dynamic_cast 有個普通版本來說,他會調用strcmp,用以比較class名稱,所以效率較低
-
reinterpret_cast(expression)
T必須是一個指針、引用、算術類型、函數指針或者成員指針。它可以把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針(先把一個指針轉換成一個整數,再把該整數轉換成原類型的指針,還可以得到原先的指針值)。之前一篇博客介紹的這個用法,使用不是很多,點擊前往
參考代碼,不好用的東西,慎用吧。reinterpret_cast體現了 C++ 語言的設計思想:用戶可以做任何操作,但要爲自己的行爲負責。
#include <iostream>
using namespace std;
class A
{
public:
int i;
int j;
A(int n):i(n),j(n) { }
};
int main()
{
A a(100);
int &r = reinterpret_cast<int&>(a); //強行讓 r 引用 a
r = 200; //把 a.i 變成了 200
cout << a.i << "," << a.j << endl; // 輸出 200,100
int n = 300;
A *pa = reinterpret_cast<A*> ( & n); //強行讓 pa 指向 n
pa->i = 400; // n 變成 400
pa->j = 500; //此條語句不安全,很可能導致程序崩潰
cout << n << endl; // 輸出 400
long long la = 0x12345678abcdLL;
pa = reinterpret_cast<A*>(la); //la太長,只取低32位0x5678abcd拷貝給pa
unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐個比特拷貝到u
cout << hex << u << endl; //輸出 5678abcd
typedef void (* PF1) (int);
typedef int (* PF2) (int,char *);
PF1 pf1; PF2 pf2;
pf2 = reinterpret_cast<PF2>(pf1); //兩個不同類型的函數指針之間可以互相轉換
}
- static_cast(expression)
①用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換。
進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;
進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。
②用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。
③把空指針轉換成目標類型的空指針。
④把任何類型的表達式轉換成void類型。
可以將non_const轉爲const,相反則不能(相反使用const_cast)
小結:
- 如果可以,儘量避免轉型,特別是注重效率的代碼避免使用dynamiy_cast
- 如果轉型是必要的,試着將它隱藏域某個函數背後,客戶隨後可以調用該函數,而不需將轉型放進他們代碼內
- 寧可使用C++ style(新式)轉型,不要使用舊式轉型。
28.避免返回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{
...
Point& upperLeft()const {return pData->ulhc;}
Point& lowerRight()const {return pData->lrhc;}
private:
std::tr1::shared_ptr<RectData> pData;
};
上述代碼是不好的,因爲客戶可以通過upperLeft或是lowerRight返回一個內部的引用,進而修改了內部值
使用如下是更好的
class Rectangle{
...
const Point& upperLeft()const {return pData->ulhc;}
const Point& lowerRight()const {return pData->lrhc;}
private:
std::tr1::shared_ptr<RectData> pData;
};
第二個例子
class GUIObject{...};
const Rectangle boundingBox(const GUIObject& obj);
GUIObject *pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());//取得一個指針指向外框左上點
boundingBox(*pgo).函數調用結束後會析構Rectangle ,導致代碼產生問題。
避免返回 handle(包括 reference、指針、迭代器)指向對象內部。遵守這個條款可增加封裝性,幫助 const 成員函數的行爲像個 const,並將發生“虛吊號碼牌”的可能性降至最低。
29.爲異常安全而努力是值得的
30.透徹瞭解inling的裏裏外外
inline的代碼會提高效率,編譯器會最優化,但也可能會導致程序體檢太大
class Person
{
public:
int age() const{return theAge;} //一個隱喻的inline申請
private:
int theAge;
}
- 將template 生命爲inline,所有的具現化才都是inlined,否則不是。
- 虛函數inlinehi沒有作用的
- 編譯器會把inline函數copy多份,這也是爲啥會程序會變大
- 如果f是程序庫內的inline函數,客戶修改了inline函數,所有用到inline的都會重新編譯
那麼如果不是inline函數,只需要重連接一下。
請記住:
- 將大多數inline限制在小型、被頻繁調用的函數身上。這可使日後的調試過程和二進制
升級更容易,也可以使潛在的代碼膨脹問題最小化,使程序的速度提升機會最大化。 -
- 不要只因爲function template出現在文件,就將它inline
31.將文件的編譯依賴關係降到最低
6.繼承與面向對象設計
32.確定你的public繼承塑模出is-a關係
public意味着,基類的事情也一定適用於子類
33.避免遮掩繼承而來的名稱
34.區分接口繼承和實現繼承
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived : public Base {
public:
virtual void mf1();
void mf3();
void mf4();
Derived d;
int x;
d.mf1();// ok 調用 Derived::mf1
d.mf1(x);//error Derived::mf1掩蓋了Base::mf1
d.mf2();// ok 調用Base::mf2()
d.mf3();//ok 調用Derived::mf3()
d.mf3(x);//error Derived::mf3遮掩了 Base::mf3
1.派生類內的名稱會遮掩基類內的名稱。
2.可以使用using 聲明式或者轉交函數。
-純虛函數只繼承接口
-虛函數既繼承接口,也提供了一份默認實現;調用基類虛函數使用Base::fun()
-普通函數既繼承接口,也強制繼承實現。這裏假定討論的成員函數都是public的。
NVI:該設計是令客戶通過public non-virtual成員函數間接調用private virtual函數,稱爲non-virtual interface(NVI)手法。
它是模板方法設計模式的一個獨特表示;相當對virtual函數進行一層的包裝,可以稱爲是virtual函數的外覆器它是模板方法設計模式的一個獨特表示;相當對virtual函數進行一層的包裝,可以稱爲是virtual函數的外覆器(warpper).
non-virtual函數,採用就近調用原則。virtual函數系動態綁定,而缺省參數值卻是靜態綁定。
Base* ps = new Derived;ps->func(defaultParam);ps的靜態類型就是Base*,而動態類型則是Derived*。
35.考慮virtual函數以外的其他選擇
36.絕不重新定義繼承而來的non-virtual
37.絕不重新定義繼承而來的non-virtual function
38.通過複合塑模出has-a或根據某物實現出
39.明智而審慎地使用private繼承
private繼承根據某物實現
40.明智而審慎地使用多重繼承
多重繼承可能導致歧義性,帶有virtual會增加成本。可以做虛接口繼承類或是private繼承
7.模板與泛型編程
41.瞭解隱式接口和編譯期多態
對於 classes(類),interfaces(接口)是 explicit(顯式)的並以 function signatures(函數識別特徵)爲中心的。polymorphism(多態性)通過 virtual functions(虛擬函數)出現在運行期。
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other); // see Item 25
...
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
Templates及泛型編程,包含顯示接口和運行期多態。也包含了
隱式接口和編譯器多態。
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
T的隱式接口似乎有這些約束
1、它必須提供一個size函數,該函數返回一個整數值。
2、必須支持一個operator!= 函數,用於比較兩個T對象。這裏我們假設someNastyWidget類型爲T
從上述代碼我們可以知道:
1、w支持哪種接口,由模版執行於w上的操作決定。如本例中w必須支持size,normalize,swap,copy構造、不等比較等(雖然不完全正確)。這組表態式(對該模板而言必須有效編譯)是T必須支持的一組隱式接口。
2、涉及w的調用,可能實例化模板。“以不同的模板參數實例化函數模版”會導致不同的函數調用。這就是所謂的編譯期多態。
類似於“哪一個重載函數被調用”(發生在編譯期)和“哪一個虛函數被綁定”(運行期)。
需要記住的:
1、類和模版都支持接口和多態。
2、類接口是顯示的,以函數聲明爲中心。多態通過虛函數發生於運行期。
3、對模版參數而言,接口是隱式的,基於有效表達式。多態通過模版實例化和函數重載解析,發生於編譯期。
42.瞭解tyename的雙重含義
- 在template的聲明式中,typename的使用和class完全相同,即以下兩種聲明方式完全相同
template<typename T>
template<class T>
然而typename還有其他用途:指明嵌套從屬類型名稱.
template <typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
嵌套從屬名稱,可能會導致解析困難,出現二義性。比如:C::const_iterator* x; 有兩種意思。
一是:模版形參C中有個靜態字段const_iterator,然後計算它與x的乘積;
二是:模版形參C中有個嵌套類型const_iterator,定義指向它的指針。默認情況下,C++編譯器按第一種意思解釋,也就是說,把它當成靜態字段,而不是類型。如果我想告訴編譯器,這是個嵌套類型,該怎麼辦?使用typename C::const_iterator* x;
請記住
- 聲明template參數時,前綴關鍵字class和typename可互換
請使用typename標識嵌套從屬類型的名稱;但不得在base class lists(基類列)或 - member initalization list(成員初值列)內以它作爲base標識符
43.學習模板化基類內名稱
44將與參與無關的代碼抽離templates
45.運用成員函數模板接受所有兼容類型
46.需要類型轉換時請爲模板定義非成員函數
47.請使用 traits classes表現類型信息
48.認識template元編程
X
8.定製new和delete
49.瞭解new-handler的行爲
operator new無法滿足內存申請時,它會不斷調用new-handler函數,直到找到足夠內存。
標準函數如下:
namespace std
{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw;
}
eg
void outOfMen()
{
std::cerr<<"unable tosatisfy requesest for memeory \n"
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int *p = new int[100000000000L]
}
設計一個new-handler函數必須做以下事情:
1.讓更多內存可以被使用,以便於下一次new可以分配內存
2.set_new_handler傳遞一個可以替換原有的函數指針,以便內存申請
3.set_new_handler傳遞空指針,這樣以便於拋出異常
4.拋出bad_alloc的異常,會在存儲所求處拋出異常
5.不返回,通常調用abort或exit
50.瞭解new和delete的合理替換時機
51.編寫new和delete時固守常規
52.寫了placement new 也要寫placement delete
9.雜項討論
53.不要忽略編譯期的警告
class B {
public:
virtual void f() const;
};
class D: public B {
public:
virtual void f();
};
以上代碼會有警告,原因參考50
1、嚴肅對待編譯器發出的警告信息,因爲編譯器作者對所發生的事情有更好的領悟,儘量爭取“無任何警告”。
2、不要過度依賴編譯器的警告,因爲不同編譯器對待事情的態度不同。
54.讓自己熟悉包括TR1在內的標準程序庫
C++ 98標準:STL、Iostreams、國際化支持、數值處理、異常階層體系、C89標準程序庫。
TR1 14個新組件:智能指針(shared_ptr和tr1:weak_ptr)、tr1:function、tr1::bind、Hash tables/
正則表達式、Tuples、tr1::array、tr1::men_fn、reference_wrapper、隨機數、數學特殊函數、C99兼容擴展
第二組TR1:Type traits、tr1::result_of.
請記住
1、C++標準程序的主要機能是由STL、iostreams、locales組成,幷包含C99標準程序庫;
2、TR1添加了諸如智能指針、一般化函數指針、哈希容器、正則表達式及其他10個組件;
3、TR1只是一個規範,爲了熟悉此規範你必須熟悉源碼,Boost是個不錯的源碼庫。
55.讓自己熟悉Boost
Boost很多的特性都可能會加入C++標準,Boost
包含 文字字符串與文本處理、容器、函數對象和高級編程、泛型編程、模板元編程
等等
1、Boost是一個社羣,一個網站。它致力於免費、源碼開放、同僚複審的C++程序開發。Boost在C++標準化過程中扮演了重要的角色。
2、Boost提供了許多TR1組件實現品,以及其他許多程序庫。