底层数据类型

容器类型容器名底层数据类型能否随机访问使当前位置的迭代器失效使后续的迭代器失效

顺序型容器

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/randomrandom_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,并且自己是作为模板参数传递给父类的。这就给让代码看起来有些"唬人",看起来像是继承自己一样。但其实呢?这里只是用到了模板派生,让父类能够在编译器感知到子类的模板存在,二者不是真正意义上的继承关系。

这里只分析下面两个问题:

  1. 为什么Bad类直接通过this构造shared_ptr会存在问题?

    答:因为原本的this指针就是被shared_ptr管理的,通过getprt函数构造的新的智能指针和和原本管理this指针的的shared_ptr并不互相感知。这会导致指向Bad的this指针被二次释放!!!

  2. 为什么通过继承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 的表达式如下:

  1. 在服从下述条款的情况下,允许相同类型之间的转换

  2. 对于 相似类型 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{}); // 允许
  3. 对于对象类型 T1 和 T2 。若 T1* 可以通过 const_cast 显式转换到 T2* ,则:

    • 通过 const_cast<T2&> 允许 T1 的左值引用转换到 T2 的左值引用

    • 通过 const­cast<T2&&> 允许 T1 的泛左值转换到 T2 的将亡值。而且若 T1 是一个类,则允许通过 const­cast<T2&&> 将 T1 的纯右值转换到 T2 的将亡值

      如果 v 是一个泛左值,则由 const_cast 产生的引用指向原始对象。否则将会使用 临时实体化转换

  4. 如果使用 const_cast 修改了指针、左值或数据成员指针的 cv 限定,那么对其施加写操作,依赖于对象的类型,可能会产生为定义行为

  5. 如果 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\)

  6. 如果纯右值 T1* 向 T2* 的转换过程中丢弃了常量性,那么使用左值引用转换将 T1 的左值引用转换为 T2 的左值引用或使用右值引用转换将 T1 类型的表达式转换为 T2 类型的将亡值也会丢失常量性

  7. 一些只涉及 cv 限定的转换无法通过 const_cast 实现。比如指针没法转换到函数,这种情况将会导致未定义行为。由于一些原因,成员函数指针,尤其是将 const 限定的成员函数指针转变为 非 const 限定的成员函数指针是不允许的

reinterpret_cast[2]

  • 若 T 是一个左值引用或函数类型的右值引用,则结果为左值

  • 若 T 是一个对象类型的右值引用,则结果为将亡值

  • 否则,使用 标准转换 (左值到右值、数组到指针、函数到指针)产生一个纯右值

所有能够显式使用 reinterpret_cast 的表达式如下:

  1. reinterpret_cast 不会移除常量性。整型、枚举、指针、指向成员的指针可以被显式转换到它本身的类型

  2. 通过 reinterpret_cast 在源值上进行映射时产生的结果 也许 和源值不同

  3. 指针可以被转换到任何足够容纳它的整数上。其映射函数取决于实现(这是因为机器的底层地址结构是不同的)

  4. std::nullptr_t 可以被转换到合法的整型上,其含义和有效性与将 (void*)0 转换为整型相同

    reinterpret_cast 不允许其他类型转换为 std::nullptr­t

  5. 整型或枚举类型可以被显式转为指针类型。指针被转到足以容纳它的整型上然后再转到指针上,其结果于源指针相同。指针与整型之间的映射取决于实现。(除了 basic.stc.dynamic.safety 中的情况外,其他情况无法产生一个安全的指针)

  6. 一个函数指针可以被转换到另一种类型的函数指针。(通过这种转换过的函数指针调用函数的结果是未定义的)。除了将一个纯右值的 T1* 转为 T2* 再转为 T1* 外,其余用法是未定义的。(详见 指针转换

  7. 一个对象指针可以被显式地转为另一种类型的对象指针。当纯右值 obj* 转换到 cv obj* 时,其结果为 static_cast<cv T*>(static_cast<cv void*>(v))

    将指向对象 T1 的 T1* 转换到 T2* (此处 T2 的对齐要求不能比 T1 更严格)再转换为初始类型,结果与原指针相同

  8. 有条件地支持将函数指针转换为对象指针类型,反之亦然。这种转换的含义是实现定义的,除非一个实现支持双向转换,将一种类型的纯右值转换为另一种类型再转回去,可能具有不同的 cv 限定,但与原始指针具有相同的值

  9. 空指针可以转为任何类型的空指针值。

    类型为 nullptr_t 的空指针常量无法被转换到空指针,整型的空指针常量不一定转换为空指针值

  10. 若 T1 和 T2 同为函数类型或对象类型,则纯右值 T1::*mem_x 可以被转换为 T2::*mem_Y 。除了以下情况外,将空成员指针转换为目的类型的空成员指针的行为是未指定的:

    • 将纯右值 T::*fn 转到一个不同类型的成员函数指针再转为 T::*fn

    • 将纯右值 T1::*data_X 转为 T2::*data_Y (T2 的内存对齐不能比 T1 严格)再转为 T1::*data_X

  11. 若 T1* 可以通过 reinterpret_cast 转为 T2* ,则 T1 类型的泛左值表达式可以转为 T2的引用。结果是 *reinterpret_­cast<T2 *>(p) 此处 p 指向是 T1 类型的 x 的指针。没有创建临时对象、没有拷贝、没有构造、没有任何转换函数被调用[1]

static_cast[3]

对于此表达式而言:

  • 若 T 是左值引用或函数类型的右值引用,则结果为左值

  • 若 T 是对象的右值引用,则结果是将亡值

  • 否则,结果是一个纯右值。 static_cast 不会去除 const 限定

    1. 类型 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 对象的左值
    2. 若 T2 和 T1 是 引用容许 (Reference-Compatible) 的,则 T1 的左值可以转到 T2 的右值引用上。如果值不是一个位域,则结果会引用对象或指定的基类子对象,否则,位域将被执行一个 左值到右值 转换,产生的纯右值被用于本节剩余部分的 static_cast 表达式。 若 T2 是 T1 的不可访问或无法确定的基类,则这种转换时不允许的。

    3. 如果存在从表达式 e 到 T 的 隐式转换序列 (Implicit Conversion Sequence) 、重载了对象的直接初始化或存在从 e 到类型 T 的引用类型,则表达式 e 可以被显式转换为类型 T 。若 T 是一个引用类型,则此行为等同于声明并初始化 T t(e) 。对一些临时变量 t 并使用此临时变量作为转换的结果。否则,此结果对象直接从 e 进行初始化

      当尝试从类表达式转换到类不可访问或含糊的基类时,此行为不合法

    4. 否则,static_cast 将会执行以下行为之一。其他形式的类型转换将不会显式使用 static_cast

    5. 任何表达式都可被转换到 cv 限定的 void 类型上,这种情况下表达式将会成为一个 无登记值表达式 (Dsicarded-Value Expression)

      然而,如果这个值时临时对象,那么它的析构函数直至 Usual Time 时才会运行。对象的值将会被保留以用作析构

    6. 标准转换 的任何反转形式不包含以下转换:左值到右值、数组到指针、函数到指针或空指针或空成员指针或布尔或函数指针的转换。正确形式的转换可以显式使用 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
      }
    7. 如果 static_cast 的转换没有丢失常量性,那么左值到右值、数值到指针和函数到指针的转换可用。以下是对特定部分的额外规则:

    8. 域化枚举 可以被显式转换为整型或浮点类型。其结果相当于枚举的底层类型向目的类型的转换。

    9. 整型和枚举类型可以被显式转换为一个类型完全的枚举类型。如果枚举具有固定的底层类型,则值先被转换为整型,必要的时候再被转换为枚举类型。如果枚举没有固定类型,而且值在枚举范围之类,则不做任何改变,否则是为定义行为。浮点类型于此类似,先被转换为枚举的底层类型,再转换为枚举

    10. 如果 D 是一个类型完全的、派生自 B 的类。那么允许纯右值 cv1 B* 转换为指向纯右值 cv2 D*,这里要求 cv1 不得大于 cv2 。如果 B 是一个虚有基类,或 B 拥有虚有基类,或没有一个从 D* 到 B* 的有效转换,则程序是非法的。允许空指针转换到目的类型的空指针。如果纯右值 cv1 B* 指向的 B 确实是是 D 的一个子类,则产生的目的指针指向 D 类型的闭包对象。否则行为未定义。

    11. 如果 D 是 B 的一个类型完全的子类,那么允许 cv1 D::* 到 cv2 B::* 的转换,这里 cv1 ≤ cv2

      函数类型(包括指向成员函数的指针)是没有 cv 限定的

      如果没有从 B::* 到 T::* 的有效标准转换,则程序非法。空成员指针值可以被转换为目的类型。如果 B 包含成员、是成员变量的基类或是有成员变量的类的派生类,则转换允许。否则行为未定义

      这里的 D::* 指的是指向成员变量的指针

      虽然B 不需要包含原始成员,但通过成员指针指向的实际对象必须包含原始成员

    12. 允许 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 类型的将亡值

    1. 若 v 与 T 类型相同,且 v 的 cv 限定不低于 T,则结果为 v

    2. 如果 v 是一个空指针,结果是产生一个 T 类型的空指针

    3. 如果 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;
      }
    4. 否则, v 将会成为一个泛左值或多态类型

    5. 如果 T 是一个 cv 限定的 void* ,则结果是产生一个 v 所指向的 最亲近对象 (Most Drived Object) 的指针。否则,将会在运行时检查被 v 说引用/指向 的对象是否可以被转换到类型 T

    6. 如果 C 代表 T 所指向的类,理论上运行时检查方式为:

  • 如果 v 指向 C 对象的公有基类的子对象,而且只有一个 C 类型的对象派生自 v 所指向的子对象,结果指向 C 对象

  • 否则,若 v 指向公有基类的派生类型的对象,而且这个派生类型只有 C 是它的基类,结果指向 C 派生类型的对象。

  • 否则,运行时检查失败

    1. 如果转换失败,则得到一个目的类型的空指针。如果是引用转换失败,则抛出一个相关的异常 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
      }
  • dynamic_cast 的转换涉及到了 vtbl,因此只有具有多态的类才能使用。dynamic_cast。这也进一步证明了 dynamic_cast 是一个运行时特性。

标准转换[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 [] 是有可能修改容器的。并不是一个线程安全的方法。

1. 当结果引用与源泛左值指向相同的对象时,这有时被称为类型双关语
Last moify: 2024-11-22 02:16:03
Build time:2025-07-18 09:41:42
Powered By asphinx