目录
1. 列表初始化initializer_list
2. 前面提到的一些知识点
2.1 小语法
2.2 STL中的一些变化
3. 右值和右值引用
3.1 右值和右值引用概念
3.2 右值引用类型的左值属性
3.3 左值引用与右值引用比较
3.4 右值引用的使用场景
3.4.1 左值引用的功能和短板
3.4.2 移动构造
3.4.3 移动赋值
3.4.4 插入右值时减少深拷贝
4. 完美转发
4.1 万能引用(引用折叠)
4.2 完美转发
5. 新的类功能
5.1 默认生成的移动构造/赋值
5.2 类里新的关键字
本篇完。
在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++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include
using namespace std;
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;
}
protected:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
int x1 = 1;
int x2 = { 2 }; // 要能看懂,但是不建议使用
int x3{ 2 };
Date d1(2023, 1, 1); // 都是在调用构造函数
Date d2 = { 2023, 2, 2 }; // 要能看懂,但是不建议使用
Date d3{ 2023, 3, 3 };
return 0;
}
可以不加等号进行初始化,如上图代码所示,但是强烈不建议使用。
这其实很鸡肋,没有什么价值,继续使用C++98中的方式就挺好的,而且容易理解,C++11中的方式反而不太好理解了。C++中这种鸡肋的语法被很多人吐槽,理性看待。
列表初始化真正有意义的地方是用于初始化STL中的容器:
之前提到:vector和list以及map等STL中的容器也可以像普通数组一样使用初始化列表来初始化了。这是因为列表初始化本身就是一个类模板:
如上图所示,这是C++11才有的一个类型,该类型叫做列表初始化,而且还有自己的成员函数,包括构造函数,计算列表大小的接口,获取列表迭代器位置。(但几乎都不用)
C++11为这些容器提供了新的构造函数,该构造函数是使用列表来初始化对象的,它的形参就是initializer_list,所以列表初始化才可以初始化STL中的容器。
赋值运算符重载函数也有一个列表的重载版本:
#include
#include
#include
#include
C++11提供了一些新的小语法,很多我们都接触过甚至是使用过。
c++11提供了多种简化声明的方式,尤其是在使用模板时。这里讲auto和decltype
auto:这个关键字我们已经使用过很多了
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
decltype:
关键字decltype将变量的类型声明为表达式指定的类型。
使用typeid().name()只能打印出类型的名称,并不能用这个名称继续创建变量,而decltype可以:
template
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; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
使用decltype可以自动推演类型,并且可以用推演出的结果继续创建变量,如上图所示,对于一些不同类型直接的运算结果,decltype有奇效。
nullptr:
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
在C++中存在条件编译:(以后用nullptr就行了)这算是修复了一个bug
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
范围for循环
范围for我们也一直都在使用,这是C++11提供的语法糖,使用起来非常方便,它的底层就是迭代器,只是编译器给自动替换了,这里就不再讲解了。
新容器:
红色框中的是C++11增加的新容器,基本只有unordered_map和unordered_set有用,其他很鸡肋。容器array对标的是静态数组,array也是一个静态的,也就是在栈区上的,大小是通过一个非类型模板参数确定的。容器forward_list是一个单链表,也很鸡肋,因为绝大部分场景双链表都可以满足要求,而且更加方便,唯一使用到单链表的地方就是哈希桶中。前面都提到过。
至于unordered_map和unordered_set,这两个容器的底层是哈希桶,虽然不能实现排序,但是可以降重。而且在查找时具有其他容器无法比拟的效率。这两个容器是非常实用的,而且也是我们经常使用的。
容器中的使用新方法:
1. 使用列表构造
在前面就讲解过了,几乎每个容器都增加了新的接口,使用std::initializer_list类型来构造。2. 移动构造和移动赋值
在下面讲解了右值引用就可以明白了。3. emplace_xxx插入接口或者右值引用版本的插入接口。
同样在后面才能学习到。
什么是左值?什么是右值?
- 左值:一个表示数据的表达式,如变量名或者指针解引用。
- 特点:可以对左值取地址 + 可以对左值赋值。
上图代码中所示的变量都属于左值,要牢记左值可以取地址这一个特性。
- 右值:也是一个表示数据的表达式。如:字面常量,表达式返回值,函数返回值,类型转换时的临时变量等等。
- 特点:右值不可以取地址,不可以赋值。
要牢记右值特性:不能取地址不能赋值。
什么是右值引用?
左值引用是给左值取别名,右值引用显而易见就是给右值取别名。
上图代码中的rr1,rr2,rr3就是三个右值的别名,也就是右值引用。
对于内置类型的右值,如字面常量,一旦右值引用以后,就会被存储到特定的位置,
并且可以取到该地址,而且还可以修改。
int main()
{
int&& rr1 = 10;
cout << rr1 << endl;
rr1 = 5;
cout << rr1 << endl;
const double&& rr2 = (1.1 + 2.2);
//rr2 = 5.5; // 不能修改
return 0;
}
字面常量10原本是不可以被修改的,但是右值引用以后,在特定的位置开辟了变量来存放10,所以就可以被修改了。
表达式或者函数的返回值,会有一个临时变量来存放返回值,我们知道这样的临时变量具有常性,也是右值。对于这种右值引用,编译器会修改它的属性,将常性修改,并且存储在特定位置。
注意const类型的右值,即便开辟了变量存放该右值也是不可以被修改的,因为被const修饰了。
内置类型的右值被称为纯右值。
自定义类型的右值被称为将亡值。
对于自定义类型的右值,如容器的临时变量,它确确实实会被销毁,而不会被存放。
自定义类型的右值才能体现出右值存在的意义,后面会详细讲解。
思考:左值引用可以引用右值吗?
我们要知道,右值引用是C++11才出来的,右值传参给函数还是右值,那我们以前写的函数都用不了右值传参了?
template
void Func(const T& x)
{}
这里去掉const肯定是不能传参的,为了给右值传参(当然还有其它原因),所以const的左值引用可以引用右值。总结:普通的左值引用不可以引用右值,const的左值引用可以引用右值:
思考:右值引用可以引用左值吗?
右值引用不可以引用普通的左值,可以引用move以后的左值:(move这个语法先记住)
左值经过move以后就变成了右值,如:
int main()
{
// 左值引用可以引用右值吗? const的左值引用可以
double x = 1.1, y = 2.2;
//double& r1 = x + y;
const double& r1 = x + y;
// 右值引用可以引用左值吗?可以引用move以后的左值
int b = 7;
//int&& rr5 = b;
int&& rr5 = move(b);
return 0;
}
成功编译:
namespace rtx
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s) // 拷贝构造
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
string& operator=(const string& s) // 拷贝赋值
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
protected:
char* _str;
size_t _size;
size_t _capacity;
};
}
先自己实现一个string,只有拷贝构造函数,赋值运算符重载函数,析构函数,以及一个普通的构造函数。无论是拷贝构造还是赋值运算符重载,都会进行深拷贝,采用现代写法来实现:
namespace rtx
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
const char* c_str() const
{
return _str;
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s) // 拷贝构造
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
string& operator=(const string& s) // 拷贝赋值
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
protected:
char* _str;
size_t _size;
size_t _capacity;
};
}
左值引用的场景:
使用普通传值调用,存在一次深拷贝:
void Func(rtx::string s)
{}
int main()
{
rtx::string s("hello world");
Func(s);
return 0;
}
使用传拷贝引用时,不存在深拷贝,Func函数直接使用main函数中的s1对象:
void Func(rtx::string& s)
{}
int main()
{
rtx::string s("hello world");
Func(s);
return 0;
}
函数返回参数和上面一样,传引用返回有时确实能提高效率,
左值引用的功能:
1、做参数。a、减少拷贝,提高效率。b、做输出型参数。
2、做返回值。a、减少拷贝,提高效率。b、引用返回,可以修改返回对象(比如: operator[ ])。
但是左值引用做返回值只解决了70%的问题,在类似 to_string 函数中:
要知道深拷贝的代价是比较大的,深拷贝次数减少可以很大程度上提高代码的效率。
但是你敢传引用返回吗?我们把int value 转换成string,此时的 string 是一个形参。出了函数就销毁了。外面拿到的就是被销毁了的栈帧。
所以左值引用存在的短板:
前面我们在调用 to_string 函数的时候,我们把int value 转换成string,此时的 string 是一个形参。
所以只能传值返回,此时mian函数中拿到 to_string 中的 string 对象要进行两次深拷贝。
第一次深拷贝,to_string函数返回时,会将string对象放在一个临时变量中,此时发生的深拷贝。函数返回时,如果是内置类型等几个字节的变量,会将函数中的临时变量放在寄存器中返回,如果是自定义类型所占空间比较大,就会放在临时变量中压栈到上一级栈帧中。
第二次深拷贝,main函数中,ret接收函数返回了的string对象时会再发生一次深拷贝。
但是编译器会进行优化,将两次深拷贝优化成一次。虽然只有一次,但有些情况代价还是很大的。
C++98是如何解决上面的问题?
那就是输出型参数:rtx::string to_string(int value)变成rtx::void to_string(int value,string& s)
但是这样不太符合使用习惯。
此时用右值引用就可以解决这个问题。
右值引用的价值之一:补齐临时对象不能传引用返回这个短板。
前面的深拷贝是拷贝构造产生的:string(const string& s) // 拷贝构造(形参是左值引用)
演示在string类中增加一个移动构造函数:
前面提到过:内置类型的右值被称为纯右值。
自定义类型的右值被称为将亡值。(这里的传右值就是将亡值)
基于拷贝构造:无论是左值还是右值都老老实实地开空间:
string(const string& s) // 拷贝构造
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
左值因为还要使用,肯定要开空间的,这里的右值是将亡值,没用了,所以也不用开空间了,
因为不用开空间了,所以深拷贝也没了,而是资源转移(直接swap):
string(string&& s) // 移动构造
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造(资源转移)" << endl;
swap(s);
}
从to_string中返回的string对象是一个临时变量,具有常性,也就是我们所说的右值。
拷贝赋值移动赋值和拷贝构造移动构造类似:
string& operator=(const string& s) // 拷贝赋值
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
string& operator=(string&& s) // 移动赋值
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
总结:右值引用和左值引用减少拷贝的原理不太一样。
使用移动构造和移动赋值时,被转移资源的对象必须是个将亡值(像to_string的使用一样),因为会被销毁。
C++11的STL标准库中也提供了移动构造和移动赋值函数。
C++11在STL库容器中的所有插入接口都提供了右值版本,push_back,insert等。
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';
}
然后分别像库里的 list 插入左值和右值
int main()
{
list lt;
rtx::string s1("hello"); // 左值
lt.push_back(s1); // 插入左值
cout << "----------------------------------" << endl;
lt.push_back(rtx::string("world")); // 插入右值
//lt.push_back("world");
return 0;
}
如果没有移动构造那么下面的也是深拷贝了。
写多个重载函数,根据实参类型调用不同函数。
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; }
// 万能引用(引用折叠):t既能引用左值,也能引用右值
template
void PerfectForward(T&& t)
{
Fun(t); // 此时t变成了左值/const左值
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
代码中的perfectForward函数模板被叫做万能引用模板,
无论调用该函数时传的是什么类型,它都能推演出来:
在函数模板推演的过程中会发生引用折叠:模板参数T&&中的两个&符号折叠成一个。
当传入的实参是左值时,就会发生引用折叠,是右值时就不会发生引用折叠。
上面万能模板中,虽然推演出来了各自实参类型,但是由于右值引用本身是左值属性,所以需要使用move改变属性后才能调用对应的重载函数。
有没有办法不用move改变左值属性,让模板函数中的t保持它推演出来的类型。答案是有的,完美转发就能够保持形参的属性不变。
完美转发同样是C++11提供的,它也是一个模板:
完美转发:完美转发在传参的过程中保留对象原生类型属性。
实参传递过来后,推演出的形参是什么类型就保持什么类型继续使用。
这里会语法就行:
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; }
// 万能引用(引用折叠):t既能引用左值,也能引用右值
template
void PerfectForward(T&& t)
{
Fun(std::forward(t)); // 完美转发:保持t引用对象属性
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
此时再使用万能引用的时候,在函数模板中调用重载函数时只需要使用完美转发就可以保持推演出来的属性不变,右值引用仍然是右值,const右值引用也仍然是右值。
需要注意的是:
虽然右值不可以被修改,但是右值引用以后具有了左值属性,才能被转移,一旦被const修饰以后就无法转移了。所以我们在使用右值引用的时候,不要使用const来修饰。
在原来的C++类中,有6大默认成员函数:
1. 构造函数 2. 析构函数 3. 拷贝构造函数 4. 拷贝赋值重载 5. 取地址重载 6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的,而且完全符号我们使用的需求。
C++11中新增了两个:移动构造和移动赋值运算符重载,此时C++11一共有8个默认成员函数了。
这两个成员函数在前面已经介绍过了,这里站在默认成员函数的角度继续谈谈。
满足下列条件,编译器会自定生成移动构造函数:
- 没有自己显示定义移动构造函数
- 且没有实现析构函数,拷贝构造函数,拷贝赋值重载中的任何一个。
此时编译器会自定生成一个默认的移动构造函数。
- 默认生成的移动构造函数,对于内置类型会逐字节进行拷贝。
- 对于自定义类型,如果实现了移动构造就调用移动构造,没有实现就调用拷贝构造。
满足下列条件,编译器会自动生成移动赋值重载函数
- 自己没有显示定义移动赋值重载函数。
- 且且没有实现析构函数,拷贝构造函数,拷贝赋值重载中的任何一个。
此时编译器会自动生成一个默认移动赋值函数。
- 对于内置类型会按字节拷贝。
- 对于自定义类型,如果实现了移动赋值就调用移动赋值,如果没有实现就调用拷贝赋值。
创建一个类,屏蔽掉拷贝构造,拷贝赋值,以及析构函数,成员变量有一个是我们自己实现的string,里面有移动构造和移动赋值。
namespace rtx
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
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;
swap(s);
}
string& operator=(const string& s) // 拷贝赋值
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
string& operator=(string&& s) // 移动赋值
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
protected:
char* _str;
size_t _size;
size_t _capacity;
};
}
class Person
{
public:
//Person(const char* name = "", int age = 0)
// :_name(name)
// , _age(age)
//{}
//Person(const Person& p) // 拷贝构造
// :_name(p._name)
// , _age(p._age)
//{}
//Person& operator=(const Person& p) // 拷贝赋值
//{
// if (this != &p)
// {
// _name = p._name;
// _age = p._age;
// }
// return *this;
//}
//~Person()
//{}
protected:
rtx::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
此时Person就自动生成了移动构造函数,并且调用了string中的移动构造和移动赋值函数来构造string对象。
将Person中的拷贝构造,拷贝赋值,析构函数任意放出一个来。(这里只放出了析构)
使用右值构建string对象时,都会调用string的拷贝构造和拷贝赋值函数。
强制生成默认函数的关键字default:
这个default并不是switch中的default,而是C++11的新用法。
5.1里的代码:将Person中的拷贝构造,拷贝复制,析构函数都显示定义,此时就破坏了自动生成移动构造的条件。把Person里的注释放开,使用default强制生成默认的移动构造函数
从结果中可以看到,仍然调用了string中的移动构造函数,而不是调用的拷贝构造(深拷贝)。
禁止生成默认成员函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
C++98不生成默认成员函数的方法:直接一个分号
在Person类中不显示定义拷贝构造函数,拷贝复制函数,析构函数,此时符合自动生成默认移动构造的条件。 声明移动构造函数,但是没有定义。此时在编译的时候就会报错,这是C++98中的方式,利用链接时找不到函数的定义报错。
同样在编译时报错了。编译器会自动生成移动构造函数,但是此时使用了delete,编译器就会报错,告诉我们这里生成了移动构造。
这是为了在编译阶段就报错,而不是运行时再报错,实话讲,这个很鸡肋。
以前提到的一道题:
// 要求delete关键字实现,一个类,只能在堆上创建对象
class HeapOnly
{
public:
HeapOnly()
{
_str = new char[10];
}
~HeapOnly() = delete;
//void Destroy() // 如果要销毁只能这样
//{
// delete[] _str;
// operator delete(this);
//}
private:
char* _str;
//...
};
继承和多态中的final与override关键字
这两个关键字在继承和多态部分详细讲解过,这里不再详细讲解。
final
override
C++11中的很多东西虽然让C++越来越不像C++,比如列表初始化等内容,但是还是有一些非常有用的东西的:比如今天讲到的右值引用,和下一篇学的lambda表达式。
下一篇:从C语言到C++_34(C++11_下)可变参数+ lambda+function+bind+笔试题。