目录
前言
列表初始化
{ }初始化
initializer_list类
类型推导
auto
decltype
范围for
右值引用与移动语义
左值引用和右值引用
移动语义
1.移动构造
2.移动赋值
3.stl容器相关更新
右值引用和万能引用
完美转发
关键字
default
delete
final和override
可变参数模板
介绍
使用场景
lambda表达式
包装器
bind函数
线程库
后记
前言
C++11 是 C++ 语言的一个重要更新,它加入了许多新的语言特性和标准库组件,旨在提高代码的可读性、可维护性、可移植性和安全性,同时也提高了语言的表达能力和性能。C++11 的引入,对于 C++ 程序员来说是一个里程碑式的事件,它使得 C++ 语言更加现代化和高效。因此我们要作为一个重点去学习。在把本篇文章中,主要介绍一些新增特性,比如花括号初始化、initializer_list类、auto、范围for等,其中较为重要的有右值引用、lambda表达式、线程库,内容较大的特性会专门出一片文章讲解,比如智能指针、异常相关新增特性等,下面来看看上述的详细介绍吧。
列表初始化
-
{ }初始化
C++98允许使用花括号{ }对数组或者结构体元素进行统一的列表初始值设定,C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。列表初始化也可以适用于new表达式中,自定义类型不仅可以通过构造函数使用圆括号构造,也可以使用花括号构造,举例如下代码块。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
struct A
{
int _a;
int _aa;
};
int main()
{
int a{ 0 };
int arr1[] = { 1,2,3 };
int arr2[] { 1,2,3 };
int arr3[3] = { 0 };
int arr4[3] { 0 };
A a1 = { 1,2 };
A a2 { 1,2 };
int* ptr = new int[3]{ 1,1,1 };
Date d1(1, 2, 3);
Date d2{ 1,2,3 };
return 0;
}
-
initializer_list类
initializer_list类似于数组和向量,可以存储一组数据,并且支持迭代器,可以用于函数参数、构造函数和赋值运算符的参数中。通过使用initializer_list,可以轻松地传递一组数据给一个函数或者对象,而不必显式地指定这组数据的长度或者元素类型。STL中的不少容器就增加 std::initializer_list作为参数的构造函数,比如
eg:
类型推导
-
auto
关键字auto在C++中是用于自动类型推导的关键字。当使用auto声明变量时,编译器会根据变量的初始化表达式自动推导出变量的类型。使用auto可以简化代码,特别是当变量类型较长或较复杂时。另外,auto还可以结合迭代器模板等使用,更加灵活和简洁。当auto与&结合说明这是个引用变量,当auto与*结合说明是个指针变量,举例如下:
eg:
-
decltype
decltype是一个关键字,用于获取表达式的类型,而不是用于实例化一个对象,可以用于函数返回值类型推断、模板参数类型推断等,举例:
eg:
范围for
C++中的范围for是一种遍历容器、数组、字符串等可迭代对象的简便方法,实际底层就是迭代器遍历。范围for循环通过在循环中声明一个变量,在每次迭代中自动将其设为下一个元素的值来遍历可迭代对象中的元素。
eg:
int arr[] = {1, 2, 3, 4, 5};
for (int x : arr) {
cout << x << " ";
}
// 输出: 1 2 3 4 5
右值引用与移动语义
-
左值引用和右值引用
首先,无论是左值引用还是右值引用,都是给对象取别名,要弄明白左值引用和右值引用,先了解一下左值与右值是什么意思。对于左值,可以获取它的地址+对它赋值,注意左值可以出现在赋值符号的左边,也可以出现在右边,左值引用就是对左值的引用,给左值取别名;在此之前所学的引用都是左值引用,左值引用使用一个&符号来声明,比如:
int a = 10; //左值
int* b = new int(1); //左值
const int c = 1; //左值
int& refa = a; //左值引用
int*& refb = b; //左值引用
const int& refc = c; //左值引用
对于右值,不能取地址+不能出现在赋值符号的左边,是一个表示数据的表达式,比如字面常量、表达式返回值等,右值引用就是对右值的引用,给右值取别名,比如:
int x = 0, y = 0;
1; //右值
x + y; //右值
x + 1; //右值
int&& rr1 = 1; //右值引用
int&& rr2 = x + y; //右值引用
int&& rr3 = x + 1; //右值引用
//int&& ref4 = x; //报错
左值引用与右值引用的比较:
①左值引用只能引用左值,不能引用右值,但是const左值引用既可引用左值,也可引用右值;
②右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值;
其中move函数的作用就在于将左值强制转换为右值,比如:
int a = 10;
//int& d = 10; //左值引用引用不了右值
const int& d = 10; //const左值引用可以引用右值
//int&& e = a; //右值引用引用不了左值
int&& e = move(a); //右值引用可以引用move之后的左值
-
移动语义
1.移动构造
那左值引用用的好好的,为什么要提出右值引用呢?我们想一下左值引用的短板,有这样一个情况,当函数返回值是一个局部变量,出了作用域就会被销毁,就不能使用(左值)引用返回,只能使用传值返回,但是传值返回至少会有一次拷贝构造(即使在编译器优化以后),因此为了减少拷贝,下面考虑其他方法——引入移动构造、移动赋值。
在此之前,先介绍一下右值的分类,包括纯右值(内置类型右值)和将亡值(自定义类型右值),对于纯右值,就算是拷贝多次也无所谓,但是对于有申请资源的将亡值,拷贝一次都是极大地降低了效率,所以考虑将将亡值的资源转给需要的新对象,也就是用将亡值即将不要的资源去构造给需要的对象,这可以大大的减少拷贝,提高效率。
通过下面一个具体的例子描述一下这个过程,在模拟实现string类时,有这样一个int转string的函数,如下图,左边是string的拷贝构造函数,右边是To_string函数传值返回的过程,正常编译器优化情况下,会将str的资源拷贝一份给main函数中的str,但是我们发现这个To_string函数中的str就是一个将亡值,退出函数str就会被释放,而main函数中的str正好是一个需要此资源的新对象,正如上面所说,将To_string函数中的str对象资源移动给main函数中的str,这样就是一次移动构造,如第二图。
对于移动构造函数,我们可以看到,参数是一个右值引用,函数体就是进行资源的交换或者说移动,为什么To_string函数返回之后会调用移动构造函数去构造str呢?因为这里编译器会将To_string函数的局部变量返回值识别成一个右值(将亡值),就会自动去找最匹配的构造函数去构造对象。当没有移动构造函数时,这里就会去调用拷贝构造函数,因为const左值引用也可以接收一个右值,当两个都存在时,存在构造对象的地方会去调用最匹配的构造函数。
2.移动赋值
不仅有移动构造,还有移动赋值,原理一样,这里也简单说一下,结合在一起较为容易理解。如下图,对于移动赋值函数,参数依旧是右值引用,函数体内也是在不是两个相同的对象赋值的情况以外,交换或移动将亡值的资源给目标对象,实现资源的转移以提高效率。
3.stl容器相关更新
移动构造和移动赋值不仅解决了传值返回的多次拷贝问题,而且这种资源移动的思想也应用到了stl的容器上,为相关接口增加了右值引用版本,以减少对象的拷贝,如:
eg:
同时,在原本6个默认成员函数的基础上又增加了两个默认成员函数——移动构造函数和移动赋值运算符重载。注意:
①如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造;
②如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值,
实际上,实现一个类有申请资源时,则得实现拷贝构造、析构、拷贝赋值以进行深拷贝,同时想减少拷贝,就得实现移动构造和移动赋值,但由于有了上面那三个,编译器就不会自动生成,所以还是得自己实现这两个,因此存在属性申请资源时,自己实现拷贝构造、析构、拷贝赋值、移动构造、移动赋值。
-
右值引用和万能引用
万能引用主要有两种,一种是在函数模板中使用的一种引用类型,它的语法形式为“T&&”,其中T是一个模板参数。还有一种是“auto&&”,万能引用可以接受任意类型的实参,并且保留了实参的左右值属性。值得注意的是,必须存在类型推导才是万能引用,否则是右值引用。举例如下图,特别要注意最后一个例子,其中push_back函数得参数虽然是T&&,但是在模板实例化时T的理性就已确定,不存在类型推导,而且在前面也提到过,这个是容器新增得右值引用版本接口,不是万能引用。
eg:
-
完美转发
完美转发提供了一种机制来保留函数参数的完整类型信息,并将其转发给另一个函数。传统上,在C++中,当一个函数接收一个参数并将其转发给另一个函数时,它会失去原始参数的类型信息(比如说右值引用版本的接口接收一个右值引用,但是在函数体内这个变量被当作左值去使用,那当我们需要去使用它的右值特性去调用其他相关函数时就没有办法了),此时C++完美转发保留了它的左值或右值的属性。语法如下:
template<typename T>
void func(T&& arg)
{
other_func(std::forward<T>(arg)); //完美转发
}
下面通过一个例子来展现一下完美转发的使用场景,如下代码是List类的模拟实现,仅包括尾插和插入函数,在mian函数中,尾插一个“1111”的常量字符串,毫无疑问,会匹配右值引用版本的push_back函数,其中需要复用insert函数,而且需要复用右值引用版本的insert函数,但是在push_back函数的函数体内,x已经被当作成了左值,已经失去了“1111”的右值特性,此时使用万能转发保持其属性,继续会匹配右值引用版本的insert函数,在这个函数体内,也需要去调用右值引用版本的Node节点的构造函数,也就是移动构造函数,也必须通过万能引用去操作,在Node的移动构造函数中,我们也需要将右值版本的字符串放进_data中,也是通过万能转发的方法。
从上面的例子当中可以看出,万能转发在实际开发中也是较为需要的,较为重要的。
代码:
template <class T>
struct ListNode
{
//构造函数用来创节点
ListNode(const T& x = T()) //左值版本
:_data(x)
, _prev(nullptr)
, _next(nullptr)
{
}
ListNode(T&& x) //右值版本
:_data(forward<T>(x))
, _prev(nullptr)
, _next(nullptr)
{
}
T _data;
ListNode<T>* _prev;
ListNode<T>* _next;
};
template <class T>
class List
{
typedef ListNode<T> Lnode;
public:
//...
iterator insert(iterator pos, const T& x) //左值版本
{
Lnode* newNode = new Lnode(x);
pos._node->_prev->_next = newNode;
newNode->_prev = pos._node->_prev;
newNode->_next = pos._node;
pos._node->_prev = newNode;
return iterator(newNode); //返回插入位置的迭代器
}
iterator insert(iterator pos, T&& x) //右值版本
{
Lnode* newNode = new Lnode(forward<T>(x));
pos._node->_prev->_next = newNode;
newNode->_prev = pos._node->_prev;
newNode->_next = pos._node;
pos._node->_prev = newNode;
return iterator(newNode); //返回插入位置的迭代器
}
void push_back(const T& x) //左值版本
{
insert(end(), x);
}
void push_back(T&& x) //右值版本
{
insert(end(), forward<T>(x));
}
private:
Lnode* _head;
};
int main()
{
List<string> lt;
lt.push_back("1111");
return 0;
}
关键字
-
default
关键字default用于强制生成默认函数,可以更好的控制默认函数。比如,当只有拷贝构造函数时,运行会报错没有默认构造函数,此时使用default强制自动生成即可,如下图
eg:
-
delete
关键字delete用于禁止生成默认函数,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
eg:
-
final和override
find和override关键字在之前的章节继承和多态中讲过,对于final,即可以修饰类不能被继承,也可以修饰虚函数不能被重写;对于override,放在子类中,检查子类虚函数是否重写了父类的虚函数,具体可见http://t.csdnimg.cn/5CvsAhttp://t.csdnimg.cn/5CvsA
可变参数模板
-
介绍
可变参数模板可以让我们编写接受可变数量参数类型的函数和类模板。下面是一个基本可变参数的函数模板,args前面有省略号,称为参数包,其中包含若干个模板参数,我们无法直接获取其中的每个参数,只能展开参数包的方式获取,在C++中,有两种方式展开可变参数模板的参数包:递归函数方式展开和逗号表达式方式展开。
template <typename... Args>
void printArgs(Args... args)
{}
递归函数方式展开:
递归展开是指在函数或类模板中递归调用自己,并将参数包展开为独立的参数列表。这可以通过使用递归模板函数或类模板来实现。如下代码,包括递归终止函数和普通展开函数,main函数中的ShowList调用过程为:
①1传进t,其余初步传进args参数包,继续递归调用展开函数;
②'a'传进t,其余传进args参数包,此时参数包只剩一个参数"111"了;
③调用最匹配的函数,即递归终止函数,传进t,之后递归结束,每个参数也最终获取到了。
template <class T>
void ShowList(const T& t) //递归终止函数
{
cout << t << endl;
}
template <class T, class ...Args>
void ShowList(const T& t, Args... args) //展开函数
{
cout << t << endl; //t就是参数包里的一个参数,这里进行使用即可
ShowList(args...);
}
int main()
{
ShowList(1, 'a', "111");
}
逗号表达式方式展开:
利用初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组arr,由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 获取到当前的参数,也就是说在构造int数组的过程中就将参数包展开了,因此获取到参数包中的所有参数,注意这个数组的目的纯粹是为了在数组构造的过程展开参数包,使用参数的地方是在PrintArg函数中。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1, 'A', "111");
return 0;
}
-
使用场景
如果上面的参数包、展开方式你并没有看懂,那就作为了解即可,但是使用场景必须能看得懂,可变参数模板应用在stl容器的emplace相关接口上,比如
可以看到emplace接口参数,既支持模板的可变参数,又是万能引用,也就是同时可以接受左值,也可以接受右值,下面看看如何使用这个接口:其中对于一个元素是pair类的vector,可以直接将一个pair的元素使用emplace_back插入,但是push_back的话就必须去调用make_pair函数。
int main()
{
vector<pair<string, int>> v;
v.emplace_back("1", 1);
//v.push_back("1", 1); //报错
v.push_back(make_pair("1", 1));
v.push_back({ "1", 1 });
return 0;
}
lambda表达式
lambda表达式是一种匿名函数,可以在需要函数对象的任何地方使用。lambda表达式的基本语法如下:
[capture-list] (parameters) mutable -> return-type { function-body }
其中,
捕获列表(capture-list):用于捕获外部变量,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用,每个变量可以指定为按值捕获或按引用捕获,
- [var]:表示值传递方式捕捉变量var,正常情况下可读不可写,加上mutable变成了一份拷贝,就可读可写了
- [=]:表示值传递方式捕获所有所在栈帧的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有所在栈帧的变量(包括this)
- 由多个捕捉项组成,并以逗号分割
eg:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量,
参数(parameters):用于传递参数,与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略;
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。注意使用该修饰符时,参数列表不可省略(即使参数为空);
返回类型(return-type):用于指定返回值类型,没有返回值时此部分可省略。返回值类型明确情况下也可省略,由编译器对返回类型进行推导;
函数体(function-body):用于实现函数的具体逻辑,可以使用捕获列表的变量也可以使用参数列表的变量,
如下图,fun2就是一个lambda表达式,值传递方式捕获了上文的所有变量,其中b是引用传递,传了一个参数c,返回值是int,这里不写也没事,因为编译器会自动推导,函数体内运算以后返回b,之后调用此lambda表达式,需要传一个参数,即可得到函数体内的计算结果。
注意:
①捕捉列表不允许变量重复传递,否则就会导致编译错误,比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复,就会报错;
②lambda表达式之间不能相互赋值,但可以拷贝构造一个lambda表达式,也可以赋值给相同类型的函数指针,比如
void (*PF)(); int main() { auto f1 = []{cout << "hello world" << endl; }; auto f2 = []{cout << "hello world" << endl; }; //f1 = f2; //报错 auto f3(f1); PF = f2; return 0; }
包装器
包装器,也叫适配器,是一种用于以统一的方式调用不同类型函数的抽象概念,本质是一个类模板。在引入lambda表达式之后,有没有这样一个问题,有的接口用函数实现,有的用函数对象实现,还有的用lambda表达式实现,万一有场景需要把这些不同实现方式的接口聚合在一起,该用什么来接收这些接口呢?对!就是使用包装器去接收,看看它的原型:
template <class T> function; template <class Ret, class... Args> class function<Ret(Args...)>;
其中,Ret: 被调用函数的返回类型,Args…:被调用函数的形参,使用方式我举个例子,实现计算器的加减乘除功能,注意实现以及调用的细节。
int Add(int a, int b)
{
return a + b;
}
class Sub
{
public:
int operator()(int a, int b)
{
return a-b;
}
};
class func
{
public:
int Div(int a, int b)
{
return a / b;
}
};
int main()
{
function<int(int, int)> ADD = Add; //函数名
function<int(int, int)> SUB = Sub(); //函数对象
function<int(func, int, int)> DIV = &func::Div; //非静态成员函数
function<int(int, int)> MUL = [](int a, int b) {return a * b; }; //lambda表达式
cout << ADD(1, 2) << endl;
cout << SUB(1, 2) << endl;
cout << DIV(func(), 1, 2) << endl;
cout << MUL(1, 2) << endl;
return 0;
}
运行:
bind函数
bind函数是一个非常强大的函数对象适配器,它可以把一个函数和一些参数绑定起来,形成一个新的函数对象,该函数对象可以像原函数一样调用,但是它已经部分确定了原函数的参数,同时还可以实现参数顺序调整,原型如下:
template <typename F, typename... Args> auto bind(F&& f, Args&&... args);
先看把普通函数和成员函数的一些参数绑定的例子:
int Add(int a, int b)
{
return a + b;
}
class Func
{
public:
int Mul(int a, int b)
{
return a * b;
}
};
int main()
{
//有了两数相加函数,实现任意数加7的功能
auto xPlus7 = bind(Add, placeholders::_1, 7);
cout << xPlus7(1) << endl;
//有了两数相乘的成员函数,实现任意数加倍的功能
auto increaseDouble = bind(&Func::Mul, Func(), placeholders::_1, 2);
cout << increaseDouble(8) << endl;
return 0;
}
运行:
再看调整参数的例子:
double Div(int a, int b)
{
return (double)a / b;
}
int main()
{
auto Divide1 = bind(Div, placeholders::_1, placeholders::_2);
auto Divide2 = bind(Div, placeholders::_2, placeholders::_1);
cout << Divide1(8, 2) << endl;
cout << Divide2(8, 2) << endl;
return 0;
}
运行:
线程库
学了之后再补充...
后记
从以上可以看出,c++11新增的知识点还是特别多的,本文章只是讲述了较为重要的一部分,面试时被提问频率高的一部分,还有一部分没有提到,比如新增容器(如array),空指针nullptr,有一些大家可能已经熟练于心了,对于文中讲过的知识点,其中包括范围for、右值引用、lambda表达式都是重点中的重点,希望大家能够真正的看懂并理解,拜拜!