一文学会《C++》进阶系列之C++11
文章目录
- 统一列表初始化
- {}初始化
- std::initializer_list
- 左值引用和右值引用
- 左值引用
- 💪右值引用
- 左值引用右值引用比较
- 🚩 右值引用的使用场景和意义
- ⭐左值引用使用场景及短板
- 右值引用和移动语义解决
- 🚩完美转发
- ⭐模板中的&&万能引用
- ⭐std::forword
- 新的类功能
- 💪default关键字
- 💪delete关键字
- final和override关键字
- 可变参数模板
- 递归函数方式展开参数包
- 逗号表达式展开参数包
- emplace
- ❤lambda表达式
- lambda语法
- ⭐函数对象与lambda表达式
- ❤function包装器
统一列表初始化
{}初始化
c++98允许花括号对数组或结构体进行初始化,如:
using namespace std;
struct point {int _x;int _y;
};
int main()
{int array1[] = { 1,2,3,4,5 };int array2[5] = { 0 };point p = { 1,2 };
}
c++11扩大了花括号的使用范围,使其可以定义所有的内置类型或自定义类型,使用初始胡列表时可以加=号也可以不加
int x = 1;
int y{ 1 };int array1[]{ 1,2,3,4,5 };
int array2[5]{ 0 };
point p{ 1,2 };//也可以用在new
int* pa = new int[4] {0};
创建对象时也可以用列表初始化调用构造函数,
class Date {
public:Date(int year, int month, int day):_year(year),_month(month),_day(day){cout << "(int year, int month, int day)"<<endl;}
private:int _year;int _month;int _day;
};
int main()
{//c++98Date d1(2025, 10, 1);//c++11Date d2{ 2025,10,1 };return 0;
}
std::initializer_list
查看initializer_list类型
auto li = {1,4,7};
cout << typeid(li).name()<<endl;
initializer_list使用场景:一般作为容器构造函数参数,c++11大部分容器都加了initializer_list构造,这样初始化容器就简单了,
也可以用于operator=参数,就可以用{}初始化了
int main()
{//c++要一个个insertvector<int> v = { 2,5,7,8 };list<int> l = { 1,3,5,6 };//c++98要make_pair,这里里面自动生成pairmap<string, string> mp{ {"apple","苹果"},{"pear","梨"} };//用花括号赋值v = { 1,3,5,7 };return 0;
}
自己模拟实现的vector添加initializer_list功能
namespace jib {template<class T>class vector {public:typedef T* iterator;vector(initializer_list<T> l){_start = new T[l.size()];_finish = _start + l.size();_endofstorage = _start + l.size();iterator vit = _start;typename initializer_list<T>::iterator lit = l.begin();while (vit != _finish){*vit = *lit;vit++;lit++;}}vector<T>& operator=(initializer_list<T> l) {vector<T> tmp(l);swap(_start, tmp._start);swap(_finish, tmp._finish);swap(_endofstorage, tmp._endofstorage);return *this;}private:iterator _start;iterator _finish;iterator _endofstorage;};
}
int main()
{jib::vector<int> v = { 1,4,6 };return 0;
}
左值引用和右值引用
c++11新增右值引用,从此我们之前学的引用都叫做左值引用,无论左值引用还是右值引用,都是给对象起别名
左值引用
左值:我们可以对它取地址+赋值,左值可以出现在赋值符号的左边,右值不能出现在符号的左边,被const修饰的左值不能赋值,但可以取地址。对左值引用就是左值引用。
int main()
{//下面是左值int* p = new int[10];int a = 1;const int b = 2;//以下是左值引用int*& rp = p;int& ra = a;const int& rb = b;int& pp = *p;return 0;
}
💪右值引用
右值:一种表示数据的表达式,如字面常量,表达式返回值,函数返回值(这个不能是左值引用返回),右值可以出现在赋值符号右边,不能出现在左边,不能取地址。右值引用就是对右值取引用,是右值的别名。
int f(int x, int y)
{return x + y;
}
int main()
{int x = 1;int y = 2;//以下是右值10;x + y;f(x, y);//以下是右值引用和int&& xv = 10;int&& yv = x + y;int&& fv = f(x, y);return 0;
}
注意:右值本身是不能取地址,但是被引用后,右值就换了个特殊地方存储,就可以被取地址,
就是说 10本来是不能取地址的,但是xv引用了10,就可以对xv取地址,也可以修改xv,但是右值引用应用场景不在这里,所以这个特性不是很重要、
int&& xv = 10;
xv = 20;
左值引用右值引用比较
左值引用总结:
1,左值引用只能引用左值不能引用右值
🚩2,加了const的左值引用既可以引用左值也可以引用右值
int x = 10;int& rx1 = x;//int& rx2=10; 错误因为10是右值//const既可以引用左值也可以引用右值const int& rx3 = 10;const int& rx4 = x;
右值引用总结:
1,右值引用只能引用右值不能引用左值
🚩2,可以引用move以后的左值
int&& rx = 10;
int a = 10;
int&& ra = std::move(a);
🚩 右值引用的使用场景和意义
我们看看左值引用的短板,以及右值引用如何补齐这个短板的
namespace jib
{class string {public:typedef char* iterator;public:string(const char* str = ""){_size = strlen(str);_capacity = _size;if (nullptr == str){assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);cout << "string(const char* str=" ")" << endl;}//拷贝构造string(const string& s):_str(new char[strlen(s._str) + 1]),_size(strlen(s._str)),_capacity(_size){strcpy(_str, s._str);cout << "string(const string& s)---深拷贝" << endl;}//移动构造string(string&& s):_str(nullptr),_size(0),_capacity(0){cout << "string(string&& s)--移动构造" << endl;Swap(s);}//赋值重载string& operator =(string& s){string tmp(s);Swap(s);cout << "operator =(string s)---深拷贝" << endl;return *this;}//移动赋值string& operator =(string&& s){cout << "operator =(string&& s)---移动赋值" << endl;Swap(s);return *this;}void push_back(char c){if (_size == _capacity){reserve(_capacity * 2 + 1);}_str[_size++] = c;_str[_size] = '\0';}string& operator +=(char c){push_back(c);return *this;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void clear(){_size = 0;_str[_size] = '\0';}void Swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}const char* C_Str(){return _str;}~string(){delete[] _str;_str = nullptr;}private:char* _str;size_t _size;size_t _capacity;};
}
⭐左值引用使用场景及短板
做参数和返回值都能提高效率
void func1(jib::string s1)
{}
void func2(const jib::string& s2)
{}
int main()
{jib::string s("hello");func1(s);func2(s);//传引用减少拷贝,提高了效率s += 'w';//string operator+=(char ch); 存在深拷贝//string& operator+=(char ch);引用无需拷贝,提高效率return 0;
}
短板:
当函数返回值是局部变量,出了作用域就销毁了,就不能左值引用返回,只能传值返回,例如下面函数(我拿我老师的板书来说了),只能传值返回,而传值返回又导致至少1次拷贝构造(旧编译器可能会两次拷贝构造)
右值引用和移动语义解决
移动语义:将右值的资源剽窃过来,那么我们就不需要深拷贝了,就是拿别人的资源来构造自己,移动构造没有开辟新空间,所以效率提升了
拿我左值引用短板那块代码运行
jib::string f()
{jib::string str;return str;
}
int main()
{jib::string s1;s1 = "qq";cout << " " << endl;jib::string str=move(f());return 0;
}
💪仔细分析以下:
第一行:构建s1对象
❤第二行:“qq”隐式构造临时对象
第三行:移动赋值
第五行:f()中str默认构造
第六行:返回右值调用移动构造
⭐插一句 string s=“hello” 如果编译器优化的话就是直接构造,如果不优化,就是先构造临时对象再调用拷贝构造
🚩完美转发
⭐模板中的&&万能引用
模板中的&&不是右值引用,而是万能引用,既能接受左值又能接受右值
我们看以下代码:
void fun(int& x)
{cout << "左值引用" << endl;
}
void fun(int&& x)
{cout << "右值引用" << endl;
}
void fun(const int& x)
{cout << "左值引用" << endl;
}
void fun(const int&& x)
{cout << "右值引用" << endl;
}
template<class T>
void perfectforward(T&& t)
{fun(t);
}
int main()
{perfectforward(10); //右值int a;perfectforward(a); //左值perfectforward(move(a)); //右值const int b=1;perfectforward(b); //左值perfectforward(move(b)); //右值return 0;
}
怎么全是左值引用????
答:模板中的&&确实接受了左值右值,但只限制了接收的类型,之后对t的操作都是左值属性
❤总结:
1,模板中的&&是万能引用,既能接受左值也能接受右值
2,t是右值,但之后的操作都退化成左值
⭐std::forword
std::forward在完美转发中保留对象的原生类型属性
template<class T>
void perfectforward(T&& t)
{fun(std::forward<T> (t));
}
完美转发实际应用
template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};
template<class T>
class List
{typedef ListNode<T> Node;
public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}void PushBack(T&& x){//Insert(_head, x);Insert(_head, std::forward<T>(x));}void PushFront(T&& x){//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x));}// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 关键位置void Insert(Node * pos, const T & x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}}
private:Node* _head;
};
int main()
{List<jib::string> lt;lt.PushBack("1111");lt.PushFront("2222");return 0;
}
用右值引用插入,少了一次临时构造string
新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。 - 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似) - 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
💪default关键字
default可以强制生成默认函数
如果我们实现了拷贝构造,移动构造可能不会自动生成,就可以用default强制生成默认函数
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(Person&& p) = default; //显式强制生成移动构造
private:jib::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
💪delete关键字
同样的,禁止编译器生成默认函数就用delete关键字
Person(Person&& p) = delete;
final和override关键字
这个我在继承和多态文章里说过了,可以去那两篇文章查看
可变参数模板
c++98类模板和函数模板仅支持固定数量的模板参数,c++11新增可变参数模板,作者水平有限,这里只讲基础用法
一个基本可变参数的函数模板:
template <class ...Args>
void Showlist(Args... args)
{}
Args是模板参数包,args是形参参数包
Args… args包含了0——N个参数(N≥0)
递归函数方式展开参数包
//递归终止函数
template<class T>
void Showlist(const T& t) //1号
{cout << t << endl;
}
//展开函数
template <class T,class ...Args>
void Showlist(T value,Args... args) //2号
{cout << value<<" ";Showlist(args...);
}
int main()
{Showlist('1');Showlist('1', 'h');Showlist('1', 'h', "hello");return 0;
}
第一行:直接进入终止函数,因为就一个参数
第二行:先进入2号函数,打印1,剩1个参数,调用终止函数
第三行:先进入2号函数,剩两个参数,再进入2号函数,剩一个参数吗,调用终止函数
逗号表达式展开参数包
逗号表达式展开参数包不需要终止函数,是在expand函数体中实现的,printfarg处理参数包中的每一个参数,逗号表达式会顺序执行逗号前面的表达式
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行
printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列
表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),
(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…
(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)
打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在
数组构造的过程展开参数
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;}
emplace
template<class... Args>
void emplace_back(Args&&... args)
{}
emplace系列接口接受模板可变参数,同时是万能引用,那么相比较insert有啥优势呢?
int main()
{list<pair<int, char>> lt;lt.emplace_back(1, 'a');lt.emplace_back(make_pair('2', 'b'));lt.push_back({'3','c'});lt.push_back(make_pair('4', 'd'));return 0;
}
emplace支持可变参数,拿到可构造pair对象就可以直接构造,那么我们可以知道emplace与push_back没什么区别,
#include <list>
#include <string>
int main()
{list<pair<int, string>> lt;lt.emplace_back(1, "hello");lt.emplace_back(make_pair(2, "hello"));lt.push_back({ 3,"hello"});lt.push_back(make_pair(4, "hello"));return 0;
}
我们再看一下支持移动构造的string,看看emplace和push有何区别
:只有(1,“hello”)是直接构造,其他都是先临时构造pair对象,再移动构造、
❤两者效率并无太大差别,(就是因为移动构造的存在使得emplace显得不那么重要)
❤lambda表达式
C++98中要对数据排序要这样做:
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{int a[] = { 1,5,7,8,4,3 };sort(a ,a+sizeof(a)/sizeof(a[0]));//sort(a, a + sizeof(a) / sizeof(a[0]),greater<int>());return 0;
}
对自定义类型要仿函数
#include <vector>
#include <algorithm>
class fruit
{
public:fruit(const char* name, double price):_name(name),_price(price){}string _name;double _price;
};struct pricegreater
{bool operator()(const fruit& a, const fruit& b){return a._price > b._price;}
};
int main()
{vector<fruit> v = { {"apple",1.5},{"pair",2.1},{"grape",1.0} };sort(v.begin(), v.end(), pricegreater());return 0;
}
C++11觉得比较太麻烦了,每次比较都要写一个类,如果比较逻辑不同还要写更多的类,所以发明了lamdba表达式
用法:
#include <algorithm>
#include <vector>
int main()
{vector<fruit> v = { {"apple",1.5},{"pair",2.1},{"grape",1.0} };sort(v.begin(), v.end(), [](const fruit& a, const fruit& b) {return a._price < b._price;});sort(v.begin(), v.end(), [](const fruit & a, const fruit & b) { return a._price > b._price; });return 0;
}
lambda语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
各部分说明:
- ❤ [capture-list]捕捉列表,编译器根据[]来判断接下来是否是lambda函数,捕捉列表能捕捉上下文变量来供lambda使用
- (parameters) 参数列表,与普通函数参数列表完全一致,如果不传参数,可以省略
- mutable lambda是常性函数,mutable可以取消常性,使用mutable时,参数列表不可省略
- -> return-type :返回值类型,也可省略,编译器会根据返回值自动推导
- ❤ { statement }:函数体,在函数体中不仅可以使用参数,还可以使用捕捉i到的所有变量
⭐注意:lambda中参数列表和返回值类型都可省略,但[]和{}至少为空,因此最简单lambda函数[]{},该函数不能做任何事情
int main()
{int a = 3;int b = 4;[=] {return a + 3;};//省略参数列表和返回值类型cout << a << " " << b << endl;auto func1=[&](int c) {return b = a + c;};//省略返回值类型func1(10);cout << a << " " << b << endl;auto func2 = [=, &b](int c)->int {return b += a + c;};//完善的lambdafunc2(10);cout << a << " " << b << endl;int x=10;//auto add_x = [x](int c) {x *= 2;return x + 2;};//常性,x不能修改auto add_x = [x](int c) mutable {x *= 2;return x + 2;};cout << add_x(10)<<endl;return 0;
}
lambda可以理解是无名函数,不能直接调,但是可以用auto接收
捕捉列表说明:
捕捉列表决定上下文那些数据可以被使用,以及是使用方式传值还是传引用
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
⭐1,父作用域指的是包含lambda表达式的语句块
⭐2,捕捉列表可以捕捉多项,以逗号分隔
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
⭐3 ,捕捉列表中不能重复捕捉,否则编译错误
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
⭐4, 在块作用域以外的lambda函数捕捉列表必须为空。
⭐5,在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错。
⭐ 6,lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)();
int main()
{auto f1 = [] {cout << "hello world" << endl; };auto f2 = [] {cout << "hello world" << endl; };//f1 = f2; // 编译失败--->提示找不到operator=()// 允许使用一个lambda表达式拷贝构造一个新的副本auto f3(f2);f3();PF = f2;// 可以将lambda表达式赋值给相同类型的函数指针return 0;
}
⭐函数对象与lambda表达式
函数对象又称仿函数,指的像函数一样使用的对象,就是类中重载operator()的类对象
class Rate
{
public:Rate(double rate) : _rate(rate){ }double operator()(double money, int year){ return money * _rate * year;}
private:
double _rate;
};
int main()
{// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);return 0;
}
两者使用差不多,
🚩实际上,lambda就是通过仿函数实现的,使用lambda时,编译器会自动生成一个类,并重载()
❤function包装器
template <class F,class T>
T Usef(F f,T x)
{static int count = 0;cout << ++count << endl;cout << &count << endl;return f(x);
}double f(double x)
{return x / 2;
}struct Functor {double operator()(double x){return x / 3;}
};
int main()
{cout << Usef(f,11.11) << endl; //函数名(函数指针)cout << Usef(Functor(), 11.11) << endl;//函数对象cout << Usef([](double x) {return x / 4;},11.11);//lambda表达式return 0;
}
我们发现Usef被实例化了三份,效率太低了,所以发明了function
template <class Ret, class… Args>
class function<Ret(Args…)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
#include <functional>
template<class F,class T>
void Usef(F f,T x)
{static int count = 0;cout << ++count << endl;cout << &count << endl;
}
double f(double x)
{return x / 2;
}struct Functor {double operator()(double x){return x / 3;}
};class Plus
{
public:static double plusi(double x){return x / 4;}double plusd(double x){return x / 5;}
};
int main()
{function<double(double x)>func1 = f;cout << func1(11.11) << endl;function<double(double)> func2 = Functor();cout << func2(11.11) << endl;function<double(double)> func3 = [](double x) {return x / 6;};cout << func3(11.11) << endl;function<double(double)> func4 = &Plus::plusi;cout << func4(11.11) << endl;function<double(Plus, double)> func5= &Plus::plusd;cout << func5(Plus(), 11.11) << endl;Usef(func1, 11.11);Usef(func2, 11.11);Usef(func3, 11.11);Usef(func4, 11.11);Usef(func5, 11.11);return 0;
}
我们发现 function<double(double)> 只被实例化了1份