C++中的类型转换

目录

1.C语言中的类型转换

2.为什么C++需要四种类型转换

3.C++强制类型转换

static_cast

reinterpret_cast

const_cast

dynamic_cast

4. RTTI(了解)

5.常见面试题


1.C语言中的类型转换

在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换

❍ 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失效

❍ 显式类型转换:需要用户自己处理

a、内置类型之间

1、隐式类型转换 整形之间/整形和浮点数之间

2、显示类型的转换 指针和整形、指针之间

int main()
{
 int i = 1;
 // 隐式类型转换
 double d = i;
 printf("%d, %.2f\n", i, d);

 int* p = &i;
 // 显示的强制类型转换
 int address = (int)p;
 printf("%p, %d\n", p, address);

 return 0;
}

b、内置类型和自定义类型之间

1️⃣ 内置类型转换自定义类型

class A
{
public:
 A(int a)//单参数构造函数,进行隐式类型转换
 :_a1(a)
 ,_a2(a)
 {}

 A(int a1, int a2)//多参数构造函数,进行隐式类型转换
 :_a1(a1)
 , _a2(a2)
 {}

private:
 int _a1 = 1;
 int _a2 = 1;
};

int main(){
 string s1 = "111111";
 A aa1 = 1; //  1:内置类型 -> aa1:自定义类型 通过构造函数进行转换
 A aa2 = { 2,2 };// 这里是类型转换(通过构造函数),不是Initialize_list
 					   	 //  这里使用多参数构造函数创建了一个A类型的对象aa2

 const A& aa3 = { 2,2 };//这里创建了一个临时对象,并将其绑定到一个const引用aa3上。
 return 0;
}

为什么aa3前面需要加const?

保持一致性:在声明引用时,如果你不打算改变其引用的对象,最好将其声明为const。这可以防止任何试图修改引用内容的操作,从而保护数据不被意外修改。

初始化要求:当使用列表初始化来初始化一个引用时,右侧必须是一个表达式,这个表达式可以是临时对象。在C++中,临时对象默认是常量,因此,为了能够将这个临时对象绑定到一个引用上,这个引用也必须是const的。

构造函数调用:在A aa3 = { 2,2 };这一行,实际上是调用了A类的多参数构造函数来创建一个临时对象,然后这个临时对象被绑定到aa3这个常量引用上。因为临时对象是常量,所以引用也必须是const的。

如果去掉const,像这样A& aa3 = { 2,2 };,编译器会报错,因为非const引用不能绑定到一个临时对象上。

2️⃣ 自定义类型转换为内置类型

增加operator()

class A
{
public:
  A(int a)//单参数构造函数,进行隐式类型转换
 :_a1(a)
 ,_a2(a)
 {}

 A(int a1, int a2)//多参数构造函数,进行隐式类型转换
 :_a1(a1)
 , _a2(a2)
 {}

 // ()被仿函数占用了,不能用
 // operator 类型实现,无返回类型
 //explicit operator int()
 operator int()
 {
     return _a1 + _a2;
 }

private:
 int _a1 = 1;
 int _a2 = 1;
};

int main(){
 string s1 = "111111";
 A aa1 = 1; //  1:内置类型 -> aa1:自定义类型 通过构造函数进行转换
 A aa2 = { 2,2 };// 这里是类型转换(通过构造函数),不是Initialize_list
 const A& aa3 = { 2,2 };

 int z = aa1.operator int();//本质
 int x = aa1;
 int y = aa2;

 cout << x << endl;
 cout << y << endl;
 return 0;
}

自定义类型转换为内置类型在智能指针 operator bool中就有体现

 std::shared_ptr<int> foo;
 std::shared_ptr<int> bar(new int(34));

 //if (foo.operator bool())
 if (foo)// foo是空 自定义类型 转换为内置类型返回false
     std::cout << "foo points to " << *foo << '\n';
 else
     std::cout << "foo is null\n";
 if (bar)
     std::cout << "bar points to " << *bar << '\n';
 else
     std::cout << "bar is null\n";

 如果不想发生隐式类型转换,在前面加上 explicit 关键字,但仍然可以强制类型转换

c、自定义类型和自定义类型之间 -- 对应的构造函数支持

// c、自定义类型和自定义类型之间 -- 对应的构造函数支持
class A
{
public:
 A(int a)
     :_a1(a)
     , _a2(a)
 {}

 A(int a1, int a2)
     :_a1(a1)
     , _a2(a2)
 {}

 int get() const
 {
     return _a1 + _a2;
 }
private:
 int _a1 = 1;
 int _a2 = 1;
};

class B
{
public:
 B(int b)
     :_b1(b)
 {}

 B(const A& aa)
     :_b1(aa.get())
 {}

private:
 int _b1 = 1;
};

int main()
{
 A aa1(1);
 B bb1(2);

 bb1 = aa1;
 B& ref1= bb1;
 const B& ref2 = aa1;
 return 0;
}

案例:

我们首先来观察库里 list 使用,这里声明了一个std::list<int>类型的对象lt,并使用列表初始化来填充它。

int main(){
 std::list<int> lt = { 1,2,3,4 };
 // 权限的缩小?权限缩小和放大,仅限于const的指针和引用
 // 不是权限缩小,这里类型转换
	// 这里发生的是从非const迭代器到const迭代器的类型转换
 std::list<int>::const_iterator cit = lt.begin();
 while (cit != lt.end())
 {
     cout << *cit << " ";
     ++cit;
 }
     cout << endl;

 return 0;
}
输出结果:
1 2 3 4 

也就是说库里面是支持从非const迭代器到const迭代器的类型转换,回想一下我们之前的自己模拟实现的list,并没有写这种函数实现,那么我们先观察一下库里面是如何实现这种操作的呢 

上面可能有些抽象,我们举一个简化的例子来说明这一点:

在C++中,当我们谈论迭代器的实例化时,确实有两种情况:

拷贝构造:当我们从一个已存在的迭代器创建一个新的迭代器时,会调用拷贝构造函数。这通常发生在需要复制一个迭代器的时候。

普通构造:当我们通过一个非const迭代器来创建一个const迭代器时,会调用普通的构造函数,这通常涉及到类型转换。在标准库的实现中,通常会有一个构造函数允许从非const迭代器到const迭代器的转换。

template<typename T>
class list {
public:
 class iterator {
 public:
     iterator() : _ptr(nullptr) {} // 默认构造函数
     iterator(const iterator& other) : _ptr(other._ptr) {} // 拷贝构造函数
     // ... 其他成员函数 ...
 private:
     T* _ptr;
 };

 class const_iterator {
 public:
     // 允许从iterator构造const_iterator
     const_iterator(const iterator& other) : _ptr(other._ptr) {}
     // ... 其他成员函数 ...
 private:
     const T* _ptr;
 };
 // ... 其他成员函数 ...
};

 

在这个例子中,const_iterator的构造函数接受一个iterator类型的参数,并使用这个参数来初始化内部的_ptr成员。这样,我们就可以通过一个非constiterator来构造一个const_iterator,从而实现了从非constconst的转换。

因此,当您有一个非const迭代器it,并需要将其转换为const迭代器时,可以这样:

list<int>::iterator it = /* ... */;
list<int>::const_iterator cit = it; // 使用普通构造函数,支持转换

这里,cit是通过const_iterator的构造函数从it转换来的

这里我们在结合一下模版的原理,模版就是实例化出各种函数重载,然后过程与这个类似,进行隐式类型转换

接下来让我们对我们之前模拟实现的 list 进行修改

先观察之前的代码

只需要在原代码加入这个就够了 

再看一下测试结果:恭喜我们运行成功 

缺陷: 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换

2.为什么C++需要四种类型转换

C风格的转换格式很简单,但是有不少缺点的:

❍ 隐式类型转化有些情况下可能会出问题:比如数据精度丢失

❍ 显式类型转换将所有情况混合在一起,代码不够清晰 因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。

3.C++强制类型转换

标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:

static_cast、reinterpret_cast、const_cast、dynamic_cast

 

static_cast

static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用 static_cast,但它不能用于两个不相关的类型进行转换

int main() {
   // 对应隐式类型转换 -- 数据的意义没有改变
  double d = 12.34;
  int a = static_cast<int>(d);
  cout<<a<<endl;
  return 0;
}

reinterpret_cast

reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换 为另一种不同的类型

int main() {
    double d = 12.34;
    int a = static_cast<int>(d);
    cout << a << endl;
  
	// 这里使用static_cast会报错,应该使用reinterpret_cast 
  //int *p = static_cast<int*>(a);
  
  // 对应强制类型转换 -- 数据的意义已经发生改变
		int *p = reinterpret_cast<int*>(a);
  
		return 0; 
}

const_cast

const_cast 最常用的用途就是删除变量的const属性,方便赋值

void Test () {
  // 对应强制类型转换中有风险的去掉const属性
    const int b = 2;
    int* p2 = const_cast<int*>(&b);
    *p2 = 3;

    cout << b << endl;
    cout << *p2 << endl;
}
输出结果:
2
3

我们明明从地址方面修改了b的值,并且从调试可以发现,b的值已经被修改为什么还是会出现打印出来的b为2

在C++中,使用const_cast来去除const属性是一种类型转换操作,但它并不改变变量本身的const性质。在上面的代码中,const int b = 2;声明了一个const整数b,然后通过const_cast<int*>(&b);b的地址转换为一个普通的int*指针p2

虽然你可以通过p2指针修改内存中的值,但这是未定义行为(Undefined Behavior, UB),因为b是一个const变量。编译器和标准不保证这种修改会有任何效果,而且可能会导致程序崩溃或其他不可预测的结果。

在底层方面,在cout汇编去看,是直接将b替换为2,而不是从寄存器等获取的,那么我们如何避免这种情况呢?加上volatile关键字

 

这样输出结果就是我们理想中的样子

dynamic_cast

dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)

向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)

向下转型:将基类的指针或引用安全地转换为派生类的指针或引用(用dynamic_cast转型是安全的)

dynamic_cast的用法有以下几点需要注意:

要求:被转换的对象必须是多态的,即基类必须至少有一个虚函数。这是因为dynamic_cast在运行时需要检查对象的实际类型,而这需要RTTI(运行时类型信息)的支持。

安全:如果转换失败(即指针或引用实际上并不指向一个派生类的对象),dynamic_cast对于指针会返回空指针(nullptr),对于引用会抛出一个std::bad_cast异常。

性能dynamic_cast可能会比其他类型转换运算符(如static_castreinterpret_cast)慢,因为它需要检查对象的类型信息。

#include <typeinfo>

class Base {
public:
    virtual void print() const { std::cout << "Base" << std::endl; }
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void print() const override { std::cout << "Derived" << std::endl; }
};

int main() {
    // 一个基类指针 指向派生类
    Base* b = new Derived();
    //将基类赋给派生类
    //如果尝试将一个不是指向派生类的Base指针转换为Derived指针
    //dynamic_cast会失败,返回空指针。
    Derived* d = dynamic_cast<Derived*>(b);
    
    if (d) {
        d->print();  // 输出 "Derived"
    } else {
        std::cout << "dynamic_cast failed" << std::endl;
    }

    delete b;  // 释放内存
    return 0;
}

在这个例子中,我们有一个基类Base和一个派生类Derived。我们创建了一个指向Derived对象的Base指针b,然后使用dynamic_cast尝试将其转换为Derived指针d。由于b确实指向一个Derived对象,转换成功,并且我们可以调用Derived类的print方法。

如果尝试将一个不是指向派生类的Base指针转换为Derived指针,dynamic_cast会失败,返回空指针。

在使用dynamic_cast时,应当注意以下几点:

❍ 避免过度使用,因为它可能会引入运行时开销。

❍ 在转换前,考虑是否可以通过虚函数和多态性来避免类型转换。

❍ 确保转换的基类指针或引用确实指向一个派生类的对象,以避免无效的转换。

class A
{
public:
    virtual void f() {}

    int _a = 1;
};

class B : public A
{
public:
    int _b = 2;
};

void fun(A* pa)// 接受的是父类指针,如果传递的是基类指针则会向上转型,它不会丢失任何信息,因为派生类对象包含了基类对象的所有成员
{
    // dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回
    // 指向父类转换时有风险的,后续访问存在越界访问的风险
    // 指向子类转换时安全
    B* pb1 = (B*)pa;
    cout << "pb1:" << pb1 <<endl;
    cout << pb1->_a << endl;
    cout << pb1->_b << endl;
    pb1->_a++;
    pb1->_b++;
    cout << pb1->_a << endl;
    cout << pb1->_b << endl;
}

//void fun(A* pa)
//{
//    // dynamic_cast会先检查是否能转换成功(指向子类对象),能成功则转换,
//    // (指向父类对象)不能则返回NULL
//    B* pb1 = dynamic_cast<B*>(pa);
//    if (pb1)
//    {
//        cout << "pb1:" << pb1 << endl;
//        cout << pb1->_a << endl;
//        cout << pb1->_b << endl;
//        pb1->_a++;
//        pb1->_b++;
//        cout << pb1->_a << endl;
//        cout << pb1->_b << endl;
//    }
//    else
//    {
//        cout << "转换失败" << endl;
//    }
//}
//
int main()
{
    A a;
    B b;
    fun(&a);
    fun(&b);

    return 0;
}

fun 函数中,将 A* 类型的指针转换为 B* 类型的指针,这在运行时可能会导致问题:

  • fun 接受 &a(即 A 类型的对象 a 的地址)时,将其转换为 B* 是不安全的。因为 a 实际上不是 B 类型的对象,而是 A 类型的对象。这种转换称为向下转型(downcasting),在没有进行适当检查的情况下进行向下转型是不安全的,因为它可能会导致程序访问不存在的 B 类型的成员变量 _b

  • fun 接受 &b(即 B 类型的对象 b 的地址)时,转换是安全的,因为 b 确实是 B 类型的对象。

注意

强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是 否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用 域,以减少发生错误的机会。强烈建议:避免使用强制类型转换

4. RTTI(了解)

RTTI:Run-time Type identification的简称,即:运行时类型识别。

C++通过以下方式来支持RTTI:

◉ typeid运算符

◉ dynamic_cast运算符

◉ decltype

5.常见面试题

1.C++中的4中类型转化分别是:__________________

2.说说4中类型转化的应用场景。

在C++中,有四种类型转换操作符:static_castdynamic_castconst_castreinterpret_cast。每种类型转换都有其特定的应用场景:

  1. static_cast:

    • 应用场景:

      • 用于执行非多态类型之间的转换,例如基本数据类型之间的转换(如intfloat)。

      • 用于执行向上转型(从派生类到基类的转换)。

      • 用于执行向下转型(从基类到派生类的转换),但不会进行运行时类型检查,因此不安全。

      • 用于任何明确定义的类型转换,它不改变对象的底层表示,只是告诉编译器以不同的方式看待对象。

    • 注意事项:

      • static_cast不会去除const属性。

      • 使用static_cast执行向下转型时,如果没有进行适当的类型检查,可能会导致未定义行为。

  2. dynamic_cast:

    • 应用场景:

      • 用于执行向下转型(从基类到派生类的转换),并且会进行运行时类型检查,确保安全性。

      • 用于交叉转换,即在两个没有直接继承关系的类之间进行转换,但要求这些类之间有虚函数。

    • 注意事项:

      • dynamic_cast要求基类有至少一个虚函数,以便进行运行时类型检查。

      • 如果转换失败,对于指针返回空指针,对于引用抛出std::bad_cast异常。

      • dynamic_cast的性能开销较大,因为它需要在运行时检查对象的类型。

  3. const_cast:

    • 应用场景:

      • 用于添加或去除对象的constvolatile属性。

      • 用于将一个const指针或引用转换为一个非const指针或引用,反之亦然。

    • 注意事项:

      • const_cast只能用于改变表达式的常量性,不能改变表达式的类型。

      • 使用const_cast去除const属性后修改对象是未定义行为,除非原始对象本身不是const

  4. reinterpret_cast:

    • 应用场景:

      • 用于执行低级转换,将一个指针转换为一个完全不同类型的指针。

      • 用于将一个指针转换为一个整型,或将一个整型转换为一个指针。

      • 用于在不同类型的指针之间进行转换,不进行任何类型或安全性检查。

    • 注意事项:

      • reinterpret_cast是非常危险的,因为它可能会违反类型系统的基本规则,导致未定义行为。

      • 使用reinterpret_cast应该非常谨慎,通常只有在理解了底层表示和确保操作安全的情况下才使用。 在编写C++代码时,应该尽可能避免使用类型转换,特别是reinterpret_cast,因为它们可能会隐藏错误并引入难以追踪的问题。当确实需要使用类型转换时,应该选择最合适的转换操作符,并确保转换是安全的。

 

  • 15
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值