发现问题

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函数的秘密相关推荐

  1. 《Effective C++》item25:考虑写出一个不抛异常的swap函数

    std::swap()是个很有用的函数,它可以用来交换两个变量的值,包括用户自定义的类型,只要类型支持copying操作,尤其是在STL中使用的很多,例如: int main(int argc, _T ...

  2. swap()函数实现变量值的交换

    1. 值传递交换值失败. #include<stdio.h> #include<stdlib.h> void swap(int a, int b) {int t;t = a;a ...

  3. 《Effective C++》学习笔记(条款25:考虑写出一个不抛异常的swap函数)

    最近开始看<Effective C++>,为了方便以后回顾,特意做了笔记.若本人对书中的知识点理解有误的话,望请指正!!! swap函数是一个非常经典又有用的函数,除了它本身用来交换两个对 ...

  4. python四个带 key 参数的函数(max、min、map、filter)

    四个带 key 参数的函数: max()点击查看详细 min()点击查看详细 map()点击查看详细 filter()点击查看详细 1)max(iterable, key) key:相当于对可迭代对象 ...

  5. 从Swap函数谈加法溢出问题

    1.      初始题目 面试题:不用额外的变量,实现一个Swap函数,交换两个参数的值(问题1). 这个题目太经典,也太简单,有很多人都会不假思索结出答案: //Code 1 void Swap(i ...

  6. C++ Swap函数有几种写法?

    该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处 本文章博客地址:https://cplusplus.blog.csdn.net/article/details/104344435 S ...

  7. C++STL中swap函数操作与内存地址改变的简析

    写在前面 这篇文章主要讨论了STL中swap函数在交换2个容器的内容的时候是交换内存还是交换元素的问题.由于博主对C++的学习并不好,如果有什么错误恳请大家提出.下面会有一些代码展示一下swap函数在 ...

  8. 3分钟了解带参数的main函数

    带参数的main函数 和大多数人一样,我原本接触的C语言main函数是不带参数的.如下: #include <stdio.h> int main() {char s[20];scanf(& ...

  9. c语言带默认参数吗,嵌入式C语言可以带“默认参数”的函数吗

    (文章来源:嵌入式时代) 使用C++开发过程序时,定义函数可以指定默认参数,例如 void fun(int x, int y=3); 在调用 fun() 时第二个参数可以不传递,此时 fun() 函数 ...

最新文章

  1. matlab7.1(ERROR STARTING DESKTOP)解决
  2. 字符串去掉两端的引号_Python3.7知其然知其所以然-第六章 字符串
  3. 【中级软考】什么是非对称加密算法?
  4. highcharts图表高级入门之polar:极地图的基本配置以及一些关键配置说明
  5. mysql 获取结果_【原创】7. MYSQL++中的查询结果获取(各种Result类型)
  6. python颜色列表代码seaborn_在Python中Seaborn – 根据色调名称更改条形颜色
  7. 两行Python代码实现电影打分与推荐
  8. Python处理mongo结果中的ObjectId类型为字符串
  9. 通过HttpModule实现IP地址屏蔽功能
  10. 全国计算机考试 二级 office pdf,全国计算机等级考试二级MSoffice讲义看看[整理].pdf...
  11. 巴比特 | 元宇宙每日必读:42.46%的人年薪超过20万,元宇宙人才没有想象中的金贵?...
  12. 常见电路面试题20道
  13. 一阶线性常微分方程解法
  14. linux用什么命令查看ip,Linux中ip命令的使用实例
  15. linux ip_conntrack_max,解?Linux NAT ip_conntrack: table full的方法
  16. HomeBank-5.5.4-个人家庭记账分析软件(开源)
  17. 元素始终置于页面底部
  18. android app根目录下cache,Android 缓存目录 Context.getExternalFilesDir()和Context.getExternalCacheDir()方法...
  19. FPGA能做什么?比单片机厉害吗?
  20. 考勤系统java源码_考勤系统 - WEB源码|JSP源码/Java|源代码 - 源码中国

热门文章

  1. 15种下载文件的方法
  2. 怜惜,才是最真挚的爱情
  3. ios的皇室战争怎么转android,皇室战争苹果怎么转安卓?小问题,让小编来告诉你...
  4. Fabric.js 使用图片遮盖画布(前景图)
  5. Video Style Transfer汇总
  6. AND和OR同时使用需注意优先级
  7. 深圳社交电商排名怎么样,企业该如何挑选
  8. contains方法。
  9. 亲民时尚的格式转换工具——炫酷乐转码先生使用感悟
  10. 六大门派身份识别 (20 分)