摘要:string类的模拟实现【设计思路+代码示例】;string类底层设计的补充说明【sizeof(string),string类设计上的效率问题:深拷贝的浪费】;扩展【写时拷贝】
目的:为了和 std 中的 string 区分。
namespace Bottle
{
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
};
size_t string::npos = -1;
}
注意:capacity 不算结尾的 '\0' ,但开空间要预留 '\0' 的位置。
示例:
//Constructor
string()//无参的构造函数
:_str(new char[1] {'\0'})
, _size(0)
, _capacity(0)
{
}
string(const char* str = "")//全缺省的构造函数
:_size(strlen(str))
, _capacity(strlen(str))
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
~string()//Destructor
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
1)capacity() ,size()
2)operator[] and const 版本(ps.检查下标的合法性)
3)iterator :用指针实现 → typedef char* iterator;
4)范围for:对 iterator 有严格的要求,本质上就是简单地替换成 iterator ,例如——如果对 begin() 函数改名为 Begin() 都会使范围for不支持。
示例:
typedef char* iterator;
typedef const char* const_iterator;
///
size_t capacity()const
{
return _capacity;
}
size_t size()const
{
return _size;
}
///
//operator[]
char operator[](size_t i)
{
if (i < _size)
return _str[i];
else
{
std::cout << "非法下标" << std::endl;//也可以选择用asset暴力检查
exit(-1);
}
}
char operator[](size_t i)const
{
if (i < _size)
return _str[i];
else
{
std::cout << "非法下标" << std::endl;
exit(-1);
}
}
///
//iterator
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator cbegin()const
{
return _str;
}
const_iterator cend()const
{
return _str + _size;
}
插入数据肯定涉及到要扩容的问题,为了先解决扩容的问题——实现 reserve 函数:
//reserve
void reserve(size_t n)//可以存储的有效数据个数为'n'个
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//多开一个给''\0'
strcpy(tmp, _str);//拷贝数据到新空间(ps.strcpy 会 copy 结尾的'\0')
delete[]_str;//释放旧空间
_str = tmp;//指向新空间
_capacity = n;
}
}
关于扩容的问题:
综上,最终选择如下这样的写法:
//push_back
void push_back(char c)
{
if (_size == _capacity)//check capacity
{
reserve((_capacity == 0) ? 4 : (_capacity * 2));
}
_str[_size] = c;//push data
++_size;
_str[_size] = '\0';
}
//append
void append(const char* str)
{
size_t len = strlen(str);
reserve(_size + len);//check capacity
strcpy(_str + _size, str);//append data
_size += len;
}
复用 push_back 和 append 函数即可。
//operator+=
string& operator+=(const char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
思路:①检查 pos 位置的有效性;②检查容量;③挪动数据;④插入数据(strncpy)。
特殊情况:头插(pos==0)ps.下面的 pos 和 end 都是指下标
①size_t end 是无符号整型,-1即为整型的最大值,以 end >= pos 作为循环判断条件,在 pos ==0 时会出现 “-1>0” 的情况,于是进入死循环;
②即使定义 int end,仍以 end >= pos 作为循环判断条件,>=操作符左右操作数的类型不同,会发生类型转换,通常是比较范围小的向范围大的转换,即在该情况下,有符号整型一般转化为无符号整型。
有两种方式解决上述情况:
①强制类型转换成 int 进行比较;(int)end>=(int)pos;
②让 end 所指数据 将要被挪动到的位置 而不是指将要被挪动的数据。_str[end+len] = _str[end] _str[end] = _str[end - len]
代码示例:
//insert
string& insert(size_t pos, const char c)//在pos位置插入c
{
//check
assert(pos < _size);
//capacity
reserve(_size + 1);
//move
size_t end = _size + 1;
for (; end >= pos + 1; --end)
{
_str[end] = _str[end - 1];
}
//insert
_str[pos] = c;
_size += 1;
return *this;
}
string& insert(size_t pos, const char* str)//在pos位置插入串str
{
//check
assert(pos < _size);
//capacity
size_t len = strlen(str);
reserve(_size + len);
//move
size_t end = _size + len;
for (; end >= pos + len; --end)
{
_str[end] = _str[end - len];
}
//insert
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
//erase
string& erase(size_t pos, size_t len = npos)
{
//check
assert(pos < _size);
if ((pos + len >= _size) || (npos == -1))//如果len足够长,长到删完pos位置后面所有的数据
{
_size = pos;
}
else
{ //数据从后往前挪动覆盖删除
for (size_t i = pos + len; i < _size; ++i)
{
_str[i - len] = _str[i];
}
_size -= len;
}
_str[_size] = '\0';
return *this;
}
注意: (pos + len >= _size) || (npos == -1) 这两个情况 pos + len >= _size 与 npos == -1 要分开判断,因为 -1已经是无符号整型的最大值,再在其上加上数据会溢出——超出无符号整型能存储的最大数值。
这些比较操作符库里实现的是全局函数,而非成员函数,是考虑到除了 string类(char) 以外还要匹配其他的类型(Types),这里我们简单地实现为成员函数。
代码示例:
//Compare
bool operator==(const string& s)const
{
return (strcmp(this->_str, s._str) == 0);
}
bool operator<(const string& s)const
{
return (strcmp(this->_str, s._str) < 0);
}
bool operator>(const string& s)const
{
return (strcmp(this->_str, s._str) > 0);
}
bool operator>=(const string& s)const
{
return ((*this > s) || (*this == s));
}
bool operator<= (const string& s)const
{
return (!(*this > s));
}
不一定要写成友元函数,因为这里不用访问私有函数。
namaspace Bottle
{
class string
{
//……
};
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
}
流提取重载
注意:cin 和 scanf 都是以空格和换行为一段输入的间隔,如下图所示。为了解决下示问题,可以采用 get 函数。get - C++ Reference (cplusplus.com)
流提取代码示例:(注意:流提取之前要清空string对象中原有的数据,不然就会变成 流提取重载 就会变成 尾插数据 )
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();//clear existing data
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
流提取中关于扩容的问题:可以通过创建缓冲区数组优化频繁扩容的问题。先把读到的数据存到缓冲区数组,如果缓冲区数据满了就插入到原本要插入的string类对象中,这样就避免了每次单个单个插入字符而导致的频繁扩容。代码示例如下:
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();//clear existing data
char buff[129] = { 0 };
size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i] = ch;//读到的数据先存到buff数组中
++i;
if (i == 128)//buff数组存满后插入到string& s中
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i != 0)//如果缓冲区数据内还有剩余数据,记得把剩下的数据插入string& s中
{
buff[i] = '\0';
s += buff;
}
return in;
}
resize 改变 size 的大小,分以下三种情况讨论:
后两种情况可以合并,因为即使 reserve(new_size) 当 new_size < capacity 时也不会缩容。
代码示例:
//resize
void resize(const size_t sz, const char c = '\0')
{
if (sz < _size)
{
//delete
_str[_size] = '\0';
_size = sz;
}
else
{
reserve(sz);//检查容量,必要时扩容
//insert
for (size_t i = _size; i < sz; ++i)
{
_str[i] = c;
}
_str[_size] = '\0';
_size = sz;
}
}
代码示例:
// 返回c在string中第一次出现的位置
size_t find(const char c, size_t pos = 0) const
{
assert(pos < _size);//check pos
size_t i = pos;
for (; i < _size; ++i)
{
if (_str[i] == c)
return i;
}
return -1;
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);//check pos
size_t len = strlen(s);
for (size_t i = pos; i + len < _size; ++i)
{
char* p = strstr(_str + i, s);
if (p != nullptr)
{
return p - _str;
}
}
return -1;
}
substr:从 pos 位置取 len 长度的字符串
思路:1. pos+len >= size or len == npos:from pos to size
2. else:from pos to pos+len
注意:传值返回,需要写 拷贝构造 → 涉及深拷贝的问题(详见下一部分 8)深拷贝 )
代码示例:
//从pos位置取len长度的字符串
string substr(const size_t pos, const size_t len = npos)const
{
if ((len == npos) || (pos + len >= _size))
{
string tmp(_str + pos);//从pos往后的数据全部copy
return tmp;
}
else
{
string tmp;
tmp.reserve(len);//check capacity
for (size_t i = 0; i < len; ++i)
{
tmp += _str[pos + i];
}
return tmp;
}
}
代码示例:
string(const string& s)//copy constructor
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
如下图所示,赋值重载函数的实现思路:①开新空间;②将右操作数的数据拷贝到新空间;③释放旧空间——左操作数原本指向的空间;④让做左操作数指向新空间。
注意:自己给自己赋值的情况!(如果第一步就是释放旧空间,而此时是自己给自己赋值时,会导致错误的结果)
代码示例:
string& operator=(const string& s)//assign
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[]_str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
}
上述进行深拷贝行为的写法被成为“传统写法”。下面介绍一种“现代写法”。
ps.这里交换 tmp 和要被构造的对象 *this 要注意一个问题:要被构造的对象存储的数据可能随机值(这里尤指其中的私有成员 _str 存的地址是一个随机地址),拷贝构造函数执行完后,tmp 会调用析构函数,delete[] 一个随机地址会出现问题,所以这个构造函数最好先在初始化列表对成员变量初始化。如下图所示。
注意:其中用到的 swap 函数要自己实现,不建议直接用C++标准库里的 swap 函数。
以上,传统写法和现代写法本质上是一样的,效率上没有区别,只是写法上更简洁。
不同平台下 string类 的底层设计不同。
既然深拷贝导致了浪费,那么我们考虑可不可以浅拷贝。
浅拷贝具有两个问题:
①两个对象中的两个指针指向同一块空间,而导致该空间会被析构两次。
②当我们修改其中一个对象的时候会影响另一个对象。
接下来我们针对以上两个问题做讨论。(以下仅作简单粗略的讨论,仅供了解)
(ps.g++采用的是写时拷贝)
END