类型转换

在《Effective C++》条款 27 强调尽量少做转型动作,参考此条款中的内容整理笔记。

在 C++ 中转型是一个你会想带着极大尊重去亲近的一个特性。

我们先回顾转型的语法,然后回过头再看怎么赋予类型转型的能力。

隐式转型

我们经常接触的隐式转换发生于内置类型中的数值类型之间。但往往因为过于自然,反而忽略了转型动作的存在。

1
2
3
4
5
int num = 3;
double dou = 3.14;
// conversation happenning...
double sum1 = num + dou;
int sum2 = num + dou;

隐式转换不涉及书写形式(语法),是编译器完成的自动转换。期间发生的转换大概和下面的代码等价:

1
2
double sum1 = (double)num + dou;
int sum2 = (int)((double)num + dou);

对于内置的数值类型来说,隐式转换使得大部分数值之间的运算符合日常习惯,但也不全是。比如,doubelint 转型时发生的截断,比如除法

1
2
3
4
int num = 7;
int num1 = 3;
double dou = num / num1;
printf("%lf\n", dou);

输出 2.00000,此时隐式转换不能满足我们的需求。

ps:后文中会提到对于涉及类类型的隐式转换只能够进行一步。我想对于内置类型同样适用,不过话说回来,内置类型之间的转换往往都是一步到位,似乎不存在 A 转 B 再转 C 的情况。

关于隐式转换,更多请参考 C++ 隐式类型转换,其中提到隐式转换的必要性和隐式转换发生的条件,也讲到了其中的风险。static_cast 的存在就是为了把隐式转换显现出来。

显式转型

old-style casts

C 风格的转型动作看起来像这样:TYPE b = (TYPE)a;

在《EC++》中还提到了函数风格的转型动作 Type(a);,但这限于 C++ 的语法(g++ 可以正常编译),此种形式使用 gcc 编译无法通过。

new-style casts

C++ 提供了四种新式转型:const_castreinterpret_castdynamic_cast 以及 static_cast

const_cast

const_cast<T>(expression) 可去除对象的常量性(const),它还可以去除对象的易变性(volatile)。const_cast 的唯一职责就在于此,若将 const_cast 用于其他转型将会报错。

例子一

1
2
3
4
5
6
7
8
9
10
 void print (char * str)
{
cout << str << endl;
}
int main ()
{
const char * c = " http://www.cppblog.com/kesalin/";
print ( const_cast<char *> (c) );
return 0;
}

例子二

1
2
3
4
5
6
7
struct SA {
int i;
};
const SA ra;
//ra.i = 10; //直接修改const类型,编译错误
SA &rb = const_cast<SA&>(ra); // 将 const 转为 non-const
rb.i =10;

ps:这篇笔记不再针对 const_cast 做更进一步的讲解,需要认识到:

const_cast 转换符不该用在对象数据上,而应该用在其指针(或引用)上。

修改 const 变量的数据不是 C++ 去 const 的目的!即,使用 const_castconst 属性不是为了修改 const 变量的数据。

更多内容请移步:C++标准转换运算符const_cast

reinterpret_cast

reinterpret_cast<T>(expression) 只用于底层代码,一般我们都用不到它,如果你的代码中使用到这种转型,务必明白自己在干什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void)
{
int *pI = new int(5);
void *pV = pI;

int *pD = reinterpret_cast<int*>(pV); // 将 void* 指针转为 typed 指针

cout << "pI is: " << pI <<
" And pV is: " << pV <<
" And pD is: " << pD << endl;

cout << (*pD) << endl;
return 0;
}

dynamic_cast

dynamic_cast<T>(expression) 主要用来在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Base
{
public:
virtual void Print(){} //基类必须有虚函数。保持多态特性才能使用 dynamic_cast
};

class Derived : public Base
{
public:
Derived():Base(), m_str("Derived") {}
void foo() { cout << "foo() " << m_str << endl; } // 派生类独有
private:
string m_str;
};

void Print_dynamic(Base* pb)
{
cout << "Before cast, address is: " << pb << endl;
Derived *pd = dynamic_cast<Derived*>(pb); // 将 pointer-to-base 转为 pointer-to-derived
cout << "After dynamic_cast, address is: " << pd << endl;
if (pd)
pd->foo();
}
void Print_static(Base* pb)
{
cout << "Before cast, address is: " << pb << endl;
Derived *pd = static_cast<Derived*>(pb);
cout << "After static_cast, address is: " << pd << endl;
if(pd)
pd->foo();
}

int main(void)
{
Base *pb1 = new Derived();
Base *pb2 = new Base();
Print_dynamic(pb1);
Print_dynamic(pb2); // 安全的,转型结果为 NULL

cout << endl << "---------" << endl;

Print_static(pb1); // 能够运行下去。只是钻了个空子
Print_static(pb2); // 转型结果不为空,但也算不得有效值!访问子类成员崩溃

return 0;
}

强调几点:

  • 基类必须有虚函数。保持多态特性才能使用 dynamic_cast
  • 基类指针转子类,使用 dynamic_cast 是正确且安全的,在基类指针指向基类对象(或其它子类对象)时转型失败,结果为空,做出及时终止,未使得程序带错运行下去。
  • 基类指针转子类,使用 static_cast 可能会出现严重问题,尤其是基类指针实际指向基类对象时,static_cast 并不能做出识别,采取有效措施,使程序带错运行。

上述代码执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vimer@debian8light:~/see-the-world/code/type-conversation$ g++ dynamic.cpp && ./a.out 
Before cast, address is: 0x2439010
After dynamic_cast, address is: 0x2439010
foo() Derived
Before cast, address is: 0x2439060
After dynamic_cast, address is: 0

---------
Before cast, address is: 0x2439010
After static_cast, address is: 0x2439010
foo() Derived
Before cast, address is: 0x2439060
After static_cast, address is: 0x2439060
段错误
vimer@debian8light:~/see-the-world/code/type-conversation$

《EC++》中强调:(详情请翻书)

它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

static_cast

static_cast<T>(expression) 很像 C 语言中旧式的强制转换。《EC++》中提到:

static_cast 用来强迫隐式转换(implicit conversions),例如将 non-cast 对象转为 const 对象,或将 int 转为 double 等等。

在此特别陈述一下,static_cast 可以用来执行上述多种转换的反向转换。这些反向转换如果不使用 static_cast,都是可以隐式转换的。

转型操作符 使用场景 反向转换 隐式转换举例
const_cast 可以将 const 转为 non-const 将 non-const 转为 const string str; const string cstr = str;
reinterpret_cast 可以将 void* 指针转为 typed 指针 将 typed 指针转为 void* 指针 int *pI = new int(5); void *pV = pI;
dynamic_cast 可以将 pointer-to-base 转为 pointer-to-derived 将 pointer-to-derived 转为 pointer-to-base Derived *pD = new Derived(); Base *pB = pD;

注意:static_cast 转换时并不进行运行时安全检查,所以是非安全的,很容易出问题。因此 C++ 引入 dynamic_cast 来处理安全转型。

总结

关于类型转换,想要在代码中正确地使用还是比较简单的(想要完完全全掌握来龙去脉,掌握原理及所有使用场景还是很难的)。重点理解 static_castdynamic_cast 的应用场景及两者之间的区别

思考一reinterpret_castdynamic_cast 的某些使用场景很相似,可以类比一下。参考前文的代码,前者用于内置类型从 void* 向各种数值类型 int* double* char* 的转换,后者用于自定义类类型从基类类型 Base* 向其子类类型 DerivedA* DerivedB* DerivedC* 的转换。

思考二:我们在谈及自定义类类型继承体系中的转型时,都是在用指针。但是否可以像 intdouble 那样,从 DerivedBase(不是 Derived*Base*),从 BaseDerived

总结 && 推荐:在整理笔记的时候肯定会从网上查找资料,每个知识点肯定都会碰到一篇帖子让自己感慨“哇,总结得好好,娓娓道来,该讲的都讲到了,却也不多一句废话”,每每到此刻都觉得自己整理的笔记就是一坨垃圾,用词不当,表述不清晰,上下文转接不流畅,有的重点落下没讲,废话说太多……可这就是成长必经的过程,如果不是整理这个知识点,就不会一板一眼、较真地去查阅好多资料,线上的博客、线下的书,随手 google 来的终究只是编码过程中的 code demo,而非知识,只有当你读了很多篇笔记,看过了很多风景,才能有足够的理解,才能通过已经掌握的,通过对比,认识到某一篇是够精彩的。

如果因为总结的笔记太烂而气馁,放弃整理,那么就不会查阅足够的资料,就不会见到那篇“哇,精彩”的帖子!整理出来的笔记是结果,整理过程中见到的风景也是结果!

变量转型的能力怎么来的

上文提到的都是转型的语法,即如果从 A 类型到 B 类型可以转型,那么针对不同的转型情形应该怎么书写以达到转型的目的。但如果两者根本不存在任何的转型可能,比如,无关类型之间的转换(从 ClassAClassB),无关类型指针之间的转换(从 int*double*,从 ClassA*ClassB*),那么……

“两者根本不存在任何的转型可能”分为两种:转型是无任何意义、无价值的;有转型的意义,但未实现转型的定义。如果坚持使用转型操作(无论是隐式的还是显式),后者肯定会报错;显式转型之 new-style casts 的意义就体现在意义界定上,通过对转型分门别类,对转型的意义做了进一步的限定,相比 old-style casts 可以发现更多的错误情况。

无意义主要针对内置类型、指针;无定义主要针对自定义类型。

如果某种转型是无意义的,比如 int*double*,旧式强制转型是可以编译通过的?新式转型编译无法通过?

思考:新式转型为什么比旧式强制转型好?通过明确的声明“我要进行 XX 转型”,让编译器帮我做进一步检查,防止出现实际代码和编码意图相悖的情况,类似 override 关键词的作用?

内置类型

这个就是“天生授予”的,随着编程语言标准(standard)的更迭换代,哪些类型能够转换是有调整的。

  • 数值类型之间的转型(!数值类型指针之间不存在转型)
  • 内置类型的指针与空指针之间的转型

继承体系

了解多态,我们都知道从基类指针可以指向派生类对象,即可以将派生类指针赋值给基类指针。可以隐式转型,也可以使用 static_cast。

反向的,如果要将基类指针赋给派生类指针,需要判断 pB 指向的对象到底是 D1、D2、D3 哪种类型,这种时候需要使用 dynamic_cast。

如果不是指针(或引用)呢?

转换构造函数

《C++ Primer》P263 提到

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数。

能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

需要强调的是,这种隐式转换只允许一步类类型转换。我们知晓 string (const char* s); 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyString
{
public
MyString(string str, int num = 23) {};
//...other mems
};

int func(MyString ms) {}

// 完整的
func(MyString(string("abc"));
// 省略一步
func(string("abc")); // 显式转换成(构造) string,隐式转换成 MyString
func(MyString("abc")); // 隐式转换成 string,显式转换成(构造) MyString
// 省略两步——这是错误的
func("abc");

// 构造
MyString ms(string("abc")); // 直接初始化
MyString ms = MyString(string("abc")); // 与上述等价
MyString ms = string("abc"); // it's ok 隐式转换,然后执行拷贝构造函数

MyString ms("abc"); // 隐式转换成 string,然后直接初始化
MyString ms = MyString("abc"); // 与上述等价
MyString ms = "abc"; // sorry, it's wrong ??????????? 无法进行两步隐式转换

explicit

我们可以通过将构造函数声明为 explicit 以阻止这种隐式转换。(这种隐式转换存在什么风险呢? 参考 Explicit——谨慎定义隐式类型转换函数,感觉有些乱)

显式转型中旧式转型形式 MyString(str); 直接理解为构造更直观、合适;(MyString)str; 形式更容易理解为转型。显式转型中新式转型不存在异议 static_cast<MyString>(str);

发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用 =)。进行拷贝形式的初始化时(使用 =)我们不能使用 explicit 构造函数。

当我们使用 explicit 关键字声明构造函数时,它将只能以直接初始化的形式使用。编译器将不会在自动转换过程中使用该构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyString
{
public
explicit MyString(string str, int num = 23) {};
//...other mems
};

int func(MyString ms) {}

// 完整的
func(MyString(string("abc"));
// 省略构造 MyString 的一步——这是错误的
func(string("abc")); // 显式转换成(构造) string,无法隐式转换成 MyString
// 省略构造string 的一步—— it's ok
func(MyString("abc")); // 隐式转换成 string,显式转换成(构造) MyString
func(static_cast<MyString>("abc")); // 隐式转换成 string,显式转换成 MyString

// 构造
MyString ms(string("abc")); // 直接初始化
MyString ms = MyString(string("abc")); // 与上述等价
MyString ms = string("abc"); // it's wrong!!!!!! 无法进行隐式转换

此种情形下的转型(包括隐式转型、显式之旧式转型、显式之新式转型)都是调用的转换构造函数。其中显式之旧式转型,直接理解为构造更自然、直观。

类型转换运算符

《C++ Primer》P514

类型转换运算符(conversion operator)是类的一种特殊成员函数,它负责将一个类类型的值转换为其他类型。类型转换函数的一般形式如下所示:operator type() const;

类型转换运算符既没有显式的返回类型,而没有形参,而且必须定义成类的成员函数。通常不应该改变待转换对象的内容,因此,一般被定义成 const 成员。

避免过度使用类型转换函数!如果在类类型和转换类型之间不存在明显的映射关系,则这样的类型转换可能具有误导性。此时不定义该类型转换运算符也许会更好。作为替代的手段,类可以定义一个或多个普通的成员函数以从各种不同形式中提取所需的信息。

explicit

《C++ Primer》P516

C++11 新标准引入了显式的类型转换运算符(explicit conversion operator)。和显式的构造函数一样,编译器(通常)也不会将一个显式的类型转换运算符用于隐式类型转换。

该规定各有一个例外,即如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它。