9.1 顺序容器概述
C++标准库提供了几种顺序容器,用于存储和管理元素的有序集合。主要的顺序容器包括vector
、deque
、list
和forward_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
:当内存有限且只需要单向遍历时。
重点与难点分析
重点:
- 了解各种顺序容器的特性:理解每种容器的特性和适用场景,选择最合适的容器来提高程序的性能。
- 掌握基本操作:熟悉顺序容器的基本操作,如插入、删除、遍历等。
难点:
- 容器的性能权衡:在选择容器时,需要根据具体的使用场景权衡性能,例如随机访问的效率、插入删除操作的效率等。
- 容器的内存管理:理解容器在内存管理方面的差异,例如
vector
的连续内存分配和list
的非连续内存分配。
练习题解析
- 练习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;
}
- 练习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;
}
- 练习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;
}
- 练习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;
}
总结与提高
本节总结:
- 了解了
vector
、deque
、list
和forward_list
等顺序容器的基本特性和适用场景。 - 掌握了这些容器的基本操作,包括插入、删除和遍历。
- 理解了如何根据具体的应用场景选择最合适的容器,以提高程序的性能。
提高建议:
- 多练习容器操作:通过编写更多涉及顺序容器的程序,熟悉各种容器的基本操作和特性。
- 优化容器选择:在实际项目中,根据具体需求选择最合适的容器,优化程序的性能。
- 深入理解容器实现:通过阅读标准库的实现代码或相关书籍,深入理解顺序容器的内部实现原理,提高编写高效代码的能力。
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成员
所有标准库容器都提供begin
和end
成员函数,用于获取指向容器第一个元素和最后一个元素之后位置的迭代器。
示例代码
#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;
}
重点与难点分析
重点:
- 迭代器的使用:掌握如何使用迭代器遍历容器元素。
- 容器类型成员:理解容器的类型成员及其用途。
- begin和end成员:熟悉容器的
begin
和end
成员函数的用法。 - 容器的定义和初始化:掌握容器的各种初始化方式。
- 赋值和swap:理解容器的赋值操作和
swap
函数的用法。 - 关系运算符:掌握如何使用关系运算符比较容器。
难点:
- 迭代器的生命周期:理解迭代器在容器操作中的有效性,避免使用失效的迭代器。
- 容器的类型成员:掌握不同容器的类型成员及其适用场景。
- 关系运算符的使用:理解容器比较的原理,正确使用关系运算符进行容器比较。
练习题解析
- 练习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;
}
- 练习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;
}
- 练习9.7:编写一个程序,使用
deque
容器,并使用begin
和end
成员函数遍历其元素。
-
- 示例代码:
#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;
}
- 练习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;
}
- 练习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;
}
- 练习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;
}
总结与提高
本节总结:
- 掌握了迭代器的基本概念和用法,能够使用迭代器遍历和操作容器中的元素。
- 理解了容器的类型成员及其用途,能够在代码中使用这些类型成员定义迭代器和其他类型。
- 熟悉了
begin
和end
成员函数的用法,能够使用它们获取指向容器起始和末尾的迭代器。 - 掌握了容器的定义和初始化方法,能够使用各种方式定义和初始化容器。
- 理解了容器的赋值操作和
swap
函数的用法,能够在程序中进行容器内容的赋值和交换。 - 熟悉了容器的关系运算符,能够使用这些运算符比较容器的内容。
提高建议:
- 多练习迭代器操作:通过编写更多涉及迭代器的程序,熟悉迭代器的各种操作,提高对容器元素的操作能力。
- 深入理解容器类型成员:通过查阅文档和实践,深入理解容器的类型成员及其应用场景,提高代码的可读性和可维护性。
- 优化容器的选择和使用:在实际项目中,根据具体需求选择最合适的容器,优化程序的性能。
- 熟练掌握关系运算符:在编写涉及容器比较的代码时,熟练使用关系运算符,提高代码的逻辑性和健壮性。
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
:在容器开头插入元素(适用于list
和deque
)。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
:删除容器开头的元素(适用于list
和deque
)。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 容器操作可能使迭代器失效
在对容器进行某些操作时,迭代器可能会失效。以下是一些常见情况:
- 对
vector
、deque
等进行插入或删除操作后,所有的迭代器、指针和引用都可能失效。 - 对
list
和forward_list
进行插入或删除操作时,只有插入或删除位置附近的迭代器会失效。 - 对
map
和set
进行插入操作时,现有的迭代器不会失效,但删除操作会使相关的迭代器失效。
示例代码
#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;
}
重点与难点分析
重点:
- 访问元素:理解如何使用成员函数和运算符访问容器中的元素。
- 插入元素:掌握在容器中插入元素的方法,包括在末尾、开头、指定位置和范围内插入元素。
- **
删除元素**:掌握删除容器中元素的方法,包括删除末尾元素、开头元素、指定位置元素和范围内的元素。
4. 修改元素:理解如何用新内容替换容器中的内容,改变容器的大小和交换两个容器的内容。
5. 迭代器和范围操作:熟悉获取迭代器和使用迭代器遍历容器的方法。
6. 迭代器失效问题:理解容器操作可能导致的迭代器失效问题,并掌握避免使用失效迭代器的方法。
难点:
- 迭代器的有效性:理解在容器操作(如插入、删除)之后,迭代器可能失效的情况,避免使用失效的迭代器。
- 容器的内存管理:掌握容器在进行插入、删除和修改操作时的内存管理机制,优化程序的性能。
练习题解析
- 练习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;
}
- 练习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;
}
- 练习9.13:编写一个程序,使用
deque
容器修改元素的内容,并使用begin
和end
成员函数遍历其元素。
-
- 示例代码:
#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;
}
- 练习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;
}
- 练习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;
}
总结与提高
本节总结:
- 了解了顺序容器的基本操作,包括访问、插入、删除和修改元素的方法。
- 掌握了如何使用迭代器进行容器的遍历和范围操作。
- 理解了顺序容器的内存管理机制和迭代器失效的问题。
提高建议:
- 多练习容器操作:通过编写更多涉及顺序容器操作的程序,熟悉各种操作方法,提高对容器的操作能力。
- 优化内存管理:在实际项目中,关注顺序容器的内存管理机制,优化程序的性能。
- 深入理解迭代器:通过阅读文档和实践,深入理解迭代器的特性和使用场景,避免使用失效的迭代器。
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
的容量以适应当前大小。
重点与难点分析
重点:
- 理解
vector
的大小和容量:掌握vector
的size
和capacity
的概念及其区别。 - 掌握
vector
的增长机制:理解vector
在添加新元素时如何增长及其重新分配内存的机制。 - 容量管理:了解
reserve
和shrink_to_fit
等成员函数的用途,掌握如何有效管理vector
的容量以优化性能。
难点:
- 重新分配内存的影响:理解重新分配内存对性能的影响,学会通过合理使用
reserve
函数来优化程序性能。 - 容量和大小的关系:理解容量和大小之间的关系,以及如何在程序中进行容量管理。
练习题解析
- 练习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;
}
- 练习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;
}
- 练习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;
}
总结与提高
本节总结:
- 了解了
vector
的大小和容量的概念,理解了它们之间的区别。 - 掌握了
vector
在添加新元素时的增长机制及其重新分配内存的影响。 - 学会了使用
reserve
和shrink_to_fit
等成员函数管理vector
的容量,以优化程序性能。
提高建议:
- 合理管理容量:在需要大量添加元素时,使用
reserve
函数提前分配内存,减少重新分配内存的次数,以提高性能。 - 优化内存使用:在删除大量元素后,使用
shrink_to_fit
函数减少vector
的容量,优化内存使用。 - 关注性能:在实际项目中,通过合理管理
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;
}
重点与难点分析
重点:
- 字符串构造和修改:掌握
string
类的各种构造和修改方法,理解如何灵活地创建和操作字符串。 - 字符串搜索:了解
find
、rfind
、find_first_of
和find_last_of
等搜索函数的用法,掌握在字符串中查找子字符串或字符的方法。 - 数值转换:掌握
std::stoi
、std::stod
和std::to_string
等函数的用法,理解如何在字符串和数值类型之间进行转换。
难点:
- 复杂字符串操作:理解并掌握复杂字符串操作的细节和参数使用,能够在实际应用中灵活使用这些操作。
- 数值转换的异常处理:在进行字符串和数值类型转换时,处理可能出现的异常情况,如无效输入或溢出。
练习题解析
- 练习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;
}
- 练习9.20:编写一个程序,使用
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.21:编写一个程序,使用
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.22:编写一个程序,使用
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;
}
总结与提高
本节总结:
- 了解了
string
类的各种构造方法,包括重复字符、子字符串和C字符串构造等。 - 掌握了
string
类的其他修改方法,包括append
、replace
、insert
和erase
函数的用法。 - 理解了
string
类的搜索操作,包括find
、rfind
、find_first_of
和find_last_of
函数。 - 学会了在字符串和数值类型之间进行转换的方法,包括
std::stoi
、std::stod
和std::to_string
函数。
提高建议:
- 多练习字符串操作:通过编写更多涉及字符串操作的程序,熟悉各种操作方法,提高对字符串的操作能力。
- 优化字符串处理:在实际项目中,关注字符串操作的效率和性能,优化字符串处理的逻辑。
- 深入理解字符串函数:通过阅读文档和实践,深入理解字符串函数的特性和使用场景,提高编写高效代码的能力。
9.6 容器适配器
容器适配器是对现有容器进行包装,以提供不同的接口和行为。C++标准库提供了三种主要的容器适配器:stack
、queue
和priority_queue
。它们分别实现了栈、队列和优先级队列的行为。本节将详细介绍这些容器适配器及其用法。
9.6.1 stack
stack
是一个后进先出(LIFO,Last In First Out)的数据结构,只允许在一端进行插入和删除操作。它通常基于deque
、vector
或list
实现。
主要操作
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)的数据结构,只允许在一端进行插入操作,在另一端进行删除操作。它通常基于deque
或list
实现。
主要操作
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;
}
重点与难点分析
重点:
- 理解容器适配器的概念:掌握
stack
、queue
和priority_queue
的基本概念和用途。 - 掌握容器适配器的主要操作:了解各个容器适配器的主要操作函数及其用法。
- 理解适配器的实现基础:理解容器适配器是如何基于其他容器实现的。
难点:
- 优先级队列的使用:理解
priority_queue
的工作原理及其在实际应用中的使用方法。 - 容器适配器的性能:理解容器适配器的性能特征,选择合适的基础容器以优化程序性能。
练习题解析
- 练习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;
}
- 练习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;
}
- 练习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;
}
总结与提高
本节总结:
- 了解了容器适配器的基本概念和用途,掌握了
stack
、queue
和priority_queue
的主要操作。 - 理解了容器适配器的实现基础,知道它们是如何基于其他容器实现的。
- 掌握了优先级队列的使用方法,理解了其工作原理和应用场景。
提高建议:
- 多练习容器适配器操作:通过编写更多涉及容器适配器的程序,熟悉各种操作方法,提高对容器适配器的操作能力。
- 优化容器适配器使用:在实际项目中,根据具体需求选择合适的基础容器和容器适配器,优化程序性能。
- 深入理解容器适配器:通过阅读文档和实践,深入理解容器适配器的特性和使用场景,提高编写高效代码的能力。
本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。