一、string介绍
C语言中,字符串是以 ‘\0’ 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C++封装了这些接口和操作,创建出string类:
- string 是表示字符串的类
- 该类的接口与常规容器的接口基本相同,还添加了一些专门用来操作 string 的常规操作。
- 不能操作多字节或者变长字符的序列
为什么要将vector和string分开实现?
- 字符串默认最后放 ‘\0’,而vector存储数据时不需要
- string 里封装了C语言中字符串的操作接口,这些接口需要 ‘\0’ 作为尾部
二、string常用接口
#include <iostream>
#include <string>
using namespace std;
int main()
{
// 构造
string str; // 无参
string str1("qwer"); // qwer
string str2(5, 'a'); // aaaaa,5个a
string str3(str1); // 拷贝构造
// 容量
int cap = str.capacity();
cout << cap << endl;
for (int i = 0; i < 100; i++) // 为了提高效率 string 内部管理了一个固定大小的数组,16字节的容量,小于15字节不需要扩容
{
str.push_back('a');
if (cap != str.capacity())
{
cap = str.capacity();
cout << cap << endl;
}
}
cout << endl;
str.reserve(50); // 底层容量和有效元素都不变
cout << str.capacity() << endl;
str.reserve(15); // 底层容量和有效元素都不变
cout << str.capacity() << endl;
// 有效元素
str1.resize(50, 'a'); // 有效元素变成50个,容量63
str1.resize(4); // 有效元素变成4个,容量不变
// 迭代器和遍历方式
for (size_t i = 0; i < str1.size(); i++)
cout << str1[i];
cout << endl;
string::iterator it = str1.begin();
while (it != str1.end())
{
cout << *it;
++it;
}
cout << endl;
for (auto ch : str1)
cout << ch;
cout << endl;
// 修改操作
str1.push_back(' ');
str1.append("hello");
str1 += 'a';
str1 += "it";
// 查找操作
size_t pos1 = str1.find("it");
size_t pos2 = str1.rfind("lo");
return 0;
}
小结:
- size() 和 length() 方法底层实现原理完全相同,引入size()是为了和其他容器接口保持一致
- clear() 只是将有效元素清空,不改变底层容量
- resize() 进行增多有效元素个数时,可能会增大容量。但减少有效元素个数时,不会改变底层容量
- reserve() 为 string 预留空间,不改变有效元素的个数,当 reserve() 参数小于底层空间时,不会改变容量大小
- 扩容时以成 1.5 倍代替固定步长可以保证常数的时间复杂度,而固定步长却只能达到 O(n) 的时间复杂度
- vs 以1.5 倍扩容,Linux 以 2 倍扩容。如果以两倍以上的方式进行扩容,则新申请的空间会大于之前分配内存的总和,导致原始分配的内存不能被使用。
三、模拟实现string类
- 标准版写法
- 构造:参数列表应该缺省,并且要判断是否为空,是则生成空字符串,strcpy() 会将 ‘\0’ 也拷贝进去
- 析构:判空
- 拷贝构造:杜绝浅拷贝,在初始化时先开辟空间
- 赋值重载:判断是否给自己赋值,杜绝浅拷贝,先释放原空间,再开辟新空间大小,进行拷贝
#include <iostream>
namespace traditional
{
class string
{
public:
string(const char* str = "")
{
if (str == nullptr)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
string(const string& s)
:_str(new char[s.size() + 1])
{
strcpy(_str, s.c_str());
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
string& operator=(const string& s)
{
if (this != &s)
{
// 1.释放旧空间
delete[] _str;
// 2.拷贝新空间
_str = new char[s.size() + 1];
strcpy(_str, s.c_str());
}
return *this;
}
int size()const
{
return strlen(_str);
}
char* c_str()const
{
return _str;
}
private:
char* _str;
};
}
- 现代版写法:注意每个函数里都有开辟、拷贝,代码复用性太低,因此可以用swap()函数进行改进,交换指针域
- 构造:常规操作
- 析构:常规操作
- 拷贝构造:初始化时对 _str 赋空,构造一个临时对象,交换临时对象和 this 的 _str 指针域
- 赋值重载:传参时拷贝构造一份临时对象,交换临时对象和 this 的 _str 指针域
注意:string 类作为函数返回值时,临时拷贝构造的对象的 _str 不为 nullptr,而是随机值。因此必须在拷贝构造初始化时赋空
namespace modern
{
class string
{
public:
string(const char* str = "")
{
if (str == nullptr)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
string(const string& s)
:_str(nullptr)
{
string strTemp(s.c_str());
std::swap(_str, strTemp._str);
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
string& operator=(string s)
{
std::swap(_str, s._str);
return *this;
}
int size()const
{
return strlen(_str);
}
char* c_str()const
{
return _str;
}
private:
char* _str;
};
}
四、深浅拷贝与写时拷贝技术
- 浅拷贝:多个对象共用一份资源,当一个对象销毁时该资源会被销毁,而另一些对象不知道已经被释放,当继续操作时就会发生越界访问。
- 拷贝构造、赋值重载没有显示定义出来会造成浅拷贝,并且可能造成内存泄漏或者程序崩溃
- 将一个对象中的内容原封不动的拷贝到另一个对象中,用了同一块物理内存,在释放时会产生6号信号 SIGABRT 而崩溃,double free
- 深拷贝:内存地址不同,但内容相同
- 写时拷贝:多个对象共用同一份内存资源,当一个对象的内容即将发生变化时,给这个对象重新分配内存并拷贝原空间内容。如果不进行修改,则由引用计数进行计算什么时候进行释放资源。
- 引用计数:用来记录资源使用者的个数,在构造时将资源计数器+1,当一个对象销毁时将资源计数器-1,当计数器值为0时将该资源释放。