如果这篇博客帮助到你,可以请我喝一杯咖啡~
CC BY 4.0 (除特别声明或转载文章外)
一 引入
1. casting 会带来的问题
struct region { int size; };
void init_region(char* backing_buffer, size_t buffer_size ) {
if(buffer_size < current_region.size) {
LOG(“Buffer size too small”);
return;
}
//other init code
}
为了消除编译器的警告,于是我们做了一番修改:
struct region { int size; };
void init_region(char* backing_buffer, size_t buffer_size ) {
if(buffer_size < (size_t)current_region.size) {
LOG(“Buffer size too small”);
return;
}
//other init code
}
ok!现在没有警告了!
但是。。。这样做合理吗?或者,这么做安全吗?
一段时间后,如果我们修改代码:
struct region {
constexpr int INVALID_SIZE = -1;
int size = INVALID_SIZE;
};
void init_region(char* backing_buffer, size_t buffer_size ) {
if(buffer_size < (size_t)current_region.size) {
LOG(“Buffer size too small”);
return;
}
//other init code
}
如果此时 current_region.size
的值为 INVALID_SIZE
,那么 (size_t)current_region.size
变为 ULONG_MAX
(unsigned long
类型最大值)。程序会以意想不到的方式失败,或者在日后某个时间点崩溃。
C++ 之父曾经说过,在任何时刻,消除
cast
都会更好
2. 为什么需要 casting
cast
允许我们:
- 操作
raw memory
- 浏览继承体系(
navigate inheritance hierarchy
)
3. 什么是类型
到底什么是类型?
类型是我们看待内存(比特位)的方式
8 个字节的内存,我们可以将其看为是一个拥有八个字节的字符串;或者是大小为 2 的 int 数组。。。
二 自动类型转换 (Automatic Type Conversion)
编译器做出的隐式转换
1. 隐式类型转换
最后一个例子中,Float
的构造函数需要去掉 Float
下面两个例子中,不管是两个整形运算或者是比较:如果两个操作数比 int
小,编译器会将两个操作数提升为 int
;如果其中一个操作数比另一个操作数小,将小的操作数(int
)提升为大的操作数(size_t
)
这种方式称为整形提升。
2. 算数类型转换 Arithmetic Type Conversion
编译器将两个 unsigned char
的运算转换为 int
,来避免溢出
3. 符号转换 Sign Conversion
4. 用户的转换操作符 User Conversion Operators
类型转换运算符重载
##
三 显式类型转换 (explicit)
1. C 风格 cast
- 创建一个
type
类型的临时变量 - 通过改变变量中每一个 bits 的含义重写类型系统
- 某些情况下可能失败
- 可用于常量表达式
- 可能造成未定义行为
- 参与操作符优先级(3)
随意的类型转换带来混乱
1982
的 16
进制为 0x7BE
,小端存储模式下,mustang
的内存模型(从低地址开始)为 :BE 07 00 00
经过 prune
函数将 car
类型的 mustang
当作 tree
类型,并将 mustang
低位的 1 个字节置为 false(0),所以 mustang 的内存模型变为:00 07 00 00
而 0x700
正好是 1792
的 16
进制
函数指针
弊端
- 语义不明。你是在截断一个变量?还是在创建一个新的变量?在调用函数?还是将所有 bit 的含义彻底改变?
- 易错。参考本节第一条(随意转型带来混乱)。
- 你不明白这个转型的意义
- 不易查找程序中 cast 的地方(Not Grep-able)
- 使 C 和 C++ 语法复杂
2. C++ 函数式 cast
- 创建临时变量
- 内置类型和 C++ 构造函数平等
- 只能使用单字的类型名
- 参与操作符优先级(2)
template<typename To, typename From>
To convert_to(From& from) { return To(from); }
class A{};
class B{};
int main()
{
int i = 7;
float f = convert_to<float>(i);
A* pa;
B* pb = convert_to<B*>(pa);
using astar = A*;
A* pa2 = astar(pb);
}
3. C++ cast 操作符
a. C++ casting
要解决的问题
- 对不同的任务使用不同的符号
- 便于辨认和查找
- 拥有和
C cast
相同的所有操作 - 消除意料之外的错误
- 第五点属于心理学范畴 233。因为 C++ 的
cast
非常难写(相比于()
),所以当你这样写的时候,你对这个转换有十足的把握。
b. static_cast
创一个 T 类型的临时变量
通过寻找两个类型之间 隐式类型转换 **, **用户定义的转换(类内转型操作符重载)和 单参构造函数。不能消除 CV 限定符。
使用场景:
- 指明 隐式类型转换
- 表明 截断
- 从派生类向基类转换
void*
与T*
之间转换
多重转换
- 类型 A 拥有一个参数为
int
的单参构造函数 - 类型 E 有一个用户定义的
int
类型操作符重载函数
继承
从派生类向基类转换,编译器会根据 基类在派生类对象中的内存布局,修改派生类指针。
struct Base1{ virtual ~Base1() = default; int i; };
struct Base2{ virtual ~Base2() = default; int i; };
struct Derived : public Base1, public Base2
{ int i; };
void cmpAddress(void* p1, void* p2)
{
if(p1 == p2) cout << "same\n";
else cout << "different\n";
}
int main()
{
Derived d;
Derived* p1 = &d;
auto p2 = static_cast<Base1*>(p1);
auto p3 = static_cast<Base2*>(p1);
cmpAddress(p1, p2);
cmpAddress(p1, p3);
// 编译器做了隐式类型转换!
if(p1 == p3) cout << "same!" << endl;
cmpAddress((char*)p1 + sizeof(Base1), p3);
}
输出:
same
different
same!
same
static_cast 不是完全可靠的
static_cast
不可用于无关类型的转换。
static_cast
也可以向下转型!
class base1 { int a; };
class base2 { int a; };
class derive : public base1, public base2 {int a;};
int main()
{
derive d;
base1* pb1 = &d ;
base2* pb2 = &d ;
cout << &d << " " << pb1 << " " << pb2 << endl;
derive* pd1 = static_cast<derive*>(pb1);
derive* pd2 = static_cast<derive*>(pb2);
cout << pd1 << " " << pd2 << endl;
return 0;
}
// 0xd293dffd54 0xd293dffd54 0xd293dffd58
// 0xd293dffd54 0xd293dffd54
加上虚函数依然可以向下转型。
c. const_cast
- 修改 CV 限定符,不修改类型
- 不修改原来那个变量的 CV 限定符
int main()
{
const int i = 7;
modify(const_cast<int*>(&i));
cout << i << endl;// 7
const int j = 7;
const int* pj = &j;
modify(const_cast<int*>(pj));
cout << *pj << endl;// 42
return 0;
}
其实 i
和 j
的值都被修改了。输出 i
的值没有变化是因为编辑器将 i
的值放入寄存器中,虽然 i
在内存中的值已经被修改,但是由于 i
是 const 类型,所有编译器默认 i
的值不会修改,所以直接输出寄存器中 i
最开始的值。
可以给 i
加上 volatile
,让 i
的内存始终可见
成员函数重载 member overload
class my_array
{
public:
my_array() : _arr("HelloWorld") {}
const char& operator[](int idx) const
{
return _arr[idx];
}
char& operator[](int idx)
{
return const_cast<char&>(
const_cast<const my_array&>(*this)[idx]
);
}
private:
char _arr[11];
};
int main()
{
const my_array arr1;
my_array arr2;
cout << arr1[3] << endl;
//arr1[3] = 10;
arr2[3] = 'X';
cout << arr2[3] << endl;
}
d. reinterpret_cast
- 用于位的简单重新解释。可以对指针和引用进行任意类型的转换!
- 也称为 类型双关(绕开类型系统)
- 不能使用
constexpr
- 不能移除
cv
限定符 - 转换类型之间的大小不需要一样
- 对于内存映射功能很有用
可访问私有的基类
类型别名 Type Aliasing
两个类型的内存布局是兼容的时候,这两个类型间是可以转化的。
The act of using the memory of one type as if it were a different type when the memory layouts of the two types are compatible
What is Strict Aliasing and Why do we Care? (github.com)
应用于哈希函数
reinterpret_cast
的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。#include <iostream> using namespace std; // Returns a hash code based on an address unsigned short Hash( void *p ) { unsigned int val = reinterpret_cast<unsigned int>( p ); return ( unsigned short )( val ^ (val >> 16)); }
e. dynamic_cast
- 公有继承体系
- 指针或引用
- 无法移除
cv
限定符 - 多态(需要虚函数指针)
- 需要
RTTI
- 如果类型不相关(继承树中没有通路),指针返回
nullptr
,引用抛出std::bad_cast
异常
dynamic_cast
可能开销很大!
四 C++ 是如何处理 C风格的类型转换
尝试顺序:
const_cast
static_cast
reinterpret_cast
五 其他的转换
1. move
- 将左值转为右值
- 如果存在
move ctor
则调用,否则调用copy ctor
- 不要返回
std::move(var)
。编译器会有RVO
优化。 - 等价于
static_cast<T&&>(val)