Wiki
C++型別轉換
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.本節解決什麼問題
C++ 中經常會遇到“把一種型別當成另一種型別使用”的場景,例如:
- 把
double轉成int。 - 把
enum class轉成整數。 - 把基類指標安全地轉成派生類指標。
- 臨時去掉
const限定,適配舊介面。 - 在底層程式碼裡把指標、整數、位元組檢視互相轉換。
C 風格強轉寫法 (目标类型)表达式 能做很多事,但問題也在這裡:它太“萬能”,讀程式碼的人很難一眼看出這次轉換到底是普通數值轉換、繼承層級轉換、去掉 const,還是危險的底層重解釋。
C++ 提供了四個更明確的型別轉換運算子,讓轉換意圖寫在程式碼裡。
2.隱式轉換和顯式轉換
型別轉換可以分為隱式轉換和顯式轉換。
2.1.隱式轉換
隱式轉換是由編譯器自動完成的轉換,程式碼中不需要寫出轉換操作。例如:
int count = 10;
double result = count;
這裡 count 是 int,而 result 是 double。編譯器會自動把整數 10 轉換成浮點數 10.0,這就是隱式轉換。
隱式轉換寫起來方便,但有些轉換可能造成資料丟失:
double price = 19.8;
int whole_price = price;
這段程式碼通常可以通過編譯,但 whole_price 得到的是 19,小數部分會被丟棄。因為轉換沒有明確寫出來,閱讀程式碼時也容易忽略它。
2.2.顯式轉換
顯式轉換是程式設計師在程式碼中明確要求進行的轉換。例如:
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++ 提供的顯式型別轉換。
3.這個特性是什麼
C++ 的四種顯式型別轉換是:
| 轉換方式 | 主要用途 | 安全程度 |
|---|---|---|
static_cast<T>(expr) |
常規型別轉換,如數值、列舉、父子類上行轉換 | 常用,編譯期檢查 |
dynamic_cast<T>(expr) |
多型型別的安全向下轉換 | 執行期檢查,失敗返回 nullptr 或拋異常 |
const_cast<T>(expr) |
新增或移除 const / volatile 限定 |
要非常剋制 |
reinterpret_cast<T>(expr) |
底層二進位制重解釋,如指標和整數互轉 | 最危險,少用 |
原則:能不用強轉就不用;必須強轉時,優先選擇語義最窄、最能說明意圖的轉換。
4.C++ 標準版本
C++98 就已經提供這四種轉換寫法,實際工程中依然推薦使用它們替代 C 風格強轉。
5.需要的標頭檔案
型別轉換本身不需要額外標頭檔案。示例程式碼使用到的庫需要對應標頭檔案,例如:
#include <cstdint>
#include <iostream>
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
6.基本語法
目标类型 value = static_cast<目标类型>(表达式);
目标类型 value = dynamic_cast<目标类型>(表达式);
目标类型 value = const_cast<目标类型>(表达式);
目标类型 value = reinterpret_cast<目标类型>(表达式);
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
7.示例程式碼
7.1.示例 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
7.2.示例 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。它要求基類至少有一個虛擬函式,通常是虛解構函式。
7.3.示例 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 只是把“只讀視角”改回“可寫視角”,它不會複製出新變數,也不會真的把一個天生的常量安全地變成普通變數。
7.4.示例 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 的地址。
8.四種轉換怎麼選
| 需求 | 推薦寫法 | 說明 |
|---|---|---|
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) |
非常底層,優先避免 |
9.型別轉換 對比 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 風格強轉更慢。
10.C風格強轉為什麼不推薦
C 風格強轉寫起來短:
int n = (int)3.14;
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
更推薦寫成:
int n = static_cast<int>(3.14);
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
C 風格強轉可能在背後組合多種轉換能力,既可能像 static_cast,也可能像 const_cast,甚至接近 reinterpret_cast。程式碼越底層、型別越複雜,這種“不說明白”的風險就越大。
11.常見錯誤
11.1.用 static_cast 做不安全向下轉換
Animal* animal = new Dog;
Cat* cat = static_cast<Cat*>(animal); // 危险:编译可能通过,但真实对象不是 Cat
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
如果不確定真實型別,應該使用 dynamic_cast。
11.2.修改真正的 const 物件
const int value = 10;
int& ref = const_cast<int&>(value);
ref = 20; // 未定义行为
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
11.3.濫用 reinterpret_cast
double d = 3.14;
int* p = reinterpret_cast<int*>(&d); // 危险:把 double 对象当 int 对象访问
執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。
12.使用建議
- 普通轉換優先用
static_cast。 - 多型向下轉換優先用
dynamic_cast,並檢查失敗情況。 const_cast只用於適配舊介面,不要用它修改真正的const物件。reinterpret_cast只放在非常底層、邊界清晰的程式碼裡,並集中封裝。- 不要為了少打幾個字使用 C 風格強轉。
13.小結
static_cast:常規轉換,最常用。dynamic_cast:多型型別安全向下轉換。const_cast:只改變const/volatile限定。reinterpret_cast:底層重解釋,風險最高。
一句話記憶:能不用就不用;必須用時,把轉換意圖寫清楚。