(智能指针,一些关键字,自动类型推导auto,右值引用移动语义完美转发,列表初始化,std::function & std::bind & lambda表达式使回调更方便,c++11关于并发引入了好多好东西,有:
std::thread相关
std::mutex相关
std::lock相关
std::atomic相关
std::call_once相关
volatile相关
std::condition_variable相关
std::future相关
async相关)
总结三方面:一个是更方便,一个是更有效率安全。 方便比如auto ,for i:arr等关键字,std::function。 有效比如智能指针,移动语义完美转发。
第三就是线程的支持。并发支持。原子变量,锁,条件变量等。异步获取结果future等,
9.智能指针问题
智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。
使用new和delete运算符进行动态内存的管理虽然可以提高程序的效率,但是也非常容易出问题:忘记释放内存,造成内存泄漏,在尚有指针引用内存的情况下就将其释放,产生引用非法内存的指针,程序发生异常后进入catch忘记释放内存。智能指针是借用RAII技术对普通指针进行封装。RAII技术,也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。使用存储在栈上的局部对象(类)来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放。 局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
C++11版本之后提供的智能指针包含在头文件中,分别是auto_ptr、shared_ptr、unique_ptr、weak_ptr
智能指针代码实现: 用两个类来实现智能指针的功能**,一个是引用计数类,另一个则是指针类。**
// 引用计数器类 用于存储指向同一对象的指针数
template<typename T>
class Counter
{
private:
// 数据成员
T *ptr; // 对象指针
int cnt; // 引用计数器
// 友元类声明
template<typename T>
friend class SmartPtr;
// 成员函数
// 构造函数
Counter(T *p) // p为指向动态分配对象的指针
{
ptr = p;
cnt = 1;
}
// 析构函数
~Counter()
{
delete ptr;
}
};
// 智能指针类
template<typename T>
class SmartPtr
{
private:
// 数据成员
Counter<T> *ptr_cnt; //
public:
// 成员函数
// 普通构造函数 初始化计数类
SmartPtr(T *p)
{
ptr_cnt = new Counter<T>(p);
}
// 拷贝构造函数 计数器加1
SmartPtr(const SmartPtr &other)
{
ptr_cnt = other.ptr_cnt;
ptr_cnt->cnt++;
}
// 赋值运算符重载函数
SmartPtr &operator=(const SmartPtr &rhs)
{
ptr_cnt = rhs->ptr_cnt;
rhs.ptr_cnt->cnt++; 增加右操作数的计数器
ptr_cnt->cnt--; 左操作数计数器减1
if (ptr_cnt->cnt == 0)
delete ptr_cnt;
return *this;
}
// 解引用运算符重载函数
T &operator*()
{
return *(ptr_cnt->cnt);
}
// 析构函数
~SmartPtr()
{
ptr_cnt->cnt--;
if (ptr_cnt->cnt == 0)
delete ptr_cnt;
else
cout << "还有" << ptr_cnt->cnt << "个指针指向基础对象" << endl;
}
};
shared_ptr:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。
shared_ptr初始化:
std::shared_ptr<T> sp; //空shared_ptr,可以指向类型为T的对象
std::shared_ptr<int> sp(new int(5)); //指定类型,传入指针通过构造函数初始化
std::shared_ptr<int> sp = std::make_shared<int>(5); //使用make_shared函数初始化
//智能指针是一个模板类,不能将一个原始指针直接赋值给一个智能指针,因为一个是类,一个是指针
std::shared_ptr<int> sp = new int(1); //error
unique_ptr unique_ptr唯一拥有其所指的对象,在同一时刻只能有一个unique_ptr指向给定对象。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
初始化:没有make_shared函数,只能通过new传入指针。
std::unique_ptr<T> up; //空unique_ptr,可以指向类型为T的对象,up会使用delete来释放它的指针
std::unique_ptr<int> up(new int(5)); //绑定动态对象
nique_ptr没有copy构造函数,不支持普通的拷贝和赋值操作;但却提供了一种移动机制来将指针的所有权从一个unique_ptr转移给另一个unique_ptr(使用std::move函数,也可以调用release或reset)
std::unique_ptr<int> upMove = std::move(up); //转移所有权
std::unique_ptr<int> up1(new int(5));
std::unique_ptr<int> up2(up1.release()); //up2被初始化为up1原来保存的指针,且up1置为空
std::unique_ptr<int> up3(new int(6));
up2.reset(up3.release()); //reset释放了up2原来指向的内存,指向up3原来保存的指针,且将up3置为空
unique_ptr适用范围比较广泛,它可返回函数内动态申请资源的所有权;可在容器中保存指针;支持动态数组的管理。
weak_ptr是一种弱引用指针,它是伴随shared_ptr而来的,不具有普通指针的行为 ,模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。。它主要是解决了shared_ptr引用计数的问题:在循环引用时会导致内存泄漏的问题。
weak_ptr指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired() 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)
lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针.
循环引用:A调用B,B调用A,这样初始化时A,B的计数为1,赋值时又加1,析构减1,最后还是1,资源没有释放。
只需要将A或B的任意一个成员变量改为weak_ptr:
不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针。因此,返回this可能会导致重复析构:
正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回this的shared_ptr:
智能指针线程安全吗?
结论:同一个shared_ptr被多线程读是安全的;同一个shared_ptr被多线程写不安全;共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。
原因,shared_ptr其实由指向对象的指针和计数器组成,计数器加减操作是原子操作,所以这部分是线程安全的,但是指向对象的指针不是线程安全的。比如智能指针的赋值拷贝,首先拷贝指向对象的指针,再使引用次数加减操作,虽然引用次数加减是原子操作,但是指针拷贝和引用次数两步操作 并不是原子操作,线程不安全,需要手动加锁解锁。
auto自动类型推倒(在以前的版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。)
auto 的一个典型应用场景是用来定义 stl 的迭代器。不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦;auto 用于泛型编程,不希望指明具体类型的时候,比如泛型编程中。
还有一个decltype和auto很像,也是自动类型推倒。
decltype(exp) varname [= value] 括号代表可省略,区别是,它根据exp表达式来推倒类型,表达式可以是简单的也可以是函数等。因为auto必须初始化,但decltype不用。auto不能用于类的非静态成员变量(也是因为没初始化),但decltype可以。
C++11右值引用右值引用只不过是一种新的 C++ 语法,真正理解起来有难度的是基于右值引用引申出的 2 种 C++ 编程技巧,分别为移动语义和完美转发。
在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。
1 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:
int a = 5; 5 = a; //错误,5 不能为左值.
C++ 中的左值也可以当做右值使用,例如:
int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用
2 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。
简单说,左值就是变量,右值就是字面常量。
引用,使用 “&” 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:
int num = 10;
int &b = num; //正确
int &c = 10; //错误
为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
int && a = 10;
a = 100;
cout << a << endl; (但是这种常量右值引用是没有意义的,引用一个不可修改的常量,因为完全可以交给常量左值引用完成)
非常量右值引用才可以实现移动语义和完美转发
移动语义
解决的问题:当拷贝对象时,如果对象成员有指针的话,是深拷贝的方式(浅拷贝的话,多次析构的问题),深拷贝会将指针指向的内存资源一起拷贝一份,如果临时对象中的指针成员申请了大量的堆空间,那么效率很低。
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。也可以用于unique_ptr转移所有权。
实现方式:手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式(引用就是浅拷贝),同时在函数内部重置原来的指针为NULL,有效避免了“同一块对空间被释放多次”情况的发生。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。
完美转发的概念只对模板编程且对函数性能非常讲究的时候才有用。
先要了解引用折叠的情况:
引用折叠是模板编程中的一个概念,是为了解决模板推导后出现双重引用(如下所示)的情况。
假设一个模板函数的定义如下:
template
void PrintType(T&& param){ … }
当T为int &类型,则param被推导成int & &&类型,而c++语法是不允许这种双重引用类型存在的(可以自己测试下定义一个双重引用变量,编译器将提示错误),所以便制定了引用折叠的规则,具体规则如下:
模板编程中参数类型推导出现双重引用时**,双重引用将被折叠成一个引用**,要么是左值引用,要么是右值引用。 折叠规则就是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
完美转发:完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)
怎么实现完美转发?
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值(不然的话只看成左值,因为引用折叠的规则);其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数,forward统一 以左值引用的形式接收参数 param。由此即可轻松实现函数模板中参数的完美转发。
列表初始化(初始化的统一性)
在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。C++11之前主要有以下几种初始化方式:小括号,等号。构造函数的初始化列表。这么多的对象初始化方式,不仅增加了学习成本,也使得代码风格有较大出入,影响了代码的可读性和统一性。
28 std::function实现回调机制的了解
什么是回调函数:如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调:解耦导致:高效、灵活现代C++的封装性导致模块之间一定的独立性。但是模块之间又需要互相协作,所以有了回调
直观一点:假设A是核心团队(A提供一个funA函数供B调用,它可以看成库函数,还有一个sell函数必须等funA返回结果才可以执行),B是另一个团队。
如果funA需要很长时间才能执行完成,如果不用回调,那么,A必须等B执行完才能执行接下来的sell(),浪费时间。
回调的话,就是funA(sell),sell回调函数虽然是A定义的,但是是B调用的,调用完直接把结果返回给A,A不用等。这就是高效
**灵活:**如果直接把sell函数写到funA函数里面不就万事大吉了吗,但是,funA不知道到底后来要做什么,只有调用者自己知道。所以如果不同人调用,只需要传入不同的函数指针就可以了。
思考:为什么用?
但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路
就是说回调函数的本质就是“只有我们B才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它模块A才知道,因此我们必须把我们知道的封装成回调函数告诉其它模块”
函数指针可以是普通函数,也可是类的静态函数。
也可是类的非静态函数。
为什么要用?
在C语言的时代,我们可以使用函数指针来把一个函数作为参数传递,这样我们就可以实现回调函数的机制。到了C++11以后在标准库里引入了std::function模板类,这个模板概括了函数指针的概念
std::function<int(int a, int b)> plusFunc;
它可以表示任何一个返回值为int,形参列表为int a, int b这样的函数指针。
int puls(int a, int b)
{
return a + b;
}
// 函数名就代表着该函数的地址,也就是指向该函数的指针
plusFunc = plus;
它同一了可调用对象的形式。可调用对象不仅仅是函数指针,也可以是lambda表达式,类的成员函数指针等。当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。若std::function不含目标,则称它为空,调用空的std::function的目标会std::bad_function_call异常。
std::bind通常有两大作用:
将可调用对象与参数一起绑定为另一个std::function供调用(仿函数)
只绑定部分参数,减少可调用对象传入的参数。要用占位符
std::bind绑定普通函数
double callableFunc (double x, double y) {return x/y;}
auto NewCallable = std::bind (callableFunc, std::placeholders::_1,2);
std::cout << NewCallable (10) << ‘\n’;
bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind(callableFunc,_1,2)等价于std::bind (&callableFunc,_1,2);
_1表示占位符,位于中,std::placeholders::_1;
第一个参数被占位符占用,表示这个参数以调用时传入的参数为准,在这里调用NewCallable时,给它传入了10,其实就相当于调用callableFunc(10,2);
lamda表达式更简短,可捕获上下文的数据,而且返回值定义可以省略。不怎么用,
并发相关
std::thread 实现多线程。
c++11之前你可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程。
std::thread t(func);c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能;
std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::Mutex主要分为超时和没有超时的互斥,以及递归没有递归的互斥。
c++11主要有std::lock_guard,RAII方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。
std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。
std::future
我们想要从线程中返回异步任务结果(即任务A的执⾏必须依赖于任务B的返回值),一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。
简单地说,std::future 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。
std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)
Provider 可以是函数或者类
std::async 函数,本文后面会介绍 std::async() 函数。
std::promise::get_future,get_future 为 promise 类的成员函数
std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数
std::future 一般由 std::async, std::promise::get_future, std::packaged_task::get_future 创建,不过也提供了构造函数。td::future 的拷贝构造函数是被禁用的,只提供了默认的构造函数和 move 构造函数(移动构造函数,利用右值作为参数)
std::future::valid()
检查当前的 std::future 对象是否有效,即释放与某个共享状态相关联。一个有效的 std::future 对象只能通过 std::async(), std::future::get_future 或者 std::packaged_task::get_future 来初始化。另外由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。
std::future::wait()
等待与当前std::future 对象相关联的共享状态的标志变为 ready.
如果共享状态的标志不是 ready(此时 Provider 没有在共享状态上设置值(或者异常)),调用该函数会被阻塞当前线程,直到共享状态的标志变为 ready。一旦共享状态的标志变为 ready,wait() 函数返回,当前线程被解除阻塞,但是 wait() 并不读取共享状态的值或者异常。(这就是和std::future::get()区别 )
std::future::wait_for() 可以设置一个时间段 rel_time
std::shared_future 与 std::future 类似,但是 std::shared_future 可以拷贝、多个 std::shared_future 可以共享某个共享状态的最终结果
参数是⼀个future,⽤这个future等待⼀个int型的产品:std::future& fut
⼦线程中使⽤get()⽅法等待⼀个未来的future,返回⼀个result。⽣产者使⽤async⽅法做⽣产⼯作并返回⼀个future,消费者使⽤future中的get()⽅法可以获取产品。
C++14,17 就不详细介绍了。 C++20多了协程。
auto自动类型推倒(在以前的版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。)
auto 的一个典型应用场景是用来定义 stl 的迭代器。不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦;auto 用于泛型编程,不希望指明具体类型的时候,比如泛型编程中。
还有一个decltype和auto很像,也是自动类型推倒。
decltype(exp) varname [= value] 括号代表可省略,区别是,它根据exp表达式来推倒类型,表达式可以是简单的也可以是函数等。因为auto必须初始化,但decltype不用。auto不能用于类的非静态成员变量(也是因为没初始化),但decltype可以。
final关键字,使得类不能被继承,函数不能被重写 override提示虚函数要重写
default:多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数;而我们有时候想禁止对象的拷贝与赋值,可以使用delete修饰,delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的。
explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换(针对一个参数的)
const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量
tuple tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。
C++11 long long超长整形
C++11 shared_ptr、unique_ptr、weak_ptr智能指针 具体看前面
还有一些新增的算法,all of any of, none of
继承构造函数:因为如果基类的构造函数很多,派生类就要重写基类的构造函数,可以使用using base::base,不用重写了。