0

0

C++STL容器swap函数使用场景

P粉602998670

P粉602998670

发布时间:2025-09-12 11:44:01

|

744人浏览过

|

来源于php中文网

原创

C++ STL容器的swap函数通过交换内部指针和元数据,在O(1)时间内高效完成容器内容交换,避免了O(N)的元素复制,显著提升性能;同时因其noexcept特性,为异常安全提供强保证,尤其在copy-and-swap惯用法中确保操作的原子性与安全性;在泛型编程中,应结合ADL使用using std::swap; swap(a, b);以自动调用最优实现。

c++stl容器swap函数使用场景

C++ STL容器的

swap
函数,说白了,就是一种高效、安全地交换两个容器所有内容的机制。它不像我们想象的那样,一个元素一个元素地复制过去,而是通过底层指针或内部状态的交换,在常数时间内完成,尤其是在处理大型数据集时,其性能优势和异常安全性显得尤为突出。

解决方案

在C++ STL中,

swap
函数主要用于在两个同类型容器之间高效地交换所有元素。这个操作的核心价值在于其极高的效率——对于大多数标准容器(如
std::vector
,
std::list
,
std::deque
,
std::map
,
std::set
等),它通常能在O(1)常数时间内完成,这远比逐个元素复制或移动要快得多。其实现原理是交换容器内部指向数据块的指针、大小、容量等管理信息,而非实际的数据元素。这不仅带来了性能上的巨大飞跃,也提供了强大的异常安全保证,因为这个底层指针交换操作本身是不会抛出异常的。

C++ STL容器
swap
操作的性能优势体现在哪里?

谈到性能,

swap
在STL容器里简直是“作弊”般的存在。你想象一下,要交换两个装满了几百万个整数的
std::vector
,如果一个一个地复制,那得耗费多少CPU周期和内存带宽?这显然是不可接受的。而
swap
的魔法就在于,它根本不关心容器里有多少个元素。它做的,仅仅是交换两个容器内部指向它们各自实际数据存储区的指针,以及一些管理容器状态的元数据(比如大小、容量等)。

所以,无论你的

vector
里是10个元素还是1000万个元素,
swap
操作的耗时理论上都是一样的,因为它只涉及几个指针的交换,这是一个固定时间的操作,我们称之为O(1)复杂度。这与O(N)复杂度的元素复制操作形成了鲜明对比,N是容器中元素的数量。当N变得非常大时,这种性能差异是决定性的。

立即学习C++免费学习笔记(深入)”;

举个例子,假设我们有两个大

vector

#include 
#include 
#include 
#include  // for std::swap

int main() {
    std::vector vec1(10000000, 1); // 1000万个元素
    std::vector vec2(10000000, 2);

    // 使用容器成员swap
    auto start_member_swap = std::chrono::high_resolution_clock::now();
    vec1.swap(vec2);
    auto end_member_swap = std::chrono::high_resolution_clock::now();
    std::chrono::duration member_swap_ms = end_member_swap - start_member_swap;
    std::cout << "Member swap time: " << member_swap_ms.count() << " ms\n";

    // 假设我们要模拟一个“手动”复制交换,虽然实际代码中不会这么写,但为了对比性能
    // 这是一个非常低效的交换方式,仅作概念对比
    // std::vector temp = vec1; // 复制 vec1 到 temp (O(N))
    // vec1 = vec2;                 // 复制 vec2 到 vec1 (O(N))
    // vec2 = temp;                 // 复制 temp 到 vec2 (O(N))
    // 这里的注释代码如果真的运行,会耗时非常久,通常是几百毫秒甚至秒级,与swap的微秒级形成鲜明对比。

    // 为了更直观地展示O(N)和O(1)的区别,我们可以对比一下创建和销毁一个大vector的时间
    // 假设我们现在想“清空”一个vector并用另一个vector的内容填充它
    std::vector original_vec(5000000, 3);
    std::vector new_data_vec(5000000, 4);

    auto start_copy_assign = std::chrono::high_resolution_clock::now();
    original_vec = new_data_vec; // 复制赋值,O(N)
    auto end_copy_assign = std::chrono::high_resolution_clock::now();
    std::chrono::duration copy_assign_ms = end_copy_assign - start_copy_assign;
    std::cout << "Copy assignment time: " << copy_assign_ms.count() << " ms\n";

    // 而如果用swap来实现类似“清空并填充”的效果,配合move语义
    std::vector another_vec(5000000, 5);
    std::vector temp_empty; // 一个空容器
    auto start_swap_clear = std::chrono::high_resolution_clock::now();
    another_vec.swap(temp_empty); // 此时another_vec变空,temp_empty持有原数据
    // 如果我们想用new_data_vec的内容填充another_vec,可以这样做:
    // std::vector new_content(std::move(new_data_vec)); // 假设new_data_vec是临时的
    // another_vec.swap(new_content); // O(1)
    auto end_swap_clear = std::chrono::high_resolution_clock::now();
    std::chrono::duration swap_clear_ms = end_swap_clear - start_swap_clear;
    std::cout << "Swap to clear time (approx): " << swap_clear_ms.count() << " ms\n"; // 实际这里只算了swap操作本身

    return 0;
}

你会发现,

vec1.swap(vec2)
几乎是瞬间完成的,耗时微乎其微,通常在微秒级别。而如果进行
O(N)
的复制操作,耗时会显著增加,可能达到毫秒甚至秒级,差异非常明显。这就是
swap
在性能上的核心优势。

异常安全编程中,
swap
函数如何提供保障?

在C++中,异常安全是一个非常重要的概念,尤其是在资源管理方面。

swap
函数在这里扮演了一个关键角色,特别是在实现“强异常安全保证”时。强异常安全保证意味着,如果一个操作失败并抛出异常,程序的状态会保持不变,就像这个操作从未发生过一样。

经典的“copy-and-swap”惯用法就是基于

swap
函数来实现强异常安全。其基本思想是:当你需要修改一个对象的状态时,首先在一个临时副本上进行所有可能抛出异常的操作。如果这些操作都成功了,那么最后一步,就是用
swap
函数将临时副本的状态与原始对象的状态进行交换。因为STL容器的
swap
操作被设计为不抛出异常(noexcept),所以这个最终的交换步骤是绝对安全的。如果中间任何一步在临时副本上操作时抛出了异常,原始对象的状态则完全不受影响。

考虑一个简单的自定义类,它管理着一块动态分配的内存:

#include  // For std::swap
#include 
#include 
#include 

class MyBuffer {
private:
    int* data;
    size_t size;

public:
    // 构造函数
    MyBuffer(size_t s) : data(nullptr), size(s) {
        if (s > 0) {
            data = new int[s];
            // 模拟可能抛出异常的操作,例如填充数据
            for (size_t i = 0; i < s; ++i) {
                if (i == s / 2 && s > 10) { // 模拟在中间某个点抛出异常
                    // throw std::runtime_error("Simulated error during data fill");
                }
                data[i] = static_cast(i);
            }
        }
        std::cout << "MyBuffer constructed with size " << size << "\n";
    }

    // 析构函数
    ~MyBuffer() {
        delete[] data;
        std::cout << "MyBuffer destructed with size " << size << "\n";
    }

    // 拷贝构造函数
    MyBuffer(const MyBuffer& other) : data(nullptr), size(other.size) {
        if (other.size > 0) {
            data = new int[other.size];
            std::copy(other.data, other.data + other.size, data);
        }
        std::cout << "MyBuffer copy constructed with size " << size << "\n";
    }

    // 移动构造函数 (C++11)
    MyBuffer(MyBuffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
        std::cout << "MyBuffer move constructed with size " << size << "\n";
    }

    // swap 函数,noexcept 是关键
    void swap(MyBuffer& other) noexcept {
        using std::swap; // 引入std::swap,以防万一
        swap(data, other.data);
        swap(size, other.size);
        std::cout << "MyBuffer swap performed\n";
    }

    // 拷贝赋值运算符 - 使用 copy-and-swap idiom
    MyBuffer& operator=(MyBuffer other) noexcept { // 注意这里是传值参数,会调用拷贝构造函数
        swap(other); // 交换this和other的内容
        std::cout << "MyBuffer copy assignment performed\n";
        return *this;
    }

    size_t getSize() const { return size; }
};

int main() {
    try {
        MyBuffer b1(10); // 原始对象
        std::cout << "b1 size: " << b1.getSize() << "\n";

        // 尝试进行一个可能失败的赋值操作
        // MyBuffer b2(5); // 临时对象,用于模拟赋值
        // b2 = MyBuffer(20); // 赋值,这里会调用拷贝构造和copy-and-swap
        // std::cout << "b2 size: " << b2.getSize() << "\n";

        // 演示copy-and-swap的异常安全性
        std::cout << "\nAttempting copy assignment with potential failure:\n";
        MyBuffer b3(5);
        std::cout << "b3 initial size: " << b3.getSize() << "\n";
        try {
            // 假设MyBuffer(1000)在构造时可能抛出异常
            // MyBuffer temp(1000); // 如果这里抛异常,b3不受影响
            // b3 = temp; // 如果拷贝构造成功,再进行swap
            b3 = MyBuffer(1000); // 临时对象的构造如果在内部抛出异常,b3状态不变
        } catch (const std::runtime_error& e) {
            std::cerr << "Caught exception: " << e.what() << "\n";
        }
        std::cout << "b3 final size after potential failure: " << b3.getSize() << "\n"; // b3状态未变

    } catch (const std::exception& e) {
        std::cerr << "Main catch block: " << e.what() << "\n";
    }
    return 0;
}

在这个例子中,

MyBuffer
的拷贝赋值运算符
operator=
接受一个
MyBuffer
对象作为值参数。这意味着在进入
operator=
函数体之前,会先调用拷贝构造函数创建一个临时副本(或者如果传入的是右值,则调用移动构造函数)。如果这个拷贝构造函数抛出异常,那么
operator=
根本不会被调用,原始对象
*this
的状态保持不变。如果拷贝构造成功,
operator=
内部只执行一个
swap(other)
操作,这个操作是
noexcept
的,保证不会抛出异常。因此,整个赋值操作要么完全成功,要么在失败时保持原始对象状态不变,从而提供了强异常安全保证。

STL容器自身的

swap
成员函数也提供了
noexcept
保证,这意味着它们在执行交换时绝不会抛出异常。这使得它们成为构建更复杂、更健壮的异常安全代码的基石。

std::swap
与容器成员
swap
函数有何不同?何时选择使用?

这其实是一个C++编程中比较细致但也非常重要的问题,尤其是在编写泛型代码时。

首先,

std::swap
标准库中定义的一个通用函数模板,位于
头文件中。它的默认实现是基于拷贝构造函数和赋值运算符来交换两个对象:

template 
void swap(T& a, T& b) {
    T temp = std::move(a); // 或者 T temp(a);
    a = std::move(b);      // 或者 a = b;
    b = std::move(temp);   // 或者 b = temp;
}

这个默认的

std::swap
对于大多数类型来说是正确的,但它的效率是O(N)(如果T是一个容器),并且可能抛出异常(如果拷贝构造或赋值操作抛出异常)。

CoverPrise品牌官网建站系统1.1.6
CoverPrise品牌官网建站系统1.1.6

CoverPrise品牌官网建站系统现已升级!(原天伞WOS企业建站系统)出发点在于真正在互联网入口方面改善企业形象、提高营销能力,采用主流的前端开发框架,全面兼容绝大多数浏览器。充分考虑SEO,加入了门户级网站才有的关键词自动择取、生成,内容摘要自动择取、生成,封面图自动择取功能,极大地降低了使用中的复杂性,百度地图生成,更大程度地对搜索引擎友好。天伞WOS企业建站系统正式版具有全方位的场景化营

下载

然而,对于STL容器来说,它们都提供了自己的

swap
成员函数(例如
std::vector::swap
std::list::swap
等)。这些成员函数是专门为各自容器优化的,它们通过交换内部指针和元数据来实现O(1)的常数时间复杂度,并且是
noexcept
的。

那么问题来了,我们什么时候用哪个?

当你直接操作一个特定类型的STL容器时,比如你有一个

std::vector v1, v2;
,你直接调用
v1.swap(v2);
是完全正确的,也是最清晰、最直接的方式。编译器会直接找到并调用
std::vector
的成员
swap
函数。

但如果你在编写泛型代码,例如一个函数模板,它接受两个任意类型的参数,并希望交换它们,这时候就应该使用

std::swap
,但要配合一个重要的技巧:Argument-Dependent Lookup (ADL),也叫Koenig lookup。

正确的泛型

swap
模式是:

template 
void generic_swap_function(T& a, T& b) {
    using std::swap; // 引入std::swap到当前作用域
    swap(a, b);      // 调用无限定的swap
}

这里的

using std::swap;
语句将
std::swap
引入到当前作用域。然后,
swap(a, b);
的调用会首先通过ADL查找与
a
b
类型相关的
swap
函数(例如,如果
a
b
std::vector
,它会找到
std::vector
的成员
swap
),如果找到了,并且它是一个更匹配的非成员函数(或者通过成员函数被包装成非成员函数),就会优先调用它。如果ADL没有找到更特殊的
swap
,或者找到的不是非成员函数,那么就会回退到调用
std::swap
的通用模板。

对于STL容器而言,

std::swap
已经被重载以特化处理它们,所以当你对两个STL容器调用
std::swap(vec1, vec2)
时,它实际上会调用
vec1.swap(vec2)
。因此,
using std::swap; swap(a, b);
这种模式能够确保:

  1. 优先调用类型T的自定义
    swap
    函数
    (如果存在,并且是更优匹配),这对于自定义类型来说很重要。
  2. 对于STL容器,它会调用O(1)的成员
    swap
    函数
    ,而不是默认的O(N)复制版本。
  3. 对于没有自定义
    swap
    的类型,它会回退到
    std::swap
    的默认实现

所以,结论是:

  • 直接操作特定容器时:使用成员函数
    container.swap(other_container);
    ,代码意图明确。
  • 编写泛型代码时:使用
    using std::swap; swap(a, b);
    ,这是最健壮和高效的方式,它能利用ADL找到最合适的
    swap
    实现,包括STL容器的O(1)成员
    swap

swap
在特定算法和数据结构实现中的妙用

swap
函数不仅仅是交换两个容器内容那么简单,它在许多C++算法和数据结构实现中都扮演着精妙的角色。

一个典型的应用场景是

std::vector::shrink_to_fit()
。这个成员函数尝试减少
vector
的容量以适应其当前包含的元素数量,从而释放多余的内存。但
std::vector
并没有直接提供一个收缩容量的接口,因为它涉及到重新分配内存和移动元素,可能抛出异常。
shrink_to_fit()
的典型实现方式就是利用
swap

std::vector myVec = {1, 2, 3, 4, 5};
myVec.reserve(100); // 容量现在是100
// ... 之后移除了很多元素,只剩下5个

// 想要收缩容量
std::vector(myVec).swap(myVec);

这里发生了什么?

std::vector(myVec)
会创建一个新的临时
vector
,通过拷贝构造函数(或者移动构造函数,如果
myVec
是右值)从
myVec
中复制所有元素。这个新的临时
vector
的容量会恰好等于它所包含的元素数量(即
myVec.size()
)。然后,
swap
函数被调用,将这个临时
vector
的内部状态(包括紧凑的容量)与
myVec
交换。操作完成后,
myVec
现在拥有了紧凑的容量,而原来的大容量
vector
的资源则由临时对象持有,并在其生命周期结束时自动释放。这个技巧既高效又异常安全。

再比如,在一些排序算法中,

swap
是核心操作。例如,快速排序的
partition
步骤中,就需要频繁地交换元素以将数组划分为小于基准值和大于基准值的两部分。虽然这里通常是交换单个元素,但其效率和正确性对整个算法至关重要。

swap
也与C++11引入的移动语义有着紧密的联系。虽然
std::move
用于将一个对象转换为右值引用以启用移动构造或移动赋值,但
swap
本身就是一种高效的资源转移方式。在某些情况下,通过
swap
来“窃取”另一个对象的资源(比如在一个对象被销毁前将其资源转移给另一个对象),可以实现类似移动语义的效果,尤其是在没有明确移动构造/赋值操作的旧代码库中。

最后,在实现一些自定义数据结构时,

swap
提供了一个强大的原语。例如,你可能需要实现一个自定义的哈希表,当需要调整表大小时,可以创建一个新的、更大的表,将旧表中的元素重新哈希并插入到新表中,最后通过
swap
来原子地替换旧表。这确保了在调整大小过程中,如果发生错误,旧表仍然保持有效状态,从而提供了强大的异常安全保障。
swap
在这里不仅仅是性能的优化,更是实现健壮、可靠代码的关键工具

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1436

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

226

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

85

2025.10.17

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

313

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

522

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

49

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

190

2025.08.29

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

529

2023.12.01

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

150

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C# 教程
C# 教程

共94课时 | 5.8万人学习

C 教程
C 教程

共75课时 | 3.8万人学习

C++教程
C++教程

共115课时 | 10.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号