本篇文章将介绍c++11的部分新功能
1.初始化列表
1.1使用 {} 初始化
C++98的时候,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定
例如:
#include<iostream>
using namespace std;
struct display
{
int _a;
int _b;
};
int main()
{
int arr[] = { 1,2,3 };
display d = { 2,3 };
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include<iostream>
using namespace std;
struct display
{
int _a;
int _b;
};
int main()
{
int x{ 2 };
int arr[]{ 1,2,3 };
display d{ 2,3 };
return 0;
}
1.2 std::initializer_list
详细可以查看官方文档:cplusplus.com/reference/initializer_list/initializer_list/
initializer_list的类型
initializer_list使用场景:
initalizer_list一般是作为构造函数的参数。用来初始化一些容器比如vector、list、map等。
initalizer_list也可以作为operator=的参数。
int main()
{
vector<int> arr = { 1,2,3,4 };
list<int> li = { 1,2,3,4 };
map<string, string> m = { {"hello","你好"}, {"111","222"} };
arr = { 2,3,4,5 };
return 0;
}
2. 声明
C++11提供了很多简化声明的方式,特别是在使用模版的时候
2.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局 部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将 其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。
2.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
int main()
{
int i = 2;
double j = 3.2;
decltype(i * j) ret;
cout << typeid(ret).name() << endl;
auto ret = i * j;
vector<decltype(ret)> v;
v.push_back(2.3);
return 0;
}
2.3 nullptr
在C++中 NULL被定义成0,因为0既能表示指针常量又能表示整型常量,为了更好的区分与安全问题,在C++中定义了nullptr用来表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
3. 右值引用与移动语义
3.1右值引用与左值引用
右值引用与左值引用都是给变量取别名。
3.1.1 左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
a1,p1,c1是对a,p,c这些左值的左值引用:
int main()
{
int a = 0;
int* p = &a;
const int c = 0;
int& a1 = a;
int*& p1 = p;
const int& c1 = c;
return 0;
}
3.1.2右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址。右值引用就是对右值的引用,给右值取别名。
double fun(double a)
{
return a;
}
int main()
{
double a = 2.3;
double b = 4.3;
int&& r1 = 10;
double&& r2 = a + b;
double&& r3 = fun(a);
//下面的代码会报错:表达式必须是可修改的左值
/*10 = 1;
a + b = 1;
fun(1.1) = 1;*/
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可 以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是r1引用后,可以对r1取地 址,也可以修改r1。如果不想r1被修改,可以用const int&& r1 去引用。
3.2 左值引用与右值引用的区别
左值引用:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
int a = 9;
//左值引用只能引用左值,不能引用右值
//int& b = 10;
//是const左值引用既可引用左值,也可引用右值
const int& b = 10;
const int& c = a;
return 0;
}
右值引用:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
int main()
{
int a = 9;
//右值引用无法引用左值
//int&& b = a;
//右值引用可以引用move后的左值
int&& b = move(a);
return 0;
}
3.3 右值引用适用于什么场景
对于自定义类型来说,构造函数使用右值引用可以节省不少资源。
例如:
class string
{
public:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
if (str == nullptr)
{
assert(false);
return;
}
_str = new char[_capacity+1];
strcpy(_str, str);
}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
}
string& operator=(string s)
{
swap(s);
return *this;
}
string& operator=(string&& s)
{
swap(s);
return *this;
}
}
左值引用的缺陷以及右值引用的实现:
当函数返回的是一个临时变量,这个变量出了作用域就销毁了,就不能使用左值引用返回。这个时候如果自定义类型支持移动构造就能直接把临时对象的内容移动过来,临时对象销毁了并不影响已经移走的内容。简单来说就是用临时变量的内容来构造自己。
STL中的容器都增加了移动构造和移动赋值,比如string
vector
3.4 move()函数
按照语法来说右值引用只能引用右值,但是在有些场景下可能会用到使用右值引用来引用左值来实现移动语义。
当要使用右值引用来引用左值的时候可以使用move()函数来强制把左值转换成右值。
mvoe()函数不搬运任何东西,仅仅是把左值转换成右值。
举个例子:
int main()
{
string s = "123";
string s1(move(s));
return 0;
}
像这样把s转换成右值,就会调用s1的移动构造,从而把s里面的资源给转移到s1里面,这样s里面就没有资源了。相当于s被置空了。
STL中一些容器的插入接口也支持了右值引用版本。
3.5 完美转发
在模版中&&不仅仅表示右值引用,而是表示万能引用,既能接受左值也能接受右值。
引用类型的唯一作用就是限制了接受类型,这些类型在后续的使用中都是左值。
如果希望在后续的使用中仍然保持类型的属性,我们就要是用完美转发。
下面这个例子就说明了forward在传参的过程中仍然保持了原生类型。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
4. 新的类功能
c++98的时候默认构造函数只有6个
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
在C++11中增加了,移动构造和移动赋值重载
注意:
1如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
2如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)
3如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
类成员变量初始化:
1.C++11允许在类在定义式给成员变量初始化默认值,默认生成的构造函数会使用这些值去初始化变量。
2.关键字default能强制生成默认的函数。
3.关键字delete能禁止生成默认的函数。
5.可变参数模版
C++11新特性可以创建可变参数模版来接收可变参数。
可变参数模版的定义:
Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
可以使用递归方式来展开参数包:
template<class T>
void showlist(const T& t)
{
cout << t << endl;
}
template<class T, class ...Args>
void showlist(T val, Args ...args)
{
cout<< val<<" ";
showlist(args...);
}
int main()
{
showlist(1);
showlist(1, 3, 4);
showlist(1, "S", string("rrr"));
return 0;
}
也可以使用逗号表达式来展开参数包
void outarg(const T& t)
{
cout << t << endl;
}
template<class T, class ...Args>
void showlist(T val, Args ...args)
{
vector<int> arr = {(outarg(args),0)...};
cout << endl;
}
int main()
{
showlist(7);
showlist(1, 3, 4);
showlist(1, "S", string("rrr"));
return 0;
}
STL中的的empalce相关接口函数
我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和 emplace系列接口的优势是emplace_back支持可变参数,拿到构建对象的参数后自己去创建对象。
6.lambda表达式
6.1举个例子:
在C++98中,我们要使用sort进行排序,如果排序对象是自定义类型的话就需要定义比较的规则,比如写一个仿函数传给sort()。
这样写有点复杂了,不同的规则还要去实现不同的类,而且类的命名也是个让人不爽的点。在这种情况下使用lambda表达式就能很好的解决这个问题。
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "2", 2.1, 5 }, { "1", 3, 4 }, { "3", 2.2,
3 }, { "4", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate > g2._evaluate; });
}
6.2 lambda表达式的语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
1. lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
lambda表达式就和匿名函数差不多
2. 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
7.包装器
7.1 function包装器
function包装器也叫做适配器,function包装器本质上是一个类模版。
包装器可以包装函数、函数指针、指向成员的指针或任何类型的函数对象
就像这样:
void swap_fun(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
class swap_fun2
{
public:
void operator()(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
};
int main()
{
int x = 0;
int y = 1;
cout << x << " " << y << endl;
auto f1 = [](int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
};
f1(x, y);
cout << x << " " << y << endl;
function<void(int& a, int& b)> f2 = f1;
function<void(int& a, int& b)> f3 = swap_fun;
function<void(int& a, int& b)> f4 = swap_fun2();
f2(x, y);
cout << x << " " << y << endl;
map<string, function<void(int& a, int& b)>> cmp = { {"lambda",f1},{"仿函数",swap_fun2()},{"函数指针",swap_fun}};
cmp["lambda"](x, y);
cout << x << " " << y << endl;
return 0;
}
包装器的使用场景:
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
map<string,function<int(int,int)>> cmp =
{
{"+",[](int a, int b){return a + b;}},
{"-",[](int a, int b){return a - b;}},
{"*",[](int a, int b){return a * b;}},
{"/",[](int a, int b){return a / b;}}
};
for(auto& str : tokens)
{
if(cmp.count(str))
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(cmp[str](left,right));
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
7.2 bind包装器
std::bind是一个函数模版,它就像一个函数包装器(适配器),可以接受一个可调用对象,并生成一个新的可调用对象来适配原来的参数列表。
bind原型如下:
template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
例如:
#include<iostream>
#include<functional>
using namespace std;
int sum(int x, int y)
{
return x + y;
}
int main()
{
function<int(int)> f1 = bind(sum, 3, placeholders::_1);
int ret = f1(6);
cout << ret << endl;
return 0;
}
可以对一个函数进行绑定,这样就可以控制这个函数的参数顺序以及参数个数。