目录
[operator[ ]](#operator[ ])
[push_back(char c)](#push_back(char c))
[reserve(size_t n = 0)](#reserve(size_t n = 0))
[结论: C语言的字符数组,以\0算中止长度(strlen),但string对象的拷贝不以\0来中止,只看size来中止](#结论: C语言的字符数组,以\0算中止长度(strlen),但string对象的拷贝不以\0来中止,只看size来中止)
1.string基本知识的回顾
参见文章:
CC12.【C++ Cont】string类字符串的创建、输入、访问和size函数
CC13.【C++ Cont】初识string类字符串的迭代器
CC14.【C++ Cont】string类字符串的push_back、pop_back、字符串+=与+运算和insert
CC15.【C++ Cont】string类字符串的find和substr函数
CC16.【C++ Cont】string类字符串的关系运算和与string有关的函数
CD34.【C++ Dev】STL库的string的使用 (上)
2.简单的模拟实现
由于STL库实现的string比较复杂,这里实现一个"微缩"版本的string
准备操作
自制一个string.h(不是C语言的头文件),包含到main.cpp中,来模拟正常使用STL库的string需要包含对于头文件<string>的操作
cpp
//string.h
#pragma once
namespace mystl
{
class string
{
};
}
cpp
//main.cpp
#include "string.h"
using namespace mystl;
int main()
{
return 0;
}
代码实现
成员变量
一个指针指向字符串,一个变量size存储string的大小,另一个变量capacity存储string的空间
粗略按照STL的标准:
cpp
class string
{
//......
private:
char* _str;
size_t _size;
size_t _capacity;
};
构造函数
C风格构造的函数
按照声明的顺序来写初始化成员列表,可修改声明的顺序
cpp
string(const char* str)
:_size(strlen(str))
, _capacity(_size+10)
, _str(new char[_capacity+1])
{
strcpy(_str, str);
}
注意参数初始化的值:
由于_size不计入字符串结尾的\0,因此_size的值等于strlen(str)
而capacity是最多能存储有效字符的个数,这里_capacity初始化为_size+10,多了10个字符的有效空间
注意_str(new char[_capacity+1])的_capacity**+1** ,这里一定要写+1!+1是给字符串结尾的\0,而\0不计入有效字符
(注strcpy函数的讲解在51.【C语言】字符函数和字符串函数(strcpy函数)文章)
初始化成员的顺序_size-->_capacity-->_str
如果new申请失败,在构造函数外捕获即可:
测试代码:
cpp
void test1()
{
try
{
string s("teststring");
return;
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
}
下断点到return,查看string的构造情况:

正好是10个cd,是多开辟了10个有效字符的空间

无参构造函数
cpp
string()
:_size(0)
, _capacity(0)
, _str(new char[1])
{
_str = '\0';
}
这里的**_str不能初始化为nullptr**,否则析构函数会出现问题,只开辟一个字节的空间,填上'\0'即可
C风格构造的函数与无参构造函数合二为一
对C风格构造的函数进行改造,带缺省参数即可
cpp
string(const char* str="")//""表示一个\0
:_size(strlen(str))
, _capacity(_size+10)
, _str(new char[_capacity+1])
{
strcpy(_str, str);
}
测试代码:
cpp
void test2()
{
string s;
return;
}
监视窗口:

析构函数
cpp
~string()
{
delete _str;
_str = nullptr;
_size = _capacity = 0;
}
注意:delete不会自动将_str置空,需要手动对_str置空
c_str()

标准库的c_str的返回类型为const char*
cpp
const char* c_str() const
{
return _str;
}
函数名后要写const,因为c_str成员函数没有对_str进行修改
size()
cpp
size_t size() const
{
return _size;
}
函数名后要写const,因为c_str成员函数没有对_size进行修改
operator[ ]
cplusplus网上给了两个版本:

返回类型为char&可读可写,而返回类型为const char&只可读
可读可写
提供的pos要合法,需要小于_size才可以,因此可以先断言,参数和返回值类型和STL保持一致
cpp
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
只可读
提供的pos要合法,需要小于_size才可以,因此可以先断言,参数和返回值类型和STL保持一致
注意像下面这样写是有问题的:

会导致两个operator[ ]的参数列表相同,因此需要使用const修饰隐藏的this指针
cpp
const char& operator[ ](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
测试代码:
cpp
void test3()
{
string s1("teststring");
const string s2("stringtest");
for (size_t pos1 = 0, pos1 < s1.size(); pos1++)
{
s1[pos1]++;
std::cout << s1[pos1];
}
std::cout << std::endl;
for (size_t pos2 = 0, pos2 < s1.size(); pos2++)
{
std::cout << s1[pos2];
}
}
int main()
{
test3();
return 0;
}
运行结果:
cpp
void test3()
{
string s1("teststring");
const string s2("stringtest");
for (size_t pos1 = 0; pos1 < s1.size(); pos1++)
{
s1[pos1]++;
std::cout << s1[pos1];
}
std::cout << std::endl;
for (size_t pos2 = 0; pos2 < s1.size(); pos2++)
{
std::cout << s2[pos2];
}
}
int main()
{
test3();
return 0;
}
运行结果:

iterator(指针版)
由CD35.【C++ Dev】STL库的string的使用 (中)文章提到的:
迭代器即iterator,是像指针一样的类型,它有可能是 指针,也有可能不是 指针
为了简单起见,现按指针方式实现,使用typedef重定义即可:
cpp
typedef char* iterator;
typedef const char* const_iterator;
begin()和end()
和STL保持一致,各自实现const修饰和没有用const修饰的,和STL的要求保持一致:


注: past是过去的的意思,那么past-the-end character指的是最后一个字符之后的字符
cpp
iterator begin()
{
return _str;
}
const_iterator begin() const
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator end() const
{
return _str + _size;
}
测试代码1:范围for
范围for会自动调用迭代器begin()和end()并解引用迭代器:
cpp
void test4()
{
string s1("teststring");
const string s2("stringtest");
for (char it1 : s1)
std::cout << it1;
std::cout << std::endl;
for (char it2 : s2)
std::cout << it2;
}
反汇编代码:

运行结果:

测试代码2:手动使用迭代器
cpp
void test5()
{
string s1("teststring");
const string s2("stringtest");
for (string::iterator it = s1.begin(); it < s1.end(); it++)
std::cout << *it;
std::cout << std::endl;
for (string::const_iterator it = s2.begin(); it < s2.end(); it++)
std::cout << *it;
}
运行结果:
push_back(char c)
依照STL标准:

尾插前先考虑需不需要扩容,C++扩容没有像realloc这样的函数,需要手动操作,当_size==_capacity时扩容,不同STL的扩容方案不同,这里展示二倍扩容:使用reserve函数对开辟新空间,修改capacity即可:
reserve(size_t n = 0)

为了简单起见,只考虑n>_capacity的情况:
步骤:开辟新空间-->拷贝旧空间的数据到新空间-->释放旧空间-->修改_str指针和_capacity
cpp
void reserve(size_t n = 0)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//+1是为\0准备的
strcpy(tmp, _str);
delete[ ] _str;
_str = tmp;
_capacity = n;
}
}
提问: 这样写有没有问题?
答:有隐患,因为strcpy只拷贝\0以前的字符,但标准库实现的reserve函数停止拷贝的条件是复制位置达到_size
对比STL的测试代码:
cpp
void test6()
{
std::string str("abc");
str.push_back('\0');
str.push_back('1');
std::string copy_str = str;//调用拷贝构造函数
return;
}
下断点到return;,监视窗口查看:

结论: C语言的字符数组,以\0算中止长度(strlen),但string对象的拷贝不以\0来中止,只看size来中止
那么在实现reserve函数时就不能使用strcpy,应该用内存拷贝函数:memcpy函数或memove函数,但之前在58.【C语言】内存函数(memcpy函数)文章讲过:
为了避免溢出,由destination和source指针指向的数组应该至少为num个字节,而且两者不能重叠(对于重叠的内存块,memmove是一种更安全的方法)
则使用memove来修改reserve函数:
(注:reserve只是扩容,而resize是扩容+填值初始化(默认填\0) resize会让size和capacity都变(size变的原因是填值初始化),这在CD36.【C++ Dev】STL库的string的使用 (下)文章提到过)
cpp
void reserve(size_t n = 0)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//+1是为\0准备的
memmove(tmp, _str,_size+1);//+1是为\0准备的
delete[ ] _str;
_str = tmp;
_capacity = n;
}
}
继续写push_back函数:
直接写下面的代码会有问题:
cpp
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity * 2);
}
_str[_size++] = c;
_str[_size] = '\0';
}
*注: 如果是空字符串,_size==0,尾插不会出现问题,因为_capacity最小为10,见之前的代码
测试代码:
cpp
void test7()
{
string str;
str.push_back('a');
str.push_back('\0');
str.push_back('1');
str.push_back('3');
str.push_back('\0');
str.push_back('5');
return;
}
下断点到return,内存窗口查看:
