💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:C++修炼之路⏪
🚚代码仓库:C++高阶🚚
🌹关注我🫵带你学习更多C++知识
🔝🔝
目录
前言
一、新的类功能
1.1默认成员函数——移动构造、移动赋值
1.2强制生成默认函数的关键字default:
1.3const延长生命周期的问题
1.4禁止生成默认函数的关键字delete:
1.5 其他新功能
缺省值
委托构造
二、可变参数模板
利用递归函数展开
逗号表达式展开参数包
三、Lambda
lambda表达式
lambda表达式语法
1.捕捉列表
函数对象与lambda表达式
前言
上篇重点讲解了右值引用,本篇的可变参数模板和Lambda也是11里面非常有用的。如果学会这两个以后编程会感觉非常的爽。废话不多说直接开始!!!
一、新的类功能
1.1默认成员函数——移动构造、移动赋值
在C++11后,类又新增了两个默认成员函数:移动构造和移动赋值。
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
#include "String.h"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:gx::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = move(s1);Person s4;s4 = move(s2);return 0;
}
如果把上面代码的拷贝构造 赋值重载、析构的任意一个代码注释取消注释就会得到下面结果
为什么我们一但自己写了默认成员函数编译器就不会自己生成移动构造、赋值?
如果我们实现了 析构、拷贝构造、赋值重载,就证明当前的类中涉及到了 动态内存管理,是需要自己进行 深拷贝 的,编译器无能为力,移动语义 也应该根据自己的实际场景进行设计,所以编译器就没有自动生成
那如果有些场景就需要我们自己写拷贝构造、析构、赋值重载这些函数那怎么办?
1.2强制生成默认函数的关键字default:
Person(Person&& p) = default;
Person& operator=(Person&& p) = default;
C++11后STL中所有容器都增加了移动构造和移动赋值
插入系列的函数也同样增加了右值的版本。
其他容器详情请看官网cplusplus.com
1.3const延长生命周期的问题
插入函数之所以会延长生命周期
当您创建一个临时对象并将其作为参数传递给函数时,这个临时对象的生命周期通常只在表达式中有效。一旦表达式结束,临时对象就会被销毁。但是,如果这个临时对象被传递给一个需要更长时间使用它的函数,比如一个需要对对象进行修改的函数,那么就需要延长这个临时对象的生命周期。
在C++中,如果一个函数的参数是一个const
类型,这意味着函数不会修改这个对象。但是,如果这个参数是通过引用传递的,那么即使它是const
,它仍然需要在函数调用期间保持有效,以便函数可以访问它。这就是所谓的生命周期延长。
既然可以延长对象生命周期那是不是也可以像这样?下图这样返回的对象加const
从结果来看显然是不可以的。出现了野引用的问题。 所以说引用也不是安全的。
1.4禁止生成默认函数的关键字delete:
在person类中我们在拷贝构造函数后面加 =delete 就无法再使用这个这个函数。
注意:delete这个关键字只对默认成员函数有效
那什么样的类是不希望其他人来调用的它的默认成员函数?
比如IO流
每个IO流对象的缓冲区都是不一样的,随意拷贝都会造成资源混乱。
1.5 其他新功能
在C++98中,类中的内置类型是不对初始化的。而在C++11中出现了缺省参数 可以给类的成员给缺省值。
缺省值
没有缺省值我们得到_a的值是随机值。
给定缺省值 1
委托构造
什么是委托构造? 简单来说就是一个构造函数可以复用其他构造函数
class Person
{
public:Person(const char* name, int age):_name(name), _age(age){}Person(const char* name):Person(name, 18) // 委托构造{}private:gx::string _name; // 自定义类型int _age = 1; // 内置类型
};int main()
{Person s1("张三");return 0;
}
这个委托构造了解一下就行了,说白了还是要调用构造函数。
二、可变参数模板
相比C++98的模板参数,C++11模板参数变成了不是固定的,可以接受任意个类型。和printf函数的可变参数列表是类似的。只是这里的模板参数变成了类型。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
如果要知道参数包中个数怎么解决?
template<class ...Args>
void ShowList(Args...args)
{cout << sizeof...(args) << endl;
}
int main()
{ShowList(1, 'x');
}
如何解析出参数包里面的值?
利用递归函数展开
void ShowList()
{cout << endl;
}
template <class T ,class ...Args>
void ShowList(const T& val, Args ...args)
{cout << __FUNCTION__ << "(" << sizeof...(args) << ")" << endl;cout << val << endl;ShowList(args...); //语法规定...必须在后面
}
int main()
{ShowList(1, 'A', std::string("sort"));return 0;
}
上面结果确实调用3次ShowList这个函数再加上无参ShowList。
能不能不用模板参数T? 我就想直接用可变模板参数包?可以 。直接再套一层。
void _ShowList()
{cout << endl;
}
template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{cout << __FUNCTION__ << "(" << sizeof...(args) << ")" << endl;cout << val << endl;_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{_ShowList(args...);
}int main()
{ShowList(1, 'A', std::string("sort"));return 0;
}
逗号表达式展开参数包
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;
}
在上篇的初始化列表 我们知道C++11在arr这个数组创建时,会初始化 列表里面的内容。实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
编译器通过解析变成下面表达式
为什么要写出成
(Print(args), 0)
的形式?
这是一个逗号表达式,目的是让整个式子最终返回0
,用于初始化arr
数组
如果不想加0也是可以的。
可变参数模板应用场景又是什么?线程
我们知道C语言中回调函数的传参。C的方案是用的void* 而C++11的线程库用可变参数模板对void* 这个指针进行封装。通过可变参数模板,就可以快乐的传递任何参数。剩下的事交给编译器来干。
可变参数包还可以用于优化STL容器中插入函数。
以容器list为例:
int main()
{list<gx::string> lt;gx::string s1("1111");lt.push_back(s1);lt.emplace_back(s1);return 0;
}
对于是深拷贝的类是没区别的。push_back和emplace_back二者没什么区别。
我们在来看看浅拷贝的类有没有影响
#include "Date.h"
int main()
{/*list<gx::string> lt;gx::string s1("1111");lt.push_back(s1);lt.emplace_back(s1);*/list<Date> lt2;Date d1(2024, 5, 20);Date d2(2024, 5, 21);lt2.push_back(d1);lt2.emplace_back(d2);return 0;}
也是没有差别。
那如果是 d1\d2是右值那?
也是没区别
但是是下面这种就有区别了
emplace_back直接就是构造。这是因为可变参数包在参数传递的过程中,参数包不会展开。直到构造函数才展开。其实这里可以理解成(2023,5,28)它不是一个匿名对象,在参数包的眼里它实际是3个整型。
再比如 下面这个。
有了可变参数包。编译器直接识别为const char* 的字符串。而不是一个匿名对象。
结论:无脑用emplace_back就行。
三、Lambda
在C++11之前,我们如果要对数据进行排序怎么做?用std::sort。
#include <algorithm>
#include <functional>
int main()
{int array[] = { 4,1,8,5,3,7,0,9,2,6 };// 默认按照小于比较,排出来结果是升序std::sort(array, array + sizeof(array) / sizeof(array[0]));// 如果需要降序,需要改变元素的比较规则std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());return 0;
}
如果我们要排序的是自定义类型那就需要用到仿函数。
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){{return gl._price < gr._price;} }
};
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());
}
生活中商品太多了难道每一种商品的排序都要写相应的仿函数,是不是有点太麻烦?,假设我们不以价格来进行排序。现在要求按水果的名字排序,是不是又要重写一个仿函数?
有没有一种办法一劳永逸?在C++11推出了lambda
lambda表达式
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate; });
}
上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函
数。
lambda表达式语法
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数 , 捕捉列表能够捕捉上下文中的变量供 lambda函数使用 。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以
连同 () 一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略 ( 即使参数为空 ) 。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
值时此部分可省略。 返回值类型明确情况下,也可省略,由编译器对返回类型进行推导 。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
我们先来一个简单的lambda语法
[](int x, int y)->int {return x + y; };
如果函数体的语句较多我们也是可以这样写代码的
[](int x, int y)->int {return x + y; };
我们如果要调用这个函数对象太长了,我们可以加 auto
auto add1 = [](int x, int y)->int {return x + y; };cout << add1(1, 2) << endl;
最简单的lambda表达式
//最简单的lambda表达式 该lambda表达式没有任何意义[] {};
1.捕捉列表
写一个交换函数。
int x = 1, y = 0;auto swap1 = [](int& rx, int& ry){int tmp = rx;rx = ry;ry = tmp;};swap1(x, y);cout << x << " " << y << endl;
参数列表是可以省略的
省略参数时,我们就要用捕捉列表
这时我们可以在参数列表后面加入关键字,mutable//异变 但是没什么用
还是没有交换
上面的这种捕捉方式叫做传值捕捉,传值具有常性,不能修改 这时我们需要用到引用捕捉
这里的引用捕捉就很坑,不注意看还以为是取地址!!
如果参数太多怎么办?难道要在捕捉列表中一个一个的捕捉吗?当然不用。我们直接全部引用捕捉
//全部引用捕捉
auto swap2 = [&]() {int tmp = x;x = y;y = tmp;};
当然还有其他的捕捉方式
//混合捕捉auto func1 = [&x, y](){//...};//全部传值捕捉auto func3 = [=](){//...};//全部引用捕捉,x传值捕捉auto func4 = [&, x](){//...};
这时我们就可以用lambad来创建线程
int main()
{int n1, n2;cin >> n1 >> n2;thread t1([n1]( int num){for (int i = 0; i < n1; i++){cout << "线程:" << num << " " << i << endl;}cout << endl;},1);thread t2([n2](int num){for (int i = 0; i < n2; i++){cout << "线程:" << num << " " << i << endl;}cout << endl;}, 2);t1.join();t2.join();return 0;
}
如果要m个线程分别打印n次如何操作? 这里我们可以利用vector 把每个线程放进vector这个容器中。
#include <vector>
int main()
{int m, n;cin >> m >> n;vector<int> arr;arr.push_back(m);arr.push_back(n);vector<thread> vthds(m);for (int i = 0; i < arr[0]; i++){vthds[i] = thread([i,arr](){for (int j = 0; j< arr[1]; j++){cout << "线程:" << i << " " << j << endl;}cout << endl;});}for (auto& t : vthds){t.join();}return 0;
}
当然这个打印会错乱,那是因为没有加锁导致线程串行。关于锁的问题我们在后序线程库在详细讲解。
void (*PF)();
int main()
{auto f1 = []{cout << "hello world" << endl; };auto f2 = []{cout << "hello world" << endl; };//f1 = f2; // 编译失败--->提示找不到operator=()// 允许使用一个lambda表达式拷贝构造一个新的副本auto f3(f2);f3();// 可以将lambda表达式赋值给相同类型的函数指针PF = f2;PF();return 0;
}
总结:
[var] :表示值传递方式捕捉变量 var[=] :表示值传递方式捕获所有父作用域中的变量 ( 包括 this)[&var] :表示引用传递捕捉变量 var[&] :表示引用传递捕捉所有父作用域中的变量 ( 包括 this)[this] :表示值传递方式捕捉当前的 this 指针
a. 父作用域指包含 lambda 函数的语句块b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割 。比如: [=, &a, &b] :以引用传递的方式捕捉变量 a 和 b ,值传递方式捕捉其他所有变量[& , a, this] :值传递方式捕捉变量 a 和 this ,引用方式捕捉其他变量c. 捕捉列表不允许变量重复传递,否则就会导致编译错误 。比如: [=, a] : = 已经以值传递方式捕捉了所有变量,捕捉 a 重复d. 在块作用域以外的 lambda 函数捕捉列表必须为空 。e. 在块作用域中的 lambda函数能捕捉父作用域中局部变量。f. lambda 表达式之间不能相互赋值 ,即使看起来类型相同
函数对象与lambda表达式
lambda的大小是多大?
要清楚这个问题我们需要通过汇编
先搞一个函数对象和lambda的代码
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);// lambdaauto r2 = [=](double monty, int year)->double {return monty * rate * year; };r2(10000, 2);auto f1 = [] {cout << "hello world" << endl; };auto f2 = [] {cout << "hello world" << endl; };//f1 = f2;return 0;
}
f1 和 f2通过汇编我们发现他们两个类名不同,类名不同怎么相互赋值?
这时候我们就能回答大小为什么是1了
在C++中,
sizeof
运算符用来确定一个类型或对象在内存中的大小。对于一个lambda表达式,sizeof
返回的是这个lambda表达式对象在内存中占用的大小。在x86 32位架构上,指针通常是4字节大小。因此,如果你的lambda表达式没有捕获任何局部变量或外部变量(或者只捕获了通过引用捕获的变量),那么lambda表达式的大小很可能是1字节,这是因为:
- Lambda表达式可能被编译器优化为一个很小的函数对象,它只包含一个指向其代码的指针。
- 在某些编译器实现中,lambda表达式可能被优化为一个空的结构体,其中只包含一个指向其代码的指针,因此
sizeof
返回1,表示空结构体的大小。