【C++】带你发掘swap函数的秘密

本文探讨了C++中swap函数的巧妙之处,特别是其在处理复杂数据类型如string时利用move语义提高效率。同时,文章揭示了swap函数无法直接用于不同长度的一维数组交换的问题,并分析了错误尝试,如通过sizeof获取数组长度的陷阱。作者提出了一种解决方案,即使用数组引用来保留长度信息,实现了一种能交换任意维度数组的mySwap函数,该函数不仅适用于一维数组,也适用于多维数组。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

发现问题

         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++ 中 `std::swap` 函数的用法 在 C++ 标准库中,`std::swap` 是一个用于交换两个对象值的标准模板函数。此功能位于 `<utility>` 头文件中[^1]。 #### 基本语法 ```cpp namespace std { template<class T> void swap(T& a, T& b); } ``` 该函数接受两个参数 `a` 和 `b` 的引用,并将它们的内容互换。对于大多数内置类型以及标准容器(如 `std::vector`, `std::list`),可以直接调用 `std::swap` 来实现高效的元素交换操作。 #### 使用示例 下面展示了一个具体的例子来说明如何利用 `std::swap` 进行向量之间的数据交换: ```cpp #include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> vec1 = {"a", "b", "c"}; std::vector<std::string> vec2 = {"d", "e", "f"}; // 调用 std::swap 实现两者的值互换 std::swap(vec1, vec2); // 输出交换后的第一个向量内容 for (const auto& s : vec1) { std::cout << s << ' '; } std::cout << '\n'; // 输出第二个向量中的字符串序列 for (const auto& s : vec2) { std::cout << s << ' '; } std::cout << '\n'; return 0; } ``` 上述代码片段创建了两个包含不同字符序列的字符串向量,在执行完 `std::swap` 后,原本属于 `vec1` 的元素被转移到了 `vec2` 中,反之亦然。 #### 性能考量 当处理大型对象或者复杂自定义类型的实例时,直接复制可能会来较高的开销;而通过移动语义支持下的 `std::swap` 可以显著提高效率,因为它通常只需要转移指针而非实际拷贝整个对象的数据成员。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值