C++11详解
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11详解相关的知识,希望对你有一定的参考价值。
C++11
文章目录
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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
列表初始化
C++98中的初始化问题
在C++98中,标准允许使用花括号对数组元素进行统一的列表初始值设定。比如:
int a[] = 1,2,3,4;
int b[5] = 0;
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v1,2,3,4,5;
这样在C++98中无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
C++11内置类型的列表初始化
int main()
// 内置类型变量
int x1 = 10;
int x210;
int x3 = 1+2;
int x4 = 1+2;
int x51+2;
// 数组
int a[5] 1,2,3,4,5;
int b[]1,2,3,4,5;
// 动态数组,在C++98中不支持
int* p1 = new int[5]1,2,3,4,5;
// 标准容器
vector<int> v1,2,3,4,5;
map<int, int> m1,1, 2,2,,3,3,4,4;
return 0;
总结:
C++11里面扩展了初始化使用,基本都可以使用他来初始化,建议还是按旧的用法使用,一般new[]建议这样来初始化
自定义类型的列表初始化
- 标准库支持单个对象的列表初始化
class A
public:
A(int x = 0, int y = 0): _x(x), _y(y)
private:
int _x;
int _y;
;
int main()
A a1,2;
return 0;
自定义类型对象可以使用初始化,必须要有对应参数类型和个数的构造函数,因为用初始化,会调用对应的构造函数。
STL当中的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即
可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器
以及获取区间中元素个数的方法size()。
#include<map>
#include<vector>
#include<list>
int main()
vector<int> v1 = 1,2,3,4,5;
vector<int> v11,2,3,4,5;//可以省略=号
auto lt1 = 1,2,3,4;//lt1的类型为initializer_list<int>
//所以使用1,2,3,4初始化本质上调用了initializer_list作为参数的构造函数
initializer_list<int> lt1 = 1,2,3,4;
map<string,int> dict = pair<string,int>("sort",1),pair<string,int>("sort",1);
map<string,int> dict = "sort",1,"sort",1;
return 0;
C++11用初始化好像是万能的,一个自定义类型调用初始化,本质是调用对应的构造函数
模拟实现vector当中的initializer_list作为参数的构造函数:
vector(initializer_list<T> lt)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
typename initializer_list<T>::iterator it = lt.begin();
auto it = it.begin();
while(it != lt.end())
push_back(*it);
++it;
本质上下面这种初始化是调用了一个initializer_list作为参数的构造函数来初始化:
vector<int> v1 = 1,2,3,4,5;
总结:
- 自定义类型对象可以使用初始化,必须要有对应参数类型和个数的构造函数,因为用初始化,会调用对应的构造函数
- STL容器支持初始化,容器里面有支持一个initializer_list作为参数的构造函数
变量类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂
auto
int main()
int i = 0;
auto p = &i;//只能用来推导类型
auto pf = strcpy;
cout<<typeid(p).name()<<typeid(pf).name()<<endl;
return 0;
auto自动推导对象类型:
decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型
decltype和auto的区别:
int main()
int i = 0;
auto p = &i;//只能用来推导类型
auto pf = strcpy;
decltype(pf) pf1;//可以作为推导类型来创建对象或者变量
cout<<typeid(pf).name()<<endl<<typeid(pf1).name()<<endl;
return 0;
可以作为推导类型来创建对象或者变量
STL当中的变化
array
固定大小的数组容器
下面这两个有什么区别呢?
#include<array>
int main()
array<int,10> a1;
int a2[10];
return 0;
这两基本没有什么区别,唯一最大的区别是:
a1[13] = 1;
a2[13] = 1;
a1只要越界一定报错,而a2只要越界不一定报错,[]对越界检查使用了assert,更严格安全,还有就是a1是有迭代器的,C++11增加这个感觉没什么用
forward_list
单链表,这个容器里面没有尾插尾删,因为尾插尾删效率低,并且实现了insert_after,C++11增加这个属实没感觉到有什么用
unordered_map和unordered_set
对于unordered_map和unordered_set的添加还是让人高兴的,它们的底层使用哈希表实现的,它们的介绍请前往博主的C++专栏阅读
针对旧容器,基本都增加了移动构造,移动赋值,所有插入数据接口函数,都增加右值引用版本这些接口都是用来提高效率,下面我们来看一下什么是右值引用:
右值引用和移动语义
左值和右值
在说右值引用之前,我们得先谈谈什么是左值,什么是右值,可能大家理解的左值和右值的概念是赋值号左边的就是左值,赋值号右边的就是右值,其实不然,左值也可能出现在右边,比如:
int main()
int a = 10;
a = 20;
int b = 10;
b = a;
这里的a是左值,b也是左值,b = a,发现左值也可以出现在赋值号右边,所以这些说法是不正确的,正确的说法应该是:左值可以获取地址,而右值不能获取地址。
左值都是可以获取地址,左值基本都可以出现在赋值符号的左边,可以修改,但是const修饰的左值,只能获取地址,不能赋值,右值不能出现赋值符号的左边,也就是不能修改右值,不能取地址
左值引用是给左值取别名,左值引用不能引用右值,const左值引用可以引用左值,也可以引用右值:
int main()
int a = 10;
int& rt = a;
//int& rt = 10;//error,左值引用不能引用右值
const int& rt = 10;//const左值引用可以引用右值
return 0;
当形参是const修饰的左值引用时,实参既可以是左值也可以是右值:
void push_back(const T& x)
右值匹配了右值引用,左值匹配了左值引用,需要注意的是,我们说当形参是const修饰的左值引用时,实参既可以是左值也可以是右值,有人说那这里为什么f(1)不匹配第一个函数呢?是因为编译器会去找最匹配的。
右值引用只能引用右值不能引用左值:
int a = 10;
int&& r2 = a;//error
右值引用可以引用move以后的左值:
int&& r3 = std::move(a);
右值引用作为形参时,实参只能传右值:
void push_back(T&& x)
右值引用不可以连续引用:
int&& r1 = 10;
int&& r4 = r1;//error,右值引用只能引用右值,不能引用左值
右值引用可以引用move后的左值:
int&& r4 = std::move(r1);
因此关于左值与右值的区分不是很好区分,一般认为:
- 普通类型的变量,因为有名字,可以取地址,都认为是左值。
- const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
- 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
- 如果表达式运行结果或单个变量是一个引用则认为是左值。
- 右值引用可以引用move后的左值
总结:
- 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断
- 能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分:
- C语言中的纯右值,比如:a+b, 100
- 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
左值引用和右值引用
左值引用的使用场景
在传参时:
左值引用的场景,引用传参可以减少拷贝
在引用返回时:
左值引用,引用返回可以减少拷贝,但是效果不明显,不使用引用返回,编译器会优化一次拷贝,使用引用返回,编译器不会优化
本质上引用都是用来减少拷贝,提高效率
1、左值引用解决大部分的场景(做参数,做返回值)
2、右值引用是左值引用一些盲区的补充
比如我们写一个to_string函数来讲解(这是一个整形转字符串的函数):
string to_string(int val)
string str;
while(val)
int i = val % 10;//取到最低的十进制位
str += ('0' + i);
val /= 10;
reverse(str.begin(),str.end());//逆置
return str;
int main()
string s = to string(1234);
cout<<s.c_str()<<endl;
return 0;
此时的to_string函数只能值返回,如果是引用返回就会出问题,因为临时对象出了作用域就销毁了,相当于s成了野指针,我们只能传值返回,传值返回会有一次拷贝构造,本来是str去拷贝构造临时对象,然后临时对象再去拷贝构造s,但是编译器做了优化,直接用str去拷贝构造s,这个临时对象小的话放在寄存器,大的话放在调用它的函数的栈帧当中。还记不记得前面说的将亡值:函数按照值的方式进行返回时产生的临时对象就是将亡值
当这个变量或者对象出了作用域在,这种场景就可以用左值引用,在这里我们可以硬用左值引用返回,将里面的string对象写成静态的就可以引用返回了,因为这个变量出了作用域还在:
string& to_string(int val)
//线程安全问题
static string str;
str.clear();
while(val)
int i = val % 10;//取到最低的十进制位
str += ('0' + i);
val /= 10;
reverse(str.begin(),str.end());//逆置
return str;
但是这样写会有多线程安全问题,而且每次进来还需要将str清理一次,所以左值引用无法解决局部对象返回,只能传值返回
那么右值引用返回呢?
string&& to_string(int val)
//线程安全问题
string str;
while(val)
int i = val % 10;//取到最低的十进制位
str += ('0' + i);
val /= 10;
reverse(str.begin(),str.end());//逆置
return (move)str;
右值引用并不会改变局部变量的生命周期,返回的也是str的别名,出了作用域str也就销毁了,所以几乎没有使用右值引用返回的场景。
总结:str在按照值返回时,必须创建一个临时对象,临时对象创建好之后,str就被销毁了,最后使用返回的临时对象构造s,s构造好之后,临时对象就被销毁了。仔细观察会发现:str、临时对象、s每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?
下面就用到了右值引用的场景:
右值引用的场景
移动语义
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解在值传递时空间的浪费问题,在C++11中如果需要实现移动语义,必须使用右值引用。string类的移动构造函数
//移动构造
String(String&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
this->swap(s);
我们自己模拟实现string,下面的场景就用到了右值引用:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
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);
//拷贝构造
String(const String& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
cout << "拷贝构造:String(const String& s)"<< endl;
String tmp(s._str);
swap(tmp);
//移动构造
String(String&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
cout << "移动构造:String(String && s)" << endl;
this->swap(s);
// 拷贝赋值
String& operator=(const String& s)
cout << "String& operator=(const string& s) -- 深拷贝" << endl;
String tmp(s);
swap(tmp);
return *this;
// s1.swap(s2)
void swap(String& s)
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
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)
push_back(ch);
return *this;
String to_string(int val安卓和ios应用的机型适配难度有何差别