一、隐式类型转换
1、整数类型转换
- 整数能被转换成其他整数类型。一个普通的枚举类型值也能转换成整数类型。
- 如果目标类型是unsigned的,则结果值所占的二进制位数以目标类型为准(如有必要,会丢掉靠前的二进制位)。更准确地说,转换前的整数值对2的n次幂取模后所得的结果就是转换结果,其中n是目标类型所占的位数。例如:
unsigned char uc = 1023; //二进制 1111111111: uc的值变为二进制11111111,即,255
- 如果目标类型是signed的,则当原值能用目标类型表示时,它不发生改变; 反之,结果值
依赖于具体实现。signed charsc = 1023; //依赖于实现, 结果可能是127或者-1
2、浮点数类型转换
3、指针和引用类型转换
- 任何指向对象类型的指针都能隐式地转换成void*。指向派生类的指针(或引用)能隐式地转换成指向其可访问的且明确无二义的基类的指针(或引用)。
- 指向函数的指针和指向成员的指针不能隐式地转换成 void* 。
- 求值结果为0的常量表达式能隐式地转换成任意指针类型的空指针。类似
地,求值结果为0的常量表达式也能隐式地转换成指向成员的指针类型。例如:
int* p=(1+2)*(2*(1-1));//正确,但是让人感觉很奇怪, 最好直接使用nullptr。
4、布尔值类型转换
- 指针、整数和浮点数都能隐式地转换成bool类型。
- 指针隐式转换为bool, 非空指针对应true, 值为nullptr的指针对应为false。
- 整数值隐式转换为bool, 非0整数值对应true, 而0对应false。
- 浮点数隐式转换为bool, 非0浮点数对应为true,0对应false。(浮点数判断是否为0,存在精度问题,因此不推荐使用该隐式转换)
- 指针向布尔值的类型转换在条件中很有用,但是其他情况下可能造成误解。例如:与 if (p!=nullptr) 相比,if § 更好,它不但简洁而且可以直接表达“p是否有效”的含义, 使用if§也不太容易出错。
void fi(int p); void fb(bool p); void ff(int* p, int* q) { if (p) do_something(*p); //OK if (q!=nullptr) do_something(*q); //正确,但是显得啰嗦 //... fi(p); //错误:不存在指针向整数的类型转换 fb(p); //OK: 指针向布尔值的类型转换(感觉奇怪吧, 这就是很容易造成误解) }
5、浮点数向整数的类型转换
- 当浮点值转换成整数值时,浮点值的小数部分被忽略掉了。换句话说,从浮点数向整
数的类型转换会导致截断。例如,int(1.6)的值是1。如果被截断的值不能用目标类型表示
则该行为是未定义的。例如:int i= 2.7; char b = 2000.7; //当char占8位时是未定义的; 2000不能用8位的字符表示
- 反之,只要硬件条件允许,那么从整数向浮点数的转换从数学意义上来说就是合法的。只有
当某个整数值无法用浮点类型完整表示时,才会出现精度的损失。例如://i的值变为2 int i = float(1234567890); //在一台int和float都用32位表示的机器上,转换后的i值是1234567936。
6、指向成员的指针的类型转换
二、显式类型转换
1、构造
用值e构建一个类型为T的值可以表示为T{e}。符号T{V}有一个好处,就是它只执行“行为良好的”类型转换。使用 {} 构建的对象,它能防止窄化转换。
auto d1 = double{2}; // d1==2.0
double d2 {double{2}/4}; // d1==0.5
这句话的意思是:
- 如果一种整型存不下另一种整型的值,则后者不会被转换成前者。例如,允许char到int的类型转换,但是不允许int到char的类型转换。
- 如果一种浮点型存不下另一种浮点型的值,则后者不会被转换成前者。例如,允许 float 到 double 的类型转换,但是不允许 double 到 float 的类型转换。
- 浮点型的值不能转换成整型值。
- 整型值不能转换为浮点型的值。
void f(int);
void f(double);
void g(int i, double d)
{
f(i); //call f(int)
f(double{i}); //error :{} doesn’t do int to floating conversion
f(d); //call f(double)
f(int{d}); //error:{} doesn’t truncate
f(static_cast<int>(d)); //call f(int) with a truncated value
f(round(d)); //call f(double) with a rounded value
f(static_cast<int>(lround(d))); //call f(int) with a rounded value
// if the d is overflows the int, this still truncates
}
2、named cast
1) static_cast
- static_cast 执行关联类型之间的转换, 比如一种指针类型向同一个类层次中其他指针类型的转换, 或者整数类型向枚举类型的转换, 或者浮点类型向整数类型的转换。它还能执行构造函数和转换运算符定义的类型转换。
- 简单来说, 就是以前C语言中可以隐式类型转换的, 在C++中都可以用 static_cast 来进行类型转换。
double d = 1.1; int a = static_cast<int>(d); short s = static_cast<short>(d); char x = 'a'; int∗ p1 = &x; //error: 不存在char*向int*的隐式类型转换 int∗ p2 = static_cast<int∗>(&x); //error: 不存在char*向int*的隐式类型转换 int∗ p3 = reinterpret_cast<int∗>(&x); //OK: 责任自负
2) reinterpret_cast
- reinterpret_cast 处理非关联类型之间的转换, 比如整数向指针的转换以及指针向另一个非关联指针类型的转换。
- 在C语言中如果使用强制类型转换的场景,那么在C++中就可以使用reinterpret_cast来进行强制类型转换。比如 32位下 int 转为 指针类型,或者不同类型指针之间的转化。
int a = 100; double* pd = reinterpret_cast<double*>(a); char* pc = reinterpret_cast<char*>(pd); //绕过私有保护(应该避免编写类似欺骗性的代码) class X { private: int a; //与 X里面a对应 int b; //与 X里面b对应 }; class Y { public: int a; //与 X里面a对应 int b; //与 X里面b对应 }; void Func(X* xPtr) { //通过取地址 (reinterpret_cast<Y*>(xPtr))->b = 2; }
3) const_cast
- const_cast, 参与转换的类型仅在const修饰符及volatile修饰符上有所区别。
- const_cast用来消除变量的const属性。一般去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行读写操作了,如果对象本身不是一个常量,使用强制类型转换获取写权限是合法行为。如果对象是一个常量,再使用const_cast 进行写操作就会产生其它未定义的结果。
/*编译器报错的情况*/
const int a = 100;
int* p = &a; //error: 不存在const int*到int*的转换
*p = 200;
/*编译通过的情况*/
const int a1 = 100;
int* pa = const_cast<int*>&a1;
*pa = 200;
/*修改字面量出现未定义的异常*/
const char* m = "wangjing";
char* n = const_cast<char*>(m);
n[0] = 's'; // Exception thrown: write access violation
- 在C语言中,被const修饰的变量具有常量的属性,但是由于他是存在栈区的,所以又称为常变量,他的本质上还是变量,其实还是可以修改的,只是该变量不能作为左值来进行修改。
const int a = 10;
int* pa = (int*)&a;
(*pa)++;
printf("%d\n", a); // 11
printf("%d\n",*pa); // 11
- 但是在C++中,被const修饰的对象表示将其放进符号表,那么这样一来,当我们只需要读取该数据而不发生写入时,就不会去访问该对象的内存,而是直接从符号表(简单理解为从寄存器中拿)读取数据。
const int a = 10;
int* pa = (int*)&a;
(*pa)++;
printf("%d\n", a); // 10
printf("%d\n",*pa); // 11
- 如何让编译器在读取的时候去读内存中的对象而不是直接从符号表拿呢?通过 volatile,可以让const修饰的变量保持内存可见性。不管发不发生写入,都是去内存中读取该对象,不会优化为直接在符号表读。而 const_static 的作用就是将 const 对象转换为非 const 对象,因为在C++中,const修饰的变量还是存在栈区的,所以其实还是可变的。
volatile const int a = 10; int* pa = (int*)&a; (*pa)++; printf("%d\n", a); // 11 printf("%d\n",*pa); // 11
- 对于基本数据类型char、float、int、double等通过引用的方式修改常量的值,再通过常量名来访问是无效的,g++编译器会对该常量进行优化,代码中的变量名其实直接被替换成常量的原始值,实际上变量名代表的常量是被修改的,只能通过指针和引用的方式可以得到修改的值。
const int a = 10; int* pa = (int*)&a; (*pa)++; //如果没有volatile修饰,访问变量名,还是直接从符号表读取值 printf("%d\n", a); // 10 printf("%d\n",*pa); // 11
4) dynamic_cast
- dynamic_cast执行指针或者引用向类层次体系的类型转换,并执行运行时检查。
- 用于将一个父类指针或引用转换为子类指针或引用,前提是父类必须有虚函数。父类的指针或者引用指向子类,这是天然支持的,中间不发生类型转换,这叫做向上转换,但是在有些场景下,需要将这些虽然类型是父类,但是实际指向的是子类对象的指针或引用转换回子类类型,我们就可以使用dynamic_cast,dynamic_cast用于向下转换。如果是指针,向下转换失败时,它的返回值是 0 ,所以我们在使用dynamic_cast 得到一个指针之后,注意要先判断它是不是 nullptr ,如果是nullptr就说明转型失败了。
- 在类层次间进行上行(子类转换成父类)转换时,dynamic_cast和static_cast的效果是一样的;
- 在进行下行转换(父类转换成子类)时,dynamic_cast具有类型检查的功能,比static_cast更安全。
class Base {};
class Derived : public Base {};
Derived d;
Base* b = static_cast<Base*>(&d); // 向上转型
Derived* derivedPtr1 = static_cast<Derived*>(b);//向下转型
Derived* derivedPtr1 = dynamic_cast<Derived*>(b); //error: source type is not polymorphic
5) 使用建议
在决定使用显式类型转换之前,请花时间仔细考虑一下是否真的必须这么做。很多情况下,C或者早期版本的C++需要用到显式类型转换,但是现在的C++语言并不需要。在很多程序中,我们完全可以避免使用显式类型转换:即使使用,也应该限定在少量代码片段中。
3、C风格类型转换
- C++从C继承了符号 (T) e,它可以执static_cast、reinterpret_cast和const_cast任意组合之后得到的类型转换,它的含义是从表达式e得到类型为T的值。
- 不幸的是,C风格的类型转换允许把类的指针转换成指向该类私有基类的指针。切记永远不要这么做,而且最好祈祷你的编译器能发现此类错误。C风格的类型转换比命名的转换运算符危险得多,原因是我们很难在一个规模较大的程序中定位它,并且不容易理解程序员真实的类型转换意图。
- (T) e 可能是执行关联类型之间的可移植的类型转换,也可能是非关联类型之间的不可移植的类型转换,还有可能是移除掉指针类型前面的const修饰符。由于T和e的类型并不明确,所以我们无法得到确切的结论。
4、函数化类型转换
- 用值e构建类型T的值的过程可以表示为函数形式的符号T(e)。T(e) 有时称为函数形式的转换(function-style cast)。不幸的是,对于内置类型T来说,T(e)等价于(T) e。这意味着对于大多数内置类型来说,T(e)并不安全。
- 即使是从一种较长的整数类型向较短的整数类型(比如long向char)的显式类型转换也会导致不可移植的依赖于实现的行为。
- 建议用 T {V} 处理行为良好的构造,用命名的转换(比如static_cast)处理其他任务。