目录
前言
相比于C++98标准和C++03标准,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以学习C++11标准中的新增的语法特性是很有很有必要的。
一、统一的列表初始化
1、{}初始化
在C++98标准中,允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
C++11标准中,扩大了用大括号括起的列表(初始化列表)的使用范围,
使其可用于所有的内置类型和用户自
定义的类型,
使用初始化列表时,可添加等号
(=)
,也可不添加。
int main()
{
//在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
A a = { 1, 2 };//初始化结构体变量(在C语言中就支持这种初始结构体的方法)
//C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
//定义的类型,使用初始化列表时,可添加等号(= ),也可不添加。
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
A a{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
创建自对象时也可以使用列表初始化方式调用构造函数初始化 。
我们先写一个简单的日期类:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year=0, int month=0, int day=0)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
{
cout<<"Date(const Date& d)"<<endl;
}
~Date()
{
cout<<"~Date()"<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); //调用Data的构造函数传统初始化方法
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };//一般建议把=号加上,因为可能存在特殊场合不加=的可能产生误解
return 0;
}
2、initiallizer_list
initializer_list是C++11标准提供的一种类模板,头文件是<initializer_list>,其内部存储的什么类型的数据取决于模板参数T。
std::initializer_list
使用场景:
std::initializer_list
一般是作为构造函数的参数,C++11对STL中的不少容器就增std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就以使用{}对STL中的容器进行初始化。
int main()
{
auto il = { 10, 20, 30 };//系统会默认将il识别为initializer_list类型
cout << typeid(il).name() << endl;
return 0;
}
接下来我们来了解一下STL中的vector,list等利用 {}初始化 与自定义类型对象利用 {}初始化 的不同之处。
我们以vector的内部模拟实现为例,实现一下以initializer_list为参数的vector的构造接口和operator=接口,如下:
vector(initializer_list& il)
{
reserve(il.size());
for(auto& e : il)
{
push_back(e);
}
}
vector<T>& operator=(initializer_list& il)
{
//首先用il构造一个新的vector再将这个新vector和目标赋值vector的成员变量交换
vector<T> tmp(il);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._ebdofstorage);
}
int main()
{
Date d = {2024, 5, 1};//利用{}初始化时,{}里的值跟对应构造函数的参数个数匹配
//多参数构造类型转换,{2024,5,1}首先构造成一个Date类型的临时对象
//再将其拷贝构造给对象d,编译器优化:构造+拷贝构造->直接构造
vector<int> v = { 1,2,3,4 };//{1,2,3,4}就是initializer_list类型的对象,对象v再调用
//自己内部以initializer_list类型对象为参数的构造函数构造出v
// 使用大括号对容器赋值
v = {10, 20, 30};//v调用以initializer_list类型的对象为参数的operator=
list<int> lt = { 1,2 }; //和vector一样
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//此时initializer_list类型对象里存储的数据类型是pair类型,和map里储存的数据类型一样。
}
二、声明
1、auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11
中废弃
auto
原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。
int main()
{
int a = 3;
auto p = &a;
auto il = {1, 2, 3};
cout << typeid(p).name() << endl;
cout << typeid(il).name() << endl;
map<string, string> dict = { {"aaa", "bbb"}, {"ccc", "ddd"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();//map储存的数据类型复杂,这种用auto表示类型的要谨慎使用。
//避免混淆
return 0;
}
2、decltype
关键字decltype将变量的类型声明为表达式指定的类型。
int main()
{
int i = 1;
double d = 5.5;
//类型以字符串形式获取到
cout << typeid(i).name();//typeid(i).name()是一个字符串int
//不可以将其当作其当作一个类型
auto ret = i * d;//ret的类型是double
decltype(ret) x;//delctype(ret)在定义对象x
vector<decltype(ret)> v;//用decltype(ret)来实例化vecto容器的参数
}
3、nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
三、范围for循环
int main()
{
//使用范围for遍历数组
int arr[] = {1, 2, 3, 4, 5};
for(auto e : arr)
cout << e << " ";
//使用范围for遍历对象
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for(auto e : arr)
cout << e << " ";
return 0;
}
遍历对象时如果对象里存储的数据类型是自定义类型的数据最好将auto写为auto&,这样可以省去在范围for遍历对象时对对象里的自定义类型的数据拷贝。
四、左值引用(C++98)
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。
无论左值引用还是右值引用,都是给对象取别名。
左值是一个表示数据的表达式(如变量名或解引用的指针),
我们可以获取它的地址
+
可以对它赋 值,左值可以出现赋值符号的左边和右边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
// 以下的p、a、b、*p都是左值 *p是一个表达式
int* p = new int(3);
int a = 1;
const int b = 2;//b是常变量 不可以给b再赋值,但是可以取b的地址
int c = a;//a是左值出现在了赋值符号的右边
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& ra = a;
const int& rb = b;
int& pvalue = *p;
return 0;
}
五、 右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;//表达式返回值 其实表达式的返回值是一个临时变量
fmin(x, y);//函数返回值也是一个临时变量
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修rr1,如果不想rr1被修改,可以用const int&& rr1 去引用。实际中右值引用的场景并不是这个,这个性质也不重要。
代码如下:
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}