STL中_Rb_tree的探索

我们知道STL中我们常用的setmultisetmapmultimap都是基于红黑树。本文介绍了它们的在STL中的底层数据结构_Rb_tree的直接用法与部分函数。难点主要是_Rb_tree的各个参数的确定。

特别注意在如下代码的Selector类用于从Node中选出用于排序的key值,这个仿函数必须返回const int&而不能是int,否则less<int>::operator(const int&, const int&)会抛出segmentation fault。一开始写成int,看了很多源码才发现是这个原因,一定要注意。(Update on 2020-5-6:通过对源码阅读,终于彻底明白为何出现写int会出现问题,见文末)

接下来是样例代码,里面都有注释了。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <iomanip>

// 原则上不要直接引用这个头文件,这里只是为了测试
#include <bits/stl_tree.h>

using namespace std;

struct Node {
int first, second;
Node(int _first, int _second) : first(_first), second(_second){};

friend ostream& operator<<(ostream& outs, const Node& node) {
outs << '{' << node.first << ',' << node.second << '}';
return outs;
}
};

template <class T>
struct Selector {
// MUST return const int&, not int.
// if return int, segmentation fault will occur.
// I have spent much time because of this.
const int& operator()(const T& obj) const {
return obj.first;
}
};

int main() {
// _Rb_tree: red-black tree in STL.
using tree_type = _Rb_tree<int, Node, Selector<Node>, less<int>>;
using iterator_type = tree_type::iterator;
using result_pair_type = pair<tree_type::iterator, bool>;
tree_type tree;

// 插入元素Node(1, 2)
result_pair_type res = tree._M_insert_unique(Node(1, 2));
cout << "insert address = " << res.first._M_node << endl;
cout << "insert result = " << boolalpha << res.second << endl; // true

iterator_type it = tree.begin();
cout << "begin address = " << it._M_node << endl;

it = tree.find(1);
cout << "address = " << it._M_node << ", value = " << *it << endl;

// 再插入元素Node(1, 2)但是因为调用的是insert_unique
// 它不会添加重复值,所以插入会被拒绝
res = tree._M_insert_unique(Node(1, 2));
cout << "insert result = " << boolalpha << res.second << endl; // false

// 再插入元素Node(1, 2)但这次调用insert_equal
// multiset和multimap就是利用这个函数来插入重复值
// 也就是这个函数允许重复值,所以插入成功
tree._M_insert_equal(Node(1, 3));
cout << "size = " << tree.size() << endl; // 大小就变为2

pair<iterator_type, iterator_type> result = tree.equal_range(1);
for (iterator_type ite = result.first; ite != result.second; ++ite) {
cout << "address = " << ite._M_node << ", value = " << *ite << endl;
}

return 0;
}

程序的输出为(内存地址不定):

1
2
3
4
5
6
7
8
insert address = 0xf91be0
insert result = true
begin address = 0xf91be0
address = 0xf91be0, value = {1,2}
insert result = false
size = 2
address = 0xf91be0, value = {1,2}
address = 0xf91c10, value = {1,3}

Update on 2020-5-6

当将Selector::operator()的返回值写为int时,执行时会在main()中的it = tree.find(1)时抛出Segmentation Fault。以下给出调用过程。

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
// main.cpp, main()
int main() {
...
it = tree.find(1);
...
}

// stl_tree.h, std::_Rb_tree::find(const _Key&)
template<typename _Key, typename _Val, typename _KeyOfValue,
typename _Compare, typename _Alloc>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
find(const _Key& __k) {
iterator __j = _M_lower_bound(_M_begin(), _M_end(), __k);
return (__j == end() || _M_impl._M_key_compare(__k,
_S_key(__j._M_node))) ? end() : __j;
}

// stl_tree.h, std::_Rb_tree::_M_lower_bound(_Link_type, _Link_type, const _Key&)
template<typename _Key, typename _Val, typename _KeyOfValue,
typename _Compare, typename _Alloc>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
_M_lower_bound(_Link_type __x, _Link_type __y, const _Key& __k) {
while (__x != 0)
if (!_M_impl._M_key_compare(_S_key(__x), __k))
__y = __x, __x = _S_left(__x);
else
__x = _S_right(__x);
return iterator(__y);
}

在此处的 _M_impl._M_key_compare 也就是比较仿函数对象,在这里即less<int>对象。异常就是在这个函数里抛出的,可见问题出现在 _S_key(__x) 中。

1
2
3
4
// stl_tree.h, std::_Rb_tree::_S_key(_Const_Base_ptr)
static const _Key& _S_key(_Const_Base_ptr __x) {
return _KeyOfValue()(_S_value(__x));
}

这里的 _KeyOfValue() 就是我们给的 Selector 了。所以在这里如果将 operator() 的返回值写为 int ,则这里将其以 const _Key& 返回,就是返回局部对象的引用,后面又在 less<int>::operator() 里面使用了这个引用(这个引用实际上指向一个已过期的栈上数据: _KeyOfValue() 的返回值),这个行为导致了Segmentation Fault

C++右值引用

Rvalue references

在C++0x中介绍了一种新的引用类型:右值引用,它帮助解决了不必要的拷贝问题和允许了完美转发的存在。当赋值的右手边(right-hand side)是一个右值时,左手边(left-hand side)的对象就可以偷走右手边对象的资源,而不是进行一次独立的分配,也就诞生了”移动语义“。

左值:可以出现在 operator= 左侧者。

右值:只能出现在 operator= 右侧者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;

int main() {
int a = 9;
int b = 4;
// a + b = 42; // error: lvalue required as left operand of assignment

string s1("Hello ");
string s2("World");
s1 + s2 = s2; // s1 + s2 可以当做左值!
cout << s1 << endl;
cout << s2 << endl;
string() = "World"; // 可以对临时对象赋值!
}

C++在用户自定义类型中会产生一些细微的问题,这些问题有关于修改或赋值动作。这种问题就导致上面的定义不正确。

(但是仍然应把临时对象认为是右值。)

当右值出现在 operator= (拷贝赋值,copy assignment)的右侧,我们认为对资源进行偷取/搬移(move)而非拷贝(copy)是合理的。则

  1. 必须有语法让我们在调用端告诉编译器“这是个右值”
  2. 必须有语法让我们在被调用端写出一个专门处理右值的所谓移动赋值(move assignment)函数

C++的4种类型转换

static_cast

用隐式和用户定义转换的组合在类型间转换。

语法

static_cast <new_type> (expression) 返回 new_type 类型的值。

解释

唯有下列转换能用 static_cast 执行,但若转换会转型走常量性易变性则亦不允许

  1. 若存在从 expressionnew_type隐式转换序列,或者若针对以 expressionnew_type 类型的对象或引用所进行的直接初始化的重载决议,找到至少一个可行函数,则 static_cast <new_type> (expression) 返回如同以 new_type Temp(expression); 所初始化的一个虚构变量 Temp,它可能涉及隐式转换,对 new_type构造函数的调用,或对用户定义转换运算符的调用。对于非引用的 new_type,static_cast 纯右值表达式的结果对象是其直接初始化的对象。 (C++17 起)

  2. new_type 是某类类型 D 的左值或指向它的指针纯右值,而 expression 的类型是到其非虚基类 B 的指针或引用,则 static_cast 进行向下转型(downcast)。若 BD 的有歧义、不可访问或虚的基类(或虚基类的基类),则此向下转型非良构。这种 static_cast 并不进行用以确保该对象的运行时类型确实是 D 的运行时检查,而且仅当这项前提条件由其他方法所保证时才能安全使用,例如在实现静多态时。可以用 dynamic_cast 执行安全的向下转型。

    1
    2
    3
    4
    5
    struct B { };
    struct D : B { };
    D d;
    B& br = d;
    static_cast<D&>(br); // 左值指代原初的 d 对象
  3. (本条自C++11起)new_type 是右值引用类型,则 static_cast 将泛左值、类纯右值或数组纯右值 (C++17 前) 任何左值 (C++17 起) expression 的值转换为与该表达式指代相同对象,或指代其基类子对象(取决于 new_type)的亡值。若目标类型是表达式的不可访问或有歧义基类,则程序非良构。若表达式是位域左值,则它首先被转换成底层类型的纯右值。这种 static_cast 用于在 std::move 中实现移动语义。

  4. new_type 是(可为 cv 限定的)void 类型,则 static_cast求值 expression舍弃其值。

  5. 若存在从 new_typeexpression 类型的标准转换序列,且它不包含左值到右值、数组到指针、函数到指针、空指针、空成员指针、函数指针 (C++17 起)或布尔转换,则 static_cast 能进行该隐式转换的逆转换。

  6. 若从 expressionnew_type 涉及左值到右值、数组到指针或函数到指针转换,则能显式用 static_cast 进行。

  7. 有作用域枚举类型(C++11)能转换成整数或浮点类型。

    1. 当目标类型为 cv bool 时,若原值为零则结果为 false,而对所有其他值结果为 true。对于其余整型类型,若该枚举的值能以目标类型表示,则结果为其值,否则结果未指明。(C++20前)
    2. 其结果与从枚举的底层类型隐式转换成目标类型的结果相同。 (C++20 起)
  8. 整数或枚举类型值可转换为任何完整枚举类型。

    • 若底层类型不固定,则当 expression 的值落在范围(范围是大到足以保有目标枚举的所有枚举项的最小位域的所有可能值)外时,结果未指明 (C++17 前) 或 为未定义行为 (C++17 起)。

    • 若底层类型固定,则其结果与转换原值到枚举的底层类型再到该枚举类型的结果相同。

      浮点类型的值也可转换为任何完整枚举类型。

    • 其结果与转换原值到枚举的底层类型再到该枚举类型的结果相同。

  9. 指向某类 D 的成员的指针可以向上转型(upcast)为指向其无歧义、可访问的基类 B 的成员。这种 static_cast 不进行用以确保成员实际存在于所指向对象的运行时类型的检查。

  10. 指向(可有 cv 限定)void 的指针类型的纯右值可转换到指向任何对象的指针类型。若原指针值所表示的内存中字节地址不满足目标类型的对齐要求,则结果指针值未指明。否则,若原指针值指向对象 a,且存在与 a 指针可互转换(定义于下文)的目标类型的(忽略 cv 限定)对象 *b,则结果为指向 *b 的指针。否则指针值不改变。任何指针转换到 void 指针,再转换回原(或更为 cv 限定的)类型的指针,都保持其原值。

同所有转型表达式,结果是:

  • 左值,若 new_type 是左值引用或到函数类型的右值引用;
  • 亡值,若 new_type 是到对象类型的右值引用;
  • 否则为纯右值。

满足以下条件时,两个对象 ab 指针可互转换(pointer-interconvertible)

  • 它们为同一对象,或
  • 一个是 union 对象而另一个是该对象的非静态数据成员,或
  • 一个是标准布局类对象,而另一个是该对象的首个非静态数据成员,或若该对象无非静态数据成员,则为该对象的任何基类子对象,或
  • 存在对象 c 使得 ac 指针可互转换,而 cb 指针可互转换。
1
2
3
4
union U { int a; double b; } u;
void* x = &u; // x 的值为“指向 u 的指针”
double* y = static_cast<double*>(x); // y 的值为“指向 u.b 的指针”
char* z = static_cast<char*>(x); // z 的值为“指向 u 的指针”

static_cast 亦可用于通过进行到指定类型的函数到指针转换,来消解函数重载的歧义。如

1
2
std::for_each(files.begin(), files.end(),
static_cast<std::ostream&(*)(std::ostream&)>(std::flush));

C++构造函数的优化

下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

class A {
public:
A() { cout << "default ctor\n"; }
A(const A& b) { cout << "copy ctor\n"; }
~A() { cout << "dtor\n"; }
};

A foo() {
A b;
return b;
}

int main() {
A a = foo();
return 0;
}

理论上,它的运行结果应为

1
2
3
4
5
6
default ctor // foo函数第一行的对象b的构造
copy ctor // 对象b拷贝到返回值临时对象
dtor // 对象b析构
copy ctor // 返回值临时对象拷贝到main中对象a
dtor // 返回值临时对象析构
dtor // main对象析构

然而,当我们实际编译并运行它时,运行结果为

1
2
default ctor
dtor

发现所有的拷贝构造和临时对象的析构都被优化掉了。这就是NRV优化带来的性能上的好处。

《深度探索C++对象模型》学习笔记

1 关于对象

1.1 C++对象模式

C++对象模型

非静态数据成员放置于每一个类对象中,静态数据成员则被存放在类对象之外。静态与非静态成员函数也被放在类对象之外。虚函数则以两个步骤支持:

  1. 每一个类产生出一堆指向虚函数的指针,放在表格之中,此表称为虚表 (virtual table, vtbl)。
  2. 在每一个类对象中安插一个指针,指向相关的虚表。通常这个指针被称为 vptr 。它的设定与重置由每一个类的构造函数、析构函数、拷贝赋值运算符自动完成。每一个类所关联的 type_info 对象(用以支持runtime type identification, RTTI)也经由虚表指出,通常放在表格的第一个slot(位置)。

3 Data语义学

3.0 综述

  • 一个虚基类子对象只会在派生类中存在一份实例,不管它在继承体系中出现了多少次。
  • C++标准并不强制规定如“基类子对象的排列顺序“或”不同存取层级的数据成员的排列顺序“这种琐碎细节。它也不规定虚函数或虚基类的实现细节。C++标准说:那些细节由各家厂商自定。
  • C++对象模型把非静态数据成员直接存放在每一个类对象中,对于继承来的非静态数据成员(无论是虚继承还是非虚继承)也是如此。不过并没有强制定义其间的排列顺序。至于静态数据成员则被放置在程序的一个全局数据段(global data segment)中,不影响类对象的大小。

3.1 Data Member 的绑定

  1. 有关数据成员的绑定问题,现在的C++已经解决了。

  2. 若一个 inline 函数在 class 声明之后立刻被定义,则还是对其评估求值。即对成员函数本体的分析会直到整个class的声明都出现了开始。因此在一个inline成员函数体之内的一个数据成员绑定操作,会在整个class声明完成之后才发生。(member scope resolution rules)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    extern int x;

    class Point3d {
    public:
    // 对于函数本体的分析将延迟直到class声明的右大括号出现才开始
    float X() const { return x; }
    private:
    float x;
    };

    // 分析在这里进行
  3. 然而这对于成员函数的参数列表并不为真。参数列表中的名称还是会在它们第一次遇到时被适当地决议(resolved)完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef int length; // global typedef

    class Point3d {
    public:
    void mumble(length val); // length: int
    length mumble() { return _val; } // length: int
    private:
    typedef float length; // nested typedef
    length _val; // length: float
    }

    对于这种情况,请总是把“nested type声明”放在class的起始处。

3.2 Data Member 的布局

3.6 指向 Data Members 的指针

取一个非静态数据成员的地址将会得到它在类中的偏移量。

下面代码中数据成员指针的类型为:float Point3d::*。有些编译器返回的偏移量总是多1,因为考虑到不指向任何成员的指针应为0。如果不加1,有可能导致第一个数据成员成员的指针和不指向任何成员的指针相等,都为0。(此时说它是偏移量就有些不合适)

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
32
33
34
35
36
37
38
39
// G++ 8.1.0, 64-bit
#include <iostream>
using namespace std;

class Point3d {
public:
virtual ~Point3d() {}
static Point3d origin;
float x, y, z;
};

struct Base1 {
int val1;
};
struct Base2 {
int val2;
};
struct Derived : Base1, Base2 {
int val3;
};

Point3d Point3d::origin;

#define show(ptrToDataMember) printf(#ptrToDataMember " = %d\n", ptrToDataMember)

int main() {
// 注意不能用 cout << &Point3d::x,会匹配到 operator<<(bool)
// 为了简便,这里定义宏(与书上不同)
show(&Point3d::x); // 8
show(&Point3d::y); // 12
show(&Point3d::z); // 16

show(&Base1::val1); // 0
show(&Base2::val2); // 0
show(&Derived::val1); // 0
show(&Derived::val2); // 0(比较奇怪)
show(&Derived::val3); // 8
return 0;
}

个人感觉,成员指针并不能拿来输出。所以输出什么值也只能作为参考,便于理解这个概念。事实上,我觉得cout给出了正确的行为。它将一个成员指针视为bool,以表示其是否真正有效。即

1
2
3
4
5
6
7
8
9
10
11
12
struct Base1 {
int val1;
};

int main() {
int Base1::* p = 0;
cout << boolalpha << p << endl; // false
p = &Base1::val1;
printf("%d\n", p); // 0
cout << boolalpha << p << endl; // true
return 0;
}

C++引用的实现

当我学习C++引用时,听到的第一句话是“引用是变量的别名,不像指针一样需要占用内存空间”。然而学到深处,发现此话并不完全正确。

本文主要介绍我如何通过实验来了解到C++引用的实现,其实引用的内部就是指针。当然这也于编译器有关,所以这里需要提及一下测试所用的编译器及环境。

测试环境是MinGW的g++ 8.1.0,64位编译器,64位的机子。所以指针的大小是8个字节,即64个bit。(注:因为目的是测试,所以测试时并没有处理对new操作符所产生对象的回收)

首先我写出了如下代码,试图通过指针偏移来获取有关引用的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
using namespace std;

int main() {
int64_t x;
string& str = *new string();
int64_t y;

cin >> str; // 对引用做一次操作,避免编译器把变量优化掉

cout << &x << endl;
cout << &y << endl;
cout << str << endl;

return 0;
}

然而,这个程序的输出如下(str的输出忽略):

1
2
0x61fe00
0x61fdf8

难道引用真的不占内存?编译器真的很聪明,可能优化掉了吧;经过一系列尝试,我写出了另外一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
using namespace std;

void foo(int64_t q, string& s, int64_t r) {
cout << "&q: " << &q << endl;
cout << "&r: " << &r << endl;

cout << "*(string**)(&q + 1): " << *(string**)(&q + 1) << endl;
}

int main()
{
string& str = *new string();
cout << "main(): " << &str << endl;
foo(0, str, 0);
return 0;
}

这段代码的输出是:

1
2
3
4
main(): 0x1e1bd0
&q: 0x61fde0
&r: 0x61fdf0
*(string**)(&q + 1): 0x1e1bd0

可见,q的地址是0x61fde0,r的地址是0x61fdf0。两个地址间相差16个字节!这里引用占用的内存出来了。显然引用对应的指针存储在q的8个字节之后。我们可以将q的地址加1,也就是加上8个字节,这里存储的就是引用的信息。假设它就是指针,那么考虑:(&q + 1)本身是一个指向string*的指针,也就是string**。所以若要获取指针的值,需要对这个值解一次引用,输出出来。(当然如果你想简单一点,可以直接把它转成int64_t然后用16进制输出亦可)

至此真相大白,程序输出的最后一行0x1e1bd0与主函数中new出来的对象的地址(见输出第一行)一致。所以得出结论:引用是用指针实现的。用户对引用的访问操作都内含一次解引用,而这对用户来说是透明的

不过需要提及的是,回想本文的第一个测试,发现引用的指针空间被优化掉了。所以引用有时也不一定会在栈上真正以指针体现出来。

C++中的虚函数、重写与多态

在C++中顺利使用虚函数需知道的细节

  • 如函数在派生类中的定义有别于基类中的定义,而且你希望它成为虚函数,就要为基类的函数声明添加保留字virtual。在派生类的函数声明中,则可以不添加virtual。函数在基类中virtual,在派生类中自动virtual(但为了澄清,最好派生类中也将函数声明标记为virtual,尽管这非必须)。
  • 保留字virtual在函数声明中添加,不要再函数定义中添加。
  • 除非使用保留字virtual,否则不能获得虚函数,也不能获得虚函数的任何好处。
  • 既然虚函数如此好用,为何不将所有成员函数都设为virtual?这似乎只有一个理由——效率。编译器和“运行时”环境要为虚函数做多得多的工作。所以,无谓地将成员函数为virtual会影响程序执行效率。

重写

虚函数定义在派生类中发生改变时我们说函数定义被重写。一些C++书籍区分了重定义(redefine)和重写(override)。两者都是在派生类更改函数定义。函数是虚函数,就称为重写。如果不是,就称为重定义。对于我们程序员而言,这种区分似乎有点无聊,因为程序员在两种情况下做的事情是一样的。不过,编译器对于这两种情况确定是区别对待的。

多态

多态性是指借助晚期绑定技术,为一个函数名关联多种含义的能力。因此,多态性、晚期绑定和虚函数其实是同一个主题。

虚函数和扩展类型兼容性、切割问题

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
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;

class Pet
{
public:
virtual void print();
string name;
};

class Dog : public Pet
{
public:
virtual void print();
string breed; // 品种
};

void Pet::print()
{
cout << "Pet name: " << name << endl;
}

void Dog::print()
{
cout << "Dog name: " << name << ", breed: " << breed << endl;
}

int main()
{
Pet vPet;
Dog vDog;
vDog.name = "Tiny";
vDog.breed = "Great Dane";
vPet = vDog;
// cout << vPet.breed;
return 0;
}

上述代码vPet = vDog;的赋值是允许的,但赋给变量vPet的值会丢失其breed字段。这称为切割问题(slicing problem)。例如,cout << vPet.breed会报错。

切割问题:在将派生类对象赋给基类变量时,派生类对象有、基类没有的数据成员会在赋值过程中丢失,基类没有的成员函数也会丢失。在最终的基类对象中,将无法使用这些丢失的成员。

切割测试:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

class Demo
{
public:
Demo(const string& s): str(s)
{
cout << "Demo constructor called (" + str + ").\n";
}
~Demo()
{
cout << "Demo destructor called (" + str + ").\n";
}
Demo(const Demo& other)
{
str = other.str;
cout << "Demo copy constructor called (" + str + ").\n";
}
Demo& operator=(const Demo& other)
{
str = other.str;
cout << "Demo operator= called (" + str + ").\n";
return *this;
}
private:
string str;
};

class Base
{
public:
Demo member1 = Demo("member1");
};

class Derived : public Base
{
public:
Demo member2 = Demo("member2");
};

int main()
{
Derived derived;
Base base;
base = derived;
}
/* Output
Demo constructor called (member1).
Demo constructor called (member2).
Demo constructor called (member1).
Demo operator= called (member1).
Demo destructor called (member1).
Demo destructor called (member2).
Demo destructor called (member1).
*/

幸好,C++提供了一种方式,允许在将一个Dog视为Pet的同时不丢失品种名称:

1
2
3
4
5
6
7
Pet *pPet;
Dog *pDog;
pDog = new Dog;
pDog->name = "Tiny";
pDog->breed = "Great Dane";
pPet = pDog;
pPet->print(); // prints "Dog name: Tiny, breed: Great Dane"

基类Petprint()声明为virtual。所以一旦编译器看到pPet->print();就会检查PetDogvirtual表,判断pPet指向的是Dog类型的对象。因此,它会使用Dog::print(),而不是Pet::print()

配合动态变量进行OOP是一种全然不同的编程方式。只要记住以下两条简单的规则,理解起来就容易得多。

  1. 如果指针pAncestor的域类型是指针pDescendant的域类型的基类,则以下指针赋值操作允许:pAncestor = pDescendant;。此外,pDescendant指向的动态变量的任何数据成员或成员函数都不会丢失。
  2. 虽然动态变量所有附加字段(成员)都没有丢,但要用virtual成员函数访问。

视图对虚成员函数定义不齐全的类进行编译

编译前,如果还有任何尚未实现的virtual成员函数,编译就会失败,并产生形如undefined reference to Class_Name virtual table的错误信息。即使没有派生类,只有一个virtual成员,并且没有调用该虚函数,只要函数没有定义,就会产生这种形式的消息。此外,可能还会产生进一步的错误消息,声称程序对默认构造函数进行了未定义的引用,即使确实已定义了这些构造函数。

始终/尽量使析构函数成为虚函数(主要讲述把析构函数声明为虚函数的优点)

这里主要阐述让析构函数称为虚函数的好处,但实际上也有坏处。在《Effective C++》条款07中有提到具体内容,见本文后记。

C++11 新用法

基于哈希的 map 和 set

简述

基于哈希的 mapset ,它们分别叫做 unordered_map, unordered_set 。数据分布越平均,性能相较 mapset 来说提升就更大。但由于它们基于哈希,所以并不像 mapset 一样能自动排序;它们都是无序的。

我做了一个测试:随机生成 $10^7$ 个 int 范围内的整数(平均分布),然后将其分别插入 mapunordered_map,再完整的做一次查询,查看时间和内存上的消耗。

测试结果

结构 总耗时 插入耗时 查询耗时 内存
map 18,041 MS 10,299 MS 7,742 MS 230.7 MB
unordered_map 7,138 MS 5,426 MS 1,712 MS 212.0 MB

当数据分布平均时,从时间上看,两者的性能差距约为 $7138 / 18041 \approx 40\%$

高效随机数据生成-random与chrono

请注意random库与chrono库均是C++11。

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
32
33
34
35
36
37
38
39
40
41
#include <chrono>
#include <iostream>
#include <random>

using namespace std;
typedef long long LL;
typedef unsigned long long ULL;

// 生成 [a, b] 范围内的整数
// 其中 INT_MIN <= a <= b <= INT_MAX
int rand(int a, int b) {
static mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<int>(a, b)(rng);
}

// 生成 [a, b] 范围内的整数
// 其中 0 <= a <= b <= UNSIGNED_INT_MAX
unsigned rand(unsigned a, unsigned b) {
static mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<unsigned>(a, b)(rng);
}

// 生成 [a, b] 范围内的整数
// 其中 LONG_LONG_MIN <= a <= b <= LONG_LONG_MAX
LL rand(LL a, LL b) {
static mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<LL>(a, b)(rng);
}

// 生成 [a, b] 范围内的整数
// 其中 0 <= a <= b <= UNSIGNED_LONG_LONG_MAX
ULL rand(ULL a, ULL b) {
static mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<ULL>(a, b)(rng);
}

int main() {
const LL MAX = 1e10;
cout << rand(-MAX, MAX);
return 0;
}

bitset

用处

一含有特定二进制位数 N 的容器,相当于 bool 数组,但压缩内存至每一个二进制位。

申明

bitset<N> bset,其中 N 为字面常量,表示 bitset 中的二进制位个数。如 bitset<10> bset;

常用方法

bitset(unsigned long val)val 构造一个对应的 bitset (至 C++11 前)

bitset(unsigned long long val)val 构造一个对应的 bitset (自 C++11 始)

bitset(string val)val 构造一个对应的 bitset

bool operator[] 获取特定二进制位的值

bool test(pos) 获取特定二进制位的值,但有越界检查:如果越界,抛出 std::out_of_range 异常。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×