C/C++共有的类型转换与c++特有的四种强制类型转换
前言
C 语言和 C++ 共有的类型转换:
- 自动类型转换(隐式类型转换): 编译器在某些情况下会自动进行的类型转换。
- 强制类型转换(显示类型转换): 使用
(type)expression
或type(expression)
语法进行的类型转换。
C++ 中新增的类型转换关键字:
- static_cast
- const_cast
- reinterpret_cast
- dynamic_cast
这四个关键字用于支持 C++ 特有风格的强制类型转换,它们相比 C 风格的强制类型转换提供了更精细的控制和更好的类型安全性。
C/C++共有的类型转换
自动类型转换(隐式类型转换)
隐式类型转换是指编译器在没有明确指示的情况下自动进行的类型转换。在算术和逻辑运算中,常见的隐式转换规则如下:
算术运算中的类型转换:
在 C/C++ 中,表达式的两个操作数可能是不同类型的,比如一个是 int,一个是 float。为了进行运算,它们必须是相同的类型。这时候,编译器就需要根据一定规则来“提升”它们,使它们变为同一种类型,然后再进行运算。
常用算术转换主要发生在以下运算符的操作数上:
- 二元算术运算符:+、-、*、/、%(模运算只用于整数)
- 位运算:&、|、^、<<、>>
- 比较运算符:<、>、<=、>=、==、!=
常用算术转换流程图:
1. 是否含有 long double?→ 转为 long double
2. 否 → 是否含有 double?→ 转为 double
3. 否 → 是否含有 float?→ 转为 float
4. 否 → 整型提升:
a. char/short → int
b. 比较剩余整型:unsigned long long > long long > unsigned long > long > unsigned int > int
由于之前我没有了解过long double类型,所以特意来补充介绍一下这个类型。
逻辑运算和关系运算中的类型转换:
在逻辑运算(&&、||、!)和关系运算(<、>、<=、>=、==、!=)中,操作数通常会被转换为 bool 类型进行求值。任何非零值都会被转换为 true,而零值会被转换为 false。运算的结果也是 bool 类型。
赋值运算中的类型转换:
在赋值运算(=)中,右侧表达式的值会被转换为左侧变量的类型。这可能会导致数据丢失或精度损失。
int i = 3.14; // double 类型的 3.14 会被截断为 int 类型的 3
double d = 10; // int 类型的 10 会被提升为 double 类型的 10.0
隐式类型转换的注意
数据丢失和精度损失:
浮点型转换为整型: 当将 float
或 double
类型的值赋给 int
或其他整型变量时,小数部分会被直接截断,导致数据丢失。
精度降低的浮点类型转换: 将 double
类型的值赋给 float
类型变量时,可能会损失精度,因为 float
的精度范围比 double
小。
有符号类型转换为无符号类型: 当将一个负的有符号整数赋给一个无符号整数变量时,其值会根据无符号类型的范围进行重新解释,通常会变成一个非常大的正数。
意想不到的符号性问题:
有符号和无符号整数的混合运算: 当有符号和无符号整数进行算术运算时,通常会将有符号整数隐式转换为无符号整数。如果带符号数是负数,这会导致非常令人困惑的结果。
函数参数的隐式转换:
当向函数传递参数时,如果实参的类型与形参的类型不完全匹配,编译器可能会尝试进行隐式转换。这可能导致类型不匹配的错误,或者调用了意想不到的函数重载版本。
隐式类型转换使用建议
- 尽量保持类型一致: 在进行运算和赋值时,尽量使用相同或兼容的类型,减少隐式转换的发生。
- 尽量不要有符号类型和无符号类型混用: 因为这样会涉及到 负数的意外转换,比较运算的陷阱,算术运算的意外结果等,这类bug非常难排查!!!
- 使用显式类型转换: 当需要进行类型转换时,使用
static_cast
、dynamic_cast
、const_cast
或reinterpret_cast
等 C++ 风格的强制类型转换运算符,明确表达你的意图,并让编译器进行必要的类型检查(在适用情况下)。
强制类型转换(显示类型转换)
(type)expression
是对紧跟在后面的变量或子表达式进行的类型转换,默认情况下,它只对紧随其后的那个操作数起作用。
int a = 5;
int b = 2;
float result;// 示例 1:只转换 'a'
result = (float) a / b;
// 这里,(float) 只作用于变量 'a'。'a' 先被转换为 float (5.0),然后与 'b' (int 型,会被隐式转换为 float) 相除,结果是 float (2.5)。// 示例 2:只转换 'b'
result = a / (float) b;
// 这里,(float) 只作用于变量 'b'。'b' 先被转换为 float (2.0),然后 'a' (int 型,会被隐式转换为 float) 与其相除,结果是 float (2.5)。// 示例 3:转换整个表达式的结果
result = (float) (a / b);
// 这里,括号内的 (a / b) 会先进行整数除法运算,结果是 int (2)。然后,(float) 将这个整数结果 2 转换为 float (2.0)。// 示例 4:不加括号,只转换 'a'
result = (float) a + b;
// 这里,(float) 只作用于 'a','a' 被转换为 float (5.0),然后与 'b' (int 型,会被隐式转换为 float) 相加,结果是 float (7.0)。// 示例 5:转换 'b'
result = a + (float) b;
// 这里,(float) 只作用于 'b','b' 被转换为 float (2.0),然后 'a' (int 型,会被隐式转换为 float) 与其相加,结果是 float (7.0)。
type(expression)
这种语法看起来像一个函数调用,其中 type
是目标数据类型,expression
是要转换的值。
double double_num = 3.14159;
int integer_part;integer_part = int (double_num); // 将 double_num 强制转换为 int 类型
算法竞赛中注意
int a;
int b;
long long c = a + b;
你可能乍一看觉得上面这个代码很对,甚至会沾沾自喜,觉得自己考虑到了a+b的结果会溢出int范围,所以将c设置成了long long类型,但是我只能说你只做对了一半~
事实上:上面这个写法在发生溢出时候,最终 c
依然得不到正确的数学结果!!!
为什么在a+b溢出时,不能得到正确结果?
运算的类型决定结果的类型: 在 C/C++ 中,算术运算的结果类型通常由参与运算的操作数的类型决定。在 a + b
这个表达式中,a
和 b
都是 int
类型,因此,它们的加法运算会以 int
类型的规则进行。
赋值给 long long
的时机: 即使你将 a + b
的结果赋值给 long long
类型的变量 c
,溢出也可能在 a + b
运算完成之后才发生。当 a + b
发生溢出并产生一个错误的 int
值后,这个错误的 int
值才会被提升(隐式转换)为 long long
类型并赋给 c
。此时,c
存储的仍然是那个错误的、溢出后的值,而不是 a
和 b
的真实数学和。
如何避免溢出并得到正确结果?
将其中一个操作数/两个操作数全部强制转换为 long long,注意一定不是对于整个表达式a+b进行强制类型转换
long long c = (long long)a + b; // 或者 long long c = a + (long long)b;
long long c = (long long)a + (long long)b;
注意千万不能像下面这样对a+b这个表达式进行转化
long long c = (long long)(a + b);
这种写法只是将可能已经溢出的 int 结果转换为 long long 类型,并不能防止 a + b 运算过程中 int 类型的溢出。要得到正确的 long long 结果,你需要确保加法运算本身是以 long long 的精度进行的。
c++特有的四种强制类型转换
static_cast<目标类型>(表达式);
const_cast<目标类型>(表达式);
reinterpret_cast<目标类型>(表达式);
dynamic_cast<目标类型>(表达式);
static_cast
static_cast: 用于执行可以在编译时确定的类型转换,例如基本数据类型之间的转换(但不会进行安全性检查,例如从 int* 到 char*),以及具有继承关系的类型之间的转换(向上转型是安全的,向下转型需要程序员保证安全性)。
double d = 3.14;
int i = static_cast<int>(d);class Base {};
class Derived : public Base {};
Derived* derivedPtr = new Derived();
Base* basePtr = static_cast<Base*>(derivedPtr); // 向上转型
dynamic_cast
dynamic_cast: 主要用于在继承层次结构中执行安全的向下转型。它会在运行时检查转换是否有效,如果转换不合法(例如,尝试将一个基类指针转换为一个派生类指针,而该基类指针实际上指向的是基类对象),则返回空指针(对于指针类型)或抛出 std::bad_cast 异常(对于引用类型)。dynamic_cast 只能用于具有虚函数的基类。
class Base { virtual void func() {} };
class Derived : public Base {};
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 向下转型,运行时检查
if (derivedPtr) {// 转换成功
} else {// 转换失败
}
const_cast
const_cast: 用于添加或移除变量的 const 或 volatile 限定符。通常情况下,应该避免使用 const_cast!!!因为它可能会破坏程序的常量性。
const int ci = 10;
int* nonConstPtr = const_cast<int*>(&ci);
*nonConstPtr = 20; // 修改了原本声明为 const 的变量,可能导致未定义行为
reinterpret_cast
reinterpret_cast: 执行低级的位模式重新解释。这是最危险的类型转换运算符,应该谨慎使用。它通常用于不同类型指针之间的转换,或者将指针转换为整数类型,反之亦然。reinterpret_cast 不进行任何类型检查,仅仅是重新解释内存中的位。