构造函数
int main()
{
vector<int> v;//无参构造(其实是全缺省)
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int> v1(v);//拷贝构造
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
void test_vector() { vector<int> v1(10, 1);//填充构造 for (auto e : v1) { cout << e << " "; } cout << endl; vector<int> v2(v1.begin(), v1.end());//迭代器构造 for (auto e : v2) { cout << e << " "; } cout << endl; string s1("hello wjj"); vector<char> v3(s1.begin()+3, --s1.end()); for (auto e : v3) { cout << e << " ";//lo wj } cout << endl; vector<char> v4(s1.begin(), s1.end());//只要迭代器的指向可以传到容器的数据类型上去,就可以构造。char类型也可以用整型接收。cout的话就是输出对应的ASCII码值 }
迭代器
所有迭代器的区间是左闭右开的
对于vector和string这种类型或许迭代器并不是很好用,因为有更好的范围for(缺点是不支持倒序)和[]+下标。但是以后的容器比如说list,虽然也可以重载[],但是由于它的底层结构并不是顺序表,所以实现的时间复杂度太高了,需要遍历才可以从第一个开始找,这个时候需要依赖迭代器。
resize函数
void resize (size_type n, value_type val = value_type())
void test_vector4()
{
vector<int> v1;
v1.resize(10, 0);//resize的时候直接给缺省值
vector<int> v2(10, 1);
for (auto ch : v1)
{
cout << ch;
}
cout << endl;
for (auto ch : v2)
{
cout << ch;
}
}
erase函数和insert函数
iterator erase (iterator position);//删除一个
iterator erase (iterator first, iterator last);//删除连续个
iterator insert (iterator position, const value_type& val);//single element
void insert (iterator position, size_type n, const value_type& val);//fill
template <class InputIterator> void insert (iterator position, InputIterator first, InputIterator last);//range
erase\insert采用的都是迭代器,但是vector并没有find函数,而是采用的标准库里面的std::find,之所以在vector中不提供find的函数的原因是因为标准库里面的find的函数是适配vector以及其他的容器的。但是由于vector的底层也是数组,所以并不建议过多的使用这两个函数。
template <class InputIterator, class T>
InputIterator find (InputIterator first, InputIterator last, const T& val);
函数返回值是:If no elements match, the function returns last。而我们提供的迭代器的区间是左闭右开的,所以last我们是取不到的,这里通过返回last来告诉我们并没有匹配的选项。
void test_vector()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator pos = find(v.begin(), v.end(), 2);//采用标准库的find函数
if (pos != v.end())
{
v.insert(pos, 20);
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
pos = find(v.begin(), v.end(), 2);//想要得到pos必须要再继续find一遍,因为pos存在迭代器失效的问题
if (pos != v.end())
{
v.erase(pos);
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v.erase(v.begin());
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
vector<char>
首先string类里面结尾会带'\0',但是vector<char>是没有的。其二vector<char>比较大小的方法和string并不是同一个比较逻辑。最后string还有许多自己特殊的要求,虽然二者再结构以及用法有许多相似的地方。
vector模拟实现
对于编译器,空间并不是直接new出来的,new出来的空间优势是已经初始化了,已经初始化的意思是比如new出来的class T的空间返回值是ptr,ptr所指向的位置一定是一个已存在的以初始化的对象,所以可以赋值,我们可以直接通过*ptr=T(xxx),但是这种方式的缺点是需要空间就去new,new需要去堆区开辟空间。而编译器一般是先去堆区申请一大块空间到内存池这个地方,但这一大块空间是没有初始化的,以后需要空间就直接去内存池去拿,但是要通过定位new对该空间进行初始化赋值。
内置类型不管是否初始化都可以赋值,但是自定义类型对于一个内存要初始化了才可以进行赋值,自定义类型new出来的空间是初始化的,这里初始化了也可以理解为就相当于是一个实例化的对象了,所以可以对自定义类型赋值。
构造函数
默认构造和fill构造
vector() :_start(nullptr) , _finish(nullptr) , _end_of_storage(nullptr) { } vector(size_t n, const T& val = T())//第二个参数是const T& val = T()中T()是匿名对象,匿名对象周期就在这一行,按道理会导致val在下面的代码中无法继续使用,因为val所代表的数据已经销毁了。但是const(普通医用不行是因为匿名对象具有常性)引用会延长匿名对象的生命周期到引用对象的作用域结束,这是因为以后用val就是用匿名对象,就好像匿名对象重新有了一个名字。 //必须要写初始化列表,否则的话用vector<int> v1(10,5)来进行实例化对象的时候,v1的成员变量都是随机值,此时开始进入reserve(n)函数,这个函数由于_start非空,会进入到delete[]_start、memcpy函数,导致后面一些列错误。所以必须初始化。 :_start(nullptr) , _finish(nullptr) , _end_of_storage(nullptr) { reserve(n); for (size_t i = 0; i < n; i++) { push_back(val); } }
fill构造函数有一个参数是const T& val = T(),对于自定义类型是存在构造函数的,那内置类型呢?c++由于函数模板的出现,不得不要求内置类型也必须要有构造函数,为了兼容函数模板的概念。也是这个原因,我们一般希望自定义类型最好要有一个默认构造函数,可以是编译器自己生成的,也可以是无参的或者是全缺省的。
int main() { int i = int(); int j = int(2); return 0; }
迭代器构造
类模板的成员函数又是一个模板,这里采用模板而非iterator的原因是因为如果采用iterator就将这个迭代器构造的函数写死了,只可以用vector的迭代器来进行构造。但事实并不是这样,只要类型符合,任意类型的迭代器都可以用。也就是只要存储的数据类型符合,比如说两方都是int、char或者是可以隐式类型转换如一个是int一个是char。
//采用迭代器进行构造 template <class InputIterator> vector(InputIterator first, InputIterator last) :_start(nullptr) , _finish(nullptr) , _end_of_storage(nullptr) { while (first != last) { push_back(*first); first++; } } //当有了迭代器构造和fill构造函数之后,会遇到下面的问题 void testvector3() { vector<int>v(10, 5);//该行代码会调用哪个构造函数,其实会调用迭代器构造函数,然后报错非常间接寻址,因为他会对10进行解引用。因为他会把class InputIterator看成int类型,而vector(size_t n, const T& val = T())第一个参数是无符号整型,第二个是整型的引用,调用fill构造函数的时候第一个需要进行类型转换,相比迭代器构造,没有那么的匹配。 } //解决方法1 void testvector3() { vector<int>v(10u, 5);//让编译器将第一个参数认为是无符号整型 } 解决方法2,再重载一个参数类型不同的fill构造函数 vector(int n, const T& val = T()) { reserve(n); for (int i = 0; i < n; ++i) { push_back(val); } }
//迭代构造函数的使用 void testvector3() { vector<int>v1(5u, 5); v1.insert(v1.begin(), 2); v1.insert(v1.begin(), 4); print(v1); vector<int> v2(v1.begin(), v1.end()); print(v2); vector<int> v3(v1.begin()+1, v1.end()-1);//vector<int> v3(v1.begin()++, v1.end()--)是会报错的,因为begin()、end()是传值返回,返回的是一个拷贝,这个拷贝其实是一个临时对象,这个临时对象具有常性,无法自己++或者-- print(v3); std::string s1("hello wjj"); vector<int> v4(s1.begin(), s1.end()); print(v4); int arr[] = { 1,2,3,4,5,6 }; vector<int> v5(arr, arr + 6);//此处采用的是原生指针,因为原生指针可以当成天然的迭代器,但要求原生指针指向数组。迭代器的区间是左闭右开,所以要注意第二个参数其实是指向最后一个有效数据arr+5的下一个位置arr+6 print(v5); }
拷贝构造
//如果采用系统默认的拷贝构造,也就是浅拷贝(值拷贝)
void testvector5()
{
vector<int> v1;
v1.push_back(5);
v1.push_back(2);
v1.push_back(7);
v1.push_back(3);
v1.push_back(6);
v1.push_back(8);
vector<int> v2(v1);
}//后果就是v2和v1的_start是指向堆区的同一个位置,会析构两次
//拷贝构造,深拷贝通过复用实现
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto ch : v)
{
push_back(ch);
}
}
拷贝构造,深拷贝通过传统方法实现
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
memcpy(_start, v._start, sizeof(T)*)v.size()];
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
//解决memcpy是浅拷贝的问题
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];//通过赋值来解决memcpy浅拷贝的问题,比如说string的赋值,回去开一个一样大的空间,并且将数据拷贝过来。
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
}
//拷贝构造一种比较现代的写法
vector(const vector<T>& v)
{
vector<T> tmp(v.begin(), v.end());//迭代器构造是通过push_back迭代器v.begin()来实现的,而push_back是通过*finish=*v.begin()来实现的,赋值运算符重载采用深拷贝自己写
swap(tmp);
}
//由上述问题牵扯出来的reserve函数也存在第二层是浅拷贝的问题
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);//如果T是string的这种自定义类型,扩容之后vector中的string元素的_str成员变量和扩容之前的string元素的_str是指向同一个空间,但是下一行代码马上就对这些旧空间进行了析构
delete[] _start;
}
_start = tmp;
_finish = _start + sz;//不要写成了_finish=_start+size()=_start+_finish-_start,否则_finish还是老样子,没有存在更新
_end_of_storage = _start + n;
}
}
//reserve函数的优化
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * sz);c次数是浅拷贝
for (size_t i = 0; i < size(); i++)
{
tmp[i] = _start[i];//但是产生了新的问题,如果此处T是vector<int>类型,需要用到赋值运算符重载,这里如果我们不写的话,就采用编译器默认生成的运算符重载,而这里是浅拷贝。所以我们要自己写一下赋值运算符重载,以实现深拷贝。
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;//不要写成了_finish=_start+size()=_start+_finish-_start,否则_finish还是老样子,没有存在更新
_end_of_storage = _start + n;
}
}
赋值运算符重载
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)//比如说v1=v2;此时v是v2的拷贝构造,此时v是v1想要的,而v1本身的可以丢弃了
{
swap(v);
return *this;
}
以上的几个构造函数都重复写了初始化列表,由于都是内置类型,我们可以不写初始化列表,全部在类的成员变量声明的地方给缺省值,缺省值是给初始化列表用的,构造函数有初始化列表,如果不屑初始化列表,会自动拿缺省值进行初始化。
迭代器的指针类型
int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::vector<int>::iterator it = v.begin(); cout << typeid(it).name() << endl;//该函数可以用来输出对象的数据类型 return 0; }
在gcc编译器当中,其实vector的迭代器是原生指针类型,我们这边模拟实现的方式也是按照linux的版本实现的。但是在vs当中迭代器并不是原生指针类型,可以理解为原生指针的封装。
typedef T* iterator; typedef const T* const_iterator;
reserve函数(再拷贝构造中还要进行修改)
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;//不要写成了_finish=_start+size()=_start+_finish-_start,否则_finish还是老样子,没有存在更新
_end_of_storage = _start + n;
}
}
resize函数
void resize(size_t n, T val = T())
{
if (n < size())
{
_finish = _start + n;//缩小size就是将_finish减减
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_finish != _start + n)//_finish指向的最后一个有效数据,而_end_of_storage指向的位置是整个容器的最后一位
{
*_finish = val;
++_finish;
}
}
}
push_back函数
void push_back(const T& x)
{
if (capacity() == size())
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;
}
insert函数
iterator insert(iterator pos, const T& val)//防止迭代器失效的方法之一是insert之后我们默认迭代器pos失效了,就不再使用了。
{
assert(pos <= _finish);
assert(pos >= _start);
if (capacity() == size())
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());//这里的扩容基本上是异地扩容,所以pos的位置在扩容之后应当发生调整。 pos = _start + len;这里是用来更新扩容之后的pos,解决pos这个迭代器失效的问题。然后再往pos位置插入val。
//但是问题是出了这个函数,pos还是扩容之前的pos,因为形参的改变并不能影响实参。这里的解决办法是将pos做为函数的返回值。stl库里面返回的也是这个变量值。
// 还有一个解决方法将第一个形参改为引用iterator& pos,但是会有问题,因为v.insert(v.begin(),0)是会报错的。这是因为所有的传值返回返回的都是拷贝,这种拷贝称为临时对象,这种临时对象具有常性,是不能直接传给引用的,可以产给const itreator& pos,但是这样的话会导致pos没办法更新。
//但我们也要知道,如果没有扩容这个步骤,是不会有上述这些问题的
}
iterator end = _finish;
while (end > pos)
{
*end = *(end - 1);
end--;
}
*pos = val;
++_finish;
return pos;
}
erase函数
iterator erase(iterator pos)//erase之后的,我们依旧认为它失效了,最好不要去访问了。因为当你erase的是vector中的末尾位置的有效数据,其实此时你再去访问pos位置就显的不是很合理了。
{
assert(pos >= _start);
assert(pos < _finish);
iterator front = pos;
while (front < _finish - 1)
{
*(front) = *(front + 1);
front++;
}
_finish--;
return pos;
}
迭代器失效的问题
insert迭代器会失效是因为insert有可能导致扩容,扩容之后pos的位置是发生变化了的,如果不对pos进行更新的话,pos就属于野指针。erase我们也认为迭代器失效了,因为当pos的位置是_finish-1,erase之后再去访问pos就不是合法的了,因为此时pos的位置和_finish的位置是相同的,而_finish并不是一个合法的位置。对于迭代器的比较,推荐使用!=,虽然现在vector和string由于底层是顺序表,可以使用大于小于进行比较,但是一旦到了list链表阶段,大小与就不好使了。
对于迭代器失效的问题,erase和insert都是通过返回值来进行解决的,insert返回pos,erase返回的是pos的下一个数据的位置,其实如果采用我自己实现的代码,返回的还是pos。
string也有迭代器失效,但是insert和erase喜欢用下标,所以很少出现迭代器失效的问题。
sort函数
void testvector4() { vector<int> v1; v1.push_back(5); v1.push_back(2); v1.push_back(7); v1.push_back(3); v1.push_back(6); v1.push_back(8); print(v1); std::sort(v1.begin(), v1.end()); print(v1); int arr[] = { 1,9,7,6,8,3 }; std::sort(arr, arr + 6); for (auto ch : arr) { std::cout << ch; } }