面试中的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Monster {
  std::shared_ptr<Monster> m_father;
  std::shared_ptr<Monster> m_son;
public:
  void setFather(std::shared_ptr<Monster>& father);//实现细节懒得写了
  void setSon(std::shared_ptr<Monster>& son);    //懒
  ~Monster() {std::cout << "A monster die!";}    //析构时发出死亡的悲鸣
};

void runGame() {
std::shared_ptr<Monster> father = new Monster();
std::shared_ptr<Monster> son = new Monster();
father->setSon(son);
son->setFather(father);
}

那么在runGame()结束时,父子指针都无法正常析构。为了解决这一问题,需要使用weak_ptr

weak_ptr

shared_ptr代表强引用,即生命周期拥有权;而weak_ptr代表弱引用,即只拥有观测权。weak_ptr不应该单独使用,应该仅作为shared_ptr的辅助。

引入weak_ptr后,技术区域需要区分两种引用计数:强引用计数和弱引用计数。当强引用计数归零时,析构指向的对象;当弱引用计数和强引用计数都归零时,才能释放该计数区域。

在定义拥有对象生命周期控制权的指针时,使用一次shared_ptr以拥有该对象(i.e. 当该指针的生命周期结束时,该对象予以析构,其他任何地方都不应该再使用该对象)。其他任何要引用该对象的地方,都应该使用weak_ptr

注意

  1. 如果在weak_ptr未感知的地方shared_ptr生命周期结束,可能导致weak_ptr指针空悬问题。为了解决这一问题,weak_ptr引入expired()成员,可以判断对象是否已释放。
  2. 同样原因,weak_ptr没有重载*->,所以称为“观测权”要比“使用权”更好。因此要使用被弱引用的对象,必须先用weak_ptrlock()获得一个 shared_ptr,用该shared_ptr使用对象,在使用完成后即使析构该指针(善用作用域)。

四种显式类型转换

static_cast

总体而言用于相关类型间的转换,编译器将尽可能检查类型转换的正确性,并在需要时修改底层二进制表示(如int转成float)。在可能丢失精度时,编译器不会提示——如果程序员显示使用了static_cast,则认为你已经预料到了这样的风险。几种常见用法:

  1. 用于内置类型间的转换;
  2. 用于void *和包含类型的指针之间的互相转换;
  3. 继承关系的类型指针或引用之间的转换:从子类到父类安全,但从父类到子类不一定安全!如果可能,最好使用dynamic_cast
  4. 自定义了转换关系的类型对象之间的转换。

自定义转换关系: 1)在源类型中定义目标类型的operator; 2)在目标类型中定义以源类型为唯一参数的构造函数。

reinterpret_cast

保证底层二进制不变,只是编译器层面上改变了对该二进制的解释类型。

一般用于无关类型的指针间互相转换。也可以用于整数和指针直接的转化,如:

1
Device *p = reinterpret_cast<Device *>(0xff00);

注意:如果转换可能导致精度丢失,则编译器报错。

static_castreinterpret_cast的几个例子:

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
float *p = new float;
*p = 2;

int *ip = reinterpret_cast<int *>(p);
printf("0x%x\n", *ip);
cout << ip << endl;

// float *fp = static_cast<float *>(ip); // forbidden: invalid type
void *vp = static_cast<void *>(p);
float *fp = static_cast<float *>(vp);
cout << *fp << endl;

int i = static_cast<int>(*p);
cout << i << endl;

// int what = reinterpret_cast<int>(p); // on x64 forbidden: loses precision
long long what = reinterpret_cast<long long>(p);
printf("0x%llx\n", what);

int lose = static_cast<int>(1.1f);
cout << lose << endl;

char losec = static_cast<char>(1 << 20);
cout << static_cast<int>(losec) << endl;

delete p;

其输出为

1
2
3
4
5
6
7
0x40000000
0x562df031eeb0
2
2
0x562df031eeb0
1
0

dynamic_cast

主要用于有(直接或间接)继承关系的类的指针和引用的转换。注意:基类必须定义虚函数!

几个例子。先定义如下类型:

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
class A {
int _a;
public:
explicit A(int a) : _a(a) {}
int get_a() { return _a; }
};

class B {
public:
virtual void func() { puts("I'm B!"); }
};

class D: public B {
public:
void func() override { puts("I'm D!"); }
};

class Dadd: public B {
int _d;
public:
explicit Dadd(int d) : _d(d) { }
int get_d() { return _d; }
void func() override { printf("I'm D(%d)!\n", _d); }
void myfunc() { printf("I'm special with %d!\n", _d); }
};

class DA: public Dadd, public A {
public:
DA(int d, int a) : Dadd(d), A(a) {}
void func() override { printf("I'm DA(%d, %d)!\n", get_d(), get_a()); }
};

继承关系:

1
2
3
4
5
6
7
8
9
A     B
^ ^
| |------
| | |
| Dadd D
| |
-------
|
DA
例1

首先是指针间互相转换。同一条继承线上的类型指针可以用static_castdynamic_cast随意转换,最终运行时都会根据VTABLE选择对象对应的函数。原理是对象就在内存中,且具有确定的类型和确定的VTABLE,指针可以随意类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
D *d = new D;
B *sb = static_cast<B *>(d);
sb->func();

B *db = dynamic_cast<B *>(d);
db->func();
D *dd = dynamic_cast<D *>(db);
dd->func();

// Dadd *sdadd = static_cast<Dadd *>(d); // forbidden
Dadd *sdadd = static_cast<Dadd *>(db);
sdadd->myfunc();

DA *sDA = static_cast<DA *>(db);
sDA->func();

Dadd *ddadd = dynamic_cast<Dadd *>(db);
if (ddadd) {
ddadd->myfunc();
} else {
cout << "failed to convert: B* -> Dadd*" << endl;
}

delete d;

输出为:

1
2
3
4
5
6
I'm D!
I'm D!
I'm D!
I'm special with 0!
I'm D!
failed to convert: B* -> Dadd*
例2

不能直接转化的类型指针之间,也可以调用dynamic_cast,但结果为空指针。

1
2
3
4
5
6
7
8
9
Dadd *da = new Dadd(2);
D *dad = dynamic_cast<D *>(da);
if (dad) {
dad->func();
} else {
cout << "failed to convert: Dadd* -> D*" << endl;
}

delete da;

输出为:

1
failed to convert: Dadd* -> D*
例3

dynamic_cast还可以用于引用的转换(注意是引用,不是对象!),成功与否同样取决于被引用的对象实际的类型。类似的,static_cast也有同样效果。

注意:不同于指针转换失败会返回空指针,引用转换失败会抛出std::bad_cast异常。

另外,static_cast还是可以用于对象本身的直接转换——会尝试构造新对象

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
    Dadd dar(3);
try {
// D &dadr = static_cast<D &>(dar); // forbidden
D &dadr = dynamic_cast<D &>(dar);
dadr.func();
} catch(const bad_cast& e) {
cout << "failed to convert: Dadd& -> D&" << endl;
}

// B &dabr = static_cast<B &>(dar); // this is also OK!
B &dabr = dynamic_cast<B &>(dar);
dabr.func();

try {
D &bdr = dynamic_cast<D &>(dabr);
bdr.func();
} catch(const bad_cast& e) {
cout << "failed to convert: B& -> D&" << endl;
}

Dadd &bdadr = dynamic_cast<Dadd &>(dabr);
bdadr.func();

// DA what1 = static_cast<DA>(dar); // no matching function for call to ‘DA::DA(Dadd&)’
B what2 = static_cast<B>(dar);
what2.func();
}

输出为:

1
2
3
4
5
failed to convert: Dadd& -> D&
I'm D(3)!
failed to convert: B& -> D&
I'm D(3)!
I'm B!
例4

dynamic_cast甚至可以实现有共同子类(多根继承)的基类间的转换——这是通过运行时检查VTABLE做到的,也是static_cast在编译时做不到的事!

To be mentioned, dynamic_cast的目标类型不一定是多态的,如例子中的class A

1
2
3
4
5
6
7
8
9
    DA *carrier = new DA(5, 6);

B *cb = static_cast<B *>(carrier);
// A *cas = static_cast<A *>(cb); // forbidden
A *cad = dynamic_cast<A *>(cb);
cout << cad->get_a() << endl;

delete carrier;
}

输出为:

1
6

const_cast

用于去除const限定符。

如果要修改const变量的某个部分,强制转换不一定是最好的方案。可以考虑使用mutable将可变的部分定义为“一定不是常量”。