底层数据类型
容器类型 | 容器名 | 底层数据类型 | 能否随机访问 | 使当前位置的迭代器失效 | 使后续的迭代器失效 |
---|---|---|---|---|---|
顺序型容器 | vector | 数组 | 是 | 是 | 是 |
顺序型容器 | deque | 分散的储存块 | 是 | 是 | 是 |
顺序型容器 | list | 双向链表 | 否 | 是 | 否 |
关联容器 | set | 红黑树 | 否 | 是 | 否 |
关联容器 | multiset | 红黑树 | 否 | 是 | 否 |
关联容器 | map | 红黑树 | 否 | 是 | 否 |
关联容器 | multimap | 红黑树 | 否 | 是 | 否 |
关联容器 | unordered_map | hash | 是 | 是 | 否 |
关联容器 | unordered_set | hash | 是 | 是 | 否 |
容器适配器 | stack | ||||
容器适配器 | queue | ||||
容器适配器 | priority_queue | 是 |
vector
vector 是 动态数组 ,当分配空间时默认会多分配一部分空间,当空间不足时,先开辟新内存,然后将数据复制过去。因而 插入操作可能导致迭代器失效 。另外,根据数组的定义,删除迭代器也会导致当前及后续迭代器失效。
vector 在遍历时不应当修改数据,因为插入可能导致所有迭代器失效,删除迭代器可能导致当前及后续迭代器失效。 |
当 vector 需要插入大量数据时,可以先使用 reserve 预留空间
当 vector 使用 memcpy 拷贝数据后,需要通过 resize 更改容器大小
当 vector 插入新构造的对象时,可以使用 emplace_back 添加元素。emplace_back 的参数为容器元素的构造参数。因而可以原地进行构造
vector容器的扩容原理:1.5倍扩容,扩容后迭代器失效
deque
deque 是双端队列,底层是链表,但是链表的节点是可以容纳若干个元素的内存块。允许随机访问
map
map 底层数据类型为红黑树。由于红黑树二叉搜索树的一种变种,因此要求:
key 为 copyable, comparable
由于 std::map 要求 key 是 comparable,因此 std::map 是根据 key 排序的,因而无法保存插入顺序。 |
对 map 中的元素进行访问时,有两种方式:
使用 at 方法进行访问,若 key 不存在则抛出异常
使用 operator [] 方法进行访问,若 key 不存在则原地 使用 value 的默认构造函数 构造一个对象,然后再返回
因此 map 的 operator[] 要求 value 类型具备默认构造函数。但是 at 则不需要
由于 map 删除迭代器只会导致当前迭代器失效,因此一种遍历时删除数据的方式为:
auto begin = m.begin();
while( begin != m.end() ){
if( *begin == 'xxx' ){
m.erase(begin++);
continue;
}
++begin;
}
要检查一个 key 是否存在,有三种方式:
使用 find 方法
使用 count 方法
使用 at 方法访问并捕获异常
map 是有序的,而 std::remove 的工作方式是将需要删除的元素放在末尾。因而 map 无法在 std::remove 上工作。 |
容器适配器
容器适配器对存储的容器有要求。 stack 和 queue 需要顺序容器,队列要求容器允许前端插入,优先队列要求提供随机访问能力
stack 默认情况下使用双端队列,
使用容器储存引用
一个简单的 vector 定义如下:
template <typename T, typename Alloc = alloc>
class vector {
public:
// traits
using value_type = T;
using pointer = value_type*;
using iterator = value_type*;
using reference = value_type&;
// others
};
可见,vector 定义了类型的引用。由于 C++ 不允许定义指向引用的指针 因而导致容器不能储存引用。
C++ 提供了 std::reference_wrapper 对引用进行包装。从而提供了使容器储存引用的方式。
可以使用 std::ref 和 std::cref 将引用包裹为对象。
迭代器
迭代器指向的是一个左闭右开的区间,这点在进行 erase、substr 等操作时一定要注意。区间的大小是 end - begin - 1。
迭代器可以配合以下情形:
函数 | 作用 |
---|---|
remove | 将需要删除的元素 移动 到容器末尾,返回值为第一个需要删除的元素 |
count_if | 对容器内指定的值进行计数 |
一个常见的用法是:
arr.erase(remove(arr.begin(), arr.end(), val), arr.end());
随机数
需要包含头文件 random
对于 Unix 系统而言,标准库提供了指向 /dev/random 的 random_device 用来提供真随机数:
void print(int n) {
for(int i = 0; i < n; ++i) { cout << '*'; }
cout << endl;
}
int main() {
random_device devcie;
for(int i = 0; i < 20; ++i) { print(devcie() % 10 + 1); }
return 0;
}
结果如下:
**** ******** ********** ****** *** ***** *** ****** ***** ********** ******* ****** ******** * ****** * ********* *** ***** **********
可见,整体还是比较随机的
如果操作系统内没有 random 设备,就回退到普通的伪随机数引擎。
标准库将随机数生成的工具分为两个部分:引擎和分发器。
引擎可以简单地将其理解为生成随机数的工具,分发器对生成的随机数进行处理,以生成随机的或符合二项分布的或符合泊松分布的随机数
既然说了引擎是生成随机数的工具,实际上引擎是可以直接被调用的。分发器有没有实际上无所谓
引擎 | 作用 |
default_random_engine | 生成结果为 uint 的伪随机数 |
linear_congruential_engine<> | 使用线性同余算法的伪随机数 |
mersenne_twister_engine<> | 使用梅森扭曲算法 |
分发器:
分发器 | 作用 |
uniform_int_distribution | 产生指定范围内的整数 |
uniform_real_distribution | 产生随机范围内的实数 |
bernoulli_distribution | 伯努利分布 |
poisson_distribution | 泊松分布 |
normal_distribution | 正态分布 |
discrete_distribution | 离散分布 |
例如我们使用正态分布:
default_random_engine eng;
normal_distribution<double> dis(1, 10);
for(int i = 0; i < 20; ++i) { print(dis(eng)); }
结果如下:
******* * ******** * ***** *** ********************** **** *********** ******** **************
常用算法
首先是排序函数:
函数 | 作用 |
sort(begin, end) | 对 [begin, end) 范围内的元素进行排序 |
stable_sort | 稳定排序 |
partial_sort(begin,middle,end) | 从 [begin,end) 中选出 middle-begin 个最小元素放到 [begin, middle) 中 |
partial_sort_copy | 不改变原数组 |
nth_element(begin, nth, end) | 返回升序排序时应当位于 nth 位置的元素 |
sort 数据量大的时候使用快排,分段进行归并排序,分段后如果数据比较少,就用插入排序,如果递归太深,就用堆排序
合并函数
函数 | 作用 |
merge | 将两个有序序列合并成一个 |
inplace_merge(begin, middle, end) | 将有序序列 [begin, middle) 和 [middle, end) 合并成一个 |
二分查找:
函数 | 作用 |
lower_bound | 要求序列升序,查找范围内第一个不小于目标值的元素 |
upper_bound | 要求序列升序,查找范围内第一个大于目标值的元素 |
equel_range | 查找范围内第一个等于目标值的元素 |
binary_search | 查找范围内是否包含目标元素 |
另外是几个谓词:
函数 | 作用 |
all_of | 序列中的元素全部可以使谓词返回 true |
any_of | |
none_of | |
equal | 检查两个序列是否相等 |
几个辅助算法:
函数 | 作用 |
unique | 对 有序 序列去重,返回新序列的结束迭代器 |
min | 参数为两个值或者是 容器 |
max_element | 给出最大值的索引 |
next_permutation | 按照字典升序的方式对序列进行全排列,已经达到最大排列时返回 false |
pre_permutation | 按照字典降序的方式对序列进行全排列,已经达到最大排列时返回 false |
emplace_back | 在容器内添加一个元素并就地构造,因此略去了 push_back 构造临时对象的开销 |
shuffle | 洗牌算法,打乱序列的顺序,需要传入随机数引擎 |
常用算法
非修改序列操作
算法 | 作用 |
adjacent_find | 查找两个相邻(Adjacent)的等价(Identical)元素 |
all_of | (C++11) 检测在给定范围中是否所有元素都满足给定的条件 |
any_of | (C++11) 检测在给定范围中是否存在元素满足给定条件 |
count | 返回值等价于给定值的元素的个数 |
count_if | 返回值满足给定条件的元素的个数 |
equal | 返回两个范围是否相等 |
find | 返回第一个值等价于给定值的元素 |
find_end | 查找范围 A 中与范围 B 等价的子范围最后出现的位置 |
find_first_of | 查找范围 A 中第一个与范围 B 中任一元素等价的元素的位置 |
find_if | 返回第一个值满足给定条件的元素 |
find_if_not | (C++11) 返回第一个值不满足给定条件的元素 |
for_each | 对范围中的每个元素调用指定函数 |
mismatch | 返回两个范围中第一个元素不等价的位置 |
none_of | (C++11) 检测在给定范围中是否不存在元素满足给定的条件 |
search | 在范围 A 中查找第一个与范围 B 等价的子范围的位置 |
search_n | 在给定范围中查找第一个连续 n 个元素都等价于给定值的子范围的位置 |
修改序列操作
算法 | 说明 |
copy | 将一个范围中的元素拷贝到新的位置处 |
copy_backward | 将一个范围中的元素按逆序拷贝到新的位置处 |
copy_if | (C++11) 将一个范围中满足给定条件的元素拷贝到新的位置处 |
copy_n | (C++11) 拷贝 n 个元素到新的位置处 |
fill | 将一个范围的元素赋值为给定值 |
fill_n | 将某个位置开始的 n 个元素赋值为给定值 |
generate | 将一个函数的执行结果保存到指定范围的元素中,用于批量赋值范围中的元素 |
generate_n | 将一个函数的执行结果保存到指定位置开始的 n 个元素中 |
iter_swap | 交换两个迭代器(Iterator)指向的元素 |
move | (C++11) 将一个范围中的元素移动到新的位置处 |
move_backward | (C++11) 将一个范围中的元素按逆序移动到新的位置处 |
random_shuffle | 随机打乱指定范围中的元素的位置 |
remove | 将一个范围中值等价于给定值的元素删除 |
remove_if | 将一个范围中值满足给定条件的元素删除 |
remove_copy | 拷贝一个范围的元素,将其中值等价于给定值的元素删除 |
remove_copy_if | 拷贝一个范围的元素,将其中值满足给定条件的元素删除 |
replace | 将一个范围中值等价于给定值的元素赋值为新的值 |
replace_copy | 拷贝一个范围的元素,将其中值等价于给定值的元素赋值为新的值 |
replace_copy_if | 拷贝一个范围的元素,将其中值满足给定条件的元素赋值为新的值 |
replace_if | 将一个范围中值满足给定条件的元素赋值为新的值 |
reverse | 反转排序指定范围中的元素 |
reverse_copy | 拷贝指定范围的反转排序结果 |
rotate | 循环移动指定范围中的元素 |
rotate_copy | 拷贝指定范围的循环移动结果 |
shuffle | (C++11) 用指定的随机数引擎随机打乱指定范围中的元素的位置 |
swap | 交换两个对象的值 |
swap_ranges | 交换两个范围的元素 |
transform | 对指定范围中的每个元素调用某个函数以改变元素的值 |
unique | 删除指定范围中的所有连续重复元素,仅仅留下每组等值元素中的第一个元素。 |
unique_copy | 拷贝指定范围的唯一化(参考上述的 unique)结果 |
划分
算法 | 作用 |
is_partitioned | (C++11) 检测某个范围是否按指定谓词(Predicate)划分过 |
partition | 将某个范围划分为两组 |
partition_copy | (C++11) 拷贝指定范围的划分结果 |
partition_point | (C++11) 返回被划分范围的划分点 |
stable_partition | 稳定划分,两组元素各维持相对顺序 |
排序
算法 | 作用 |
is_sorted | (C++11) 检测指定范围是否已排序 |
is_sorted_until | (C++11) 返回最大已排序子范围 |
nth_element | 部份排序指定范围中的元素,使得范围按给定位置处的元素划分 |
partial_sort | 部份排序 |
partial_sort_copy | 拷贝部分排序的结果 |
sort | 排序(快速排序) |
stable_sort | 稳定排序 |
二分法查找(用于已划分/已排序的序列)
算法 | 作用 |
binary_search | 判断范围中是否存在值等价于给定值的元素 |
equal_range | 返回范围中值等于给定值的元素组成的子范围 |
lower_bound | 返回指向范围中第一个值大于或等于给定值的元素的迭代器 |
upper_bound | 返回指向范围中第一个值大于给定值的元素的迭代器 |
合并(用于已排序的序列)
算法 | 作用 |
includes | 判断一个集合是否是另一个集合的子集 |
inplace_merge | 就绪合并 |
merge | 合并 |
set_difference | 获得两个集合的差集 |
set_intersection | 获得两个集合的交集 |
set_symmetric_difference | 获得两个集合的对称差 |
set_union | 获得两个集合的并集 |
堆
算法 | 作用 |
is_heap | 检测给定范围是否满足堆结构 |
is_heap_until | (C++11) 检测给定范围中满足堆结构的最大子范围 |
make_heap | 用给定范围构造出一个堆 |
pop_heap | 从一个堆中删除最大的元素 |
push_heap | 向堆中增加一个元素 |
sort_heap | 将满足堆结构的范围排序 |
最大/最小值
算法 | 作用 |
is_permutation | (C++11) 判断一个序列是否是另一个序列的一种排序 |
max | 返回两个元素中值最大的元素 |
max_element | 返回给定范围中值最大的元素 |
min | 返回两个元素中值最小的元素 |
min_element | 返回给定范围中值最小的元素 |
minmax | (C++11) 返回两个元素中值最大及最小的元素 |
minmax_element | (C++11) 返回给定范围中值最大及最小的元素 |
其他
算法 | 作用 |
lexicographical_compare | 比较两个序列的字典序 |
next_permutation | 返回给定范围中的元素组成的下一个按字典序的排列 |
prev_permutation | 返回给定范围中的元素组成的上一个按字典序的排列 |
使用 vector/string 作为缓冲区
在开发中一个经常碰到的情况是使用 vector/string 作为缓冲区,这种情况一定要注意调用 resize,否则容器的实际容量和观察到的容量大小不等:
这部分有误,需要重写 |
std::string buf { 20, '\0'};
memcpy(&buf[0], source, 20);
buf.resize(20);
时间
头文件 <chrono> 中命名空间 std::chrono 提供了处理事件段和事件点的组件。此组件的产生来自高能物理的需求,因此没有提供常用日期和日历的这种组件。
系统时间的获取有两种方式:system_clock 或者 steady_clock,两者的区别在于 steady_clock 在联系调用时,时间不会发生倒退的情况
auto start_time = steady_clock::now();
auto end_time = steady_clock::now();
auto duration = end_time - start_time;
auto milli_duration = duration_cast<milliseconds>(duration);
cout<<milli_duration.count()<<"ms";
正如上面所显示的那样,调用 now 返回一个 time_point,time_point 作差得到 duration,直接可以通过 duration_cast 得到指定精度的时间。
chrono 定义了以下几种时间:
类型 | 含义 |
nanoseconds | 纳秒 |
microseconds | 微秒 |
milliseconds | 毫秒 |
seconds | 秒 |
minutes | 分钟 |
hours | 小时 |
不访问时钟的时间运算结果可以是 constexpr 的 |
智能指针
什么是智能指针?
智能指针是一个句柄
智能指针在离开作用域时自动调用实际指针的析构函数
第一点句柄代表了智能指针本身不会失效,因此没有判空问题
第二点代表了智能指针可以自动管理内存
shared_ptr
shared_ptr 的参数有两个,一个是需要管理的 ptr,另外一个是 shared_ptr 销毁时需要执行的函数。
shared_ptr 的第一种写法为:
int *a = new int(10);
shared_ptr<int> scopedA(a);
但是另一种更自然的方式是将两行代码合并起来:
shared_ptr<int> scopedA(new int(10));
对于数组而言需要更改默认的删除函数:
shared_ptr<int> scopedA(int int[10], std::default_delete<int[]>());
shared_ptr 和 make_shared
如下代码:
shared_ptr<string> str(nullptr);
auto str2 = make_shared<string>();
*str2 = "haha";
assert(str == nullptr);
assert(*str2 == "haha");
使用 shared_ptr 创建出来的对象用于绑定一个已有的堆对象,其参数为一个对象指针,因此 可以绑定到空指针 ,但是 make_shared 是直接创建了一个匿名的堆对象,并直接使用 shared_ptr 管理它,其参数是参数 T 的构造参数,因此不能绑定到空指针。
C++ 20 开始,make_shared 才能用来创建数组 |
类型转换
智能指针的转换不能使用普通的 dynamic_cast 等,而应当使用 std::static_pointer_cast, std::dynamic_pointer_cast, std::const_pointer_cast, std::reinterpret_pointer_cast 这几个函数。
shared_ptr 的注意事项
根据 C++ shared_ptr四宗罪) 可以由以下结论:
一个对象只能由一个 shared_ptr 管理,否则会出现多次析构。因此 shared_ptr 适用于移动语义
shared_ptr 应当至少与原对象的声明周期相同,但是一定不能比原对象的声明周期短。
shared_ptr 无法转移对象的所有权
一旦使用 shared_ptr,则 shared_ptr 将会从库中传播到用户代码中,则唯一的方法就是使用回调函数。
若资源在外部已经由 shared_ptr 管理了,这时候资源需要获取指向自己的 shared_ptr,那么唯一的办法就是获取一个引用计数与外部 shared_ptr 共享的智能指针。这唯一的方法就是让对象继承 enable_shared_from_this
智能指针在多线程中性能不佳。
enable_shared_from_this
以下为摘录内容
我们来看看具体的代码实现逻辑:
struct Good: std::enable_shared_from_this<Good> // 注意:继承
{
std::shared_ptr<Good> getptr() {
return shared_from_this();
}
};
struct Bad
{
// 错误写法:用不安全的表达式试图获得 this 的 shared_ptr 对象
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}
};
这里我们可以看到,Good类继承了std::enable_shared_from_this,并且自己是作为模板参数传递给父类的。这就给让代码看起来有些"唬人",看起来像是继承自己一样。但其实呢?这里只是用到了模板派生,让父类能够在编译器感知到子类的模板存在,二者不是真正意义上的继承关系。
这里只分析下面两个问题:
为什么Bad类直接通过this构造shared_ptr会存在问题?
答:因为原本的this指针就是被shared_ptr管理的,通过getprt函数构造的新的智能指针和和原本管理this指针的的shared_ptr并不互相感知。这会导致指向Bad的this指针被二次释放!!!
为什么通过继承std::enable_shared_from_this之后就没有上述问题了?
答:这里截取了部分std::enable_shared_from_this的源码并且简化了一下:
template<typename _Tp>
class enable_shared_from_this
{
protected:
enable_shared_from_this(const enable_shared_from_this&) noexcept { }
~enable_shared_from_this() { }
public:
shared_ptr<_Tp>
shared_from_this()
{ return shared_ptr<_Tp>(this->_M_weak_this); }
shared_ptr<const _Tp>
shared_from_this() const
{ return shared_ptr<const _Tp>(this->_M_weak_this); }
private:
mutable weak_ptr<_Tp> _M_weak_this;
};
std::enable_shared_from_this 的实现由于有些复杂,受限于篇幅。笔者就不展开来分析它具体是怎么样实现的了。它的能够规避上述问题的原因如下:
通过自身维护了一个 std::weak_ptr
让所有从该对象派生的 shared_ptr
都通过了 std::weak_ptr
构造派生。
std::shared_ptr
的构造函数判断出对象是 std::enable_shared_from_this
的子类之后也会同样通过对象本身的 std::weak_ptr
构造派生。这个这样引用计数是互通的,也就不会存在上述 double delete 的问题了。
weak_ptr
weak_ptr 用于关联到一个 shared_ptr 上。而且这个关联不会导致 shared_ptr 中的引用计数增加。主要目的是防止环形引用
weak_ptr 比较重要的一点是用来做资源的 handle,这样就不用担心空指针的问题了,因为 handle 是不会失效的
另外
析构动作在创建时被捕获
这是一个非常有用的特性,这意味着:
虚析构不再是必需的
shared_ptr<void> 可以持有任何对象,而且能安全地释放
shared_ptr 对象可以安全地跨越模块边界,比如从 DLL 里返回,而不会造成从模块 A 分配的内存在模块 B 里被释放这种错误
二进制兼容性,即便 Foo 对象的大小变了,那么旧的客户代码仍然可以使用新的动态库,而无须重新编译。前提是 Foo 的头文件中不出现访问对象的成员的 inline 函数,并且 Foo 对象的由动态库中的 Factory 构造,返回其 shared_ptr
析构动作可以定制
析构所在的线程
对象的析构是同步的,当最后一个指向 x 的 shared_ptr 离开其作用域的时候,x 会同时在同一个线程析构。这个线程不一定是对象诞生的线程。
这个特性是把双刃剑:如果对象的析构比较耗时,那么可能会拖慢关键线程的速度(如果最后一个 shared_ptr 引发的析构发生在关键线程);同时,我们可以用一个单独的线程来专门做析构,通过一个 BlockingQueue<shared_ptr<void> > 把对象的析构都转移到那个专用线程,从而解放关键线程。
std::shared_ptr<Node> getptr() {
return shared_ptr<Node>(this);
}
没问题,但是
this->shared_from_this()
会出现问题
对象池
对象池算是对智能指针指针的一次综合运用
class Stock {
string key_;
public:
Stock(string key) : key_(key) { }
const string& key() {
return key_;
}
};
class StockFactory : enable_shared_from_this<StockFactory> {
mutable mutex mutex_;
map<string, weak_ptr<Stock>> stocks_;
public:
shared_ptr<Stock> get(const string& key) {
std::lock_guard<mutex> lock(mutex_);
auto& wkStock = stocks_[key];
auto pStock = wkStock.lock();
if(!pStock) {
pStock.reset(new Stock(key), bind(&StockFactory::weakDeleteCallBack,
weak_ptr<StockFactory>(this->shared_from_this()), std::placeholders::_1));
}
return pStock;
}
static void weakDeleteCallBack(const weak_ptr<StockFactory>& wkFactory, Stock* stock) {
auto factory = wkFactory.lock();
if(factory) factory->removeStock(stock);
delete stock;
}
void removeStock(Stock* stock) {
if(stock) {
lock_guard<mutex> lock(mutex_);
stocks_.erase(stock->key());
}
}
};
类型转换
在类型转换中,有以下几个简单的原则:
相同的类型之间可以任意转换
空指针可以转变为目的类型的空指针
源类型的 cv 限定 ≤ 目的类型的 cv 限定
为了方便撰写,本文中做出以下规定:
下面假设源类型为 v ,目的类型为 T。例如 const_cast<T>(v)
T1::*data_x
代表指向 T.x 的数据成员指针,T::*fn_x
代表指向 T.x() 成员函数指针,T::*mem_x
代表指向 T.x 或 T.x() 的指针
当初学者刚学到类型转换的时候,可能会迫不及待地将其用于向下类型转换(这里指针指向的对象实际上是基类对象)或者横向类型转换,那么我现在告诉你。C++ 中从来不鼓励也不会教导你这么做,你使用 dynamic_cast 得到的只是一个空指针罢了,使用 reinterpret_cast 是一个未定义行为,使用 static_cast 还不清楚,但是所有的方法都不鼓励你这么做。更可怕的是,如果你不加校验地使用了得到的指针,除非你调用的函数涉及到了类的成员变量,否则函数是可以被调用成功而的。 也就是说: 空指针可以调用不涉及成员变量读写的成员函数 |
const_cast[1]
表达式 const_cast<T>(v) 的结果是类型 T
若 T 是一个左值引用,则结果是一个左值
若 T 是一个对象类型的右值引用,则结果是一个将亡值
否则,使用 标准转换 (左值到右值、数组到指针、函数到指针)产生一个纯右值
所有能够显式使用 const_cast 的表达式如下:
在服从下述条款的情况下,允许相同类型之间的转换
对于 相似类型 T1 和 T2。在考虑 cv 分解的情况下,纯右值 T1 可以转换到类型 T2。表达式的结果取决于源类型
typedef int *A[3]; // int* 数组,大小为 3 typedef int const *const CA[3]; // const int 指针的数组 CA &&r = A{}; // 经过 cv 转换后将临时数组对象绑定到引用 A &&r1 = const_cast<A>(CA{}); // 不允许!临时数组退化到指针 A &&r2 = const_cast<A&&>(CA{}); // 允许
对于对象类型 T1 和 T2 。若 T1* 可以通过 const_cast 显式转换到 T2* ,则:
如果使用 const_cast 修改了指针、左值或数据成员指针的 cv 限定,那么对其施加写操作,依赖于对象的类型,可能会产生为定义行为
如果 T1 和 T2 不同,从类型 T1 向类型 T2 的变易性的改变将会导致 T1 进行 cv 分解 以让 T2 的 cv 分解 产生这种形式:
\[cv_0^2P_0^2cv_1^2P_1^2\cdots cv_{n-1}^2P_{n-1}^2cv_n^2U_2\]但是 资格转换 并不会将 T1 转换为 \(cv_0^2P_0^2cv_1^2P_1^2\cdots cv_{n-1}^2P_{n-1}^2cv_n^2U_1\)
如果纯右值 T1* 向 T2* 的转换过程中丢弃了常量性,那么使用左值引用转换将 T1 的左值引用转换为 T2 的左值引用或使用右值引用转换将 T1 类型的表达式转换为 T2 类型的将亡值也会丢失常量性
一些只涉及 cv 限定的转换无法通过 const_cast 实现。比如指针没法转换到函数,这种情况将会导致未定义行为。由于一些原因,成员函数指针,尤其是将 const 限定的成员函数指针转变为 非 const 限定的成员函数指针是不允许的
reinterpret_cast[2]
若 T 是一个左值引用或函数类型的右值引用,则结果为左值
若 T 是一个对象类型的右值引用,则结果为将亡值
否则,使用 标准转换 (左值到右值、数组到指针、函数到指针)产生一个纯右值
所有能够显式使用 reinterpret_cast 的表达式如下:
reinterpret_cast 不会移除常量性。整型、枚举、指针、指向成员的指针可以被显式转换到它本身的类型
通过 reinterpret_cast 在源值上进行映射时产生的结果 也许 和源值不同
指针可以被转换到任何足够容纳它的整数上。其映射函数取决于实现(这是因为机器的底层地址结构是不同的)
std::nullptr_t 可以被转换到合法的整型上,其含义和有效性与将 (void*)0 转换为整型相同
reinterpret_cast 不允许其他类型转换为 std::nullptrt
整型或枚举类型可以被显式转为指针类型。指针被转到足以容纳它的整型上然后再转到指针上,其结果于源指针相同。指针与整型之间的映射取决于实现。(除了 basic.stc.dynamic.safety 中的情况外,其他情况无法产生一个安全的指针)
一个函数指针可以被转换到另一种类型的函数指针。(通过这种转换过的函数指针调用函数的结果是未定义的)。除了将一个纯右值的 T1* 转为 T2* 再转为 T1* 外,其余用法是未定义的。(详见 指针转换 )
一个对象指针可以被显式地转为另一种类型的对象指针。当纯右值 obj* 转换到 cv obj* 时,其结果为
static_cast<cv T*>(static_cast<cv void*>(v))
。将指向对象 T1 的 T1* 转换到 T2* (此处 T2 的对齐要求不能比 T1 更严格)再转换为初始类型,结果与原指针相同
有条件地支持将函数指针转换为对象指针类型,反之亦然。这种转换的含义是实现定义的,除非一个实现支持双向转换,将一种类型的纯右值转换为另一种类型再转回去,可能具有不同的 cv 限定,但与原始指针具有相同的值
空指针可以转为任何类型的空指针值。
类型为 nullptr_t 的空指针常量无法被转换到空指针,整型的空指针常量不一定转换为空指针值
若 T1 和 T2 同为函数类型或对象类型,则纯右值
T1::*mem_x
可以被转换为T2::*mem_Y
。除了以下情况外,将空成员指针转换为目的类型的空成员指针的行为是未指定的:将纯右值
T::*fn
转到一个不同类型的成员函数指针再转为T::*fn
将纯右值
T1::*data_X
转为T2::*data_Y
(T2 的内存对齐不能比 T1 严格)再转为T1::*data_X
若 T1* 可以通过 reinterpret_cast 转为 T2* ,则 T1 类型的泛左值表达式可以转为 T2的引用。结果是
*reinterpret_cast<T2 *>(p)
此处 p 指向是 T1 类型的 x 的指针。没有创建临时对象、没有拷贝、没有构造、没有任何转换函数被调用[1]
static_cast[3]
对于此表达式而言:
若 T 是左值引用或函数类型的右值引用,则结果为左值
若 T 是对象的右值引用,则结果是将亡值
否则,结果是一个纯右值。 static_cast 不会去除 const 限定
类型 B 的左值可以被转换到其子类 D 的引用上, D 的 cv 限定不得比 B 小。若 B 是 D 的虚有基类、或虚有基类的基类、或不存在从 D* 到 B* 的转换表达式,则不合法。B 的将亡值可以被转换到 D 的右值引用上,此时 D 的 cv 限定与 B 相同。如果 B 实际上是 D 的基类的子对象,则结果引用了 D 类型的嵌套对象。否则结果是未定义的。
例如:
struct B {}; struct D : public B {}; D d; B &br = d; static_cast<D&>(br); // 产生最初 d 对象的左值
若 T2 和 T1 是
引用容许 (Reference-Compatible)
的,则 T1 的左值可以转到 T2 的右值引用上。如果值不是一个位域,则结果会引用对象或指定的基类子对象,否则,位域将被执行一个 左值到右值 转换,产生的纯右值被用于本节剩余部分的 static_cast 表达式。 若 T2 是 T1 的不可访问或无法确定的基类,则这种转换时不允许的。如果存在从表达式 e 到 T 的
隐式转换序列 (Implicit Conversion Sequence)
、重载了对象的直接初始化或存在从 e 到类型 T 的引用类型,则表达式 e 可以被显式转换为类型 T 。若 T 是一个引用类型,则此行为等同于声明并初始化T t(e)
。对一些临时变量 t 并使用此临时变量作为转换的结果。否则,此结果对象直接从 e 进行初始化当尝试从类表达式转换到类不可访问或含糊的基类时,此行为不合法
否则,static_cast 将会执行以下行为之一。其他形式的类型转换将不会显式使用 static_cast
任何表达式都可被转换到 cv 限定的 void 类型上,这种情况下表达式将会成为一个
无登记值表达式 (Dsicarded-Value Expression)
然而,如果这个值时临时对象,那么它的析构函数直至 Usual Time 时才会运行。对象的值将会被保留以用作析构
标准转换 的任何反转形式不包含以下转换:左值到右值、数组到指针、函数到指针或空指针或空成员指针或布尔或函数指针的转换。正确形式的转换可以显式使用 static_cast 转换。如果程序使用了 static_cast 进行了非法形式的转换,那末程序也是非法的
struct B { }; struct D : private B { }; void f() { static_cast<D*>((B*)0); // error: B is a private base of D static_cast<int B::*>((int D::*)0); // error: B is a private base of D }
如果 static_cast 的转换没有丢失常量性,那么左值到右值、数值到指针和函数到指针的转换可用。以下是对特定部分的额外规则:
域化枚举 可以被显式转换为整型或浮点类型。其结果相当于枚举的底层类型向目的类型的转换。
整型和枚举类型可以被显式转换为一个类型完全的枚举类型。如果枚举具有固定的底层类型,则值先被转换为整型,必要的时候再被转换为枚举类型。如果枚举没有固定类型,而且值在枚举范围之类,则不做任何改变,否则是为定义行为。浮点类型于此类似,先被转换为枚举的底层类型,再转换为枚举
如果 D 是一个类型完全的、派生自 B 的类。那么允许纯右值 cv1 B* 转换为指向纯右值 cv2 D*,这里要求 cv1 不得大于 cv2 。如果 B 是一个虚有基类,或 B 拥有虚有基类,或没有一个从 D* 到 B* 的有效转换,则程序是非法的。允许空指针转换到目的类型的空指针。如果纯右值 cv1 B* 指向的 B 确实是是 D 的一个子类,则产生的目的指针指向 D 类型的闭包对象。否则行为未定义。
如果 D 是 B 的一个类型完全的子类,那么允许 cv1 D::* 到 cv2 B::* 的转换,这里 cv1 ≤ cv2
函数类型(包括指向成员函数的指针)是没有 cv 限定的
如果没有从 B::* 到 T::* 的有效标准转换,则程序非法。空成员指针值可以被转换为目的类型。如果 B 包含成员、是成员变量的基类或是有成员变量的类的派生类,则转换允许。否则行为未定义
这里的 D::* 指的是指向成员变量的指针
虽然B 不需要包含原始成员,但通过成员指针指向的实际对象必须包含原始成员
允许 cv void* 到 cv2 T* 的转换,这里 cv1 ≤ cv2 。如果源指针指向的是 A 的地址,而且 A 不是内存对齐的,则产生的值
未指定 (Unspecified)
。如果源指针指向的对象 a ,而类型 T(忽视 cv 限定) 的对象 b 是与 a 指针兼容 的,那么产生的结果是指向 b 的指针,否则,指针的值在转换过程中不会发生变化T* p1 = new T; const T* p2 = static_cast<const T*>(static_cast<void*>(p1)); bool b = p1 == p2; // b will have the value true.
dynamic_cast[4]
dynamic_cast 用于将表达式 v 转换为类型 T,这里 T 是一个 类型完全 的类的指针或引用,或者是 cv 限定的 void*。dynamic_cast 不会移除 const 限定。
如果 T 是指针类型, v 是类型完全的类的指针的纯右值,结果产生了一个 T 类型的纯右值
如果 T 是一个左值引用, v 是类型完全的类的左值引用,结果是产生了一个 T 类型的左值引用
如果 T 是一个右值引用, v 是类型完全的类的泛右值,结果是产生了一个 T 类型的将亡值
若 v 与 T 类型相同,且 v 的 cv 限定不低于 T,则结果为 v
如果 v 是一个空指针,结果是产生一个 T 类型的空指针
如果 T 是 B*, v 是 D*,且 B 是 D 的基类,则将 v 所指向的 D 对象的子对象转为唯一一个 B 类型的对象。类似地,如果 T 是一个 B 类型的引用, v 是 D 类型的引用,产生的是唯一引用。不管是指针还是对象,要求 B* 的 cv 限定不得小于 D*,而且 B 不得是 D 的不可访问或混淆基类。
例如:
struct B { }; struct D : B { }; void foo(D* dp) { B* bp = dynamic_cast<B*>(dp); // equivalent to B* bp = dp; }
否则, v 将会成为一个泛左值或多态类型
如果 T 是一个 cv 限定的 void* ,则结果是产生一个 v 所指向的
最亲近对象 (Most Drived Object)
的指针。否则,将会在运行时检查被 v 说引用/指向 的对象是否可以被转换到类型 T如果 C 代表 T 所指向的类,理论上运行时检查方式为:
如果 v 指向 C 对象的公有基类的子对象,而且只有一个 C 类型的对象派生自 v 所指向的子对象,结果指向 C 对象
否则,若 v 指向公有基类的派生类型的对象,而且这个派生类型只有 C 是它的基类,结果指向 C 派生类型的对象。
否则,运行时检查失败
如果转换失败,则得到一个目的类型的空指针。如果是引用转换失败,则抛出一个相关的异常 std::bad_cast
例如:
class A { virtual void f(); }; class B { virtual void g(); }; class D : public virtual A, private B { }; void g() { D d; B* bp = (B*)&d; // cast needed to break protection A* ap = &d; // public derivation, no cast needed D& dr = dynamic_cast<D&>(*bp); // fails ap = dynamic_cast<A*>(bp); // fails bp = dynamic_cast<B*>(ap); // fails ap = dynamic_cast<A*>(&d); // succeeds bp = dynamic_cast<B*>(&d); // ill-formed (not a runtime check) } class E : public D, public B { }; class F : public E, public D { }; void h() { F f; A* ap = &f; // succeeds: finds unique A D* dp = dynamic_cast<D*>(ap); // fails: yields null; f has two D subobjects E* ep = (E*)ap; // ill-formed: cast from virtual base E* ep1 = dynamic_cast<E*>(ap); // succeeds }
|
标准转换[5]
optional
optional 提供了可选值。optional 可以被转换为 bool 值。跟随 optional 一起出现的还有一个 std::nullopt_t
类型用于指示 optional 为空、一个 nullopt 对象。
optional 在以下情况下非空:
使用非 std::nullopt_t 类型构造对象
使用赋值运算符分配了一个非 std::nullopt_t 对象
optional 在以下情况下为空:
使用默认构造函数
使用 std::nullopt_t 初始化构造函数
使用 std::nullopt_t 为 optional 赋值
调用 reset 函数清空了对象
常用的几个函数为:
函数 | 作用 |
---|---|
operator bool | 判断是否有值 |
value | 获取值 |
value_or | 有值的时候返回值,没值的时候提供默认值。(该函数不会更新 optional) |
reset | 清空值 |
variant
variant 是对 enum 的扩充,是一个类型安全的 enum,并不是广义上的异质类型。
variant 对保存的值具有特殊要求:
参数列表中的第一个值必须有默认构造函数
不允许保存不完整类型
不允许保存引用和数组
不允许不储存值
为了避免最后一种清空,C++ 引入了 std::monostate 类型来使 variant 可以陷入无值的状态。此外,当 variant 切换值时如果抛出了异常,因为旧值已经被清除,新值又构造失败,就会导致陷入无值的状态。 |
variant 允许保存各种 cv 限定的类型,但是不允许保存引用、数组、void 类型,同时也不允许不保存值。
variant 常用函数为:
函数 | 作用 |
---|---|
index | 获取当前保存值的索引。如果没有保存值,返回 std::variant_npos |
emplace | 原地构造一个值 |
std::get | 同元组。获取一个值 |
get_if | 将指定索引的值写入入参。没有值就返回 false |
any
any 是允许保存任何值的异质类型。any 要求被保存的类型具备拷贝构造 。
常用的函数为:
函数 | 用途 |
---|---|
reset | 清除值 |
has_value | 检查是否有值 |
type | 返回类型的 typeid |
std::any_cast | 将 any 转换到指定类型 |
std::make_any | 构造一个 any |
当 std::any_cast 失败时,抛出 std::bad_any_cast
异常。
tuple
tuple 是一个可以保存不同类型的容器。能够保存的类型是已知的,类似于 std::multi_variant。
线程安全性
标准库容器中的线程安全定义如下:[2]
能同时在不同容器上由不同线程调用所有容器函数。更广泛而言, C++ 标准库函数不读取能通过其他线程访问的对象,除非这些对象能直接或间接地经由函数参数,包含 this 指针访问。
能同时在同一容器上由不同线程调用 const 成员函数。而且,成员函数 begin()、end()、rbegin()、rend()、front()、back()、data()、find()、lower_bound()、upper_bound()、equal_range()、at() 和除了关联容器中的 operator[] 对于线程安全的目标表现如同 const (即它们也能同时在同一容器上由不同线程调用)。更广泛而言,C++ 标准库函数不会修改对象,除非这些对象能直接或间接地经由函数参数,包含 this 指针访问。
同一容器中不同元素能由不同线程同时修改,除了 std::vector<bool> 的元素(例如, std::future 对象的 vector 能从多个线程接收值)。 迭代器操作(例如自增迭代器)读但不修改底层容器,而且能与同一容器上的其他迭代器操作同时由 const 成员函数执行。会使任何迭代器失效的容器操作都会修改容器,且不能与任何在既存迭代器上的操作同时执行,即使这些迭代器尚未失效。
同一容器上的元素可以同时由不指定为访问这些元素的函数修改。更广泛而言, C++ 标准库函数不间接读取能从它的参数访问的对象(包含容器的其他对象),除非其规定要求如此。
任何情况下,容器操作(还有算法,或其他 C++ 标准库函数)可于内部并行化,只要不更改用户可见的结果(例如 std::transform 可并行化,但指定了按顺序观览序列的每个元素的 std::for_each 不行)
请注意:map.operator [] 是有可能修改容器的。并不是一个线程安全的方法。 |