类型转换(Type conversions)

重要的事情说三遍:

强烈建议按照目录结构中的顺序学习!!!点我查看教程目录结构

强烈建议按照目录结构中的顺序学习!!!点我查看教程目录结构

强烈建议按照目录结构中的顺序学习!!!点我查看教程目录结构

隐式转换

隐式转换在值被复制到兼容类型时会自动执行。例如:

short a = 2000;
int b;
b = a;

在这里,a 的值从 short 类型提升到 int 类型,不需要任何显式操作符。这被称为“标准转换”。标准转换影响基本数据类型,并允许在数值类型之间(例如 shortintintfloatdoubleint 等),bool 类型之间以及某些指针转换之间进行转换。

从较小的整数类型转换为 int 或从 float 转换为 double 被称为“提升”,并且保证在目标类型中产生相同的值。其他算术类型之间的转换可能无法始终表示相同的值:

  • 如果将负整数值转换为无符号类型,结果值将对应其二进制补码表示(例如,-1 成为该类型的最大值,-2 成为第二大值,依此类推)。
  • 从/到 bool 的转换将 false 视为零(对于数值类型)或空指针(对于指针类型);true 等价于所有其他值,并转换为等价的 1
  • 如果从浮点类型转换为整数类型,值会被截断(小数部分被移除)。如果结果超出类型所能表示的范围,则转换会导致未定义行为。
  • 否则,如果转换在相同种类的数值类型之间进行(整数到整数或浮点到浮点),则转换是有效的,但值是实现特定的(可能不具有可移植性)。

一些转换可能意味着精度的丢失,编译器可以通过警告信号提示。这种警告可以通过显式转换来避免。

对于非基本类型,数组和函数会隐式转换为指针,指针一般允许以下转换:

  • 空指针可以转换为任何类型的指针。
  • 任何类型的指针可以转换为 void 指针。
  • 指针上转:指向派生类的指针可以转换为指向可访问和无歧义的基类的指针,而不会修改其 constvolatile 限定。

类的隐式转换

在类的世界中,可以通过三种成员函数来控制隐式转换:

  • 单参数构造函数:允许从特定类型进行隐式转换以初始化对象。
  • 赋值运算符:允许在赋值时从特定类型进行隐式转换。
  • 类型转换运算符:允许转换为特定类型。
    例如:
// 类的隐式转换:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  // 从 A 转换(构造函数):
  B (const A& x) {}
  // 从 A 转换(赋值运算符):
  B& operator= (const A& x) {return *this;}
  // 转换为 A(类型转换运算符)
  operator A() {return A();}
};

int main ()
{
  A foo;
  B bar = foo;    // 调用构造函数
  bar = foo;      // 调用赋值运算符
  foo = bar;      // 调用类型转换运算符
  return 0;
}

类型转换运算符使用特定的语法:它使用 operator 关键字,后跟目标类型和一个空括号。注意,返回类型是目标类型,因此在 operator 关键字之前不需要指定返回类型。

关键字 explicit

在函数调用中,C++ 允许对每个参数进行一次隐式转换。这对于类来说可能有些问题,因为这并不总是预期的行为。例如,如果我们在上一个例子中添加以下函数:

void fn (B arg) {}

该函数接受类型为 B 的参数,但它也可以接受类型为 A 的对象作为参数:

fn (foo);

这可能不是预期的情况,但无论如何,可以通过使用 explicit 关键字标记受影响的构造函数来防止这种情况:

// explicit:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  explicit B (const A& x) {}
  B& operator= (const A& x) {return *this;}
  operator A() {return A();}
};

void fn (B x) {}

int main ()
{
  A foo;
  B bar (foo);
  bar = foo;
  foo = bar;
  
//  fn (foo);  // 不允许用于显式构造函数。
  fn (bar);  

  return 0;
}

此外,使用 explicit 标记的构造函数不能使用赋值语法调用;在上面的例子中,bar 不能用以下方式构造:

B bar = foo;

类型转换成员函数(在前一节中描述的那些)也可以指定为 explicit。这可以防止隐式转换,就像 explicit 构造函数对目标类型所做的一样。

类型转换

C++ 是一种强类型语言。许多转换,特别是那些涉及值不同解释的转换,需要显式转换,在 C++ 中称为“类型转换”。存在两种主要的泛型类型转换语法:函数式和 C 风格:

double x = 10.3;
int y;
y = int (x);    // 函数式语法
y = (int) x;    // C 风格类型转换语法

这些通用类型转换形式的功能对于大多数基本数据类型的需求来说已经足够。然而,这些运算符可以不加区分地应用于类和类指针,这可能导致代码在语法上正确但在运行时出错。例如,以下代码可以无错误地编译:

// 类类型转换
#include <iostream>
using namespace std;

class Dummy {
    double i, j;
};

class Addition {
    int x, y;
  public:
    Addition (int a, int b) { x = a; y = b; }
    int result() { return x + y; }
};

int main () {
  Dummy d;
  Addition* padd;
  padd = (Addition*) &d;
  cout << padd->result();
  return 0;
}

程序声明了一个指向 Addition 的指针,但随后将其分配给一个指向另一个不相关类型对象的引用,使用显式类型转换:

padd = (Addition*) &d;

不受限制的显式类型转换允许将任何指针转换为任何其他指针类型,而不考虑它们指向的类型。随后的对成员 result 的调用会产生运行时错误或其他意外结果。

为了控制这些类之间的转换,我们有四个特定的类型转换运算符dynamic_castreinterpret_caststatic_castconst_cast。它们的格式是新类型用尖括号 (<>) 包围,并紧接其后的是需要转换的表达式用括号包围。

dynamic_cast <new_type> (expression)  
reinterpret_cast <new_type> (expression)  
static_cast <new_type> (expression)  
const_cast <new_type> (expression)

这些表达式的传统类型转换等价形式是:

(new_type) expression  
new_type (expression)

但每个都有自己的特殊特性:

dynamic_cast

  • 用途:主要用于在运行时进行安全的向下转换(downcasting),即将基类指针或引用转换为派生类指针或引用。
  • 特性:使用时需要运行时类型识别(RTTI),只能用于多态类型(即包含虚函数的类)。
  • 转换失败时
    • 对指针类型,转换失败会返回nullptr
    • 对引用类型,转换失败会抛出std::bad_cast异常。

dynamic_cast 只能用于指向类的指针和引用(或 void*)。其目的是确保类型转换的结果指向目标指针类型的有效完整对象。

这自然包括指针上转(从派生类指针转换为基类指针),与隐式转换相同。

dynamic_cast 还可以将多态类(那些具有虚成员的类)从基类指针转换为派生类指针(即“下转”),前提是指向的对象是目标类型的完整对象。例如:

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

int main () {
  try {
    Base* pba = new Derived;
    Base* pbb = new Base;
    Derived* pd;

    pd = dynamic_cast<Derived*>(pba);
    if (pd == 0) cout << "第一次类型转换的空指针。\n";

    pd = dynamic_cast<Derived*>(pbb);
    if (pd == 0) cout << "第二次类型转换的空指针。\n";

  } catch (exception& e) { cout << "异常: " << e.what(); }
  return 0;
}

兼容性说明:这种类型的 dynamic_cast 需要运行时类型信息 (RTTI) 来跟踪动态类型。一些编译器支持此功能作为一个默认禁用的选项。需要启用此功能才能使 dynamic_cast 在运行时类型检查中正常工作。

上面的代码尝试从类型为 Base*pbapbb)的指针对象执行两次动态转换到类型为 Derived* 的指针对象,但只有第一次成功。注意它们各自的初始化:

Base* pba = new Derived;
Base* pbb = new Base;

尽管它们都是类型为 Base* 的指针,但 pba 实际上指向一个 Derived 类型的对象,而 pbb 指向一个 Base 类型的对象。因此,当使用 dynamic_cast 执行它们各自的类型转换时,pba 指向的是一个完整的 Derived 类对象,而 pbb 指向的是一个 Base 类对象,这是一个不完整的 Derived 类对象。

dynamic_cast 不能转换指针时(因为它不是所需类的完整对象),它会返回一个空指针以表示失败。如果 dynamic_cast 用于转换为引用类型且转换不可能,会抛出类型为 bad_cast 的异常。

dynamic_cast 还可以执行指针之间的其他隐式转换:在指针类型之间(即使在不相关的类之间)转换空指针,以及将任何类型的指针转换为 void* 指针。

static_cast

  • 用途:用于在编译时进行类型安全的转换。可以进行基本类型之间的转换、向上转换(upcasting)和向下转换(需要确保安全)。
  • 特性:不会进行运行时类型检查,只要在编译时类型兼容就可以转换。

static_cast 可以在相关类的指针之间执行转换,不仅可以上转(从派生类指针转换为基类指针),还可以下转(从基类指针转换为派生类指针)。在运行时不执行任何检查以保证被转换的对象实际上是目标类型的完整对象。因此,程序员需要确保转换是安全的。另一方面,它不承担 dynamic_cast 的类型安全检查的开销。

class Base {};
class Derived : public Base {};
Base* a = new Base;
Derived* b = static_cast<Derived*>(a);

这段代码是合法的,尽管 b 会指向类的一个不完整对象,若解引用它可能导致运行时错误。

因此,static_cast 能够在类指针上执行隐式允许的转换,以及它们的相反转换。

static_cast 还能够执行所有隐式允许的转换(不仅限于类指针),并且还能够执行这些转换的反向操作。它可以:

  • void* 转换为任何指针类型。在这种情况下,它保证如果 void* 值是通过从同一指针类型转换而获得的,则结果指针值相同。
  • 将整数、浮点值和枚举类型转换为枚举类型。
    此外,static_cast 还可以执行以下操作:
  • 显式调用单参数构造函数或转换运算符。
  • 转换为右值引用。
  • 将枚举类值转换为整数或浮点值。
  • 将任何类型转换为 void,评估并丢弃值。

reinterpret_cast

  • 用途:用于进行低级别的、几乎没有任何类型检查的强制转换。可以用来在指针和整数之间转换,或者在不同的指针类型之间转换。
  • 特性:不安全,通常用于底层操作或需要与硬件接口的场合。
  • 注意:转换结果依赖于实现,可能导致未定义行为。

reinterpret_cast 将任何指针类型转换为任何其他指针类型,即使是不相关类的指针类型。操作结果只是简单的二进制复制指针值。

它还可以将指针转换为整数类型或从整数类型转换为指针类型。这种整数值表示指针的格式是平台特定的。唯一的保证是将指针转换为足够大的整数类型(如 intptr_t)来完全包含它,并且保证可以转换回有效的指针。

reinterpret_cast 能执行的转换是基于重新解释类型的二进制表示,通常会导致系统特定的代码,因此不具备可移植性。例如:

class A { /* ... */ };
class B { /* ... */ };
A* a = new A;
B* b = reinterpret_cast<B*>(a);

这段代码可以编译,尽管它没有多大意义,因为现在 b 指向一个完全不相关且可能不兼容的类对象。解引用 b 是不安全的。

const_cast

  • 用途:用于添加或移除constvolatile修饰符,但不能用于将const对象转化为非const对象并修改其值(这样做会导致未定义行为)。
  • 特性:常用于需要对const对象进行某些特殊操作时,例如通过API传递常量对象,但API需要非const参数。

这种类型转换操作符操纵指针所指对象的常量性,可以设置或移除。例如,为了将常量指针传递给期望非常量参数的函数:

// const_cast
#include <iostream>
using namespace std;

void print (char* str) {
  cout << str << '\n';
}

int main () {
  const char* c = "sample text";
  print (const_cast<char*> (c));
  return 0;
}

上面的例子是可以保证工作的,因为函数 print 不会写入指向的对象。请注意,移除指向对象的常量性然后实际写入它会导致未定义行为。

typeid

typeid 允许检查表达式的类型:

typeid (expression)

该运算符返回一个对常量对象的引用,类型为 type_info,它在标准头文件 <typeinfo> 中定义。由 typeid 返回的值可以用 ==!= 运算符与另一个 typeid 返回的值进行比较,或者可以通过其 name() 成员获取表示数据类型或类名的以空字符结尾的字符串。

// typeid
#include <iostream>
#include <typeinfo>
using namespace std;

int main () {
  int* a, b;
  a = 0; b = 0;
  if (typeid(a) != typeid(b)) {
    cout << "a 和 b 是不同类型:\n";
    cout << "a 是: " << typeid(a).name() << '\n';
    cout << "b 是: " << typeid(b).name() << '\n';
  }
  return 0;
}

typeid 应用于类时,typeid 使用 RTTI 来跟踪动态类型。当 typeid 应用于多态类的表达式时,结果是最派生的完整对象的类型:

// typeid, 多态类
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;

class Base { virtual void f(){} };
class Derived : public Base {};

int main () {
  try {
    Base* a = new Base;
    Base* b = new Derived;
    cout << "a 是: " << typeid(a).name() << '\n';
    cout << "b 是: " << typeid(b).name() << '\n';
    cout << "*a 是: " << typeid(*a).name() << '\n';
    cout << "*b 是: " << typeid(*b).name() << '\n';
  } catch (exception& e) { cout << "异常: " << e.what() << '\n'; }
  return 0;
}

注意:type_info 成员 name 返回的字符串取决于编译器和库的具体实现。它不一定是一个简单的典型类型名称字符串,像本输出中的编译器。

请注意,typeid 对指针类型的考虑是指针类型本身(ab 都是 class Base * 类型)。然而,当 typeid 应用于对象时(如 *a*b),typeid 会得到它们的动态类型(即最派生的完整对象的类型)。

如果 typeid 评估的类型是一个由解引用运算符(*)前缀的指针,并且该指针的值为 null,typeid 会抛出 bad_typeid 异常。

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值