C++ STL——string和vector
注:原创不易,转载请务必注明原作者和出处,感谢支持!
注:内容来自某培训课程,不一定完全正确!
一 STL基本概念
STL(Standard Template Library)标准模板库,最早是惠普实验室开发的一系列软件的统称,现在主要出现在C++中,但是在引入C++之前该技术已经存在很长的时间了。
STL从广义上分为:容器(container),算法(alogrithm)和迭代器(iterator)。容器和算法之间通过迭代器进行无缝连接。STL几乎所有的代码都采用了模板类或者模板函数,这相比传统的由函数和类组成的库来说提供了更好的代码重用机会。
在C++标准库当中,隶属于STL的占到了80%以上。在C++标准库中,STL被组织成以下13个头文件:
<algorithm>
<deque>
<functional>
<iterator>
<vector>
<list>
<map>
<memory>
<numeric>
<queue>
<set>
<stack>
<utility>
STL的优点有哪些?
(1)STL是C++的一部分,因此不用额外安装什么,它被内建在你的编译器之内。
(2)STL的一个重要的特点是数据结构和算法的分离。尽管这是个简单的概念,但是这种分离确实使得STL变得非常通用。例如在STL的vector容器中,可以放入元素,基础数据类型变量,元素的地址;STL的sort()排序函数可以用来操作vector, list等容器。
(3)程序员可以不用思考STL具体的实现过程,只要能够熟练使用STL就OK了。这样他们就可以把精力放在程序开发的别的方面。
(4)STL具有高可重用性,高性能,高移植性,跨平台的优点
- 高可重用性:STL中几乎所有的代码都采用了模板类和模板函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用的机会。
- 高性能:如map可以高效地从十万条记录里查找出指定的记录,因为map是采用红黑树的变体实现的。(红黑树是平衡二叉树的一种)
- 高移植性:在项目A上用STL编写的模块,可以直接移植到项目B上
- 跨平台:windows的visual studio上编写的代码可以在Mac OS的XCode上直接编译
C++中的容器是指用来存放数据的类模板。容器分为两种,分别是序列式容器和关联式容器。它们的区别如下:
序列式容器:容器的元素的位置是由进入容器时机和地点来决定的
关联式容器:容器有其自身的规则,进入容器元素的位置不是由进入的时机和地点决定的
迭代器可以理解为指针,对指针的操作基本都可以对迭代器操作。实际上,迭代器是一个类模板,这个类模板封装了一个指针。迭代器一般用来遍历容器中的元素。
算法可以理解为解决问题有限步骤。STL提供了大约100个实现算法的模板函数,比如算法for_each将为指定序列中的每个元素调用指定的函数等。这样一来,只要我们熟悉了STL之后,许多代码可以被大大的化简,只需要通过调用一两个算法函数,就可以完成所需要的功能并大大地提升效率。
从下面这个小例子可以窥见容器、迭代器和算法三者之间的关系。
// 算法,负责统计某个元素的个数
int MyCount(int *begin, int *end, int val)
{
int cnt = 0;
while (begin != end)
{
if (*begin == val)
++cnt;
++begin;
}
return cnt;
}
int main()
{
// 数组,容器
int arr[] = { 0, 7, 5, 4, 9, 2, 0 };
// 迭代器,开始和结束指针
int *pbegin = arr;
int *pend = &(arr[sizeof(arr) / sizeof(int)]);
int num = MyCount(pbegin, pend, 0);
cout << "num = " << num << endl;
getchar();
return 0;
}
下面的案例揭示了STL的基本使用语法。
void PrintVector(int v)
{
cout << v << endl;
}
// STL基本语法
void Test()
{
// 实例化一个包含int类型元素的容器
vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
// STL提供的for_each算法
// 容器提供迭代器
vector<int>::iterator pBegin = v.begin();
vector<int>::iterator pEnd = v.end();
for_each(v.begin(), v.end(), PrintVector);
}
下面的案例演示了容器包含自定义类型的应用案例。
class Person
{
public:
Person() = default;
Person(int age, int id) : age(age), id(id) {}
void Show()
{
cout << "(age, id) = (" << age << ", " << id << ")" << endl;
}
private:
int age;
int id;
};
void Test2()
{
vector<Person> v;
v.push_back(Person(10, 20));
v.push_back(Person(30, 40));
v.push_back(Person(50, 60));
for (vector<Person>::iterator it = v.begin(); it != v.end(); ++it)
{
it->Show();
}
}
二 string容器
string的特性
说到string的特性,就不得不和char *
类型的字符串进行对比:
(1)char *
是一个指针,而string是一个类。string封装了char *
,管理这个字符串,是一个char *
型的容器。
(2)string封装了很多实用的成员方法。查找find,拷贝copy,删除delete,替换replace和插入insert等
(3)string不用考虑内存释放和越界问题。string管理char *
所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。
string转成char *
,实用成员方法c_str()
。char *
转string,直接将char *
传入string的构造方法中即可生成相应的string对象。
下面是string的初始化,赋值和取值操作
// string的初始化
void Test1()
{
string s1; // 无参构造,为空字符串
string s2(10, 'a');
string s3("hello");
string s4(s3); // 拷贝构造
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
// string的赋值操作
void Test2()
{
string s1;
string s2("app");
s1 = s2; // 重载等号运算符
cout << s1 << endl;
s1 = 'a';
cout << s1 << endl;
s1.assign("jkl"); // 赋值的成员方法assign
cout << s1 << endl;
}
// 取值操作
void Test3()
{
string s1 = "abcdefg";
// 重载[]操作符
for (int i = 0; i < s1.size(); ++i)
{
cout << s1[i] << endl;
}
// 成员方法at()
for (int i = 0; i < s1.size(); ++i)
{
cout << s1.at(i) << endl;
}
// 两者区别
// []方式,如果访问越界,程序直接崩溃
// at()方式,如果访问越界,程序抛出out_of_range异常
try
{
// 直接奔溃
// cout << s1[100] << endl;
// 抛出异常
cout << s1.at(100) << endl;
}
catch (...) // 捕获所有异常
{
cerr << "越界了!" << endl;
}
}
string的拼接操作
// 重载+=操作符
string &operator+=(const string &str);
string &operator+=(const char *str);
string &operator+=(const char c);
// append()方法
// 把字符串s连接到当前字符串尾部
string &append(const char *s);
// 把字符串s的前n个字符连接到当前字符串结尾
string &append(const char *s, int n);
// 同operator+=()
string &append(const string &str);
// 把字符串s中从pos开始的n个字符串连接到当前字符串结尾
string &append(const string &s, int pos, int n);
// 在当前字符串结尾添加n个字符c
string &append(int n, char c);
下面是字符串拼接的应用案例。
// string的拼接
void Test4()
{
string s1 = "abcd";
string s2 = "1111";
s1 += "abcd";
s1 += s2;
cout << s1 << endl;
string s3 = "2222";
s2.append(s3);
cout << s2 << endl;
string s4 = s2 + s3;
cout << s4 << endl;
}
string查找与替换
// 查找str第一次出现的位置,从Pos位置开始查找
int find(const string &str, int pos = 0) const;
// 查找s第一次出现的位置,从pos位置开始查找
int find(const char *s, int pos = 0) const;
// 从pos位置查找s的前n个字符第一次位置
int find(const char *s, int pos, int n) const;
// 查找字符c第一次出现的位置
int find(const char c, int pos = 0) const;
// 从pos位置开始查找str最后一次出现的位置
int rfind(const string &str, int pos = npos) const;
// 从pos位置开始查找s最后一次出现的位置
int rfind(const char *s, int pos = npos) const;
// 从pos查找s的前n个字符最后一次位置
int rfind(const char *s, int pos, int n) const;
// 查找字符c最后一次出现的位置
int rfind(const char c, int pos = 0) const;
// 替换从pos开始n个字符为字符串str
string &replace(int pos, int n, const string &str);
// 替换从pos开始的n个字符为字符串s
string &replace(int pos, int n, const char *s);
下面是字符串查找和替换的应用案例。
// 查找操作
void Test5()
{
string s1 = "abcdefghifgjklmn";
// 查找第一次出现的位置
int pos = s1.find("fg");
cout << "pos = " << pos << endl;
// 查找最后一次出现的位置
int rpos = s1.rfind("fg");
cout << "rpos = " << rpos << endl;
}
// string的替换
void Test6()
{
string s1 = "abcdefg";
s1.replace(0, 2, "111111");
cout << s1 << endl;
}
string比较
类似strcmp()
,string提供了成员方法compare()用于进行string的比较
int compare(const string &s) const;
int compare(const char *s) const;
string子串
// 返回由pos开始的n个字符串组成的字符串
string substr(int pos = 0, int n = npos) const;
string插入和删除
// 插入字符串
string &insert(int pos, const char *s);
string &insert(int pos, const string &str);
// 在指定位置插入n个字符c
string &insert(int pos, int n, char c);
// 删除从pos开始的n个字符
string &erase(int pos, int n = npos);
下面是string插入和删除的应用案例。
// string插入和删除
void Test7()
{
string s = "abcdefg";
s.insert(3, "111");
cout << s << endl;
s.erase(3, 3);
cout << s << endl;
}
三 vector容器
3.1 vector动态增长原理
当插入新元素的时候,如果空间不足,那么vector会重新申请更大的一块内存空间,将原空间数据拷贝到新的空间,释放旧空间。再把新元素插入到新申请的空间。
3.2 vector构造函数
// 采用模板实现类实现,默认构造函数
vector<T> v;
// 将v[begin(), end())区间中的元素作为初值进行初始化
vector(v.begin(), v.end());
// 将n个elem元素作为初值进行初始化
vector(n, elem);
// 拷贝构造函数
vector(const vector &vec);
下面是vector初始化的应用案例
// 初始化
void Test1()
{
// 默认构造
vector<int> v1;
int arr[] = { 10, 20, 30, 40, 50 };
vector<int> v2(arr, arr + sizeof(arr) / sizeof(int));
vector<int> v3(v2.begin(), v2.end());
vector<int> v4(v3);
printVector(v2);
printVector(v3);
printVector(v4);
}
3.3 vector常用赋值操作
assign(beg, end);
assign(n, elem);
// 重载等号运算符
vector &operator=(const vector &vec);
// 将vec与本身的元素互换
swap(vec);
下面是赋值操作的应用案例
// 常用赋值操作
void Test2()
{
int arr[] = { 10, 20, 30, 40, 50 };
vector<int> v1(arr, arr + sizeof(arr) / sizeof(int));
// 成员方法进行赋值
vector<int> v2;
v2.assign(v1.begin(), v1.end());
// 重载=号赋值
vector<int> v3;
v3 = v2;
// 将v1和v4的数据进行交换
vector<int> v4;
v4.push_back(100);
v4.push_back(200);
v4.push_back(300);
v4.swap(v1);
printVector(v1);
printVector(v4);
}
3.4 vector大小操作
// 返回容器中元素的个数
size()
// 判断容器是否为空
empty();
// 重新制定容器长度为num,如果容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素将被删除
resize(int num);
// 容器的容量
capacity();
// 容器预留len个元素长度,预留位置不初始化,元素不可访问
reserve(int len);
// 常用大小操作
void Test3()
{
int arr[] = { 100, 200, 300, 400 };
vector<int> v4(arr, arr + sizeof(arr) / sizeof(int));
cout << "v4.size() = " << v4.size() << endl;
if (v4.empty())
{
cout << "v4为空!" << endl;
}
printVector(v4);
v4.resize(2);
printVector(v4);
// 设定填充值为1
v4.resize(6, 1);
printVector(v4);
cout << "v4的容量:" << v4.capacity() << endl;
}
3.5 vector数据存取操作和插入删除
// 返回索引idx所指的数据,如果idx越界,则抛出out_of_range异常
at(int idx);
// 返回索引idx所指的数据,越界时,运行直接报错
operator[]
// 返回容器中的第一个数据元素
front();
// 返回容器中的最后一个数据元素
back();
// 往迭代器指向位置pos插入count个元素ele
insert(const_iterator pos, int count, T ele)
// 尾部插入元素
push_back();
// 删除最后一个元素
pop_back();
// 删除迭代器从start到end之间的元素
erase(const_iterator start, const_iterator end);
// 删除迭代器指向的元素
erase(const_iterator pos);
// 清空容器中的所有元素
clear();
下面是vector的插入和删除的应用案例
// 插入和删除
void Test4()
{
vector<int> v;
v.push_back(10);
v.push_back(20);
// 头插法
v.insert(v.begin(), 30);
v.insert(v.begin(), 40);
v.insert(v.end(), 50);
// vector支持随机访问,在位置v.begin()+2处插入60
v.insert(v.begin() + 2, 60);
printVector(v);
v.erase(v.begin());
printVector(v);
v.pop_back();
printVector(v);
v.clear();
cout << "v.size() = " << v.size() << endl;
}
3.6 使用swap()收缩空间
// swap()收缩空间
void Test5()
{
vector<int> v;
for (int i = 0; i < 100000; ++i)
v.push_back(i);
cout << "v.size() = " << v.size() << endl;
cout << "v.capacity() = " << v.capacity() << endl;
// swap()收缩空间,匿名对象随后会被释放空间
vector<int>(v).swap(v);
cout << "v.size() = " << v.size() << endl;
cout << "v.capacity() = " << v.capacity() << endl;
}
3.7 使用reserve()预留空间提高程序效率
reserve和resize有啥区别?
reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新对象之间,不能引用容器内的元素。resize是改变容器的大小,且在创建对象,因此,调用这个函数之后,就可以引用容器内的对象了。
// reserve预留空间
void Test6()
{
int num = 0;
int *addr = nullptr;
vector<int> v;
for (int i = 0; i < 100000; ++i)
{
v.push_back(i);
if (addr != &(v[0]))
{
++num;
addr = &(v[0]);
}
}
cout << "申请了" << num << "次内存!" << endl;
// 如果你知道容器大概要存储多少内存空间,你可以使用reserve()预留空间
// 减少内存申请的次数以提高程序运行效率
num = 0;
addr = nullptr;
vector<int> u;
u.reserve(100000);
for (int i = 0; i < 100000; ++i)
{
u.push_back(i);
if (addr != &(u[0]))
{
++num;
addr = &(u[0]);
}
}
cout << "申请了" << num << "次内存!" << endl;
}