C++11_2
文章目录
- 前言
- 一、新的类功能——新的默认成员函数
- 1. 编译器默认生成的移动构造与移动赋值
- 2. 手动提供移动构造或移动赋值的影响
- 二、可变参数模板
- 1. 多参数
- 1. 可变参数模板的基础
- 2. 可变参数模板的展开
- (1)递归展开参数包
- (2)使用逗号表达式展开参数包
- 3. 实际应用场景
- 三、STL容器中的empalce相关接口函数
- 1. empalce_back用法
- 2. emplace_back与push_back与优缺
- 四、包装器
- 1. function包装器
- (1)function是什么?
- (2)function语法
- (3)function应用
- 2. bind
- (1)bind语法
- (2)bind用处
- (3)对于成员函数
- 总结
前言
接下来我们接着看C++11的新功能吧~
一、新的类功能——新的默认成员函数
这里是你的内容经过格式优化后的版本,保证清晰、易读,并符合技术文档的风格:
C++11 新增:移动构造函数与移动赋值运算符
C++11 新增了移动构造函数和移动赋值运算符重载,它们主要用于资源所有权转移,提高性能,减少不必要的拷贝操作。
1. 编译器默认生成的移动构造与移动赋值
在特定条件下,编译器会自动生成默认的移动构造函数和移动赋值运算符。但需要注意以下几点:
- 默认移动构造函数
- 如果类没有定义:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 那么编译器会自动生成默认的移动构造函数。
- 其行为:
- 对于内置类型(如
int
、double
),执行按字节逐成员拷贝。 - 对于自定义类型,如果该类型实现了移动构造,则调用其移动构造;否则调用拷贝构造。
参考代码——string类
- 对于内置类型(如
- 如果类没有定义:
namespace jyf
{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(const 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):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;//string tmp(s._str);//swap(tmp);}string(string&& s):_str(nullptr){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;}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;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}
这里我们采用如下代码来进行学习:
class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}//想让他强制生成就用关键字default,如下所示//Person(Person&& p) = default;//Person(const Person& p) = default;private:jyf::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
如上述代码所示,person类没有实现析构,拷贝构造,拷贝赋值,因此传右值的时候就因该调到移动拷贝。
- 默认移动赋值运算符
- 如果类没有定义:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 那么编译器会自动生成默认的移动赋值运算符。
- 其行为:
- 对于内置类型,执行按字节逐成员拷贝。
- 对于自定义类型,如果该类型实现了移动赋值,则调用其移动赋值;否则调用拷贝赋值。
- 如果类没有定义:
注意:默认移动赋值运算符的行为与默认移动构造函数完全类似。
2. 手动提供移动构造或移动赋值的影响
如果类手动实现了:
- 移动构造函数
- 移动赋值运算符
那么编译器不会自动提供:
- 拷贝构造函数
- 拷贝赋值运算符
如果你的类需要支持拷贝与移动,必须手动实现拷贝构造与拷贝赋值,否则可能导致拷贝操作被禁止(=delete
)。
二、可变参数模板
1. 多参数
C++11 引入了可变参数模板,允许创建可以接受任意数量参数的函数模板和类模板。
1. 可变参数模板的基础
在可变参数模板中,参数列表使用 ...
省略号表示。例如:
template <class ...Args>
void ShowList(Args... args)
{}
在上面的代码中:
Args...
是模板参数包,它可以包含**0 到 N(N ≥ 0)**个模板参数。args...
是函数形参包,它与Args...
一一对应。
可变参数模板的一个主要特点是:
不能直接获取参数包中的元素,只能通过展开参数包的方式访问每个参数。这也是可变参数模板最难理解的地方,因为语法不支持 args[i]
这种方式直接访问参数。
2. 可变参数模板的展开
由于无法直接访问参数包中的元素,常见的展开方式有:
(1)递归展开参数包
递归展开是最常见的方式,我们通过递归终止函数来控制递归的结束:
#include <iostream>
using namespace std;// 递归终止函数
template <class T>
void ShowList(const T& t)
{cout << t << endl;
}// 递归展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " ";ShowList(args...);
}int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
执行结果:
1
1 A
1 A sort
原理:
- 递归终止函数:当
Args...
为空时,调用ShowList(const T&)
结束递归。 - 递归展开:每次调用
ShowList(T value, Args... args)
,打印value
,然后递归调用ShowList(args...)
继续展开。
也可以这样:
void _ShowList()
{// 结束条件的函数cout << endl;
}template <class T, class ...Args>
void _ShowList(T val, Args... args)
{cout << val << " ";_ShowList(args...);
}//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{_ShowList(args...);
}int main()
{CppPrint();CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 2.2);CppPrint(1, 2, 2.2, string("xxxx"));// ...return 0;
}
(2)使用逗号表达式展开参数包
逗号表达式可以用于参数包展开,不需要额外定义递归终止函数:
#include <iostream>
using namespace std;template <class T>
void PrintArg(T t)
{cout << t << " ";
}// 直接展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... }; // 逗号表达式展开参数包cout << endl;
}int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
执行结果:
1
1 A
1 A sort
原理:
PrintArg(args)
依次执行PrintArg(arg1)
,PrintArg(arg2)
,PrintArg(arg3)...
- 由于
int arr[] = { (PrintArg(args), 0)... };
是一个初始化列表,所以所有PrintArg(args)
在构造数组时就被执行,从而展开参数包。
3. 实际应用场景
我们来看一个实用一点的实际场景:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){cout << "Date构造" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date拷贝构造" << endl;}private:int _year;int _month;int _day;
};template <class ...Args>
Date* Create(Args... args)
{Date* ret = new Date(args...);return ret;
}int main()
{Date* p1 = Create();Date* p2 = Create(2023);Date* p3 = Create(2023, 9);Date* p4 = Create(2023, 9, 27);Date d(2023, 1, 1);Date* p5 = Create(d);return 0;
}
如下图所示:有了多参数之后,我们只要提供多参数的构造,再将构造函数写缺省,就可以更加灵活多变的传参来创建对象,和传统对象的创建相比,这个方法不需要创建额外的对象,参数包
传过去会自动匹配
构造函数,省去了一层拷贝,提高了效率!
也可以直接传日期类对象,参数包接收就会掉拷贝构造:
三、STL容器中的empalce相关接口函数
1. empalce_back用法
http://www.cplusplus.com/reference/vector/vector/emplace_back/
http://www.cplusplus.com/reference/list/list/emplace_back/
我们可以看到每一个容器都新增了empalce系类的内容,那么它有什么用呢?
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和
emplace系列接口的优势到底在哪里呢?
2. emplace_back与push_back与优缺
int main()
{list< std::pair<int, char> > mylist;// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别mylist.emplace_back(10, 'a');mylist.emplace_back(20, 'b');mylist.emplace_back(make_pair(30, 'c'));mylist.push_back(make_pair(40, 'd'));mylist.push_back({ 50, 'e' });for (auto e : mylist)cout << e.first << ":" << e.second << endl;return 0;
}
主要是这样:mylist.emplace_back(20, 'b');
,这样就不用先创建临时对象在进行拷贝构造,而是直接走参数包,有一定性能提升的。而push_back只能传对象。
我们再来看下一个场景:
int main()
{// 下面我们试一下带有拷贝构造和移动构造的jyf::string,再试试呢// 我们会发现其实差别也不大,emplace_back是直接构造了,push_back// 是先构造,再移动构造,其实也还好。std::list< std::pair<int, jyf::string> > mylist;mylist.emplace_back(10, "sort");mylist.push_back(make_pair(30, "sort"));std::list<Date> lt;Date d(2023, 9, 27);// 只能传日期类对象lt.push_back(d);// 传日期类对象// 传日期类对象的参数包// 参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象lt.emplace_back(d);lt.emplace_back(2023, 9, 27);return 0;
}
因为有移动构造的存在,所以对于深拷贝的类其实差别不大,对于浅拷贝有一定提升,但因为浅拷贝本来就没有多少资源,所以也影响不大。
四、包装器
1. function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么,function有什么用呢?
function在#include <functional>
里面~
(1)function是什么?
我们来看下面一段代码:
#include<iostream>
using namespace std;
#include <functional>template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}double f(double i)
{return i / 2;
}struct Functor
{double operator()(double d){return d / 3;}
};int main()
{// 函数指针cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lambda表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}
有三类:1.函数指针 2. 函数对象 3. lambda表达式 分别去调用useF函数,但是他们三个虽然都是起到类似定义函数变量的作用,但是他们却实例化出三份不同的函数,证据就是运行结果静态变量没有则会增加到3,而是有3个1。
那有没有什么方法可以包装一下这三类1.函数指针 2. 函数对象 3. lambda表达式
呢?
有的兄弟,有的~
我们的包装器就要登场了——function
(2)function语法
语法:function<返回值类型(参数列表)> xxx = ???
像这样:
// 包装器 -- 可调用对象的类型问题function<double(double)> f1 = f;function<double(double)> f2 = [](double d)->double { return d / 4; };function<double(double)> f3 = Functor();
因此我们就可以吧三种不同类的函数放到同一个vector中,如下:
方法一:
vector<function<double(double)>> v = { f1, f2, f3 };
方法二:
vector<function<double(double)>> v = { f, [](double d)->double { return d / 4; }, Functor() };
因此我们就可以这样取调用它:
vector<function<double(double)>> v = { f, [](double d)->double { return d / 4; }, Functor() };double n = 3.3;
for (auto f : v)
{cout << f(n++) << endl;
}
有了function以后,我们就可以同一类型,解决实例化多份的问题,如下图,三类只实例化出一份。
#include <functional>
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名std::function<double(double)> func1 = f;cout << useF(func1, 11.11) << endl;// 函数对象std::function<double(double)> func2 = Functor();cout << useF(func2, 11.11) << endl;// lamber表达式std::function<double(double)> func3 = [](double d)->double { return d /4; };cout << useF(func3, 11.11) << endl;return 0;
}
(3)function应用
我们来通过一道习题展示他的应用:
https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/submissions/
这是我们之前写的代码,要判断多次运算符而且冗余:
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> s;for (size_t i = 0; i < tokens.size(); i++){if (tokens[i] == "+" ||tokens[i] == "-" ||tokens[i] == "*" ||tokens[i] == "/" ){int right = s.top();s.pop();int left = s.top();s.pop();switch(tokens[i][0]){case '+':s.push(left + right);break;case '-':s.push(left - right);break;case '*':s.push(left * right);break;case '/':s.push(left / right);break;}}else{s.push(atoi(tokens[i].c_str()));}}return s.top();}
};
有了function之后我们就可以这样做~
class Solution {
public:int evalRPN(vector<string>& tokens) {map<string, function<int(int, int)>> cmdFuncMap ={{"+", [](int x, int y){return x + y;}},{"-", [](int x, int y){return x - y;}},{"*", [](int x, int y){return x * y;}},{"/", [](int x, int y){return x / y;}}};stack<int> st;// 运算数入栈,运算符运算for(auto& e : tokens){if (cmdFuncMap.count(e)){//运算int right = st.top();st.pop();int left = st.top();st.pop();st.push(cmdFuncMap[e](left, right));}else{st.push(stoi(e));}}return st.top();}
};
代码看起来是不是清晰很多呢~
2. bind
(1)bind语法
- 对于函数:
bind(函数名, placeholders::_x, placeholders::_x, ...)
- 对于静态成员函数:
bind(作用域::静态成员函数, placeholders::_x, placeholders::_x, ...)
,也可以写成:bind(作用域::&静态成员函数, placeholders::_x, placeholders::_x, ...)
- 对于普通成员函数:
bind(作用域::&普通成员函数, '&对象'或'匿名对象',placeholders::_x, placeholders::_x, ...)
(2)bind用处
bind(绑定)是什么呢?
简单来说就是传参的时候我们可以改变参数的顺序,以此达到我们想要的效果:
int Sub(int a, int b)
{return a - b;
}int main()
{function<int(int, int)> sub1 = bind(Sub, placeholders::_2, placeholders::_1);cout << sub1(10, 5);
}
他的原理是这样的:
传参的时候第一个函数会去匹配placeholder::_1
,第二个会去匹配_2,而bind却是按照参数的顺序绑定的,因此我们可以更加灵活调整传参数顺序。
如果有其他参数放到对应位置正常写即可,就像这样:
(3)对于成员函数
class SubType
{
public:static int sub(int a, int b){return a - b;}int ssub(int a, int b, int rate){return (a - b) * rate;}
};
对于静态成员函数是这样的:
这个&
可加可不加,建议加上。
对于普通成员变量:
第一个前必须加&
,后面可以匿名对象,也可以&对象。其实就是.
还是->
问题。
总结
C++11语法持续更新中,还有智能指针等章节在后续讲解中,谢谢大家支持!