C++11
一,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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。
二,列表初始化
列表初始化就是内置类型和自定义类型都可以用 {} 去初始化,可以加 = 也可以不加
2.1 {}初始化
C++新增了用{}初始化内置类型和自定义类型
如下:
int a3{ 1 };
int a4 = { 2 };
对于自定义的类型,{}初始化其实是调用了其构造函数
下面定义了一个Date类,看看两种初始化的不同
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;
};
Date d1(2013, 1, 1);//构造
Date d2 = { 1221,1,1 };
第一种方式我们知道,是调用了构造函数,而第二种方式是走了列表初始化,本质是一种隐式类型转换,用{}中的数据去调用构造函数。
这里就像用字符串去定义string
string s = "xxxx";
这里是用"xxx"构造一个临时string对象,再用临时对象拷贝构造s,但是编译器会优化为直接构造
在C++中我们可以new一个Date指针
Date* p1 = new Date[2]{ d1,d2 };
C++11支持{}后,就可以按照下面的方式去定义
Date* p2 = new Date[2]{ {2323,2,1},{1212,2,1} };
这里也是用{}里面的{2323,2,1}内容构造一个临时对象,再进行拷贝构造,编译器优化为直接构造
2.2 initializer_list
现在来看下面两种初始化,有什么区别
vector<int> v1 = {1,2,3,4};
list<int> l1 = { 1,2,3,4 }
Date d1 = { 2024,3,24 };
Date d2 = {2024,11,11,1};//这里会报错
这里就要牵扯出C++11新增加的initializer_list
可以看到initializer_list也是支持迭代器的。
而且initializer_list是可以被识别的:
auto it = { 1,2,3,4 };
cout << typeid(it).name() << endl;//打印类型名
initializer_list 内部有两个指针一个指向第一个数据,一个指向最后一个数据的下一个位置
在上述vector v1 = {1,2,3,4};时,{1,2,3,4}会被识别为initializer_list,vector会遍历initializer_list,逐个将initializer_list 中的数据插入到vector中。
list也是同理,因为其内部构造时实现了用initializer_list去初始化。
这里的Date d1 = { 2024,3,24 }; 我们上面讲过这里和Date的构造函数对应,而这一句Date d2 = {2024,11,11,1};会报错,是因为其内部构造没有实现用initializer_list去初始化。
另外set和map也是支持initializer_list的
看下面的代码:
pair<string, string> kv("xxxx", "1111");
map<string, string> dict = { kv,{"aaaa","222222"} };
这里也是将{“aaaa”,“222222”}隐式类型转换构造一个pair,再用initializer_list{ kv,{“aaaa”,“222222”} }去构造map
vector的operator=也支持initializer_list
vector<int> v;
v = { 1,2,3,4 };
三,右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
3.1 左值引用和右值引用
左值简单来说就是=左边的值,右值就是=右边的值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
但是加上const后可以给右值取别名
int* p = new int(0);
int b = 1;
const int c = 2;
//对左值的引用
int*& pp = p;
int& bb = b;
const int cc = c;
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名
不能给左值,除非move(左值)
比如下面的:
10
x + y
Func(x,y)
//对右值的引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = Func(x, y);
int a = 1;
int&& aa = move(a);
3.2 右值引用使用场景
现在我们来看看右值引用的使用场景,C++11为什么要加上右值引用呢?
其实右值引用有两个实际用途:
移动拷贝
移动赋值
右值又分为将亡值和纯右值,内置类型会被识别为纯右值,自定义类型会被识别为将亡值。
在这里我们拿之前模拟实现的string(我们省略了其他代码)来举例:
class string
{
public:
//...
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(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
/*string tmp(s);
swap(tmp);*/
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
//string operator+=(char ch)
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
};
string to_string(int x)//这里就不可以用引用(左值引用)返回,因为ret是临时变量出了作用域销毁
{
string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;//会拷贝构造(深拷贝)一个临时对象返回,
}
int main(){
string s;
s = to_string(1234);
return 0;
}
这里的to_string不可以用引用返回,因为函数里面定义的string出了作用域会销毁,所以这里是传值返回,出了作用域会产生一个临时变量
但是在有了右值引用之后,我们可以再重载构造函数和赋值重载,让其传入右值引用。
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)-- 移动赋值" << endl;
swap(s);
return *this;
}
传入的参数是右值引用,这样在调用to_string函数时,编译器会将函数中的ret识别为将亡值,进而调用移动构造和移动赋值,从而调用移动构造一个临时对象(不再重新创建新的地址),再将移动构造后的临时对象移动赋值给s(也不用再深拷贝创建新的地址),自始至终都只有一个地址。
移动构造和移动赋值对于深拷贝有意义,对于浅拷贝来说意义不大
C++11有了移动构造和移动赋值,传值返回就不再担心
C++11后的STL容器都加了移动构造和移动赋值
push_back和insert也增加了移动构造和移动赋值
3.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; }
template<class T>
void PerfectForward(T&& t)
{
//Fun(t);//这里调用的都是左值引用---因为右值被右值引用后属性是左值
}
这里的 PerfectForward(T&& t)就可以做到传入左值是左值引用,传入右值是右值引用(引用折叠
)。
但是这里面的Fun(t)中的 t 都会被识别为左值,所以调用的都是左值引用的版本
那么如何做到传入左值是左值引用,传入右值是右值引用呢?
就要用到完美转发
了
std::forward 完美转发在传参的过程中保留对象原生类型属性!!
void PerfectForward(T&& t)
{
//Fun(t);//这里调用的都是左值引用---因为右值被右值引用后属性是左值
// 完美转发
Fun(forward<T>(t));
}
这里加上完美转发就可以达到目的。
int main()
{
PerfectForward(10); // 传入右值,是右值引用
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // 传入const左值,是const左值引用
PerfectForward(std::move(b)); // 传入const右值,是const右值引用
return 0;
}
四,新的类的功能
前面我们知道,C++的类中有六个默认的成员函数
C++11 新增了两个:移动构造函数和移动赋值运算符重载
这里生成默认的移动构造的条件比较严格:
没有实现析构,拷贝构造,拷贝赋值中的任意一个(都不实现),这样编译器才会自己生成一个默认的移动构造。其中对于内置类型会逐成员按字节拷贝,对于自定义的会调用其移动构造,如果其没有实现移动构造则会调用拷贝构造。
默认的移动赋值也是类似。
好了 ,这一节关于C++11的新特性的介绍就到这里,一下节我会继续带来C++11的内容,希望大家可以持续关注…