面试中的C++问题(2)
继续这个系列。本篇的问题可能比上一篇稍复杂一些。
三种智能指针
C++ 11之后建议使用三种智能指针。需要包含<memory>
头文件。请区分对象的“生命周期拥有权”和“观测权”。
auto_ptr
已经被淘汰,不建议使用。
unique_ptr
独占对象的生命周期拥有权,当然也包括观测权。它指向的对象不能复制、值传递,不能用于基于值传递的STL库。只能移动,之后原始的unique_ptr
变成空指针。
注意:如果unique_ptr
是临时右值,则允许拷贝。
可以用reset()
、release()
,也可以直接重新赋值。
unique_ptr
生命周期结束时,自动析构其指向的资源。
shared_ptr
可以共享对象的生命周期拥有权(包括观测权)。底层原理是引用计数,因此除了对象本身,还需要分配一块内存用于记录引用计数,所有shared_ptr
共享该计数区域,每多一个shared_ptr
,引用+1. 每个shared_ptr
结束生命周期时,引用-1. 当引用归零时,自动析构其指向的资源。
注意:在涉及循环引用时,可能出现问题,如(例子来自KillerAery - C++11智能指针的深度理解):
1 | class Monster { |
那么在runGame()
结束时,父子指针都无法正常析构。为了解决这一问题,需要使用weak_ptr
。
weak_ptr
shared_ptr
代表强引用,即生命周期拥有权;而weak_ptr
代表弱引用,即只拥有观测权。weak_ptr
不应该单独使用,应该仅作为shared_ptr
的辅助。
引入weak_ptr
后,计数区域需要区分两种引用计数:强引用计数和弱引用计数。当强引用计数归零时,析构指向的对象;当弱引用计数和强引用计数都归零时,才能释放该计数区域。
在定义拥有对象生命周期控制权的指针时,使用一次shared_ptr
以拥有该对象(i.e. 当该指针的生命周期结束时,该对象予以析构,其他任何地方都不应该再使用该对象)。其他任何要引用该对象的地方,都应该使用weak_ptr
。
注意:
- 如果在
weak_ptr
未感知的地方shared_ptr
生命周期结束,可能导致weak_ptr
指针空悬问题。为了解决这一问题,weak_ptr
引入expired()
成员,可以判断对象是否已释放。 - 同样原因,
weak_ptr
没有重载*
和->
,所以称为“观测权”要比“使用权”更好。因此要使用被弱引用的对象,必须先用weak_ptr
的lock()
获得一个shared_ptr
,用该shared_ptr
使用对象,在使用完成后即使析构该指针(善用作用域)。
四种显式类型转换
static_cast
总体而言用于相关类型间的转换,编译器将尽可能检查类型转换的正确性,并在需要时修改底层二进制表示(如int
转成float
)。在可能丢失精度时,编译器不会提示——如果程序员显示使用了static_cast
,则认为你已经预料到了这样的风险。几种常见用法:
- 用于内置类型间的转换;
- 用于
void *
和包含类型的指针之间的互相转换; - 有继承关系的类型指针或引用之间的转换:从子类到父类安全,但从父类到子类不一定安全!如果可能,最好使用
dynamic_cast
; - 自定义了转换关系的类型对象之间的转换。
自定义转换关系: 1)在源类型中定义目标类型的operator; 2)在目标类型中定义以源类型为唯一参数的构造函数。
reinterpret_cast
保证底层二进制不变,只是编译器层面上改变了对该二进制的解释类型。
一般用于无关类型的指针间互相转换。也可以用于整数和指针直接的转化,如:
1 | Device *p = reinterpret_cast<Device *>(0xff00); |
注意:如果转换可能导致精度丢失,则编译器报错。
static_cast
和reinterpret_cast
的几个例子:
1 | float *p = new float; |
其输出为
1 | 0x40000000 |
dynamic_cast
主要用于有(直接或间接)继承关系的类的指针和引用的转换。注意:基类必须定义虚函数!
几个例子。先定义如下类型:
1 | class A { |
继承关系:
1 | A B |
例1
首先是指针间互相转换。同一条继承线上的类型指针可以用static_cast
或dynamic_cast
随意转换,最终运行时都会根据VTABLE选择对象对应的函数。原理是对象就在内存中,且具有确定的类型和确定的VTABLE,指针可以随意类型。
1 | D *d = new D; |
输出为:
1 | I'm D! |
注:
Dadd *sdadd
导致的越界访存错误在本文初版中没有提示,详见评论区。也可以试试将
_d
改为一个堆上指针,这种情况下,直接运行可能并不会报错而是输出随机值,但用 gdb launch 就会 SEGV,有趣的一点是,某些 gdb 实现会给_d
这种未初始化的堆指针赋默认值如0xabababab
,因此sdadd->myfunc()
会触发段错误。但本文中放在栈上,即使用 gdb 也不会段错误。
例2
不能直接转化的类型指针之间,也可以调用dynamic_cast
,但结果为空指针。
1 | Dadd *da = new Dadd(2); |
输出为:
1 | failed to convert: Dadd* -> D* |
例3
dynamic_cast
还可以用于引用的转换(注意是引用,不是对象!),成功与否同样取决于被引用的对象实际的类型。类似的,static_cast
也有同样效果。
注意:不同于指针转换失败会返回空指针,引用转换失败会抛出std::bad_cast
异常。
另外,static_cast
还是可以用于对象本身的直接转换——会尝试构造新对象!
1 | Dadd dar(3); |
输出为:
1 | failed to convert: Dadd& -> D& |
例4
dynamic_cast
甚至可以实现有共同子类(多根继承)的基类间的转换——这是通过运行时检查VTABLE做到的,也是static_cast
在编译时做不到的事!
To be mentioned,
dynamic_cast
的目标类型不一定是多态的,如例子中的class A
。
1 | DA *carrier = new DA(5, 6); |
输出为:
1 | 6 |
const_cast
用于去除const
限定符。
如果要修改
const
变量的某个部分,强制转换不一定是最好的方案。可以考虑使用mutable
将可变的部分定义为“一定不是常量”。