C++11的一些实用特性
1.统一的列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
//统一的列表初始化
struct Date
{int year;int month;int day;
};void test1()
{Date d1 = { 2024,11,14 };int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
//统一的列表初始化
struct Date
{int year;int month;int day;
};void test1()
{Date d1{ 2024,11,14 };const Date& d2{ 2024,11,14 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };}
1.1.std::initializer_list
std::initializer_list的介绍文档: http://www.cplusplus.com/reference/initializer_list/initializer_list/
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值。
//用std::initializer_list初始化容器
vector<int> v1 = {2024,11,4,17,30};
map<string,string> dict = {{"sort","排序"},{"sort1","排序1"},{"sort2","排序3"}};
2. 声明
c++11提供了多种简化声明的方式
2.1.auto
C++11中,将auto用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
vector<int> v = {2,0,2,4,11,14};//auto推导e为v的元素的类型
for(auto& e : v)
{cout << e <<' ';
}//e自动推导为const char*类
auto e = "str";
2.1.decltype
关键字decltype将变量的类型声明为表达式指定的类型。
//decltype的使用场景
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
vector<decltype(&x)> v;
2.3.nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr关键字,用于表示空指针。
3.范围for
3.1范围for的使用条件
1for循环迭代的范围必须是确定的 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin和end的成员函数返回其迭代器,并且要实现operator++,begin和end就是for循环迭代的范围。
vector<int> v = {2,0,2,4,11,14};//范围for会依次取容器中的值
for(auto& e : v)
{cout << e <<' ';
}for(int e : v)
{cout << e <<' ';
}
4.STL容器的变化
array //对静态数组的封装,operator[]会检查封装,但vector完全够用
https://cplusplus.com/reference/array/
forward_list //单向链表
https://cplusplus.com/reference/forward_list/
unordered_map
https://cplusplus.com/reference/unordered_map/
unordered_set
https://cplusplus.com/reference/unordered_set/
5.右值引用和移动语义
5.1.什么是左值、右值?什么是左值、右值引用?
简单来说,
左值:可以取地址(我们可以获取它的地址+可以对非const左值赋值)
左值可以出现在赋值对象左边
右值:不可以取地址(通常为:常量、临时对象、匿名对象)
右值不可以出现在赋值对象左边
// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值/表达式必须是可修改的左值10 = 1;x + y = 1;fmin(x, y) = 1;
左值引用、右值引用之间的相互转换:不能直接转换
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将右值引用绑定到左值
int a = 10;
int&& r2 = a;// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
//move的实质相当于
int&& rrx5 = (int&&)a;
5.2.右值引用的意义
右值引用:可以减少拷贝
左值引用解决的场景:引用传参、引用传返回值
左值引用没有解决的场景:传返回值
右值可以分为两种
1.纯右值:内置类型右值
2.将亡值:类、结构体类型右值(生命周期只在当前作用域的)
那右值引用怎么减少拷贝呢?
移动构造
class string
{
public://移动构造string(string&& s):str(nullptr){//移动拷贝,抢夺资源//所以右值引用即为标记//s为右值引用,可能是将亡值,会直接将s的资源抢给自己swap(s,*this);}
private:char* str;};
稍早一点的编译器,C++11之前,在实践使用中要求避免对象传值返回(为节省资源),C++11出现后彻底解决了这样的问题
当然右值引用使用的场景除了移动构造外还有移动赋值等,即用此种方法实现operator=(),C++11后STL中也用了这样的设计
另外还需注意,右值引用本身的属性是——>左值,如果还是右值无法修改
为什么呢?因为只有右值引用本身属性是左值,才能传递他的资源
即:传递过程中会退化
//r1的属性为左值
string&& r1 = string("2222");
5.3.函数模版中的引用
左值引用、右值引用在模板中的使用值得注意
template<class T> //引用折叠
void Func(T&& x) //传左值即为左值引用,传右值即为右值引用
{}
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
5.4.完美转发
完美转发要用的forward函数它包含在<utility>中。
forward - C++ Referencey
右值引用本身属性是——左值,为什么呢?
因为只有右值引用本身属性是左值,才能转移他的资源。即右值在传递过程中会退化。
而在语法层我们无法判断一个引用本质引用的左值还是右值,完美转发可以解决这个问题。
若t为左值,保持属性直接传参给Fun。
若t为右值,传入时右值引用属性会退化成左值,forward会将其转换成右值再传给Func。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)
{//模板实例化左值引用,保持属性直接传参给Fun//模板实例化右值引用,右值引用属性会退化成左值,转换成右值再传给FuncFun(forward<T>(t));
}
6.新的类功能
C++11 新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。
移动构造函数和移动赋值运算符重载:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
默认移动构造会将内置类型按字节拷贝,自定义类型实现了移动构造调移动构造,没实现调用其拷贝构造。移动赋值与移动构造类似。
需要显式实现析构说明有资源需要释放,则:
- 说明需要显式写拷贝构造和赋值重载
- 说明需要显式写移动构造和移动赋值
默认生成的移动构造对Date这样的没有申请资源的类,没什么意义,与拷贝构造功能一样,对Person这样(申请了资源的类)很有意义。Person为右值时,内部的string也是右值,string就可以走移动构造提高效率。
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。
class HashTable
{
public:HashTable() = default;
}
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class HashTable
{
public:HashTable() = delete;
}
7.可变参数模板
C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板。可变参数模板的参数类型个数可变。
下面就是一个基本可变参数的函数模板:
template <class ...Args>//Args是一个模板参数包
void ShowList(Args... args)//args是一个函数形参参数包
{//可变模板参数模板编译时解析cout<< sizeof...(args)<<endl;
}
由于语法不支持使用args[i]这样方式获取可变 参数,所以我们的用下面的方法获取参数包的值。
递归函数方式展开参数包
//解析方式1
//编译时递归推导解析参数
void Print()
{}
template<class T,class ...Args>
void Print(T&& x, Args&& ...args)
{cout << x << ' ';//参数包参数数量为零时会调用无参的重载函数Print(args...);
}
template<class ...Arg>
void showList(Args&& ...args)
{Print(args...);
}
逗号表达式展开参数包
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行 printarg(args),再得到逗号表达式的结果0。通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };//int arr[] = {(cout << (args)<<' ',0)...};cout << endl;
}
emplace系列的接口,支持模板的可变参数,并且万能引用。emplace系列可以将参数包向下传递(就可以直接调取底层元素的构造函数)。
emplace系列总体而言更加高效,推荐使用。特别是对于Date这种没有拷贝构造、移动构造概念的类很高效。
int main()
{std::list< std::pair<int, char> > mylist;// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象// 除了用法上,和push_back没什么太大的区别mylist.emplace_back(10, 'a');mylist.emplace_back(20, 'b');mylist.emplace_back(make_pair(30, 'c'));mylist.push_back(make_pair(40, 'd'));mylist.push_back({ 50, 'e' });for (auto e : mylist)cout << e.first << ":" << e.second << endl;return 0;
}
int main()
{// 带有拷贝构造和移动构造的bit::string// 其实差别也不大,emplace_back是直接构造了,push_back// 是先构造,再移动构造。std::list< std::pair<int, bit::string> > mylist;mylist.emplace_back(10, "sort");mylist.emplace_back(make_pair(20, "sort"));mylist.push_back(make_pair(30, "sort"));mylist.push_back({ 40, "sort"});return 0;
}
8.lambda表达式
格式:
捕捉列表 参数 可修改性 返回值 函数体
[capture_list] (parameters)mutable->returntype{statement}
√ 可省 可省 可省 √
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推 导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
// 最简单的lambda表达式, 该lambda表达式没有任何意义[]{}; // 省略参数列表和返回值类型,返回值类型由编译器推导为intint a = 3, b = 4;[=]{return a + 3; }; // 省略了返回值类型,无返回值类型auto fun1 = [&](int c){b = a + c; }; fun1(10)cout<<a<<" "<<b<<endl;// 各部分都很完善的lambda函数auto fun2 = [=, &b](int c)->int{return b += a+ c; }; cout<<fun2(10)<<endl;// 复制捕捉xint x = 10;auto add_x = [x](int a) mutable { x *= 2; return a + x; }; cout << add_x(10) << endl;
lambda表达式其本质为匿名函数对象
捕捉列表
int main()
{int a = 1, b = 2;//lambda只能用当前lambda局部域和捕捉的对象和全局对象//传值捕捉本质是一种拷贝,并且const修饰了auto swap2 = [a, b]()mutable//mutable慎用,mutable即——可修改的{ //mutable相当于去掉const属性,可以修改了//修改不会影响外面被捕捉的值,因为是一种拷贝int tmp = a;a = b;b = tmp;};//引用捕捉,默认可修改auto swap3 = [&a, &b](){};}
[=]//所有值的传值捕捉(本质上只会捕捉函数体中使用了的对象)
[&]//所有值引用捕捉(本质上只会捕捉函数体中使用了的对象)
[&a,b]//混合捕捉
[&,b]//除b外其他全部引用捕捉
底层:
捕捉列表的对象是成员变量存储在lambda类对象中
捕捉的本质是构造函数的初始化列表
注意:
1. 父作用域指包含lambda函数的语句块
2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
4. 在块作用域以外的lambda函数捕捉列表必须为空。
5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者 非局部变量都 会导致编译报错。 f. lambda表达式之间不能相互赋值,即使看起来类型相同
9.包装器
function包装器 function包装器 也叫作适配器。可以将1.函数指针 2.仿函数 3.lambda包装起来。
function - C++ Reference
function在头文件<functional>
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
包装器使用方法如下:
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};int main()
{// 函数名(函数指针)std::function<int(int, int)> func1 = f;cout << func1(1, 2) << endl;// 函数对象std::function<int(int, int)> func2 = Functor();cout << func2(1, 2) << endl;// lamber表达式std::function<int(int, int)> func3 = [](const int a, const int b){return a + b; };cout << func3(1, 2) << endl;// 包装静态成员函数std::function<int(int, int)> func4 = &Plus::plusi;cout << func4(1, 2) << endl;//包装普通成员函数,因为普通成员函数隐患this指针,(第一个参数传对象或指针)//因为底层无论是对象或指针都是通过对象调成员函数std::function<double(Plus*, double, double)> func5 = &Plus::plusd;cout << func5(&Plus(), 1.1, 2.2) << endl;std::function<double(Plus, double, double)> func6 = &Plus::plusd;cout << func6(Plus(), 1.1, 2.2) << endl;return 0;
}
还可以用map将对象与函数进行映射:
//用operator[]进行访问
map < string, function<int(int, int)>> opFuncMap;
bind
一般用于绑定一些固定参数,bind的返回值返回一个仿函数
using placeholders::_1;
using placeholders::_2;
int main()
{//调整参数顺序auto sub1 = bind(sub, _1, _2);//_1代表第一个实参,_2代表第二个实参以此类推auto sub2 = bind(sub, _2, _1);cout << sub2(10, 5) << endl;//调整参数个数(常用)auto sub3 = bind(sub, 100, _1);//绑定死第一个形参cout << sub3(20) << endl;}