本篇文章是对string底层库函数的具体模拟实现,目的就是为了更好的了解string 的底层 , C++是一个极度追求效率的一个语言,我们需要了解为啥要少用 某个函数,为啥这个函数就坑了程序,导致程序的效率降低。
本篇文章,我们会像上篇一样,从是什么,为什么,怎么用来告诉大家string隐藏的底层逻辑到底是怎么样的,那么直接开始。
目录
一、经典的string类问题
对于自己编写的 string , 为了和库里的 string 进行区分 , 我们可以使用 命名空间域 namespace进行隔离。
string.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
using namespace std;
namespace CYH
{
class string
{
public:
//这里我们一般只留一个构造 - 全缺省的默认构造(最后面会有)
string();
~string();
string(const char* str);
//比较短的代码就放在类里,内联函数,不用建立函数栈帧了,提高效率
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
string.cpp
#include"string.h"
namespace CYH
{
string::string()
:_str(new char[1] {'\0'})
, _size(0)
, _capacity(0)
{
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
string::string(const char* str)
//+1 的原因是还有一个 \0 要保存
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
}
问题1:为什么无参构造的初始化列表的 c_str 需要 new char[1] {'\0'} ?
如果我们按照往常一样, 把_str 置为 nullptr 。当你调用 c_str( ) 的时候 , 编译器认为你需要打印的不是指针,而是字符串 , 字符串的结束符标志位 '\n' 。 初始化如果为空,就会导致编译器进行空指针的解引用,从而导致程序崩溃。
问题2:不是说尽量在初始化列表里面初始化吗 ? 可是在有参构造这里,为什么除了 _size , 其他的都在函数体内进行初始化 ?
平常我们就是按照如下的方法来初始化变量 ,尽可能的在 初始化列表里面初始化 变量 。
但是 strlen 是在运行时运算的 ,时间复杂度为 O(n) 这里用三个 O(n) 。
这里我们一想,那就先计算 _size , 然后其他的都调用_size这样效率就提高了,但是,这样是不对的。这是一个大坑。
我们乍一看,好像效率确实有很大的提升,但是存在一定的安全隐患,比如空间没开够 , 数组越界 , 野指针析构 , 程序就崩溃了。
string::string(const char* str)
//+1 的原因是还有一个 \0 要保存
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
比如我们这里提到的_size需要加一,表明这里的size此时是0,那么也就会达到我所说的越界的问题。
二、模拟实现
2.1构造函数
首先实现传递常量字符串实现构造函数。遇见\0停止构造,\0前面为一段字符。
思路:内置类型可以在初始化列表以及函数体实现初始化,自定义类型在初始化列表初始化。
易错点:初始化列表一定要按照内置类型的声明顺序进行初始化操作,对常量字符串要在堆上重新创建空间存储,便于后面的操作。
string(const char* str) //权限平移,加上cosnt
{
_str = new char[strlen(str) + 1];
//strcpy(_str, str);
memcpy(_str,s._str._size+1);
_size = strlen(str);//_size为有效字符数量不包含 \0
_capacity = _size;//对于capacity来说也是以有效空间(可以存储多少有效字符),开辟空间已经预留\0位置。
}
对无参的默认构造编写
这个上面讲过了,这里直接给出代码
string()
{
_str = new char[1];
str[0] = '\0';//开辟一个空间储存 \0
_size = _capacity = 0;
}
2.2.c_str函数
为普通对象以及常量对象提供的提供对字符串内容的只读访问;前面const是为了防止修改,后面const是为了权限的平移和缩小保证正常传值。
const char* c_str() const;
模拟:
const char* c_str() const // 确保字符串无法修改,返回为const修饰的常量
{
return _str;
}
2.3.析构函数
思路:释放空间,内置类型置0或者nullptr
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
2.4.size字符串大小
size_t size() const
{
return _size;
}
2.5.operator[ ]符号重载
思路:字符串本质为有序的数组,为了防止访问越界加上assert限制访问位置,按照访问字符数组的方式,返回对应位置的字符即可。
易错点:如果要在字符串的外部修改字符串 ,需要返回值为引用。
引用返回的好处:返回引用不需要拷贝字符,直接返回内存地址的别名,避免了不必要的内存开销,尤其在处理长字符串时能提升性能。
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
2.6.迭代器
思路:迭代器作为访问方式的一种,而且迭代器可以是指针或者内部类,但是内部类实现太麻烦,因此这里使用typedef 将char* 名为iterator较为简单。end()指向字符串最后有效字符的下一个位置以及begin()函数指向第一个字符的位置。
//迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
2.7.push_back函数(含reserve函数)
思路:检查容量和有效字符个数,小于容量先扩容二倍。然后对有效字符的后一个位置赋值。
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];// 开辟+1 为了放\0
//strcpy(tmp, _str);
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
tmp = nullptr;
}
}
void push_back(const char a)
{
if (_size == _capacity)
reserve(_capacity * 2);
_str[_size] = a;
_size++;
_str[_size] = '\0';
}
2.8.append函数
思路:检测追加字符串的大小,然后进行扩容检测,在复用push_back尾插字符。
void append(const char* str)
{
size_t len = strlen(str);
//检测容量
if (len + _size > _capacity)
{
reserve(len + _size);
}
//插入数据 复用push_back 左开右闭 len 位置为\0。
//size_t i = 0;
//for (i = 0; i < len ; i++)
//{
// push_back(str[i]);
//}
memcpy(_str + _size, str, len + 1 );
_size += len;
}
2.9.insert函数
插入一个字符串
注意:挪动数据时要注意类型转换
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size);
}
//挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
//插入数据
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
插入多个字符
思路同上,但是
当操作符两边的操作数的类型不一样的时候,会发生类型提升
类型提升核心规则:
小容量类型向大容量类型提升:
char/short 等小整型会先提升为 int 类型
float 会提升为 double 类型符号处理规则:
有符号和无符号类型混合运算时,会统一提升为无符号类型
但若大容量类型是有符号类型,则可能保留有符号特性在相同宽度(内存大小相等)下,无符号整型能表示的最大值是有符号整型最大值的约 2 倍,整体范围更大 只能表示非负数
void insert(size_t pos, size_t n, const char a)
{
assert(pos <= _size);//确保插入在有效字符附近
//容量
if (_size + n > _capacity)
{
reserve(_size + n);
}
//挪动数据
size_t end = _size;
while (end >= pos && end != npos)//由于size_t类型 end=0之后再-1会导致end变为-1但是对于
//size_t来说是size_t的最大值
//无法正常的进行比较,所以加上end!=npos
{
_str[end + n] = _str[end];
end--;
}
//插入数据
for (size_t i = 0; i < n ; i++)
{
_str[pos + i] = a;
}
_size += n;
}
2.10.erase函数
//移除数据 覆盖删除
void erase(size_t pos,size_t n = npos )
{
assert(pos <= _size);
size_t i = 0;
//分为两种情况
if (pos + n >=_size || n == npos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (i = 0; i < _size - pos; i++)
{
_str[pos + i] = _str[pos + n + i];
}
_size -= n;
}
}
2.11.find函数
//寻找 字符
size_t find(const char ch, size_t pos = 0)
{
assert(pos < _size);//寻找ch的位置,\0位置舍去,就没有等号了
size_t i = 0;
while (i < _size)
{
if (_str[i] == ch)
return i;
i++;
}
return npos;
}
//寻找字符串
size_t find(const char* str, size_t pos = 0)
{
char* tmp = strstr(_str, str);
if (tmp)
{
return tmp - _str;
}
else
{
return npos;
}
}
2.12.operator+= 函数重载
//+=操作符重载
void operator+=(const char ch)
{
push_back(ch);
}
void operator+=(const char* str)
{
append(str);
}
2.13.substr函数
功能:从pos位置开始,取出len长度的字符
思路:npos和len+pos大于_size的情况,需要重置len,然后创建临时tmp字符串,再重新输出 。
//从pos处取出长度为len的字符串
string substr(const size_t pos, size_t len = npos)//函数全部模拟完成后验证此函数
{
assert(pos < _size);
if (len == npos || len + pos > _size)
{
len = _size - pos;
}
size_t i = 0;
string tmp;
reserve(len);
for (i = 0; i < len; i++)
{
tmp += _str[pos + i];
}
return tmp;
}
这里再看一下深浅拷贝的区别:

2.14.拷贝构造(深拷贝)
//拷贝构造函数
string(const string& s)
{
_str = new char[s._capacity + 1];
//strcpy(_str, s._str);
memcpy(_str,s._str._size+1);
_size = s._size;
_capacity = s._capacity;
}
2.15.resize函数
功能:将有效字符的长度扩展或者缩短为 n,多余的位置若有c则填充c,没有c则填充\0;已经存在字符的位置是不能修改的。
思路:分为三种,n小于_size,n大于_size(细分的n大于capacity,n小于capacity)
易错点:插入字符ch的位置和结束的位置。
void resize(size_t n, const char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
2.16.流插入
功能:重载<< 让自定义类型string适用于<< 符号
思路:一种为友元函数,但是对于string来说,\0不能代表结束的标识符,因此会出了错;
建议使用第二种,按照_size的大小输出每一个字符。直到_size的位置
易错点:忽略 \0 不是结束操作符的情况,ostream具有防止拷贝的功能,因此<<重载函数拒绝拷贝构造。因此采用引用参数的格式。
第一种
void operator<<(ostream& out, const string& s)
{
cout << s._str;
}
第二种
// 流插入
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
2.17.流提取
注意:
1.cin为阻塞型接口,其遇见空格以及换行符号,不会读取,默认为输入间隔符号,换行符同时承担着 “输入间隔标识” 和 “输入结束触发” 的双重角色。因此再流提取需要使用istream的成员函数 get来读取符号,其可以读取任何值,包括空格。(getline)
2.每一次的输入,都是新的输入,因此要将原本string中的内容进行覆盖或者清理。
3.随着输入的增加,频繁使用s+=符号,会不断的调用reserve,不断的开空间,导致程序的成本增大,浪费空间和时间。因此采用数组的先行存储,再复制到string上
4.对于std中的cout 会忽略字符前面的空格,因此需要一个循环过滤掉空格
思路:按照注意来,先过滤空格一个换行符号,创建数组,将输入的数据暂存于数组中,然后等数组满了之后或者输入结束之后,将其复制给string中。
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch == ' ' || ch == '\n')
{
ch = in.get();//重新读取
}
char arr[128] = { '\0' };
size_t i = 0;
while( ch != ' '&& ch != '\n')
{
arr[i] = ch;
i++;
if (i == 127)
{
arr[i] = '\0';
s += arr;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
arr[i] = '\0';
s += arr;
}
return in;
}
2.18.关系运算符号
功能:适用于自定义类型string的比较大小。
思路:使用memcmp函数比较,写出>再写出=就可以复用了。
bool operator<(const string& s)
{
size_t size = _size;
if (_size < s._size)
{
size = s._size;
}
int ret = memcmp(_str, s._str, size);
/*if (ret == 0)
{
return _size < s._size ? 1 : 0;
}
else
return ret;*/
return ret == 0 ? _size < s._size : ret < 0;
}
bool operator==(const string& s)
{
return _size == s._size&&
memcmp(_str, s._str, _size) == 0;
}
bool operator<=(const string& s)
{
return *this < s || *(this) == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return *this > s || *(this) == s;
}
2.19.赋值操作符重载
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size,s._size);
std::swap(_capacity,s._capacity);
}
string& operator=(const string& s)
{
string tmp(s);
swap(tmp);
return *this;
}
//简写为
string& operator=(const string s)
{
swap(tmp);
return *this;
}
3515

被折叠的 条评论
为什么被折叠?



