vector(理解和使用)
- 1.什么是vector?
- 2.vector的使用
- 2.1 vector构造函数
- 2.2 vector迭代器(Iterators)函数
- 2.2.1 begin()
- 2.2.2 end()
- 2.2.3 rbegin()
- 2.2.4 rend()
- 2.2.5 cbegin()、cend()、crbegin()和crend() C++11
- 2.3 vector容量函数
- 2.4 vector元素访问函数
- 2.4.1 operator[]
- 2.4.2 at
- 2.4.3 front()
- 2.4.4 back()
- 2.4.5 data() C++11
- 2.5 vector增删查改函数
- 2.5.1 assign()
- 2.5.2 push_back()
- 2.5.3 pop_back()
- 2.5.4 insert()
- 2.5.5 erase()
- 2.5.6 swap()
- 2.5.7 clear()
- 2.5.8 emplace() C++11
- 2.5.9 emplace_back() C++11
- 2.5.10 vector 迭代器失效问题
- 结语
1.什么是vector?
在C++中,std::vector是标准模板库(STL)中的一种动态数组容器,它可以存储任意类型的元素,并且能够自动调整大小。std::vector提供了许多方便的成员函数,使得对数组的操作更加简单和高效。
vector声明:
template <class T, class Alloc = allocator<T> > ;
这是 std::vector 的一般模板定义。它使用了两个模板参数 T 和 Alloc,其中 T 表示容器中存储的元素类型,Alloc 表示容器的内存分配器类型,默认为 std::allocator。该模板定义了一个通用的 vector 类模板,用于存储任意类型的元素。
template <class Alloc> class vector<bool,Alloc>;
这是特化版本的 std::vector,用于存储 bool 类型的元素。这是因为 C++ 中的 std::vector 不能直接存储布尔类型,而是使用了一种特殊的实现来节省空间。这个特化版本接受一个模板参数 Alloc,表示容器的内存分配器类型,但是对于 std::vector 来说,这个分配器类型实际上不会影响它的内部实现,因为 std::vector 会进行特殊优化来节省空间。
总结:
template <class T, class Alloc = allocator<T> > ;
是 std::vector 的一般模板定义,用于存储任意类型的元素。
template <class Alloc> class vector<bool,Alloc>;
是特化版本的 std::vector,用于存储 bool 类型的元素。
特点和优势:
动态数组:std::vector内部实现了动态内存管理,可以根据需要自动调整存储容量,当需要添加新元素时,会自动分配更多的内存空间。
随机访问:可以通过下标或迭代器来访问元素,支持快速的随机访问。
自动扩容:当元素个数超过当前容量时,std::vector会自动重新分配更大的内存块,以便容纳更多元素。
连续存储:std::vector的元素在内存中是连续存储的,这样可以提高访问效率。
支持动态调整大小:可以使用 push_back() 和 pop_back() 方法在尾部添加或删除元素,也可以使用 resize() 方法动态调整容器大小。
与其它动态序列容器相比(deque, list and forward_list): vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好
使用std::vector前需要包含头文件 <vector>
,例如:#include <vector>
。
下面是一个使用std::vector的简单示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec; // 声明一个存储int类型的vector
// 在vector中添加元素
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
// 遍历vector并输出元素
for (const auto& element : vec) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
输出:
10 20 30
2.vector的使用
成员类型参照
2.1 vector构造函数
explicit vector (const allocator_type& alloc = allocator_type()); //构造一个空容器,不包含任何元素。
explicit vector (size_type n, const value_type& val = value_type(),
const allocator_type& alloc = allocator_type());//构造一个包含n 个元素的容器。每个元素都是val的副本。
template <class InputIterator>
vector (InputIterator first, InputIterator last,
const allocator_type& alloc = allocator_type()); //构造一个包含与范围[first,last)一样多的元素的容器,其中每个元素均从该范围内的相应元素按相同的顺序构造。
vector (const vector& x);//构造一个容器,其中包含x中每个元素的副本(按相同顺序)。
参数:
alloc
分配器对象。
容器保留并使用该分配器的内部副本。
成员类型allocator_type是容器使用的内部分配器类型,在向量中定义为其第二个模板参数( Alloc )的别名。
如果allocator_type是默认分配器(没有状态)的实例,则这是不相关的。
n
初始容器大小(即构造时容器中的元素数量)。
成员类型size_type是无符号整型。
val
填充容器的值。容器中的n 个元素中的每一个都将被初始化为该值的副本。
成员类型value_type是容器中元素的类型,在向量中定义为其第一个模板参数 ( T ) 的别名。
first, last
将迭代器输入到范围中的初始位置和最终位置。使用的范围是[first,last) ,包括first和last之间的所有元素,包括first指向的元素但不包括last指向的元素。
函数模板参数InputIterator应该是一个输入迭代器类型,它指向可以构造value_type对象的类型的元素。
x
另一个相同类型的向量对象(具有相同的类模板参数T和Alloc),其内容可以被复制或获取。
il
一个initializer_list对象。
这些对象是根据初始值设定项列表声明符自动构造的。
成员类型value_type是容器中元素的类型,在向量中定义为其第一个模板参数 ( T ) 的别名。
构造函数的分析和使用
explicit vector (const allocator_type& alloc = allocator_type());
这是默认构造函数,用于创建一个空的std::vector对象。它可以使用默认的分配器allocator_type,也可以通过参数alloc指定自定义的分配器。
explicit vector (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type());
这个构造函数用于创建包含n个元素的std::vector对象,所有元素都使用val进行初始化。n表示初始化的元素数量,val表示初始化的元素值,alloc表示使用的分配器。
template <class InputIterator> vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());
这是一个模板构造函数,可以通过迭代器范围来创建std::vector对象。它接受两个参数 first 和 last,分别表示迭代器的起始和结束位置。在构造函数内,使用 first 和 last 迭代器所指向的元素范围来初始化std::vector对象,alloc表示使用的分配器。
vector (const vector& x);
这是拷贝构造函数,用于创建一个新的std::vector对象,它与参数x是相同类型的,内容也相同。在创建新的std::vector对象时,会复制x中的元素到新对象中,但是新对象与原对象是独立的,修改一个对象不会影响另一个对象。
示例:
#include <iostream>
#include <vector>
int main ()
{
//构造函数的使用顺序与上述相同:
std::vector<int> first; // 默认构造
std::vector<int> second (4,100); // 初始化四个值为 100 的整数
std::vector<int> third (second.begin(),second.end()); // 使用迭代器的拷贝
std::vector<int> fourth (third); // 拷贝构造
// 迭代器构造函数也可用于从数组构造:
int myints[] = {16,2,77,29};
std::vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );
return 0;
}
这里再提一下赋值运算符的重载
vector& operator= (const vector& x);
类的拷贝赋值运算符重载函数,它用于将一个已存在的 std::vector 对象 x 的内容复制给另一个已存在的 std::vector 对象,并返回赋值后的对象的引用。
该运算符重载函数使得两个 std::vector 对象的内容相同,但是它们是独立的,修改一个对象不会影响另一个对象。
示例:
std::vector<int> vec1 = {1, 2, 3, 4};
std::vector<int> vec2 = {5, 6, 7};
vec1 = vec2; // 将 vec2 的内容赋值给 vec1
在上面的示例中,vec1 的内容将变成 {5, 6, 7},与 vec2 相同。vec2 本身不受影响。
2.2 vector迭代器(Iterators)函数
2.2.1 begin()
iterator begin();
const_iterator begin() const;
iterator begin();
和 const_iterator begin() const;
是 std::vector 类的成员函数,分别用于获取可修改元素的迭代器和获取常量元素的迭代器。
iterator begin();
: 返回一个指向 std::vector 容器中第一个元素的可修改迭代器。通过这个迭代器,可以对容器中的元素进行修改。
const_iterator begin() const;
: 返回一个指向 std::vector 容器中第一个元素的常量迭代器。通过这个迭代器,可以遍历容器中的元素,但不能对容器中的元素进行修改。
使用示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 使用 begin() 获取可修改迭代器
std::vector<int>::iterator it = myVector.begin();
// 使用 begin() 获取常量迭代器
std::vector<int>::const_iterator cit = myVector.begin();
// 修改第一个元素的值
*it = 10;
// 错误:常量迭代器不允许修改元素的值
// *cit = 20;
// 输出容器中的元素
for (int num : myVector) {
std::cout << num << " ";
}
return 0;
}
在上面的示例中,我们创建了一个存放整数的 std::vector 对象 myVector,然后使用 begin() 获取可修改迭代器 it 和常量迭代器 cit。通过可修改迭代器 it,我们可以修改容器中的元素值;但是通过常量迭代器 cit,我们不能修改容器中的元素值。最后,我们遍历并输出容器中的元素,输出结果将是 “10 2 3 4 5”。
2.2.2 end()
iterator end();
const_iterator end() const;
iterator end();
和const_iterator end() const;
是 std::vector 类的成员函数,分别用于获取可修改元素的迭代器和获取常量元素的迭代器,指向容器中最后一个元素的下一个位置。
iterator end();
: 返回一个指向 std::vector 容器中最后一个元素的下一个位置的可修改迭代器。通过这个迭代器,可以对容器中的元素进行修改。
const_iterator end() const;
: 返回一个指向 std::vector 容器中最后一个元素的下一个位置的常量迭代器。通过这个迭代器,可以遍历容器中的元素,但不能对容器中的元素进行修改。
使用示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 使用 end() 获取可修改迭代器
std::vector<int>::iterator it = myVector.end();
// 使用 end() 获取常量迭代器
std::vector<int>::const_iterator cit = myVector.end();
// 修改最后一个元素的值
*(--it) = 100;
// 错误:常量迭代器不允许修改元素的值
// *(--cit) = 200;
// 输出容器中的元素
for (int num : myVector) {
std::cout << num << " ";
}
return 0;
}
在上面的示例中,我们创建了一个存放整数的 std::vector 对象 myVector,然后使用 end() 获取可修改迭代器 it 和常量迭代器 cit。通过可修改迭代器 it,我们可以修改容器中的元素值;但是通过常量迭代器 cit,我们不能修改容器中的元素值。最后,我们遍历并输出容器中的元素,输出结果将是 “1 2 3 4 100”。注意,在修改最后一个元素的值时,我们使用了 --it 将迭代器移动到最后一个元素的位置。因为 end() 返回的迭代器指向容器中最后一个元素的下一个位置,所以在修改最后一个元素时,需要先将迭代器移动到最后一个元素的位置。
2.2.3 rbegin()
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
reverse_iterator rbegin();
和const_reverse_iterator rbegin() const;
是 std::vector 类的成员函数,用于获取逆向迭代器。
reverse_iterator rbegin();
: 返回一个指向 std::vector 容器中最后一个元素的可修改逆向迭代器。逆向迭代器是一种特殊的迭代器,它可以从容器的末尾向前遍历元素。
const_reverse_iterator rbegin() const;
: 返回一个指向 std::vector 容器中最后一个元素的常量逆向迭代器。通过这个逆向迭代器,可以从容器的末尾向前遍历元素,但不能对容器中的元素进行修改。
使用示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 使用 rbegin() 获取可修改逆向迭代器
std::vector<int>::reverse_iterator rit = myVector.rbegin();
// 使用 rbegin() 获取常量逆向迭代器
std::vector<int>::const_reverse_iterator crit = myVector.rbegin();
// 修改最后一个元素的值
*rit = 100;
// 错误:常量逆向迭代器不允许修改元素的值
// *crit = 200;
// 输出容器中的元素
for (int num : myVector) {
std::cout << num << " ";
}
return 0;
}
在上面的示例中,我们创建了一个存放整数的 std::vector 对象 myVector,然后使用 rbegin() 获取可修改逆向迭代器 rit 和常量逆向迭代器 crit。通过可修改逆向迭代器 rit,我们可以修改容器中的元素值;但是通过常量逆向迭代器 crit,我们不能修改容器中的元素值。最后,我们遍历并输出容器中的元素,输出结果将是 “100 4 3 2 1”。注意,在修改最后一个元素的值时,我们直接通过 *rit 修改,因为逆向迭代器指向的位置即为最后一个元素。
2.2.4 rend()
reverse_iterator rend();
const_reverse_iterator rend() const;
reverse_iterator rend();
和 const_reverse_iterator rend() const;
是 std::vector 类的成员函数,用于获取逆向迭代器的结束位置。
reverse_iterator rend();
: 返回一个指向 std::vector 容器中的逆向迭代器的结束位置的可修改逆向迭代器。逆向迭代器的结束位置实际上是容器中的第一个元素之前的位置。
const_reverse_iterator rend() const;
: 返回一个指向 std::vector 容器中的逆向迭代器的结束位置的常量逆向迭代器。通过这个逆向迭代器,可以从容器的第一个元素之前的位置向后遍历元素,但不能对容器中的元素进行修改。
使用示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> myVector = { 1, 2, 3, 4, 5 };
// 使用 rend() 获取可修改逆向迭代器的结束位置
std::vector<int>::reverse_iterator ritEnd = myVector.rend();
// 使用 rend() 获取常量逆向迭代器的结束位置
std::vector<int>::const_reverse_iterator critEnd = myVector.rend();
// 遍历输出容器中的元素
std::cout << "Using const_reverse_iterator: ";
for (std::vector<int>::const_reverse_iterator crit = myVector.rbegin(); crit != critEnd; ++crit) {
//*rit += 1; //错误:常量逆向迭代器不允许修改元素的值
std::cout << *crit << " ";
}
std::cout << std::endl;
// 遍历输出容器中的元素
std::cout << "Using reverse_iterator: ";
for (std::vector<int>::reverse_iterator rit = myVector.rbegin(); rit != ritEnd; ++rit) {
*rit += 1;// 从后往前修改每一个元素的值
std::cout << *rit << " ";
}
std::cout << std::endl;
return 0;
}
在上面的示例中,我们使用 rend() 分别获取了可修改逆向迭代器的结束位置 ritEnd 和常量逆向迭代器的结束位置 critEnd。然后,我们分别使用这两个结束位置的逆向迭代器来遍历输出容器中的元素。注意,在使用逆向迭代器遍历容器时,要将结束条件设置为 rit != ritEnd 或 crit != critEnd,以避免越界访问容器的第一个元素之前的位置。输出结果将是:
Using const_reverse_iterator: 5 4 3 2 1
Using reverse_iterator: 6 5 4 3 2
2.2.5 cbegin()、cend()、crbegin()和crend() C++11
在 C++11 标准中,std::vector 类模板引入了以下成员函数用于获取常量迭代器:
cbegin()
: 返回指向 std::vector 容器中第一个元素的常量迭代器。
cend()
: 返回指向 std::vector 容器中最后一个元素之后位置的常量迭代器。
crbegin()
: 返回指向 std::vector 容器中最后一个元素的常量逆向迭代器。
crend()
: 返回指向 std::vector 容器中第一个元素之前位置的常量逆向迭代器。
这些函数在 std::vector 中的使用示例如下:
#include <vector>
#include <iostream>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 使用 cbegin() 获取常量迭代器的开始位置
std::vector<int>::const_iterator citBegin = myVector.cbegin();
// 使用 cend() 获取常量迭代器的结束位置
std::vector<int>::const_iterator citEnd = myVector.cend();
// 使用 crbegin() 获取常量逆向迭代器的开始位置
std::vector<int>::const_reverse_iterator critRBegin = myVector.crbegin();
// 使用 crend() 获取常量逆向迭代器的结束位置
std::vector<int>::const_reverse_iterator critREnd = myVector.crend();
// 遍历输出容器中的元素
std::cout << "Using const_iterator: ";
for (std::vector<int>::const_iterator cit = citBegin; cit != citEnd; ++cit) {
std::cout << *cit << " ";
}
std::cout << std::endl;
// 遍历输出容器中的元素
std::cout << "Using const_reverse_iterator: ";
for (std::vector<int>::const_reverse_iterator crit = critRBegin; crit != critREnd; ++crit) {
std::cout << *crit << " ";
}
std::cout << std::endl;
return 0;
}
在上面的示例中,我们使用 cbegin() 和 cend() 获取常量迭代器的开始和结束位置,以及使用 crbegin() 和 crend() 获取常量逆向迭代器的开始和结束位置。然后,我们分别使用这些常量迭代器来遍历输出容器中的元素。输出结果将是:
Using const_iterator: 1 2 3 4 5
Using const_reverse_iterator: 5 4 3 2 1
需要注意的是,常量迭代器意味着在遍历容器时不能修改容器中的元素,只能读取。
2.3 vector容量函数
在 C++ 的 std::vector 类模板中,有以下成员函数用于管理容器的大小和容量:
size_type size() const;
: 返回当前 std::vector 容器中实际存储的元素个数。
size_type max_size() const;
: 返回 std::vector 容器可能包含的最大元素数,这个值取决于编译器和系统的限制。
void resize (size_type n, value_type val = value_type());:
改变 std::vector 容器中元素的个数,可以增加或缩减元素个数。
size_type capacity() const;
: 返回当前 std::vector 容器在不重新分配内存的情况下可以存储的最大元素个数。
bool empty() const;
: 检查 std::vector 容器是否为空,如果为空返回 true,否则返回 false。
void reserve (size_type n);
: 请求 std::vector 容器预留至少容纳指定个数的元素的内存空间,但并不实际创建元素。
void shrink_to_fit();
: 要求 std::vector 容器缩小其内存使用,使其适合其当前存储的元素数,但并不保证一定能缩小到需要的大小。(C++11)
下面对每个函数进行简要介绍:
size()
: 返回当前 std::vector 容器中实际存储的元素个数。对于一个具有 n 个元素的 std::vector,size() 将返回 n。
max_size()
: 返回 std::vector 容器可能包含的最大元素数,这个值取决于编译器和系统的限制。通常情况下,这个值是非常大的,远远超过实际需要的元素个数。
resize()
: 改变 std::vector 容器中元素的个数。如果新的大小比当前大小小,会删除多余的元素;如果新的大小比当前大小大,会用默认值或指定的值填充新元素。
capacity()
: 返回当前 std::vector 容器在不重新分配内存的情况下可以存储的最大元素个数。capacity() 不会改变 std::vector 中的元素个数。
empty()
: 检查 std::vector 容器是否为空,如果为空返回 true,否则返回 false。空容器意味着 size() 返回值为 0。
reserve()
: 请求 std::vector 容器预留至少容纳指定个数的元素的内存空间,但并不实际创建元素。这个函数通常用于避免频繁的内存分配和释放操作。
shrink_to_fit()
: 要求 std::vector 容器缩小其内存使用,使其适合其当前存储的元素数,但并不保证一定能缩小到需要的大小。这个函数通常在删除大量元素后调用,以释放不再需要的内存。
这些函数在 std::vector 容器中的使用示例如下:
#include <vector>
#include <iostream>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
std::cout << "Size: " << myVector.size() << std::endl;
std::cout << "Max Size: " << myVector.max_size() << std::endl;
myVector.resize(8, 10); // 增加元素并用值 10 填充
std::cout << "Resized Size: " << myVector.size() << std::endl;
std::cout << "Capacity: " << myVector.capacity() << std::endl;
std::cout << "Is Empty: " << (myVector.empty() ? "Yes" : "No") << std::endl;
myVector.reserve(10); // 预留内存,但不实际创建元素
std::cout << "New Capacity After Reserve: " << myVector.capacity() << std::endl;
myVector.shrink_to_fit(); // 尝试缩小内存使用
std::cout << "New Capacity After Shrink: " << myVector.capacity() << std::endl;
return 0;
}
输出结果将是:
Size: 5
Max Size: 4611686018427387903
Resized Size: 8
Capacity: 8
Is Empty: No
New Capacity After Reserve: 8
New Capacity After Shrink: 8
注意:capacity() 和 size() 的区别在于,capacity() 表示在不重新分配内存的情况下 std::vector 可以容纳的最大元素个数,而 size() 表示实际存储在 std::vector 中的元素个数。
提到这里就不得不提一下vector的扩容机制
使用capacity扩容时在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
代码如下:
#include <iostream>
#include <vector>
using namespace std;
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
int main() {
TestVectorExpand();
return 0;
}
VS2022运行结果
g++运行结果
面对这类问题,可以使用reserve缓解vector增容的代价缺陷问题或者使用resize,在开空间的同时还会进行初始化,影响size。
如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够
就可以避免边插入边扩容导致效率低下的问题了
#include <iostream>
#include <vector>
using namespace std;
void TestVectorExpandOP()
{
vector<int> v;
size_t sz = v.capacity();
v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容
cout << "making bar grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
int main() {
TestVectorExpandOP();
return 0;
}
2.4 vector元素访问函数
2.4.1 operator[]
reference operator[] (size_type n);
const_reference operator[] (size_type n) const;
std::vector 提供了两个重载的 operator[],用于通过索引访问容器中的元素:
reference operator[] (size_type n)
: 这个重载用于访问 std::vector 中指定索引位置 n 处的元素,并返回该元素的引用。通过此重载,可以对元素进行读取和修改操作。如果 n 超出了有效的索引范围(即小于 0 或大于等于 size()),行为将是未定义的(Undefined Behavior)。
const_reference operator[] (size_type n) const;
: 这个重载是 const 成员函数,用于在 const 修饰的 std::vector 对象中访问指定索引位置 n 处的元素,并返回该元素的常量引用。通过此重载,只能对元素进行读取操作,不能修改元素的值。
下面是一个使用 operator[] 的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 修改元素
myVector[2] = 10; // 将索引 2 处的元素修改为 10
// 读取元素
std::cout << "Element at index 2: " << myVector[2] << std::endl;
// 使用 const_reference 进行读取(适用于 const std::vector)
const std::vector<int>& constVector = myVector;
std::cout << "Const element at index 4: " << constVector[4] << std::endl;
return 0;
}
输出结果:
Element at index 2: 10
Const element at index 4: 5
需要注意的是,通过 operator[] 访问元素时,应该确保索引在有效的范围内,否则可能导致未定义的行为。通常情况下,应该在访问元素之前使用 size() 函数检查索引的有效性。
2.4.2 at
reference at (size_type n);
const_reference at (size_type n) const;
std::vector 提供了两个重载的 at 函数,用于通过索引访问容器中的元素:
reference at (size_type n)
: 这个重载用于访问 std::vector 中指定索引位置 n 处的元素,并返回该元素的引用。通过此重载,可以对元素进行读取和修改操作。如果 n 超出了有效的索引范围(即小于 0 或大于等于 size()),at 函数会抛出 std::out_of_range 异常,以保证程序的健壮性。
const_reference at (size_type n) const;
: 这个重载是 const 成员函数,用于在 const 修饰的 std::vector 对象中访问指定索引位置 n 处的元素,并返回该元素的常量引用。通过此重载,只能对元素进行读取操作,不能修改元素的值。
下面是一个使用 at 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 修改元素
myVector.at(2) = 10; // 将索引 2 处的元素修改为 10
// 读取元素
std::cout << "Element at index 3: " << myVector.at(3) << std::endl;
// 使用 const_reference 进行读取(适用于 const std::vector)
const std::vector<int>& constVector = myVector;
std::cout << "Const element at index 4: " << constVector.at(4) << std::endl;
// 尝试访问越界的索引(会抛出 std::out_of_range 异常)
try {
int value = myVector.at(10);
} catch (const std::out_of_range& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
输出结果:
Element at index 3: 4
Const element at index 4: 5
Caught exception: vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)
可以看到,在尝试访问越界的索引时,at 函数抛出了 std::out_of_range 异常,提醒我们访问索引越界了。因此,相比于 operator[],使用 at 函数可以更加安全地访问元素,特别是在不确定索引是否有效时。
2.4.3 front()
reference front();
const_reference front() const;
std::vector 提供了两个重载的 front 函数,用于访问容器中的第一个元素:
reference front()
: 这个重载用于访问 std::vector 容器中的第一个元素,并返回该元素的引用。通过此重载,可以对元素进行读取和修改操作。
const_reference front() const;
: 这个重载是 const 成员函数,用于在 const 修饰的 std::vector 对象中访问第一个元素,并返回该元素的常量引用。通过此重载,只能对元素进行读取操作,不能修改元素的值。
下面是一个使用 front 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 修改第一个元素
myVector.front() = 10; // 将第一个元素修改为 10
// 读取第一个元素
std::cout << "First element: " << myVector.front() << std::endl;
// 使用 const_reference 进行读取(适用于 const std::vector)
const std::vector<int>& constVector = myVector;
std::cout << "Const first element: " << constVector.front() << std::endl;
return 0;
}
输出结果:
First element: 10
Const first element: 10
可以看到,front 函数用于获取 std::vector 容器中的第一个元素,并且可以通过不同的重载版本来实现读取和修改操作,适用于常规和 const 对象。
2.4.4 back()
reference back();
const_reference back() const;
std::vector 提供了两个重载的 back 函数,用于访问容器中的最后一个元素:
reference back()
: 这个重载用于访问 std::vector 容器中的最后一个元素,并返回该元素的引用。通过此重载,可以对元素进行读取和修改操作。
const_reference back() const;
: 这个重载是 const 成员函数,用于在 const 修饰的 std::vector 对象中访问最后一个元素,并返回该元素的常量引用。通过此重载,只能对元素进行读取操作,不能修改元素的值。
下面是一个使用 back 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 修改最后一个元素
myVector.back() = 10; // 将最后一个元素修改为 10
// 读取最后一个元素
std::cout << "Last element: " << myVector.back() << std::endl;
// 使用 const_reference 进行读取(适用于 const std::vector)
const std::vector<int>& constVector = myVector;
std::cout << "Const last element: " << constVector.back() << std::endl;
return 0;
}
输出结果:
Last element: 10
Const last element: 10
可以看到,back 函数用于获取 std::vector 容器中的最后一个元素,并且可以通过不同的重载版本来实现读取和修改操作,适用于常规和 const 对象。
2.4.5 data() C++11
value_type* data() noexcept;
const value_type* data() const noexcept;
std::vector 提供了两个重载的 data 函数,用于访问底层存储数组的指针:
value_type* data() noexcept;
: 这个重载返回一个指向 std::vector 容器底层存储数组的非常量指针。通过此重载,可以对底层数组进行读写操作。noexcept 表示这个函数不会抛出异常。
const value_type* data() const noexcept;
: 这个重载是 const 成员函数,返回一个指向 std::vector 容器底层存储数组的常量指针。通过此重载,只能对底层数组进行读取操作,不能修改数组的内容。noexcept 表示这个函数不会抛出异常。
下面是一个使用 data 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
// 获取底层存储数组的非常量指针
int* ptr = myVector.data();
// 修改底层存储数组
ptr[0] = 10;
ptr[3] = 20;
// 通过 const value_type* 获取底层存储数组的常量指针(适用于 const std::vector)
const std::vector<int>& constVector = myVector;
const int* constPtr = constVector.data();
// 读取底层存储数组
for (size_t i = 0; i < myVector.size(); ++i) {
std::cout << "Element " << i << ": " << constPtr[i] << std::endl;
}
return 0;
}
输出结果:
Element 0: 10
Element 1: 2
Element 2: 3
Element 3: 20
Element 4: 5
可以看到,data 函数用于获取 std::vector 容器底层存储数组的指针,并可以通过不同的重载版本来实现读取和修改操作,适用于常规和 const 对象。需要注意的是,在使用 data 函数时,需要确保 std::vector 不为空,否则获取到的指针可能是空指针。
2.5 vector增删查改函数
2.5.1 assign()
template <class InputIterator> void assign (InputIterator first, InputIterator last);
void assign (size_type n, const value_type& val);
std::vector 提供了两种重载的 assign 函数,用于替换 std::vector 中的内容:
template <class InputIterator> void assign (InputIterator first, InputIterator last);
:
- 这个重载模板函数接受一对迭代器 first 和 last 作为参数,它们表示一个范围。assign 函数将容器的内容替换为指定范围中的元素。
- 这个重载函数可以接受任何类型的迭代器,例如指针,普通迭代器或者 const 迭代器等。只要指定的范围是有效的,assign 函数就会将容器的内容替换为该范围内的元素。
void assign (size_type n, const value_type& val);
:
- 这个重载函数接受一个整数 n 和一个值 val 作为参数,将容器的内容替换为 n 个 val 值。
- n 表示新容器应该包含多少个 val 值,容器的大小将设置为 n,原来的元素都将被替换。
下面是使用 assign 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector;
// 使用 assign 重载函数替换容器内容为一组元素
std::vector<int> sourceVector = {10, 20, 30, 40, 50};
myVector.assign(sourceVector.begin(), sourceVector.end());
// 打印容器内容
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
// 使用 assign 重载函数替换容器内容为多个相同的元素
myVector.assign(5, 100);
// 打印容器内容
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
10 20 30 40 50
100 100 100 100 100
在这个示例中,我们首先使用 assign 函数将 myVector 容器的内容替换为另一个向量 sourceVector 中的元素。然后,我们再次使用 assign 函数将 myVector 的内容替换为 5 个值为 100 的元素。
2.5.2 push_back()
void push_back (const value_type& val);
std::vector 的 push_back 函数用于向容器的末尾添加一个新元素。它接受一个引用参数 const value_type& val,表示要添加的新元素的值。
其中,value_type 是 std::vector 中元素的类型,通常是模板参数中指定的类型。const 表示在函数中不会修改传入的值。
下面是一个使用 push_back 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector;
myVector.push_back(10);
myVector.push_back(20);
myVector.push_back(30);
// 打印容器内容
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
10 20 30
2.5.3 pop_back()
void pop_back();
std::vector 的 pop_back 函数用于从容器的末尾移除一个元素。它没有参数,直接从容器的末尾删除最后一个元素。
下面是一个使用 pop_back 函数的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {10, 20, 30};
std::cout << "Before pop_back: ";
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
myVector.pop_back();
std::cout << "After pop_back: ";
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
Before pop_back: 10 20 30
After pop_back: 10 20
在这个示例中,我们创建了一个 std::vector 容器,并初始化了三个元素:10、20 和 30。然后,我们使用 pop_back 函数将最后一个元素 30 从容器中移除。
2.5.4 insert()
iterator insert (iterator position, const value_type& val);
void insert (iterator position, size_type n, const value_type& val);
template <class InputIterator> void insert (iterator position, InputIterator first, InputIterator last);
std::vector 的 insert 函数用于在指定位置插入一个或多个元素。
1. 插入单个元素:
这个版本的 insert 函数在 position 位置之前插入一个元素,并返回指向插入元素的迭代器。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3};
std::vector<int>::iterator it = myVector.begin() + 1;
myVector.insert(it, 100);
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
1 100 2 3
2. 插入多个相同元素:
这个版本的 insert 函数在 position 位置之前插入 n 个值为 val 的元素。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3};
std::vector<int>::iterator it = myVector.begin() + 1;
myVector.insert(it, 3, 100);
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
1 100 100 100 2 3
3. 插入范围内的元素:
这个版本的 insert 函数在 position 位置之前插入从 first 到 last 区间内的元素。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3};
std::vector<int> toInsert = {100, 200};
std::vector<int>::iterator it = myVector.begin() + 1;
myVector.insert(it, toInsert.begin(), toInsert.end());
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
1 100 200 2 3
在所有的 insert 函数中,参数 position 是一个迭代器,用于指定要插入的位置。这些函数将在 position 之前插入元素,并调整容器中的其他元素。
2.5.5 erase()
iterator erase (iterator position);
iterator erase (iterator first, iterator last);
std::vector 的 erase 函数用于从容器中移除一个或一段元素。
1.移除单个元素:
这个版本的 erase 函数移除由 position 指向的元素,并返回指向下一个元素的迭代器。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = myVector.begin() + 2;
myVector.erase(it);
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
1 2 4 5
2. 移除一段元素
这个版本的 erase 函数移除从 first 到 last 区间内的元素,并返回指向下一个元素的迭代器。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
std::vector<int>::iterator it1 = myVector.begin() + 1;
std::vector<int>::iterator it2 = myVector.begin() + 3;
myVector.erase(it1, it2);
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
1 4 5
在所有的 erase 函数中,参数可以是迭代器或者是一个表示范围的迭代器对,用于指定要移除的元素的位置。这些函数将移除指定的元素,并调整容器中的其他元素。移除后,迭代器失效,需小心使用。
2.5.6 swap()
void swap (vector& x);
std::vector 的 swap 函数用于交换两个顺序表的内容。它将当前顺序表和参数顺序表 x 的内容进行交换。
示例:
#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;
for (int num : vec1) {
std::cout << num << " ";
}
std::cout << std::endl;
for (int num : vec2) {
std::cout << num << " ";
}
std::cout << std::endl;
vec1.swap(vec2);
std::cout << "After swap:" << std::endl;
for (int num : vec1) {
std::cout << num << " ";
}
std::cout << std::endl;
for (int num : vec2) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
Before swap:
1 2 3
4 5 6
After swap:
4 5 6
1 2 3
注意:swap 函数交换的是容器的内容,并不交换容器的容量大小,所以在交换后,两个容器的容量仍然保持不变。这可能导致在交换后,某个容器的容量比实际元素个数多,从而导致额外的内存分配和浪费。如果你想要交换两个容器的容量大小,可以使用 std::vector 的移动构造函数和移动赋值运算符。
2.5.7 clear()
void clear();
clear 函数用于清空 std::vector 中的所有元素,使其变为空顺序表。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << "Before clear:" << std::endl;
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
vec.clear();
std::cout << "After clear:" << std::endl;
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
Before clear:
1 2 3 4 5
After clear:
注意:clear 函数只清空容器中的元素,并不改变容器的容量大小。如果你想要同时清空元素并释放内存,可以使用 std::vector 的 shrink_to_fit 函数。
2.5.8 emplace() C++11
template <class... Args> iterator emplace (const_iterator position, Args&&... args);
emplace 函数是 std::vector 的一个成员函数,用于在指定位置插入一个新元素,并通过传递参数来构造该元素。它支持在容器中的指定位置就地构造元素,而不需要额外的拷贝或移动操作。
参数说明:
position: 指定插入位置的迭代器,表示新元素将插入到该位置之前。
args: 可变参数模板,表示新元素的构造参数。
返回值:
返回一个迭代器,指向插入的新元素。
示例:
#include <iostream>
#include <vector>
class Person {
public:
Person(const std::string& name, int age) : name_(name), age_(age) {
std::cout << "Constructing " << name_ << ", Age: " << age_ << std::endl;
}
private:
std::string name_;
int age_;
};
int main() {
std::vector<Person> people;
// 使用 emplace 在位置 0 处插入新元素
people.emplace(people.begin(), "zhangsan", 25);
people.emplace(people.begin() + 1, "lisi", 30);
return 0;
}
输出结果:
Constructing zhangsan, Age: 25
Constructing lisi, Age: 30
在上述示例中,emplace 函数会在指定位置就地构造 Person 类的对象,并通过传递参数 “zhangsan” 和 25 构造了一个名为 zhangsan,年龄为 25 的 Person 对象。然后又在位置 1 处就地构造了一个名为 lisi,年龄为 30 的 Person 对象。由于 emplace 避免了拷贝或移动操作,因此效率较高。
2.5.9 emplace_back() C++11
template <class... Args> void emplace_back (Args&&... args);
emplace_back 是 std::vector 的一个成员函数,用于在容器的末尾就地构造一个新元素,并通过传递参数来构造该元素。
示例:
#include <iostream>
#include <vector>
class Person {
public:
Person(const std::string& name, int age) : name_(name), age_(age) {
std::cout << "Constructing " << name_ << ", Age: " << age_ << std::endl;
}
private:
std::string name_;
int age_;
};
int main() {
std::vector<Person> people;
// 使用 emplace_back 在容器末尾插入新元素
people.emplace_back("zhangsan", 25);
people.emplace_back("lisi", 30);
return 0;
}
输出结果:
Constructing zhangsan, Age: 25
Constructing lisi, Age: 30
在上述示例中,emplace_back 函数会在 people 容器的末尾就地构造 Person 类的对象,并通过传递参数构造了一个名为 zhangsan,年龄为 25 的 Person 对象,然后又构造了一个名为 lisi,年龄为 30 的 Person 对象。由于 emplace_back 避免了拷贝或移动操作,因此效率较高。
2.5.10 vector 迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
对于vector可能会导致其迭代器失效的操作有:
1. 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。
#include <iostream>
using namespace std;
#include <vector>
int main()
{
vector<int> v{ 1,2,3,4,5,6 };
auto it = v.begin();
v.assign(100, 8);
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
出错原因:
以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
解决方式:
在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。
2. 指定位置元素的删除操作–erase
#include <iostream>
using namespace std;
#include <vector>
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据,导致pos迭代器失效。
v.erase(pos);
cout << *pos << endl; // 此处会导致非法访问
return 0;
}
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
3.删除vector中所有的偶数
迭代器失效:
int main()
{
vector<int> v{ 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
v.erase(it);
++it;
}
return 0;
}
正确调用:
int main()
{
vector<int> v{ 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
it = v.erase(it);
else
++it;
}
return 0;
}
4.Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。
//扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main()
{
vector<int> v{ 1,2,3,4,5 };
for (size_t i = 0; i < v.size(); ++i)
cout << v[i] << " ";
cout << endl;
auto it = v.begin();
cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
// 通过reserve将底层空间设置为100,目的是为了让vector的迭代器失效
v.reserve(100);
cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
// 经过上述reserve之后,it迭代器肯定会失效,在vs下程序就直接崩溃了,但是linux下不会
// 虽然可能运行,但是输出的结果是不对的
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
程序输出:
1 2 3 4 5
扩容之前,vector的容量为: 5
扩容之后,vector的容量为 : 100
0 2 3 4 5 409 1 2 3 4 5
//erase删除任意位置代码后,linux下迭代器并没有失效
// 因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的
#include <vector>
#include <algorithm>
int main()
{
vector<int> v{ 1,2,3,4,5 };
vector<int>::iterator it = find(v.begin(), v.end(), 3);
v.erase(it);
cout << *it << endl;
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
程序可以正常运行,并打印:
4 4
5
// erase删除的迭代器如果是最后一个元素,删除之后it已经超过end
// 此时迭代器是无效的,++it导致程序崩溃
int main()
{
vector<int> v{ 1,2,3,4,5 };
// vector<int> v{1,2,3,4,5,6};
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
v.erase(it);
++it;
}
for (auto e : v)
cout << e << " ";
cout << endl;
return 0;
}
使用第一组数据时,程序可以运行
[kingxzq@localhost]$ g++ testVector.cpp - std = c++11
[kingxzq@localhost]$ . / a.out
1 3 5
使用第二组数据时,程序最终会崩溃
[kingxzq@localhost]$ vim testVector.cpp
[kingxzq@localhost]$ g++ testVector.cpp - std = c++11
[kingxzq@localhost]$ . / a.out
Segmentation fault
从上述三个例子中可以看到:SGI STL中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的。
5. 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效
#include <string>
void TestString()
{
string s("hello");
auto it = s.begin();
// 放开之后代码会崩溃,因为resize到20会string会进行扩容
// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
// 后序打印时,再访问it指向的空间程序就会崩溃
//s.resize(20, '!');
while (it != s.end())
{
cout << *it;
++it;
}
cout << endl;
it = s.begin();
while (it != s.end())
{
it = s.erase(it);
// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
// it位置的迭代器就失效了
// s.erase(it);
++it;
}
}
迭代器失效解决办法:在使用前,对迭代器重新赋值即可。
结语
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!