✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++学习
贝蒂的主页:Betty’s blog
1. C/C++中的数组
1.1. C语言中的数组
在 C 语言中,数组是一组相同类型元素的有序集合。与字符串类似,它的大小在编译时就已经确定,不可更改。
//大小为5的整型数组
int arr1[5] = { 1,2,3,4,5 };
//大小为5的浮点型数组
double arr2[5] = {0.0};
1.2. C++中的数组
同样与string
类似,C++为了更加方便就引入了一个支持可动态大小数组的序列容器vecotr
。其特点如下:
vector
是可变大小的序列容器,采用连续存储空间存储元素,可通过下标高效访问。- 与数组不同,
vector
大小可动态改变,由容器自动处理。vector
本质上用动态分配数组存储元素,插入新元素时可能重新分配空间,即分配新数组并移动全部元素,此操作时间代价高,但不是每次插入都重新分配。vector
会分配额外空间适应增长,不同库策略不同,但重新分配通常是对数增长间隔,使末尾插入元素能在常数时间完成。- 与
deque
、list
和forward_list
相比,vector
访问元素及末尾添加和删除元素更高效,非末尾的删除和插入操作效率低,且统一的迭代器和引用更好。
//整型数组
vector<int> v1;
//浮点型数组
vector<double> v2;
并且注意每次使用vector
都需要包含头文件#include<vector>
。并且vector
是一个模版类,所以在使用时需要显示实例化。
2. vector的接口
接下来我们将介绍一些vector
的常见接口,因为很多接口的作用都与string
的接口非常类似,所以很多就不在详细说明,大家具体也可以参考vector的使用。
2.1. vector的迭代器
同样的vector
中也存在迭代器iterator
,因为定义在vector
类中,所以其需要通过域作用限定符访问——vector<类型>::iterator
下面将介绍的begin()
,end()
,rbeign()
,rend()
的使用访问方法与string
中的几乎一摸一样,我们直接上实例演示:
void Test1()
{vector<int> v = {1,2,3,4,5,6,7,8};vector<int>::iterator it = v.begin();cout << "顺序遍历:";while (it != v.end()){cout << *it << " ";++it;}cout << endl;cout << "逆序遍历:";vector<int>::reverse_iterator rit = v.rbegin();while (rit != v.rend()){cout << *rit << " ";++rit;}
}
当然vector
也支持const_iterator
,用法也类似,这里就不在赘述。
2.2. vector的初始化与销毁
同样的vector
也支持多种构造函数,拷贝构造以及赋值运算符重载。
void Test2()
{//1.默认构造函数初始化vector<int> v1;//2.n个val初始化vector<int> v2(3, 2);string s("abcd");//3.利用迭代器区间初始化vector<int> v3(s.begin(), s.end());//4.拷贝构造vector<int> v4(v3);//5.赋值重载v2 = v3;//6.可变参数列表初始化vector<int> v5 = { 1,2,3,4,5 };vector<char> v6 = v4;//error 不同类型不能赋值
}
其中需要注意的是可变参数列表初始化,这是在C++11之后支持的新语法,具体讲解我们之后再谈。
2.3. vector的容量操作
函数名称 | 功能 |
---|---|
size | 返回数组的有效长度 |
capacity | 返回数组的容量大小 |
clear | 清空数组 |
empty | 检查是否为空数组,是则返回ture,否则返回false |
reserve | 请求改变数组的容量 |
resize | 重新设置有效元素的数量,超过原来有效长度则用c字符填充 |
2.3.1. 有效长度与容量大小
在vector
类中,我们同样可以通过size()
容器的有效长度;capacity()
返回容器的容量大小。
void Test3()
{vector<int> v = { 1,2,3,4,5 };cout << v.size() << endl;cout << v.capacity() << endl;
}
在初始化时,vecotr
中的size
与capacity
一般相同。这时我们也可以通过以下程序探究一下其扩容机制:
void TestExpand()
{size_t sz;vector<int> v;sz = v.capacity();cout << "making v grow:"<<endl;for (int i = 0; i < 100; ++i){v.push_back(i);if (sz != v.capacity()){sz = v.capacity();cout << "capacity changed: " << sz << endl;}}
}
在VS环境下,vector
一般是以1.5倍扩容。但是在Linux环境下一般就以2倍扩容。
至于clear()
与empty()
两个函数用法就十分简单,这里就不在赘述了。
2.3.2. 有效长度与容量操作
接下来我们来使用一下vector
中的resize()
与reserve()
。其实他们的用法与特点也是与string
类中的相同,我们直接上手即可
- 当
n<sz
时,reserve
并不会发生任何改变,resize
会删除有效字符到指定大小。- 当
sz<n<capcity
时,reserve
并不会发生任何改变,resize
会补充有效字符(默认为0)到指定大小。- 当
n>capacity
时,reserve
会发生扩容,resize
会补充有效字符(默认为0)到指定大小。
void Test4()
{vector<int> v1 = { 1,2,3,4,5 };cout << "v1的有效长度为:" << v1.size() << endl;cout << "v1的容量大小为:" << v1.capacity() << endl;v1.reserve(10);cout << "v1的有效长度为:" << v1.size() << endl;cout << "v1的容量大小为:" << v1.capacity() << endl;v1.resize(8, 10);for (auto& e : v1){cout << e << " ";}
}
在这里我们需要注意一个经典错误,如下列代码:
void Test5()
{vector<int> v;v.reserve(10);for (int i = 0; i < 10; i++){v[i] = i;}for (auto& e : v){cout << e << " ";}
}
一旦运行就会发生如上错误,这是为什么呢?因为reserve
只是改变了容量capacity
并没有改变size
,而operator[]
访问时元素时是禁止访问下标size
以后的元素的,一旦访问就会直接报错。
2.4. vector的访问操作
函数名称 | 功能 |
---|---|
operator[] | 返回指定位置的元素,越界则报错 |
at | 返回指定位置的元素,越界则抛异常 |
back | 返回字符串最后一个元素 |
front | 返回字符串第一个元素 |
这四个函数的用法也与string
中的函数用法相同,我们就直接上手示例
void Test6()
{vector<int> v = { 1,2,3,4,5 };for (int i=0;i<v.size();i++){cout << v[i] << " ";}cout << endl;for (int i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl;cout << "front:" << v.front() << endl;cout << "back:" << v.back() << endl;
}
2.4. vector的修改操作
函数名称 | 功能 |
---|---|
push_back | 在数组后追加元素 |
insert | 在指定位置追加元素 |
assign | 使用指定数组替换原数组 |
pop_back | 删除数组最后一个元素 |
erase | 删除数组指定部分区间 |
swap | 交换两个数组 |
我们首先先介绍最简单的四个函数push_back()
,pop_back()
,assign()
,swap()
。
void Test7()
{vector<int> v = { 1,2,3,4,5,6 };cout << "back:" << v.back() << endl;//尾插v.push_back(7);//尾删cout << "back:" << v.back() << endl;v.pop_back();cout << "back:" << v.back() << endl;vector<int> vv = { 6,5,4,3,2,1 };//n个val赋值给原数组vv.assign(3, 2);for (int i = 0; i < vv.size(); i++){cout << vv[i] << " ";}cout << endl;vv.swap(v);for (int i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl;for (int i = 0; i < vv.size(); i++){cout << vv[i] << " ";}
}
然后我们来介绍insert()
与·earse()
的用法,这两个函数的用法就与string
中的有所不同。首先是insert()
函数:
void Test8()
{vector<int> myvector(3, 100);vector<int>::iterator it = myvector.begin();//1.向指定位置插入一个元素it = myvector.insert(it, 200);cout << "myvector contains:";for (it = myvector.begin(); it < myvector.end(); it++)cout << ' ' << *it;cout << endl;//2.向指定位置插入n个元素myvector.insert(it, 2, 300);cout << "myvector contains:";for (it = myvector.begin(); it < myvector.end(); it++)cout << ' ' << *it;cout << endl;//3.向指定位置插入一段迭代器区间it = myvector.begin();vector<int> anothervector(2, 400);cout << "myvector contains:";for (it = myvector.begin(); it < myvector.end(); it++)cout << ' ' << *it;cout << endl;it = myvector.begin();myvector.insert(it + 2, anothervector.begin(), anothervector.end());//4.向指定位置插入一段迭代器区间int myarray[] = { 501,502,503 };myvector.insert(myvector.begin(), myarray, myarray + 3);cout << "myvector contains:";for (it = myvector.begin(); it < myvector.end(); it++)cout << ' ' << *it;cout << endl;
}
接下来我们继续来使用erase()
函数
void Test9()
{//1.删除迭代器所指元素vector<int> myvector;for (int i = 1; i <= 10; i++) myvector.push_back(i);vector<int>::iterator it = myvector.erase(myvector.begin() + 5);it = myvector.erase(it);//2.删除一段迭代器区间it = myvector.erase(myvector.begin(), myvector.begin() + 3);cout << "myvector contains:";for (int i = 0; i < myvector.size(); ++i)cout << ' ' << myvector[i];cout << endl;
}
虽然看起来vector
的insert()
和erase()
与string
的没有什么区别,但是仔细观察就可以发现我们每次使用完迭代器之后都会更新,这是为什么呢?
主要还是因为我们每次插入数组都可能发生扩容,而扩容分为就地扩容与异地扩容。如果发生的异地扩容,这时的迭代器就不在指向原来的空间,而就指向一块释放的内存,我们一旦继续访问就会报错,这种现象我们称为迭代器失效。为了避免出现这种情况,所以我们在使用完迭代器之后需要更新。