C++11中引入的比较常用的新特性讲解(上)
目录
1、C++11简介
2、统一的列表初始化
2.1、{}初始化
2.2、std::initializer_list
3、变量类型推导
3.1、auto
3.2、decltype
3.3、nullptr
4、范围for循环
5、STL中一些变化
6、右值引用和移动语义
6.1、左值引用和右值引用
6.2、右值引用使用场景和意义
6.3、完美转发
1、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引入的新特性
关于C++2X最新特性的讨论
查看C++各种库说明的一个好用的网站
C++官网
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。C++官网
2、统一的列表初始化
想达到的目的就是:一切都可以用列表{}初始化。
注意:列表初始化跟初始化列表(这是类的构造函数里的东西)不是一个概念。
2.1、{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
C++11扩大了用大括号括起的列表(列表初始化)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用列表初始化时,可添加等号(=),也可不添加。
创建对象时也可以使用列表初始化方式调用构造函数初始化。
栗子:
struct Point
{
int _x;
int _y;
};
class A
{
public:
// explicit A(int x, int y) // explicit关键字,加上之后就不允许隐式类型的转换了
A(int x, int y)
: _x(x), _y(y)
{
}
A(int x)
: _x(x), _y(x)
{
}
private:
int _x;
int _y;
};
void test1()
{
// C语言带过来的
int array1[] = {1, 2, 3, 4, 5};
int array2[5] = {0};
Point p = {1, 2}; // 结构体的初始化列表
// C++11有的,一切都可用列表初始化
int array3[5]{1};
int i{1};
// 单参数的隐式类型转换
A aa1 = 1;
A aa2 = {1};
A aa3{1};
// 多参数的隐式类型转换
A aa4 = {1, 2};
A aa5{3, 4};
const A& aa6 = { 7,7 }; // 不加 const 是不行的,因为这中间有临时变量,临时变量具有常性。 // 注:这句代码在 C++11 之后才支持
}
2.2、std::initializer_list
std::initializer_list的介绍文档
栗子:
void test2()
{
// the type of il is an initializer_list
auto il = {10, 20, 30};
initializer_list<int> il2 = {10, 20, 30};
cout << typeid(il).name() << endl;
cout << sizeof(il2) << endl; // 8/16
// std::initializer_list 虽然是一个容器,但是它本身并没有去新开空间,本质就是两个指针,一个begin指向常量数组的开头,一个end指向常量数组的结尾
vector<int> v1;
vector<int> v2(10, 1);
// 隐式类型转换
vector<int> v3 = {1, 2, 3, 4, 5};
vector<int> v4{10, 20, 30};
// 构造
vector<int> v5({10, 20, 30});
// 补充:X自定义 = Y类型 --> 想要达成隐式类型转换,则 自定义X 必须支持 Y 为参数类型的构造
// 1、pair多参数隐式类型转换
// 2、initializer_list<pair>的构造
map<string, string> dict = {{"sort", "排序"}, {"insert", "插入"}}; // 在没有支持initializer_list之前,这一句代码可是要分成三句写的,有了initializer_list之后,就方便了很多
}
总结:当容器想用不固定的数据个数初始化时,initializer_list就派上用场了。
注:所有的容器都支持 initializer_list。
3、变量类型推导
c++11提供了多种简化声明的方式,尤其是在使用模板时。
3.1、auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11中废弃auto原来的用法,将其用于实现自动类型推断。
这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
栗子:
void test3()
{
int i = 0;
auto &x = i; // auto可以加引用&
++x; // x的修改会影响i
int &j = i;
auto y = j; // 这里的 y 是单纯的 int 还是 int& 呢?
// 答:从语法层面上讲 y 就是 int,也可以打开监视窗口,&y,&j,&i 看看,会发现y和j、i的地址不一样,j、i的地址是一样的
// j 虽然是 i 的别名(int&),但 j 的本质也是int。
// 所以:这里就是一个很普通的拷贝,实际上这句代码就等价于 auto y = i;
++y;
pair<string, string> kv = {"sort", "排序"};
// auto [x, y] = kv; // 这是C++17支持的一种写法(当前编译器默认支持到C++14)
}
3.2、decltype
关键字decltype将变量的类型声明为表达式指定的类型。
栗子:
template <class T>
class B
{
public:
T *New(int n)
{
return new T[n];
}
};
auto func2()
{
list<int> it;
auto ret = it.begin();
return ret;
}
auto func1()
{
auto ret = func2();
return ret;
}
void test4()
{
list<int>::iterator it1;
cout << typeid(it1).name() << endl; // typeid 是能直接拿到这个变量类型的最原始的名字(字符串)(你所看到的类型名可能是typedef过的)
// typeid(it1).name() it2; // typeid 推出的只是一个单纯的字符串,不能用来定义一个新的对象
// decltype 可以帮你推断出()内的变量的类型,并且你可以直接使用 decltype 推断出的结果(类型)来定义新的变量。
decltype(it1) it2; // it2和it1的类型是一样的
cout << typeid(it2).name() << endl;
// 光从上面这几句代码,体现不出decltype的作用,因为auto也有这样的功能,而且写起来还更方便
auto it3 = it1;
cout << typeid(it3).name() << endl;
auto ret = func1();
// 此时如果你想要用ret的类型去实例化出一个B类型的对象,该怎么办?(假设这里的func不只套了这么几层,套了好多层,那你想要知道 ret 到底是什么类型,就会很麻烦)
// 那么 decltype 此时就派上用场了,decltype 推断出的结果(类型)可以用来做模板的传参
B<decltype(ret)> bb1;
map<string, string> dict = { {"insert","插入"}, {"erase","删除"} };
auto it4 = dict.begin();
B<decltype(it4)> bb2;
B<map<string, string>::iterator> bb3; // 这句代码可读性更好
}
注:decltype 感觉就是跟 auto 配套使用的,用来解决一些auto搞出的问题
总结:auto 和 decltype 还是要慎用,虽然两者都可以帮助你缩短代码量,但是很影响可读性。
3.3、nullptr
注:也是为了解决一些历史遗留的问题而造出的东西。
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能表示指针常量,又能表示整形常量。
所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
4、范围for循环
栗子:
void test5()
{
map<string, string> m = {{"sort", "排序"}, {"insert", "插入"}};
for (auto &[x, y] : m) // 这里的auto最好加上引用&和const(无需修改就加上const),否则会有深拷贝的问题(如果有大量的string要拷贝的话,会影响程序的效率)
{
cout << x << ":" << y << endl;
// x += "1"; // map里的key是不能被修改的
y += "1";
}
}
5、STL中一些变化
总结:
- 增加的4个新容器中,也就 unordered_map、unordered_set 有点用,另外两个(array、forward_list)没什么屁用。
- 给所有容器添加了 initializer_list 构造。
- 给所有容器添加了 移动赋值和移动构造。
- 给所有容器添加了 emplace系列(与右值引用和模板的可变参数有关)。
6、右值引用和移动语义
6.1、左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,之前学习的引用就叫做左值引用。
无论左值引用还是右值引用,都是给对象取别名。
那么什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,左值可以出现在赋值符号的左边/右边。
而右值则不能出现在赋值符号的左边。
左值引用就是给左值的引用,给左值取别名。
那么什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等。
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
右值引用就是对右值的引用,给右值取别名。
总之,简单说可以取地址的就是左值,不能取地址的就是右值。
注:不能说可以修改的就是左值,不能修改的就是右值。
栗子:
const int val = 7; // 这里的 val 还是左值
void test6()
{
// 左值:可以取地址的
int a = 10;
int b = a;
const int c = 10;
int *p = &a;
vector<int> v(10, 1);
(void)v[1]; // 强转成 void 取消警告
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &(*p) << endl;
cout << &(v[1]) << endl;
// 右值:不可以取地址的
// 10、string("111")、to_string(123)、x+y
// cout << &10 << endl; // 字面常量
// cout << &string("111") << endl; // 匿名对象
// cout << &to_string(123) << endl; // 该函数的返回值返回的是一个临时拷贝、临时对象
int x = 1, y = 2;
// cout << &(x + y) << endl; // 表达式返回值
// 几个右值引用的栗子
// 为了方便理解,补充两个概念:纯右值(内置类型) 将亡值(自定义类型)
int &&rref1 = (x + y); // 纯右值(内置类型)
string &&rref2 = string("111"); // 将亡值(自定义类型) // 生命周期就这一行
string &&rref3 = to_string(123); // 将亡值(自定义类型) // 生命周期就这一行
int &&rref4 = 10; // 纯右值(内置类型)
// 左值引用能否给右值取别名?
// 答:不可以,但是 const 左值引用可以
const string &ref1 = string("111");
const int &ref2 = 10;
// 右值引用能否给左值取别名?
// 答:不可以,但是可以给 move 以后的左值取别名
string s1("222");
// string&& rref5 = s1;
string &&rref6 = move(s1);
// 补充:当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。
// C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,
// 唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
// 下面的s2是左值还是右值呢?
string &&s2 = string("111");
// 要验证s2是左值还是右值实际上很简单,只需要看一下能不能取的出s2的地址就好了。
cout << &s2 << endl;
// 总结:右值引用本身的属性是左值!!!
// 需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。
// 下面的移动构造和移动赋值也说明了右值引用的属性是左值。
// 因为只有右值引用本身被处理成了左值,才能实现移动构造和移动赋值,才能转移资源。-- 因为中间要 swap
// 通过反汇编也是能看到右值是有地址的,只是语法上规定不能取。(没有地址的话,右值存在哪里呢?右值也得有个地方给它存吧)
// 右值引用本身是左值的意义是:为了 移动构造和移动赋值 中要 转移资源 的语法逻辑能够逻辑自洽。
// 如果右值引用的属性是右值,那么移动构造和移动赋值中要转移资源的语法逻辑就是矛盾的了。
// 右值是不能被改变的。(可以理解为右值带有const属性)
// 补充:即使没有右值引用,实际上也是有办法能够改变右值的
// ex:
string& s = (string&)string("kk"); // 强转可以让普通的左值引用直接引用右值
// 因为不管是什么值,终归要有空间去存储它,有空间,不就能改了嘛。
}
6.2、右值引用使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值。
那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?
下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
首先,引用的意义是什么?
答:引用的意义就是为了减少拷贝,提高效率。
比如:
左值引用解决的场景
void function1(const string &s); // 减少传参时的拷贝的消耗
string &function2(); // 传引用返回,减少拷贝的消耗
但是左值引用没有彻底解决返回值的拷贝消耗问题,因为不是什么情况下都能传引用返回的。
比如当返回值是function2中的局部对象,就不能用引用返回。
举个栗子:
namespace kk
{
class string
{
public:
typedef char *iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char *str = "")
: _size(strlen(str)), _capacity(_size)
{
cout << "string(char* str) -- 构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(kk::string &s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造(左值引用)
string(const kk::string &s) // 左值的拷贝构造会去调用这个
: _str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造(右值引用) -- 移动将亡值的资源
string(kk::string &&s) // 右值的移动(拷贝)构造会去调用这个
: _str(nullptr)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 赋值重载
kk::string &operator=(const kk::string &s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
char *tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
// 移动赋值
kk::string &operator=(kk::string &&s)
{
cout << "operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char &operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char *tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
// string operator+=(char ch)
kk::string &operator+=(char ch)
{
push_back(ch);
return *this;
}
const char *c_str() const
{
return _str;
}
private:
char *_str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
kk::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
kk::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str; // 虽然这里的str是左值,但是你加不加move,都是可以的,因为编译器已经帮你处理好了(编译器会强行认为这是右值),它都会去调用移动构造/移动赋值。
// 注意:右值引用/这里的移动构造 并不会延长对象的生命周期,str的生命周期到了,该销毁还是销毁,只是发生了资源转移。(严格来说,是延长了资源的生命周期)
}
}
总结:右值引用彻底拯救了传值返回所引发的效率低下的问题。一般的传值返回需要进行 一次拷贝构造 + 一次赋值/拷贝构造 -- 拷贝构造和普通的赋值的成本很大。有了右值引用之后,传值返回变成了 一次移动构造 + 一次移动赋值/移动构造 -- 移动构造和移动赋值只是对资源的转移,成本很低。
举个栗子:
vector<vector<int>> func(int rows); // 该函数 在C++11之前(右值引用出来之前) 不能这样写,因为效率十分的低下!!!
void func(int rows, vector<vector<int>>& vv); // 该函数 在C++11之前(右值引用出来之前) 只能这样写,才能保证效率。
6.3、完美转发
直接上代码:
void Fun(int &x) { cout << "左值引用" << endl; }
void Fun(const int &x) { cout << "const 左值引用" << endl; }
void Fun(int &&x) { cout << "右值引用" << endl; }
void Fun(const int &&x) { cout << "const 右值引用" << endl; }
// std::forward 完美转发在传参的过程中保留对象原生类型属性
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
// 在函数模板里面,这里可以称之为万能引用(引用折叠)
template <typename T>
void PerfectForward(T &&t) // 注意:模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 这里的 T&& 这一坨都是模板,而不只是那个T。
// 这里的t的属性根据你传过来的参数推导。
// 你传的是左值,t就是左值,你传的右值,t就是右值。
// 你传的 const 左值,t就是 const 左值。
// 你传的 const 右值,t就是 const 右值。
{
Fun(t); // 如果仅仅只是这样写传参的话,最后走的全是 左值引用 和 const 左值引用。-- 因为右值引用的属性还是左值
Fun(std::forward<T>(t)); // 要这样写,才能保证t的属性不变,不会发生从右值退化成左值(右值引用)的情况。-- 注意不能用 move,因为这里是模板,你不知道传过来的值本身是左值还是右值引用(右值退化成左值)?用了 move,就变成了全部都是调用 右值引用 和 const右值引用
Fun((T &&)t); // 这样写的效果跟完美转发一样。 // 但要注意,这不是官方的写法,不太清楚这种写法有没有什么缺陷。
}
void test7()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
}
总结:完美转发的意义就是在一个既能接收左值又能接收右值的函数模板中,当需要保持一个右值引用的属性保持不变,不想其退化成左值的时候,就用完美转发。