深入应用C++11的第二篇笔记,包括智能指针,多线程,cast和一些其他工具
智能指针
share_ptr共享的智能指针
基础使用
1 | void delete_ptr(int* p) { |
封装一个可以支持数组的方法
1 | template<typename T> |
陷阱
接下来介绍一些使用shared_ptr需要注意的陷阱
1.不要用一个原始指针初始化多个shared_ptr
1 | int* ptr = new int; |
2.不要在函数实参中创建shared_ptr
function (shared_ptr<int>(new int), g( ) ); // 有缺陷
因为C++的函数参数的计算顺序在不同的编译器不同的调用约定下可能是不一样的,一般是从右到左,但也有可能是从左到右,所以,可能的过程是先new int,然后调g()。
3.通过shared_from_this()返回this指针
不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构。
1 | struct A |
4.避免循环引用
这个其实其实从逻辑层面上就出错了,不多做介绍,如果真的要这样,可以用weak_ptr来避免。
unique_ptr独占的智能指针
unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个unique_ptr。
unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。
C++11目前还没有提供make_unique方法,在C++14中会提供和make_shared类似的make_unique来创建unique_ptr。
1 | unique_ptr<int> myPtr(new int); // Okay |
关于shared_ptr和unique_ptr的使用场景要根据实际应用需求来选择,如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
weak_ptr弱引用的智能指针
弱引用指针weak_ptr是用来监视shared_ptr的,不会使引用计数加1,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。weak_ptr还可以用来返回this指针和解决循环引用的问题。
基本使用
1 | // 使用use_count获取计数 |
weak_ptr返回this指针
在上一小节节中提到不能直接将this指针返回为shared_ptr,需要通过派生std::enable_shared_from_this类,并通过其方法shared_from_this来返回智能指针,原因是std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观测this智能指针,调用shared_from_this()方法时,会调用内部这个weak_ptr的lock()方法,将所观测的shared_ptr返回。
需要注意的是,获取自身智能指针的函数仅在shared_ptr
解决循环应用问题
这个我不想多做解释,因为我还是觉得逻辑设计成这样已经有些奇怪了,但用weak_ptr确实能解决这个问题。
线程/异步
线程
1 | void f(string message){ |
互斥量
互斥量是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据。
C++11中提供了如下4种语义的互斥量(mutex):
std::mutex:独占的互斥量,不能递归使用。
std::timed_mutex:带超时的独占互斥量,不能递归使用。
std::recursive_mutex:递归互斥量,不带超时功能。
std::recursive_timed_mutex:带超时的递归互斥量。
1 | // mutex用法,lock()/unlock()就完事了 |
条件变量
条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来用。
C++11提供了两种条件变量:
- condition_variable,配合std::unique_lock进行wait操作。
- condition_variable_any,和任意带有lock、unlock语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。
条件变量的使用过程如下:
- 拥有条件变量的线程获取互斥量。
- 循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行。
- 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程。
看个同步队列的例子
1 | template<typename T> |
原子变量
使用原子变量就不需要使用互斥量来保护该变量了,用起来更简洁。给个例子就懂了
1 | struct Counter |
call_once/once_flag
为了保证在多线程环境中某个函数仅被调用一次,使用std::call_once时,需要一个once_flag作为call_once的入参。
1 | std::once_flag flag; |
异步操作
C++11提供了异步操作相关的类,主要有std::future、std::promise和std::package_task。std::future作为异步结果的传输通道,可以很方便地获取线程函数的返回值;std::promise用来包装一个值,将数据和future绑定起来,方便线程赋值;std::package_task用来包装一个可调用对象,将函数和future绑定起来,以便异步调用。
future
首先介绍future,我想结合书上的原文和别处搜到的英文进行总结。
C++11中增加的线程,使得我们可以非常方便地创建和使用线程,但有时会有些不便,比如希望获取线程函数的返回结果,就不能直接通过thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后执行join(),最后得到结果,这个过程是比较烦琐的。thread库提供了future用来访问异步操作的结果,因为一个异步操作的结果不能马上获取,只能在未来某个时候从某个地方获取,这个异步操作的结果是一个未来的期待值,所以被称为future,future提供了获取异步操作结果的通道。我们可以以同步等待的方式来获取结果,可以通过查询future的状态(future_status)来获取异步操作的结果。
future_status有如下3种状态:
- Deferred,异步操作还没开始。
- Ready,异步操作已经完成。
- Timeout,异步操作超时。
获取future结果有3种方式:get、wait、wait_for,其中get等待异步操作结束并返回结果,wait只是等待异步操作完成,没有返回值,wait_for是超时等待返回结果。
这段介绍是书上的原文,但是并没有很好的例子,从别的博客哪里找来了进一步的介绍和补充(英文的材料确实讲的更好…)
In this post, we are going to talk about futures, more precisely std::future
1 | auto future = std::async(std::launch::async, [](){ |
Nothing really special here. std::async will execute the task that we give it (here a lambda) and return a std::future. Once you use the get() function on a future, it will wait until the result is available and return this result to you once it is. The get() function is then blocking. Since the lambda, is a void lambda, the returned future is of type std::future
这里提到了一点,同一个future,通过get的返回值,只能拿一次,之后就拿不到了,要重复使用返回结果,需要自己存储。
1 | auto future = std::async(std::launch::async, [](){ |
This time, the future will be of the time std::future
But get() is not the only interesting function in std::future. You also have wait() which is almost the same as get() but does not consume the result. For instance, you can wait for several futures and then consume their result together. But, more interesting are the wait_for(duration) and wait_until(timepoint) functions. The first one wait for the result at most the given time and then returns and the second one wait for the result at most until the given time point. I think that wait_for is more useful in practices, so let’s discuss it further. Finally, an interesting function is bool valid(). When you use get() on the future, it will consume the result, making valid() returns :code:`false. So, if you intend to check multiple times for a future, you should use valid() first.
这里介绍了wait,wait_for,wait_until,valid等函数以及它们的作用。
1 |
|
std::promise、std::packaged_task和std::future三者之间的关系
对于future已经介绍了很多了,但是promise,packaged_task,书上的解释有点抽象,直接从给的代码里分析他们的作用。
std::future提供了一个访问异步操作结果的机制,它和线程是一个级别的,属于低层次的对象。在std::future之上的高一层是std::packaged_task和std::promise,它们内部都有future以便访问异步操作结果,std::packaged_task包装的是一个异步操作,而std::promise包装的是一个值,都是为了方便异步操作,因为有时需要获取线程中的某个值,这时就用std::promise,而有时需要获一个异步操作的返回值,这时就用std::packaged_task。
但其实我们在介绍future中,已经介绍了async。std::async比std::promise、std::packaged_task和std::thread更高一层,它可以用来直接创建异步的task,异步任务返回的结果也保存在future中,当需要获取异步任务的结果时,只需要调用future.get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话,则调用future.wait()方法。这应该作为我们的首选。
1 | // a simple task: |
C++四种类型转换运算符
学习链接:http://c.biancheng.net/cpp/biancheng/view/3297.html
从上面扒下来的,做个简单的笔记,以后尽可能使用cast,养成好的习惯。
static_cast
static_cast 也不能用来去掉表达式的 const 修饰和 volatile 修饰。换句话说,不能将 const/volatile 类型转换为非 const/volatile 类型。
static_cast 是“静态转换”的意思,也就是在编译期间转换,转换失败的话会抛出一个编译错误。
1 |
|
dynamic_cast
当使用 dynamic_cast 对指针进行类型转换时,程序会先找到该指针指向的对象,再根据对象找到当前类(指针指向的对象所属的类)的类型信息,并从此节点开始沿着继承链向上遍历,如果找到了要转化的目标类型,那么说明这种转换是安全的,就能够转换成功,如果没有找到要转换的目标类型,那么说明这种转换存在较大的风险,就不能转换。
1 |
|
这个例子配合着上面的图,还是很好理解的。
const_cast
const_cast 比较好理解,它用来去掉表达式的 const 修饰或 volatile 修饰。换句话说,const_cast 就是用来将 const/volatile 类型转换为非 const/volatile 类型。
1 |
|
reinterpret_cast
可以用 reinterpret_cast 来完成,两个具体类型指针之间的转换、int 和指针之间的转换(有些编译器只允许 int 转指针,不允许反过来)。
1 |
|
C++11中的工具
时间库chrono
这个等要看了,看看就好了。
数值类型和字符串的相互转换
给个例子就好,不过很多我在mac上是失败了,做一个简单了解就可以了。
1 |
|
C++11的一些其余特性
委托构造函数
委托构造函数允许在同一个类中一个构造函数可以调用另外一个构造函数,从而可以在初始化时简化变量的初始化。
1 | class class_c { |
这里只要注意委托构造函数,不要循环调用就可以了,这点一般性不会发生。但是另一点需要注意,使用了代理构造函数就不能用类成员初始化了。
1 | class class_a { |
继承构造函数
1 | struct Base { |
这个报错的原因是:通过基类的构造函数去构造派生类对象是不合法的,因为派生类的默认构造函数隐藏了基类,比较常见的争取用法如下。
1 | struct Derived : Base { |
在派生类中重新定义和基类一致的构造函数的方法虽然可行,但是很烦琐且重复,C++11的继承构造函数特性正是用于解决派生类隐藏基类同名函数的问题的,可以通过using Base::SomeFunction来表示使用基类的同名函数,通过using Base::Base来声明使用基类构造函数。
1 | struct Derived2 : Base { |
原始字面量
这个其实就和markdown里的输入的字符串原样输出的功能类似,只要使用R”xxx(raw string)xxx”的格式即可,其中raw string会原样输出,跑个例子就可以了。
1 | void chap7_2() { |
final和override
final
看到final还是很亲切的,毕竟我是java转过来的,在c++11中,final的作用和java的类似,但又有些不同。
C++11中增加了final关键字来限制某个类不能被继承,或者某个虚函数不能被重写。如果修饰函数,final只能修饰虚函数,并且要放到类或者函数的后面。
1 | struct A { |
override
override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,还可以防止因疏忽把本来想重写基类的虚函数声明成重载。这样,既可以保证重写虚函数的正确性,又可以提高代码的可读性。它的用法和Java、C#中的用法类似,区别是override关键字和final关键字一样,需要放到方法后面。
1 | struct A { |
内存对齐
这个知识点,之前其实已经了解过了,再进一步学习一下,记一下笔记。
1 | struct MyStruct { |
利用alignas指定内存对齐大小
1 | alignas(32) long long a = 0; |
需要注意的是alignas只能改大不能改小 [2] 。如果需要改小,比如设置对齐数为1,仍然需要使用#pragma pack。但微软不支持改小,那就算了。
利用alignof和std::alignment_of获取内存对齐大小,这个我在第一篇笔记的option中有介绍过,就不做赘述了。
c++11一些新增加的算法直接看书上的介绍就好,也不展开了。
本文链接: http://woaixiaoyuyu.github.io/2022/03/30/%E6%B7%B1%E5%85%A5%E5%BA%94%E7%94%A8C++11%EF%BC%9A%E4%BB%A3%E7%A0%81%E4%BC%98%E5%8C%96%E4%B8%8E%E5%B7%A5%E7%A8%8B%E7%BA%A7%E5%BA%94%E7%94%A8--note2/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!