【C++】带你发掘swap函数的秘密
发现问题
swap函数是C++标准库<algorithm>里的一个常见函数,用于交换两个变量的值。如果你写过代码,相信交换两个变量的值对于你来说应该是易如反掌,甚至还可以想到多种方法来实现它。在我之前的认知里,C++里的swap函数是一个没有什么技术含量的函数,不过是一个可以交换两个变量的值的模板函数,除了方便一点点,其他也没有什么了,不是么?
直到最近,我才发现,swap函数并不是我想象的那样简单。它的背后可以发掘到一些有意思的内容。于是就有了这篇文章。
swap函数除了可以对基本数据类型变量(如int,double,char等等)进行交换以外,还可以交换一些复杂的数据类型(如string类)的值。值得一提的是,这种交换借助C++11中的move移动语义,对复杂的数据无需进行大量的复制操作。比如交换两个string,只需交换两个string变量中的指针,即可完成它们的交换,无需多次进行串的拷贝。
基于move移动语义实现的swap源码如下:
template<typename T>
void swap(T &a,T &b) noexcept
{ T temp = std::move(a);a = std::move(b);b = std::move(temp);
}
move函数相关的机制已经超出了本文能够的讨论范围,这里暂时不予过多的深究。
我们需要知道的是,借助move语义,交换复杂数据结构(C++中的class、struct)的效率将得到有效的改善。
这个时候,你可能会发现我们好像把数组给漏了,两个数组是不是也可以呢?
我们不妨在main函数中写入如下代码:
//compile error
char ss1[] = "889", ss2[] = "888923932";
swap(ss1, ss2);
可惜,编译器立马爆出一堆错误,错误原因:“no matching function for call to 'swap(char[4], char[10])' ”。
让我们在回过头去看看,前面swap函数的定义。哦!原来得保证传入的两个参数类型相同!刚刚我们传入的两个参数,一个是长度为4的一维数组,一个是长度为10的一维数组,自然就报错了啦。我们让两个数组长度保持一致,这样就可以成功交换了。
然而数组名是一个指针常量,它是无法像普通的指针那样,被重新赋值的。难道swap函数可以交换两个指针常量的值?我们不妨测试一下:
char ss1[] = "889", ss2[] = "888";
//交换前
printf("交换前的地址: ss1=%p, ss2=%p\n", ss1, ss2);
printf("交换前的内容: ss1=%s, ss2=%s\n", ss1, ss2);
//交换
swap(ss1, ss2);
//交换后
printf("交换后的地址: ss1=%p, ss2=%p\n", ss1, ss2);
printf("交换后的内容: ss1=%s, ss2=%s\n", ss1, ss2);
交换前后的结果:
可以看到,两个数组的首地址并未发生变化。swap函数似乎是识别了数组类型,然后交换了两个数组的内容。我不禁陷入了沉思:怎么做到的?基于move移动语义吗?
令人惊讶的是,这里实现的一维数组交换并非直接通过前面已经实现的swap函数,为了验证这一点,我们可以把上面的swap代码Ctrl+V一下,换成其他名字,在程序中进行调用,就像下面这样:
//把函数名swap改为change
template<typename T>
void mySwap(T &a,T &b) noexcept
{ T temp = std::move(a);a = std::move(b);b = std::move(temp);
}int main(){char ss1[] = "889", ss2[] = "888";printf("交换前的地址: ss1=%p, ss2=%p\n", ss1, ss2);printf("交换前的内容: ss1=%s, ss2=%s\n", ss1, ss2);mySwap(ss1, ss2); printf("交换后的地址: ss1=%p, ss2=%p\n", ss1, ss2);printf("交换后的内容: ss1=%s, ss2=%s\n", ss1, ss2);return 0;
}
编译过后,编译器成功地报错了:
这就表明,前面给出的swap函数无法实现两个等长数组之间的元素交换。
可是既然都是同一个名字swap,想必得重载swap函数了吧。在网上我没有找到这种情形下的swap函数的实现,我们不妨自己来造一个轮子,探索一下它的实现机理。
为了和标准库中的swap函数区分开来,在后面的代码中,我们一律把自己实现的带swap功能的函数命名为mySwap。
方法论
我们的先确立一下实现思路:识别数组类型,获得数组长度,通过循环逐个交换数组元素,最终实现对两个数组内容的交换。
基于上面的思路,我们要实现的mySwap函数需要满足三个要求:
首先,它得和标准库里的swap函数一样是模板函数;
其次,它要能够自动识别出数组类型;
最后,它也要能在不增加函数参数的前提下,自动获取数组的长度。
两个容易出现的错误
在上面的思路和要求的指导下,我们可能会定义出下面这个样子的模板函数:
//错误示范1
template<typename T>
void mySwap(T a[], T b[]){//获取数组长度int SIZE = sizeof(a) / sizeof(a[0]);//其余代码略
}
这种定义方法是有问题的。前面已经提过,T类型的数组名本质上是一种指针常量,当数组以这种形式的作为函数的参数时,它会退化为指针。也就是说,在上面的函数体中,a实际上是一个指针,sizeof(a)实际上表示的是一个指针的大小。因此,数组的长度信息由于参数传递已经丢失,函数体中的SIZE并非真正的数组长度。
既然要获取数组的长度信息,我们加入一个非类型的模板参数SIZE,代码如下,这样是不是可以在编译的时候获取到数组长度了呢?
//错误示范2
template<typename T, int SIZE>
void mySwap(T a[SIZE], T b[SIZE]){//代码略
}
一定程度上是可以的,但是需要显式指定它的长度。
以交换长度为4的char数组ss1和ss2中元素为例。
mySwap(ss1, ss2); //会出现编译错误
mySwap<char, 4>(ss1, ss2); //编译通过
mySwap(ss1,ss2)编译错误的根源在于,这里的mySwap依然会把参数ss1,ss2解读为char指针,而不是长度为4的char数组,这样在无形中又丢弃了数组的长度信息,无法自动获知SIZE的大小,必须通过手动指定才能解决问题。
之前我们已经看到,标准库里的swap函数并不需要我们显示地给出数组长度,所以一定还有其他办法,可以让编译器自动获取到数组的长度。
前两个错误示例的关键问题在于,函数总是把传入的数组参数解读为指针,致使长度信息丢失。这就不得不让我们去思考:是否存在一种参数定义方式,可以保留数组的长度信息?
一种实现方案
还记得C语言里学过的数组指针的定义么?声明一个指向长度为4的char数组的数组指针p,写法如下:
char (*p)[4]; //指向长度为4的char数组
如果把这种指针作为函数的参数,由于它指向一个定长数组,数组的长度信息就能够保留下来。
然而,编译器在编译期进行类型推导时,不会把数组类型推导成对应数组指针,使用数组指针作为函数参数,将会编译报错。
幸运的是,我们已经接近答案了。
做一个小小的调整,把数组指针改为数组引用,作为函数参数,在原来已有的swap函数的基础上重载,这种实现下的mySwap函数,在交换两个数组元素时进行,函数调用上可以获得等同于标准库swap函数的体验。代码如下:
//正确实现
//基于move移动语义对一般数据进行交换的Swap
template<typename T>
void mySwap(T &a,T &b) noexcept
{ T temp = std::move(a);a = std::move(b);b = std::move(temp);
}//对数组元素进行交换的Swap
template<typename T, int SIZE>
void mySwap(T (&a)[SIZE], T (&b)[SIZE]) noexcept
{ for(int i = 0; i < SIZE; ++i)mySwap(a[i], b[i]);
}
如果希望重载后的mySwap函数拥有更高的运行效率,我们可以进行循环展开,使用某些奇技淫巧,如Duff's Device,进行优化。这方面暂时不讨论。
如果你仔细推敲一下,你会惊喜地发现,我们在这里实现的mySwap函数不但可以交换两个一维数组的数据,也可以交换二维数组、三维数组,甚至是 n 维数组的数据。实际上,一个 n 维数组可以看成是由若干个 n - 1 维数组构成的一维数组,交换两个 n 维数组,在编译时,编译器会自动为我们生成交换 n-1维、n-2维、······、2维、1维数组对应的mySwap函数。这种运作方式和函数递归是有些许神似之处的。
【C++】带你发掘swap函数的秘密相关推荐
- 《Effective C++》item25:考虑写出一个不抛异常的swap函数
std::swap()是个很有用的函数,它可以用来交换两个变量的值,包括用户自定义的类型,只要类型支持copying操作,尤其是在STL中使用的很多,例如: int main(int argc, _T ...
- swap()函数实现变量值的交换
1. 值传递交换值失败. #include<stdio.h> #include<stdlib.h> void swap(int a, int b) {int t;t = a;a ...
- 《Effective C++》学习笔记(条款25:考虑写出一个不抛异常的swap函数)
最近开始看<Effective C++>,为了方便以后回顾,特意做了笔记.若本人对书中的知识点理解有误的话,望请指正!!! swap函数是一个非常经典又有用的函数,除了它本身用来交换两个对 ...
- python四个带 key 参数的函数(max、min、map、filter)
四个带 key 参数的函数: max()点击查看详细 min()点击查看详细 map()点击查看详细 filter()点击查看详细 1)max(iterable, key) key:相当于对可迭代对象 ...
- 从Swap函数谈加法溢出问题
1. 初始题目 面试题:不用额外的变量,实现一个Swap函数,交换两个参数的值(问题1). 这个题目太经典,也太简单,有很多人都会不假思索结出答案: //Code 1 void Swap(i ...
- C++ Swap函数有几种写法?
该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处 本文章博客地址:https://cplusplus.blog.csdn.net/article/details/104344435 S ...
- C++STL中swap函数操作与内存地址改变的简析
写在前面 这篇文章主要讨论了STL中swap函数在交换2个容器的内容的时候是交换内存还是交换元素的问题.由于博主对C++的学习并不好,如果有什么错误恳请大家提出.下面会有一些代码展示一下swap函数在 ...
- 3分钟了解带参数的main函数
带参数的main函数 和大多数人一样,我原本接触的C语言main函数是不带参数的.如下: #include <stdio.h> int main() {char s[20];scanf(& ...
- c语言带默认参数吗,嵌入式C语言可以带“默认参数”的函数吗
(文章来源:嵌入式时代) 使用C++开发过程序时,定义函数可以指定默认参数,例如 void fun(int x, int y=3); 在调用 fun() 时第二个参数可以不传递,此时 fun() 函数 ...
最新文章
- matlab7.1(ERROR STARTING DESKTOP)解决
- 字符串去掉两端的引号_Python3.7知其然知其所以然-第六章 字符串
- 【中级软考】什么是非对称加密算法?
- highcharts图表高级入门之polar:极地图的基本配置以及一些关键配置说明
- mysql 获取结果_【原创】7. MYSQL++中的查询结果获取(各种Result类型)
- python颜色列表代码seaborn_在Python中Seaborn – 根据色调名称更改条形颜色
- 两行Python代码实现电影打分与推荐
- Python处理mongo结果中的ObjectId类型为字符串
- 通过HttpModule实现IP地址屏蔽功能
- 全国计算机考试 二级 office pdf,全国计算机等级考试二级MSoffice讲义看看[整理].pdf...
- 巴比特 | 元宇宙每日必读:42.46%的人年薪超过20万,元宇宙人才没有想象中的金贵?...
- 常见电路面试题20道
- 一阶线性常微分方程解法
- linux用什么命令查看ip,Linux中ip命令的使用实例
- linux ip_conntrack_max,解?Linux NAT ip_conntrack: table full的方法
- HomeBank-5.5.4-个人家庭记账分析软件(开源)
- 元素始终置于页面底部
- android app根目录下cache,Android 缓存目录 Context.getExternalFilesDir()和Context.getExternalCacheDir()方法...
- FPGA能做什么?比单片机厉害吗?
- 考勤系统java源码_考勤系统 - WEB源码|JSP源码/Java|源代码 - 源码中国