Item26:Postpone variable definitions as long as possible.盡可能延后變量定義的出現時間
這里所說的變量是variable of type,它需要調用constructor和destructor。
簡單情況下,如果函數拋出異常,導致變量沒有被使用到,但是由于已經定義過,其實還是需要調用它的constructor和destructor。
std::string encryptPassword(const std::string& password)
{
…
std::string encrypted; //這是錯誤的寫法,這個定義已經需要調用構造函數了。
encrypted = password;
encrypt(encrypted);
return encrypted;
}
正確的寫法:
std::string encryptPassword(const std::string& password)
{
…
std::string encrypted(password); //正確的寫法。
encrypt(encrypted);
return encrypted;
}
postpone有兩層含義:
1 postpone a variable’s definition until right before you have to use the variable。
2 postpone the definition until you have initialization arguments for it.
循環怎么辦?
最常見的寫法一種是在循環體里面定義變量,另一種是在循環體執行前定義一個變量。
|
A
|
B
|
approach
|
Widget w;
for(int i=0; i<n;++i)
{
w=取決于i的某個值;
…
}
|
for(int i=0; i<n;++i)
{
Widget w(取決于i的某個值);
…
}
|
開銷
|
1 constructor + 1 destructor + n assignment
if assignment小于 constructor-destructor對,這種寫法比較有效
|
n constructor + n destructor
|
defect
|
這種方法定義的變量在較大的范圍都是可見的,不好。
|
|
結論:除非你明確知道approach A的開銷小于approach B,否則盡量使用approach B。
Item 27:Minimize casting盡量少做轉型動作
C++提供的4中cast形式
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
盡量使用這些新風格的轉型,最起碼的它好認啊。
cast,到底做了什么?
基類和派生類的指針有時是不相等的,存在一個偏移量offset。single object might have more than one address。為啥說有時呢,因為這取決于Compiler。所以不能想當然在基類指針上加個值就得到什么。
dynamic_cast
cast并不是啥都沒有做的,它會創建一個臨時的基類的copy,然后調用的是這個臨時類的方法,和當前被cast類無關哦。
同時多級的繼承關系里面cast也要通過字符串比較來確定匹配的基類,因此如果對性能有要求,就盡量少使用cast。
解決方法
有兩種:
1 使用type-safe的容器
這種方法的缺陷是容器里面只能允許有1種類型的指針
Example見P121
2 在基類里面定義虛函數
class Window{
public:
virtual void blink();
…
};
class SpecialWindow : public Window {
public:
virtual void blink();
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
…
for(VPW::iterator iter=winPtrs.begin(); iter!=winPtrs.end(); ++iter)
(*iter).blink(); //注意這里就不需要dynamic_cast
Item 28:Avoid returning "handles" to object internals.避免返回handles指向對象內部成分 123
handles
handles可以理解為句柄,或者說是一個“把柄”,握著這個“把柄”,你可以修改對象的數據成員。
handle包括reference,pointer,iterator。
問題出現的背景
很簡單,就是看上去函數返回的是一個const對象,但是由于是handle,導致數據還是可以被client修改的。這種情況,debug時很難發現,而且給人的感覺是系統不穩定,后果很嚴重,不可輕視。
struct rectData {
Point ulhc;
Point lrhc;
};
class Rectangle {
public:
…
Point& upperLeft() const {return pData->ulhc; }
private:
std::tr1::shared_ptr<RecData> pData;
};
const member function:不能修改數據成員。
client調用:
const Rectangle rec(Point (0,0), Point(100,100));
rec.upperLeft().setX(50);//坐標就這么不知不覺中被修改了
結論
避免返回指向對象內部的handles,這種handles包括:pointer,reference,iterator。
如果某個函數的訪問級別低,禁止其它成員函數返回指向這個函數的指針。因為如果這樣,就可以通過函數指針來調用這個函數了。
Item 29:Strive for exception-safe code.為“異常安全”而努力是值得的 127
class PrettyMenu{
public:
void PrettyMenu::changeBackgroud(std::istream& imgSrc) ;
private:
Mutex mutex;
int imageChanges;
Image * bgImage;
};
void PrettyMenu::changeBackgroud(std::istream& imgSrc)
{
Lock m1(&mutex); //Item13,對象管理資源的典型應用
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
異常安全性函數應該提供以下3種保證之一:
基本保證
如果異常拋出,程序里所有的東西都保持有效的狀態。
增強保證
如果異常拋出,程序的狀態保持不變。
不拋出異常保證
決不拋出異常。因為總能夠完成希望要做的事情。
這同時也是一種理想狀態。
一般情況下,我們的選擇是在basic和strong異常保證之間。
重新寫changeBackgroud,提供basic異常保證:
class PrettyMenu{
…
std::tr1::shared_ptr<Image> bgImage;
…
};
void PrettyMenu::changeBackgroud(std::istream& imgSrc)
{
Lock m1(&mutex); //Item13,對象管理資源的典型應用
bgImage.reset(new Image(imgSrc) );
++imageChanges;
}
strong異常保證的設計策略說出來也很簡單,就是copy and swap。具體描述見P131 L5開始的描述。
strong異常保證
基本思路就是copy and swap。
struct PMImpl {
std::tr1::shared_prt<Image> bgImage;
int imageChanges;
};
class PrettyMenu{
…
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackgroud(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex); //Item13,對象管理資源的典型應用
std::tr1:: shared_prt< PMImpl > pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); //修改副本
++pNew-> imageChanges;
swap(pImpl, pNew); //置換
}
trade-off折中考慮異常保證
并不是一定要提供strong異常保證就是好的,這里是需要一個trade-off。strong異常保證的代價一定是很大的,efficiency,complexity都是要考慮的因素。
同時如果函數中調用了沒有異常保證的其它函數,這個函數也不能是異常保證的。
void someFunc()
{
…
f1();
f2();
…
}
這引申出一個結論:如果代碼中調用了沒有異常保證的C的傳統代碼,這樣的函數,就大可不必還要提供strong的異常保證了
Item 30:Understand the ins and outs of inlining.透徹了解inlining的里里外外 134
什么是內聯函數inline
將函數指定為 inline 函數,(通常)就是將它在程序中每個調用點上“內聯地”展開。函數本體Function body是不存在的。
inline是對Compiler的請求,而不是對命令的。
The inline specification is only a request to the compiler. The compiler may choose to ignore this request.
一般來說,內聯機制適用于優化小的、只有幾行的而且經常被調用的函數。
大多數的編譯器都不支持遞歸函數的內聯。
復雜函數也是不可以的,一個 1200 行的函數也不太可能在調用點內聯展開。
虛函數也是不可以的。
inline與函數模板Function Template
template<typename T>
inline const T& std::max(const T& a, const T& b)
{ return a<b?b:a; }
inline函數大多數都是定義在頭文件了,這是因為編譯器必須知道inline函數是什么樣子。
Template也是定義在頭文件里,因為Compiler需要知道Template是什么樣子,為了初始化Template。
Template獨立于inline。這是不同的概念,當然函數模板也可以聲明成inline。
函數指針和內聯函數
函數指針指向的內聯函數一定不會被inline,Compiler會生成這個函數的本體function body,函數指針就是指向的這個函數本體。
inline void f() {…}
void (*pf)() = f;
…
f();
pf();//這個調用就不會是inline,因為這是一個函數指針。
構造函數、析構函數和內聯函數
構造函數和析構函數不要做內聯。
簡單的說,如果base constructor內聯,派生類中從基類繼承來的data member被初始化2次,一次是來自內聯,一次來自C++執行基類的構造函數。
同理,析構函數,也就會被釋放2次L慘了,但是實際上,只有一個對象啊。不死等什么。
內聯函數修改的影響
內聯函數的機制決定調用內聯函數,就是把代碼插入調用的位置,因此如果內聯函數修改了,所有調用該函數的地方都需要重新編譯。如果這個函數不是內聯的,修改后,只要重新鏈接就可以了。
記住
inline僅僅適用于小型的,被頻繁調用的函數。
不要因為函數模板定義在頭文件里就把它聲明成incline,Function Template和inline這是2個不同的概念。
Item 31:Minimize compilation dependencies between files.將文件間的編譯依存關系降至最低 140
前向聲明forward declaration
1. 首先前向聲明僅僅是聲明了一個類
class Date; //forward declaration
class Address;
2. 其次前向聲明是一種不完全類型
只能以有限的方式使用。只能用于定義指向該類型的指針,或者用于聲明(而不是定義)使用該類型作為形參類型或返回類型的函數。不能定義該類型的對象。
pimpl:pointer to implementation
#include <string>
#include <memory>
class PersonImpl;
class date;
class Address;
class Person {
public:
Person(const std::string& name, const date& birthday, const Address& addr);
std::string name() const;
std::string birthday() const;
std::string address() const;
…
private:
std::tr1::shared_ptr<PersonImpl> pImpl; //point to implementation
};
基本解決之道
replacement of dependencies on definitions with dependencies on declarations.以聲明的依賴性替換定義的依賴性
盡量讓頭文件做到自包含,萬一做不到,則讓它和其它文件內的聲明相互依賴,而不是定義。
規則
如果使用對象引用活對象指針可以解決問題,就避免使用對象,
取代類定義,依賴于類聲明
頭文件要成對出現,一個是聲明,一個是定義。
兩種具體的操作方法
Handle Classes
定義Handle class
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const=0;
std::string birthday() const=0;
std::string address() const=0;
…
private:
std::tr1:shared_ptr<PersonImpl> pImpl;
};
std::string Person::name() const
{
return pImpl->name();
}
定義實現類PersonImpl
Handle Classes
這里給出的Handle Classes的定義太精辟了。喜歡J
Classes like Person that employ the pimpl idiom are often called Handle classes.
Making Person a Handle class doesn’t change what Person does, it just changes the way it does it.
Interface Classes
定義接口
class Person {
public:
virtual ~Person();
virtual std::string name() const=0;
virtual std::string birthday() const=0;
virtual std::string address() const=0;
…
static std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr );
};
定義實現
class RealPerson:public Person {
public:
realPerson(const std::string& name, std::string& birthday, const Address& addr)
:theName(name),theBirthdate(birthday), theAddress(addr)
{}
virtual ~RealPerson()
std::string name() const;
std::string birthday () const;
std::string address () const;
private:
std::string theName;
date theBirdate;
Address theAddress;
};
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr )
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,addr));
}
結論
減少耦合行的慣用的思路是用聲明依賴替代定義依賴。有2種方法:Interface classes和Handle classes.
任何減少耦合的做法都是會帶來負面影響的:占用內存增加,間接調用。但是關鍵還是在于引用本身是不是對這些額外的開銷敏感。