在前面我们已经学习过string的模拟实现了,这里简单说一下两者的区别和联系。
1、string有\0结尾。
2、vector和string比较大小的逻辑会有一定的差异:string使用ASCII进行比较,而vector可能会使用长度直接进行比较。
3、string 类型实际上可以看做是对字符序列的高级封装,它提供了许多方便的字符串操作方法,比如查找、替换、连接等,而且可以直接进行输入输出操作。
4、在模拟实现时,string中的_str总是要留出一个位置给\0,而vector不需要。
std::vector<char>
?是一个能够存储任意类型的动态数组,而?std::string
?则专门用于存储字符串,即字符序列。std::string
?实际上就是使用?std::vector<char>
?来实现的,因此在内部结构上它们可能是类似的。两者底层结构都是顺序表。vector?的底层就是一个动态的顺序表,其大小是可以动态改变的。
注意这里的动态改变通常是指当存储数据的空间不足时,它会自动扩容;当我们删除数据时,我们是不会进行缩容的,因为缩容的代价比较大。(这里我们在模拟实现string中讲到了)。
既然需要动态改变这个数组的大小,所以vector需要有一个指针指向空间起始位置 _start? ,有一个指针指向这块空间的末尾 _end_of_storage?;同时为了删除和增加数据方便还需要有一个指针指向数据的末尾 _finish .
private:
iterator _start; // 指向第一个元素
iterator _finish; // 指向最后一个元素的后一个地方(左闭右开)
iterator _end_of_storage; // 指向这块空间的后一个位置
这里的iterator是一个迭代器,我们使用指针来进行的一个宏定义。
// 迭代器
typedef T* iterator;
typedef const T* const_iterator;
先来看一下常用的构造函数模拟实现方法:
// 构造函数
vector()
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{}
vector(size_t n, const T& val = T())
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
}
第一种就是无参的构造函数,可以直接使用初始化列表对各个成员变量进行初始化,这种初始化的vector里边的内容是空的。
第二种,创建一个空间大小为n、并且使用一个初始值val对这块空间进行初始化的vector。
第三种,使用迭代器进行初始化。
需要注意的是,因为push_back()操作是需要使用到_start和_finish的,所以在进行push_back之前还要对这些成员变量进行初始化。
对于 vector(size_t n, const T& val = T()) 类型的构造构造来说,有几个要注意的点:
这里使用的 const T& val = T() ,还需要介绍一下:
首先,T() 相当调用一个默认构造函数,如果这个vector中存储的是一个Student自定义类型的对象,这行代码就相当于? const Student& val = Student();? ?这里使用 Student() 无参构造函数构建一个匿名对象,并将这个匿名对象取了一个别名 val,此时val的值就是这个匿名对象。
需要注意的是,对于自定义类型 T() 会调用该自定义类型的默认构造函数;对于几种基本类型来说,基本类型是没有默认构造函数的,但是为了适配这个场景下的用法,编译器是会为基本类型创建一个默认构造函数的。
int a = int();
double b = double();
cout << "a = " << a << endl;
cout << "b = " << b << endl;
class A
{
public:
A()
{
cout << "A()被调用" << endl;
}
~A()
{
cout << "~A()被调用" << endl;
}
};
int main()
{
A a1;
A();
A a3;
return 0;
}
在没有const修饰时,匿名对象的声明周期只在它定义的那一行(因为在这一行之后不会再使用这个对象了),这一行过后这个匿名对象就会被销毁,析构函数被调用。
int main()
{
A a1;
const A& a2 = A();
A a3;
return 0;
}
如果使用const修饰匿名对象,就能够将这个匿名对象的周期延长至a2的生命周期(其实就相当于为这个匿名对象起一个名字后,它的声明周期就被延长了)。
注:能够延长声明周期是因为加了const关键字,而不是引用的作用。
int main()
{
A a1;
A& a2 = A();
A a3;
return 0;
}
如果这样使用会发生以下报错:
这是因为匿名对象和临时对象具有常性。
在利用 vector<int> v1(10, 5); 构造一个vector时,可能会出现错误。
vector(size_t n, const T& val = T())
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
}
void test4_vector()
{
vector<int> v1(10, 5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
}
这是因为,在 v1(10, 5) 进行构造函数时,10和5都是int整形的,所以在调用构造函数时,会调用
vector(InputIterator first, InputIterator last)-----vector(int first, int last),然后内部会对int进行解引用(*first),发生错误。
而预期上,我们是想要调用 vector(size_t n, const T& val = T()),但是因为 n 是 size_t 的,由整形 int 10需要进行强制类型转化才能被 n 接收,而使用上面一个构造函数vector(int first, int last)是不需要进行强制类型转化的,所以会调用上面一个构造函数。
至于改进方法,可以利用函数重载或将 n 的实参设为 unsigned int 类型:
1、函数重载
vector(int n, const T& val = T())
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
2、实参类型使用unsigned int
void test4_vector()
{
vector<int> v1(10u, 5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
}
在vector进行初始化时,可以使用迭代器进行初始化,并且这个迭代器并不一定非要是vector的迭代器,也可以是其他类型的迭代器,只要能保证数据类型匹配就行了。
下面这个在v进行初始化时传递的是?string?的迭代器( s1.begin(), s1.end() ),因为字符可以转化成ASCII码,ASCII是整形的,所以可以这样使用,如:
int main()
{
string s1("abcdefg");
vector<int> v1(s1.begin(), s1.end());
for (auto n : v1)
{
cout << n << " ";
}
cout << endl;
vector<int> v2(s1.begin()+2, --s1.end());
for (auto n : v2)
{
cout << n << " ";
}
cout << endl;
return 0;
}
结果打印出的字符的ASCII码。
vector和string的迭代器都是随机迭代器,是可以进行++,--,+x
先来看一下最终拷贝构造函数的写法:
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
// 第一种
vector(const vector<T>& v)
: _start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
reserve(v.capacity());
iterator it = begin();
const_iterator vit = v.cbegin();
while (vit != v.cend())
{
*it++ = *vit++;
}
_finish = it;
}
// 第二种
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
当然还可以有以下这种写法:
// 第三种
vector(const vector<T>& v)
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
reserve(v.capacity());
for (const auto& e v)
push_back(e);
}
从下面开始,我们来看一下,拷贝构造函数是如何一步步写成这样的。
初始版本:
vector(const vector<T>& v)
{
T* temp = new T[v.capacity()];
memcpy(temp, v.begin(), sizeof(T) * v.size());
_start = temp;
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
这个拷贝构造函数(与string拷贝构造函数实现相同),对于一般情况是正确的。
void test5_vector()
{
vector<int> v1(10, 5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
vector<int> v2(v1);
for (size_t i = 0; i < v2.size(); ++i)
{
cout << v2[i] << " ";
}
cout << endl;
}
但是当在下面这种情况使用这个拷贝构造函数使用时,会出现程序崩溃的错误:
void test5_vector()
{
vector<std::string> v3(3, "111111111111111 ");
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
vector<std::string> v4(v3);
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
}
图解:
我们发现,vector存储的对象string中,它的字符数组和我们拷贝后vector对象中的字符数组指向同一块空间。
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
这时,在调用析构函数时会出现错误,在delete[] _start; 这一步中,在释放vector每一个元素string时,都会再调用string的析构函数,最后string中_str空间会被释放两次,发生错误。
只要当vector中存储的元素有深拷贝问题时,都会出现上面这个问题。
memcpy是浅拷贝,所以我们不能再使用memcpy进行拷贝vector内部的数据。
因为vector内部元素是 string,而在实现string中我们知道:operator=是深拷贝,所以可以采用循环 + operator= 的方式进行拷贝vector内部的数据。
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
还需要注意的是,只要是有拷贝过程都要考虑深浅拷贝的问题,不仅仅是在拷贝构造中。
在扩容时,我们开辟一块指定大小空间后,还要将原先数据拷贝到这块新空间中……
所以,扩容操作也注意不能使用memcpy进行拷贝,而应该使用赋值操作进行拷贝:
void reserve(size_t n)
{
// 判断n与capacity的关系,避免缩容
if (n > capacity())
{
T* temp = new T[n];
if (_start) // 如果_start为空,就不进行拷贝
{
// 深拷贝问题
//memcpy(temp, _start, sizeof(T) * size());
for (size_t i = 0; i < size(); i++)
{
temp[i] = _start[i];
}
delete[] _start;
}
size_t sz = size();
_start = temp;
_finish = _start + sz; // temp + (_finish - _start);
_end_of_storage = _start + n;
}
}
这样,就解决了vector中存放string对象的拷贝构造函数了。
那么如果vector中存放的是vector对象的拷贝构造函数又怎么实现呢?
同样的道理,我们还是不能使用memcpy进行拷贝数据(无论是? vector外壳的成员变量??还是 内部元素的成员变量?),拷贝内部元素时采用? ?循环 + operator=? ? 的方式进行拷贝。
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator= (vector<T> v) // 注意这里不能使用&
{
swap(v);
return *this;
}
vector(const vector<T>& v)
{
/*T* temp = new T[v.capacity()];
memcpy(temp, v.begin(), sizeof(T)* v.size());
_start = temp;
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();*/
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
这里operator=实现深拷贝的操作是直接交换两块空间的指针,而保持内容不变,这样效率比较高。
需要注意的是,如果我们自己没有进行operator=重载为深拷贝,编译器自己生成的operator=是浅拷贝,所以对于vector<vector<int>>类型的类,我们需要实现一个深拷贝的operator=的重载。
void swap(vector<int>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator= (vector<T> v)
{
swap(v);
return *this;
}
vector(const vector<T>& v)
{
vector<T> temp(v.begin(), v.end());
swap(temp);
}
分析:
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
int main()
{
vector<int> s1(5);
vector<int> s2;
s2 = s1;
return 0;
}
这里s2 = s1;语句会调用operator=操作符重载,并利用拷贝构造函数将s1作为参数传递给v,再利用交换指针的方法,将s2的指针指向v,v的指针指向s2,最后operator=函数调用结束后v指向的空间会被析构函数处理,也就是s1原本的空间会被释放掉。
注意:这里的operator=的参数不能使用引用:如果使用引用接收参数的话,会导致s1和s2的指针发生交换,s1的值会发生变化,不能达到赋值的功能。
使用空间配置器申请空间时,不会对已申请的空间进行初始化;
如果使用new进行申请空间时,是会对这块空间进行初始化的。(操作 = 开空间+调用构造函数)
对未初始化的空间进行直接赋值操作是一种未定义行为,这意味着它可能会导致程序的不可预测结果。C++编译器不会对未初始化的空间进行任何默认初始化,因此尝试直接对其进行赋值可能会导致访问未定义的内存。
在C++中,为了安全起见,应该始终确保在使用变量之前将其初始化。这可以通过以下几种方式实现:
在定义变量时进行初始化:
int x = 0; // 初始化为0
在声明变量后立即进行初始化:
使用构造函数进行初始化(适用于类对象):
class MyClass {
public:
int x;
MyClass() : x(0) {} // 构造函数初始化
};
MyClass obj; // 通过构造函数进行初始化
使用空间配置器申请空间通常要配合着定位new使用。
在vector的插入和删除操作需要用到迭代器,所以我们在讲解迭代器失效时,也会顺便实现一下插入和删除操作。
如果插入函数是以下这种方式实现的,在执行下面一个程序时就会出错:
void insert(iterator pos, const T& val)
{
assert(pos <= _finish);
assert(pos >= _start);
// 判断空间是否足够
if (_finish == _end_of_storage)
{
size_t sz = pos - _finish;
reserve(capacity() == 0 ? 4 : capacity() * 2); // 扩容
}
// 移动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val; // 在pos位置插入目标值
_finish++;
}
void test2_vector()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
auto pos = find(v1.begin(), v1.end(), 3);
v1.insert(pos, 30); // 在3之前插入30
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
}
该程序没有成功地在3的前面插入30.
先使用find找到指向指定位置的迭代器后,由于在插入函数reserve()进行了扩容操作导致原先的空间被释放,即pos指向的空间被释放了,pos不在指向3了,之后再利用pos迭代器进行移动数据和*pos = val 操作时就会出现错误。所以在插入函数中进行扩容后要更新pos的位置。
在插入函数内部更新迭代器的位置。
void insert(iterator pos, const T& val)
{
assert(pos <= _finish);
assert(pos >= _start);
// 判断空间是否足够
if (_finish == _end_of_storage)
{
size_t sz = pos - _finish;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _finish + sz; // 更新pos的位置
}
// 移动数据
iterator end = _finish - 1;
while (end>=pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
这种情况又分为两种情况:在插入时进行扩容和在插入时没有进行扩容操作。
void test2_vector()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
auto pos = find(v1.begin(), v1.end(), 3);
v1.insert(pos, 30); // 在3之前插入一个30
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
(*pos)++;
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
}
这个程序第一步成功地在3的前面插入了30;第二步利用pos迭代器将3进行++操作,但我们发现,3的值并没有被改变。
在插入之前,空间为4,而此时的数据已经有四个了,所以再进行插入操作时,会进行扩容操作,也就代表着原先的空间会被释放,pos指向的空间被释放了。
虽然在插入中修改pos的位置,可以正确地将30插入到3的前面。但是因为插入函数的迭代器是使用值传递,函数内部更新的迭代器只是形参,外部实参pos的位置没有改变 ,这样外部pos还是指向一块已经被释放的空间,最终导致不仅没有修改pos位置的值,同时还存在野指针解引用的问题。
如果没有进行扩容操作还是会有迭代器失效的问题:由于插入一个数后,迭代器的指向没有动态变化,此时通过迭代器修改的值并不是原先的值。
void test2_vector()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
auto pos = find(v1.begin(), v1.end(), 3);
v1.insert(pos, 30);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
(*pos)++;
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
}
在插入之前,空间有8个,数据只有5个,所以在插入一个数据时不会进行扩容操作。上述代码中在插入一个数据时没有进行扩容,同时迭代器pos指向3,但是在插入30之后,迭代器pos指向30而不再是3,所以修改的值为30,最后30变成31,而3却没有变成4.
导致这种错误的根源就在于insert内部的改变并不影响外部迭代器的指向。
但是传引用会导致下边这种情况:
// 使用引用接收参数
void insert(iterator& pos, const T& val)
{
assert(pos <= _finish);
assert(pos >= _start);
// 判断空间是否足够
if (_finish == _end_of_storage)
{
size_t sz = pos - _finish;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _finish + sz; // 更新pos
}
// 移动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
iterator begin()
{
return _start;
}
void test2_vector()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
cout << v1.size() << endl;
v1.insert(v1.begin() + 2, 10);
}
报错信息:
这是因为begin()函数的返回值是传值返回,而传值返回是通过创建一个临时变量返回给接收变量,又因为临时变量具有常性,所以返回值的的类型为const iterator,const iterator 不能传递给iterator pos(权限被放大了),如果将pos参数类型改为const iterator& ,这样就会导致pos指向的内容不能被修改,这就不能进行*pos = val 插入目标值了。
显然这个方法不完美。
看一下库里面的vector是怎么实现的。
它通过一个返回值来修改外部的迭代器,这个返回值返回的是指向新插入元素的迭代器。这样如果想要在外部使用迭代器,就需要自己手动地接收这个返回值来更新pos
错误写法:
void reserve(size_t n)
{
// 判断n与capacity的关系,避免缩容
if (n > capacity())
{
T* temp = new T[n];
if (_start) // 如果_start为空,就不进行拷贝
{
// 拷贝操作……
}
_start = temp;
_finish = _start + size(); // temp + (_finish - _start);
_end_of_storage = _start + capacity();
}
}
在该程序中,拷贝操作完成之后,将_start指向新空间的开头,之后利用_start + size更新_finish,这一步就出错了,因为_size = _finish - _start,所以_finish = _start + size(); --> _finish = _start + _finish - _start; 而这里的_finish 还是指向原来空间的数据末尾,所以_finish没有更新成功。
其做法就是先将原来空间中_finish和_start的相对位置记录下来,再利用这个相对位置更新_finish。同理,_end_of_strrage也需要进行同样的操作。
再加上上面拷贝构造函数深拷贝的讲解,可以得到正确写法:
void reserve(size_t n)
{
// 判断n与capacity的关系,避免缩容
if (n > capacity())
{
T* temp = new T[n];
if (_start) // 如果_start为空,就不进行拷贝
{
// 深拷贝问题
//memcpy(temp, _start, sizeof(T) * size());
for (size_t i = 0; i < size(); i++)
{
temp[i] = _start[i];
}
delete[] _start;
}
size_t sz = size(); // 记录相对位置
_start = temp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
iterator insert(iterator pos, const T& x)
{
assert(pos <= _finish);
// 空间不够先进行增容
if (_finish == _endOfStorage)
{
//size_t size = size();
size_t newCapacity = (0 == capacity()) ? 1 : capacity() * 2;
reserve(newCapacity);
// 如果发生了增容,需要重置pos
pos = _start + size();
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
测试:
void test2_vector()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
auto pos = find(v1.begin(), v1.end(), 3);
pos = v1.insert(pos, 30); // 利用返回值更新迭代器
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
(*pos)++;
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
}
void erase(iterator pos)
{
// 检查位置是否合法
assert(pos >= _start && pos < _finish);
iterator start = pos + 1;
while (start != _finish)
{
*(start - 1) = *start;
start++;
}
_finish--;
}
如果如上述方法实现erase(),在删除一个数据后,迭代器同样会失效。
void test2_vector()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
pos = find(v1.begin(), v1.end(), 5);
v1.erase(pos);
(*pos)++;
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
}
此时,显然通过pos访问5,并进行++是不合法的。并且在VS库中的 vector 是不允许这样使用的,所以我们在实现时也应该使用已经失效的迭代器。
同时,这样可能会导致行为结果未定义--在不同编译器下,同样的代码结果可能会不同:在VS标准库中利用迭代器删除一个数据后,这个迭代器会被强制检查;而在gcc编译器上,是不会进行报错的。
所以erase()也应该设计一个返回值来更新迭代器:
iterator erase(iterator pos)
{
// 挪动数据进行删除
iterator begin = pos + 1;
while (begin != _finish) {
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos;
}
总结一下,对于插入和删除操作后的迭代器,我们认为都是失效的,所以在插入和删除后尽量不要在使用这个迭代器了,如果还想使用就需要使用函数返回值来更新迭代器。
std::vector::at 和 std::vector::operator[]。
区别:
联系:
需要注意的是,在使用 std::vector::operator[] 时,需要自行确保索引的有效性,以避免访问越界导致未定义行为。而 std::vector::at 则在访问越界时提供了一种安全的异常处理机制。因此,在需要对越界情况进行处理时,推荐使用 std::vector::at;如果确定不会发生越界,可以使用 std::vector::operator[] 进行更高效的访问。
注意:
vs系列编译器,debug模式下
at() 和 operator[] 都是根据下标获取任意位置元素的,在debug模式下两者都会去做边界检查。
当发生越界行为时,at 是抛异常,operator[] 内部的assert会触发。
int main()
{
vector<string> vstr;
string s1("张三");
vstr.push_back(s1); // 第一种插入方法
vstr.push_back(string("李四")); // 第二种插入方法
vstr.push_back("王五"); // 第三种插入方法:隐式类型转化
for (const auto& e : vstr)
cout << e;
cout << endl;
cout << vstr[0][0]; // 只取出了'张'的第一个字节,不能打印出'张'
return 0;
}
vstr[0] == string s1,vstr[0][0] == string s1[0];并且一个汉字占两个字节,只有将两个字符完整地取出来汉字才能正确的打印出来。
int main()
{
vector<string> vstr;
string s1("张三");
vstr.push_back(s1); // 第一种插入方法
vstr.push_back(string("李四")); // 第二种插入方法
vstr.push_back("王五"); // 第三种插入方法:隐式类型转化
for (const auto& e : vstr)
cout << e;
cout << endl;
cout << vstr[0][0]; // 只取出了'张'的第一个字节
cout << vstr[0][1] << endl; // 取出了'张'的第二个字节
return 0;
}
同时,要注意endl的作用:
std::endl
是一个 C++ 标准库中的输出流控制符,它的作用有两个方面:
换行:std::endl
在输出流中插入一个换行符,并刷新输出缓冲区,使数据立即被输出到目标设备(比如屏幕)上。这意味着在使用 std::cout
输出内容时,可以通过插入 std::endl
来实现换行效果。
刷新缓冲区:除了换行之外,std::endl
还会刷新输出缓冲区。输出缓冲区是为了提高程序的效率而引入的,它会将输出数据先存储在缓冲区中,然后一次性地输出到目标设备上。但有时候我们需要立即将缓冲区的内容输出,比如在程序崩溃或需要立即观察输出结果的情况下。这时,可以使用 std::endl
来强制刷新输出缓冲区,确保数据被立即输出。
使用 std::endl
的语法是在输出流中插入该控制符,例如 std::cout << "Hello" << std::endl;
。与 '\n'
不同,std::endl
是一个函数模板,而不是字符常量,这使得它更加灵活和可移植。
需要注意的是,频繁地使用 std::endl
可能会导致性能下降,因为每次插入 std::endl
都会引发一次刷新操作。如果只需要换行而不需要刷新缓冲区,可以使用 '\n'
字符来实现相同的效果,例如 std::cout << "Hello\n";
。
今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……