深入解析 C++ 类型转换

简介

C++ 类型转换是开发者必须掌握的重要技能之一, 无论是处理隐式转换还是显式转换, 理解其背后的机制与用法至关重要. 本篇博客旨在从基础到高级全面解析 C++ 的类型转换, 包括实际开发中的应用场景和性能分析.


自动转换

隐式类型转换

编译器可以在无需明确指示的情况下, 将一种类型的值自动转换为另一种兼容类型. 例如:

cpp 复制代码
struct Struct {
  Struct(float f) : m_(f) {}
  float m_ = 0;
};
float f = -3.1415926;
double d = f;
int i = f;
size_t s = f;
char c = f;
Struct st = f;
赋值语句
float f = -3.14; -3.14159
double d = f; -3.14159
int i = f; -3
size_t s = f; 18446744073709551613
char c = f; 乱码
Struct st = f; {.m_ = -3.14159}

算术类型转换

cpp 复制代码
void drawLine(uint8_t start, uint8_t end);
uint8_t x = 10;
uint8_t width = 50;
// 此处等价于: drawLine(x, static_cast<unsigned char>(static_cast<int>(x) + static_cast<int>(width)));
drawLine(x, x+width);

符号转换

cpp 复制代码
void print(const std::vector<int>& vec) {
  // 此处等价于: static_cast<unsigned long>(i) < vec.size()
  for (int i = 0; i < vec.size(); i++) {
    std::cout << i << ",";
  }
}

用户转换运算符

cpp 复制代码
class Struct {
 public:
  Struct(float f) : m_(f) {}
  // 重载了转为int类型的操作符
  operator int() const { return m_; }

 private:
  float m_ = 0;
};

int main() {
  Struct si(1);
  int i = si;
}

显示类型转换

C 风格类型转换

cpp 复制代码
(type)var;
  1. 使用 var 创建 <type> 的临时变量
  2. <type> 可以是任何带有限定符的有效类型
  3. 通过更改变量中位的含义来覆盖类型系统
  4. 在某些情况下无法编译(稍后详细介绍)
  5. 支持在 constexpr 上下文中使用(稍后详细介绍)
  6. 可能导致未定义的行为
  7. 参与运算符优先级(级别 3)
cpp 复制代码
struct A {};
struct B {};

int main() {
  float f = 7.406f;
  int i = (int)f;             // int i = static_cast<int>(f);
  A* pa = (A*)&f;             // A* pa = reinterpret_cast<A*>(&f);
  B* pb = (B*)pa;             // B* pb = reinterpret_cast<B*>(pa);
  double d = *(double*)(pb);  // double d = *reinterpret_cast<double*>((pb));

  return 0;
}
C 风格和函数式符号转换的问题
  1. 单一符号, 多重含义
  2. 容易出错
  3. 无法 grep
  4. 使 C 和 C++ 语法复杂化

C++ 强制转换的目标

  1. 不同的符号或不同的任务
  2. 易于识别和搜索
  3. 执行 C 强制转换可以执行的所有操作
  4. 消除意外错误
  5. 使强制转换不那么诱人

C++有如下几种类型转换的关键词:

  1. static_cast
  2. const_cast
  3. dynamic_cast
  4. reinterpret_cast

static_cast

cpp 复制代码
T1 var;
T2 var2 = static_cast<T>(var)
  1. var 类型创建临时变量
  2. 尝试通过隐式和用户定义的转换或构造找到从 T1T2 的路径. 无法删除 const 限定.

使用场景:

  1. 阐明隐式转换

    cpp 复制代码
    int i = 1;
    double d = static_cast<double>(i);
  2. 指示有意截断

    cpp 复制代码
    int a = 1234;
    uint8_t u8 = static_cast<uint8_t>(a);
  3. 在基类和派生类之间进行强制转换

    cpp 复制代码
    struct Base {};
    struct Derived : public Base {};
    
    Derived derived;
    Base& rb = derived;
    Derived& rd = static_cast<Derived&>(rb);
  4. void*T* 之间进行强制转换

    cpp 复制代码
    struct MyStruct {};
    void callback(void* handle) {
     auto p = static_cast<MyStruct*>(handle);
     //...
    }
多重转换
cpp 复制代码
#include <cstdio>

struct A {
  explicit A(int) { puts("A"); }
};
struct E {
  operator int() {
    puts("B::operator int");
    return 0;
  }
};

int main() {
  E e;
  A a = static_cast<A>(e);
  return 0;
}
  1. A 有一个接受单个 int 的构造函数
  2. E 有一个用户定义的到 int 的转换
  3. 所以从ea的路径为: e -> int -> a

static_cast 与继承

cpp 复制代码
#include <iostream>

struct B1 {
  virtual ~B1() = default;
  int i;
};

struct B2 {
  virtual ~B2() = default;
  int j;
};

struct Derived : public B1, public B2 {
  int k;
};

void Compare(void* p1, void* p2) {
  if (p1 == p2) {
    std::cout << "Same.\n";
  } else {
    std::cout << "Different.\n";
  }
}

int main() {
  Derived d;
  // pd 指向派生类
  Derived* pd = &d;

  // pb1 是指向基类B1的指针
  B1* pb1 = static_cast<B1*>(&d);
  Compare(pd, pb1);  // Same.

  // pb2 是指向基类B1的指针
  B2* pb2 = static_cast<B2*>(&d);
  Compare(pd, pb2);  // Different.

  void* derived_plus_offset = (char*)pd + sizeof(B1);
  Compare(derived_plus_offset, pb2);  // Same.
  return 0;
}

为什么会出现这样的情况? 因为Derived的布局为:

txt 复制代码
+---------+  <--- pd and pb1
|    B1   |
+---------+  <--- pb2
|    B2   |
+---------+
| Derived |
+---------+
    ...
static_cast 并非绝对正确

static_cast 无法防止向下转型为不相关的类型

cpp 复制代码
#include <iostream>
#include <type_traits>

struct Base {
  virtual void f() { std::cout << "base\n"; }
  virtual ~Base() = default;
};
struct Derived : public Base {
  void f() override { std::cout << "Derived\n"; }
};

struct Other : public Base {
  void f() override { std::cout << "Other\n"; }
};

int main() {
  Derived d;
  Base& b = d;  // OK

  d.f();  // Derived
  b.f();  // Derived

  Other& a = static_cast<Other&>(b);  // 危险, 转换到了其他类型
  a.f();                              // Derived
  static_assert(std::is_same<decltype(a), Other&>::value, "not the same");
  return 0;
}

const_cast

  1. 从变量中删除或添加 constvolatile 限定符, 不能更改类型
  2. 不会更改原始变量的 CV 限定符
cpp 复制代码
#include <iostream>

void use_pointer(int* p) { std::cout << "*p = " << *p << std::endl; }
void modify_pointer(int* p) {
  *p = 42;
  std::cout << "\tmodify_pointer *p <- 42\n"
            << "\tmodify_pointer *p = " << *p << std::endl;
}

int main() {
  const int i = 7;
  use_pointer(const_cast<int*>(&i));
  modify_pointer(const_cast<int*>(&i));

  std::cout << "i = " << i << std::endl;  // i = 7
  int j = 4;
  const int* cj = &j;
  modify_pointer(const_cast<int*>(cj));
  std::cout << "i = " << i << std::endl;  // i = 7

  return 0;
}

输出

txt 复制代码
*p = 7
        modify_pointer *p <- 42
        modify_pointer *p = 42
i = 7
        modify_pointer *p <- 42
        modify_pointer *p = 42
i = 7

可以看到虽然在函数modify_pointer里面指针指向的值发生了变化, 但是在外面的值却不受影响.

const_cast example: member overload

cpp 复制代码
#include <stddef.h>

class my_array {
 public:
  char& operator[](size_t offset) {
    // 此处调用const版本的实现, 避免重写一遍逻辑.
    return const_cast<char&>(const_cast<const my_array&>(*this)[offset]);
  }
  const char& operator[](size_t offset) const { return buffer[offset]; }

 private:
  char buffer[10];
};
int main() {
  const my_array a{};
  const auto& c = a[4];
  my_array mod_a;
  mod_a[4] = 7;
  return 0;
}

用于防止成员函数的代码重复.

运行时类型信息 (RTTI)

  1. 为实现定义的结构中的每个多态类型存储额外信息
  2. 允许在运行时查询类型信息
  3. 可以禁用以节省空间(gcc/clang: --fno-rtti, msvc: /GR-)

dynamic_cast

  1. 查看 To 是否与 From 位于同一公共继承树中
  2. 只能是引用或指针
  3. 不能删除 CV
  4. From 必须是多态的
  5. 需要 RTTI
  6. 如果类型不相关, 则对指针返回 nullptr, 对引用抛出 std::bad_cast
cpp 复制代码
#include <cstdio>
#include <vector>

struct A {
  virtual ~A() = default;
};
struct B : public A {};
struct C : public A {};
int main() {
  C c;
  B b;
  std::vector<A*> a_list = {&c, &b};
  for (size_t i = 0; i < a_list.size(); ++i) {
    A* pa = a_list[i];
    if (dynamic_cast<B*>(pa)) {
      printf("a_list[%lu] was a B\r\n", i);
    }
    if (dynamic_cast<C*>(pa)) {
      printf("a_list[%lu] was a C\r\n", i);
    }
  }
  return 0;
}

dynamic_cast 用例: UI 框架

cpp 复制代码
struct Widget {};
struct Label : public Widget {};
struct Button : public Widget { void DoClick(); };

dynamic_cast can be expensive

from gcc's rtti.c

reinterpret_cast

cpp 复制代码
#include <cstdint>

struct A {};
struct B {
  int i;
  int j;
};

int main() {
  int i = 0;
  int* pi = &i;
  uintptr_t uipt = reinterpret_cast<uintptr_t>(pi);
  float& f = reinterpret_cast<float&>(i);
  A a;
  B* pb = reinterpret_cast<B*>(&a);
  char buff[10];
  B* b_buff = reinterpret_cast<B*>(buff);
  return 0;
}
  1. 可以将任何指针或引用类型更改为任何其他指针或引用类型
  2. 也称为类型双关
  3. 不能在 constexpr 上下文中使用
  4. 不能删除 CV 限定
  5. 不确保 To 和 From 的大小相同
  6. 适用于内存映射功能

reinterpret_cast 访问私有继承的基类

cpp 复制代码
struct B {
  void m() { puts("private to D"); }
};
struct D : private B {};
int main() {
  D d;
  B& b = reinterpret_cast<B&>(d);
  b.m();
  return 0;
}

Type Aliasing

当两种类型的内存布局兼容时, 将一种类型的内存当作另一种类型的内存来使用的行为.

compatible types

cpp 复制代码
struct Point {
  int x;
  int y;
};
struct Location {
  int x;
  int y;
};
Point p{1, 2};
auto* loc = reinterpret_cast<Location*>(&p);

incompatible types

cpp 复制代码
float f = 1.0f;
int* i = reinterpret_cast<int*>(&f);

C 风格类型转换在 C++ 中是如何实际执行的

对与一个类型转换

cpp 复制代码
T conv = (T)val;

C++会依次尝试:

  1. T conv = const_cast<T>(val);
  2. T conv = static_cast<T>(val);
  3. T conv = const_cast<T>(static_cast<const T>(val));
  4. T conv = reinterpret_cast<T>(val);
  5. T conv = const_cast<T>(reinterpret_cast<const T>(val));

如果找到匹配则会选择并执行编译, 否则会报错.

总结

C++ 提供了更安全, 更明确的类型转换工具, 开发者应根据场景选择合适的转换方式. 通过熟练掌握这些工具, 您可以编写更健壮, 更易维护的代码. 希望本博客能帮助您更深入地理解 C++ 类型转换的精髓!

参考资源

相关推荐
wlyang66615 分钟前
4. scala高阶之隐式转换与泛型
大数据·开发语言·后端·spark·scala
JovaZou2 小时前
[Python学习日记-75] 计算机基础与网络
开发语言·网络·python·网络协议·学习·tcp/ip·计算机网络
KeyPan3 小时前
【Ubuntu与Linux操作系统:十、C/C++编程】
linux·运维·服务器·c语言·c++·算法·ubuntu
五行星辰4 小时前
Servlet与JSP:Java的秘密花园入口
java·开发语言·servlet
代码驿站5204 小时前
Scala语言的软件工程
开发语言·后端·golang
Code花园5 小时前
Objective-C语言的多线程编程
开发语言·后端·golang
Rverdoser5 小时前
接口项目架构流程图-thinkphp6-rabbitmq
开发语言·microsoft·ruby
我想学LINUX5 小时前
【2024年华为OD机试】 (C卷,100分)- 消消乐游戏(Java & JS & Python&C/C++)
java·c语言·javascript·c++·游戏·华为od
Jelena技术达人5 小时前
利用 Python 爬虫获取 1688 关键字 API 接口
开发语言·爬虫·python
graceyun5 小时前
C语言初阶习题【23】输出数组的前5项之和
c语言·开发语言·算法