第 19 節

C++類型轉換

0瀏覽次數0訪問次數--跳出率--平均停留

本節解決什麼問題

C++ 中經常會遇到“把一種類型當成另一種類型使用”的場景,例如:

  • double 轉成 int
  • enum class 轉成整數。
  • 把基類指針安全地轉成派生類指針。
  • 臨時去掉 const 限定,適配舊接口。
  • 在底層代碼裏把指針、整數、字節視圖互相轉換。

C 風格強轉寫法 (目标类型)表达式 能做很多事,但問題也在這裏:它太“萬能”,讀代碼的人很難一眼看出這次轉換到底是普通數值轉換、繼承層級轉換、去掉 const,還是危險的底層重解釋。

C++ 提供了四個更明確的類型轉換運算符,讓轉換意圖寫在代碼裏。

隱式轉換和顯式轉換

類型轉換可以分爲隱式轉換和顯式轉換。

隱式轉換

隱式轉換是由編譯器自動完成的轉換,代碼中不需要寫出轉換操作。例如:

int count = 10;
double result = count;

這裏 countint,而 resultdouble。編譯器會自動把整數 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_castdynamic_castconst_castreinterpret_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_refvalue 的一個“只讀引用”。它並沒有創建一個新的變量,而是給 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";

這行代碼比較的是 pagain 這兩個指針保存的地址是否相同。

由於它們都指向 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 的地址。

四種轉換怎麼選

需求推薦寫法說明
doubleintstatic_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 函數,是語法、接口或分文件示例,不能獨立運行,因此沒有終端輸出。

使用建議

  1. 普通轉換優先用 static_cast
  2. 多態向下轉換優先用 dynamic_cast,並檢查失敗情況。
  3. const_cast 只用於適配舊接口,不要用它修改真正的 const 對象。
  4. reinterpret_cast 只放在非常底層、邊界清晰的代碼裏,並集中封裝。
  5. 不要爲了少打幾個字使用 C 風格強轉。

小結

  • static_cast:常規轉換,最常用。
  • dynamic_cast:多態類型安全向下轉換。
  • const_cast:只改變 const / volatile 限定。
  • reinterpret_cast:底層重解釋,風險最高。

一句話記憶:能不用就不用;必須用時,把轉換意圖寫清楚。

音乐页