string类的模拟实现
- [一、Member constants(成员常数)](#一、Member constants(成员常数))
- [二、Member functions(成员函数)](#二、Member functions(成员函数))
-
- constructor(构造)、destructor(析构)、c_str
- [遍历1 :Iterators](#遍历1 :Iterators)
- [遍历2:下标+operator[](捎带着实现 size、capacity)](#遍历2:下标+operator[](捎带着实现 size、capacity))
- reserve
- push_back、append、operator+=
- insert、erase
- [substr、copy constructor、operator=](#substr、copy constructor、operator=)
- find
- clear
- [三、Non-member function overloads(非成员函数重载)](#三、Non-member function overloads(非成员函数重载))
-
- operator>>、operator<<
- [relational operators(关系运算符)](#relational operators(关系运算符))
- 四、swap
- 五、拷贝构造和赋值运算符重载的传统写法和现代写法
- 六、扩展
- 七、string的模拟实现代码
- 八、编码
结合底层的角度理解string类,并不是说自己实现出一个更好的或者是一样的string类。
不按照模板实现,因为按照模板实现会涉及编码,有点复杂。自己写一个简洁版的学习即可。
string类就是一个管理字符数组的类,实际空间中永远都有标识字符'\0',开空间时就要多开一个存储标识字符'\0',支持扩容底层就有size、capacity。但是无论是size还是capacity都不包含'\0'(size指向最后一个有效数据的下一个位置),所以我们自己实现的也要和库里的保持一致。这段话一定要牢牢记得,后面底层实现会反复用到的。
cpp
// 自己实现
// string.h
#include <iostream>
using namespace std;
namespace bit
{
class string {
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
string类的设计本身就有一些是冗余的,实现最基本的使用即可。实现时我们可以参考文档看看库里的函数的声明怎么给的,根据声明试着实现定义底层。
文档链接: https://legacy.cplusplus.com/
为了防止发生命名冲突,我们将对实现string类的测试、声明定义都被一个同名命名空间域包起来,不同的文件的同名namespace会被认为是同一个namespace。
cpp
// string.h
#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;
namespace bit
{
class string {
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
size_t size() const;
size_t capacity() const;
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
无参默认构造
//string()
// // 设置成空串就会有问题,一个字符都没有就等于没有'\0',但是string类永远都有标识字符'\0',所以要new一下。
// //:_str(nullptr)
// :_str(new char[1] {'\0'})
// // 但是_size和_capacity永远都不包括标识字符'\0'
// ,_size(0)
// ,_capacity(0)
//{}
带参构造
//string(const char* str)
// :_size(strlen(str))
//{
// _str = new char[_size + 1];
// _capacity = _size;
// //strcpy(_str, str);
// memcpy(_str, str, _size + 1);
//}
// 析构函数
~string()
{
if (_str)
{
delete[] _str;// 配套使用
_str = nullptr;
_size = _capacity = 0;
}
}
const char* c_str() const
{
return _str;
}
// 无参、带参构造可以合并成全缺省默认构造
// string(const char* str = nullptr)// 不能这样写,strlen(nullptr),会对空指针解引用计算字符串的长度,遇到'\0'才会停止
// string(const char* str = " ")// 什么都不写也行,因为C中会默认加标识字符'\0'(空串中也会默认加'\0')
string(const char* str = "\0")
// 不传实参,用缺省值,strlen()计算串的长度是0,即_size、_capacity也是0,但是开空间会多开一个
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
//strcpy(_str, str);
memcpy(_str, str, _size + 1);
}
// range constructor
template <class InputIterator>
string(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
void reserve(size_t n = 0);
void push_back(char c);
string& append(const char* s);
string& operator+=(char c);
string& operator+=(const char* s);
void insert(size_t pos, char c);
void insert(size_t pos, const char* s);
void erase(size_t pos, size_t len = npos);
// 传统写法
//string(const string& s);
//string& operator=(const string& s);
// 现代写法
string(const string& s);
string& operator=(const string& s);
string& operator=(string s);
string substr(size_t pos = 0, size_t len = npos) const;
size_t find(char c, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;
void clear();
bool operator>(const string& s) const;
bool operator>=(const string& s) const;
bool operator<(const string& s) const;
bool operator<=(const string& s) const;
bool operator==(const string& s) const;
bool operator!=(const string& s) const;
void swap(string& s);
private:
// 相当于在这里有优化
// char _buff[16];
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const size_t npos;
// 支持特殊处理
//static const size_t npos = -1;
// 不支持下列写法
//static const double d = 12.34;
//static size_t x = 1;
};
std::ostream& operator<< (std::ostream& os, const string& s);
std:: istream& operator>> (std::istream& is, string& s);
void swap(string& x, string& y);
}
一、Member constants(成员常数)
npos
npos(全称"no position"或"not a position")是编程中常见的特殊常量,主要用于表示无效位置或未找到的标识。
定义:std::string::npos是std::string类的静态常量成员,类型为size_t,表示字符串中不存在的索引位置。其值通常被定义为size_t类型的最大值(即-1的补码表示,大概42亿9000万)。
npos是一个公有的静态常量成员,在类外面能被访问。自己怎么实现呢?
法一:类里声明,类外面定义。声明中加静态的属性就够了。
法二:缺省值是给初始化列表的,静态成员不走初始化列表,不能加缺省值。但是这里可以加,这是C++的特殊处理,只有整型(比如浮点型就不可以)的const静态可以这样,就不用在类外面定义了。但是普通静态成员变量不能这样。
cpp
// string.h
namespace bit
{
class string
{
public:
static const size_t npos;
// 支持特殊处理
//static const size_t npos = -1;
// 不支持下列写法
//static const double d = 12.34;
//static size_t x = 1;
};
}
cpp
// string.cpp
namespace bit
{
const size_t stirng::npos = -1;
}
二、Member functions(成员函数)
constructor(构造)、destructor(析构)、c_str
构造和析构都是被最高频调用的短小函数,直接定义在类里会默认是内联函数。
分别实现无参构造和带参构造,无参和带参的合并就是全缺省构造。
流插入、流提取写起来比较复杂,我们暂且用c_str也可以输出string类对象。流插入、流提取运算符重载后面会讲解及其实现。
无参默认构造:设置成空串就会有问题,一个字符都没有就等于没有'\0',但是string类永远都有'\0',所以要new出一个空间。但是_size和_capacity永远都不包括'\0'。new底层相当于调用了malloc(),会有资源,所以析构函数也要自己实现,对象出了作用域会调用对应的析构函数的。
cpp
// 无参默认构造
string()
// 设置成空串就会有问题,一个字符都没有就等于没有'\0',但是string类永远都有标识字符'\0',所以要new一下。
//:_str(nullptr)
:_str(new char[1] {'\0'})
// 但是_size和_capacity永远都不包括标识字符'\0'
,_size(0)
,_capacity(0)
{}
cpp
// 析构函数
~string()
{
// 析构函数析构空是没有问题的,但是若怕delete空可以加个条件
if (_str)
{
delete[] _str;// 配套使用
_str = nullptr;
_size = _capacity = 0;
}
}
cpp
// c_str
const char* c_str() const
{
return _str;
}

带参构造:
- 按照成员变量的声明顺序进行初始化,_size还没有初始化,是随机值,所以_str是随机值,即输出的类对象s2也是随机值:

- 但是成员变量的声明顺序改成与初始化列表的顺序一样,还是会打印出随机值。因为只把空间开出来了,但是并没有把数据拷贝过去。strcpy()遇到'\0'会终止,遇到中间有'\0'的字符串会拷贝不完全,可以用memcpy()解决这一问题。拷贝数据在函数体内实现:

极端情况下字符串中间会出现'\0':

这样的情况下,实现reserve扩容(异地扩容),用strcpy就会出现问题,strcpy遇到'\0'就会停止拷贝,会导致数据丢失,所以用memcpy。拷贝逻辑最好都改成memcpy,避免字符串中间有'\0'的问题。

- 但是顺序一旦不一样了,按照声明顺序初始化,编译器下_size可能是随机值,也可能是0,是0的情况下就只会new一个字节的空间,但是现在要拷贝过去12个字节(hello world)的数据就会有越界的问题,越界不会报错,但是析构的时候(delete有检查是否越界的功能)会报错,也就是出了作用域才会检查有没有越界。所以要将size和capacity的声明顺序放在前面,即强制绑定声明顺序与初始化列表顺序一致,但是这样也不可靠,其他人用可能会改变顺序,就会很麻烦。解决办法:干脆直接将_capacity和_str在函数体内初始化,就不会受声明顺序的影响。
cpp
// 带参构造
string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
//strcpy(_str, str);
memcpy(_str, str, _size + 1);
}
- 其他两种初始化成员变量的劣势:若都用strlen()作为初始值初始化成员变量,三次strlen()会有效率问题;强制绑定声明顺序与初始化列表顺序一致,其他人用可能会改变顺序,就会很麻烦。(用sizeof()计算_str的大小也是不行的,_str是指针了。)
全缺省默认构造:
无参、带参构造可以合并成全缺省默认构造。
cpp
// string(const char* str = nullptr)// 不能这样写,strlen(nullptr),会对空指针解引用计算字符串的长度,遇到'\0'才会停止
// string(const char* str = " ")// 什么都不写也行,因为C中会默认加标识字符'\0'(空串中也会默认加'\0')
string(const char* str = "\0")
// 不传实参,用缺省值,strlen()计算串的长度是0,即_size、_capacity也是0,但是开空间会多开一个
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];// 空间数总是多一个,注意是空间!
//strcpy(_str, str);
memcpy(_str, str, _size + 1);// 拷贝数据不要忘了最后的标识字符'\0'
}
cpp
void test_string1()
{
//初步定义一个空串(_str(nullptr))也没有问题,但是提供流插入、流提取就有问题了(暂且不提供,流插入、流提取写起来比较复杂,用c_str也可以输出str)。
//string s1;
//c_str返回const char*,程序崩溃的原因:c_str里面的_str是空指针,cout一个空指针,char*类型打印不会去按照指针去打印,cout输出时,char*类型会按照字符串打印(会对返回的指针进行解引用,遇到'\0'就终止。所以遇到字符串中间有'\0'时没有打印完全也不要惊讶,可以通过调试看看变化或者范围for遍历输出)
//cout << s1.c_str() << endl;
//
//而库里面的空对象是不会有问题的,啥都不打印,打印一个空
//std::string s1;
//cout << s1.c_str() << endl;
// 都可以调用全缺省默认构造
string s1;
cout << s1.c_str() << endl;// 无参构造,什么都不会打印出来,通过调试可以看到标识字符'\0'
string s2("hello world");
cout << s2.c_str() << endl;// 带参构造,随机值
string s3("hello\0\0\0\0world");
cout << s3.c_str() << endl;
}

range constructor:
迭代器区间的构造函数

cpp
// string.h
template <class InputIterator>
string(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
但是,若是传递过来的字符串中间有非标识字符'\0',那么strlen()就计算到'\0'就终止了,这样会导致数据缺失,所以,可以实现一个范围构造函数。
我们发现,实现string类不像之前实现日期类一样那么简单,相反,string类的实现要考虑很多层面,稍有不慎,就会出错。
若出现:

之前的封装就是数据和方法放在一起,其实还有上下层的分离,上下层是不一样的,上层是指所有的容器都用着同一种方式去访问的,但是下层(底层实现)就千变万化了。
遍历1 :Iterators
迭代器(不一定是指针)。
给迭代器提供类型。在类里面实现类型有两种方法:内部类或typedef。
反向迭代器的实现比较复杂,涉及适配器,学到后面会讲解
范围for会替换成迭代器,就是迭代器假如写成Begin范围for就执行不了,因为没有找到begin()
cpp
// string.h
typedef char* iterator;
typedef const char* const_iterator;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
cpp
// string.cpp
string::iterator string::begin()
{
return _str;
}
string::const_iterator string::begin() const
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::end() const
{
return _str + _size;
}
cpp
// Test.cpp
void func(const string& s)
{
string::const_iterator it = s.begin();
while (it != s.end())
{
//(*it)++;
cout << *it << " ";
++it;
}
cout << endl;
}
void test_string2()
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
(*it)++;
cout << *it << " ";
++it;
}
cout << endl;
func(s1);
// 范围for会替换为迭代器
for (auto ch : s1)
{
--ch;
cout << ch << " ";
}
cout << endl;
for (auto& ch : s1)
{
--ch;
}
cout << s1.c_str() << endl;
}

遍历2:下标+operator[](捎带着实现 size、capacity)
cpp
// string.h
size_t size() const;
size_t capacity() const;
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
cpp
// string.cpp
size_t string::size() const
{
return _size;
}
size_t string::capacity() const
{
return _capacity;
}
char& string::operator[] (size_t pos)
{
// pos已经是非负整数了,肯定大于等于0,只用判断它小于_size即可
assert(pos < _size);
return _str[pos];
}
const char& string::operator[] (size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
cpp
// Test.cpp
void test_string3()
{
string s1("hello world");
// 可读可修改
s1[0]++;
for (size_t i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
// 只读
const string s2("hello world");
//s2[0]++;
for (size_t i = 0; i < s2.size(); ++i)
{
cout << s2[i] << " ";
}
cout << endl;
}

实现append(追加)数据:push_back、append、operator+=,一旦追加数据就避免不了扩容的问题,所以在实现它们之前先实现reserve。
reserve
cpp
// string.h
void reserve(size_t n = 0);
cpp
// string.cpp
void string::reserve(size_t n)// 声明和定义分离时缺省值只能给声明
{
if (n > _capacity)
{
// 关于new扩容,需要自己手动实现
char* tmp = new char[n + 1];
if (_str)
{
memcpy(tmp, _str, _size + 1);// 拷贝空会出现问题的
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
push_back、append、operator+=
cpp
// string.h
void push_back(char c);
string& append(const char* s);
string& operator+=(char c);
string& operator+=(const char* s);
这个逻辑写得不对,因为有一种极端的场景,若用默认参数初始化(不传实参),也就是用空串初始化,此时_size、_capacity都是0,开了一个空间。这时去尾插扩2倍的容量,就会出现_capacity还是0的问题,就要写个三目操作符。
cpp
// string.cpp
void string::push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// 插入数据后,新的数据会把标识字符'\0'覆盖,需要自己手动加上'\0'
_str[_size++] = c;
_str[_size] = '\0';
}
// 扩2倍逻辑不好用了,假设_size、_capacity都为10,新加入的字符串长度len为30,那么2倍扩后的容量肯定不够。
// 需要多少开多少也不行,每次append就会导致频繁的扩容。有一个扩容方法可以缓解上面的问题:
string& string::append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
size_t newCapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
reserve(newCapacity);
}
memcpy(_str + _size, s, len + 1);
_size += len;
return *this;
}
string& string::operator+=(char c)
{
push_back(c);
return *this;
}
string& string::operator+=(const char* s)
{
append(s);
return *this;
}
cpp
// Test.cpp
void test_string4()
{
string s1;
s1.push_back('1');
s1.push_back('2');
s1.push_back('3');
s1.push_back('4');
s1.push_back('x');
s1.push_back('y');
s1.push_back('z');
s1.push_back('\0');
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
s1.append("11111111111111111111");
// 遇到'\0'就终止了,可以用范围for遍历输出
cout << s1.c_str() << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
string s2("hello");
s2 += 'a';
s2 += ' ';
s2 += "world";
cout << s2.c_str() << endl;
}

insert、erase
cpp
// string.h
void insert(size_t pos, char c);
void insert(size_t pos, const char* s);
void erase(size_t pos, size_t len = npos);
insert头插一个字符,-1 >= 0,还是会进入循环,原因是运算符的两个操作数类型不同时会引发类型提升,范围小的向范围大的提升,end是有符号,pos是无符号的,比较的时候int会提升成无符号整型。那把pos强制类型转换成int类型也不可取,因为库里面pos接口设计的都是无符号整型的。法一:end >= (int)pos,避免类型提升。法二:避免无符号end小于0,end一开始在'\0'后一个位置(_size + 1),让前一个数据往后挪动。



cpp
// string.cpp
void string::insert(size_t pos, char c)
{
// 也有可能在_size的位置插入数据
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// end可能会出界
//int end = _size;
//while (end >= (int)pos)// 为什么类型不同却能正常运行?end是int类型,pos是size_t类型,类型提升。但是若end为-1,也就是越界了,会有符号位丢失及比较运算符陷阱的问题。若将pos强制类型转换为int也不是一劳永逸的事情。
//{ }
/*size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}*/
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
_size++;
}
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
size_t newCapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
reserve(newCapacity);
}
size_t end = _size + len;
while (end >= pos + len)// 或while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 若是while循环写成下面这样,会多挪动几个数据,这样实现结果也没有问题。但是第一个问题是多挪动几个数据会导致白做功了,第二个问题就是pos为0时会越界,越界不会报错。
/*while (end > pos)
{
_str[end - len] = _str[end];
--end;
}*/
for (size_t i = 0; i < len; ++i)
{
_str[i + pos] = s[i];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
// 没办法在_size位置删除数据,_size位置不是有效数据
assert(pos < _size);
if (len == npos || len >= _size - pos)// len == npos表示len肯定大于后面删除的长度,不可能有npos(42亿9000万)这么长
{
// 全部删完
_str[pos] = '\0';
_size = pos;
}
else {
// 删除部分,挪动数据覆盖
//memmove可以解决内存重叠的问题,别忘了标识字符'\0'
memmove(_str + pos, _str + pos + len, _size + 1 - (pos + len));
_size -= len;
}
}
cpp
// Test.cpp
void test_string5()
{
string s1("hello world");
s1.insert(6, 'x');
cout << s1.c_str() << endl;
s1.insert(0, '1');
cout << s1.c_str() << endl;
string s2("hello world");
s2.insert(6, "xxx");
cout << s2.c_str() << endl;
s2.insert(0, "aaa");
cout << s2.c_str() << endl;
s2.erase(6);
cout << s2.c_str() << endl;
s2.erase(3, 100);
cout << s2.c_str() << endl;
s1.erase(5, 3);
cout << s1.c_str() << endl;
}

substr、copy constructor、operator=
cpp
// string.h
string(const string& s);
string substr(size_t pos = 0, size_t len = npos) const;
string& operator=(const string& s);
拷贝构造:一个已经存在的对象去拷贝初始化另一个要创建的对象。
赋值运算符重载:两个已经存在的对象之间的赋值。
若在模拟实现substr时没有自己实现拷贝构造:
ret拷贝构造出临时对象,因为我们没有自己实现拷贝构造,所以这里是浅拷贝,会导致ret和临时对象都指向同一块空间,ret出了作用域会调用析构函数(我们之前自己实现了),ret及其指向的空间会释放,所以临时对象中指向这块空间的指针就是一个野指针。(实际上,编译器会优化成一个拷贝构造)。本质都是传值返回会生成拷贝。这个问题跟下面图中的问题是一样的。
后定义的(s5)先析构,会调用两次析构:

那么就要自己写拷贝构造来实现深拷贝!
所有开空间的地方,像拷贝构造、reserve,永远都会多开一个给'\0',所以不用考虑'\0'的空间,它永远有空间的。需要多少个字符就开多少空间,调用对应的接口。
string类对象赋值,s1 = s2,有三种情况:
第一种:s1 s2两个capacity一样或者s1的capacity比s2的大,s2的都可以赋值给s1。
第二种:s1的capacity比s2的小,原s1空间释放,开一块与s2一样大的空间,赋值。
第三种:s1的capacity比s2的大很多,拷贝赋值后,后面的空间用不上会浪费。
综上,干脆直接将s1的空间释放,开一块与s2一样大的新空间,s2里的值赋值给s1。
我们不写,编译器默认生成的赋值运算符重载实现浅拷贝,会导致s1空间丢失:
所以,自己实现substr和赋值运算符重载时都需要自己写拷贝构造实现深拷贝!
cpp
// string.cpp
string::string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
string string::substr(size_t pos, size_t len) const
{
assert(pos < _size);
//两个条件综合在一起可以去掉第一个条件
//if (len == npos || len > _size - pos)
// 若len大于后面剩余的字符个数,就将len改为实际长度
if (len > _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0; i < len; ++i)
{
ret += _str[i + pos];
}
return ret;
}
string& string::operator=(const string& s)
{
// 避免自己跟自己赋值
if (this != &s)
{
// 先开新空间拷贝数据再释放旧空间
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
cpp
// Test.cpp
void test_string6()
{
string s1("hello world");
//string s2 = s1.substr(6);// 拷贝构造
string s2 = s1.substr(6, 100);// 拷贝构造
cout << s2.c_str() << endl;
string s3("hello world");
string s4 = s3.substr(6, 3);// 拷贝构造
cout << s4.c_str() << endl;
s4 = s3;
cout << s4.c_str() << endl;
string s5(s4);
cout << s5.c_str() << endl;
string s6("hello world");
string s7("xxxxxxxxxxxhello world");
s6 = s7;
cout << s6.c_str() << endl;
cout << s7.c_str() << endl;
s6 = s6;
cout << s6.c_str() << endl;
cout << s6.c_str() << endl;
}

find
cpp
// string.h
size_t find(char c, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;
cpp
// string.cpp
size_t string::find(char c, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t string::find(const char* s, size_t pos) const
{
assert(pos < _size);
const char* p = strstr(_str, s);
if (nullptr == p)
{
return npos;
}
else {
return p - _str;
}
}
cpp
// Test.cpp
void test_string7()
{
string url("https://legacy.cplusplus.com/reference/string/string/find/");
size_t pos1 = url.find("://");
if (pos1 != string::npos)
{
string sub1 = url.substr(0, pos1);
cout << sub1.c_str() << endl;
}
size_t pos2 = url.find('/', pos1 + 3);
if (pos2 != string::npos)
{
string sub2 = url.substr(pos1 + 3, pos2 - (pos1 + 3));
cout << sub2.c_str() << endl;
string sub3 = url.substr(pos2 + 1);
cout << sub3.c_str() << endl;
}
}

clear
cpp
// string.h
void clear();
cpp
// string.cpp
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
三、Non-member function overloads(非成员函数重载)
operator>>、operator<<
cpp
// string.h
std::ostream& operator<< (std::ostream& os, const string& s);
std:: istream& operator>> (std::istream& is, string& s);
判断:流插入、流提取运算符重载都要写成友元。错误的。
流插入、流提取运算符重载,只能写成全局的。
不用写成友元,原因是输出一个又一个的字符,访问字符(内置类型库里提供了<< >>运算符重载)可以范围for、下标+[]。
一般情况下流提取和c_str没什么区别,但是特殊情况下有区别:字符串中间有'\0'时,c_str会按照字符串的形式输出,遇到'\0'就终止。所以用流提取输出肯定是更靠谱的。
流提取后面不加const:流里面提取内容放到string类对象里。
流插入跳过空格直接读4,读不到空格:

流提取读取字符的时候,还是会跳过空格、换行符的,所以一般想将所有的都识别为字符读取,要用get函数。是因为cin(和scanf)会把空格、换行当作是多项之间的分割,都会忽略掉的,就会有输入个不停的现象。那么想读取空格怎么办呢?字符一个一个读可以调用C++里的get():

cpp
std::istream& operator>> (std::istream& is, string& s)
{
s.clear();
char ch;
//is >> ch;
ch = is.get();//is.get(ch);
while (ch != ' ' && ch != '\n')
{
s += ch;
//is >> ch;
ch = is.get();//is.get(ch);
}
return is;
}

但是若输入了一个长度非常长的串,可能会存在大量扩容的问题,若是提前开好假如是1024个空间,这时又会导致另一个问题,输入短串会有空间浪费的问题。在没有get()到字符之前是不知道有多少个字符的,就没有办法一次性开好空间。折中办法:开256(自己定空间大小,在栈上开空间效率很高,栈在编译时就运算好了要开多少空间)个空间,不+=这个字符,而是把这个字符放进数组buff里,就可以根据s+=buff一次性开好空间,这样会很大程度上减少扩容的次数,函数结束局部数组buff也跟着销毁了,效率高。而用reserve提前开好大空间,追加短串,string对象不销毁,空间就一直在,会造成空间浪费。------综上,提前开空间,开少了不够用会频繁扩容,开多了会浪费。就可以放在数组buff里一段一段加,数组空间大小自己定。
cpp
// string.cpp
std::ostream& operator<< (std::ostream& os, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
os << s[i];
}
return os;
}
std::istream& operator>> (std::istream& is, string& s)
{
s.clear();
char buff[256];
char ch = is.get();
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
is.get(ch);
if (255 == i)
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return is;
}
cpp
// Test.cpp
void test_string8()
{
string s1("hello world");
string s2("xxxxxxxxxxxhello world");
cout << s1 << endl;
cout << s1.c_str() << endl;
s1 += '\0';
s1 += "xxxxxxxx";
cout << s1 << endl;
cout << s1.c_str() << endl;// 返回的const char*类型按照字符串的形式输出,遇到'\0'就停止
cin >> s1 >> s2;
cout << s1 << endl;
cout << s2 << endl;
system("pause");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
}

relational operators(关系运算符)
库里面是重载成非成员函数了,我们从学习的角度就把它们重载成成员函数即可。
cpp
// string.h
bool operator>(const string& s) const;
bool operator>=(const string& s) const;
bool operator<(const string& s) const;
bool operator<=(const string& s) const;
bool operator==(const string& s) const;
bool operator!=(const string& s) const;
直接调用库里的strcmp(),strcmp()也有遇到'\0'就停止的问题,用memcm,但是memcmp的比较字节数没办法确定(比如"hello"和"hello world",指定短的字节数,就会认为他们俩是相等的),所以需要我们自己实现。
string与C++库里的string不能比较,不同的命名空间里就是两种类型了:

cpp
// string.cpp
bool string::operator>(const string& s) const
{
return !(*this <= s);
}
bool string::operator>=(const string& s) const
{
return !(*this < s);
}
bool string::operator<(const string& s) const
{
size_t len1 = _size, len2 = s._size;
size_t i1 = 0, i2 = 0;
while (i1 < len1 && i2 < len2)
{
if (_str[i1] < s._str[i2])
{
return true;
}
else if (_str[i1] > s._str[i2])
{
return false;
}
else {
++i1;
++i2;
}
}
/*if (i1 == len1 && i2 < len2)
return true;
else
return false;*/
return i1 == len1 && i2 < len2;// 也包括 i1 < len1 && i2 == len2,返回false
}
bool string::operator<=(const string& s) const
{
return *this == s || *this < s;
}
bool string::operator==(const string& s) const
{
size_t len1 = _size, len2 = s._size;
size_t i1 = 0, i2 = 0;
while (i1 < len1 && i2 < len2)
{
if (_str[i1] != s._str[i2])
return false;
else {
++i1;
++i2;
}
}
return i1 == len1 && i2 == len2;
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
cpp
// Test.cpp
void test_string9()
{
string s1("hello");
string s2("hello");
s1 += '\0';
s1 += "world";
string s3("hello");
cout << (s1 == s2) << endl;
cout << (s1 < s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s2 == s3) << endl;
cout << (s2 < s1) << endl;
}

四、swap
swap有点特殊,有重载为类的成员函数和重载为非成员函数两个版本。
cpp
// string.h
namespace bit
{
class string
{
public:
void swap(string& s);
private:
};
void swap (string& x, string& y);
}
cpp
// string.cpp
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
void swap(string& x, string& y)
{
x.swap(y);
}
假设我们还没有实现任何针对string类的swap()函数,其实直接调用算法库里的swap也可以实现,但是会有问题,两个类对象指向的空间会被换成新的空间,原因是内部会出现3个深拷贝。我们自己模拟实现库里的swap()函数就按照下图中红色线框住的内容实现。a拷贝构造c,调用自己实现的能完成深拷贝的拷贝构造,c就开了一块跟a一样大的空间一样的值(第一次深拷贝);b赋值a(调用自己实现的能完成深拷贝的赋值运算符重载),a开一块与b一样的空间,与b有一样的值,释放a旧空间(第二次深拷贝);c赋值给b,b开一块与c一样的空间,与c有一样的值,释放b旧空间(第三次深拷贝)。c是局部变量,出了作用域会销毁,c空间释放。a与b指向的空间及其内容完成了交换,但是3次深拷贝的代价太大了。像日期类拷贝代价没那么大,直接用算法库里的swap是没问题的,但是要深拷贝的类直接调用代价就太大了。解决办法:直接改变指向两块空间的两个指针的指向即可,但是不要只交换指针,还要交换空间及其数据,这是等会实现类中的成员函数swap和非成员函数swap时需要注意的。这也就是为什么像string类这样内部有资源的类,类中自己就提供了swap成员函数或针对自身类型的非成员函数swap。

模拟实现类的成员函数swap,非成员函数swap直接对类的成员函数swap进行复用即可。

学C++的人分为两类人,一类是学了使用还学习底层的人,另一类就是只学习使用的人。只学习使用的人可能就会觉得直接用算法库里的swap何乐而不为呢?所以就从使用的角度出发,string类中还提供了全局的交换函数。在讲函数模板的时候提到过普通函数和函数模板能同时存在,下图中的它们同时存在,但是会调用现成的函数而不是函数模板(有现成吃现成)。这样,下面两种调用方式(调用针对string类的全局函数swap和string类的成员函数swap)效率都很高,因为始终调用不到函数模板的模板函数。

cpp
// Test.cpp
// 模拟实现算法库里的swap()
template <class T> void swap(T& a, T& b)
{
T c(a); a = b; b = c;
}
void test_string10()
{
string s1("hello");
string s2("helloxxxxxxxxxxxxxxx");
cout << s1 << endl;
cout << s2 << endl;
swap(s1, s2);
s1.swap(s2);
cout << s1 << endl;
cout << s2 << endl;
}

五、拷贝构造和赋值运算符重载的传统写法和现代写法
拷贝构造和赋值运算符重载还有不同于传统写法的现代写法,都是开空间拷贝数据但是方式更另类一些。
对于传统写法和现代写法可以这么理解:比如我想吃红烧牛腩。
传统吃法(传统写法):自己养牛,给它喂草,让它长大,把它吃了。(什么都自己实现)
现代吃法(现代写法):点外卖,钱与食物的交换。(交给其他函数去做,然后交换,本质是一种复用)
现代写法和传统写法对于空间的消耗都是一样的 ,没有效率的提升。
cpp
// string.h
string(const string& s);
string& operator=(const string& s);
string& operator=(string s);
拷贝构造现代写法
s1的_str初始化tmp,swap后tmp指向s2之前指向的空间,因为s2还没有初始化,所以s2之前指向的空间是随机值(vs下是空),若是随机值,tmp出了作用域后析构会有问题,析构函数析构空是没有问题的,但是若怕delete空可以加个条件。若怕其他编译器让s2是随机值,那么就可以给成员变量缺省值。极端情况下,现代写法会受困于中间有'\0'的字符串,会拷贝不完全,原因是里面的构造的strlen的问题,这样会不满足我们的需求。我们其实可以实现迭代器区间的构造(前面已经实现过啦)来解决这样的问题,成员函数也可以是一个模板。
cpp
// string.cpp
// s2(s1)
string::string(const string& s)
{
//string tmp(s._str);
string tmp(s.begin(), s.end());
swap(tmp);
}

怕其他编译器给s2随机值,那么可以给成员变量缺省值:
cpp
class string
{
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
赋值运算符重载现代写法:复用现代写法的拷贝构造
s2拷贝构造tmp,构造出跟s2一样大小的空间和值,s1与tmp交换,s1还有空间需要释放,也是借助tmp,tmp是局部变量,出了作用域直接调用析构销毁tmp(tmp是原来s1的空间和值)。
cpp
// string.cpp
// s1 = s2
/*string& string::operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}*/
// 更简洁版本
// s1 = s2
string& string::operator=(string tmp)// 传值传参调用拷贝构造,s2拷贝构造tmp
{
swap(tmp);
return *this;
}
六、扩展
我们计算类对象的大小时只计算成员变量的大小,下面图中两个对象sizeof()一样大吗?一样大。理论上的计算结果是12字节,为什么输出结果是28字节呢?输出结果取决于平台,不同平台实现不同。

_Mysize是size,_Myreserve是capacity。_Buf点开加了16字节,也就是可以存储15个有效字符,字符个数小于16的话,字符就会存储在buff里,若大小是16及其以上,就会存储在_ptr(_str)里,这样就避免了在堆上频繁开小块的空间引发内存碎片、降低效率的问题。字符串比较短的就不要在堆上开了,就存储在string对象里面的数组buff里。这是vs自己给的优化方案。小于16字符在buff里找,大于等于16在_str里找。

cpp
class string
{
private:
// 相当于在这里有优化
// char _buff[16];
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
七、string的模拟实现代码
cpp
// string.cpp
#include "string.h"
namespace bit
{
const size_t string::npos = -1;
string::iterator string::begin()
{
return _str;
}
string::const_iterator string::begin() const
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::end() const
{
return _str + _size;
}
size_t string::size() const
{
return _size;
}
size_t string::capacity() const
{
return _capacity;
}
char& string::operator[] (size_t pos)
{
// pos已经是非负整数了,肯定大于等于0,只用判断它小于_size即可
assert(pos < _size);
return _str[pos];
}
const char& string::operator[] (size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
void string::reserve(size_t n)// 声明和定义分离时缺省值只能给声明
{
if (n > _capacity)
{
// 关于new扩容,需要自己手动实现
char* tmp = new char[n + 1];
if (_str)
{
memcpy(tmp, _str, _size + 1);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void string::push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// 插入数据后,新的数据会把标识字符'\0'覆盖,需要自己手动加上'\0'
_str[_size++] = c;
_str[_size] = '\0';
}
string& string::append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
size_t newCapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
reserve(newCapacity);
}
memcpy(_str + _size, s, len + 1);
_size += len;
return *this;
}
string& string::operator+=(char c)
{
push_back(c);
return *this;
}
string& string::operator+=(const char* s)
{
append(s);
return *this;
}
void string::insert(size_t pos, char c)
{
// 也有可能在_size的位置插入数据
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// end可能会出界
//int end = _size;
//while (end >= (int)pos)// 为什么类型不同却能正常运行?end是int类型,pos是size_t类型,类型提升。但是若end为-1,也就是越界了,会有符号位丢失及比较运算符陷阱的问题。若将pos强制类型转换为int也不是一劳永逸的事情。
//{ }
/*size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}*/
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
_size++;
}
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
size_t newCapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
reserve(newCapacity);
}
size_t end = _size + len;
while (end >= pos + len)// 或while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 若是while循环写成下面这样,对于头插就不行了
/*while (end > pos)
{
_str[end - len] = _str[end];
--end;
}*/
for (size_t i = 0; i < len; ++i)
{
_str[i + pos] = s[i];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
// 没办法在_size位置删除数据,_size位置不是有效数据
assert(pos < _size);
if (len == npos || len >= _size - pos)
{
// 全部删完
_str[pos] = '\0';
_size = pos;
}
else {
// 删除部分,挪动数据覆盖
memmove(_str + pos, _str + pos + len, _size + 1 - (pos + len));
_size -= len;
}
}
// 传统写法的拷贝构造和赋值运算符重载
//string::string(const string& s)
//{
// _str = new char[s._capacity + 1];
// memcpy(_str, s._str, s._size + 1);
// _size = s._size;
// _capacity = s._capacity;
//}
//string& string::operator=(const string& s)
//{
// // 避免自己跟自己赋值
// if (this != &s)
// {
// // 先开新空间拷贝数据再释放旧空间
// char* tmp = new char[s._capacity + 1];
// memcpy(tmp, s._str, s._size + 1);
// delete[] _str;
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
// 现代写法的拷贝构造和赋值运算符重载
// s2(s1)
string::string(const string& s)
{
string tmp(s._str);
//string tmp(s.begin(), s.end());
swap(tmp);
}
// s1 = s2
/*string& string::operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}*/
// 更简洁版本
// s1 = s2
string& string::operator=(string tmp)// 传值传参调用拷贝构造,s2拷贝构造tmp
{
swap(tmp);
return *this;
}
string string::substr(size_t pos, size_t len) const
{
assert(pos < _size);
// 若len大于后面剩余的字符个数,就将len改为实际长度
if (len > _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0; i < len; ++i)
{
ret += _str[i + pos];
}
return ret;
}
size_t string::find(char c, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t string::find(const char* s, size_t pos) const
{
assert(pos < _size);
const char* p = strstr(_str, s);
if (nullptr == p)
{
return npos;
}
else {
return p - _str;
}
}
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
std::ostream& operator<< (std::ostream& os, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
os << s[i];
}
return os;
}
//std::istream& operator>> (std::istream& is, string& s)
//{
// s.clear();
// char ch;
// //is >> ch;
// ch = is.get();//is.get(ch);
// while (ch != ' ' && ch != '\n')
// {
// s += ch;
// //is >> ch;
// ch = is.get();//is.get(ch);
// }
// return is;
//}
std::istream& operator>> (std::istream& is, string& s)
{
s.clear();
char buff[256];
char ch = is.get();
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
is.get(ch);
if (255 == i)
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return is;
}
bool string::operator>(const string& s) const
{
return !(*this <= s);
}
bool string::operator>=(const string& s) const
{
return !(*this < s);
}
bool string::operator<(const string& s) const
{
size_t len1 = _size, len2 = s._size;
size_t i1 = 0, i2 = 0;
while (i1 < len1 && i2 < len2)
{
if (_str[i1] < s._str[i2])
{
return true;
}
else if (_str[i1] > s._str[i2])
{
return false;
}
else {
++i1;
++i2;
}
}
/*if (i1 == len1 && i2 < len2)
return true;
else
return false;*/
return i1 == len1 && i2 < len2;// 也包括 i1 < len1 && i2 == len2,返回false
}
bool string::operator<=(const string& s) const
{
return *this == s || *this < s;
}
bool string::operator==(const string& s) const
{
size_t len1 = _size, len2 = s._size;
size_t i1 = 0, i2 = 0;
while (i1 < len1 && i2 < len2)
{
if (_str[i1] != s._str[i2])
return false;
else {
++i1;
++i2;
}
}
return i1 == len1 && i2 == len2;
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
void swap(string& x, string& y)
{
x.swap(y);
}
}
八、编码
为什么string是个模板?不是针对某种类型例如char实现的,而是针对多种类型实现的,为什么会有这么多类型呢?这时就涉及编码的问题了,编码本质是编码表,值和符号的映射,内存里面存储的都是ASCII值,不能存储符号,
ASCII编码表编码的是老美的文字,后来计算机向全世界推广,肯定要显示全世界的文字。
一个汉字就是一个符号,值和汉字怎么映射呢?
unicode编码方案,可以编码全世界的文字,里面有3大方案,UTF-8(以1个字节为单位)最常用,省空间、UTF-16(以2个字节为单位)、UTF-32(以4个字节为单位)。GBK能编码我们的文字。GBK类比UTF-8。
字符串就是用来存储各种类型的文字的,文字编码表主要采用unicode的UTF-8(以1个字节为单位)方案,所以用:
分别对应:
string(1个字节的char)
u16string(2个字节的char)、u32string(4个字节的char),它们里面与string的实现很相似的。
wstring对应宽字符,一般是2个字节,但是不太规范,由于历史发展的原因,具体是几个字节还不太清楚,有可能是2个字节,有可能是4个字节。根据不同的平台而不同。早期只有string和wstring,由于在不同的平台下实现不同,C++11又出了u16string和u32string。
cpp
int main()
{
/*std::string s1("11111111111111111111111111");
std::string s2("22222");
cout << sizeof(s1) << endl;
cout << sizeof(s2) << endl;*/
char buff1[] = "abcd";
cout << buff1 << endl;
char buff2[] = "大家好";
cout << buff2 << endl;
buff2[1]++;
cout << buff2 << endl;
buff2[1]++;
cout << buff2 << endl;
buff2[1]++;
cout << buff2 << endl;
return 0;
}
