目录
1.类的声明
2.成员变量的确定
3.构造函数与析构函数
4.赋值运算符重载
5.流插入运算符重载
6.reserve()的实现
7.push_back()的实现
8.append()的实现
9.+=运算符重载
10.[]运算符重载
11.迭代器的实现
12.insert()的实现
13.erase()的实现
14.find()的实现
15.拷贝构造与赋值运算符重载的现代写法
16.完整代码
与C语言模拟实现
那么我们的代码可以这么写:
//string.h 头文件
namespace lzw //使用命名空间与标准库隔离
{
class string
{
public:
//成员函数
private:
//成员变量
};
}
我们知道string类是其原生模板被char类型实例化的结果,也就是说我们需要char类型的数组或者指针(指向堆的某块空间)用来存储字符串,像顺序表一样,我们需要可以表示有效字符个数的变量以及表示当前除'\0'以外的容量的变量。
那么成员变量就确定了:
//成员变量
char* _str; //指向堆的某块空间的指针
size_t _size; //表有效字符的个数
size_t _capacity; //除'\0'外的可用容量
我们在使用标准库的string类时,最常用的初始化的方式有三种:不使用任何参数构造、使用字符串构造、使用对象构造。
那么构造函数我们便可以这样实现:
string(const char* str = "")
{
_size = _capacity = strlen(str);
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s) //使用对象初始化就变成拷贝构造了
{
_size = _capacity = s._size;
_str = new char[_capacity + 1]; //一定是深拷贝
strcpy(_str, s._str);
}
第一个构造函数我们使用了缺省参数,也就是给了一个空字符串。空字符串实际上并不为空,还有一个隐藏的'\0',后来用 strcpy 拷贝至我们自己设计的成员变量指向的空间中(包括'\0'一起拷贝),所以我们看似没有给定'\0'实际上我们已经悄悄的给了。
那么析构函数的设计就是与构造函数的逻辑相反了:
~string()
{
delete[] _str;
_size = _capacity = 0;
}
虽然我们可以使用 = 来给给stirng对象赋值一个字符串,这是因为隐式类型转换的缘故,如果构造函数前面加上了 explicit 关键字的话,隐式类型转换就不能使用了。
即使我们可以使用隐式类型转换来给string对象赋值一个字符串,但我们依然要提供一个 =运算符重载来让对象与对象之间赋值。我么不写不依然可以使用赋值运算符重载吗?记住了,我们不显示定义赋值运算符重载,编译器会自动生成一个进行浅拷贝的赋值运算符重载。
string& operator=(const string& s)
{
if (this != &s) //确保不是自我赋值
{
char* tmp = new char[s._capacity + 1]; //开辟空间
strcpy(tmp, s._str); //拷贝字符串
delete[] _str; //改变_str,指向新的空间
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
这个运算符重载就可以明显感受到C++相比C语言的优越性了,我们不需要像在C语言中输出结构体的成员时那么辛苦了,我们只需要简单的使用一个运算符重载。
friend std::ostream& operator<<(std::ostream& out, const string& s)
{
out << s._str;
return out;
}
这个函数可谓是极其重要的函数,我们在模拟实现的时候许多地方会用到这个函数。原生reserve()作用是改变对象的容量大小,我们在模拟实现的时候也要实现这个功能。因为很多地方,例如push_back、insert、甚至+=运算符重载时,都会涉及到对象的元素个数变化,一旦变化容量就要随之变化,否则就会出现元素个数多于容量的尴尬场面。
首先设计这个函数是一个合理并且明智的选择:
void reserve(size_t n)
{
char* tmp = new char[n + 1]; //C++没有类似realloc的函数
strcpy(tmp, _str); //所以统一使用异地扩容办法
delete[] _str;
_str = tmp;
_capacity = n; //注意我们的_capacity描述的是不包括'\0'的容量
}
有了reserve()的支持,那么尾插字符的工作就轻松多了:
void push_back(char ch)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);;
}
_str[_size++] = ch;
_str[_size] = '\0'; //对于尾插字符,我们需要手动放置'\0'
}
现在让我们复习一下顺序表或者说学一个新东西:为什么要定义 newcapacity 这个变量?
如果我们实例对象时没有给定任何参数,那么成员变量 _capacity 的值就为0。我们的容量变化理念是呈二倍增长。如果贸然使用值为0的 _capacity 作为 reserve() 函数的实参,那么尾插的工作就无法完成。
string s1; //如果直接用 _capacity*2 作为 reserve() 的参数
s1.push_back('x'); //那么尾插必定是不行的
有了尾插字符,我们也要想办法尾插字符串。实际上过程太过于简单,这里直接放代码了:
void append(const char* str)
{
if (_size + strlen(str) > _capacity)
{
reserve(_size + strlen(str)); //开辟足够的空间
}
strcat(_str, str); //巧用函数
_size += strlen(str); //不忘改变有效字符个数
}
+= 才是最常用的武器。我们已经实现了push_back和append了,剩下的任务就是调用它们了:
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(char ch) //重载
{
push_back(ch);
return *this;
}
[]运算符也是常用的武器。
char& operator[](size_t pos)
{
return _str[pos];
}
char operator[](size_t pos) const //需要兼顾const 修饰的对象
{
return _str[pos];
}
我们一定要知道:迭代器的很多行为与指针类似。或者说,我们完全可以把迭代器当成指针来用。因为迭代器可以代表指向某个元素,指针也可以;迭代器可以更新到下一个元素的位置,指针也可以;迭代器可以通过解引用来找到指向的元素,指针也可以。
typedef char* iterator;
iterator begin()
{
return _str; //数组名就是首元素地址
}
iterator end()
{
return _str + _size; //指向最后一个元素的下一个位置,即'\0'
}
iterator begin() const
{
return _str;
}
iterator end() const
{
return _str + _size;
}
兜兜转转又到了顺序表的位置挪动,插入的类型有两个:一是字符,而是字符串。至于代码的算法,还希望读者亲自体会。
string& insert(size_t pos, char ch)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
} //检查容量是否足够
int end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
} //数据的挪动
_str[pos] = ch;
_size += 1;
return *this;
}
string& insert(size_t pos, const char* str)
{
if (_size + strlen(str) > _capacity)
{
reserve(_size + strlen(str));
}
int end = _size + strlen(str);
while (end > pos)
{
_str[end] = _str[end - strlen(str)];
--end;
}
strncpy(_str + pos, str,strlen(str));
_size += strlen(str);
return *this;
}
这个函数的功能与insert()相反。不过这个函数涉及到一个概念——npos。npos是一个无符号的数,是size_t类型的最大值。也就是 size_t 的比特位全为1,所以赋值可以直接给它赋-1(原、反、补规则)。
如果不加任何修饰地作为成员变量,虽然可以,但还有优化的空间。首先 npos 属于我们的string类,而且它的值不会改变,所以我们可以用const修饰;其次我们完全可以像函数那样把 npos 放在公共空间,也就是使用 static 修饰。不过有人又要问了,static 修饰的成员变量要在类外定义, 我懒得去定义。这里我告诉大家,C++11中,const修饰的static成员变量(只能是整型),可以直接给定缺省值。
那么我们的erase就可以出来了:
string& erase(size_t pos =0, size_t len = npos) //如果不传参,默认清空字符串
{
assert(pos < _size); //删除的起始位置必定在有效字符里
if (len >= _size - pos)
{
_str[pos] = '\0'; //起始位置之后的都要删除
}
else
{
size_t end = pos + len;
strcpy(_str + pos, _str + end);
_size -= len;
}
return *this;
}
const static size_t npos = -1; //成员变量
上一篇我们讲过这是个非常实用的接口。原生的string类的find()函数可以查找字符、字符串、对象并返回第一次出现的位置,如果没有就返回npos。我们也要实现这个功能。
其中的算法是比较简单的,我认为各位读者花三秒钟就能理清楚其中的思路:
size_t find(char ch, size_t pos = 0) const //pos是查找的起始位置
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
pos++;
}
return npos;
}
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
char* ret = strstr(_str+pos, str); //注意开始查找的位置
if (ret == nullptr)
{
return npos;
}
else
{
return ret - _str; //指针相减等于元素个数
}
}
size_t find(const string& s, size_t pos = 0) const
{
assert(pos < _size);
char* tmp = strstr(_str+pos, s._str); //注意开始查找的位置
if (tmp == nullptr)
{
return npos;
}
else
{
return tmp - _str;
}
}
即使上面的实现的拷贝构造和赋值运算符能够完成任务,但是这样的写法是非常保守的,体现不出我们作为C++程序员的"功力"。所以现在对拷贝构造和赋值运算符进行整改和升级,代码非常简单,大家仔细分析即可:
//拷贝构造的现代写法
void swap(string& s) //模拟实现了string类提供的swap接口
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str); //先使用一个临时对象构造
swap(tmp); //然后交换内容
//this->swap(tmp); //与上面等同的swap调用
}
//赋值运算符重载的现代写法
string& operator=(string s) //使用传值调用,s就是一个临时对象
{
swap(s); //交换,这里的swap等同与拷贝构造的swap
return *this;
}
因为string类的接口设计的实在是太多了,我们就只挑几个常用来模拟实现一下。如果大家有兴趣,大家可以参照我给的这个头文件,继续往后实现吧!
//string.h 头文件
#include
#include
namespace lzw //使用命名空间与标准库隔离
{
class string
{
public:
//成员函数
typedef char* iterator;
iterator begin()
{
return _str; //数组名就是首元素地址
}
iterator end()
{
return _str + _size; //指向最后一个元素的下一个位置,即'\0'
}
iterator begin() const
{
return _str;
}
iterator end() const
{
return _str + _size;
}
string(const char* str = "")
{
_size = _capacity = strlen(str);
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//string(const string& s) //使用对象初始化就变成拷贝构造了
//{
// _size = _capacity = s._size;
// _str = new char[_capacity + 1]; //一定是深拷贝
// strcpy(_str, s._str);
//}
//拷贝构造的现代写法
void swap(string& s) //模拟实现了string类提供的swap接口
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str); //先使用一个临时对象构造
swap(tmp); //然后交换内容
//this->swap(tmp); //与上面等同的swap调用
}
~string()
{
delete[] _str;
_size = _capacity = 0;
}
friend std::ostream& operator<<(std::ostream& out, const string& s)
{
out << s._str;
return out;
}
//string& operator=(const string& s)
//{
// if (this != &s) //确保不是自我赋值
// {
// char* tmp = new char[s._capacity + 1]; //开辟空间
// strcpy(tmp, s._str); //拷贝字符串
// delete[] _str; //改变_str,指向新的空间
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
//赋值运算符重载的现代写法
string& operator=(string s) //使用传值调用,s就是一个临时对象
{
swap(s); //交换,这里的swap等同与拷贝构造的swap
return *this;
}
~string()
{
delete[] _str;
_size = _capacity = 0;
}
friend std::ostream& operator<<(std::ostream& out, const string& s)
{
out << s._str;
return out;
}
void reserve(size_t n)
{
char* tmp = new char[n + 1]; //C++没有类似realloc的函数
strcpy(tmp, _str); //所以统一使用异地扩容办法
delete[] _str;
_str = tmp;
_capacity = n; //注意我们的_capacity描述的是不包括'\0'的容量
}
void push_back(char ch)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);;
}
_str[_size++] = ch;
_str[_size] = '\0'; //对于尾插字符,我们需要手动放置'\0'
}
void append(const char* str)
{
if (_size + strlen(str) > _capacity)
{
reserve(_size + strlen(str)); //开辟足够的空间
}
strcat(_str, str); //巧用函数
_size += strlen(str); //不忘改变有效字符个数
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(char ch) //重载
{
push_back(ch);
return *this;
}
char& operator[](size_t pos)
{
return _str[pos];
}
char& operator[](size_t pos) const //需要兼顾const 修饰的对象
{
return _str[pos];
}
string& insert(size_t pos, char ch)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
} //检查容量是否足够
int end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
} //数据的挪动
_str[pos] = ch;
_size += 1;
return *this;
}
string& insert(size_t pos, const char* str)
{
if (_size + strlen(str) > _capacity)
{
reserve(_size + strlen(str));
}
int end = _size + strlen(str);
while (end > pos)
{
_str[end] = _str[end - strlen(str)];
--end;
}
strncpy(_str + pos, str,strlen(str));
_size += strlen(str);
return *this;
}
string& erase(size_t pos =0, size_t len = npos) //如果不传参,默认清空字符串
{
assert(pos < _size); //删除的起始位置必定在有效字符里
if (len >= _size - pos)
{
_str[pos] = '\0'; //起始位置之后的都要删除
}
else
{
size_t end = pos + len;
strcpy(_str + pos, _str + end);
_size -= len;
}
return *this;
}
size_t find(char ch, size_t pos = 0) const //pos是查找的起始位置
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
pos++;
}
return npos;
}
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
char* ret = strstr(_str+pos, str); //注意开始查找的位置
if (ret == nullptr)
{
return npos;
}
else
{
return ret - _str; //指针相减等于元素个数
}
}
size_t find(const string& s, size_t pos = 0) const
{
assert(pos < _size);
char* tmp = strstr(_str+pos, s._str); //注意开始查找的位置
if (tmp == nullptr)
{
return npos;
}
else
{
return tmp - _str;
}
}
private:
//成员变量
char* _str; //指向堆的某块空间的指针
size_t _size; //表有效字符的个数
size_t _capacity; //除'\0'外的可用容量
const static size_t npos = -1;
};
}