C++的四種類型轉換:static_cast、dynamic_cast、const_cast、reinterpret_cast;typedef與類型別名,作用域和一些偶爾會看到的C風格工具。 ...
C++筆記系列:靈活易錯的特性 - 02
關鍵詞:
- 類型和類型轉換
- typedef
- 作用域
- 頭文件
類型和類型轉換
typedef
typedef
為已有的類型聲明提供一個新名稱。可以將typedef
看成已有類型聲明的同義詞。
typedef int* IntPtr
可以互換地使用新的類型名稱及其別名:
int* p1;
IntPtr p2;
從根本上,它們就是同一類型:
// 完全合法
p1 = p2;
p2 = p2;
typedef
最常見的用法是當類型的聲明過於笨拙時,提供易於管理的名稱。特別是在模板當中。
如果你不想不厭其煩地寫std::vector<std::string>
,就可以使用typedef
,
typedef std::vector<std::string> StringVector;
void ProcessVector(const StringVector& vec) {}
int main {
StringVector my_vector;
return 0;
}
實際上,STL廣泛地使用typedef
,比如string
是這樣定義的:
typedef basic_string<char, char_traits<char>, allocator<char>> string
函數指針typedef
C++中函數指針並不常見(被virtual
關鍵字替代),但在某些情況下還是需要獲取函數指針。不過在定義函數指針時,typedef
最讓人費解。
考慮一個動態鏈接庫(DLL),庫中有一個名為MyFunc()
的函數。只有需要調用MyFunc()
時,才會載入這個庫。
假定MyFunc()
的原型如下,
int __stdcall MyFunc(bool b, int n, const char* p);
__stdcall
時Microsoft特有的指令,指示如何將參數傳遞到函數,如何執行清理。
現在可以使用typedef
為指向函數的指針定義一個縮寫名稱(MyFuncProc),該函數具有前面所示的原型:
typedef int (__stdcall *MyFuncProc)(bool b, int n, const char* p);
註意typedef
的名稱MyFuncProc
嵌在這個語法中間,相當費解。解決這個問題還有另外更為簡潔的方法,就是下一節將描述的類型別名。
類型別名
某些情況下,類型別名比typedef
更容易理解。
typedef int MyInt;
可以用類型別名寫作
using MyInt = int;
當typedef
變得複雜時,類型別名就特別有用。
typedef int (*FuncType)(char, double);
// 使用類型別名
using FuncType = int (*)(char, double);
類型別名不只是易於閱讀的typedef
,在模板中使用typedef
,typedef
的問題變得很突出。
舉個例子,假定有如下的類模板:
template<typename T1, typename T2>
class MyTemplateClass {
};
// ok
typedef MyTemplateClass<int, double> OtherName;
// Error
typedef MyTemplateClass<T1, double> OtherName;
// 如果要這麼做,應該使用模板別名
template<typename T1>
using OtherName = MyTemplateClass<T1, double>;
類型轉換
C++提供了四種類型轉換:static_cast
、dynamic_cast
、const_cast
、reinterpret_cast
。強烈建議再新代碼中僅使用C++風格的類型轉換,它們更安全,語法上也比C風格的()
更加優秀。
1.const_cast
const_cast
最直接,可以用於變數的常量特性。這是四個類型轉換中唯一可以捨棄常量特性的類型轉換。當然,從理論上講,並不需要const
類型轉換。變數是const
,就應該一直是const
。
但是,有時候某個函數需要把const
變數傳遞給非const
變數的函數。最好的方法當然是保持const
一致,很多時候使用了第三方庫,沒有辦法,只能委曲求全,不然就只能重新構建程式。
// 第三方外部方法
extern void ThirdPartyLibraryMethod(char * str);
void f(const char* str) {
ThirdPartyLibraryMethod(const_cast<char*>(str));
}
2.static_cast
顯示執行C++直接支持的轉換。例如,在算術表達式中,將int
轉換為double
避免截斷,就可以使用static_cast
。
int i = 3;
int j = 4;
// 只需要轉換其中一個就可以確保執行浮點數除法
double result = static_cast<double>(i) / j;
如果用戶定義了相關的構造函數或者轉換常式,也可以使用static_cast
執行顯式轉換。例如,類A的構造函數將類B的對象作為參數,就可以使用static_cast
將B對象轉換為A對象。很多時候都需要這種行為,編譯器一般會自動隱式執行這個轉換。
static_cast
的另一種用法是在類繼承層次結構中執行向下轉換。註意,只能用於對象的指針和引用,不能轉換對象本身。
另外還需要註意,static_cast
不執行運行期間的類型檢測。比如下麵代碼可能會導致災難性的後果,包括記憶體重寫超出了對象的邊界。
// Derived繼承自Base
Base* b = new Base();
Derived* d = static_cast<Derived*>(b);
要執行具有運行時檢測的更安全轉換,可以用dynamic_cast
。
static_cast
不是萬能的,無法將const
類型轉換成non-const類型,無法把某種類型的指針轉換到其它不相關類型的指針,無法直接轉換對象的類型。基本上,無法完成類型規則中沒有意義的轉換。
3.reinterpret_cast
reinterpret_cast
比static_cast
更強大,同時安全性更差。可以用來執行技術上不被規則允許,但某些情況下又需要的類型轉換。例如,可在兩個引用之間相互轉換,即使兩者並不相關。同樣,即使兩個指針不存在繼承關係,也可以將某種指針類型轉換為其他指針類型。
reinterpret_cast
經常用於將指針轉換為void*
,以及將void*
轉換為指針。
class X {};
class Y {};
int main() {
X x;
Y y;
X* px = &x;
Y* py = &y;
// 無關的指針類型間轉換隻能用reinterpret_cast
px = reinterpret_cast<X*>(py);
void *p = px;
px = reinterpret_cast<X*>(p);
X& rx = x;
Y& ry = reinterpret_cast<Y&>(x);
}
理論上,reinterpret_cast
還可以把指針轉換為int
,或者把int
轉換為指針。但是,這種程式可能是不正確的。許多平臺上(特別是64位),指針和int
的大小不同。例如,在64位平臺上,指針是64位,整數可能是32位。將64位的指針轉換為32位整數會丟失32位。
另外,使用reinterpret_cast
要特別小心,它不會執行任何類型檢測。
4.dynamic_cast
dynamic_cast
為繼承層次結構內的類型轉換提供運行時檢測。可用它來轉換指針或者引用。dynamic_cast
在運行時檢測底層對象的類型信息。如果類型轉換沒有意義,dynamic_cast
返回一個空指針(用於指針)或者拋出一個std::bad_cast
異常(用於引用)。
註意運行時類型信息存儲在對象的虛表中。因此,為了使用dynamic_cast
,類至少要有一個虛方法。如果類沒有虛表,使用dynamic_cast
會導致編譯錯誤。
下麵用於引用的dynamic_cast
將拋出異常:
#include <iostream>
using std::bad_cast;
using std::cout;
class Base {
public:
Base() {}
virtual ~Base() {}
};
class Derived : public Base {
public:
Derived() {}
virtual ~Derived() {}
};
int main() {
Base base;
Derived derived;
// 改成Base& br = derived;就不會拋出異常
Base& br = base;
try {
Derived& dr = dynamic_cast<Derived&>(br);
} catch (const bad_cast&) {
cout << "Bad cast!\n";
}
}
5.類型轉換總結
情形 | 類型轉換 |
---|---|
刪除const特性 | const_cast |
語言支持的類型轉換(例如,int轉換為double,int轉換為bool) | static_cast |
用戶自定義構造函數或者轉換常式所支持的類型轉換 | static_cast |
類的對象轉換為其他(無關)類的對象 | 無法完成 |
類對象的指針(pointer-to-object)轉換為同一繼承層次結構中其他類對象的指針 | static_cast或者dynamic_cast(推薦) |
類對象的引用(reference-to-object)轉換為同一繼承層次結構中其他類對象的引用 | static_cast或者dynamic_cast(推薦) |
類型的指針轉換(pointer-to-type)為其他無關類型的指針 | reinterpret_cast |
類型的引用轉換(reference-to-type)為其他無關類型的引用 | reinterpret_cast |
函數指針(pointer-to-function)為其他函數指針 | reinterpret_cast |
作用域解析
創建作用域可以使用
- 名稱空間
- 函數定義
- 花括弧界定的塊
- 類定義
試圖訪問某個變數、函數或者類時,會從最近的作用域開始查找這個名稱,然後時相鄰的作用域,以此類推,直到全局作用域。
如果不想用預設的作用域解析某個名稱,就可以使用作用域解析運算符::
和特定的作用域限定這個名稱。例如,訪問類的靜態方法,第一種做法是將類名(方法的作用域)和作用域解析運算符放在方法名的前面。第二種方法是通過類的對象訪問這個靜態方法。
頭文件
頭文件是為代碼提供抽象介面的一種機制。使用頭文件需要註意的一點是避免迴圈引用或者多次包含同一個頭文件。最常用的方法就是使用#ifndef
機制。
#ifndef
可以用來避免迴圈包含和多次包含。在每個頭文件的開頭,#ifndef
指令檢測是否定義了某個標記,如果標記已經定義,則代表已經包含了頭文件,編譯器將調到對應的#endif
,這個指令一般位於文件的結尾。如果沒有定義該標記,將定義這個標記,重覆包含就會被忽略。這個機制也稱為頭文件保護(include guards)。
#ifndef LOGGER_H
#define LOGGER_H
#include "Preferences.h"
class Logger {
public:
static void setPreferences(const Preferences& prefs);
static void logError(const char* error);
};
#endif // LOGGER_H
如果編譯器支持#pragma once
(Visual C++或者g++),可以這樣寫:
#pragma once
#include "Preferences.h"
class Logger {
public:
static void setPreferences(const Preferences& prefs);
static void logError(const char* error);
};
前置聲明(forward declarations)是另一個避免頭文件問題的工具。如果需要使用某個類,但是無法包含它的頭文件(例如,這個類嚴重依賴當前編寫的類),就可以告訴編譯器,存在這麼一個類,但是無法使用#include
。當然,不能在代碼中使用這個類,只有鏈接成功後才存在這個已命名的類。
#ifndef LOGGER_H
#define LOGGER_H
class Preference; // 前置聲明
class Logger {
public:
static void setPreferences(const Preferences& prefs);
static void logError(const char* error);
};
在大型項目中,使用前置聲明可以減少編譯和重編譯的時間,因為它破壞了一個頭文件對其它頭文件的依賴。但是,前置聲明也隱藏了依賴關係,頭文件改動時,用戶的代碼會跳過必要的重新編譯過程。
前置聲明有時候會妨礙頭文件變動API,特別是函數,例如擴大形參類型。另外,一般的項目,還沒到需要縮短編譯時間的程度。
建議函數總是用
#include
,類模板優先考慮#include
。
C的工具
C語言有一些晦澀的特性在C++中還是偶爾可以看到。比如,變長參數列表(variable length argument lists)和預處理器巨集(preprocessor macros)。
變長參數列表
在C語言中,對於這個特性一定不陌生,printf()
就是使用了這種特性。
註意,新的代碼應該通過variadic
模板使用類型安全的變長參數列表。
#include <cstdio>
#include <cstdarg>
bool debug = false;
void DebugOut(const char* str, ...) {
va_list ap;
if (debug) {
va_start(ap, str);
vfprintf(stderr, str, ap);
va_end(ap);
}
}
int main(int argc, char const *argv[]) {
debug = true;
DebugOut("int %d\n", 5);
DebugOut("String %s and int %d\n", "hello", 5);
DebugOut("Many ints: %d, %d, %d, %d, %d\n", 1, 2, 3, 4, 5);
return 0;
}
調用va_start()
之後必須調用va_end()
,確保函數結束後,堆棧處於穩定狀態。
1.訪問參數
如果想要自己訪問實際參數,可以使用va_arg()
。不過,如果不提供顯式的方法,就無法知道參數列表的結尾是什麼。
可以讓第一個參數計算參數的數目,或者當參數是一組指針時,可以要求最後一個是nullptr
。
下麵給一個示例,調用者需要在第一個已命名的函數中指定提供的參數數目。
void PrintInts(int num, ...) {
int temp;
va_list ap;
va_start(ap, num);
for (int i = 0; i < num; ++i) {
temp = va_arg(ap, int);
cout << temp << " ";
}
va_end(ap);
cout << endl;
}
2,為什麼不應該使用C風格變長參數列表
- 不知道參數的數目。例如在
PrintInts()
,需要信任調用者傳遞了與第一個參數數目相等的參數。 - 不知道參數的類型。
儘可能避免使用C風格的變長參數列表,傳array
、vector
或者使用C++11引入的初始化列表更好。也可以通過variadic
模板使用類型安全的變長參數列表。
預處理巨集
應該用內聯函數代替巨集。巨集易錯,而且不執行類型檢查。調用巨集時,預處理階段會自動展開替換,但不會真正用函數調用語義,這一行為可能導致無法預測的後果。還有,巨集很容易出錯,比如這樣寫是有問題的:
#define SQUARE(x) (x * x)
假如使用SQUARE(2 + 3)
,展開後變成2 + 3 * 2 + 3
,計算的結果是11,不是預期中的25。正確的寫法是這樣的
#define SQUARE(x) ((x) * (x))
註意,最外面的括弧不能省略,不然可能會因為優先順序問題在算術表達式中被另行結合。
如果計算比較複雜,重覆的展開替換意味著在做重覆運算,就會有一筆不小的開銷。