Effective C++條款27:實現——儘量少做轉型動作
阿新 • • 發佈:2021-02-07
技術標籤:effective c++c++
一、資料型別轉換語法
C語言風格的型別轉換格式
C++新式轉型
- const_cast、dynamic_cast、reinterpret_cast、static_cast
- 詳細介紹參閱:https://blog.csdn.net/qq_41453285/article/details/89187875
二、建議使用C++新式轉型
- 舊式轉型雖然雖然合法,但是程式設計時應該儘量使用新式轉型。原因如下:
- ①新式轉型在程式碼中很容易被識別出來
- ②新式轉型使轉型動作的目標比較有限。例如你打算將常量性移除,那麼只能只用const_cast
舊式轉型的使用
- 唯一使用舊式轉型的場景就是,呼叫一個explicit建構函式將一個物件傳遞給一個函式時
- 例如:
class Widget { public: explicit Widget(int size); ... }; void doSomeWork(const Widget& w); doSomeWork(Widget(15)); //函式風格 doSomeWidget(static_cast<Widget>(15)); //c++風格
三、對轉型的一種誤解
- 很多人認為轉型其實什麼都沒做,只是告訴編譯器把某種型別視為另一種型別而已,其實這是錯誤的觀念
- 任何一種型別轉換(不論是顯式還是隱式,或是編譯器自身完成的隱式轉換),編譯器在編譯出執行期間執行的程式碼時都要做相應的改變
演示案例
- 下面程式碼將int型別轉換為double型別,x的儲存結構肯定會改變
int x, y; double d = static_cast<double>(x) / y;
演示案例
- 上面將派生類轉換為基類,但有時上述兩個指標值並不相同。這時候會有個偏移量在執行期被施行於Derived*指標身上,用以取得正確的Base*指標值
class Base {}; class Derived :public Base {}; int main() { Derived d; Base* pb = &d; //派生類轉換為基類 return 0; }
四、關於轉型的一個錯誤演示案例
- 例如許多應用框架都要求在派生類的virtual函式中第一個動作就是先呼叫基類的對應函式。例如下面我們有一個Windows類以及它的派生SpecialWindow,其中onResize()是虛擬函式
class Window {
public:
virtual void onResize() {}
};
class SpecialWindows :public Window {
public:
virtual void onResize() {
//嘗試呼叫基類的虛擬函式,語法雖然沒錯,但是邏輯是錯誤的(程式會產生不明確行為)
static_cast<Window>(*this).onResize();
}
};
- 為什麼上面的型別轉換是錯誤的:
- 我們在函式中將*this轉換為基類型別,然後嘗試呼叫onResize()虛擬函式,但是該轉型動作產生的實際上是一個“this物件之基類成分”的一個副本,然後在這個副本上呼叫onResize函式
- 所以當在副本上面呼叫onResize()函式對於this物件根本沒有任何的影響,於是客戶端程式以為呼叫了onResize()函式使物件本身改變了,但是實際上沒有改變,只是改變了一個臨時物件而已
- 替代方案:就是在虛擬函式中顯式的呼叫基類的函式。例如將上面派生類的虛擬函式更改為下面的形式就是對的了
class SpecialWindows :public Window {
public:
virtual void onResize() {
Window::onResize(); //呼叫Window::onResize作用於*this身上
}
};
五、關於dynamic_cast的一些概述
- dynamic_cast主要用於:想要使用派生類的方法,但是此時只有一個基類的指標/引用,此時可以使用該型別轉換將基類的指標轉換為派生類指標
- 為什麼不建議使用該轉型:
- 因為該轉換會使程式碼執行的非常慢
- 有的編譯器對於該轉型的實現原理是“基於class名稱的字串比較”,也就是說如果在繼承體系很多的物件身上應用這個型別轉換,那麼會有很多的strcmp的呼叫來比較class名稱
- 下面是一個dynamic_cast的演示案例,只有派生類有一個閃爍效果的blink()函式,此時我們想用基類指標訪問這個函式
class Window {
public:
//...
};
class SpecialWindows :public Window {
public:
void blink();
};
int main()
{
std::vector<std::tr1::shared_ptr<Window> > VPW;
//遍歷容器中的每個Window物件並且呼叫blink函式
for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
//使用dynamic_cast轉型,get()函式是獲取shared_ptr中的Window物件指標的意思
if (SpecialWindows* psw = dynamic_cast<SpecialWindows*>(iter->get())) {
psw->blink();
}
}
return 0;
}
替代dynamic_cast方法①
- 對於上面的演示案例我們給出了下面的一種替代方法:直接使用容器儲存派生類物件,不儲存基類物件
- 缺點:但是這種方法可能需要為每一種派生類都建立一個容器來儲存
int main() { std::vector<std::tr1::shared_ptr<SpecialWindows> > VPW; for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) { (*iter)->blink(); } return 0; }
替代dynamic_cast方法②
第二種方法是:當你在基類中想做一些事情,那麼也就基類中同時宣告一份,並且將函式設定為virtual的(但是基類中的虛擬函式什麼都不做)
程式碼如下:
class Window { public: virtual void blink() { //條款34告訴你預設實現程式碼可能是個餿主意 //什麼都不做 } }; class SpecialWindows :public Window { public: virtual void blink() { //實現相關程式碼 } }; int main() { std::vector<std::tr1::shared_ptr<Window> > VPW; for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) { (*iter)->blink(); } return 0; }
- 絕對必須避免的一件事情就是所謂的“連串dynamic_cast”,例如下面的程式碼,下面的程式碼執行又大又慢,而且基礎不穩定,因為每次Window類繼承體系一旦改變,所有的程式碼都需要再次更改
class Window {};
class SpecialWindows :public Window {};
class SpecialWindows2 :public Window {};
class SpecialWindows3 :public Window {};
//...
int main()
{
std::vector<std::tr1::shared_ptr<Window> > VPW;
for (auto iter = VPW.begin(); iter != VPW.end(); ++iter)
{
if (SpecialWindows *psw1 = dynamic_cast<SpecialWindows*>(iter->get())) {
//...
}
else if (SpecialWindows2 *psw2 = dynamic_cast<SpecialWindows2*>(iter->get())) {
//...
}
else if (SpecialWindows3 *psw3 = dynamic_cast<SpecialWindows3*>(iter->get())) {
//...
}
//...
}
return 0;
}
五、總結
- 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_cast。如果有個設計需要轉型動作,試著發展無需轉型的替代設計
- 如果轉型是必須的,試著將它隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需要將轉型放進它們自己的程式碼
- 寧可使用新式的轉型,不要使用舊式轉型。前者很容易辨別出來,而且也比較有著分門別類的職掌