【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函数。这种运作方式和函数递归是有些许神似之处的。

  • 43
    点赞
  • 142
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值