C++11语法解析(二)

可变参数模板

基本语法及原理 


C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。例如我们在C语言中使用的printf和scanf,它们都是支持传不同个数和类型的参数 
・template <class...Args> void Func (Args... args) {}
・template <class...Args> void Func (Args&... args) {}
・template <class...Args> void Func (Args&&... args) {} 

我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟... 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
・这里我们可以使用 sizeof... 运算符去计算参数包中参数的个数。 

・...表示0到多个参数,所以在使用参数包的时候不传参和传多个参数都是可行的

template <class ...Args>
void Print(Args&&... args)
{
 cout << sizeof...(args) << endl;
}
int main()
{
 double x = 2.2;
 Print(); // 包⾥有0个参数 
 Print(1); // 包⾥有1个参数 
 Print(1, string("xxxxx")); // 包⾥有2个参数 
 Print(1.1, string("xxxxx"), x); // 包⾥有3个参数 
 return 0;
}
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数 
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持 
// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础 
// 上叠加数量变化,让我们泛型编程更灵活。 
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

包扩展

对于一个参数包,我们除了能计算它的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号 (...) 来触发扩展操作。底层的实现细节如图 1 所示。 
・C++ 还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。

这里有同学可能会有疑惑,为什么要多写一个showlist()函数,在函数中多加一个判断条件不行吗?

if (sizeof...(args) == 0)
	return;

注意:模板递归机制需要处理递归终止的情况,需要有一个接受零个参数的重载版本 

template <class T>
const T& GetArg(const T& x)
{
 cout << x << " ";
 return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
 // 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments 
 Arguments(GetArg(args)...);
}
int main()
{
 Print(1, string("xxxxx"), 2.2);
 return 0;
}

在扩展函数包的时候Arguments(GetArg(args)...);实际上扩展成Arguments(GetArg(x), GetArg(y), GetArg(z));会依次输出1,xxxxx,2.2

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)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。

例一:

lt.emplace_back("111111111111");

emplace在传匿名对象的时候达成的效果比push和insert的效果高效多,在传参的时候emplace不会对匿名对象进行拷贝构造,而是直接把构造string参数包往下传,直接用string参数包构造string ,只会产生一个移动构造,而push和insert还会多一个拷贝构造。

但是在传左值和右值的时候并无太大差别。

例二:

lt1.emplace_back("苹果", 1);

在传pair对象的时候,emplace并不用加上大括号,那是因为当容器中存储的元素类型是 std::pair时,std::pair 有一个构造函数可以接受两个参数,在 lt1.emplace_back("苹果", 1); 这个例子中,编译器会尝试将 "苹果" 和 1 这两个参数与 std::pair 的构造函数进行匹配。std::pair 的构造函数会根据传入参数的类型和顺序来正确地初始化 pair 对象,就好像是直接调用 std::pair<std::string, int>("苹果", 1) 一样。

emplace实现

template <class... Args>
ListNode(Args&&... args)
    : _next(nullptr)
    , _prev(nullptr)
    , _data(std::forward<Args>(args)...)
    {}
 };

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);
}

这是一份模拟链表的emplace_back,在调用emplace_back的时候,由于参数包的特性,在传匿名对象的时候并不会进行拷贝构造,而是讲参数继续传下去,直到new的时候进行移动构造

新的类功能

默认的移动构造和移动赋值

原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 /const 取地址重载,最后重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上文移动构造完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

defult和delete

C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成

如果能想要限制某些默认函数的生成,在 C++98 中,是将该函数设置成 private,并且只声明不定义,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数为删除函数。有些类不允许被拷贝,如:i/o流,增加deletd关键字就可以不被拷贝

lambda

lambda表达式语法

lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
lambda 表达式语法使用层面而言没有类型,所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象。

lambda 表达式的格式: [capture-list] (parameters)-> return type {function boby }

[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。

(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 () 一起省略

->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{function boby}函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

例如:加法lambda函数

auto add1 = [](int x, int y)->int {return x + y; }

lanbda不能用来递归,没有函数名也递归不了

只能用当前lambda局部域和捕捉的对象和全局对象
 

捕捉列表

lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。

第一种捕捉方式是在捕捉列表中显示的传值捕捉传引用捕捉,捕捉的多个变量用逗号分割。[x,y,&z] 表示 x 和 y 值捕捉,z 引用捕捉。在捕捉列表中,&并不是取地址的符号,而是引用

第二种捕捉方式是在捕捉列表中隐式捕捉我们在捕捉列表写一个 = 表示隐式值捕捉,在捕捉列表写一个 & 表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。隐式捕捉实际上不是全部都捕捉,而是使用哪个才捕捉哪个。

第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x] 表示其他变量隐式值捕捉,x 引用捕捉;[&, x, y] 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 & 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。

lambda 表达式如果在函数局部域中,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

默认情况下,lambda 的值捕捉是被 const 修饰的,也就是说传值捕捉的过来的对象不能修改,而传引用捕捉可以被修改,引用捕捉的值在里面被修改后外面的值也会被改变,mutable 加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参使用该修饰符后,参数列表不可省略(即使参数为空)。

lambda的应用

在学习 lambda 表达式之前,我们所使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda 的应用还是很广泛的,以后我们会不断接触到。

lambda的原理

lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是仿函数对象,也就是说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。

底层是仿函数 ,捕捉列表相当于成员对象
捕捉列表捕捉到的东西就相当于函数中的成员变量
lambda使用时会调用编译器生成的仿函数operator()
lambda生成对应的仿函数是在编译时,不会影响运行时的效率


仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数 / 返回类型 / 函数体就是仿函数 operator () 的参数 / 返回类型 / 函数体,lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。

包装器

function

std::function 是⼀个类模板,也是⼀个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。

・函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function 的优势就是统⼀类型,对它们都可以进⾏包装,这样在很多地方就方便声明可调用对象的类型

如:function<int(int,int)> 第一个int是返回值类型,第二个和第三个是参数类型,只要返回值类型和参数类型匹配就可以使用包装器。

包装静态成员函数,成员函数要指定类域并且前面加&才能获取地址,使用function时需要加上this指针,由于包装器来包装成员函数的时候会使用.*去调用this指针,所以才包装成员函数的时候传this指针时传指针,引用,普通类型甚至匿名对象都可以

class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}

	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}

private:
	int _n;
};

int main()
{
   function<int(int, int)> f4 = &Plus::plusi;
   Plus pl;
   cout << f5(&pl, 1.111, 1.1) << endl;
   return 0;
}

bind

bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把它看做⼀个函数适配器,对接收的 fn 可调用对象进行处理后返回⼀个可调用对象。bind 可以⽤来调整参数个数和参数顺序。bind 也在<functional>这个头文件中。

调用 bind 的⼀般形式:auto newCallable = bind (callable,arg_list); 其中 newCallable 本身是⼀个可调用对象,arg_list 是⼀个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。

arg_list 中的参数可能包含形如_n 的名字,其中 n 是⼀个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1/_2/_3.... 这些占位符放到 placeholders 的⼀个命名空间中。

#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int Sub(int a, int b)
{
 return (a - b) * 10;
}

int SubX(int a, int b, int c)
{
 return (a - b - c) * 10;
}

int main()
{
 auto sub1 = bind(Sub, _1, _2);
 cout << sub1(10, 5) << endl;

 auto sub2 = bind(Sub, _2, _1);
 cout << sub2(10, 5) << endl;

 // 调整参数个数 (常⽤) 
 auto sub3 = bind(Sub, 100, _1);
 cout << sub3(5) << endl;
 auto sub4 = bind(Sub, _1, 100);
 cout << sub4(5) << endl;

 // 绑死第1个参数 
 auto sub5 = bind(SubX, 100, _1, _2);
 cout << sub5(5, 1) << endl;

 return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/937160.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《知识拓展 · 统一建模语言UML》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

轻量级日志管理平台:Grafana Loki搭建及应用(详细篇)

前言 Grafana Loki是Grafana Lab团队提供的一个水平可扩展、高可用性、多租户的日志聚合系统&#xff0c;与其他日志系统不同的是&#xff0c;Loki最初设计的理念是为了为日志建立标签索引&#xff0c;而非将原日志内容进行索引。 现在目前成熟的方案基本上都是&#xff1a;L…

【原生js案例】如何让你的网页实现图片的按需加载

按需加载&#xff0c;这个词应该都不陌生了。我用到你的时候&#xff0c;你才出现就可以了。对于一个很多图片的网站&#xff0c;按需加载图片是优化网站性能的一个关键点。减少无效的http请求&#xff0c;提升网站加载速度。 感兴趣的可以关注下我的系列课程【webApp之h5端实…

用于卫星影像间接RPC模型精化的通用光束法平差方法

引言 介绍了通用RPC模型的表达式&#xff0c;which has been down to death 描述了RPC模型产生误差的原因——主要与定义传感器方位的姿态角有关。 每个影像都会对应一个三维点云&#xff0c;但是对同一地物拍摄的不同影像对应出来的三维点云是不一样的&#xff0c;所以才需…

搭建Tomcat(一)---SocketServerSocket

目录 引入1 引入2--socket 流程 Socket&#xff08;应用程序之间的通讯保障&#xff09; 网卡(计算机之间的通讯保障) 端口 端口号 实例 client端 解析 server端 解析 相关方法 问题1&#xff1a;ServerSocket和Socket有什么关系&#xff1f; ServerSocket Soc…

玩转个性地图样式!蜂鸟视图蜂鸟云主题编辑器正式上线

当地图不再只是冷冰冰的数据呈现&#xff0c;而是具有美感、适应多场景需求的设计作品时&#xff0c;地图应用的价值也随之提升。 蜂鸟视图推出全新“主题编辑器”功能&#xff0c;助你轻松定制个性化地图样式&#xff0c;赋予地图更多创意与生命力&#xff01; 一、主题编辑器…

【Figma_01】Figma软件初始与使用

Figma初识与学习准备 背景介绍软件使用1.1 切换主题1.2 官方社区 设计界面2.1 创建一个项目2.2 修改文件名2.3 四种模式2.4 新增界面2.5 图层2.6 工具栏2.7 属性栏section透明度和圆角改变多边形的边数渐变效果描边设置阴影等特效拖拽相同的图形 背景介绍 Ul设计:User Interfa…

MATLAB中all,any函数的应用

all表示要查的范围内全非 0 0 0返回 1 1 1&#xff0c;否则返回 0 0 0 any表示要查的范围内有一个非 0 0 0返回 1 1 1&#xff0c;否则返回 0 0 0 向量和矩阵都可以使用&#xff0c;在矩阵中&#xff0c;可以通过1(看列)或2(看行)设置维度 a l l all all和 a n y any any函数…

.NET 9 已发布,您可以这样升级或更新

.NET 9 已经发布&#xff0c;您可能正在考虑更新您的 ASP.NET Core 应用程序。 我们将介绍更新应用程序所需的内容。从更新 Visual Studio 和下载 .NET SDK 到找出可能破坏应用程序的任何重大更改。 下载 .NET 9 SDK 这些是下载 .NET 9 SDK 所需的步骤。 更新 Visual Studi…

服务器数据恢复—热备盘上线过程中硬盘离线导致raid5阵列崩溃的数据恢复案例

服务器数据恢复环境&#xff1a; 两组分别由4块SAS接口硬盘组建的raid5阵列&#xff0c;两组raid5阵列划分LUN并由LVM管理&#xff0c;格式化为EXT3文件系统。 服务器故障&#xff1a; RAID5阵列中一块硬盘未知原因离线&#xff0c;热备盘自动激活上线替换离线硬盘。在热备盘上…

Mac上使用ln指令创建软链接、硬链接

在Mac、Linux和Unix系统中&#xff0c;软连接&#xff08;Symbolic Link&#xff09;和硬连接&#xff08;Hard Link&#xff09;是两种不同的文件链接方式。它们的主要区别如下&#xff1a; 区别&#xff1a; 硬连接&#xff1a; 不能跨文件系统。不能链接目录&#xff08;为…

PCIe学习笔记

PCIE高速串行数据总线 当拿到一块板子 比如你要用到PCIE 首先要看这块板子的原理图 一般原理图写的是 PCI express 表示PCIE 以下是Netfpga为例下的PCIE插口元件原理图 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/01dc604fbdc847e8998a978c83c7b2eb.png 一般主…

Elasticsearch Kibana (windows版本) 安装和启动

目录 1.安装 2.启动 elasticsearch 3.启动 kibana 1.安装 elasticsearch下载&#xff0c;官网链接&#xff1a; Download Elasticsearch | Elastichttps://www.elastic.co/downloads/elasticsearch kibana下载&#xff0c;官网链接&#xff1a; Download Kibana Free | G…

linux下查看nginx的安装路径

一般会安装在默认位置下&#xff1a;/usr/local/openresty/nginx 或/usr/local/nginx 查看nginx运行进程&#xff0c;mast process 后面一般是nginx 的安装目录 ps -aux|grep nginx执行ls -l /proc/进程号/exe 会打印出安装/运行位置 ps -aux|grep nginx ls -l /proc/进程号/ex…

strongswan构建测试环境

make-testing脚本文件负责构建strongswan的虚拟化测试系统。位于目录strongswan-5.9.14/testing/&#xff0c;需要以管理员身份运行make-testing。生成测试用到的虚拟客户机镜像&#xff0c;KVM虚拟机和虚拟网络的配置文件位于目录:config/kvm。 ~/strongswan-5.9.14/testing$…

以太网链路详情

文章目录 1、交换机1、常见的概念1、冲突域2、广播域3、以太网卡1、以太网卡帧 4、mac地址1、mac地址表示2、mac地址分类3、mac地址转换为二进制 2、交换机的工作原理1、mac地址表2、交换机三种数据帧处理行为3、为什么会泛洪4、转发5、丢弃 3、mac表怎么获得4、同网段数据通信…

噪杂环境(房车改装市场)离线语音通断器模块

一直在坚持&#xff0c;却很难有机会上热门&#xff0c;在现在这个以流量为导向的时代&#xff0c;貌似很难靠所谓的坚守和热爱把产品成功的推向市场了。目前的客户仍然是以老客户为主&#xff0c;应用场景主要是房车改装&#xff0c;根据九客户的需求定制化一些模块。因为没有…

Android实现RecyclerView边缘渐变效果

Android实现RecyclerView边缘渐变效果 1.前言&#xff1a; 是指在RecyclerView中实现淡入淡出效果的边缘效果。通过这种效果&#xff0c;可以使RecyclerView的边缘在滚动时逐渐淡出或淡入&#xff0c;以提升用户体验。 2.Recyclerview属性&#xff1a; 2.1、requiresFading…

操作系统(10)存储器的层次结构

前言 操作系统存储器的层次结构是一个复杂而有序的系统&#xff0c;它旨在提供不同速度、容量和成本的存储设备&#xff0c;以满足计算机系统中各种数据存取需求。 一、层次结构概述 操作系统存储器的层次结构通常包括多个层次&#xff0c;从高速到低速、从高成本到低成本排列。…

数据库中的代数运算

这些代数基本运算通常被封装在数据库查询语言中&#xff0c;如SQL中的SELECT、FROM、WHERE等子句&#xff0c;使得用户可以更方便地对数据库进行查询和处理。 下面的介绍基于以下两个关系来说明&#xff1a; 传统的集合运算 并&#xff08;∪&#xff09; 合并两个关系中的元组…