C++類型轉換
本節解決什麼問題
C++ 中經常會遇到“把一種類型當成另一種類型使用”的場景,例如:
- 把
double轉成int。 - 把
enum class轉成整數。 - 把基類指針安全地轉成派生類指針。
- 臨時去掉
const限定,適配舊接口。 - 在底層代碼裏把指針、整數、字節視圖互相轉換。
C 風格強轉寫法 (目标类型)表达式 能做很多事,但問題也在這裏:它太“萬能”,讀代碼的人很難一眼看出這次轉換到底是普通數值轉換、繼承層級轉換、去掉 const,還是危險的底層重解釋。
C++ 提供了四個更明確的類型轉換運算符,讓轉換意圖寫在代碼裏。
隱式轉換和顯式轉換
類型轉換可以分為隱式轉換和顯式轉換。
隱式轉換
隱式轉換是由編譯器自動完成的轉換,代碼中不需要寫出轉換操作。例如:
int count = 10;
double result = count;
這裏 count 是 int,而 result 是 double。編譯器會自動把整數 10 轉換成浮點數 10.0,這就是隱式轉換。
隱式轉換寫起來方便,但有些轉換可能造成數據丟失:
double price = 19.8;
int whole_price = price;
這段代碼通常可以通過編譯,但 whole_price 得到的是 19,小數部分會被丟棄。因為轉換沒有明確寫出來,閲讀代碼時也容易忽略它。
顯式轉換
顯式轉換是程序員在代碼中明確要求進行的轉換。例如:
double price = 19.8;
int whole_price = static_cast<int>(price);
這裏的 static_cast<int> 明確表示:程序員知道 price 會被轉換成 int,也接受小數部分被丟棄。
兩者的區別可以簡單理解為:
- 隱式轉換:編譯器根據上下文自動轉換。
- 顯式轉換:程序員在代碼中明確寫出要轉換成什麼類型。
隱式轉換並不一定不安全,顯式轉換也不代表一定安全。顯式寫出轉換的主要價值,是讓轉換意圖更清楚,並讓編譯器按照指定的轉換方式進行檢查。
下面介紹的 static_cast、dynamic_cast、const_cast 和 reinterpret_cast,都是 C++ 提供的顯式類型轉換。
這個特性是什麼
C++ 的四種顯式類型轉換是:
| 轉換方式 | 主要用途 | 安全程度 |
|---|---|---|
static_cast<T>(expr) | 常規類型轉換,如數值、枚舉、父子類上行轉換 | 常用,編譯期檢查 |
dynamic_cast<T>(expr) | 多態類型的安全向下轉換 | 運行期檢查,失敗返回 nullptr 或拋異常 |
const_cast<T>(expr) | 添加或移除 const / volatile 限定 | 要非常剋制 |
reinterpret_cast<T>(expr) | 底層二進制重解釋,如指針和整數互轉 | 最危險,少用 |
原則:能不用強轉就不用;必須強轉時,優先選擇語義最窄、最能説明意圖的轉換。
C++ 標準版本
C++98 就已經提供這四種轉換寫法,實際工程中依然推薦使用它們替代 C 風格強轉。
需要的頭文件
類型轉換本身不需要額外頭文件。示例代碼使用到的庫需要對應頭文件,例如:
#include <cstdint>
#include <iostream>
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
基本語法
目标类型 value = static_cast<目标类型>(表达式);
目标类型 value = dynamic_cast<目标类型>(表达式);
目标类型 value = const_cast<目标类型>(表达式);
目标类型 value = reinterpret_cast<目标类型>(表达式);
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
示例代碼
示例 1:static_cast 處理常規轉換
static_cast 適合明確、常規、編譯期能檢查的轉換。比如數值轉換、enum class 轉整數、父類指針指向子類對象等。
#include <iostream>
enum class Status
{
ok = 200,
not_found = 404
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
double score = 89.7;
int integer_score = static_cast<int>(score);
Status status = Status::not_found;
int status_code = static_cast<int>(status);
std::cout << "integer_score = " << integer_score << "\n";
std::cout << "status_code = " << status_code << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
運行結果:
integer_score = 89
status_code = 404
示例 2:dynamic_cast 安全判斷真實對象類型
dynamic_cast 常用於多態基類。向下轉換時,它會在運行期檢查對象真實類型。
#include <iostream>
struct Animal
{
virtual ~Animal() = default;
};
struct Cat : Animal
{
void meow() const
{
std::cout << "cat: meow\n";
}
};
struct Dog : Animal
{
};
void try_meow(Animal* animal)
{
Cat* cat = dynamic_cast<Cat*>(animal);
if (cat != nullptr)
{
cat->meow();
}
else
{
std::cout << "not a cat\n";
}
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
Cat cat;
Dog dog;
try_meow(&cat);
try_meow(&dog);
// 返回 0 表示程序正常结束。
return 0;
}
運行結果:
cat: meow
not a cat
注意:
dynamic_cast用在指針上,轉換失敗會得到nullptr;用在引用上,轉換失敗會拋出std::bad_cast。它要求基類至少有一個虛函數,通常是虛析構函數。
示例 3:const_cast 只改變 const 限定
const_cast 只能添加或移除 const / volatile 限定,不能把 int 轉成 double,也不能把兩個不相關的類型互相轉換。
#include <iostream>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int value = 10;
const int& readonly_ref = value;
int& writable_ref = const_cast<int&>(readonly_ref);
writable_ref = 20;
std::cout << "value = " << value << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
運行結果:
value = 20
這個例子裏,value 本身是一個普通的 int 變量,並不是 const int。
int value = 10;
const int& readonly_ref = value;
這兩行代碼的意思是:readonly_ref 是 value 的一個“只讀引用”。它並沒有創建一個新的變量,而是給 value 起了一個只能讀取、不能修改的別名。
也就是説:
readonly_ref
本質上還是指向原來的 value。
接下來:
int& writable_ref = const_cast<int&>(readonly_ref);
這行代碼使用 const_cast 去掉了 readonly_ref 上的 const 限定,讓它重新變成一個可以修改的引用。由於它引用的還是原來的 value,所以:
writable_ref = 20;
本質上就是在修改:
value = 20;
因此最後輸出的是:
value = 20
需要注意的是,這個例子能夠修改成功,是因為原始對象 value 本身不是 const,只是我們臨時用 const int& 這種只讀方式去引用它。
如果原始對象本身就是 const,再用 const_cast 強行修改就是未定義行為,例如:
const int value = 10;
const int& readonly_ref = value;
int& writable_ref = const_cast<int&>(readonly_ref);
writable_ref = 20; // 未定义行为
所以可以簡單記住:
const_cast 只是把“只讀視角”改回“可寫視角”,它不會複製出新變量,也不會真的把一個天生的常量安全地變成普通變量。
示例 4:reinterpret_cast 做底層重解釋
reinterpret_cast 表示“把這段二進制數據按另一種類型解釋”。它繞過了很多類型系統保護,通常只應該出現在非常底層的代碼裏。
比如把一個指針轉換成整數形式的地址,再把這個整數地址轉換回指針。
reinterpret_cast 可以用來做一些比較“底層”的類型轉換,比如把一個指針轉換成整數形式的地址,再把這個整數地址轉換回指針。
需要注意的是,這裏轉換的是地址,不是變量裏面保存的值。
#include <cstdint>
#include <iomanip>
#include <iostream>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int value = 42;
int* p = &value;
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
int* again = reinterpret_cast<int*>(raw);
std::cout << "value = " << value << "\n";
std::cout << "p = " << p << "\n";
std::cout << "raw = " << raw << "\n";
std::cout << "raw(hex) = 0x" << std::hex << raw << std::dec << "\n";
std::cout << "again = " << again << "\n";
std::cout << "*again = " << *again << "\n";
std::cout << std::boolalpha;
std::cout << "same pointer = " << (p == again) << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
運行結果示例:
value = 42
p = 0x7ffd60a576d4
raw = 140726224910036
raw(hex) = 0x7ffd60a576d4
again = 0x7ffd60a576d4
*again = 42
same pointer = true
其中,具體的地址值每次運行都可能不同,所以你的運行結果不一定和上面完全一樣。
這段代碼裏:
int value = 42;
int* p = &value;
value 是一個普通的 int 變量,裏面保存的值是 42。
p 是一個 int* 指針,裏面保存的是 value 的地址,而不是 value 的值。
也就是説,p 裏面存的不是:
42
而是類似這樣的地址:
0x7ffd60a576d4
接着:
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
這行代碼把指針 p 轉換成了一個整數。
std::uintptr_t 是一種無符號整數類型,它的作用是儘可能安全地保存一個指針地址。
所以如果:
p = 0x7ffd60a576d4
那麼 raw 保存的就是這個地址對應的整數形式。
例如:
raw = 140726224910036
這只是同一個地址的十進制寫法。
下面這行代碼:
std::cout << "raw(hex) = 0x" << std::hex << raw << std::dec << "\n";
是把 raw 按十六進制打印出來。這樣就可以看到,raw 的十六進制形式和指針 p 打印出來的地址基本是一樣的。
然後:
int* again = reinterpret_cast<int*>(raw);
這行代碼又把整數形式的地址 raw 轉回了 int* 指針。
也就是説:
p 指向 value
again 也指向 value
所以:
std::cout << "*again = " << *again << "\n";
這裏的 *again 就是在訪問 again 指向的變量。
因為 again 重新指向了原來的 value,所以:
*again
等價於:
value
因此輸出結果是:
*again = 42
最後:
std::cout << "same pointer = " << (p == again) << "\n";
這行代碼比較的是 p 和 again 這兩個指針保存的地址是否相同。
由於它們都指向 value,所以結果是:
same pointer = true
可以把整個過程理解成:
value 是一个变量,值是 42
p 保存 value 的地址
raw 把这个地址用整数形式保存下来
again 又把这个整数地址转换回指针
所以 again 重新指向 value
所以這段代碼的重點不是把 42 轉成整數,而是把 value 的地址轉成整數。
簡單記憶:
int* p = &value;
表示:
拿到 value 的地址
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
表示:
把地址当成整数保存
int* again = reinterpret_cast<int*>(raw);
表示:
把整数地址再变回指针
*again
表示:
通过这个地址找到原来的 value
一句話總結:
reinterpret_cast 在這個例子裏轉換的是“地址的表示形式”,不是變量裏面的值。value 的值是 42,而 raw 保存的是 value 的地址。
四種轉換怎麼選
| 需求 | 推薦寫法 | 説明 |
|---|---|---|
double 轉 int | static_cast<int>(x) | 明確可能丟失小數 |
enum class 轉整數 | static_cast<int>(e) | 枚舉類不會隱式轉整數 |
| 基類指針轉派生類指針,並且不確定真實類型 | dynamic_cast<Derived*>(p) | 轉換失敗可判斷 nullptr |
去掉 const 適配舊接口 | const_cast<T*>(p) | 前提是舊接口不會修改真正的 const 對象 |
| 指針和整數互轉、底層字節解釋 | reinterpret_cast<T>(x) | 非常底層,優先避免 |
類型轉換 對比 C風格強轉 會增加運行開銷嗎
static_cast<T>(expr) 中雖然使用了尖括號,看起來有些像模板,但它不是模板,而是 C++ 語言內置的類型轉換運算符。這裏的 T 表示要轉換成的目標類型。
與功能相同的 C 風格強轉相比,C++ 的顯式類型轉換通常不會產生額外的運行開銷:
| 轉換方式 | 通常的運行開銷 |
|---|---|
static_cast | 不會因為使用這種寫法而增加額外開銷 |
dynamic_cast | 需要在運行時檢查對象的真實類型,可能產生時間和程序體積開銷 |
const_cast | 通常只改變編譯器進行類型檢查的方式,沒有運行時操作 |
reinterpret_cast | 通常只改變編譯器解釋數據的方式,沒有額外轉換步驟 |
例如,下面兩種寫法表達的是同一種數值轉換:
double value = 3.14;
int a = (int)value;
int b = static_cast<int>(value);
編譯器通常會為它們生成相同或等價的機器指令。static_cast 的優勢不是運行得更快,而是轉換意圖更加明確,並且能夠得到更嚴格的編譯期檢查。
不過,需要區分“轉換寫法的開銷”和“轉換操作本身的開銷”。例如,把浮點數轉換成整數可能需要執行實際的處理器指令:
double value = 3.14;
int result = static_cast<int>(value);
這裏可能產生的運行開銷來自“把 double 轉換成 int”這個操作,而不是來自 static_cast 語法。即使改成隱式轉換或 C 風格強轉,這個轉換操作仍然存在。
因此可以簡單記住:選擇 C++ 的顯式類型轉換,主要是為了讓類型檢查更嚴格、代碼意圖更清楚;除了需要運行時檢查的 dynamic_cast,它通常不會因為寫法不同而比 C 風格強轉更慢。
C風格強轉為什麼不推薦
C 風格強轉寫起來短:
int n = (int)3.14;
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
更推薦寫成:
int n = static_cast<int>(3.14);
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
C 風格強轉可能在背後組合多種轉換能力,既可能像 static_cast,也可能像 const_cast,甚至接近 reinterpret_cast。代碼越底層、類型越複雜,這種“不説明白”的風險就越大。
常見錯誤
用 static_cast 做不安全向下轉換
Animal* animal = new Dog;
Cat* cat = static_cast<Cat*>(animal); // 危险:编译可能通过,但真实对象不是 Cat
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
如果不確定真實類型,應該使用 dynamic_cast。
修改真正的 const 對象
const int value = 10;
int& ref = const_cast<int&>(value);
ref = 20; // 未定义行为
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
濫用 reinterpret_cast
double d = 3.14;
int* p = reinterpret_cast<int*>(&d); // 危险:把 double 对象当 int 对象访问
運行結果: 此代碼塊不含 main 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。
使用建議
- 普通轉換優先用
static_cast。 - 多態向下轉換優先用
dynamic_cast,並檢查失敗情況。 const_cast只用於適配舊接口,不要用它修改真正的const對象。reinterpret_cast只放在非常底層、邊界清晰的代碼裏,並集中封裝。- 不要為了少打幾個字使用 C 風格強轉。
小結
static_cast:常規轉換,最常用。dynamic_cast:多態類型安全向下轉換。const_cast:只改變const/volatile限定。reinterpret_cast:底層重解釋,風險最高。
一句話記憶:能不用就不用;必須用時,把轉換意圖寫清楚。