类型转换
隐式转换
C++自动执行很多类型转换:
-
将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进行转换;
-
表达式中包含不同的类型时,C++将对值进行转换;
-
将参数传递给函数时,C++将对值进行转换。
C++类型转换的规则
初始化和赋值进行的转换
扩展:将一个值赋给值取值范围更大的类型通常不会导致什么问题。
截取:将浮点型转换为整型时,C++采取截取(丢弃小数部分)而不是四舍五入(查找最接近的整数)。
使用赋值运算符给类初始化
下面是C++ Primer Plus 的一个案例:
// stonewt.h -- definition for the Stonewt class
#ifndef STONEWT_H_
#define STONEWT_H_
class Stonewt
{
private:enum {Lbs_per_stn = 14}; // pounds per stoneint stone; // whole stonesdouble pds_left; // fractional poundsdouble pounds; // entire weight in pounds
public:Stonewt(double lbs); // constructor for double poundsStonewt(int stn, double lbs); // constructor for stone, lbsStonewt(); // default constructor~Stonewt();void show_lbs() const; // show weight in pounds formatvoid show_stn() const; // show weight in stone format
};
#endif
// stonewt.cpp -- Stonewt methods
#include <iostream>
using std::cout;
#include "stonewt.h"// construct Stonewt object from double value
Stonewt::Stonewt(double lbs)
{stone = int (lbs) / Lbs_per_stn; // integer divisionpds_left = int (lbs) % Lbs_per_stn + lbs - int(lbs);pounds = lbs;
}// construct Stonewt object from stone, double values
Stonewt::Stonewt(int stn, double lbs)
{stone = stn;pds_left = lbs;pounds = stn * Lbs_per_stn +lbs;
}Stonewt::Stonewt() // default constructor, wt = 0
{stone = pounds = pds_left = 0;
}Stonewt::~Stonewt() // destructor
{
}// show weight in stones
void Stonewt::show_stn() const
{cout << stone << " stone, " << pds_left << " pounds\n";
}// show weight in pounds
void Stonewt::show_lbs() const
{cout << pounds << " pounds\n";
}
如果编写这样的代码:
Stonewt myCat;
myCat=19.6;//usetStonwt(double) to convert 19.6 to Stonewt
看看这是什么意思?我们使用了赋值运算符,可是=两边的类型不匹配,究竟发生了什么?可能你已经习惯了这种用法,下面用类和对象的知识解释一下:
程序将使用构造函数 Stonewt(double)来创建一个临时的 Stonewt 对象,这个临时对象用19.6初始化值。然后调用默认拷贝构造函数,将tmp 的值复制到myCat。就像这样:
Stonewt myCat;
Stonewt tmp(19.6)
myCat=tmp;//usetStonwt(double) to convert 19.6 to Stonewt
只接受一个参数的构造函数定义了从参数类型到类类型的转换。特别要注意的是:当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造函数 Stonewt(long),则编译器将拒绝这些语句,可能指出:int 可被转换为 long 或 double,因此调用存在二义性。
但是,当我们去掉Stonewt(double lbs);函数,并且Stonewt(int stn, double lbs=0.6); 第二个参数有默认值,结果看起来与我们设想的不一样:
class Stonewt
{
private:enum { Lbs_per_stn = 14 }; // pounds per stoneint stone; // whole stonesdouble pds_left; // fractional poundsdouble pounds; // entire weight in pounds
public://Stonewt(double lbs); // constructor for double poundsStonewt(int stn, double lbs=0.6); // constructor for stone, lbsStonewt(); // default constructor~Stonewt();void show_lbs() const; // show weight in pounds formatvoid show_stn() const; // show weight in stone }; // stonewt.cpp -- Stonewt methods
#include <iostream>
using std::cout;
Stonewt::Stonewt(int stn, double lbs)
{stone = stn;pds_left = lbs;pounds = stn * Lbs_per_stn + lbs;
}Stonewt::Stonewt() // default constructor, wt = 0
{stone = pounds = pds_left = 0;
}Stonewt::~Stonewt() // destructor
{
}// show weight in stones
void Stonewt::show_stn() const
{cout << stone << " stone, " << pds_left << " pounds\n";
}// show weight in pounds
void Stonewt::show_lbs() const
{cout << pounds << " pounds\n";
}#include <iostream>
using std::cout;
using std::endl;
int main() {/*double a = 34.5643;int b=static_cast<int>(a);cout<<b<<endl;*/Stonewt wt=19;wt.show_stn();//19 stone, 0.6 poundswt.show_lbs();//266.6 poundsStonewt wt1=19.8;wt1.show_stn();//19 stone, 0.6 poundswt1.show_lbs();//266.6 poundsStonewt wt2=19.5;wt2.show_stn();//19 stone, 0.6 poundswt2.show_lbs();//266.6 poundsreturn 0;
}
如果外界有人使用Stonewt wt1=19.8; 不了解Stonewt类的构造函数,是不是容易出现意料之外的结果?当然外界有人就会认为可以用double值给Stonewt赋值,结果与预想完全不同。
因此,C++新增了关键字 explicit,用于关闭这种自动特性。所以应该在构造函数前尽可能加上explicit,避免不必要的二次转换。
explicit Stonewt(int stn, double lbs=0.6);
类的转换函数
是否可以将 Stonewt 对象转换为 double 值,就像如下所示的那样?
可以这样做,但不是使用构造函数。构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的 C++运算符函数——转换函数。
转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。
请注意以下几点:
-
转换函数必须是类方法;
-
转换函数不能指定返回类型;
-
转换函数不能有参数。
例如,转换为 double 类型的函数的原型如下:
operator double();
我们在类中加上声明和定义:
operator int() const;
operator double() const;Stonewt::operator int() const
{return int(pounds + 0.5);}Stonewt::operator double()const
{return pounds;
}
现在Stonewt类可以隐式转换为double类和int类了。但是要避免歧义。比如下面要转成long类型,两种转换函数都可以用,我们只能选择一种转换函数。
long l = int(wt1);
long l = (double)wt1;
当然,一些不必要的隐式转换也要避免,C++11提供的explicit关键字能起到规避作用,在修改后:
explicit operator int() const;
explicit operator double() const;
这里是必须显式转换,避免了隐式转换。
更推荐的写法是使用static_cast显式转换
long l = static_cast<int>(wt1);
long l = static_cast<double>(wt1);
以{ }方式初始化时进行的转换(C++11)
C++11 将使用大括号的初始化称为列表初始化(list-initialization),因为这种初始化常用于给复杂的数据类型提供值列表。
当类有一个接受 std::initializer_list
参数的构造函数,或者有匹配参数列表的构造函数,并且这些构造函数不是 explicit
的时候,列表初始化会尝试进行隐式转换。
如果构造函数被声明为 explicit
,则不能通过列表初始化进行隐式转换。这防止了意外的类型转换,提高了代码的安全性和清晰度。
const int code = 66;
int x = 66;
char c1 {31325};//narrowing, not allowed
char c2 = {66};//allowed because char can hold 66char c3 {code};//ditto
//这里使用了常量 `code` 来初始化 `char` 类型的变量 `c3`。由于 `code` 的值是66,在 `char` 的范围内,所以这是允许的。
//注释中的“ditto”意味着这与前面的例子类似,即初始化是安全的,不会发生窄化。char c4 = {x};// not allowed x is not constant
//尽管 x 的当前值也是66,但由于 x 不是一个常量,编译器不能保证它的值在未来不会改变。
//因此,当使用花括号进行列表初始化时,编译器会拒绝这种可能引起窄化的转换,即使在当前情况下它是安全的。x = 31325;char c5 = x;//allowed by this form of initialization
C++11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义
的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加:
int x ={5};
double y {2.75};
short quar[5] {4,5,2,76,1};
另外,列表初始化语法也可用于new表达式中:
int * ar = new int [4] {2,4,6,7};// C++11
创建对象时,也可使用大括号(而不是圆括号)括起的列表来调用构造函数:
class Stump{
private:int roots;double weight;
public:Stump(int r,double w):roots(r),weight(w){}
};Stump s1(3,15.6);// old styleStump s2{5,43.4};// C++11
Stump s3 = {4,32.1};;//C++11
初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作:
char c1 = 1.57e27;// double-to-char,undefined behavior
char c2 = 459585821;// int-to-char, undefined behavior
然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它“窄”的变量中:
char c1 {1.57e27};// double-to-char,compile-time error
char c2 = {459585821};// int-to-char,out of range, compile-time error
但允许转换为更宽的类型。另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的:
char c1 {66};// int-to-char, in range, allowed
double c2 ={66};//int-to-double,allowed
表达式中的转换
整型提升(integral promotion):当运算涉及两种类型时,较小的类型将被转换为较大的类型。
简单地说,有符号整型按级别从高到低依次为 long long、long、int、short 和 signed char。无符号整型的排列顺序与有符号整型相同。类型 char、signed char和 unsigned char 的级别相同。类型 bool 的级别最低。wchar_t、char16_t 和 char32_t 的级别与其底层类型相同。
-
传递参数时的转换
-
强制类型转换
显式转换
强制转换的通用格式如下:
(typeName) value// converts value to typeName type
typeName (value)// converts value to typeName type
第一种格式来自C语言,第二种格式是纯粹的C++。新格式的想法是,要让强制类型转换就像是函数调用。这样对内置类型的强制类型转换就像是为用户定义的类设计的类型转换。C++还引入了4个强制类型转换运算符,对它们的使用要求更为严格,这将在第15章介绍。在这四个运算符中,static_cast<>可用于将值从一种数值类型转换为另一种数值类型。
static_cast<typeName> (value)// converts value to typeName type
Stroustrup认为,C语言式的强制类型转换由于有过多的可能性而极其危险。运算符 static_cast<>比传统强制类型转换更严格。
四种强制类型转换
为什么C语言的强制转换不继续用,反而C++自己搞一套呢?
Here is a short quote from Bjarne Stroustrup’s (the author of C++) book The C++ Programming Language 4th edition - page 302.
下面是Bjarne Stroustrup(C++的作者)的书The C++ Programming Language第4版-第302页的简短引用。
This C-style cast is far more dangerous than the named conversion operators because the notation is harder to spot in a large program and the kind of conversion intended by the programmer is not explicit.
Static Cast
static_cast操作符是C++中最常用的类型转换操作符。它执行编译时类型转换,主要用于编译器认为安全的显式转换。常用场景:
-
基本数据类型之间的转换,如把 int 转换为 char,这种转换带来的安全性问题由程序员来保证。
-
相关类型的转换(类有转换函数),比如上面提到的Stonewt类可以转换为double类和int类。
-
把void* 类型的指针转换为任意类型指针。
-
基类和派生类的转换:基类->派生类:安全;派生类->基类:安全。
Dynamic Cast
reference:An In-Depth Guide to Static_Cast vs Dynamic_Cast in C++ - LinuxHaxor
18.9 动态类型转换 - LearnCPP 中文教程
Syntax语法
dynamic_cast<new_type>(expression)
与static_cast
不同,dynamic_cast
验证对象expression
的求值结果确实是类型new_type
,结果如下:
-
If conversion succeeds, it returns a pointer/reference to the casted object.
如果转换成功,它返回一个指向转换对象的指针/引用。 -
If conversion fails and
new_type
is a pointer, it returns nullptr.
如果转换失败并且new_type
是指针,则返回nullptr。 -
If conversion fails and
new_type
is a reference, it throws std::bad_cast exception.
如果转换失败,并且new_type
是引用,它会抛出std::bad_cast异常。
Requirements要求
- Base class must contain at least one
virtual
function fordynamic_cast
to work properly.
基类必须包含至少一个virtual
函数,才能使dynamic_cast
正常工作。 - Dynamic memory allocation – it does not work on stack variables.
动态内存分配-它不适用于堆栈变量。
相比于static_cast,dynamic_cast运行时需要一点额外的开销。有时,在进行向下类型转换时,我们知道正在处理的是何种类型,这时使用dynamic_cast产生的额外开销就没有必要,可以通过使用static_cast来代替它。 但是如果不符合需要的类型,static_cast会返回错误的指针或引用。
某些时候使用向下转换是一个更好的选择:
- 当你不能修改基类来添加一个虚函数时(例如:因为基类是标准库中的一种)
- 当你仍然需要访问一些只有派生类独有的东西时(例如:某个访问函数只存在于派生类)
- 当添加一个虚函数到你的基类毫无意义时(例如:没有一个合适的值让基类返回)使用纯虚函数也许可以纳入考虑,如果你不要实例化基类。
Const Cast
用于常量指针(或引用)与非常量指针(或引用)之间的转换。
将const
对象转换成 non-const
对象,慎用。
Reinterpret Cast
reference: https://stackoverflow.com/questions/573294/when-to-use-reinterpret-cast
c++ - When should static_cast, dynamic_cast, const_cast, and reinterpret_cast be used? - Stack Overflow
https://www.geeksforgeeks.org/reinterpret_cast-in-c-type-casting-operators/
这种类型转换比较随意,慎用。