目录
- 1. 引子
- 2. 自实现string类功能模块
- 3. string类功能模块的具体实现
- 3.1 默认成员函数
- 3.2 遍历访问相关成员函数
- 3.3 信息插入相关成员函数
- 3.4 信息删除
- 3.5 信息查找
- 3.6 非成员函数
- 3.7 杂项成员函数
- 4. 补充知识
1. 引子
- 通过对string类的初步学习,没有对知识进行较深度学习了解剖析,只是囫囵吞枣地学会其的使用方式。
- 仅仅掌握string类的使用方式,对我们熟练掌握使用STL的相关容器及其背后的深层知识是远远不够的。
- 因此,我们来通过模拟尝试自实现的方式,对string类背后的知识与编程思想做一个更深入的学习掌握。
2. 自实现string类功能模块
- 默认成员函数:构造,拷贝构造,析构,operator=运算符重载
- 杂类成员函数:size,c_str,swap,substr
- 遍历访问相关成员函数:operator[],const_operator[],iterator(迭代器)
- 信息插入,扩容相关成员函数:append,push_back,operator+=,reserve,insert
- 信息删除相关成员函数:erase
- 信息查找相关成员函数:find
- 非成员函数:operator+,<<,>>
3. string类功能模块的具体实现
3.1 默认成员函数
string类的结构,成员变量
class string
{
private:
//指向存储数据地址的指针
char* _str;
//能够存储有效字符的容量
int _capacity;
//有效字符的长度
int _size;
//正常定义方式:类内声明,类外声明类域定义初始化
//特殊定义方式
const static size_t npos = -1;
};
1. 构造函数
- 字符串构造
<1> 使用初始化列表的构造方式,初始化容量与size都需要使用strlen函数计算一遍字符串长度
<2> strlen计算字符串长度的方式为暴力遍历法,多次调用会使得效率低下
<3> 先用strlen初始化capacity,再使用capacity初始化其他两个成员变量的方式,会使得成员变量的定义方式变得死板
//字符串构造
string(const char* str)
{
//开辟空间
int len = strlen(str);
_str = new char[len + 1];
//拷贝
strcpy(_str, str);
//处理尾部
_str[len] = '\0';
_capacity = len;
_size = len;
}
- 无参构造
<1> 无参构造时,初始化str不能使用空指针,当我们使用流插入操作符对str的内容进行打印时,cout会将其自动默认为字符串进行打印,对其进行解引用,而对空指针进行解引用会出现bug
<2> 无参构造时,不意味着指针为空,无参构造时只是有效字符的长度为0,因,我们可以在空间中存储一个’\0’字符进行标识
//无参构造,空字符串
string()
{
_str = new char[1]{ '\0' };
_capacity = 0;
_size = 0;
}
2. 析构函数
~string()
{
delete[] _str;
_capacity = 0;
_size = 0;
}
3. 拷贝构造函数
//string(const string& str)
//{
// _str = new char[str._capacity + 1];
// strncpy(_str, str._str, str._size);
//
// _size = str._size;
// _capacity = str._capacity;
//
// _str[_size] = '\0';
//}
string::string(const string& str)
{
//现代写法
string tmp(str._str);
swap(tmp);
}
4. operator=运算符重载
//string& string::operator=(const string& str)
//{
// //优化:
// //参数变为string
// //再将拷贝形参与当前string进行交换
// char* tmp = new char[str._size + 1];
// strcpy(tmp, str._str);
// _size = str._size;
// _capacity = str._capacity;
// delete[] _str;
// _str = tmp;
// return *this;
//}
string& string::operator=(string str)
{
//现代写法
swap(str);
return *this;
}
3.2 遍历访问相关成员函数
1. operator[]
- operator[]的访问方式需要支持普通string类与const修饰的string类的调用,因此,要进行重载两次
//普通string类调用
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//const修饰的string类调用
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
2. iterator
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
//最后一个字符后一位
return _str + _size;
}
3. const修饰的iterator
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
4. 范围for的访问方式
- 此遍历方式的底层实现为,将迭代器的遍历方式进行替换,当迭代器线管函数的名与库中不同时,范围for的遍历方式也不能正常使用(err:Begin())
for(auto e : s)
{
cout << e;
}
cout << endl;
3.3 信息插入相关成员函数
1. 扩容成员函数reserve
- 将string类的空间容量进行调整,当指定参数大于当前空间容量时进行扩容,小于时不会进行缩容
//插入相关
void reserve(int size)
{
if (size > _capacity )
{
//申请一段额外的空间
//失败抛异常
char* tmp = new char[size + 1];
//将值进行拷贝
strncpy(tmp, _str, _size);
//销毁原空间
delete[] _str;
//调整
_str = tmp;
_capacity = size;
}
}
2. push_back
void push_back(const char& c)
{
if (_capacity == _size)
{
reserve(_size + 1);
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
}
3. append
- 拼接一串字符串
void append(const char* str)
{
int len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size);
}
strncpy(_str + _size, str, len);
_size += len;
_str[_size] = '\0';
}
- 拼接参数string类的内容
void append(const string& s)
{
int len = s.size();
if (len + _size > _capacity)
{
reserve(len + _size);
}
strncpy(_str + _size, s.c_str(), len);
_size += len;
_str[_size] = '\0';
}
4. insert
- 给指定位置插入一个字符
//在pos位置插入一个字符
void insert(size_t pos, const char& c)
{
assert(pos < _size);
if (_size == _capacity)
{
reserve(_size + 1);
}
//当pos为0时
int end = _size;
//pos类型为size_t,比较时,end隐式类型转换为size_t类型
//当插入位置为0时,会出现bug
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = c;
_size++;
_str[_size] = '\0';
}
- 给指定位置插入一串字符串
//在pos位置插入一串字符串
void insert(size_t pos, const char* str)
{
assert(pos < _size);
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//当pos为0时
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
_str[_size] = '\0';
}
5. operator+=
- 在当前string类后拼接参数string类的内容
string& operator+=(const string& str)
{
if (_size + str._size > _capacity)
{
reserve(_size + str._size + 1);
}
strncpy(_str + _size, str._str, str._size);
_size += str._size;
_str[_size] = '\0';
return *this;
}
- 在当前string类后拼接参数字符串
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
- 拼接字符(复用)
string& string::operator+=(const char c)
{
push_back(c);
return *this;
}
3.4 信息删除
1. erase
- 从指定下标开始,向后删除指定个元素
//在pos位置插入一串字符串
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
//直接添加'\0'
if (len == npos || pos + len > _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
//连'\0'一起拷贝
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
3.5 信息查找
1. find
- 从指定位置开始向后查找指定的字符
size_t find(const char& c, size_t pos = 0)
{
assert(pos < _size);
int i = pos;
while (i < _size)
{
if (_str[i] == c)
{
return i;
}
i++;
}
return npos;
}
- 从指定位置开始,向后查找指定字符串
//查找字符串
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
char* cur = strstr(_str + pos, str);
if (cur == nullptr)
{
return npos;
}
return cur - _str;
}
3.6 非成员函数
1. operator<<
- 流插入操作符重载,非成员函数,非友元,非静态成员函数
- 间接访问方式,不直接访问成员变量
ostream& operator<<(ostream& out, const string& str)
{
//将每一个字符的拷贝插入到流中
for (auto e : str)
{
out << e;
}
return out;
}
2. operator>>
- 流提取操作重载
istream& operator>>(istream& in, string& str)
{
str.clear();
char data[128] = "";
//从流中获取字符
char ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n')
{
data[i++] = ch;
//溢出,截断
if (i == 127)
{
data[i] = '\0';
str += data;
break;
}
ch = in.get();
}
if (i < 127)
{
data[i] = '\0';
str += data;
}
return in;
}
3. operator+
- 返回一个临时的string类,其内容为当前string类与参数字符串拼接
string operator+(const string& str1, const string& str2)
{
string tmp(str1);
return tmp += str2;
}
string operator+(const string& str1, const char* str2)
{
string tmp(str1);
return tmp += str2;
}
4. swap
- 非成员函数swap,交换两个string类,优先级高于算法库中swap模板
void swap(string& str1, string& str2)
{
//会生成中间变量
string tmp = str1;
str1 = str2;
str2 = tmp;
}
3.7 杂项成员函数
1. c_str
- 以C语言类型的字符串方式返回string类的内容
const char* c_str() const
{
return _str;
}
2. substr
- 返回内容为当前string类子串的string类
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
string tmp;
int end = pos + len;
if (pos + len > _size || len == npos)
{
end = _size;
}
//先预先扩容
reserve(len + 1);
for (int i = pos; i < end; i++)
{
tmp += _str[i];
}
return tmp;
}
3. swap
- 成员函数swap,交换两者数据指针,效率高
void swap(string& str)
{
//直接调用库函数
std::swap(_str, str._str);
std::swap(_capacity, str._capacity);
std::swap(_size, str._size);
}
4. 补充知识
- Linux下g++编译器对于string类的实现
<1>string类的结构
<2>拷贝构造方式:浅拷贝 + 引用计数 + 写时拷贝
1. Linux环境g++编译器string类的结构
- string类的成员函数只有一个指针(8字节),指针指向的结构,如下图:
2. 引用计数与浅拷贝
- <1> 当我们进行拷贝构造时,先只进行浅拷贝,使得拷贝而得的新对象的数据指针与原对象指向同一块数据空间
<2> 当新创建的对象的被调用数据需要修改时再进行拷贝,而后修改(写实拷贝)
<3> 当一块数据空间的引用计数清零时,才会进行指针的销毁
<4> 这样的拷贝方式,如果拷贝构造得到对象不被修改时会提高效率,较少开销