《C++ Primer》导学系列:第 9 章 - 顺序容器

9.1 顺序容器概述

C++标准库提供了几种顺序容器,用于存储和管理元素的有序集合。主要的顺序容器包括vectordequelistforward_list。每种容器都有其独特的特性和适用场景。

9.1.1 vector

vector是一个动态数组,支持快速的随机访问和在末尾高效的插入和删除操作。它的大小可以动态调整,并且内存是连续的。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.push_back(6);

    for (const auto& elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.1.2 deque

deque(双端队列)支持在两端进行高效的插入和删除操作。它提供了类似于vector的随机访问能力,但内存不是连续的。

示例代码
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3, 4, 5};
    deq.push_front(0);
    deq.push_back(6);

    for (const auto& elem : deq) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.1.3 list

list是一个双向链表,支持在任何位置进行高效的插入和删除操作,但不支持随机访问。

示例代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    lst.push_front(0);
    lst.push_back(6);

    for (const auto& elem : lst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.1.4 forward_list

forward_list是一个单向链表,只支持单向遍历,适用于内存有限且插入删除操作频繁的场景。

示例代码
#include <iostream>
#include <forward_list>

int main() {
    std::forward_list<int> flst = {1, 2, 3, 4, 5};
    flst.push_front(0);

    for (const auto& elem : flst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.1.5 容器选择

选择合适的容器是编写高效程序的关键。以下是一些指导原则:

  • 使用vector:当需要频繁随机访问元素或在末尾插入删除元素时。
  • 使用deque:当需要在两端进行高效的插入和删除操作时。
  • 使用list:当需要在中间进行频繁插入和删除操作且不需要随机访问时。
  • 使用forward_list:当内存有限且只需要单向遍历时。

重点与难点分析

重点

  1. 了解各种顺序容器的特性:理解每种容器的特性和适用场景,选择最合适的容器来提高程序的性能。
  2. 掌握基本操作:熟悉顺序容器的基本操作,如插入、删除、遍历等。

难点

  1. 容器的性能权衡:在选择容器时,需要根据具体的使用场景权衡性能,例如随机访问的效率、插入删除操作的效率等。
  2. 容器的内存管理:理解容器在内存管理方面的差异,例如vector的连续内存分配和list的非连续内存分配。

练习题解析

  1. 练习9.1:编写一个程序,使用vector存储一组整数,并输出所有元素。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (const auto& elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.2:编写一个程序,使用deque存储一组整数,并在两端插入元素,输出所有元素。
    • 示例代码
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3, 4, 5};
    deq.push_front(0);
    deq.push_back(6);
    for (const auto& elem : deq) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.3:编写一个程序,使用list存储一组整数,并在两端插入元素,输出所有元素。
    • 示例代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    lst.push_front(0);
    lst.push_back(6);
    for (const auto& elem : lst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.4:编写一个程序,使用forward_list存储一组整数,并在开头插入元素,输出所有元素。
    • 示例代码
#include <iostream>
#include <forward_list>

int main() {
    std::forward_list<int> flst = {1, 2, 3, 4, 5};
    flst.push_front(0);
    for (const auto& elem : flst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

总结与提高

本节总结

  1. 了解了vectordequelistforward_list等顺序容器的基本特性和适用场景。
  2. 掌握了这些容器的基本操作,包括插入、删除和遍历。
  3. 理解了如何根据具体的应用场景选择最合适的容器,以提高程序的性能。

提高建议

  1. 多练习容器操作:通过编写更多涉及顺序容器的程序,熟悉各种容器的基本操作和特性。
  2. 优化容器选择:在实际项目中,根据具体需求选择最合适的容器,优化程序的性能。
  3. 深入理解容器实现:通过阅读标准库的实现代码或相关书籍,深入理解顺序容器的内部实现原理,提高编写高效代码的能力。

9.2 容器库概览

C++标准库提供了一套丰富的容器,用于存储和管理数据。每种容器都有其独特的特性和适用场景。在本节中,我们将概览一些常用的容器功能,并介绍它们的基本用法和特性。

9.2.1 迭代器

迭代器是用于遍历容器中元素的对象,类似于指针。通过迭代器,可以实现对容器中元素的访问和操作。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}

9.2.2 容器类型成员

每个容器都有一些特定的类型成员,用于表示迭代器类型、大小类型等。例如,vector<int>::iterator表示vector<int>的迭代器类型。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::vector<int>::iterator it = vec.begin();
    while (it != vec.end()) {
        std::cout << *it << " ";
        ++it;
    }
    std::cout << std::endl;
    return 0;
}

9.2.3 begin和end成员

所有标准库容器都提供beginend成员函数,用于获取指向容器第一个元素和最后一个元素之后位置的迭代器。

示例代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    for (auto it = lst.begin(); it != lst.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}

9.2.4 容器定义和初始化

容器可以通过各种方式进行定义和初始化,包括默认构造函数、初始值列表、拷贝构造函数等。

示例代码
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq1;  // 默认构造函数
    std::deque<int> deq2 = {1, 2, 3, 4, 5};  // 初始值列表
    std::deque<int> deq3(deq2);  // 拷贝构造函数

    for (const auto& elem : deq3) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

9.2.5 赋值和swap

容器支持赋值操作,可以将一个容器的内容赋值给另一个容器。swap函数用于交换两个容器的内容。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {4, 5, 6};

    vec1 = vec2;  // 赋值操作
    vec1.swap(vec2);  // 交换操作

    for (const auto& elem : vec1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    for (const auto& elem : vec2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.2.6 关系运算符

标准库容器支持关系运算符,如==!=<><=>=。这些运算符用于比较两个容器的内容。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {1, 2, 3};
    std::vector<int> vec3 = {4, 5, 6};

    if (vec1 == vec2) {
        std::cout << "vec1 is equal to vec2" << std::endl;
    } else {
        std::cout << "vec1 is not equal to vec2" << std::endl;
    }

    if (vec1 < vec3) {
        std::cout << "vec1 is less than vec3" << std::endl;
    } else {
        std::cout << "vec1 is not less than vec3" << std::endl;
    }

    return 0;
}

重点与难点分析

重点

  1. 迭代器的使用:掌握如何使用迭代器遍历容器元素。
  2. 容器类型成员:理解容器的类型成员及其用途。
  3. begin和end成员:熟悉容器的beginend成员函数的用法。
  4. 容器的定义和初始化:掌握容器的各种初始化方式。
  5. 赋值和swap:理解容器的赋值操作和swap函数的用法。
  6. 关系运算符:掌握如何使用关系运算符比较容器。

难点

  1. 迭代器的生命周期:理解迭代器在容器操作中的有效性,避免使用失效的迭代器。
  2. 容器的类型成员:掌握不同容器的类型成员及其适用场景。
  3. 关系运算符的使用:理解容器比较的原理,正确使用关系运算符进行容器比较。

练习题解析

  1. 练习9.5:编写一个程序,使用迭代器遍历并输出一个vector中的所有元素。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.6:编写一个程序,定义一个list容器,并使用其类型成员定义一个迭代器。
    • 示例代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    std::list<int>::iterator it = lst.begin();
    while (it != lst.end()) {
        std::cout << *it << " ";
        ++it;
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.7:编写一个程序,使用deque容器,并使用beginend成员函数遍历其元素。
    • 示例代码
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3, 4, 5};
    for (auto it = deq.begin(); it != deq.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.8:编写一个程序,定义和初始化一个vector容器,并将其内容赋值给另一个vector容器。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2;
    vec2 = vec1;

    for (const auto& elem : vec2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习9.9:编写一个程序,使用swap函数交换两个vector容器的内容,并输出交换后的结果。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {4, 5, 6};

    std::cout << "Before swap:" << std::endl;
    std::cout << "vec1: ";
    for (const auto& elem : vec1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    std::cout << "vec2: ";
    for (const auto& elem : vec2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    vec1.swap(vec2);

    std::cout << "After swap:" << std::endl;
    std::cout << "vec1: ";
    for (const auto& elem : vec1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    std::cout << "vec2: ";
    for (const auto& elem : vec2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.10:编写一个程序,使用关系运算符比较两个vector容器,并输出比较结果。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {1, 2, 3};
    std::vector<int> vec3 = {4, 5, 6};

    if (vec1 == vec2) {
        std::cout << "vec1 is equal to vec2" << std::endl;
    } else {
        std::cout << "vec1 is not equal to vec2" << std::endl;
    }

    if (vec1 != vec3) {
        std::cout << "vec1 is not equal to vec3" << std::endl;
    } else {
        std::cout << "vec1 is equal to vec3" << std::endl;
    }

    if (vec1 < vec3) {
        std::cout << "vec1 is less than vec3" << std::endl;
    } else {
        std::cout << "vec1 is not less than vec3" << std::endl;
    }

    return 0;
}

总结与提高

本节总结

  1. 掌握了迭代器的基本概念和用法,能够使用迭代器遍历和操作容器中的元素。
  2. 理解了容器的类型成员及其用途,能够在代码中使用这些类型成员定义迭代器和其他类型。
  3. 熟悉了beginend成员函数的用法,能够使用它们获取指向容器起始和末尾的迭代器。
  4. 掌握了容器的定义和初始化方法,能够使用各种方式定义和初始化容器。
  5. 理解了容器的赋值操作和swap函数的用法,能够在程序中进行容器内容的赋值和交换。
  6. 熟悉了容器的关系运算符,能够使用这些运算符比较容器的内容。

提高建议

  1. 多练习迭代器操作:通过编写更多涉及迭代器的程序,熟悉迭代器的各种操作,提高对容器元素的操作能力。
  2. 深入理解容器类型成员:通过查阅文档和实践,深入理解容器的类型成员及其应用场景,提高代码的可读性和可维护性。
  3. 优化容器的选择和使用:在实际项目中,根据具体需求选择最合适的容器,优化程序的性能。
  4. 熟练掌握关系运算符:在编写涉及容器比较的代码时,熟练使用关系运算符,提高代码的逻辑性和健壮性。

9.3 顺序容器操作

顺序容器提供了一系列的成员函数,用于对容器进行操作,包括插入、删除、访问和修改容器中的元素。本节将介绍这些操作,并通过示例代码展示它们的用法。

9.3.1 访问元素

顺序容器提供了一些成员函数和运算符用于访问容器中的元素。

  • at:返回指定位置的元素,并进行范围检查。
  • operator[]:返回指定位置的元素,不进行范围检查。
  • front:返回第一个元素。
  • back:返回最后一个元素。
示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::cout << "Element at index 2: " << vec.at(2) << std::endl;
    std::cout << "Element at index 2 using operator[]: " << vec[2] << std::endl;
    std::cout << "First element: " << vec.front() << std::endl;
    std::cout << "Last element: " << vec.back() << std::endl;
    return 0;
}

9.3.2 插入元素

顺序容器提供了一些成员函数用于在容器中插入元素。

  • push_back:在容器末尾插入元素。
  • push_front:在容器开头插入元素(适用于listdeque)。
  • insert:在指定位置插入单个元素或范围内的元素。
  • emplace_back:在容器末尾原地构造元素。
  • emplace:在指定位置原地构造元素。
示例代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3};
    lst.push_back(4);  // 在末尾插入元素
    lst.push_front(0);  // 在开头插入元素
    auto it = lst.begin();
    ++it;
    lst.insert(it, 5);  // 在指定位置插入元素

    std::list<int> new_elements = {6, 7, 8};
    lst.insert(it, new_elements.begin(), new_elements.end());  // 插入范围内的元素

    for (const auto& elem : lst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

9.3.3 删除元素

顺序容器提供了一些成员函数用于删除容器中的元素。

  • pop_back:删除容器末尾的元素。
  • pop_front:删除容器开头的元素(适用于listdeque)。
  • erase:删除指定位置或范围内的元素。
  • clear:删除容器中的所有元素。
示例代码
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3, 4, 5};
    deq.pop_back();  // 删除末尾的元素
    deq.pop_front();  // 删除开头的元素
    auto it = deq.begin();
    ++it;
    deq.erase(it);  // 删除指定位置的元素

    for (const auto& elem : deq) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    deq.clear();  // 删除所有元素
    std::cout << "Deque size after clear: " << deq.size() << std::endl;

    return 0;
}

9.3.4 特殊的forward_list操作

forward_list是单向链表,只支持单向遍历。它的一些操作与其他顺序容器有所不同:

  • before_begin:返回指向第一个元素之前的位置的迭代器。
  • insert_after:在指定位置之后插入元素。
  • emplace_after:在指定位置之后原地构造元素。
  • erase_after:删除指定位置之后的元素。
示例代码
#include <iostream>
#include <forward_list>

int main() {
    std::forward_list<int> flst = {1, 2, 3, 4, 5};
    auto before_begin = flst.before_begin();
    flst.insert_after(before_begin, 0);  // 在开头插入元素

    auto it = flst.begin();
    ++it;
    flst.insert_after(it, 6);  // 在指定位置之后插入元素
    flst.erase_after(it);  // 删除指定位置之后的元素

    for (const auto& elem : flst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

9.3.5 修改元素

顺序容器提供了一些成员函数用于修改容器中的元素。

  • assign:用新内容替换容器中的内容。
  • resize:改变容器的大小。
  • swap:交换两个容器的内容。
示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {4, 5, 6};

    vec1.assign(5, 10);  // 用新内容替换容器中的内容
    vec1.resize(3);  // 改变容器的大小

    for (const auto& elem : vec1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    vec1.swap(vec2);  // 交换两个容器的内容

    for (const auto& elem : vec1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.3.6 迭代器和范围操作

顺序容器提供了一些成员函数用于获取迭代器和执行范围操作。

  • begin:返回指向容器第一个元素的迭代器。
  • end:返回指向容器末尾的迭代器。
  • cbegin:返回指向容器第一个元素的常量迭代器。
  • cend:返回指向容器末尾的常量迭代器。
  • rbegin:返回指向容器最后一个元素的反向迭代器。
  • rend:返回指向容器第一个元素前一个位置的反向迭代器。
示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    std::cout << "Forward iteration: ";
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    std::cout << "Reverse iteration: ";
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    return 0;
}

9.3.7 容器操作可能使迭代器失效

在对容器进行某些操作时,迭代器可能会失效。以下是一些常见情况:

  • vectordeque等进行插入或删除操作后,所有的迭代器、指针和引用都可能失效。
  • listforward_list进行插入或删除操作时,只有插入或删除位置附近的迭代器会失效。
  • mapset进行插入操作时,现有的迭代器不会失效,但删除操作会使相关的迭代器失效。
示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = vec.begin();
    ++it;

    vec.insert(it, 10);  // 迭代器it在插入操作后失效

    // 访问失效迭代器会导致未定义行为
    // std::cout << *it << std::endl;  // 不要这样做!

    // 正确的做法是重新获取迭代器
    it = vec.begin();
    ++it;
    std::cout << *it << std::endl;

    return 0;
}

重点与难点分析

重点

  1. 访问元素:理解如何使用成员函数和运算符访问容器中的元素。
  2. 插入元素:掌握在容器中插入元素的方法,包括在末尾、开头、指定位置和范围内插入元素。
  3. **

删除元素**:掌握删除容器中元素的方法,包括删除末尾元素、开头元素、指定位置元素和范围内的元素。
4. 修改元素:理解如何用新内容替换容器中的内容,改变容器的大小和交换两个容器的内容。
5. 迭代器和范围操作:熟悉获取迭代器和使用迭代器遍历容器的方法。
6. 迭代器失效问题:理解容器操作可能导致的迭代器失效问题,并掌握避免使用失效迭代器的方法。

难点

  1. 迭代器的有效性:理解在容器操作(如插入、删除)之后,迭代器可能失效的情况,避免使用失效的迭代器。
  2. 容器的内存管理:掌握容器在进行插入、删除和修改操作时的内存管理机制,优化程序的性能。

练习题解析

  1. 练习9.11:编写一个程序,使用vector容器访问和修改元素,并输出修改后的结果。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec[2] = 10;  // 修改元素

    std::cout << "Modified vector: ";
    for (const auto& elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.12:编写一个程序,使用list容器插入和删除元素,并输出操作后的结果。
    • 示例代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3};
    lst.push_back(4);  // 插入元素
    lst.push_front(0);  // 插入元素
    lst.pop_back();  // 删除元素
    lst.pop_front();  // 删除元素

    std::cout << "Modified list: ";
    for (const auto& elem : lst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.13:编写一个程序,使用deque容器修改元素的内容,并使用beginend成员函数遍历其元素。
    • 示例代码
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3, 4, 5};
    deq[1] = 20;  // 修改元素

    std::cout << "Modified deque: ";
    for (auto it = deq.begin(); it != deq.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.14:编写一个程序,使用forward_list容器进行元素的插入、删除和访问操作。
    • 示例代码
#include <iostream>
#include <forward_list>

int main() {
    std::forward_list<int> flst = {1, 2, 3, 4, 5};
    auto before_begin = flst.before_begin();
    flst.insert_after(before_begin, 0);  // 在开头插入元素

    auto it = flst.begin();
    ++it;
    flst.insert_after(it, 6);  // 在指定位置之后插入元素
    flst.erase_after(it);  // 删除指定位置之后的元素

    std::cout << "Modified forward_list: ";
    for (const auto& elem : flst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.15:编写一个程序,使用vector容器进行迭代器的正向和反向遍历,并输出结果。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    std::cout << "Forward iteration: ";
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    std::cout << "Reverse iteration: ";
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    return 0;
}

总结与提高

本节总结

  1. 了解了顺序容器的基本操作,包括访问、插入、删除和修改元素的方法。
  2. 掌握了如何使用迭代器进行容器的遍历和范围操作。
  3. 理解了顺序容器的内存管理机制和迭代器失效的问题。

提高建议

  1. 多练习容器操作:通过编写更多涉及顺序容器操作的程序,熟悉各种操作方法,提高对容器的操作能力。
  2. 优化内存管理:在实际项目中,关注顺序容器的内存管理机制,优化程序的性能。
  3. 深入理解迭代器:通过阅读文档和实践,深入理解迭代器的特性和使用场景,避免使用失效的迭代器。

9.4 vector对象是如何增长的

vector是一种动态数组,能够根据需要自动调整其大小以容纳更多的元素。在添加新元素时,如果当前容量不足,vector会重新分配内存。了解vector的增长机制对于编写高效的代码至关重要。

9.4.1 容量和大小

vector大小(size)是当前存储的元素数量,而容量(capacity)是当前分配的内存能够存储的最大元素数量。当新元素添加到vector中且超过当前容量时,vector会重新分配内存,通常以双倍容量进行分配。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
        std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
    }
    return 0;
}

在这个示例中,随着元素的添加,vector的大小和容量会动态变化。观察输出,可以看到vector的容量是如何增加的。

9.4.2 重新分配内存

vector需要增长时,会重新分配内存。这涉及到分配新内存、将旧数据复制到新内存、然后释放旧内存。重新分配内存是一个相对昂贵的操作,因为它需要额外的时间和空间来复制数据。

示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;
    vec.reserve(5);  // 提前分配内存以减少重新分配的次数
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
        std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
    }
    return 0;
}

在这个示例中,使用reserve函数提前分配内存,可以减少vector在元素添加过程中重新分配内存的次数,从而提高性能。

9.4.3 容量管理

vector提供了一些成员函数用于管理其容量:

  • reserve:提前分配指定数量的内存,以减少在元素添加过程中重新分配内存的次数。
  • shrink_to_fit:减少vector的容量,使其等于当前大小,以节省内存。
示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::cout << "Initial size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;

    vec.reserve(20);
    std::cout << "After reserve(20): size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;

    vec.shrink_to_fit();
    std::cout << "After shrink_to_fit: size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;

    return 0;
}

在这个示例中,通过reserve函数增加vector的容量,通过shrink_to_fit函数减少vector的容量以适应当前大小。

重点与难点分析

重点

  1. 理解vector的大小和容量:掌握vectorsizecapacity的概念及其区别。
  2. 掌握vector的增长机制:理解vector在添加新元素时如何增长及其重新分配内存的机制。
  3. 容量管理:了解reserveshrink_to_fit等成员函数的用途,掌握如何有效管理vector的容量以优化性能。

难点

  1. 重新分配内存的影响:理解重新分配内存对性能的影响,学会通过合理使用reserve函数来优化程序性能。
  2. 容量和大小的关系:理解容量和大小之间的关系,以及如何在程序中进行容量管理。

练习题解析

  1. 练习9.16:编写一个程序,创建一个vector,并观察其在添加元素时的大小和容量变化。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;
    for (int i = 0; i < 15; ++i) {
        vec.push_back(i);
        std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
    }
    return 0;
}
  1. 练习9.17:编写一个程序,使用reserve函数提前分配内存,减少vector在添加元素过程中的重新分配次数,并比较性能。
    • 示例代码
#include <iostream>
#include <vector>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    std::vector<int> vec1;
    for (int i = 0; i < 100000; ++i) {
        vec1.push_back(i);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed1 = end - start;
    std::cout << "Without reserve: " << elapsed1.count() << " seconds" << std::endl;

    start = std::chrono::high_resolution_clock::now();
    std::vector<int> vec2;
    vec2.reserve(100000);
    for (int i = 0; i < 100000; ++i) {
        vec2.push_back(i);
    }
    end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed2 = end - start;
    std::cout << "With reserve: " << elapsed2.count() << " seconds" << std::endl;

    return 0;
}
  1. 练习9.18:编写一个程序,使用shrink_to_fit函数在vector中移除一些元素后减少其容量,并观察效果。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::cout << "Initial size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;

    vec.erase(vec.begin() + 5, vec.end());
    std::cout << "After erase: size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;

    vec.shrink_to_fit();
    std::cout << "After shrink_to_fit: size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;

    return 0;
}

总结与提高

本节总结

  1. 了解了vector的大小和容量的概念,理解了它们之间的区别。
  2. 掌握了vector在添加新元素时的增长机制及其重新分配内存的影响。
  3. 学会了使用reserveshrink_to_fit等成员函数管理vector的容量,以优化程序性能。

提高建议

  1. 合理管理容量:在需要大量添加元素时,使用reserve函数提前分配内存,减少重新分配内存的次数,以提高性能。
  2. 优化内存使用:在删除大量元素后,使用shrink_to_fit函数减少vector的容量,优化内存使用。
  3. 关注性能:在实际项目中,通过合理管理vector的容量和内存分配,优化程序性能,提升运行效率。

9.5 额外的string操作

C++标准库中的string类提供了丰富的成员函数,用于处理和操作字符串。在这一节中,我们将探讨一些额外的string操作,包括构造字符串的其它方法、改变字符串的其它方法、字符串搜索操作和数值转换。

9.5.1 构造string的其它方法

除了常见的构造方法外,string类还提供了其他一些构造字符串的方式:

  • 重复字符构造:使用一个字符重复指定次数来构造字符串。
  • 子字符串构造:从另一个字符串的子串构造新字符串。
  • 从C字符串构造:使用C风格的字符串(char*)构造string对象。
示例代码
#include <iostream>
#include <string>

int main() {
    // 使用重复字符构造字符串
    std::string str1(10, 'a');
    std::cout << "str1: " << str1 << std::endl;  // 输出:aaaaaaaaaa

    // 从另一个字符串的子串构造字符串
    std::string str2 = "Hello, World!";
    std::string str3(str2, 7, 5);
    std::cout << "str3: " << str3 << std::endl;  // 输出:World

    // 从C字符串构造字符串
    const char* cstr = "C-style string";
    std::string str4(cstr);
    std::cout << "str4: " << str4 << std::endl;  // 输出:C-style string

    return 0;
}

9.5.2 改变string的其它方法

除了常见的修改方法外,string类还提供了一些其他改变字符串的方法:

  • append:在字符串末尾追加另一个字符串。
  • replace:替换字符串中的部分内容。
  • insert:在指定位置插入字符串。
  • erase:删除字符串中的部分内容。
示例代码
#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";
  

    // 在末尾追加字符串
    str.append(", World");
    std::cout << "After append: " << str << std::endl;  // 输出:Hello, World

    // 替换字符串中的部分内容
    str.replace(7, 5, "Universe");
    std::cout << "After replace: " << str << std::endl;  // 输出:Hello, Universe!

    // 在指定位置插入字符串
    str.insert(5, " Beautiful");
    std::cout << "After insert: " << str << std::endl;  // 输出:Hello Beautiful, Universe!

    // 删除字符串中的部分内容
    str.erase(5, 10);
    std::cout << "After erase: " << str << std::endl;  // 输出:Hello, Universe!

    return 0;
}

9.5.3 string搜索操作

string类提供了多种搜索函数,用于查找子字符串或字符在字符串中的位置:

  • find:查找子字符串或字符的第一次出现位置。
  • rfind:查找子字符串或字符的最后一次出现位置。
  • find_first_of:查找字符集合中的任意字符的第一次出现位置。
  • find_last_of:查找字符集合中的任意字符的最后一次出现位置。
示例代码
#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    // 查找子字符串的第一次出现位置
    std::size_t pos = str.find("World");
    if (pos != std::string::npos) {
        std::cout << "'World' found at position: " << pos << std::endl;
    }

    // 查找字符的最后一次出现位置
    pos = str.rfind('o');
    if (pos != std::string::npos) {
        std::cout << "Last 'o' found at position: " << pos << std::endl;
    }

    // 查找字符集合中的任意字符的第一次出现位置
    pos = str.find_first_of("aeiou");
    if (pos != std::string::npos) {
        std::cout << "First vowel found at position: " << pos << std::endl;
    }

    // 查找字符集合中的任意字符的最后一次出现位置
    pos = str.find_last_of("aeiou");
    if (pos != std::string::npos) {
        std::cout << "Last vowel found at position: " << pos << std::endl;
    }

    return 0;
}

9.5.4 数值转换

string类与数值类型之间的转换是C++中常见的操作。标准库提供了一些函数用于将字符串转换为数值类型,或将数值类型转换为字符串:

  • std::stoi:将字符串转换为整数。
  • std::stod:将字符串转换为双精度浮点数。
  • std::to_string:将数值类型转换为字符串。
示例代码
#include <iostream>
#include <string>

int main() {
    std::string str = "42";
  

    // 将字符串转换为整数
    int num = std::stoi(str);
    std::cout << "Integer: " << num << std::endl;  // 输出:42

    // 将字符串转换为双精度浮点数
    str = "3.14159";
    double pi = std::stod(str);
    std::cout << "Double: " << pi << std::endl;  // 输出:3.14159

    // 将数值类型转换为字符串
    int val = 100;
    str = std::to_string(val);
    std::cout << "String: " << str << std::endl;  // 输出:100

    return 0;
}

重点与难点分析

重点

  1. 字符串构造和修改:掌握string类的各种构造和修改方法,理解如何灵活地创建和操作字符串。
  2. 字符串搜索:了解findrfindfind_first_offind_last_of等搜索函数的用法,掌握在字符串中查找子字符串或字符的方法。
  3. 数值转换:掌握std::stoistd::stodstd::to_string等函数的用法,理解如何在字符串和数值类型之间进行转换。

难点

  1. 复杂字符串操作:理解并掌握复杂字符串操作的细节和参数使用,能够在实际应用中灵活使用这些操作。
  2. 数值转换的异常处理:在进行字符串和数值类型转换时,处理可能出现的异常情况,如无效输入或溢出。

练习题解析

  1. 练习9.19:编写一个程序,使用重复字符、子字符串和C字符串构造不同的string对象。
    • 示例代码
#include <iostream>
#include <string>

int main() {
    std::string str1(10, 'a');
    std::cout << "str1: " << str1 << std::endl;  // 输出:aaaaaaaaaa

    std::string str2 = "Hello, World!";
    std::string str3(str2, 7, 5);
    std::cout << "str3: " << str3 << std::endl;  // 输出:World

    const char* cstr = "C-style string";
    std::string str4(cstr);
    std::cout << "str4: " << str4 << std::endl;  // 输出:C-style string

    return 0;
}
  1. 练习9.20:编写一个程序,使用appendreplaceinserterase函数修改字符串,并输出修改后的结果。
    • 示例代码
#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";
    str.append(", World");
    std::cout << "After append: " << str << std::endl;  // 输出:Hello, World

    str.replace(7, 5, "Universe");
    std::cout << "After replace: " << str << std::endl;  // 输出:Hello, Universe!

    str.insert(5, " Beautiful");
    std::cout << "After insert: " << str << std::endl;  // 输出:Hello Beautiful, Universe!

    str.erase(5, 10);
    std::cout << "After erase: " << str << std::endl;  // 输出:Hello, Universe!

    return 0;
}
  1. 练习9.21:编写一个程序,使用findrfindfind_first_offind_last_of函数查找字符串中的子字符串或字符,并输出查找结果。
    • 示例代码
#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    std::size_t pos = str.find("World");
    if (pos != std::string::npos) {
        std::cout << "'World' found at position: " << pos << std::endl;
    }

    pos = str.rfind('o');
    if (pos != std::string::npos) {
        std::cout << "Last 'o' found at position: " << pos << std::endl;
    }

    pos = str.find_first_of("aeiou");
    if (pos != std::string::npos) {
        std::cout << "First vowel found at position: " << pos << std::endl;
    }

    pos = str.find_last_of("aeiou");
    if (pos != std::string::npos) {
        std::cout << "Last vowel found at position: " << pos << std::endl;
    }

    return 0;
}
  1. 练习9.22:编写一个程序,使用std::stoistd::stodstd::to_string函数在字符串和数值类型之间进行转换,并输出转换结果。
    • 示例代码
#include <iostream>
#include <string>

int main() {
    std::string str = "42";
    int num = std::stoi(str);
    std::cout << "Integer: " << num << std::endl;  // 输出:42

    str = "3.14159";
    double pi = std::stod(str);
    std::cout << "Double: " << pi << std::endl;  // 输出:3.14159

    int val = 100;
    str = std::to_string(val);
    std::cout << "String: " << str << std::endl;  // 输出:100

    return 0;
}

总结与提高

本节总结

  1. 了解了string类的各种构造方法,包括重复字符、子字符串和C字符串构造等。
  2. 掌握了string类的其他修改方法,包括appendreplaceinserterase函数的用法。
  3. 理解了string类的搜索操作,包括findrfindfind_first_offind_last_of函数。
  4. 学会了在字符串和数值类型之间进行转换的方法,包括std::stoistd::stodstd::to_string函数。

提高建议

  1. 多练习字符串操作:通过编写更多涉及字符串操作的程序,熟悉各种操作方法,提高对字符串的操作能力。
  2. 优化字符串处理:在实际项目中,关注字符串操作的效率和性能,优化字符串处理的逻辑。
  3. 深入理解字符串函数:通过阅读文档和实践,深入理解字符串函数的特性和使用场景,提高编写高效代码的能力。

9.6 容器适配器

容器适配器是对现有容器进行包装,以提供不同的接口和行为。C++标准库提供了三种主要的容器适配器:stackqueuepriority_queue。它们分别实现了栈、队列和优先级队列的行为。本节将详细介绍这些容器适配器及其用法。

9.6.1 stack

stack是一个后进先出(LIFO,Last In First Out)的数据结构,只允许在一端进行插入和删除操作。它通常基于dequevectorlist实现。

主要操作
  • push:将元素压入栈顶。
  • pop:移除栈顶元素。
  • top:访问栈顶元素。
  • empty:检查栈是否为空。
  • size:返回栈中的元素数量。
示例代码
#include <iostream>
#include <stack>

int main() {
    std::stack<int> stk;
    stk.push(1);
    stk.push(2);
    stk.push(3);

    std::cout << "Stack size: " << stk.size() << std::endl;

    while (!stk.empty()) {
        std::cout << stk.top() << " ";
        stk.pop();
    }
    std::cout << std::endl;

    return 0;
}

9.6.2 queue

queue是一个先进先出(FIFO,First In First Out)的数据结构,只允许在一端进行插入操作,在另一端进行删除操作。它通常基于dequelist实现。

主要操作
  • push:将元素插入队尾。
  • pop:移除队头元素。
  • front:访问队头元素。
  • back:访问队尾元素。
  • empty:检查队列是否为空。
  • size:返回队列中的元素数量。
示例代码
#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;
    q.push(1);
    q.push(2);
    q.push(3);

    std::cout << "Queue size: " << q.size() << std::endl;

    while (!q.empty()) {
        std::cout << q.front() << " ";
        q.pop();
    }
    std::cout << std::endl;

    return 0;
}

9.6.3 priority_queue

priority_queue是一个优先级队列,元素按优先级顺序排列,最大的元素在队头。默认情况下,priority_queue使用<运算符比较元素,因此是一个最大堆。它通常基于vector实现,并使用堆算法管理元素。

主要操作
  • push:将元素插入优先级队列。
  • pop:移除队头元素(最大元素)。
  • top:访问队头元素(最大元素)。
  • empty:检查优先级队列是否为空。
  • size:返回优先级队列中的元素数量。
示例代码
#include <iostream>
#include <queue>
#include <vector>

int main() {
    std::priority_queue<int> pq;
    pq.push(3);
    pq.push(1);
    pq.push(4);
    pq.push(2);

    std::cout << "Priority queue size: " << pq.size() << std::endl;

    while (!pq.empty()) {
        std::cout << pq.top() << " ";
        pq.pop();
    }
    std::cout << std::endl;

    return 0;
}

重点与难点分析

重点

  1. 理解容器适配器的概念:掌握stackqueuepriority_queue的基本概念和用途。
  2. 掌握容器适配器的主要操作:了解各个容器适配器的主要操作函数及其用法。
  3. 理解适配器的实现基础:理解容器适配器是如何基于其他容器实现的。

难点

  1. 优先级队列的使用:理解priority_queue的工作原理及其在实际应用中的使用方法。
  2. 容器适配器的性能:理解容器适配器的性能特征,选择合适的基础容器以优化程序性能。

练习题解析

  1. 练习9.23:编写一个程序,使用stack容器适配器,实现一个简单的整数栈操作。
    • 示例代码
#include <iostream>
#include <stack>

int main() {
    std::stack<int> stk;
    stk.push(1);
    stk.push(2);
    stk.push(3);

    std::cout << "Stack size: " << stk.size() << std::endl;

    while (!stk.empty()) {
        std::cout << stk.top() << " ";
        stk.pop();
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.24:编写一个程序,使用queue容器适配器,实现一个简单的整数队列操作。
    • 示例代码
#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;
    q.push(1);
    q.push(2);
    q.push(3);

    std::cout << "Queue size: " << q.size() << std::endl;

    while (!q.empty()) {
        std::cout << q.front() << " ";
        q.pop();
    }
    std::cout << std::endl;

    return 0;
}
  1. 练习9.25:编写一个程序,使用priority_queue容器适配器,实现一个简单的优先级队列操作。
    • 示例代码
#include <iostream>
#include <queue>
#include <vector>

int main() {
    std::priority_queue<int> pq;
    pq.push(3);
    pq.push(1);
    pq.push(4);
    pq.push(2);

    std::cout << "Priority queue size: " << pq.size() << std::endl;

    while (!pq.empty()) {
        std::cout << pq.top() << " ";
        pq.pop();
    }
    std::cout << std::endl;

    return 0;
}

总结与提高

本节总结

  1. 了解了容器适配器的基本概念和用途,掌握了stackqueuepriority_queue的主要操作。
  2. 理解了容器适配器的实现基础,知道它们是如何基于其他容器实现的。
  3. 掌握了优先级队列的使用方法,理解了其工作原理和应用场景。

提高建议

  1. 多练习容器适配器操作:通过编写更多涉及容器适配器的程序,熟悉各种操作方法,提高对容器适配器的操作能力。
  2. 优化容器适配器使用:在实际项目中,根据具体需求选择合适的基础容器和容器适配器,优化程序性能。
  3. 深入理解容器适配器:通过阅读文档和实践,深入理解容器适配器的特性和使用场景,提高编写高效代码的能力。

本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。 

  • 34
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iShare_爱分享

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值