目录
0.写在前面
想要了解C++11有哪些重大更新,先来了解C++11挫折的历史背景:
2003 年,ISO/IEC JTC1 SC22 工作组启动了 C++ 标准的下一个版本的工作,该版本被命名为 C++0x。2007 年,C++ 委员会发布了 C++0x 的第一版草案。经过多次迭代和改进,2011 年 3 月 27 日,ISO C++ 委员会正式批准了 C++ 编程语言国际标准最终草案。2011 年 8 月 10 日,C++11 最终国际投票结束,所有国家都投出了赞成票。国际标准化组织(ISO)和国际电工委员会(IEC)于 2011 年 9 月 1 日出版发布 C++11 标准,正式名称为 ISO/IEC 14882:2011。
可以说,C++11是一次计划已久诞生于拖延之中的C++新版本。
1.统一的列表初始化
1.1{}初始化
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
补充:1.如果在类的构造函数前加上explicit就代表禁止隐式类型转换,那么多参数进行{}构造就失效了;2.进行初始化的时候可以不用写=,但是作者建议加上=,增加代码的可读性
1.2 std::initializer_list
C++11新增了初始化列表,这使得vector<int> v = { 1,2,3,4,5,6 };类似的构造可以合法,什么是初始化列表,给出官方文档:
为什么上述的vector可以使用初始化列表进行初始化呢?因为vector新增了初始化列表进行构造的函数:
那么如果使用初始化列表去初始化一个数组,例如:
initializer_list<int> il = { 1,2,3,4,5,6 };
那么这段列表实际上是放在常量区的,initializer_list其实存储的是begin和end指针,在初始化的过程中实际上就是使用begin和end指针进行遍历初始化的。
有了初始化列表,就可以配合{}完成下列操作:
map<string, string> map = { {"插入","insert"},{"拔出","takeout"} };
// 这样在插入map时较为方便
2.声明
2.1 auto
C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
struct test
{
test(int first=0,int second=0)
:a(first)
,b(second)
{ }
private:
int a; int b;
};
void practice1()
{
int a{ 1 };
int b = { 2 };
auto arr = new int[10] {1, 2, 3, 4, 5, 6, 7};
auto aa = new test[2]{ test(1,1),test(2,2) };
}
2.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型,decltype适用于不方便描述变量名时使用,如果在vector中要存储函数指针该如何初始化?显然不能用auto进行实例化,那么decltype就派上了用场:
void test_cpp11_2()
{
auto ptr = malloc;
cout << typeid(ptr).name() << endl;
//单纯定义一个变量出来
auto pptr = ptr;
decltype(ptr) ppptr;
//这点如果是定义一个类那么非常有用
teststruct<decltype(ptr)> t;
//这样就完成了
}
2.3 nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
void testfunc(int* a)
{
return;
}
void test_cpp11_2()
{
//nullptr
testfunc(0);
testfunc(nullptr);
testfunc(NULL);//这三者不同,NULL宏替换成了0
}
3.范围for循环
范围for循环是一颗语法糖,支持了正向遍历,其实底层实现是通过迭代器进行实现的,范围for通过自动匹配beign迭代器开始向后遍历直到end迭代器结束:
void test_cpp11_2()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto e : arr)
{
cout << e << " ";
}
}
4.STL的变化
4.1新容器
如图所示,STL中新增了这些容器:
其中unordered_map以及unordered_set用哈希表底层实现在之前的博文已经讲过,这次介绍array以及forward_list。
array的初衷是替代数组,其优点是可以对越界访问进行断言检查,但是vector也可以做到这一点,实际使用vector较多。
array<int, 10 > array= {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//比起静态数组,增加了越界访问的检查,但可以用vector进行替换
forward_list实际上是单链表,list实际上是带有哨兵位点也就是头结点的双向循环链表,这里的forward_list只支持单向迭代器,对于插入删除来说,只支持在指定pos位置之后进行插入和删除,函数名因此为insert_after,erase_after,实际上使用list较多。
forward_list<int> 单链表;
单链表.insert_after(单链表.begin(), 1);
5.右值引用和移动语义
5.1 左值引用和右值引用
double x = 1.1, y = 2.2;
int a = 0;
int& b = a;//这里变量是常见的左值
int&& right = 10;
int&& count = x + y;//临时变量,常量都是常见的右值
//可见,这里使用类型+&&的方式来接收右值
5.2 左值引用与右值引用比较
- 可寻址性 左值:具有固定的内存地址,可以通过取地址运算符& 获取其地址。例如,变量是典型的左值,因为它们在内存中有固定的存储位置。右值:通常没有固定的内存地址,不能对其使用取地址运算符& 。例如,字符常量、临时对象等都是右值,它们在表达式求值过程中可能存在于寄存器或临时存储区域,没有可获取的固定地址。
- 出现在赋值运算符的位置 左值:可以出现在赋值运算符的左边,作为赋值的目标。因为左值代表一个可以被修改的对象,所以能够接受赋值操作。 右值:一般出现在赋值运算符的右边,作为赋值的值来源。虽然有些右值也可以出现在赋值表达式中,但它们不能作为赋值的目标。
- 表达式的类型和性质 左值:包括变量、数组元素、函数返回值为左值引用的情况等。例如,函数返回一个左值引用,那么这个返回值就是左值,可以被继续赋值或取地址。 右值:除了前面提到的字面常量和临时对象外,还包括一些表达式的结果,如算术表达式的结果(除非该表达式的结果是一个变量的引用)、函数返回值为非引用类型的情况等。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a;
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
5.3 左值引用与右值引用使用场景和意义
左值引用的使用场景:
1.引用返回 2.做参数 3.减少拷贝,提高效率
void func1(string s)
{}
void func2(const string& s)
{}
int main()
{
string s1("hello world");
// func1和func2的调用左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
但是,不可能所有传值返回都可以用引用返回代替,总有需要传值拷贝返回的情况发生:而传值返回一般都会进行优化->
左值引用的短板:
右值引用和移动语义解决上述问题:

// 简单实现移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
swap(s);
}

// 移动赋值
string& operator=(string&& s)
{
swap(s);
return *this;
}
实际中,如果是左值想要调用移动构造,可以使用move()函数,上面好像已经介绍过,左值经过move后可以被右值类型接收,这样可以将一个左值强制转换为右值,从而去调用移动赋值,节省空间。
5.4 完美转发
在介绍完美转发之前,先来看这段代码的结果是什么?
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(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;
}
嘶,为什么是这样的结果? 不管是左值还是右值,为什么将t再次传给Func函数后全部变成了左值的属性?这里要介绍两点:万能引用以及完美转发,可以看到PerfectForward这个类使用了模板T&&来接受变量,其实这就是万能引用:
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,但只是提供了能够接收同时接收左值引用和右值引用的能力,举个例子,如果参数类型为int&,那么此时接收类型为int&&发生引用折叠后的int&,没错这里又有一个新东西引用折叠,引用折叠只会在万能引用接收左值时发生,总之只需知道万能引用可以接收左右值即可。
但是这里的万能引用接收参数后再次传参会在后续使用中都退化成左值,为什么会退化为左值?让我们在string的移动构造和移动赋值中去寻找答案,在string&&接收对象后必须退化为左值,才能够对参数进行修改,从而完成swap操作,因为右值是不能进行取地址操作的,也就不能修改右值,但是左值可以,这便是退化的原因,如何解决?使用完美转发:
我们在t传参前加上forward<T>就可以保持t原有的左右值属性,实现完美转发,解决退化问题:
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(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;
}
补充:思考能否用移动构造代替拷贝构造?
不行!请注意辨明万能引用与右值引用的区别,string为例,类中的T进行的是不同类型的实例化,string&&的类型是固定死的,那么这里只能为右值引用,如果想要实现万能引用,应该再提供一个模板,让模板自动根据接收参数的类型进行推导,而不是进行实例化操作,才是万能引用!
6.新的类功能
6.1 默认成员函数
这里重点介绍C++11 新增的两个:移动构造函数和移动赋值运算符重载,这些函数默认生成需要注意的规则如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
这些都是默认移动构造函数的生成规则,至于具体实现前面已经结合右值引用讲解~
6.2 类成员变量初始化
在C++11中新增了初始化类成员变量的方式,在C++98中给予成员变量缺省值可以在构造函数中给出,而C++11支持在声明时给出。
class A
{
public:
//c++98中我们习惯这样给出缺省值
A(int _a=1)
:a(_a)
{
cout << "class A has been created" << endl;
}
~A()
{
cout << "class A has been deleted" << endl;
}
private:
int a;
//实际上C++11支持了在这里声明时给出
int a=1;//这样是支持的
};
6.3 强制/禁止生成默认函数
这里新增了default关键字,以及delete的新用法:如果想要类强制生成默认函数,则在函数名后加上=default,如果想要类强制禁用默认生成的函数,则在函数名后加上=delete。这在反拷贝技术中经常用到!
class defaultcopy
{
public:
defaultcopy()
{
_a = new char[10];
}
defaultcopy(const defaultcopy& dc) = default;
defaultcopy(defaultcopy&& dc) = default;
//~defaultcopy() = delete;//如果不想生成默认函数,可以直接加上delete,不必在98中定义在private中
private:
char* _a;
};
7.可变参数模板
C++11的新特性可变参数模板能够可以接受可变参数的函数模板和类模板,C++98/03,类模版和函数模版中只能含固定数量的模版参数,下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,可以运用递归展开以及数组展开两种方式:
//那么如何进行访问呢?运用递归
template<class T>
int showlist(T t) ///一号
{
cout << t << endl;
return 0;
}
template<class T, class...Args>
void showlist(T t,Args...args) ///二号
{
cout << t << " ";
showlist(args...);
}
上述代码,将参数传给showlist就可以进行打印,原理是什么?如果只有一个参数,会直接匹配一号函数,但如果有两个及以上n个参数,会传参给二号,将其中一个参数传给t进行打印,剩余n-1个参数会传给args参数包进行接收,一直递归下去,直到只有一个参数传给一号,递归结束。
template<class ...Args>
void cppprint(Args...args)
{
int a[] = { showlist(args)... };//这里就代表展开,按照参数包的数据个数来开辟数组空间
}
也可以使用展开的方式进行打印,这里args则代表参数包中每一个参数依次展开。
介绍可变参数模板的目的也是为了更好介绍emplace系列:
可以看到STL容器中新增的emplace系列就使用了可变参数模板, 设计者设计emplace系列的目的是为了提高构造效率:
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;
}
在进行插入时使用emplace系列可以直接传参构造,无需进行makepair进行额外的拷贝构造,但是由于makepair构造成为临时对象,属于右值中的将亡值,所以会进行移动构造,相比之下,两者效率差距并不明显。
8.lambda表达式
lambda表达式是什么? 这是C++11中新增可以代替仿函数的良药:在c++98中如果我们想控制sort的比较如何实现,当然传参一个写好的仿函数即可,但是在项目中仿函数太多不好寻找就会造成极大的不便,亦或者如果只是想要简单的比较去完成一个仿函数的代价又过于太大,于是便诞生了lambda表达式:
下面演示一下lambda的强大功能:如果要进行的是日期类的比较,先给出日期类:
class Date
{
public:
Date(const int& year=2025,const int& month=4,const int& day=5)
:_year(year)
,_month(month)
,_day(day)
{ }
public:
int _year;
int _month;
int _day;
};
按照传统C++98的方式,这样比较是正确的:
struct compare
{
bool operator()(const Date& d1, const Date& d2)
{
if (d1._year < d2._year) return true;
else if (d1._year == d2._year)
{
if (d1._month < d2._month) return true;
else if (d1._month == d2._month)
{
if (d1._day < d2._day) return true; else return false;
}
else return false;
}
else return false;
}
};//这里使用了仿函数,进行比较
int main()
{
vector<Date> v = { {2006,8,21},{2006,12,28},{2007,7,21} };
sort(v.begin(), v.end(),compare() );
for (const auto& e : v)
{
cout << e._year << '-' << e._month << '-' << e._day << endl;
}
return 0;
}
现在,c++11的lamb表达式可以直接省去struct仿函数:
int main()
{
vector<Date> v = { {2006,8,21},{2006,12,28},{2007,7,21} };
sort(v.begin(), v.end(), [](const Date& d1, const Date& d2)->bool
{if (d1._year < d2._year) return true;
else if (d1._year == d2._year)
{
if (d1._month < d2._month) return true;
else if (d1._month == d2._month)
{
if (d1._day < d2._day) return true; else return false;
}
else return false;
}
else return false; });
for (const auto& e : v)
{
cout << e._year << '-' << e._month << '-' << e._day << endl;
}
return 0;
}
可以看到,原本传参仿函数的地方放进去的是lambda表达式,一个完整的lambda表达式是这样构成的:
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type {statement }
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,后面会进一步说明
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
//补充lameda函数:
double a = 1.0, b = 2.0, c = 3.0;
auto add = [](int a,int b) {return a + b; };
cout << add(a, b) << endl;
//auto add1 = [](int a, int b) {return add(a, b); };//可以这么调用函数吗?不行,但是可以在结构内调用全局函数
auto add1 = [](int a, int b) {return func(a); };//这里的func是定义的全局函数
//请注意,参数列表和返回值类型都是可以省略的部分,返回值可以自动推导
//如果想要使用其他变量,可以捕捉
auto add2 = [c](int a, int b)mutable { c = 2.0; return a + b + c; };//那么可以对c进行修改吗?不可以,捕捉的值默认使用const修饰
//如果想要进行修改可以加上mutable,但是,加上muable也仅仅是对拷贝过来的c进行修改,并不能影响到实参
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
//mutable一般没有人用,使用&拷贝更多,而且可以改变实参
auto add3 = [&c](int a, int b) {c = 2.0; return a + b + c; };
int A = 1, B = 2,C = 3, D = 4;
//如果想要全部捕捉,就使用=,引用就使用&
auto add4 = [=]() {return A+B+C+D; };
auto add5 = [&]() {A=B=C=D = 0; return A+B+C+D; };
//需要注意的是add4和add5之间不能互相赋值,本质上这两个都是类名
f. lambda表达式之间不能相互赋值,即使看起来类型相同,但是真的相同吗?

其中 lambda_后的数字是UUID,UUID是一个128比特的数值,这个数值可以通过一定的算法计算出来。为了提高效率,常用的UUID可缩短至16位。UUID用来识别属性类型,在所有空间和时间上被视为唯一的标识。也就是说每个lambda表达式被打上了独一无二的标签,各不相同。其实lambda表达式的底层就是通过仿函数封装进行实现的。
9.包装器
如果想要将函数指针,仿函数,lambda表达式,这三个毫不相关的类型放进一个数组中可能实现吗?有了function就可以!C++11新增function包装器,使用时需要包含functional头文件,那么包装器是用来干什么的?顾名思义,包装器可以用来包装一些东西,其中包括:函数指针,仿函数,lambda表达式,这些都可以用包装器这一个类型来接收。那么将它们包装起来放进一个用包装器实例化的数组就成为了现实。
class funcc
{
public:
int operator()(int a, int b)
{
return a + b;
}
int Func(int a, int b)
{
return a + b;
}
};
int Func(int a, int b)
{
return a + b;
}
int main()
{
//包装器
//如果想把仿函数,lameda,函数指针放进数组该怎么办,这时需要使用包装器
//好处在于把则这三个包装成同一类型的东西方便放入数组
function<int(int,int)> f1 = [](int a, int b) {return a + b; };
function<int(int,int)> f2 = funcc();
function<int(int,int)> f3 = Func;
//包装器中间是返回值加参数包
vector < function<int(int, int)>> v = { f1,f2,f3 };
//这样仿函数,函数指针以及lameda可以放进vector
return 0;
}
这里请注意包装器的书写规则:
function<函数返回值(需要传参的参数1,需要传参的参数2...)> 变量名
什么叫需要传参的参数?好抽象啊,参数难道还可以不需要传吗?不不不,这里说的不是缺省参数,介绍:绑定bind!

使用时,第一参数传参函数指针,仿函数,lambda表达式都是可以的!后面的参数包搭配placeholders使用,这又是个什么玩意?
上述是placeholders在库中的定义,可见placehoders中存储的是传参参数,并且可以用::访问,那么结合bind绑定就可以发生下面的场景:
int main()
{
//bind绑定:
function<double(char, int)> func1 = bind(Funcc, placeholders::_1, placeholders::_2, 3.1415);
function<double(char, int)> func2 = bind(Funcc, placeholders::_2, placeholders::_1, 3.1415);
function<double( int, double)> func3 = bind(Funcc, 3.1415, placeholders::_1, placeholders::_2);
function<double( int, double)> func4 = bind(Funcc, 3.1415, placeholders::_2, placeholders::_1);
function<double(char, double)> func5 = bind(Funcc, placeholders::_1, 3.1415, placeholders::_2);
function<double(char, double)> func6 = bind(Funcc, placeholders::_2, 3.1415, placeholders::_1);
cout << func1(1, 2) << endl;
cout << func2(1, 2) << endl;
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
cout << func5(1, 2) << endl;
cout << func6(1, 2) << endl;
//注意placeholders中的_1_2_3代表参数的第几个,部分值在bind的省略,可以实现绑定,但是注意function中的格式
//括号前代表返回值,括号中代表传参的类型,请注意传参的个数一定要符合()中的参数个数
//这里的绑定相较于缺省参数来说:缺省值是死的,这里的值可以多变
return 0;
}
用包装器实现了绑定,那么就可以直接用包装器对象进行传参,并且绑定在这里可以绑定一些参数的值,使得只需要传参控制其他部分即可。
class funcc
{
public:
int operator()(int a, int b)
{
return a + b;
}
int Func(int a, int b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> func7 = bind(&funcc::Func, funcc(), placeholders::_1, placeholders::_2);
//请注意,上面func7,第一个参数需要函数地址,其实第一个参数也可以传参仿函数lambda表达式或者函数指针,bothok
//第二个参数需要隐藏的this指针,但是这里属于临时对象右值不能够取地址
return 0;
}
bind中第一个和第二个参数还可以是类中的函数以及不能忘记传参的隐藏this指针,至于这里的funcc匿名对象为什么不取地址,是因为充当临时对象,右值无法取地址,还请多加注意!