【C++】31.C++11(3)
文章目录
- 5. 新的类功能
- 5.1 默认的移动构造和移动赋值
- 5.2 成员变量声明时给缺省值
- 5.3 defult和delete
- 5.4 final与override
- 6. 可变参数模板
- 6.1 基本语法及原理
- 6.2 包扩展
- 6.3 empalce系列接口
- list实现empalce
5. 新的类功能
5.1 默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
class Person
{
public:
// 构造函数
// const char* name = "" 默认参数为空字符串
// int age = 0 默认参数为0
// 初始化列表初始化成员变量_name和_age
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 拷贝构造函数
// 参数为const引用,避免修改传入对象
// 初始化列表完成深拷贝
/*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:
bit::string _name; // 姓名,string类型
int _age; // 年龄,整型
};
int main()
{
Person s1; // 调用构造函数,默认构造
Person s2 = s1; // 调用拷贝构造(编译器生成的默认版本)
Person s3 = std::move(s1); // 调用移动构造(编译器生成的默认版本)
Person s4; // 调用构造函数,默认构造
s4 = std::move(s2); // 调用移动赋值运算符(编译器生成的默认版本)
return 0;
}
5.2 成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个却绳子初始化,这个在类和对象部分讲过了,忘了就去复习。
5.3 defult和delete
- C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
- 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
// 构造函数
// const char* name = "" 默认参数为空字符串
// int age = 0 默认参数为0
// 使用参数构造bit::string对象_name,直接赋值给_age
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 拷贝构造函数
// 对传入对象p进行深拷贝
// 会调用bit::string的拷贝构造函数完成_name的深拷贝
// _age直接赋值拷贝
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
// 移动构造函数
// = default表示使用编译器生成的默认版本
// 会调用bit::string的移动构造函数,转移_name的资源所有权
// _age直接按位拷贝
Person(Person&& p) = default;
// 如果打开这个注释,会禁用拷贝构造
// 禁用后,对象将不能进行拷贝构造,只能进行移动构造
//Person(const Person& p) = delete;
private:
bit::string _name; // 姓名
int _age; // 年龄
};
int main()
{
Person s1; // 调用默认构造函数
Person s2 = s1; // 调用拷贝构造函数,深拷贝s1
Person s3 = std::move(s1); // 调用移动构造函数,转移s1的资源
// 转移后s1的_name应该被置空
return 0;
}
5.4 final与override
这个在继承和多态章节已经进行了详细讲过了,忘了就去复习。
6. 可变参数模板
6.1 基本语法及原理
- C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
- 我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,
class...
或typename...
指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟…指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这里我们可以使用
sizeof...
运算符去计算参数包中参数的个数。
// 可变参数模板函数
// Args&&... 是参数包,可以接受任意数量、任意类型的参数
// Args&&是万能引用,可以接受左值和右值
template <class ...Args>
void Print(Args&&... args)
{
// sizeof...(args)返回参数包中参数的个数
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 参数包为空
Print(1); // 参数包含一个右值1
Print(1, string("xxxxx")); // 参数包含右值1和右值string
Print(1.1, string("xxxxx"), x); // 参数包含右值1.1、右值string和左值x
return 0;
}
// 原理1:编译器会根据实际调用展开成以下具体函数
// 根据引用折叠规则:
// - 右值传入时,T&&折叠为右值引用
// - 左值传入时,T&&折叠为左值引用
void Print(); // 对应Print()
void Print(int&& arg1); // 对应Print(1)
void Print(int&& arg1, string&& arg2); // 对应Print(1, string("xxxxx"))
void Print(double&& arg1, string&& arg2, double& arg3); // 对应Print(1.1, string("xxxxx"), x)
// 原理2:如果没有可变参数模板,就需要手动写很多重载版本
// 为了支持不同数量的参数,需要写很多函数模板
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T2, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ... 需要写更多重载来支持更多参数
// 可变参数模板的优势:
// 1. 一个模板就能处理任意数量参数
// 2. 通过参数包和引用折叠,实现了完美转发
// 3. 大大减少了代码量
6.2 包扩展
- 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作。底层的实现细节如图1所示。
- C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
代码1:
// ================== 不可行的实现方式 ==================
template <class ...Args>
void Print(Args... args)
{
// 这种实现方式是错误的,原因:
// 1. 可变参数模板是在编译时展开的
// 2. 无法在运行时通过下标访问参数包
// 3. args不是一个数组,不支持下标访问
cout << sizeof...(args) << endl; // sizeof...是编译时操作符
for (size_t i = 0; i < sizeof...(args); i++)
{
cout << args[i] << " "; // 错误:参数包不支持下标访问
}
cout << endl;
}
// ================== 正确的递归实现 ==================
// 递归终止函数
// 当参数包为空时的特化版本
void ShowList()
{
cout << endl; // 仅打印换行
}
// 递归展开函数
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " "; // 处理第一个参数
ShowList(args...); // 递归处理剩余参数
// args... 是参数包展开,将剩余参数传递给下一次递归调用
}
// 包装函数,提供更简洁的接口
template <class ...Args>
void Print(Args... args)
{
ShowList(args...); // 转发所有参数到ShowList
}
int main()
{
// 测试各种参数数量的调用
Print(); // 调用空参数版本
Print(1); // 调用一个参数版本
Print(1, string("xxxxx")); // 调用两个参数版本
Print(1, string("xxxxx"), 2.2); // 调用三个参数版本
return 0;
}
// ================== 编译器展开示例 ==================
// 对于 Print(1, string("xxxxx"), 2.2) 的调用
// 编译器会将模板展开为以下具体函数:
// 第一层展开
void ShowList(int x, string y, double z)
{
cout << x << " "; // 打印1
ShowList(y, z); // 继续递归
}
// 第二层展开
void ShowList(string x, double z)
{
cout << x << " "; // 打印"xxxxx"
ShowList(z); // 继续递归
}
// 第三层展开
void ShowList(double x)
{
cout << x << " "; // 打印2.2
ShowList(); // 调用终止函数
}
// 最终调用基础版本
void ShowList()
{
cout << endl; // 打印换行,结束递归
}
// Print函数相应展开为:
void Print(int x, string y, double z)
{
ShowList(x, y, z); // 调用展开后的ShowList
}
代码2:
// 特化版本:处理无参数的情况
void CppPrint()
{
cout << endl; // 只打印换行
}
// 辅助函数:打印单个参数并返回0
// 返回0是为了配合数组初始化方式展开参数包
template <class T>
int PrintArg(T t)
{
cout << t << " "; // 打印参数
return 0; // 返回0用于数组初始化
}
// 可变参数模板函数
// 使用数组初始化的方式展开参数包
template <class ...Args>
void CppPrint(Args... args)
{
// 这行代码完成了参数包的展开
// PrintArg(args)... 会展开为 PrintArg(arg1), PrintArg(arg2), ...
// 使用数组初始化列表,确保按顺序展开和执行
int a[] = { PrintArg(args)... };
cout << endl; // 最后打印换行
}
int main()
{
// 测试不同数量参数的调用
CppPrint(); // 调用无参数版本
CppPrint(1); // 调用一个参数版本
CppPrint(1, 2); // 调用两个参数版本
CppPrint(1, 2, 2.2); // 调用三个参数版本
CppPrint(1, 2, 2.2, string("xxxx")); // 调用四个参数版本
return 0;
}
代码3:
// Date类定义
class Date
{
public:
// 构造函数,使用默认参数
// 支持传入0-3个参数的构造
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date构造" << endl;
}
// 拷贝构造函数
// 用于通过已有Date对象构造新对象
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
// 可变参数模板工厂函数
// 用于创建Date对象,可接收0-3个参数
template <class ...Args>
Date* Create(Args... args)
{
// 将参数包完美转发给Date的构造函数
Date* ret = new Date(args...);
return ret;
}
int main()
{
// 测试不同参数数量的对象创建
Date* p1 = Create(); // 调用无参构造:Date(1, 1, 1)
Date* p2 = Create(2023); // 调用单参构造:Date(2023, 1, 1)
Date* p3 = Create(2023, 9); // 调用双参构造:Date(2023, 9, 1)
Date* p4 = Create(2023, 9, 27); // 调用三参构造:Date(2023, 9, 27)
Date d(2023, 1, 1); // 创建一个Date对象
Date* p5 = Create(d); // 通过拷贝构造创建新对象
return 0;
}
6.3 empalce系列接口
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position, Args&&... args);
- C++11以后STL容器新增了
empalce
系列的接口,empalce
系列的接口均为模板可变参数,功能上兼容push
和insert
系列,但是empalce
还支持新玩法,假设容器为container<T>
,empalce
还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。emplace_back
总体而言是更高效,推荐以后使用emplace
系列替代insert
和push
系列- 第二个程序中我们模拟实现了
list
的emplace
和emplace_back
接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce
支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。- 传递参数包过程中,如果是
Args&&... args
的参数包,要用完美转发参数包,方式如下std::forward<Args>(args)...
,否则编译时包扩展后右值引用变量表达式就变成了左值。
代码1:
#include<list>
int main()
{
list<bit::string> lt;
// 1.传入左值的情况
bit::string s1("111111111111");
lt.emplace_back(s1); // 会调用拷贝构造,和push_back效果一样
cout << "*********************************" << endl;
// 2.传入右值的情况
lt.emplace_back(move(s1)); // 会调用移动构造,和push_back效果一样
cout << "*********************************" << endl;
// 3.直接传入构造string需要的参数
lt.emplace_back("111111111111"); // 直接在list节点空间构造string对象
// 避免了先构造临时对象再拷贝/移动的过程
// 这是push_back做不到的
cout << "*********************************" << endl;
// 4.对于复杂类型(pair)的测试
list<pair<bit::string, int>> lt1;
// 4.1 传入左值pair
pair<bit::string, int> kv("苹果", 1);
lt1.emplace_back(kv); // 会调用pair的拷贝构造,和push_back效果一样
cout << "*********************************" << endl;
// 4.2 传入右值pair
lt1.emplace_back(move(kv)); // 会调用pair的移动构造,和push_back效果一样
cout << "*********************************" << endl;
// 4.3 直接传入构造pair需要的参数
lt1.emplace_back("苹果", 1); // 直接在list节点空间构造pair对象
// 避免了先构造临时pair再拷贝/移动的过程
// 这是push_back做不到的
cout << "*********************************" << endl;
return 0;
}
list实现empalce
List.h
namespace bit
{
// 链表节点结构
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
// 移动构造版本的节点构造函数
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(move(data)) // 使用移动语义构造数据
{}
// 完美转发构造函数,支持任意参数包构造数据成员
template <class... Args>
ListNode(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...) // 完美转发参数包到数据成员的构造函数
{}
};
// 迭代器类,支持不同类型的引用和指针
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node)
:_node(node)
{}
// 前置++运算符重载
Self& operator++()
{
_node = _node->_next;
return *this;
}
// 前置--运算符重载
Self& operator--()
{
_node = _node->_prev;
return *this;
}
// 解引用运算符重载
Ref operator*()
{
return _node->_data;
}
// 不等运算符重载
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
// list类的实现
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// 返回指向第一个节点的迭代器
iterator begin()
{
return iterator(_head->_next);
}
// 返回指向哨兵节点的迭代器
iterator end()
{
return iterator(_head);
}
// 初始化空链表(创建哨兵节点)
void empty_init()
{
_head = new Node(); // 创建哨兵节点
_head->_next = _head; // 首尾相连
_head->_prev = _head;
}
// 默认构造函数
list()
{
empty_init();
}
// 尾插左值
void push_back(const T& x)
{
insert(end(), x);
}
// 尾插右值
void push_back(T&& x)
{
insert(end(), move(x));
}
// 在指定位置插入左值
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// 连接节点
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// 在指定位置插入右值
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// 连接节点
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// emplace_back的可变参数模板版本
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
// 在指定位置直接构造插入的可变参数模板版本
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
// 直接在节点位置构造对象
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// 连接节点
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
private:
Node* _head; // 指向哨兵节点的指针
};
}
Test.cpp
#include"List.h"
int main()
{
bit::list<bit::string> lt;
// 测试1:传入左值
bit::string s1("111111111111");
lt.emplace_back(s1); // 参数是左值,会调用拷贝构造
// 这种情况下emplace_back和push_back性能一样
cout << "*********************************" << endl;
// 测试2:传入右值
lt.emplace_back(move(s1)); // 参数是右值,会调用移动构造
// 这种情况下emplace_back和push_back性能一样
cout << "*********************************" << endl;
// 测试3:直接传入构造string需要的参数
lt.emplace_back("111111111111"); // 直接在节点空间构造string对象
// 避免了先构造临时string对象,再拷贝/移动的过程
// 这是push_back做不到的,更高效
cout << "*********************************" << endl;
// 测试4:对复杂类型pair的操作
bit::list<pair<bit::string, int>> lt1;
// 测试4.1:传入左值pair
pair<bit::string, int> kv("苹果", 1);
lt1.emplace_back(kv); // 会调用pair的拷贝构造
// 和push_back效果一样
cout << "*********************************" << endl;
// 测试4.2:传入右值pair
lt1.emplace_back(move(kv)); // 会调用pair的移动构造
// 和push_back效果一样
cout << "*********************************" << endl;
// 测试4.3:直接传入构造pair需要的参数
lt1.emplace_back("苹果", 1); // 直接在节点空间构造pair对象
// 避免了先构造临时pair再拷贝/移动
// 这是push_back做不到的,更高效
cout << "*********************************" << endl;
return 0;
}