一、C++ 11的简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于
C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中
约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。
C++11新增加的语法特性很多,这里只记录和演示一些重要且常用的语法。如果想学习或者了解更多,就去这个网站,去查看。网址:https://en.cppreference.com/w/cpp/11
二、统一的列表初始化
1、{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
cpp
#include<iostream>
using namespace std;
struct Point
{
int _x;
int _y;
};
int main()
{
int array[] = { 1,2,3,4,5 };
Point p = { 1,2 };
Point ps[] = { {1,2},{3,4} };
return 0;
}
C++11扩大了花括号{}的使用范围。所有的内置类型和自定义类型都可以使用{}进行初始值的设定。(=可加可不加,但是个人感觉加上更好,至少感觉更好看)
cpp
#include<iostream>
using namespace std;
struct Point
{
int _x;
int _y;
};
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;
};
int main()
{
int array[] = { 1,2,3,4,5 };
Point p = { 1,2 };
Point ps[] = { {1,2},{3,4} };
int a{ 4 };
int b = { 5 };
Date d1{ 2025,3,20 };//这种方式还是直接调用的构造函数,进行初始化
Date d2 = { 2025,3,20 };//这种方式同样也是
int* pa = new int[4]{ 9 };
for (size_t i = 0; i < 4; i++)
{
cout << pa[i] << " "; //结果 9 0 0 0
}
cout << endl;
int* pb = new int[4]{ 1,2,3,4 };
for (size_t i = 0; i < 4; i++)
{
cout << pb[i] << " "; //结果 1 2 3 4
}
cout << endl;
return 0;
}
注意:用花括号{}对new表达式初始化时不能加=
2、initializer_list容器

这个容器相对于其它容器来说提供的成员函数非常少。只支持迭代器和求容器中元素的个数。
所以这个容器的主要作用不和vector、list这种容器的作用相同的。这个容器的主要作用是为了实现使用花括号{}不定长的去给容器设置初始值。
①initializer_list是一种容器
cpp
#include<iostream>
using namespace std;
int main()
{
auto i = { 1,2,3,4 };
cout << typeid(i).name() << endl;
}//结果是:class std::initializer_list<int>
②initializer_list的使用场景
1°initializer_list 一般是用做容器构造函数中的参数,这样可以使用花括号{}不定长初始化容器。
2°initializer_list 还可以用作容器赋值运算符重载的参数,这样可以使用花括号{}对容器重新赋值。
演示这两种使用场景:
cpp
#include<iostream>
#include<vector>
#include<list>
#include<set>
#include<map>
#include<string>
using namespace std;
int main()
{
vector<int> v = { 1,2,3,4,5 };
list<int> ls = { 3,4,5,6 };
set<int> s = { 3,2,4,6 };
map<string, int> m = { {"AAA",1},{"BBB",2},{"CCC",3} };
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v = { 9,8,7,6 };
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
ls = { 1,2,3,4,5 };
s = { 9,2,5,7 };
m = { {"DDD",4},{"EEE",5},{"FFF",6} };
return 0;
}
这两种使用场景是怎么完成的呢?
场景1:以vector容器为例

我们可以看到,vector容器提供了包含initializer_list 容器的构造函数,所以我们可以传递一个initializer_list对象来构造vector对象。构造过程如下:
当我们使用花括号{}来构建initializer_list对象时,编译器会开辟一个临时数组,存放花括号中的数据,并且设置两个指针,一个指针指向数组的开始位置,一个指针指向数组的结束位置的下一个位置。然后将两个指针封装起来,就构成了initializer_list对象。
使用initializer_list对象来构造vector对象时,vector的构造函数会根据initializer_list对象中的两个指针遍历数组,将数组中的每个元素,挨个放入vector容器中。
场景二:还是以vector容器为例

我们看到赋值重载函数中也有一个以initializer_list对象为参数的构造函数,那就是说我们也可以使用花括号{}构建的initializer_list对象来对vector容器进行重新赋值。原理与场景一解释的一样,这里不多赘述。
③我们来实现一下vector容器中的initializer_list 拷贝和initializer_list赋值重载
cpp
namespace zrf
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector(initializer_list<T> lt)
{
_start = new T[lt.size()];
_finish = _start + lt.size();
_endOfStorage = _start + lt.size();
//采用范围for
/*iterator it = _start;
for (auto e : lt)
{
*it = e;
it++;
}*/
//采用迭代器
iterator it = _start;
typename initializer_list<T>::iterator itl = lt.begin();
while (itl != lt.end())
{
*it = *itl;
++it;
++itl;
}
}
vector<T>& operator=(initializer_list<T> lt)
{
vector<T> tmp(lt);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endOfStorage, tmp._endOfStorage);
return *this;
}
void Print()
{
iterator it = _start;
while (it != _finish)
{
cout << *it << " ";
it++;
}
cout << endl;
}
private:
iterator _start;
iterator _finish;
iterator _endOfStorage;
};
}
int main()
{
zrf::vector<int> v = { 1,2,3,4,5 };
v.Print();
v = { 6,7,8,9,4 };
v.Print();
return 0;
}
注意:这段代码有一个需要注意的地方,有一个有意思的地方。
需要注意的地方是:我们在使用迭代器拷贝数据的时候,我们initializer_list对象的迭代器前面要加关键字typename声明一下这是initializer_list对象中的类型,而不是静态变量。因为我们在使用其他类中类型或者静态变量的时候都需要在前面加上类型和域作用限定符。
有意思的地方是:我们将上面代码的赋值运算符重载屏蔽掉,也可以打印出重新赋值后的结果。原因是initializer_list对象构造出了一个临时的vector对象,然后通过编译器生成的赋值运算符重载,对临时的vector对象造成了浅拷贝。结果正确是因为,我们没写析构函数,临时的vector对象开辟的空间没有被析构,而我们的对象v也控制着这块空间。但是我们原本的那块空间造成了内存泄漏。如果我们写上析构函数,那临时的vector对象在对象v拷贝完成之后就释放了,而我们的对象v通过浅拷贝,实际上控制的是一块已经被释放的空间。而原来的空间也同样造成了内存泄漏。
如果我们不屏蔽赋值运算符重载,那如果不写析构函数,也同样会造成内存泄漏。所以,析构函数是必须要写上的。写上析构函数的代码,我在下面又写了一份。
cpp
#include<iostream>
using namespace std;
namespace zrf
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector(initializer_list<T> lt)
{
_start = new T[lt.size()];
_finish = _start + lt.size();
_endOfStorage = _start + lt.size();
//采用范围for
/*iterator it = _start;
for (auto e : lt)
{
*it = e;
it++;
}*/
//采用迭代器
iterator it = _start;
typename initializer_list<T>::iterator itl = lt.begin();
while (itl != lt.end())
{
*it = *itl;
++it;
++itl;
}
}
vector<T>& operator=(initializer_list<T> lt)
{
vector<T> tmp(lt);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endOfStorage, tmp._endOfStorage);
return *this;
}
~vector()
{
delete[] _start;
_start = _finish = _endOfStorage = nullptr;
}
void Print()
{
iterator it = _start;
while (it != _finish)
{
cout << *it << " ";
it++;
}
cout << endl;
}
private:
iterator _start;
iterator _finish;
iterator _endOfStorage;
};
}
int main()
{
zrf::vector<int> v = { 1,2,3,4,5 };
v.Print();
v = { 6,7,8,9,4 };
v.Print();
return 0;
}
三、声明
1、auto
C++11中废弃auto原来的用法,将其用于自动推断类型。根据初始化值的类型,推断出变量的类型。
cpp
#include<iostream>
#include<map>
using namespace std;
int main()
{
auto i = 10;
auto pi = &i;
map<string,int> m = { {"string",3},{"map",3} };
auto it = m.begin();
cout << typeid(i).name() << endl; //int
cout << typeid(pi).name() << endl; //int*
cout << typeid(it).name() << endl; //class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,int> > > >
return 0;
}
2、decltype
将变量的类型声明为表达式指定的类型,简而言之,表达式的结果是什么类型,那么decltype会自动获得这个表达式的类型,然后进行声明。
cpp
#include<iostream>
using namespace std;
template<typename T1,typename T2>
void F(T1 x, T2 y)
{
decltype(x * y) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 3;
double y = 2.2;
decltype(x * y) ret;
decltype(&x) p;
cout << typeid(ret).name() << endl; //double
cout << typeid(p).name() << endl; //const int*
F(1, 'a'); //int
return 0;
}
注意:通过typeid(变量名).name()获得一个变量的类型名,但是无法用获取到的这个变量名去定义变量。
decltype出了能推导表达式的类型,还能推导函数本身的类型和函数返回值的类型。
cpp
#include<iostream>
using namespace std;
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
//没有带参数,就推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
//带参数,就会推导返回值类型。注意:这里只是推导,不是执行
cout << typeid(decltype(GetMemory(0))).name() << endl;
return 0;
}
decltype不仅可以指定表达式得到的类型,还可以指定返回类型
cpp
#include<iostream>
using namespace std;
template<class T1,class T2>
auto Add(T1 t1, T2 t2) ->decltype(t1 + t2)
{
decltype(t1 + t2) ret;
ret = t1 + t2;
cout << typeid(ret).name() << endl;
return ret;
}
int main()
{
Add(1, 2); //int
Add(1, 3.4); //double
return 0;
}
去掉尾置返回类型可能也可以运行正确,但是存在一定的风险。在C++11中,要decltype(t1+t2)要知道T1和T2的类型,但是如果模版未实例化,那么就无法获得这个类型,那么就会编译报错。在C++14中,存在的风险有两个,一个是decltype(t1+t2)是ret的声明类型,而auto推导的是ret的值的类型,如果在复杂的场景中两者不一样,肯能会出现问题。另一个是,如果函数内有多个return语句,那么可能会导致返回类型推导不一致,从而导致编译错误。
3、nullptr
在C++中NULL被定义为字面量0,所以可能会造成一些问题。因为0既能表示指针常量,又能表示整形常量。所以处于清晰和安全的角度,C++11中新增了nullptr表示空指针。
在C++中使用NULL可能会出现错误,如下代码就是一个示例:
cpp
#include<iostream>
using namespace std;
void func(int arg)
{
cout << "void func(int arg)" << endl;
}
void func(int* arg)
{
cout << "void func(int* arg)" << endl;
}
int main()
{
func(NULL); //void func(int arg)
func(nullptr); //void func(int* arg)
return 0;
}
四、范围for
1、在过去我们使用C语言和C++98的时候我们对一个数组进行遍历的方式。
就是如下这样:
cpp
#include<iostream>
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5 };
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << " ";
}
cout << endl;
return 0;
}
我们对vector、list这种容器进行遍历是采用迭代器的方式,进行遍历的。2、咋i
2、我们在C++11中对数组和容器进行遍历,除了上述方法外,多了一种范围for
cpp
#include<iostream>
using namespace std;
int main()
{
int array[] = { 2,3,4,5,6,9,1,8 };
for (auto& e : array)
{
e *= 2;
}
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
return 0;
}
对范围for进行一点解释,auto是为了让这个循环可以对任意类型的元素进行遍历,如果是对上述代码中的数组进行遍历,完全可以换成int。引用符号,是为了可以在遍历的时候对元素进行修改,如果不加引用符号,那么遍历的时候的数据就是原数据的临时拷贝,改变遍历时的数据并不能改变原数据。e是遍历的数组或容器中元素的名称。:这个是语法规则。array是遍历的数组的名字,遍历哪个数组或者容器,就是哪个数组或者容器的名字。
3、要使用范围for要满足的两个条件
①for循环的范围必须是确定的。数组的范围就是第一个元素到最后一个元素。容器的范围就是begin()到end()的范围。
②使用范围for的对象必须重载了++和==。因为范围for的本质就是迭代器,在编译阶段,编译器会将范围for转换为迭代器。而使用迭代器遍历,是需要对迭代器进行++并且使用==判断结束的。
五、STL中的一些变化
C++11中STL库新增了四个容器,分别是array、forward_list、unordered_map和unordered_set
1、array容器
array容器有两个模版参数,第一个模板参数代表的是存储的类型,第二个模板参数是一个非类型模板参数,代表的是数组中可存储元素的个数。如:
cpp
#include<iostream>
#include<array>
using namespace std;
int main()
{
array<int, 10> a1;
array<double, 20> a2;
return 0;
}
array容器与普通数组对比
①array容器与普通数组一样,支持通过[ ]访问指定下标的元素,也支持使用范围for遍历数组元素,并且创建后数组的大小也不可改变。
②array容器与普通数组的不同之处在于,array容器用一个类对数组进行了封装,并且在访问array容器时,会进行越界检查。用[ ]访问元素时采用断言检查,调用at成员函数访问元素时会抛异常。而对于普通数组来说,越界写操作时会报错,而越界读操作时一般不会报错。
☆array容器的对象是定义在栈上的,因此array容器不适合定义太大的容器。
2、forward_list容器
forward_list容器本质就是一个单链表
forward_list很少使用,因为:
①forward_list容器只支持头插头删,不支持尾插尾删。要尾插尾删的话就得遍历找尾,这样的时间复杂度是O(N)
②forward_list容器提供的插入函数叫做insert_after。这个函数是在当前位置的后面进行插入。其它容器的插入是在当前元素的前面进行插入,但是,要在当前元素的前面进行插入就要遍历找前一个元素,这个时间复杂度是O(N)
③forward_list容器提供的删除函数叫做erase_after。这个函数是删除当前位置的后一个元素。其它容器的删除是删除当前位置的元素,但是删除当前位置的元素需要找到前一个元素,这就需要遍历找前一个元素,这样时间复杂度也是O(N)
3、unordered_map和unordered_set容器
unordered_map和unordered_set容器底层都是采用哈希表实现的。
详细介绍在其他章节。
六、字符串转换函数
C++11提供了各种内置类型与string之间相互转换的函数,比如to_string,stoi,stol,stod等函数
1、将内置类型转换成string类型

2、将string类型转换成内置类型

七、容器中的新方法
C++11中为每个容器都新增加了一些方法:
①提供了一个以initializer_list作为参数的构造函数,用于支持列表初始化。
②提供了cbegin和cend方法,用于返回const迭代器。
③提供了emplace系列方法,并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数,用于提高向容器中插入元素的效率