目录
一.列表初始化initializer_list
1.什么是列表初始化
2.列表初始化的原理
二.auto/decltype/nullptr
1.auto - 自动类型推导
2.decltype - 指定类型
3.nullptr - C++空指针
三.范围for
四.右值引用/移动构造/移动赋值/万能引用/完美转发
1.什么是右值
2.左值与右值的概念
3.左值引用与右值引用
4.右值引用的价值(移动构造/移动赋值)
1).为何需要右值引用
2).回顾左值引用的价值
3).右值引用的价值
4).移动构造与移动赋值的实现(string类举例)
5.万能引用与完美转发
1).万能引用, 发生引用折叠
2).完美转发forward(x)
6.C++11类的八大默认成员函数
1.C++98 vs C++11
2.默认生成的移动构造与移动赋值
五.default/delete/final/override
六.可变参数模板
1.参数包的概念
2.计算参数包中参数个数
3.递归函数的方式展开args参数包
4.以数组的形式展开args参数包
5.可变模板参数在stl中的应用
七.lambda表达式
1.lambda表达式的优势
2.lambda表达式的语法
3.lambda表达式捕捉列表的多种捕捉方式
4.lambda表达式使用原理与底层实现原理
1).lambda表达式的使用原理
2).lambda表达式的实现原理
5.总结
八.function包装器/bind(绑定)包装器适配器
1.function包装器
2.bind适配器
在C++98中, 允许使用{}对数组或结构体进程初始化
C++11扩大了{}的适用范围, 并命名为列表初始化, 让所有的内置类型和自定义类型都可以使用列表初始化, 使用列表初始化可以加"=", 也可以不加
#include
using namespace std;struct point
{int _x;int _y;point(int x, int y):_x(x),_y(y){}
};int main()
{//对于内置类型变量//正常初始化int a1 = 10;double b1 = 3.14;//列表初始化int a2 = { 10 };int a3{ 10 };double b2 = { 3.14 };double b3{ 3.14 };//对于数组//正常初始化(也是用的列表)int arr1[] = { 1,2,3,4,5 };//列表初始化新特性int arr2[]{ 1,2,3,4,5 };//对于自定义类型变量//正常初始化(调用构造)point p1(13, 23);//列表初始化(本质:也是调用构造)point p2 = { 5,5 };point p3{ 5,5 };return 0;
}
列表初始化对于以上的演示并不常用, 且由于可读性比较差应该谨慎使用
列表初始化的真正意义是可以让stl容器进行使用(通过实现initializer_list构造)
//列表初始化
list
list
vector
对于内置类型而言, 是强制的语法规定, 暂且不谈
对于自定义类型而言, 如何支持的初始化列表, 其实本质上就是调用构造函数
1.对于大括号中的内容而言, 一开始在编译器看来就是一堆数据, 如果{...}这堆数据有匹配构造函数,
那么就直接用这些数据去调构造函数即可, 如下图
2.如果类中没有{...}这堆数据符合的构造函数, 那么列表{...}中的内容就会构造成一个initializer_list对象, 在C++11中新增了一个类型是initializer_list, 且该类型支持迭代器与范围for
initializer_list
initializer_list在C++中定义为类模板, 其模板参数根据列表内数据而定, 故列表{..}中的所有数据必须为同一类型
使用auto自动类型推导, 最终推到结果x是一个initializer_list类型对象
例子1: vector
例子2: map
对于内置类型和函数返回值, auto可以直接推导
对于自定义类型, auto无法推导还未定义的变量的类型
错误演示
指定某一变量在定义时的类型, 指定为与其他变量相同的类型, 但必须合理!
即使用double类型数据初始化, 也不会是double类型, 而是隐式转换为int类型
使用方法的扩展
decltype(a*b)
C语言中, NULL是指针类型, 即void*, C++中, NULL是int类型
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
为了更加安全, C++11中新增nullptr(空指针)
例如: 在C++代码中, 函数重载时NULL优先匹配int类型, 而nullptr优先匹配指针类型
范围for的底层就是迭代器的遍历
只是在形式上变得非常简洁, 但是底层与迭代器是一模一样的
所以只要是能够支持迭代器, 则一定可以支持范围for
用上面的initializer_list类的迭代器遍历举例
对于内置类型右值 -- 纯右值 对于自定义类型右值 -- 将亡值 简单来说: 就是一个即将被释放的值 例如: 1).函数传值返回的返回值 2).表达式(x+y, x*y) 3).字面常量(注意: 字符串常量属于左值) 4).匿名对象 左值一定可以被取地址, 如果不能取地址那么就是右值 左值可以修改, const修饰左值则不可修改, 右值不可以修改 左值可以在等号左边也可以在等号右边, 右值必须在等号右边 注: 字符串常量是左值! 总结: 左值: 可取地址, 可修改, 可以在等号左右 右值: 不可取地址, 不可修改, 不可在等号左边 引用左值的变量就是左值引用, 引用右值的变量就是右值引用 左值引用可以引用左值, 也可以引用右值, 但是在引用右值时必须加上const来延长其生命周期 右值引用只能引用右值 右值引用可以引用move之后的左值 右值引用完右值或是move的左值之后, 该引用会变成左值, 编译器会为其开辟空间, 并且可以对该右值引用进行修改, 若不想被修改可以用const修饰 由于左值引用想要引用右值必须加const, 也就意味着不可修改, 在某些场景下无法解决特定问题 例如, 当我以一个右值传参, 且这个参数如果是普通形参的话需要发生深拷贝, 我既不希望他发生深拷贝, 又要对内部数据进行修改 不希望拷贝说明一定要以引用方式传参 想要引用右值, 不能使用左值引用, 如果想要使用则需要用const左值引用 需要对数据进行修改, 说明该形参不能被const修饰 这时C++11中的右值引用就很好的解决了上述问题, 右值引用可以直接引用右值 (1).做参数, 可以减少拷贝增加效率, 或者可以作为输出型参数 (2).做返回值, 可以减少拷贝增加效率, 或者可以支持修改原数据, 例如vector与map中的operator[] 但左值引用有些场景效率仍不是最优 例如在stl的容器中, 对于传值返回的自定义类型对象, 在拷贝或赋值时, 需要调用其拷贝构造或者赋值重载, 并且这两个都必须是深拷贝, 需要重新开辟空间进行拷贝, 会大大降低效率 在C++11中, 新增右值引用, 间接性的解决了这个问题, 因为有了右值引用, 所以支持了移动构造与移动赋值 对于传值返回的自定义类型对象拷贝或赋值时, 编译器会选择最匹配的函数进行调用, 如果此时支持了移动构造与移动赋值就会去进行调用 移动构造与移动赋值不是拷贝, 而是资源转移, 相当于我要拷贝你, 而你是一个右值, 那么你就是一个将亡值, 我只需要将你我资源互换, 在你即将死亡时把我的数据也带走一起释放掉 与传统的拷贝构造相比, 移动构造减少了开辟空间的资源消耗, 仅仅是简单的资源转移(swap交换), 进一步提高效率, 右值引用的价值就体现于此 什么时候会调用移动构造呢? 当使用一个匿名对象拷贝时, 或者当使用一个返回了对象的值的函数时(返回值是string等,例如to_string, string类的operator+) 概括来说就是用一个右值对象去拷贝时, 如果实现了移动构造, 编译器优先调用移动构造, 如果没有实现的话, 编译器就退而求其次, 去调用拷贝构造 所以由于右值引用的出现, 使得C++支持了移动构造与移动赋值, 右值引用并没有延长将亡值的生命周期, 准确来说, 如果使用了移动构造或是移动赋值, 则是延长了资源的生命周期! 用string类举例 总结: 只有拷贝时需要深拷贝的类, 移动构造和移动赋值才有意义, 其本身就是为了减少拷贝提高效率而存在 在模板函数或类模板中如果有右值引用作为参数的函数, 那么这个右值引用就是万能引用 基于以上的引用折叠之后, 在模板中, 不管是左值还是右值, 传给右值引用后都变为了左值, 在去拿这个形参调用时, 优先匹配到的就都是左值引用参数的函数 如果我们想在传右值给万能引用时, 虽然发生了引用折叠但是仍想恢复回来, 也就是让他仍保留右值属性, 这时就要用到完美转发: std::forward 以下图片还是基于以上代码, 在这就不重复的编写代码, 只需要将万能引用参数进行完美转发即可 注: 完美转发之后只影响当次传参时为右值, 在这之后, 还是可以对a进行取地址的, 也就是说在完美转发传参结束后, a仍是左值, 下一次若仍有需求还要传参的话, 还需再进行完美转发 如果不是模板的话, 传入右值给右值引用时, 这时的右值引用依旧是一个左值, 如果还需要继续向下传参的话, 依旧可以使用完美转发, 此时T需要填具体类型 例如forward C++98中创建一个空类, 编译器会默认生成六个成员函数 构造, 析构, 拷贝构造, 赋值重载, 取地址重载, const取地址重载 C++11中创建一个空类, 在C++98的基础上又新增了两个默认生成的成员函数 移动构造, 移动赋值 C++98中 拷贝构造与赋值重载通过const左值引用的参数, 来完成左值与右值的拷贝 C++11中 拷贝构造与赋值重载主要来完成左值的拷贝 移动构造与移动赋值主要来完成右值的拷贝, 减少了不必要的深拷贝, 采用资源转移的方式拷贝 以移动构造举例, 如果在类中没有显式实现移动构造, 且没有显式实现析构, 拷贝构造, 赋值重载, 则编译器会自动生成一个移动构造 编译器自动生成的移动构造会做什么? 对于内置类型直接进行值拷贝 对于自定义类型, 去看这个自定义类型是否有移动构造, 如果有则优先调用移动构造, 如果没有则调用拷贝构造 注: 当一个类需要进行深拷贝, 就要自己显示去写移动构造和移动赋值 且如果显式实现了移动构造或移动赋值, 编译器就不再会自动生成拷贝构造和赋值重载 default: 强制生成默认成员函数 delete: 强制禁止生成默认成员函数 final: 在一对继承关系中, 禁止父类虚函数被重写 override: 在一对继承关系中, 检查子类是否重写父类虚函数, 若没重写则报错 把带有...的参数称为参数包, 参数包中含有N个参数(N>=0) Args是一个可变模板参数包, args是一个函数形参参数包 关于可变参数模板的使用, 我们无法直接获取到参数包args中每个参数的值, 只能够通过递归展开函数包或者数组打印的方式来依次展开每个参数 语法: sizeof...(args) 每一次调用ShowList都会将第一个参数传给val, 将剩余参数传给args参数包 接下来递归调用ShowList直至参数包中剩余参数为0, 调用ShowList()结束递归 使用数组来间接依次遍历, 数组初始化时采用逗号表达式使数组初始化为int类型 emplace与insert的使用相同, 只是传入参数不同 emplace与push_back的使用相同, 只是传入参数不同 emplace/insert, emplace_back/push_back其不同是体现在emplace系列是传入的可变参数, 而普通插入是直接传值或对象 例如 > v; > l; 如果是传统的插入方式, 则需要先构造一个pair对象, 然后再调用拷贝构造插入 emplace则不需要构造pair对象, 由于支持可变参数模板, 且传入的是可变参数, 则可以直接用传入的可变参数, 那么就可以依次遍历参数包来直接构造到插入位置, 也就不需要可以的再去构造一个pair对象了 所以在stl的插入中, 如果支持了emplace系列, 且要插入的是一个自定义类型, 则完全可以代替传统的insert或push_back, 可以减少一次构造, 更进一步提高效率 特例说在最前: lambda捕捉列表对于全局变量的捕捉问题, 假设此时有全局变量int a = 10 vs下使用[=]或[&]捕捉没有问题, 若[a]或[&a]捕捉则编译报错, linux则没有类似问题 让一个对象有像函数一样使用的方式: 1.函数指针(C语言很喜欢使用这种方式) 2.仿函数(C++98且STL中很多地方用到了仿函数) 3.lambda表达式(C++11) 站在使用者角度来看, lambda相较于仿函数而言更加简洁, 更加灵活, 更加直观 站在lambda底层实现的角度来看, 这两者差别不大 语法: [捕捉列表](参数列表)mutable->返回值类型{函数体实现} 捕捉列表: 可以捕捉"父"作用域的变量("父"代表当前函数栈帧), 在使用lambda时, 捕捉变量的过程类似于函数传参的过程(并不是传到显式写在参数列表中的形参, 这个过程对于使用者而言是透明的), 捕捉列表不可省略, 编译器根据[]来判断这是否为一个lambda表达式 参数列表: 与普通函数传参一致, 如果不需要传递参数则可将参数与()一同省略, 如果当mutable存在时, 参数列表便不可胜率, 如果参数为空应该写为() mutable: 如果列表以值的形式捕捉变量, 则捕捉到的变量默认都有const属性, 想要修改则需要mutable, 一般情况下不需要mutable, 并且在使用mutable时, 参数列表不可省略 ->返回值类型: 显式指明该lambda要返回的返回值类型, 若返回值类型为void则与->一同省略不写, 若返回值类型很明确, 则也同样可以省略, 便由编译器自动推导 {函数体}: 与普通函数一样, 此处为函数实现, 与普通函数不同的是, 除了可以使用参数列表中的内容, 还可以使用所有捕捉列表捕捉到的变量 [x] -- 值传递捕捉x [=] -- 值传递捕捉所有"父"作用域变量 [&x] -- 引用传递捕捉x [&] -- 引用传递捕捉所有"父"作用域变量 [this] -- 值传递捕捉当前this指针 1."父"作用域指只要捕捉列表能够看到的, 就能够被捕捉 2.可以混合捕捉, [=, &a, &b][&, a, b], 但不可[=, a]这样捕捉, 因为a构成了重复传递, 总之一个变量只能被同一种形式的捕捉捕捉一次 3.lambda表达式不可以互相赋值 lambda表达式在定义时会返回一个对象, 这个对象的类型对于使用者而言是透明的, 需要使用auto来接收, 让编译器自动推导 先创建lambda表达式对象: auto x = lambda表达式; 再以类似函数调用的方式去调用该对象: x(有参则传参, 无参则省略); lambda表达式的底层实现, 本质上与仿函数的底层原理相同, 先创建一个类, 再构造一个对象, 调用时就通过对象调用operator() 以上结论可以通过汇编观察 对于每个lambda表达式, 编译器都会为其创造独一无二的类, 即便是两个完全一致的lambda表达式也是如此 对于用户而言, 编译器为lambda表达式创建类这一过程是透明的, 用户是没有办法参与的 每个lambda表达式创建的类的类名是: lambda + uuid组成, 以确保该类是唯一的 lambda表达式的对象不能互相赋值, 但是可以赋值给函数指针 lambda表达式 对于使用者而言: 是匿名的, 使用起来更加直观便捷 对于编译器而言: 是命名的, 底层实现就是仿函数 总的来说, 可以说lambda表达式是让编译器替使用者做了更多的事(底层创建仿函数), 与范围for的原理相同 lambda表达式与仿函数对比, 使用差别很大, 但lambda底层实现就是仿函数的实现 函数指针, 仿函数对象, lambda表达式对象, 可以统一传给function包装器 使用function包装器需要包头文件: functional 使用方式: function<返回值类型(参数包, 存放参数类型)> 例如: 有一个int参数, 一个bool参数, 一个double参数, 返回值类型为float: function 函数指针, 仿函数, lambda表达式本质都是在做相同的事, 只是方式不同, 通过function包装器可以让这三者统一起来 同时还解决了lambda表达式返回的对象只能用auto接收的问题, 例如在map bind的概念 bind - 绑定 - 可以调整函数传参顺序与个数 bind返回对象可以用auto接收, 也可以用function包装器接收 bind的使用 1.bind的第一个参数可以传: 函数指针, 函数地址, 仿函数对象, lambda对象, function包装器 2.其余的参数(参数包): 传_1, _2... (_1, _2是定义在std::placeholders中的), 使用前一般要using namespace placeholders; 或者placeholders::_1 ... _1代表第一个形参, _2代表第二个形参, _n代表第n个形参 根据第一个参数中的形参个数, 参数包应传对应的_1, _2, _3... , 若不填_n则也可以用指定值绑定 bind的用途 1.bind可以调整传参顺序, 这个调整是根据bind中的_1,_2来自动调整的 2.bind可以调整传参个数, 通过在传参之前就绑定指定值的方式 在某些场景下, 如果需要用mapinitializer_list
initializer_list
while (it != list.end())
{cout <<*it <<&#39; &#39;;++it;
}
cout <
{cout <
cout <四.右值引用/移动构造/移动赋值/万能引用/完美转发
1.什么是右值
2.左值与右值的概念
//函数以值形式返回
string func()
{return "hello str\n";
}int main()
{int a = 10;//左值int b = 5;cout <<&a <<"-" <<& b <
3.左值引用与右值引用
int& i = a;//左值引用
int&& j = 10;//右值引用const int& k = 55;//被const修饰的左值引用可以引用右值
//int&& m = a;//右值引用不可以引用左值int&& z = move(a);//右值引用可以引用move之后的左值
int&& e = 89;
cout <<&e <
cout <const int&& u = 100;//const修饰右值引用防止被修改
4.右值引用的价值(移动构造/移动赋值)
1).为何需要右值引用
2).回顾左值引用的价值
3).右值引用的价值
4).移动构造与移动赋值的实现(string类举例)
namespace zsl
{//移动构造string(string&& s):_str(nullptr),_size(0),_capacity(0){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);//如果string类内实现了swap//swap(s);}//移动赋值operator=(string&& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);//如果string类内实现了swap//swap(s);}
}5.万能引用与完美转发
1).万能引用, 发生引用折叠
//万能引用//非模板函数下, &&是右值引用,只能引用右值
//且右值引用是左值,因为可以取地址
void func1(int&& a)
{cout <<&a <
//并且万能引用自身也变为左值
void func(int& a)
{cout <<"左值引用" <
void func(const int& a)
{cout <<"const左值引用" <
void func(int&& a)
{cout <<"右值引用" <
void func(const int&& a)
{cout <<"const右值引用" <
void func2(T&& a)
{//cout <<&a <
{int c = 20;const int i = c;func1(10);//func1(c);报错,因为右值引用不能引用左值func2(c);//左值func2(10);//右值func2(i);//const左值func2(move(i));//const右值return 0;
}2).完美转发forward
6.C++11类的八大默认成员函数
1.C++98 vs C++11
2.默认生成的移动构造与移动赋值
五.default/delete/final/override
Date() = default;
~Date() = delete;
virtual void func() final;
virtual void func() override;
六.可变参数模板
1.参数包的概念
//Args是一个模板参数包, args是一个函数形参参数包
template
void ShowList(Args... args)
{//...
}2.计算参数包中参数个数
template
void ShowList(Args... args)
{cout <
int main()
{ShowList(29, "world");ShowList(&#39;a&#39;, 1, "hello", 3.14);return 0;
}3.递归函数的方式展开args参数包
//递归出口,最后一次args...的参数为0个
void ShowList()
{cout <<"结束递归\n";
}template
void ShowList(const T& val, Args... args)
{cout <<"当前获取到的参数: " <
{//ShowList(29, "world");ShowList(&#39;a&#39;, 1, "hello", 3.14);return 0;
}4.以数组的形式展开args参数包
template
void MyPrint(const T& val)
{cout <
void ShowList(Args... args)
{int a[] = { (MyPrint(args), 0)... };
}int main()
{ShowList(&#39;a&#39;, 1, "hello", 3.14);return 0;
}5.可变模板参数在stl中的应用
vector
v.push_back(make_pair("left", 1));
v.emplace_back("right", 3);list
l.push_back(make_pair("sort", 0));
l.emplace_back("hello", 2);七.lambda表达式
1.lambda表达式的优势
2.lambda表达式的语法
3.lambda表达式捕捉列表的多种捕捉方式
4.lambda表达式使用原理与底层实现原理
1).lambda表达式的使用原理
int g_val = 10;int main()
{auto lambda1 = [=]()mutable->int {return g_val; };cout <<"捕捉全局变量: " <2).lambda表达式的实现原理
//对比lambda实现与仿函数实现class FunClass
{
public:FunClass(double rate):_rate(rate){}double operator()(int x, int y){cout <<"I am FunClass -> operator\n";return x * y * _rate;}
private:double _rate = 0;
};int main()
{int a = 10, b = 5;//创建仿函数对象fcFunClass fc(0.5);//调用仿函数double res1 = fc(a, b);//创建lambda表达式对象auto ld = [](int x, int y)->int{cout <<"I am Lambda -> operator\n"; return x * y;};//调用lambda表达式int res2 = ld(a, b);cout <<"仿函数: " <5.总结
八.function包装器/bind(绑定)包装器适配器
1.function包装器
#include
{return a * b;
}class myclass
{
public:int operator()(int a, int b){return a * b;}
};class Date
{
public:static void print_static(){cout <<"static:2022_12_7" <
{//函数指针传给包装器对象function
}2.bind适配器
#include
{return a * b;
}class myclass
{
public:int operator()(int a, int b){return a * b;}
};class Date
{
public:static void print_static(){cout <<"static:2022_12_7" <
{//函数指针传给包装器对象function#include
using namespace placeholders;int func(int a, int b)
{cout <<"a: " <}int main()
{auto b1 = bind(func, _2, _1);cout <#include
using namespace placeholders;class Date
{
public:static void print_static(){cout <<"static:2022_12_7" <
{//function版,需要传一个Date对象function
}#include
#include