string类
1. 为什么学习string类?
1.1 C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,
但是这些库函数与字符串是分离开的,不太符合面向对象编程思想(OOP),而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
2. 标准库中的string类
2.1 string类的常用接口说明
在使用string类时,必须包含#include头文件以及using namespace std;
2.1.1 string类对象的常见构造
(constructor)函数 | 功能说明 |
---|---|
string() | 构造空的string类对象(即空字符串) |
string(const char* s) (重点) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) (重点) | 拷贝构造函数 |
void TestString1()
{
string s1;
cin >> s1;
cout << "s1:"<<s1 << endl;
string s2("123456");
cout << "s2:" << s2 << endl;
string s3(s2);
cout << "s3:" << s3 << endl;
string s4("123456", 5);//赋前五个字符
cout << "s4:" << s4 << endl;
string s5(10, 's');//赋10个字符's';
cout << "s5:" << s5 << endl;
}
2.1.2 string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
clear | 清空有效字符 |
reserve (重点) | 为字符串预留空间 |
resize(重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
void TestString2() // 容量相关
{
string s("hello");
cout << s.size() << endl; // 一般用size比较多一些,查看有效元素个数
cout << s.length() << endl;
cout << s.capacity() << endl; //看容量
// s如果是空字符串 "" 返回true 否则返回false
if (!s.empty())
{
cout << s << endl;
}
s.clear();
if (s.empty())
{
cout << "空字符串" << endl;
}
}
// reserve: 扩容,不会改变有效元素的个数
// reserve(size_t newcapacity)
// 假设当前扩容的string对象底层旧容量为 oldcapacity
// newcapacity > oldcapacity:reserve方法才会真正扩容(vs下:1.5倍扩容)
// newcapacity < oldcapacity
// newcapacity >= 16: reserve不会将空间缩小的
// newcapacity <= 15: reserve才会将空间缩小--15
void TestString3()
{
string s("hello");
//是 reserve 不是 reverse
s.reserve(20); // 31
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(30);
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(40); // 47
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(50); // 70
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(60); // 70
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(50); // 70
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(40); // 70
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(30); // 70
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(20); // 70
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(15); // 15
cout << s.size() << endl;
cout << s.capacity() << endl;
}
void TestString4()
{
/*
char*
size_t size
size_t capacity
总共占用12字节
*/
cout << sizeof(string) << endl; // 28字节的大小
}
//
// void resize(size_t newsize, char ch) //增加
// void resize(size_t newsize) //减少
// newsize: 将string对象中有效字符个数修改到newsize个
// ch: 如果是增多,多出的元素使用ch填充
// 将string对象中有效元素个数增加到newsize,多出的位置使用ch填充
// 注意:在增多的过程中可能会扩容
// 在减少的过程中容量不变
void TestString5()
{
string s("hello"); // size:5 capacity:15
s.resize(10, '!'); // size:10 capacity:15
s.resize(20, '$'); // size:20 capacity:31
s.resize(30, '%'); // size:30 capacity:31
s.resize(20, '8');
s.resize(8);
s.resize(3);
}
注意:
1.size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()
- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字
符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的
元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大
小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于
string的底层空间总大小时,reserve不会改变容量大小。
(以我的vs2019为例,是PJ版本的STL的string,内部维护了一个固定大小的数组大小是16个字节)
2.1.3 string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[] (重点) | 返回pos位置的字符,const string类对象调用 |
at | 与operator[]类似,越界时警告与其不同 |
begin+ end | begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | rend获取第一个字符的迭代器 +rbegin获取最后一个字符下一个位置的迭代器 |
#include <assert.h>
void TestString7()
{
string s("hello");
cout << s[0] << endl;
s[0] = 'H';
cout << s << endl;
cout << s.at(0) << endl;
s.at(0) = 'h';
cout << s << endl;
// cout << s[100] << endl; // []越界:assert
cout << s.at(100) << endl; // at越界:exception: std::out_of_range
}
2.1.4 string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串str |
c_str(重点) | 返回C格式字符串 |
find (重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
npos | 是string类内部维护的一个静态成员变量 |
void TestString6()
{
/*
1. 可以知道string内部的扩容机制
2. 如果大概知道要往string中放多少个元素,在push_back之前
可以提前先将容量给好,否则一边插入一边扩容效率非常低
*/
string s;
// s.reserve(100);
size_t cap = s.capacity();
//void push_back(char c);
for (size_t i = 0; i < 100; ++i)
{
s.push_back('A');
if (cap != s.capacity())
{
cap = s.capacity();
cout << cap << endl;
}
}
}
void TestString8()
{
string s("hello");
s += ' ';
s += "world";
string ss("!!!");
s += ss;
s.append(10, 'A');
s.clear();
s.append("abcd");
s.append("1234",2);
}
void TestString13()
{
string s("hello world");
string s1(s);
s.c_str();
if (s.c_str() == s1.c_str())
{
cout << s.c_str()<< endl;
cout << s1.c_str() << endl;
}
printf("%p\n", s.c_str());
printf("%p\n", s1.c_str());
}
void TestString14()
{
int array[] = { 1, 2, 3 };
string s("hello");
// 借助下标+[]
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i]; // char& operator[](size_t index)
}
cout << endl;
// 范围for
for (auto e : s)
cout << e;
cout << endl;
// 使用正向迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it;
++it;
}
cout << endl;
// 使用反向迭代器
// string::reverse_iterator rit = s.rbegin();
auto rit = s.rbegin();
// auto a = 10; int a = 10;
while (rit != s.rend())
{
cout << *rit;
++rit;
}
cout << endl;
reverse(s.begin(), s.end());
}
// 获取一个文件的后缀
void TestString11()
{
string filename("1.t22111.txt");
cout << filename.substr(filename.rfind('.') + 1) << endl;
// string substr (size_t pos = 0, size_t len = npos) const;
}
2.1.5 string类非成员函数
函数 | 功能说明 |
---|---|
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
// 一行单词,单词和单词之间使用空格隔开,最后一个单词的长度
void TestString12()
{
string words;
// cin >> words;
while (getline(cin, words))
{
//cout << words.substr(words.rfind(" ") + 1).size() << endl;
cout << words.substr(words.rfind(' ') + 1).size() << endl;
}
}
3. string类的模拟实现
3.1 经典的string类问题
模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
以下string类的实现是否有问题?
class string
{
public:
string(const char* str = "")
{
// 构造string类对象时,如果传递nullptr指针,认为程序非法
if(nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~string()
{
if(_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void Teststring()
{
string s1("hello world");
string s2(s1);
}
说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
3.2 浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来(按照字节方式进行拷贝)。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。要解决浅拷贝问题,C++中引入了深拷贝。
//浅拷贝
string(const string& s)
: _str(s._str)
{}
string& operator=(const string& s)
{
_str = s._str;
return *this;
}
3.3 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
3.3.1 传统版写法的string类
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
string& operator=(const string& s)
{
if (this != &s)
{
//先释放掉被赋值对象的空间,以免造成内存泄漏
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
3.3.2 现代版写法的string类
//高级写法
// 深拷贝
string(const string& s)
: _str(nullptr) //此时最好赋值为空,
//不然_str就是系统默认值0xcccc cccc,文章后面有解释
{
string strTemp(s._str);
swap(_str, strTemp._str);
}
// 深拷贝
// s2 = s1; s1--->s--->strtemp
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
拷贝构造函数图解:
1.先通过构造函数在函数调用栈上,创建一个临时空间 strTemp,此时这个临时空间的成员 _str 指向构造函数给其新开辟的地址空间 0x22334455,里面的内容是"hello"。
2.交换 s2._str 和 strTemp._str 的值。此时s2._str指向0x22334455。 strTemp._str 指向nullptr
3.出了拷贝构造函数作用域,销毁临时对象(黄色部分)
----------------------------------------------------------------------------------------------------
注意:初始化列表处给 _str 最好赋一个空。不然编译器有可能会给_str一个默认值0xcccc cccc ,出了拷贝构造函数作用域,销毁临时对象时,临时对象的 _str不为空(此时_str相当于是一个野指针),在析构函数中 delete[] 释放资源,报错。
赋值运算符现代写法原理和上面类似,就不再赘述了
3.4 写时拷贝
写时拷贝是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。