前言
为什么学习 string 类呢?
C语言中是没有字符串类型的,字符串是以 ‘\0’ 结尾的一些字符的集合(即字符数组),为了操作方便,C 标准库 <string.h> 中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合面向对象 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C++ STL string 是对字符串进行管理的类。实际上就是一个管理字符数组的顺序表。
在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 语言库中的字符串操作函数。
一、STL - string 的介绍
文档介绍:string - C++ Reference (cplusplus.com)
- 字符串是表示字符序列的对象
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string 类是使用 char (即作为它的字符类型,使用它的默认 char_traits 和分配器类型(关于模板的更多信息,请参阅 basic_string)。
- string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits 和 allocator 作为 basic_string 的默认参数(关于更多的模板信息请参考 basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
- string 是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
- string 在底层实际是 basic_string 模板类的别名
typedef basic_string<char> string;
- 不能操作多字节或者变长字符的序列。
补充:
-
编码:计算机中只存储二进制数(0 / 1),那如何去表示文字呢,需要制定对应的编码表,规定用哪些二进制数字表示(映射)哪个符号,当然每个人都可以约定自己的一套,而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,比如美国有关的标准化组织就出台了 ASCII 码表(基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言) -
ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符,到目前为止共定义了 128 个字符。 -
所以早期的计算机中只能表示英文,不能表示其它国家的文字,当全世界各个国家都开始使用计算机了,就需要建立出对应语言的编码表。 -
UTF - 8 是对不同范围的字符使用不同长度的编码,这样能够适应不同的语言,比如有些语言单字节编码就够了,有些语言需要多字节编码才够。
-
还有其它编码表,GBK码(对多达2万多的简繁汉字进行了编码,简体版的Win95和Win98都是使用GBK作系统内码)等等。
注意:使用 string 类需要包含头文件
二、string 的使用(常用接口介绍)
string 类的成员函数(接口)非常的多,我们学习一些常用的就行了,其它不常用的,需要时去查文档就好了。
2.1 常见构造
构造函数:
string();
string (const string& str);
string (const char* s);
string (const char* s, size_t n);
string (size_t n, char c);
template <class InputIterator>
string (InputIterator first, InputIterator last);
👉Example:
#include <iostream>
#include <string>
int main()
{
std::string s0 ("Initial string");
std::string s1;
std::string s2 (s0);
std::string s4 ("A character sequence");
std::string s5 ("Another character sequence", 12);
std::string s6 (10, 'x');
std::string s7 (s0.begin(), s0.begin() + 7);
return 0;
}
👉思考:空串是什么都没有吗,存储空间为空吗?
2.2 容量操作
函数名称 | 功能说明 |
---|
size? | 返回字符串有效字符长度(为了统一设计,所有容器都是用 size 表示有效元素个数) | length | 返回字符串有效字符长度(这是早期提供的接口) | resize? | 将字符串大小调整为 n 个有效字符的长度 | capacity | 返回有效字符的最大容量(即已分配 size 的大小) | reserve? | 更改容量(capacity)的大小 | clear? | 清空字符串的内容,变为空字符串(size 变为 0,不改变 capacity 的大小) | empty? | 检测字符串是否为空串,是返回 true,否则返回 false |
👉Example1:
resize 函数的两种重载形式:
void resize (size_t n);
void resize (size_t n, char c);
void Test1()
{
string s("hello");
s.resize(10, 'a');
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
s.resize(20);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
s.resize(2);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
return 0;
}
👉Example2:
reserve 函数介绍:
void reserve (size_t n = 0);
void Test2()
{
string s("hellohellohello");
s.reserve(20);
cout << s.size() << endl;
cout << s.capacity() << endl;
s.reserve(10);
cout << s.size() << endl;
cout << s.capacity() << endl;
}
测试:reserve 是如何进行增容呢?
void Test3()
{
string s;
cout << "initial value: " << s.capacity() << endl;
size_t sz = s.capacity();
for (size_t i = 0; i < 500; i++)
{
s.push_back('a');
if (s.capacity() != sz)
{
sz = s.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
经过测试,VS2019 下大概是 1.5 倍增容:
Linux g++下是 2 倍增容:
思考:resize 和 reserve 的意义在哪里呢?
reserve 的作用:如果知道需要多大的空间,可以利用 reserve 提前一次性把空间开好,避免增容带来的开销。
resize 的作用:既要开好空间,还要对这些空间初始化,就可以使用 resize
2.3 访问操作
?有了这个运算符重载,我们就可以像使用数组一样去使用 string 类对象。
函数名称 | 功能说明 |
---|
operator[]? | 返回对字符串中 pos 位置处的字符的引用(string 类对象支持随机访问)(一般物理地址是连续的才支持) |
operator[] 函数的两种重载形式:
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
operator[] 函数会检查越界(pos 必须小于 size)
2.4 迭代器及遍历操作
?所有容器都有迭代器,迭代器提供了用统一类似的方式去访问容器。
函数名称 | 功能说明 |
---|
begin(iterator) | 返回指向第一个有效字符的迭代器 | end | 返回指向字符串末尾字符的迭代器(即最后一个有效字符的下一个位置) | rbegin(reverse_iterator) | 反向迭代器(可以反向遍历对象) | rend | 反向迭代器 | 范围 for | C++11支持更简洁的范围 for 的新遍历方式(底层其实是被替换成迭代器,所以支持迭代器就支持范围 for) |
迭代器有两个版本:普通迭代器和 const 迭代器
iterator begin();
const_iterator begin() const;
👉Example:
void test(const std::string& s) {
std::string::const_iterator it = s.begin();
while (it != s.end()) {
std::cout << *it;
it++;
}
}
int main()
{
std::string s1;
std::string s2("hello");
for (size_t i = 0; i < s.size(); ++i)
cout << s2[i] << endl;
std::string::iterator it = s2.begin();
while (it != s2.end()) {
std::cout << *it;
it++;
}
for (std::string::reverse_iterator rit = s2.rbegin(); rit != s2.rend(); ++rit)
std::cout << *rit;
for (auto rit = s2.rbegin(); rit != s2.rend(); ++rit)
std::cout << *rit;
for (auto& e : s2) {
std::cout << e;
}
return 0;
}
2.5 修改操作
尽量不要用 insert 和 erase 函数,因为要挪动字符,时间效率太低。
函数名称 | 功能说明 |
---|
operator+=?? | 在当前字符串末尾追加字符串(追加 string / char* / char 类型的都可以) | append | 在当前字符串末尾追加字符串 | push_back | 将一个字符附加到字符串的末尾(尾插)(void push_back (char c); ) | swap | 交换两个字符串的内容(注意:还存在一个具有相同名称的非成员函数 swap) | c_str? | 返回指向 C 格式字符串的数组的指针(const char* c_str() const; ) | find? | 从 pos 位置开始往后找字符,返回该字符在字符串中的位置,如果未找到返回 npos | npos? | 作为返回值,通常用于表示不匹配(npos是一个静态成员变量 static const size_t npos = -1; ) | rfind | 从 pos 位置开始往前找字符,返回该字符在字符串中的位置,如果未找到返回 npos | substr? | 在字符串中从 pos 位置开始,截取 len 个字符,然后将其作为新的 string 类对象返回 |
👉Example1:
void Test4()
{
string s1("hello");
string s2("world");
s1 += ' ';
s1 += s2;
s1 += "!!!";
}
👉Example2:
find 函数介绍:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
size_t find (const char* s, size_t pos, size_t n) const;
size_t find (char c, size_t pos = 0) const;
substr 函数介绍:
string substr (size_t pos = 0, size_t len = npos) const;
void Test5()
{
string file1("test.txt");
size_t pos = file1.find(".");
if (pos != string::npos)
{
cout << file1.substr(pos) << endl;
}
string file2("test.txt.zip");
size_t rpos = file2.rfind(".");
if (rpos != string::npos)
{
cout << file2.substr(rpos) << endl;
}
}
👉Example3:
void Test6()
{
string url("http://www.cplusplus.com/reference/");
size_t pos1 = url.find("://");
if (pos1 != string::npos)
{
cout << url.substr(0, pos1 - 0) << endl;
}
size_t pos2 = pos1 + 3;
size_t pos3 = url.find('/', pos2);
if (pos3 != string::npos)
{
cout << url.substr(pos2, pos3 - pos2) << endl;
}
cout << url.substr(pos3) << endl;
}
👉Example4:
void Test7()
{
string s("hello");
printf("%s\n", s.c_str());
}
2.6 string 类的非成员函数重载
getline 函数介绍:
string s;
getline(cin, s);
2.7 补充一些接口
C 语言库文件 <ctype.h> 中的处理 C 字符的接口
-
字符处理函数:
函数名称 | 功能说明 |
---|
int isalpha(int c) | 检查字符是否为字母,是返回非零(true),不是则返回0(false) | int isdigit(int c) | 检查字符是否为十进制数字,是返回非零(true),不是则返回0(false) |
-
字符转换函数:
函数名称 | 功能说明 |
---|
int tolower(int c) | 把字母转换成小写字母,返回转换之后的字符 | int toupper(int c) | 把字母转换成大写字母,返回转换之后的字符 |
头文件 中:
头文件 中:
-
函数 std::reverse:反转范围 [first,last) 中元素的顺序。
template <class BidirectionalIterator>
void reverse (BidirectionalIterator first, BidirectionalIterator last);
-
函数 std::sort:将 [first,last) 范围内的元素按升序排序。
template <class RandomAccessIterator, class Compare>
void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
👉Example:
int arr[] = { 1, 5, 4, 2, 3 };
sort(arr, arr + 5);
三、string 类的模拟实现
string 类的模拟实现最主要是实现 string 类的构造、拷贝构造、赋值运算符重载以及析构函数。
3.1 深浅拷贝(?重要)
① 浅拷贝 & 深拷贝
需要「深拷贝」的类,其内部往往是很复杂的,是需要用户显式定义拷贝构造函数来完成「深拷贝」的。
👉请看下面这个浅拷贝的例子:
namespace winter
{
class string
{
private:
char* _str;
public:
string(const char* s)
:_str(new char[strlen(s) + 1])
{
strcpy(_str, s);
}
~string()
{
delete[] _str;
_str = nullptr;
}
};
void test()
{
string s1("hello");
string s2(s1);
}
}
这里必须是「深拷贝」,编译器默认生成的拷贝构造函数是「浅拷贝」,会导致两个 string 对象中的字符指针 _str 指向的是同一个字符数组。(因为浅拷贝只拷贝了 _str 数组指针的 4 个字节的内容)
如图:指向了同一块空间
那么会引发什么问题呢?当 test() 函数结束时,会先调用析构函数清理 s2,此时 _str 指向空间已经还给操作系统了,然后再调用析构函数清理 s1,导致 _str 指向的空间被释放两次,引发程序崩溃。
所以在上述类中必须要显式定义拷贝构造函数,否则编译器默认生成的拷贝构造函数无法正常完成拷贝。
总结:上述 string 类没有显式定义其拷贝构造函数与赋值运算符重载函数,此时编译器会默认生成一个,当用 s1 构造 s2 时,编译器会调用默认生成的拷贝构造函数。最终导致:s1 和 s2 共用同一块内存空间,在调用析构函数清理对象资源时,同一块空间被释放多次,引起程序崩溃,这种拷贝方式是浅拷贝。
浅拷贝:也称位拷贝,编译器只是将对象中的数据「按字节序」拷贝过来。如果对象中管理的有其它资源(比如堆上的资源),那就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而另一些对象不知道该资源已经被释放,以为还有效,就继续对资源进行操作(比如增删查改),此时就会发生违规访问。
浅拷贝引发的问题:
- 同一块空间会被析构多次
- 一个对象修改会影响另外一个对象
为了解决浅拷贝问题,C++ 中引入了「深拷贝」。
如果一个类中涉及到资源的管理,其拷贝构造、赋值运算符重载以及析构函数必须要显式给出,一般情况都是按照深拷贝方式提供。
深拷贝:给每个对象独立分配资源,保证多个对象之间不会因为共享资源问题而造成多次释放资源,导致程序崩溃。
② 拷贝构造 & 赋值运算符重载(传统写法)
👉👉拷贝构造函数的深拷贝(传统写法):
string(const string& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
string s2(s1); 调用显式定义的拷贝构造函数,实现深拷贝:
如图:string 类对象 s1 和 s2 中的 _str 指向不同空间:
👉👉赋值运算符重载函数的深拷贝(传统写法):
需要「深拷贝」的类,其内部往往是很复杂的,是需要用户显式定义赋值运算符重载函数来完成「深拷贝」的。
定义赋值运算符重载函数,有两个 string 类对象 s1 和 s2,要把其中一个赋值给另外一个,但是我们不知道这两个字符串谁长谁短,那该如何实现呢?
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
string s1("hello");
string s2("hello world");
s1 = s2;
仔细观察,这个代码有没有什么问题呢?new 开辟空间失败了怎么办?
所以我们先开辟空间,如果开辟空间没有失败,再去释放自己的空间。
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[strlen(s._str) + 1];
delete[] _str;
_str = tmp;
strcpy(_str, s._str);
}
return *this;
}
③ 拷贝构造 & 赋值运算符重载(现代写法)
上面的拷贝构造、赋值运算符重载函数的深拷贝实现,我们称之为传统写法。
接下来介绍拷贝构造、赋值运算符重载函数的深拷贝实现的「现代写法」。
👉👉拷贝构造函数的深拷贝(现代写法):
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(_str, tmp._str);
}
👉👉赋值运算符重载函数的深拷贝(现代写法):
通过参数间接调用拷贝构造函数,将「拷贝构造出来的 string 类对象 s」和「当前对象」的成员变量分别进行交换即可,这样当前对象就拿到了自己想要的内容,当函数调用结束后,拷贝构造出来的对象 s 出了作用域会被自动析构。
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
3.2 string 类的结构
string 是对字符串进行管理的类。实际上就是一个管理字符数组的顺序表。
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;
namespace winter
{
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
string(const char* str = "");
void swap(string& s);
string(const string& s);
string& operator=(string s);
~string();
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
void clear()
{
_str[0] = '\0';
_size = 0;
}
void reserve(size_t n);
void resize(size_t n, char ch = '\0');
string& insert(size_t pos, const char ch);
string& insert(size_t pos, const char* str);
void push_back(const char ch);
void append(const char* str);
string& operator+=(const char ch);
string& operator+=(const char* str);
string& operator+=(const string& s);
string& erase(size_t pos = 0, size_t len = npos);
size_t find(char ch, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0) const;
char* c_str() const { return _str; }
};
const size_t string::npos = -1;
};
3.3 string 类的迭代器
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
};
const size_t string::npos = -1;
👉测试:
void test3()
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it;
it++;
}
cout << endl;
for (auto e : s1)
{
cout << e;
}
cout << endl;
}
3.4 一些成员函数的实现
① 默认成员函数
👉 默认构造函数:
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
👉 交换两个容器的内容(即一一交换两个对象的成员变量),为了方便实现现代写法:
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
👉 拷贝构造函数:现代写法
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
this->swap(tmp);
}
👉 赋值运算符重载函数:现代写法
string& operator=(string s)
{
this->swap(s);
return *this;
}
👉 析构函数:
~string()
{
delete[] _str;
_str = nullptr;
}
② 访问操作 [] 运算符重载
注意:
- 对于普通数组而言,越界读一般是检查不出来的,越界写是抽查,可能会被检查出来。
- 对于string类而言,越界读和越界写都会被检查出来,因为在[]中进行了严格的检查。
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
③ 容量操作
👉 reserve 函数:更改容量(capacity)的大小
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
👉 resize 函数:调整字符串有效字符的长度
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else if (n > _size)
{
if (n > _capacity) reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
④ 修改操作
👉 insert 函数:在pos位置插入一个字符
string& insert(size_t pos, const char ch)
{
assert(pos >= 0 && pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
for (size_t i = _size + 1; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = ch;
_size++;
return *this;
}
insert 函数,在pos位置插入一个字符串,画图演示:
string& insert(size_t pos, const char* str)
{
assert(pos >= 0 && pos <= _size);
size_t len = strlen(str);
if (_size + len >= _capacity)
{
reserve(_size + len);
}
for (size_t i = _size + len; i >= pos + len; i--)
{
_str[i] = _str[i - len];
}
for (size_t i = 0; i < len; i++)
{
_str[pos++] = str[i];
}
_size += len;
return *this;
}
👉 push_back 函数:尾插一个字符
void push_back(const char ch)
{
insert(_size, ch);
}
👉 append 函数:在当前字符串末尾追加一个字符串
void append(const char* str)
{
insert(_size, str);
}
👉 += 运算符重载函数:在当前字符串末尾追加一个字符 / 字符串
直接复用前面的代码
string& operator+=(const char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(const string& s)
{
append(s._str);
return *this;
}
👉 erase 函数:删除从 pos 位置(包含 pos 位置)开始的 len 个字符
情况1:
情况2:
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos >= 0 && pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
⑤ 字符串操作
👉 find 函数:从 pos 位置开始查找字符,若找到,返回该字符第一次出现的下标;若没找到,返回npos
size_t find(char ch, size_t pos = 0) const
{
assert(pos >= 0 && pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch) return i;
}
return npos;
}
👉 find 函数:从pos位置开始查找子串,若找到,返回该子串第一个字符的下标;若没找到,返回npos
size_t find(const char* str, size_t pos = 0) const
{
assert(pos >= 0 && pos < _size);
const char* p = strstr(_str + pos, str);
if (p) return p - _str;
else return npos;
}
3.5 一些非成员函数的实现
① 流插入 << 运算符重载
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
void test6()
{
string s1("hello");
cout << s1 << endl;
cout << s1.c_str() << endl;
string s2("hello");
s2.resize(10);
s2[9] = 'x';
cout << s2 << endl;
cout << s2.c_str() << endl;
}
② 流提取 >> 运算符重载
👉先来看看这个写法,是哪里出了问题呢:
istream& operator>>(istream& in, string& s)
{
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;
}
return in;
}
void test7()
{
string s1;
cin >> s1;
cout << s1 << endl;
}
上面代码测试发现,一直卡在这里没有反应:
输入流中有 ‘1’、‘2’、‘3’、‘4’、‘\n’ 这些字符,
用 in >> ch 从输入流中获取字符,是获取不到空格和换行的,
因为 istream 对象 in 从输入流中获取字符时,会自动忽略掉输入流中的空格字符 ' ' 和换行字符 '\n' ,它会认为这是你输入两个字符之间的间隔,
所以需要借助 std::istream 类中的一个成员函数:
函数名称 | 功能说明 |
---|
std::istream::get | 从流中提取一个字符,不管是什么字符,都可以获取(好比C中的getchar()函数) |
👉流提取运算符重载函数中,改成用 get() 函数获取字符:但还是存在一个小问题
istream& operator>>(istream& in, string& s)
{
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
void test7()
{
string s1;
cin >> s1;
cout << s1 << endl;
}
运行结果:
但还是存在一些问题,比如:
流提取运算符,会对之前对象中的内容进行覆盖的,所以我们需要先清空字符串内容
👉正确写法:
class string
{
private:
public:
void clear()
{
_str[0] = '\0';
_size = 0;
}
};
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
③ getline 函数
获取一行字符串,直到遇到换行符 ‘\n’
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
④ 关系运算符 > 重载
关系运算符,进行大小比较
bool operator>(const string& s1, const string& s2)
{
size_t i = 0, j = 0;
for (; i < s1.size() && j < s2.size(); i++, j++)
{
if (s1[i] != s2[j]) return s1[i] > s2[j];
}
if (i == s1.size() && j == s2.size()) return false;
else if (i == s1.size()) return false;
else if (j == s2.size()) return true;
}
bool operator==(const string& s1, const string& s2)
{
size_t i = 0, j = 0;
for (; i < s1.size() && j < s2.size(); i++, j++)
{
if (s1[i] != s2[j]) return false;
}
if (i == s1.size() && j == s2.size()) return true;
else return false;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
bool operator>=(const string& s1, const string& s2)
{
return s1 > s2 || s1 == s2;
}
bool operator<(const string& s1, const string& s2)
{
return !(s1 >= s2);
}
bool operator<=(const string& s1, const string& s2)
{
return !(s1 > s2);
}
3.6 补充:string 类对象的大小?
👉下面代码的运行结果是什么呢?
int main()
{
string s1("xxx");
string s2("xxxxxxxxxxxxxxxx");
cout << sizeof(s1) << endl;
cout << sizeof(s2) << endl;
return 0;
}
🔨 VS2019 下测试,输出 28,按照我们上面模拟实现的版本应该是 12,为什么会是 28 呢?
这其实和 VS 下 PJ 版本的 STL string 的实现有关:
class string
{
private:
char _buf[16];
char* _str;
size_t _size;
size_t _capacity;
};
如果有效字符个数 < 16,不会去堆上开空间,而是存到一个名叫 _buf 的数组空间上,即存到对象中。
如果有效字符个数 >= 16,则会存到 _str 指向的堆空间上。
这样做可以减少内存碎片,提高效率。
🔨 Linux 下(SGI 版本的 STL)测试,输出 8
这个和 Linux 下 SGI 版本的 STL string 的实现有关。
因为没有看过源码,这里猜测 string 类对象中应该是只存了一个指向字符数组的指针(指针的大小是 8 字节,Linux默认是编译成64位的可执行程序)
那么对象的 _size 和 _capacity 是存在哪里的呢?猜测可能是这样存的,存在字符数组的前面12字节的空间中。
?
结论:所以当我们计算 string 类对象大小时,不要觉得奇怪,因为各家实现的版本可能会有所差异。
四、写时拷贝(了解)
首先回顾一下浅拷贝引发的问题:
- 同一块空间会被析构(释放)多次
- 一个对象修改会影响另外一个对象
为了解决这两个问题:
-
为了应对同一块空间会被析构多次这个问题,提出了引用计数。 引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象是资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。 -
为了应对一个对象修改会影响另外一个对象这个问题,提出了写时拷贝计数。 写时拷贝就是一种拖延症,是在「浅拷贝」的基础之上增加了引用计数的方式来实现的。 多个对象共用同一块内存空间,哪个对象去写数据,哪个对象就再进行深拷贝,本质是一种延迟深拷贝。当然,如果不进行写数据,那就不用进行深拷贝,提高了效率。 但这种方案也是有副作用的,现在基本上也被放弃了。
推荐文章:
👉我们来验证一下 STL string 是否用的是写时拷贝技术
#include<iostream>
#include<string>
int main()
{
std::string s1("hello world");
std::string s2(s1);
printf("%p\n", s1.c_str());
printf("%p\n", s2.c_str());
s2[0] = 'x';
printf("%p\n", s1.c_str());
printf("%p\n", s2.c_str());
return 0;
}
运行结果(VS2019 下 PJ 版本的 STL):没有用写时拷贝技术,直接深拷贝。
运行结果(Linux 下 SGI 版本的 STL):这个早期15年的版本用了写时拷贝技术,加上了引用计数。
最新版本的 gcc 编译器
五、拓展阅读
|