C++11新特性解析与应用
目录
一、C++11简介
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习
由于c++11的新特性比较多,所以小编部分讲解一下c++11标准中比较常用的一些语法
二、统一的列表初始化
2.1{}初始化
- C++98
98允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定
struct Point
{
int _x;
int _y;
};
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
- C++11
C++11想统⼀数据的初始化⽅式,想实现⼀切对象皆可⽤{}初始化 ,所以ta扩大了用大括号{}括起的列表(也叫做列表初始化)的使用范围,使其可用于所有的内置类型和用户自定义的类型(本质是类型的转换,中间会产⽣临时对象,最后编译器会优化,变成直接构造),使用初始化列表时,可添加等号(=),也可不添加(小编建议添加)
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()
{
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d1(2025, 10, 1); // 调用d1的构造函数
Date d2{ 2025, 10, 2 };//调用d2的构造函数
Date d3 = { 2025, 10, 3 };//调用d3的构造函数
//用引⽤,引用的是{ 2025, 10, 1 }构造的临时对象,所以必须加const
const Date& d4={2025,10 1};
vector<Date> ve;
ve.push_back({2025,10,1});
return 0;
}
Date d1 = { 2025, 10, 11 };
vector<int> ve = { 1,2,3,4,5 };
这两种初始化的原理是不一样的,d1是调用了拷贝构造 ,而ve则是用了initializer_list对vector的初始化
2.2initializer_list
- initializer_list是模板类,用于访问{}列表中的值,该{}列表是由类型为const T的元素组成的列表
int main()
{
auto arr = { 1, 2, 3, 4, 5 };
cout << typeid(arr).name() << endl;
cout << sizeof(arr) << endl;
return 0;
}

- initializer_list的底层
template<class T>
class initializer_list
{
private:
const T* _start;
const T* _finish;
};
- 在c++11中使用花括号括起来的列表的类型会被编译器识别为initializer_list类型
Date d1 = { 2025, 10, 11 };
vector<int> ve = { 1,2,3,4,5 };
在这里
- nitializer_list的私有成员变量只有两个const T*的指针,分别指向{}花括号括起来的列表的开头和结尾的下一个位置
- 编译器会将花括号括起来的列表的类型进行特殊处理为initializer_list
- 分别将initializer_list中两个指针分别指向{}列表的开头的地址和结尾的下一个位置的地址
- 这样支持了迭代器的begin和end,并且两个指针相减 ,就是size() :元素的个数

然后initializer_list再去初始化ve ,vector中构造函数:
template<class T>
vector
{
public:
vector(initializer_list<T> lt)
{
reserve(lt.size());//initializer_list支持了size(),可以获取initializer_list的元素个数
for(auto e : lt)//initializer_list支持迭代器,所以可以使用范围for
{
push_back(e);//调用尾插将列表中的元素逐个插入即可
}
}
};
因为vector中,或者说不少容器中都增加了用initializer_list的构造函数,如 initializer_list构造map对象:
int main()
{
//map<string, string> dict = { "sort","排序","left","左边" };
//{{"sort","排序"},{"left","左边"}}被识别成了initializer_list<pair>
map<string, string> dict = { {"sort","排序"},{"left","左边"} };//这里调了pair的拷贝构造
return 0;
}
三、decltype
int main()
{
int i = 10;
auto p = &i;//auto自动推导类型,但不能定义变量
//auto x;
cout << typeid(p).name() <<endl;//获取变量的类型
cout << typeid(i).name() << endl;//只能看,不能用
///typeid(i).name() ai;
decltype(i) p1;//可以自己推导类型,并且不初始化 ,
//这不和auto p2 = i;一个意思吗, 不是的 ,p1想当与 int p1 ,p1没有值,auto p2 = i ,p2是int p2=10 ;
//decltype(i) p1;可以单纯定义一个变量
//auto只能用来自动推导类型定义变量
//typeid(i).name() 只能用来推导类型 ,不能定义变量
//decltype(i)既可以自动推导类型定义变量,还可以只用来推导类型 或作模板实参: vector<decltype(i)> ve;
//还可以推导一个表达式 : vector<decltype(x*y)> ve;
return 0;
}
- autoauto只能用来自动推导类型定义变量,但不能定义变量
- typeid(i).name() 只能用来推导类型 ,不能定义变量如: typeid(i).name() ai;
- decltype(i) p1;可以单纯定义一个变量 ,而不初始化如:decltype(i) p1 ,既可以自动推导类型定义变量,还可以只用来推导类型 或作模板实参: vector<decltype(i)> ve; 还可以推导一个表达式 : vector<decltype(x*y)> ve;
四、nullptr
//nullptr
int main()
{
//由于NULL不够”专一“ ,ta既可以表示空指针 又可以表示0
//而这里的nullptr只用来表示空指针
int* p = NULL;//被替换成了 int* p=0,0可以用来赋值指针 ,但0依旧是整型
return 0;
}
- 由于NULL不够”专一“ ,ta既可以表示空指针 又可以表示0 ,当NULL用作空指针时,直接就替换成了0
- nullptr只用来表示空指针
五、array
//array (ps: 鸡肋)
int main()
{
//array为了弥补c语言的数组中对越界的不检查, array[15]在这里调用operator[] 中会强制检查
array<int, 10> a1;
int a2[10];
a1[15] = 10;
a2[15] = 10;//array[15]在这里调用operator[] 中会强制检查数字小于size()
return 0;
}
- 比上不足,比下有余
六、左值和右值
int main()
{
//左值和右值的最大区别 :左值可以取地址,右值不能被取地址
//左值一个表示数据的表达式和变量名或解引用的指针
//右值也是一个表示数据的表达式,如字面常量,表达式返回值 ,函数返回值
//右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,左值可以出现在赋值符号的右边,也可以出现在左边
常见左值
int* p = new int(0);
int b = 1;
const int c = 2;
double x = 1.1, y = 2.2;
常见右值
10;字面常量
x + y;表达式返回值
func(x, y);函数返回值
"xxxxx"是左值,表达式本身是首元素地址
return 0;
}
- 左值和右值的最大区别 :左值可以取地址,右值不能被取地址
- 左值一个表示数据的表达式和变量名或解引用的指针
- 右值也是一个表示数据的表达式,如字面常量(10),表达式返回值 (x+y),函数返回值func()
- 右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,左值可以出现在赋值符号的右边,也可以出现在左边
七、左值引用和右值引用
//左值引用和右值引用
int main()
{
//左值引用 : 给左值取别名
//右值引用 :给右值取别名
//左值引用
int a = 0;
int& r1 = a;
//右值引用
int&& r5 = 10;
double x = 1.1, y = 2.2;
double&& r6 = x + y;//x+y的临时变量是右值
//左值引用不能直接给右值取别名 : int& z=10;(X)
//要加const修饰 : const int& z=10 ,这里也可以看到实参传给函数的形参时,函数的形参要加const的修饰,这样既可以右值传过来,也可以左值传过来
//右值引用不能直接给左值取别名 ,右值引用可以引用move以后的左值:int&& r7=move(a)//a(0)
// move : 把左值变成右值
return 0;
}
- 左值引用 : 给左值取别名 ,右值引用 :给右值取别名
- 左值引用不能直接给右值取别名 : int& z=10;(X) ,要加const修饰 : const int& z=10 ,(这里也可以看到实参传给函数的形参时const的作用,函数的形参要加const的修饰,这样既可以右值传过来,也可以左值传过来)
- 右值引用不能直接给左值取别名 ,右值引用可以引用move以后的左值:int&& r7=move(a)//a(0)
- move : 把左值变成右值
左值引用的使用场景和价值是什么?
- 使用场景:
1:做参数
2: 做返回值 - 价值:减少拷贝
- 左值引用的缺陷 : 局部变量不能用引用返回,局部变量的生命周期结束了
为了解决左值引用的缺陷,有了移动拷贝(将左值变成右值)
八、移动拷贝
我们来看一段代码:
string& func()
{
string str;
cin >> str;
//...
return str;//此时str是临时变量,函数栈帧销毁,str变成野指针所以不能用string& 作返回值 ,要不然就给野指针取别名了
//那返回值只能是string 要创建一个临时变量拷贝str返回
}
int main()
{
string ret = func();//编译器在这里会优化,会在函数栈帧销毁前str变成野指针前,会直接用str去拷贝ret
return 0;
}
- str是临时变量,不能够传引用返回 ,因为函数栈帧销毁,str变成野指针所以不能用string& 作返回值 ,要不然就给野指针取别名了
- 那返回值只能是string 要创建一个临时变量拷贝str返回, 此处是深拷贝 ,在这里就会产生空间的消耗
- 好在编译器看不下这种拷贝又拷贝的情况 ,此处编译器会优化

在有了左值和右值的知识基础上,我们来看左值和右值的函数匹配:
void func(int& r)
{
cout << "func(int& r)" << endl;
}
void func(int&& r)
{
cout << "func(int&& r)" << endl;
}
int main()
{
int a = 0;
int b = 1;
func(a);//
func(a + b);//会走更匹配的func(int&& r)
return 0;
}
- 此处a+b是右值,它会走更匹配的右值参数的函数func
string func1()
{
string str("xxxxxxxxxx");
//...
return str;
}
int main()
{
string ret = func1();
// ,
//移动拷贝:直接将str与ret发生swap(),不仅得到str的值,而且还把ret的值了str(str反正要销毁)
/*
移动拷贝的实现:
string中重载operator=(string&& s)
string& operator=(string&& s)
{
swap(s);
return *this;
}
*/
//以前的operator=就像是只写了void func(int& r),无论是左值还是右值,都调用这个函数,
//而现在如果是右值就会调用更适合的void func(int&& r)
//回到string str ,str是函数的返回值是右值,自定义类型的右值就是将亡值, 那么有了operator=(string&& s)之后
//会去调用operator=(string&& s),就直接将str与ret发生swap(),不仅得到str的值,而且还把ret的值了str(str反正要销毁)
//这个过程叫移动拷贝
//移动拷贝降低了原来的拷贝的次数
return 0;
}
- 内置类型的右值 : 纯右值 ,自定义类型的右值: 将亡值
- 当func1返回的是左值时是深拷贝,到ret要三次深拷贝

- 而func1返回的是右值时是移动拷贝 ,移动拷贝:会直接将str与ret发生swap(),不仅得到str的值,而且还把ret的值了str(str反正要销毁),这样只用拷贝一次

移动拷贝的实现:在string类中重载了operator=(string&& s) (原先的operator=是针对左值,这个是针对右值)
在string中重载operator=(string&& s)
string& operator=(string&& s)
{
swap(s);
return *this;
}
以前的operator=就像是只写了void func(int& r),无论是左值还是右值,都调用这个函数,而现在如果是右值就会调用更适合的void func(int&& r) ,回到string str ,str是函数的返回值是右值,自定义类型的右值就是将亡值, 那么有了operator=(string&& s)之后 ,会去调用operator=(string&& s),就直接将str与ret发生swap(),不仅得到str的值,而且还把ret的值了str(str反正要销毁) ,这个过程叫移动拷贝 ,移动拷贝降低了原来的拷贝的次数
那么,函数的返回值能是string&& 吗,str不是被识别成了右值吗
string&& func1()
{
string str("xxxxxxxxxx");
//...
return str;//识别str是右值
}
int main()
{
string ret = func1();
return 0;
}
- 答案是不行
- func1 返回值不能是string&& , 如果是,那么编译器就会去取别名,str是临时变量,函数栈帧销毁,str变成野指针所以不能用string&& 作返回值 ,要不然就给野指针取别名了
- 编译器看返回值是string,要传值拷贝才回去优化,编译器才会把str强行识别成右值,并且直接把str识别成右值 ,如果不优化,则会把返回值产生的临时变量识别成右值
总体来说:
1.编译器优化掉临时变量,直接把str当作返回值
2.把str当作返回值以后 ,有了移动拷贝,会把str强行看作右值
所以如果一开始传的string&& ,则就没有编译器的优化,不会把str当作返回值,更没有移动拷贝
为什么是强行 : move的作用是将左值转换成右值 ,这里可没写move哦 ,但都被强行识别成了右值 ,免得去原来写好的代码中加上move,那样太麻烦了
更多推荐



所有评论(0)