赞
踩
我们今天继续来学习C++11的特性:
在右值被引入之后,一些库和全局函数发生了变化,第一个是我们的全局函数:swap:
但是在C++11中:
实现了移动构造版本,大大提高了效率:
还有就是我们熟悉的各种STL容器,比如vector,list,map,set等等都实现了移动构造和移动赋值的版本:
还有其他的,这里就不一一列举了。
还有就是STL的容器的插入函数都实现了右值引用版本,这样在传参传值的时候都大大提升了效率。
之前我们说过move这个函数不会影响数据本身的属性,只会影响返回值的属性,并且返回值的属性是右值。
这就有一个问题:
我现在有一个简单的单链表,并且我完成了移动构造:
#pragma once #include<stdio.h> namespace Mylist { template <class T> //模板参数 struct listonde { T _data; //数据 listonde<T>* _next; //指向下一个结点 //构造函数 listonde(const T& data= T()): _data(data), _next(nullptr) { cout<<"Mylistnode----->"<<"构造函数"<<endl; } //移动构造 listonde(T&& data): _data(data), _next(nullptr) { cout<<"Mylistnode----->"<<"移动构造"<<endl; } }; template <class T> class list { public: typedef listonde<T> _Node; void empty_list() { _head = new _Node; //给头结点开辟空间 _head->_next = nullptr; } //构造函数 list() { empty_list(); } // 移动构造函数 list(list&& other) noexcept : _head(other._head) { // 重置其他对象的指针,避免悬挂指针 other._head = nullptr; } bool push_back(const T& data) { //给结点开辟新的空间 _Node* newnode = new _Node(data); if(newnode == nullptr) { perror("new fail"); return false; } // 假设_head始终指向一个哑元节点 _Node* current = _head; while (current->_next != nullptr) { current = current->_next; } current->_next = newnode; newnode->_next = nullptr; return true; } bool push_back(T&& data) { //给结点开辟新的空间 _Node* newnode = new _Node(data); if(newnode == nullptr) { perror("new fail"); return false; } // 假设_head始终指向一个哑元节点 _Node* current = _head; while (current->_next != nullptr) { current = current->_next; } current->_next = newnode; newnode->_next = nullptr; return true; } private: _Node* _head; }; }
这个时候我插入一个右值:
private:
_Node* _head;
};
void Test_1()
{
Mylist::list<int> head1;
head1.push_back(move(23));
}
这个时候,发生了怪事:
我们头结点是左值的析构函数,没问题,但是我不是push的是一个右值吗?咋还是左值的析构函数呢?
问题出在这里:
尽管我们这里传的是右值,但是还记得吗move之后data自身的属性是没变的,还是一个左值,所以,它会去调左值的构造:
这就解释了为啥明明传的右值,却没有调用移动构造。
这也向我们传达一个信息:右值引用本身是左值(即右值引用的对象),有点绕,我们举个例子:
int&& rref = 42; // rref 是右值引用本身,它绑定到右值 42 上
&rref; // 合法,因为 rref 是一个左值
// &42; // 不合法,42 是一个右值,不能取地址
这样大家应该就比较清楚了,但是应该怎样解决这个问题呢?所以我们在new的时候,要再转一次右值:
这样解决了我们的问题,
我们来看这样子的一个栗子:
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<typename T> void PerfectForward(T&& t) { Fun(t); //完美转发 } int main() { //Mylist::Test_1(); PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
运行结果:
也在我们的意料之中,但是如果我们使用move全都又会变成右值:
这不是我们想要的结果,我们想要保持实参的属性,这个时候我们就要用到完美转发:
这个函数会帮助我们完美保持实参的属性:
这个时候,我们再来看:
是不是很神奇:
我们可以用完美转发解决我们上面的问题:
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(都没有实现)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
namespace MyString { 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); } // 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(string&& s) { cout << "string(string&& s) -- 移动拷贝" << endl; swap(s); } // 赋值重载 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& operator=(string&& s) { cout << "string& operator=(string&& s)-- 移动赋值" << endl; swap(s); return *this; } ~string() { 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]; 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) 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 }; MyString::string to_string(int x) { MyString::string ret; while (x) { int val = x % 10; x /= 10; ret += ('0' + val); } reverse(ret.begin(), ret.end()); return ret; } } 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() {}*/ private: MyString::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); // Person s4; // s4 = std::move(s2); return 0; }
我们看到这里:
我们的s2会默认去调构造函数,然后s3,因为没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,自定义类型会去调用他自己的移动构造函数或者移动拷贝:
如果我们放开析构函数 、拷贝构造、拷贝赋值重载中的任意一个,就不会调用移动构造或者移动赋值:
如果一个类没有定义任何构造函数,编译器会自动生成一个默认构造函数。但是,如果类定义了其他构造函数,并且没有显式地定义默认构造函数,编译器就不会自动生成默认构造函数。在这种情况下,可以使用default关键字来显式地请求编译器生成默认构造函数:
比如说:
class MyClass {
public:
MyClass(int value) { /* ... */ }
MyClass() = default; // 显式请求默认构造函数
};
default可以帮助我们强制让编译器生成我们想要的函数(析构,移动构造,赋值等等…)。
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete; //删除拷贝赋值
private:
bit::string _name;
int _age;
};
在C++中,可变参数模板(variadic templates)是C++11引入的一项特性,它允许用户定义能够接受任意数量和类型的模板参数的函数、类或别名模板。通过使用可变参数模板,可以创建非常灵活和通用的代码结构。
可变参数模板的基本语法如下:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
可变参数模板可以接受任意类型的参数:
我们还可以计算它的大小:
不要小看这个问题,大家可能一开始会这么写:
我们是这样想的,但是人家不准我们这样用,要想拿出可变参数包中的元素,我们得这样写:
这里我们来解释一下:
如果嫌弃这样写太麻烦,C++17新增了折叠表达式:
在C++17及以后的版本中,折叠表达式(Fold Expressions)是一个新特性,它允许我们在编译时递归地展开参数包。折叠表达式主要有三种形式:左折叠、右折叠和二元折叠。
这里我们了解一下左折叠即可:
左折叠从参数包的第一个元素开始,依次向右展开。在逗号操作符的上下文中,左折叠看起来像这样:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
((cout<< " " <<args),...); //C++17
cout<<endl;
}
这里简单了解即可。注意的是,我们这里没有了模板参数First,是因为折叠表达式帮我们从第一个元素展开了,如果我们写了模板参数,那么args就会把自己的第一个元素拿个这个模板参数,然后剩下的元素放进args:
学了可变参数包后,我们再去看STL的话,发现会有这么一个函数:
这个函数和push_back是一样的效果,那么他们两个到底谁更好呢?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。