c++ 类型转换与explicit
Posted zkccpro
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++ 类型转换与explicit相关的知识,希望对你有一定的参考价值。
c++ 类型转换与explicit
本文试图搞清楚这样2个问题:
- c++ 类型转换有哪些?作用分别是什么?实际工程中应该怎么用?
- 经常查看标准库源码的同学会发现标准库中很多类构造函数会带有explicit声明,explicit关键字是为了做什么的?
一、c++的类型转换简介
可不要上来就说4种类型转换昂。。类型转换按照触发条件分可分为:隐式类型转换,显式类型转换;而按照行为分可分为:static,dynamic,const,reinterpret。我们先按照行为划分介绍4种类型转换:
1. static_cast
静态转换是最常用的一种转换了!作用效果与c风格的强制类型转换写法一样。另外,下文会提及:所有隐式转换默认使用static_cast。
int a=2;
double b1=static_cast<double>(a);//c++
double b2=(double)a;//c
但static_cast在下行转换时可能会使代码不安全(未定义行为):
class base
public:
virtual void func()cout<<"this is base!"<<endl;
;
class sub:public base
public:
virtual void func()overridecout<<"this is sub!"<<endl;
;
int main()
base* b=new base;//父类指针指向父类对象
sub* s=static_cast<sub*>(b);//下行转换用了static_cast
cout<<s<<endl;//转换成功,指针不为nullptr
s->func();//输出:this is base!
可以发现,用static_cast把父类转换成子类之后,对其调用虚函数仍然是父类的版本!可见static_cast对父类
所以一个常见的建议是:涉及子类和父类之间的转换不要用static_cast就完事了!
2. dynamic_cast
动态转换就会“动态地”分析转换前后的类型,是否会导致上面这种未定义行为,如果可能出现,则直接转换失败。接下来我们详细谈谈这个:
首先,dynamic_cast是有语法上的使用条件的:
- 必须用于带有虚函数的继承体系
- 转换目标类型和待转换类型必须是指针或引用
知道了使用条件,接下来看看dynamic_cast的行为:
class base
public:
virtual void func()cout<<"this is base!"<<endl;
;
class sub:public base
public:
virtual void func()overridecout<<"this is sub!"<<endl;
;
int main()
base* b=new base;//父类指针指向父类对象
sub* s=dynamic_cast<sub*>(b);// 下行转换
cout<<s<<endl;//输出:0,转换失败
可见,为了避免static_cast造成【经转换后的子类无法调用自己的虚函数版本】的问题,dynamic_cast的作用就在于直接阻止这种情况的出现,以免你的代码出现“难以发现”的错误。
另外,如果基类不含虚函数时,使用dynamic_cast转换会不过编译:【source type is not polymorphic,基类类型并非多态】
class base
public:
int a;
;
class sub:public base
public:
int b;
;
int main()
base* b=new base;
sub* s=dynamic_cast<sub*>(b);// 编译出错
3. const_cast
//去除指针的底层const属性
int aa=2;
const int* a=&aa;//底层const
int* b=const_cast<int*>(a);
*a=3;//a指向内容不能改,底层const,编译出错!
*b=3;//转换之后就可以了
cout<<*b<<endl;
//去除指针的顶层const属性
int aa=2;
int bb=3;
int* const a=&aa;//顶层const
int* b=const_cast<int*>(a);
a=&bb;//a的指向不能改,顶层const,编译出错!
b=&bb;//转换之后就可以了
cout<<*b<<endl;
说到这里,就不得不提一下我对顶层/底层const的一些新的理解:
首先是写法,初学者可能都会觉得顶层底层的写法难以记住,容易混淆:
const int* a;//1. 底层
int* const b;//2. 顶层
int const *c;//3. 底层
上面3种都是比较常见的写法,你能快速反应出来吗?一开始我也迷糊,特别是看到第3种写法。。。
我是怎么记的呢?const放在 * 后面就是修饰指针的,即为顶层,其他都是底层,即修饰变量类型(int)的。
而对于底层const,一般理解是:不能修改指向的内容,这句话没错,但不准确,看下面的代码:
int a = 1;
int const *p_a = &a;
*p_a = 2;//底层const指向的区域不能由此底层const指针修改,编译出错
a=2;//但并不意味着此段内存中的值不能改,只是不能通过此底层const指针修改!
上述代码在GNU下测试过,所以,对于底层const,我们的结论是:
底层const的本质是规定一段地址,这段地址中的值不能由此底层const指针修改,但却能通过其他方式修改。
4. reinterpret_cast
噢。。这个东西真的是“潘多拉魔盒”!为什么这么说呢?因为它帮你跨过c++帮你苦心经营的“抽象”,给你提供一个直接和底层的汇编语言打交道的机会!
在之后的章节我会详细介绍汇编语言提供怎样的机制,来访问不同类型指针指向的内存。现在我们先简单“尝尝鲜”:
int aa=2;
int* a=&aa;
char* c=reinterpret_cast<char*>(a);
int i=0;
while(i<sizeof(int))
cout<<static_cast<int>(*(c++))<<" ";
i++;
cout<<endl;
第一次接触reinterpret_cast看上面的代码可能会觉得头皮发麻,比如说我。。。实际上,上面的代码意味着:
将一段存储int类型的内存空间,按照char类型的方法来解析之!
啥意思呢?仔细观察你会发现:c指针每次++,起始地址只向前移动了1B,而int类型的指针每次++应该移动4B才对!这就说明对一段地址位级别的解释发生了变化。
上段代码GNU下的输出:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_cast.cc -o test_cast && ./test_cast
2 0 0 0
它直接输出了存储4字节int类型变量地址中,每1字节的存储的值,具体输出的内容与整数机内表示有关,我们之后的文章也会详细介绍。
说完了reinterpret_cast的行为,那它有什么用呢?我们什么时候非得跨国c/c++的抽象来直接改变底层地址的解析性质呢?
恐怕一个经典的例子就是buffer设计。当你的buffer需要存储不同类型的值时,将他们存入buffer前都reinterpret_cast<char*>
,再放入一个char*类型的buffer;当你自己动手设计网络消息中间件的buffer时,你需要统一按字节(char类型)将各种信息打出去,这或许也需要reinterpret_cast<char*>
。
但必须要提醒的是,用了这个东西,也就意味着没有完全遵守c++2.0规范,因为直接修改了底层的字节解释,如果你对你程序的行为不敢100%保证它可以按你的想法去改变解析的地址内容(特别是网络buffer,因为网络复杂的情况很可能导致你的buffer出现一些时序对不上的情况,除非你考虑了所有可能发生的时序问题并做好了正确的处理,否则在网络buffer中用reinterpret_cast<char*>
属于“找死”行为。
二、 explicit关键字的使用场景
当你通读过《c++ primer》后,你会了解到,explicit一般用于修饰类的构造函数,阻止类调用此构造函数发生隐式转换。但你了解到此你可能还是不知道到底什么时候该用explicit修饰构造函数。。(反正我是这样)
幸亏看了侯捷老师的视频,帮我搞清楚了explicit的使用场景究竟是什么,我在GNU下复现了侯捷老师给的例子:
class fraction
public:
fraction()=default;
fraction(int num,int den=1)//有参构造,允许隐式转换
:num_(num),den_(den)
//自定义statis_cast的规则,一个类重载了这个就可以向目标类型隐式转换/静态转换了
operator double()const
return static_cast<double>(num_/den_);
fraction operator+(const fraction& f)
//应该按分数运算的规则返回,但在这里这不重要
return fraction();
private:
int num_0;
int den_1;
;
int main()
fraction f(3,5);
double d=f+3;//ambiguous
上面这段代码会编译报错吗?答案是 会的!但是为啥呢。。
原因在于主函数的最后一行,表达式f+3.0
对编译器来说有2种执行方法:
- 由于你重载了double(),所以可以把 f 隐式转换成double,3转换成double,然后相加,没毛病。
- 由于你没有阻止有参构造的隐式转换,而且整数3正好可以匹配你的有参构造(有默认参数,可以接受一个参数),所以编译器可以把 3 隐式转换成 fraction,然后两个fraction正好又有相加规则,最后再把相加后的fraction隐式转换成double,赋值给d。稍麻烦点,但也没毛病啊!
至此,编译器有了2种执行方法,且都可以走通,这种情况会导致编译出错,这是GNU下的编译输出:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_cast.cc -o test_cast && ./test_cast
test_cast.cc: In function ‘int main()’:
test_cast.cc:67:15: error: ambiguous overload for ‘operator+’ (operand types are ‘fraction’ and ‘double’)
67 | double d=f+3.0;
| ~^~~~
| | |
| | double
| fraction
test_cast.cc:67:15: note: candidate: ‘operator+(double, double)’ <built-in>
67 | double d=f+3.0;
| ~^~~~
test_cast.cc:24:14: note: candidate: ‘fraction fraction::operator+(const fraction&)’
24 | fraction operator+(const fraction& f)
| ^~~~~~~~
那如何避免这种ambiguous呢?很简单,只要阻止一条路径就行了,下面的方法都在GNU下经过测试了
- 阻止路径1:把operator double()重载去掉
- 阻止路径2:给有参构造加上explicit修饰:
explicit fraction(int num,int den=1)//阻止隐式转换,explicit修饰要写在前面
:num_(num),den_(den)
而避免这种ambiguous的出现恰好是explicit关键字最常用的用途了,掌握这个即可!
以上是关于c++ 类型转换与explicit的主要内容,如果未能解决你的问题,请参考以下文章
(译)What does explicit keyword mean?