【C++进阶篇】C++11新特性(中篇)
这里写目录标题
- 一. 右值引用
- 1.1 什么是右值与左值
- 1.2 左值引用和右值引用
- 1.2.1 move
- 1.3 右值使用场景
- 1.4 右值引用意义
- 二. 完美转发
- 2.1 模版中的万能引用
- 2.2 实际应用
- 三. 新增类功能
- 3.1 默认的移动构造和移动赋值
- 3.2 新增关键字
- 四. 可变参数
- 4.1 可变参数包
- 4.2 可变参数包解析
- 4.3 emplace系列函数
- 五. 最后
一. 右值引用
右值引用本质就是移动(挪动)资源,原因在于右值一般是马上要被销毁的资源,同时在函数返回值中可以减少拷贝问题。
1.1 什么是右值与左值
右值一般包括字面值常量,创建临时对象,如10,fmin(x,y),iterator(cur,nullptr)等,具有临时性。
左值是一个变量名,解引用指针如int a=1,*p=10等,一般具有持久性。
- 不同点:左值可以取地址,右值不可以取地址。
1.2 左值引用和右值引用
两个引用都是取别名,前者是给左值取别名,后者是给右值取别名,左值引用不可以直接引用右值,原因:右值具有常性,涉及权限放大,可以使用const左值引用可以引用右值。
右值引用不可以直接引用左值,右值引用可以引用move(左值)。
结论:引用和指针在汇编实现都是一样的。
1.2.1 move
move是一个函数模版,本质就是类型强制转换。
template <class T>
typename remove_reference<T>::type&& move (T&& arg);
虽然右值引用引用是右值,但&rr是允许的,因为右值引用本身是左值,左值是允许取地址的,右值是可以对该临时资源进行修改的。
int a = 10;//左值引用 引用 右值
const int& a = 20;//右值引用 引用 左值
int&& rr = move(a);
给右值加上const 就不允许修改它了。
注意:使用move左值语句后,可能会导致左值中的资源被挪动,所以在使用该move前须确保左值的资源不再使用。
string str = "hello C++!";
cout << "str: " << str << endl;string rr = move(str);
cout << "str: " << str << endl;
输出结果:
1.3 右值使用场景
右值分为:纯右值和将亡值(说白了就是马上要被销毁的资源,与其它被销毁,不如二次回收,将所要被销毁的资源转移给需要的其它系统等)。将亡值的意义很大,尤其体现在拷贝场景下。
下面通过场景分析一下右值的用处。
下面是自己模拟实现的string类,未实现移动构造及移动赋值。
class string
{
public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{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);}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;reserve(s._capacity);for (auto ch : s){push_back(ch);}}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷贝赋值" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}~string(){cout << "~string() -- 析构" << endl;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];if (_str){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;}const char* c_str() const{return _str;}string operator+(char ch){string tmp(*this);tmp += ch;return tmp;}size_t size() const{return _size;}
private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};
main.cpp
int main()
{A::string str= "hello!";//str为左值A::string s = str;//str + '\n'为右值A::string s1 = str + '\n';return 0;
}
输出结果:
发生了三次构造,一次是"hello"构造,一次是s的构造,另一次是s1的构造。
给上述代码假如移动构造。
// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}
在运行代码,结果如下:
1.4 右值引用意义
右值引用是个非常奇妙的想法,它可以利用临时资源,避免低效率拷贝问题。
- 左值引用:直接引用对象资源,本身该对象资源仍然存在。
- 右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝
二. 完美转发
2.1 模版中的万能引用
泛型编程的核心在于 模版根据传入参数类型推导函数,当我们分别传入左值引用,右值引用时,模版是否能正确推导?
下面这段代码的含义是 分别传入 左值、const 左值、右值、const 右值,并设计对应参数的回调函数,将参数传给模板,看看模板是否能正确回调函数。
void func(int& a)
{cout << "func(int& a) 左值引用" << endl;
}void func(const int& a)
{cout << "func(const int& a) const 左值引用" << endl;
}void func(int&& a)
{cout << "func(int&& a) 右值引用" << endl;
}void func(const int&& a)
{cout << "func(const int&& a) const 右值引用" << endl;
}template<class T>
void perfectForward(T&& val)
{// 调用函数func(val);
}int main()
{int a = 10;const int b = 10;// 左值perfectForward(a);perfectForward(b);// 右值perfectForward(move(a));perfectForward(move(b));return 0;
}
- 输出结果:
从结果可以看出全是左值相关的func函数,模版出问题了吗???实则不然。
模版是不会出现问题的,问题必出现在模版传入参数的属性上。val不管是左值,还是右值,传给func函数后都是左值属性了,左值传给func不言而喻了,为啥右值传给func后,属性变成左值???右值引用后的变量是具有指向资源和地址的,它本质就是左值,因为右值不可以取地址,左值可以取地址。这样就说的通了。如何保持保持值得属性,不改变它,这就需要使用完美转发,完美转发本质是函数模版。即forward函数。
template<class T>
void perfectForward(T&& val)
{// 调用函数func(forward<T>(val));
}
运行结果:
可以发现结果与预期相同。
注意:forward为函数模版,需制定函数模版类型T,确保参数正确传递。
2.2 实际应用
完美转发实际应用中较为广泛,特别是在链表中,可以规避较多无意义的拷贝行为。
下面来看看它的实际应用。
#pragma once
#include<assert.h>namespace A
{template<class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _data;list_node(const T& x = T()):_next(nullptr), _prev(nullptr), _data(x){}};template<class T, class Ref, class Ptr>struct __list_iterator{typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> self;node* _node;__list_iterator(node* n):_node(n){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}};template<class T>class list{typedef list_node<T> node;public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;iterator begin(){return iterator(_head->_next);}const_iterator begin() const{return const_iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator end() const{return const_iterator(_head);}void empty_init(){_head = new node(T());_head->_next = _head;_head->_prev = _head;}list(){empty_init();}template <class Iterator>list(Iterator first, Iterator last){empty_init();while (first != last){push_back(*first);++first;}}void swap(list<T>& tmp){std::swap(_head, tmp._head);}list(const list<T>& lt){empty_init();list<T> tmp(lt.begin(), lt.end());swap(tmp);}list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){//it = erase(it);erase(it++);}}void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}void insert(iterator pos, const T& x){node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(x);prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;}iterator erase(iterator pos){assert(pos != end());node* prev = pos._node->_prev;node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;return iterator(next);}private:node* _head;};
}
主函数中只需创建一个 list 对象,,查看 移动构造是否被正确调用
测试移动构造是否被正确调用
int main()
{A::list<A::string> l;l.push_back("Hello World!");return 0;
}
运行结果:为两次深拷贝
第一次是构造本身构造,第二次是插入时传参的构造。
// 右值引用版
void push_back(T&& x)
{// 完美转发insert(end(), std::forward<T>(x));
}
// 右值引用版
void insert(iterator pos, T&& x)
{node* cur = pos._node;node* prev = cur->_prev;// 完美转发node* new_node = new node(std::forward<T>(x));prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;
}
list_node(T&& x):_next(nullptr), _prev(nullptr), _data(x)
{}
要想让我们之前模拟实现的 list 成功进行 移动构造,需要增加:一个移动构造、两个右值引用版本的函数、三次完美转发,并且整个 完美转发 的过程是层层递进、环环相扣的,但凡其中有一层没有进行 完美转发,就会导致整个传递链路失效,无法触发 移动构造。但凡任何一层有左值属性,直接诶调用拷贝构造。
三. 新增类功能
3.1 默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重
载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器
会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
生成条件:
- 如果没有显示实现移动构造函数,且没有显示实现析构函数,拷贝构造函数,拷贝赋值重载任意一个。那么编译器会自动生成一个移动构造函数,默认生成的移动构造函数对于内置类型成员会逐成员按字节拷贝,自定义类型成员,看该成员是否实现移动构造,如果实现调用移动构造,没有就调用拷贝构造。
- 如果没有显示实现移动赋值重载函数,且没有实现析构函数,拷贝构造函数,拷贝赋值重载中任意一个函数,那么编译器会自动生成一个默认移动赋值函数,默认生成的移动赋值函数对于内置类型逐成员按字节拷贝,自定义类型成员,看该成员是否实现移动赋值函数,如果实现调用移动赋值重载,没有就调用拷贝赋值。
- 显示实现移动构造或移动赋值,编译器不再主动生成拷贝构造和拷贝赋值。
如何实现移动语义函数呢???
// 移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{swap(s);
}// 移动赋值
string& operator=(string&& s)
{swap(s);return *this;
}
- 移动语义是否能延长临时对象(将亡值)的生命周期?
移动语义本身不会直接延长临时对象(将亡值)的生命周期,但通过特定机制(如绑定到右值引用)可以间接实现生命周期的延长。
- const引用延长生命周期问题
#include<iostream>
using namespace std;int Add(int& x, int& y)
{int m = x + y;return m;
}
int main()
{int a = 10, b = 30;const int& ret = Add(a, b);//临时对象被绑定到了ret对象上,临时对象与ret对象共存亡。//被const修饰的临时对象不可修改//ret += 2;//errorreturn 0;
}
const引用不可以延长局部对象的生命周期,局部对象在函数早就被销毁了,不可访问。
3.2 新增关键字
default 关键字
功能:显式要求编译器生成默认的特殊成员函数(如默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等)。因为一些特殊原因导致默认函数没有生成,我们的确想要它,就可以使用default关键字,强制编译器生成。
class MyClass {
public:MyClass() = default; // 显式要求编译器生成默认构造函数MyClass(int x) {} // 自定义构造函数// 编译器会自动生成析构函数、拷贝构造函数等(除非被删除)
};
delete关键字
功能:禁用类的某些特殊成员函数(如禁用拷贝构造、赋值运算符等),或禁用特定函数重载。在Linux中就见过单例类模式,只允许生成有一个类对象,此时我们需要将拷贝构造,赋值等具有生成对象功能的函数禁用,就可以使用delete关键字。
class NonCopyable {
public:NonCopyable() = default;NonCopyable(const NonCopyable&) = delete; // 禁用拷贝构造NonCopyable& operator=(const NonCopyable&) = delete; // 禁用拷贝赋值
};// 禁止将int隐式转换为类类型
void func(double) {}
void func(int) = delete; // 调用func(1)会编译错误
额外:声明成员变量时,是可以给缺省值的,不给则是随机值。
手动简单实现一个Date类,此时成员变量没有给缺省值。
class Date
{
public:Date(){//cout << "Date()" << endl;}~Date(){//cout << "~Date()" << endl;}int GetYear(){return _year;}private:int _year;
};int main()
{Date d1;cout << "d1.GetYear() = " << " " << d1.GetYear() << endl;return 0;
}
输出结果:
可以看出是随机值。
此时我们给成员变量 _year 一个缺省值,设置为2025。再看下结果。
int _year = 2025;
输出结果:
随机值的危害:导致逻辑错误,代码维护性下降 等。建议:声明成员变量时,给缺省值。
四. 可变参数
可变参数允许程序员传递任意个参数,函数都可以接收和作出相应的行为。C语言的scanf,printf函数就设计可变参数,需要用户指定数据类型及参数数量,这样使用相当复杂和繁琐,假如我有100个甚至上万个参数呢,都显示写吗???显然太烦杂了,此时C++11引用可变参数模版,将上述繁琐工作交给编译器去完成。
4.1 可变参数包
求参数包个数:sizeof…(参数包名)
//sizeof...运算符去计算参数包中参数的个数
template <class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}
int main()
{double x = 2.2;Print(); // 包⾥有0个参数Print(1); // 包⾥有1个参数Print(1, string("xxxxx")); // 包⾥有2个参数Print(1.1, string("xxxxx"), x); // 包⾥有3个参数return 0;
}
可变参数包使用万能引用模版,既可以引用左值,又可以引用右值。
4.2 可变参数包解析
通过递归展开参数包,逐个处理每个参数,直至参数包为空时终止递归。解析可变参数包的核心方法是模板递归和折叠表达式(C++17)。
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}
int main()
{Print();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);return 0;
}
输出结果:
图解:
注意:不可以递归这样操作:
if (args[0] == 0) return;
4.3 emplace系列函数
emplace系列函数增加参数为可变参数模版的版本函数。
emplace系列函数通过完美转发(Perfect Forwarding)将参数直接传递给元素的构造函数,在容器内存中直接构造对象,而非先创建临时对象再拷贝/移动到容器中。
总之:emplace函数功能比push_back函数更高效。
最佳实践:合理使用emplace系列函数可显著提升性能,但需权衡代码可读性与构造参数的明确性。在元素构造简单或需兼容旧代码时,insert系列函数可能更直观。
五. 最后
本文深入解析C++11核心特性:右值引用通过移动语义减少拷贝开销,结合move实现资源所有权转移;完美转发利用模板万能引用与std::forward保留参数值类别,优化链表等场景性能;新增类功能如默认移动语义、default/delete关键字提升代码健壮性;可变参数包与emplace函数通过模板递归和折叠表达式实现类型安全的高效参数处理。这些特性共同推动C++向零开销抽象迈进,适用于高性能库开发与资源敏感场景。