文章目录
C++11
相比于C++98,C++11则带来了数量可观的变化,以及对C++03缺陷的修正。C++11语法更加泛化简单化、更加稳定安全,功能更强大,提升开发效率。
1. 列表初始化
C++11扩大了用{}
(初始化列表)的使用范围,可用于所有的内置类型和自定义类型,可以省略赋值符=
。
// 内置类型变量
int x1 = {10};
int x2{10};//建议使用原来的,不推荐
int x3 = 1+2;
int x4 = ={1+2};
int x5={1+2};
// 数组
int arr1[5] ={1,2,3,4,5};
int arr2[]={1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]={1,2,3,4,5};
// 标准容器
vector<int> v={1,2,3,4,5};//这种初始化就很友好,不用push_back一个一个插入
map<int, int> m={{1,1}, {2,2,},{3,3},{4,4}};
C++11对容器也可以使用列表初始化,这到底是如何做到的呢?
vector (initializer_1ist<value_type> il);
vector<int> v1 = { 1,2,3,4,5,6 };
map<int, int> m={{1,1}, {2,2,},{3,3},{4,4}};
{}
的常量数组的类型被C++解释为初始化列表initializer_list
。它的底层就是用常量区数组存储列表中的内容。
template<class T>
class initializer_list;
auto il = { 10, 20, 30 };
initializer_list<int> il = { 10, 20, 30 };
cout << typeid(i1).name() << endl;//initializer_list
如何为自定义类型实现列表初始化呢?
initializer_list
类型支持begin
和end
接口。
- 自定义类型中添加初始化列表构造函数
- 在其中用初始化列表的迭代器调用迭代器区间构造函数。
template<class T>
class Vector {
public:
typedef T* Iterator;
Vector(std::initializer_list<T> list) {
_start = new T[list.size()];
_finish = _start + list.size();
_endOfStorage = _start + list.size();
Iterator vt= _start;
typename std::initializer_list<T>::iterator lt = list.begin();
while (lt != list.end()) {
*vt++ = *lt++;
}
/*
for(auto e:list)
{
*vt++=e;
}
*/
}
Vector<T>& operator=(std::initializer_list<T> list) {
Vector<T> temp(list);
std::swap(_start, temp._start);
std::swap(_finish, temp._finish);
std::swap(_endOfStorage, temp._endOfStorage);
return *this;
}
private:
T* _start;
T* _finish;
T* _endOfStorage;
};7
2. 变量类型推导
2.1 auto
C++11定义变量时,auto用于自动类型推导,让编译器自动推导变量的类型,使用auto更加便捷省时。
auto i = 0;
cout << typeid(i).name() << endl;
2.2 decltype
关键字decltype
使用表达式的类型声明一个新的变量。
// 使用变量的类型创建新变量
decltype(x) i = 1;
decltype(x * y) i = 1;
int(*pfunc1)(int) = &func; // 类型过于复杂,使用decltype获取类型
decltype(func) pfunc2;
3. 右值引用和移动语义
右值引用和移动语义是C++11中最重要的更新,在根本上减少拷贝,提升效率。
3.1 左值引用和右值引用
左值右值
- 左值是一个表达式,如变量名或解引用的指针。一般指表达式结束依然存在的持久对象。
- 右值是一个表达式,如字面常量、表达式返回值、函数返回值。一般指表达式结束就不存在的临时对象。
左值特点 | 右值特点 |
---|---|
左值可以取地址可以赋值 | 右值不可被取地址不可赋值 |
左值可以出现在赋值符的左右 | 右值只能出现在赋值符的右边 |
左值就是变量,右值就是常量,不完全对。
可以取地址的就是左值,不可以取地址的就是右值。
右值的分类
- 将亡值:指生命周期即将结束的值,通常是将要被销毁被移动的对象。
- 纯右值:值返回的临时对象、表达式运算产生的临时对象、字面常量和lambda表达式等。
左右值引用
左值引用就是给左值取别名,右值引用就是给右值取别名。左值引用用&
表示,右值引用使用&&
表示。
给右值取别名后,该右值会被当作变量存储在内存中,且可以取地址。
// 左值引用
int& ra = a;
int& rp = *p;
const int& rb = b;
// 右值引用
int&& r1 = 10; // 字面常量
int&& r2 = x + y; // 表达式运算的临时对象
int&& r3 = func(x, y); // 值返回的临时对象
交叉引用
const int& x1 = 10; // 左值引用 引用 右值
int*&& x2 = std::move(p); // 右值引用 引用 左值
- 左值引用不能直接引用右值,但
const
常引用可以。 - 右值引用不能直接引用左值,但可以引用
std::move
后的左值。
总结
左值引用和右值引用都旨在减少拷贝以提高效率,但左值引用是直接减少拷贝,而右值引用则是间接减少拷贝间接减少拷贝,识别出是左值还是右值,若识别出是右值,则不再深拷贝,直接移动拷贝(资源转移),提高效率。左值引用适用于需要保留原对象的情况,而右值引用则适用于不需要保留原对象,或者原对象不再需要时的情况,能够实现资源的有效转移,从而提高代码的效率和性能。
3.2 移动构造和移动赋值
移动构造
左值引用无法解决的问题有两点:局部对象返回,接口传参对象拷贝。
右值引用作参数(纯右值)
编译器可以识别表达式是左值还是右值。因此传入的不同属性的表达式会进入不同的构造函数。
移动构造就是单独拎出右值的情况来优化,具体如何进行资源转移还取决于代码。
移动构造减少拷贝的前提是编译器支持识别右值,这便是右值引用的意义。
//拷贝构造
string(const string& s)
: _size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
//移动构造
string(string&& s)
: _size(0)
, _capacity(0)
{
swap(s); // 资源转移
}
string ret1 = s1; // 拷贝构造
string ret2 = s1 + s2; // 移动构造
**
move()
**也就是将左值强制转化为右值引用,然后直接转移其资源。
右值引用作返回值(将亡值)
什么是将亡值?
在C++11中,将亡值是与右值引用密切相关的新概念。将亡值表达式包括:
- 返回右值引用的函数调用表达式
- 转换为右值引用的转换函数的调用表达式
当一个右值准备完成初始化或赋值任务时,它已经“将亡”,这就是将亡值的含义。它常用于移动构造或移动赋值的特殊任务,扮演着资源转移的角色。
将亡值的销毁过程
值返回函数会构造出一个即将销毁的临时对象用来返回,编译器会将临时对象视为将亡值,会调用移动构造来构造对象。
string func()
{
string s("hello");
return s;
}
int main()
{
string ret = func();
return 0;
}
-
如果编译器完全不做优化,上述代码应该有两次拷贝:
- 使用
s
拷贝构造出一个临时对象tmp
以供返回。 - 使用临时对象
tmp
拷贝构造ret
以接收返回值。
- 使用
-
一般C++98编译器,会将连续两次的拷贝构造,优化成一次:
- 直接用
s
拷贝构造ret
。
- 直接用
-
C++11编译器支持移动构造后,做到一般优化:
- 栈变量
s
是左值,拷贝构造出临时对象tmp
。 - 临时对象
tmp
是将亡值,再调用移动构造,转移tmp
资源到ret
中。
- 栈变量
-
C++11编译器支持移动构造后,做到最大优化:
- 栈变量
s
会被识别成将亡值,直接调用移动构造,转移s
的资源到ret
中。
- 栈变量
移动构造和拷贝构造对比图
移动赋值
类默认成员函数新增一个移动赋值,移动赋值的参数是对象右值引用。当用右值对象赋值给其他对象时,会调用移动赋值。
// 移动赋值
string& operator=(string&& s)
{
swap(s);
return *this;
}
用已经存在的对象接受值返回函数返回时:
- 先用将亡值对象
s
构移动构造出一个临时对象tmp
, - 再用临时对象
tmp
移动赋值给这个已经存在的对象ret
。
只有连续的构造可以合二为一,其他不行。
C++11后STL所有容器也新增了移动构造和移动赋值,以及插入接口也新增了右值引用版本。
3.4 万能引用和完美转发
万能引用
template <class T>
void PerfectForward(T&& t) /* 万能引用 */
{}
&&
放在具体类型的后面代表右值引用,放在模版类型后面叫做万能引用或引用折叠。
万能引用既能接收左值也能接收右值。
完美转发
右值本身是占据空间的,右值引用后会变成左值。因为我们需要能够修改它,转移它的资源。
也就是说,右值引用做参数时会丢失右值属性。如果要维持属性,需要传参时使用完美转发std::forward()
。
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const右值引用" << endl; }
template <class T>
void ImperfectForward(T&& t) {
Func(t);
}
template <class T>
void PerfectForward(T&& t) {
Func(t);
//Func(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 右值
}
运行一下,跟实际情况不一样
使用完美转发,保留变量值属性
Func(std::forward<T>(t)); /* 完美转发 */
为什么会这样呢?
写一个函数 ,无论传过来的参数为左值还是右值,都可以接受 (将左值move后,返回值为右值)
当左值作为参数 时, 会发生引用折叠,调用 fun(t),此时t作为左值,所以会输出 左值引用
当右值作为参数时,实际上右值接收后,要进行移动拷贝,右值引用 引用后属性会变成左值,否则无法进行资源转移
库或者自行实现的各种容器的右值插入也要支持完美转发。
void push_back(T&& x)
{
insert(end(), std::forward<T>(x));
}
iterator insert(iterator pos, T&& x)
{
list_node* prev = pos._node->_prev;
list_node* next = pos._node;
list_node* new_node = new list_node(std::forward<T>(x));
new_node->_prev = prev;
prev->_next = new_node;
new_node->_next = next;
next->_prev = new_node;
return iterator(new_node);
}
__list_node<T>(T&& t)
: _data(std::forward<T>(t))
{}
4 默认成员函数
4.1 默认成员函数控制
任何事物的出现都必然有着其出现的理由,伴随着每一个新的概念产生都会带来一系列的便利和价值。C++在不断的演变与发展,与此同时,伴随着许多新的特性和功能产生。=default、=delete 是C++11的新特性,分别为:显式缺省(告知编译器生成函数默认的缺省版本)和显式删除(告知编译器不生成函数默认的缺省版本)
拷贝构造也是构造,如果只实现拷贝构造,编译器也是不会生成默认构造的。
- 在默认构造函数声明后加
=default
,可以指示编译器生成该函数的默认版本。 - 相反,加上
=delete
可以避免生成该函数的默认版本。
class A
{
public:
A() = default;
A(const A& a);
A operator=(const A& a) = delete;
private:
// ...
};
C++98没有这样的关键字,那就必须将构造函数至声明不实现并私有化,能防止类外使用和类外实现。
4.2 新增默认成员函数
C++98有六个默认成员函数:构造函数、拷贝构造、拷贝赋值、析构函数以及取地址符重载。C++11新增两个:移动构造、移动赋值。
移动构造的特性
- 没有实现移动构造,且没有实现析构函数、拷贝构造和拷贝赋值,那编译器会生成默认移动构造。
- 默认生成的移动构造,对内置类型会逐字节拷贝,对自定义类型如果内部有移动构造就调移动构造,没有就调拷贝构造。
移动赋值的特性
- 如果没有实现移动赋值,且没有实现析构函数、拷贝构造和拷贝赋值,那编译器会生成默认移动赋值。
- 默认生成的移动赋值,对内置类型会逐字节拷贝,对自定义类型如果内部有移动赋值就调移动赋值,没有就调拷贝赋值。
默认移动构造和移动赋值的生成规则和成员处理规则一致。
//test
class Person {
public:
Person(const char* name = "", int age = 18) : _name(name), _age(age)
{}
// #define kb 1
#ifdef kb
Person(const Person& p) : _name(p._name), _age(p._age)
{}
Person operator=(const Person& p) {
if (this == &p) {
Person tmp(p);
return tmp;
}
return *this;
}
~Person() {}
#endif
private:
test::string _name;
int _age;
};
int main()
{
Person p1("hello", 18);
Person p2 = std::move(p1); // 移动构造
p1 = std::move(p2); // 移动赋值
}