文章目录
1. C++11简介
- 在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。
- 不过由于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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
2. 列表初始化
2.1 C++98中{}的初始化问题
在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。比如:
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表达式中(C++98无法初始化)
int* p1 = new int[4]{0}; //不可添加等号
int* p2 = new int[4]{1,2,3,4}; //不可添加等号
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化
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()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
2.1 initializer_list容器
initializer_list 类型
C++11中新增了initializer_list容器,该容器没有提供过多的成员函数。
- 提供了
begin
和end
函数,用于支持迭代器遍历。 - 以及
size
函数支持获取容器中的元素个数。
initializer_list本质就是一个大括号括起来的列表,如果用auto关键字定义一个变量来接收一个大括号括起来的列表,然后以typeid(变量名).name()
的方式查看该变量的类型,此时会发现该变量的类型就是initializer_list。
int main()
{
auto il = { 1, 2, 3, 4, 5 };
cout << typeid(il).name() << endl;
//class std::initializer_list<int>
return 0;
}
std::initializer_list使用场景:
- std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
- std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为 operator= 的参数,这样就可以用大括号赋值。
- initializer_list容器没有提供对应的增删查改等接口,因为initializer_list并不是专门用于存储数据的,而是为了让其他容器支持列表初始化的。比如:
示例代码
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()
{
//用大括号括起来的列表对容器进行初始化
vector<int> v = { 1, 2, 3, 4, 5 };
list<int> l = { 10, 20, 30, 40, 50 };
vector<Date> vd = { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 } };
map<string, string> m{ make_pair("sort", "排序"), { "insert", "插入" } };
//用大括号括起来的列表对容器赋值
v = { 5, 4, 3, 2, 1 };
return 0;
}
代码分析
vector<Date> vd = { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 }
- 第一步:编译器会将
{ Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 }
转换成initializer_list<Date>
类型 - 第二步:调用
vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
构造函数 - 当用列表对容器进行初始化时,这个列表被识别成initializer_list类型,于是就会调用这个新增的构造函数对该容器进行初始化。
- 这个新增的构造函数要做的就是遍历initializer_list中的元素,然后将这些元素依次插入到要初始化的容器当中即可。
模拟参数为 initializer_list类型的vector构造函数和赋值函数
template<class T>
class vector
{
vector(initializer_list<T> il)
{
_start = new T[il.size()];
_finish = _start;
_endofstorge = _start + il.size();
//范围for遍历
for (auto e : il)
{
push_back(e);
}
}
vector<T>& operator=(initializer_list<T> il)
{
vector<T> tmp(il);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorge, tmp._endofstorge);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorge;
};
说明一下:
最好也增加一个以initializer_list作为参数的赋值运算符重载函数,以支持直接用列表对容器对象进行赋值,但实际也可以不增加。
如果没有增加以initializer_list作为参数的赋值运算符重载函数,下面的代码也可以正常执行:
vector<int> v = { 1, 2, 3, 4, 5 };
v = { 5, 4, 3, 2, 1 };
原因在于:
- 对于第一行代码,就是调用以initializer_list作为参数的构造函数完成对象的初始化。
- 而对于第二行代码,会先调用initializer_list作为参数的构造函数构造出一个vector对象,然后再调用vector原有的赋值运算符重载函数完成两个vector对象之间的赋值。
3. 声明
C++11提供了多种简化声明的方式,尤其是在使用模板的时候。
3.1 auto
auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。比如:
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); //简化代码
vector<int> v;
cout << typeid(p).name() << endl; //int *
cout << typeid(pf).name() << endl; //char * (__cdecl*)(char *,char const *)
cout << typeid(it).name() << endl;
cout << typeid(v).name() << endl; //class std::vector<int,class std::allocator<int> >
return 0;
}
自动类型推断在某些场景下还是非常必要的,因为编译器要求在定义变量时必须先给出变量的实际类型,而如果我们自己设定类型在某些情况下可能会出问题。比如:
int main()
{
short a = 32670;
short b = 32670;
//c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
auto c = a + b;
cout << typeid(c).name() << endl;//int
return 0;
}
3.2 对于auto的一些使用规则
对于auto的一些使用规则
1.对于用auto修饰的变量一定要初始化
int main()
{
auto a ;// 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
2. 注意auto是不可以作为函数的参数和返回值的
这是因为用auto修饰的变量必须要初始化,这样才能在编译阶段推断出变量的类型,而函数的参数和返回值在程序运行阶段才能确定,所以auto是不可以作为函数的参数和返回值的。
3. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加& 。
4. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的类型定义其他变量 。
int main()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
return 0;
}
5.auto不能用来声明数组
3.3 decltype
decltype
decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。
decltype是在编译期用来推导表达式类型的。其语法格式为:decltype(expression)
。大家可以看到,decltype是可以对一个表达式取类型的,并不仅是单个的变量。所以,把形式再扩展一下:
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1*t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x*y) ret;
decltype(&x) p;
cout << typeid(ret).name() << endl; //double
cout << typeid(p).name() << endl; //int const *
F(1, 'a'); //int
F(1, 2.2); //double
return 0;
}
那么有了auto为什么还要有decltype呢?
这是因为:auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
3.4decltype的推导规则
对于形如decltype(expr)
的饰词,
expr | 推导过程 |
---|---|
当expr为左值表达式时 | 若expr是一个名字,则该饰词得到expr的类型,不忽略引用,数组和函数也不会退化为指针;若expr不是一个名字,则该饰词得到左值引用。 |
当expr为右值表达式时 | 该饰词得到expr的类型 |
比如,
int x = 0;
// expr为左值表达式
decltype(x) i = x; // int
decltype((x)) ri = i; // int&
// expr为右值表达式
decltype(6) c; // int
decltype((6)) rc; // int
其中,decltype(x)是int,因为"x"是一个名字,而decltype( (x) )是int&,因为"(x)"不是一个名字。
decltype(auto) (鸡肋)
这个饰词的意思是,auto指定欲实施推导的型别,decltype指定推导的规则。
比如,
int i = 0;
const int& rci = i;
auto a = rci; // a是int
decltype(auto) da = rci; // da是const int&
另外,decltype(auto) 可以用来定义变量、表示函数返回值类型,但不能用于lambda的形参或函数的形参。
3.5 decltype返回值类型追踪
template <class T1, class T2>
T1 add(T left, T right)
{
return left + right;
}
int main()
{
add(1, 1.2));
return 0;
}
上面的场景我们发现在模板函数中返回值类型是难以确认的因为在调用的时候传入参数不同那么返回值也相应的不同,所以我们就想如果返回值可以根据结果来自动检测类型从而返回那么该多好,这里就用到了decltype返回值追踪。
示例代码
#include<iostream>
//decltype返回值追踪
template <class T1, class T2>
//auto是起到占位符的作用因为返回值不可以不给
auto add(T1 left, T2 right)->decltype(left+right)
{
return left + right;
}
int main()
{
cout << typeid(decltype(add(1, 1.2))).name() << endl;;
return 0;
}
错误代码
template <class T1, class T2>
decltype(left + right) add(T1 left, T2 right)
{
return left + right;
}
我们可以看到直接报错了,这是因为编译器编译代码从左到右编译那么,这里将decltype(left+right) 写到返回值处,这时编译器就懵逼了left和right是个啥东西因为这里还没有编译到left和right所以我们一般都是写道函数名后面,函数的返回值用占位符auto来表示。
3.6 auto 与 decltype
其实auto和decltype都是用来自动识别类型的,那么我们就会有疑问:我们在定义变量的时候不是直接给出变量类型吗? 那么还为什么需要类型推导呢?
这是因为在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂。例如上述给的例子:
int main()
{
short a = 32670;
short b = 32670;
//c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
auto c = a + b;
cout << typeid(c).name() << endl;//int
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "banana", "香蕉" } };
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
对于上面的这两种情况我们可以用auto和decltype来解决:
#include<iostream>
#include<string>
#include<map>
using namespace std;
int main()
{
short a = 32670;
short b = 32670;
auto c = a + b;//这里直接用auto来识别c的类型
decltype(a + b) d = a + b;//或者用decltype来识别a+b的得到的结果类型,然后来定义c的类型
cout << typeid(decltype(a + b)).name ()<< endl;
cout << typeid(c).name() << endl;
cout << endl;
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "banana", "香蕉" } };
auto it1 = m.begin();//可以直接用auto来识别迭代器的类型
while (it1 != m.end())
{
cout << it1->first << " " << it1->second << endl;
++it1;
}
cout << endl;
decltype(m.begin()) it2=m.begin();
while (it2 != m.end())
{
cout << it2->first << " " << it2->second << endl;
++it2;
}
return 0;
}
3.7 nullptr
由于C++中NULL被定义成字面量0,这样就可能会带来一些问题,因为0既能表示指针常量,又能表示整型常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
在大部分情况下使用NULL不会存在什么问题,但是在某些极端场景下就可能会导致匹配错误。比如:
void f(int arg)
{
cout << "void f(int arg)" << endl;
}
void f(int* arg)
{
cout << "void f(int* arg)" << endl;
}
int main()
{
f(NULL); //void f(int arg)
f(nullptr); //void f(int* arg)
return 0;
}
NULL和nullptr的含义都是空指针,所以这里调用函数时肯定希望匹配到的都是参数类型为int*的重载函数,但最终却因为NULL本质是字面量0,而导致NULL匹配到了参数为int类型的重载函数,因此在C++中一般推荐使用nullptr。
4.范围for
范围for的使用条件
一、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
二、迭代的对象要支持++和==操作
范围for本质上是由迭代器支持的,在代码编译的时候,编译器会自动将范围for替换为迭代器的形式。而由于在使用迭代器遍历时需要对对象进行++和操作,因此使用范围for的对象也需要支持++和操作。