在C语言中,关于字符串的函数很杂乱无章,与面向对象语言不太相符。
C++为了更好的管理字符数组,专门写了一个string类,把常用函数都封装在了string类里面。我在这篇博客中将string类模拟实现一下。
1.string是表示字符串的字符串类。
2.该类的接口与常规容器的接口基本相同,再添加一些专门用来操作的string的常规操作。
3.string在底层实际上是:basic_string模板类的别名,typedef basic_string
4.不能操作多个字节或者变长字符的序列。
5.在使用string类时,必须包含#include头文件以及using namespace std;
几点说明:
1、有些接口与C++11有关,我都在声明处标明,如不熟悉,读者可自行跳过。跳过C++11的部分不会影响整体功能的实现。
2、有些接口实现有好几种写法,我会注释掉其他的,保留一种写法。
3、有些接口我没有实现,因为库里的string类实在复杂,读者可自行实现。
4、string类不是模板,适合用分离编译。但是我为了书写方便,没有使用。
5,我用的string类名字跟库里一样,需要封装在命名空间里。
string(const char* str = ""); //构造函数
string(const string& s); //拷贝构造
string(const&& s); //C++11 移动构造
string& operator=(string s); //赋值
string& operator=(string&& s); // C++11 移动赋值
~string(); //析构函数
const size_t size() const;
const size_t capacity()const;
void reserve(size_t n = 0);
void resize(size_t n, char c = '\0');
void clear();
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
void append(const char* str);
void push_back(const char c);
const string operator+(const string& s);
string& operator+=(const string& s);
string& operator+=(const char c);
string& operator+=(const char* str);
void insert(size_t pos, const char c);
void insert(size_t pos, const char* str);
string& erase(size_t pos, size_t len = npos);
size_t find(const char c, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0)const;
const char* c_str()const;
bool operator==(const string& s);
bool operator!=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator<(const string& s);
bool operator<=(const string& s);
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
istream& getline(istream& in, string& s);
注意:
1.在string尾部追加字符时,push_back / append/ += 三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
2.对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。(这一点当string用多了就会发现很重要。)
任何一个类我觉得最先摆在我们面前的一个问题就是:**我的成员变量是哪些?**而不得不说,这是一个不好回答的问题——因为成员变量的确认往往是在成员方法的实现中完成,即,缺少哪个,需要哪个就补充哪个。这就好像冬天到了我们会添加衣服一样。而C++string类的实现有3个成员变量。
_str是个字符指针,指向字符串。_size表示已经存储的有效字符个数。_capacity表示允许存储的最多有效字符数。
为什么我要加上有效两个字呢?因为字符串末尾需要’\0’,而这个字符是无效的。
string
{
public:
// 以下实现的函数除了非成员函数都是在这实现
private:
char* _str;
size_t _size;
size_t _capacity;
}
也许你会对size_t产生疑惑,因为你可能一直在使用(或者是只见过别人使用),但你却没有深究why size_t ?实际上,大多数编译器上size_t只是unsigned int的typedef,深究可以参考why size_t matter
//4大默认成员函数
// : _str(str) 不能这样,因为此时_str指向常量区,不能改
//我们要拷贝构造一个出来。
//strlen会解引用str,所以不能给nullptr,缺省值给空字符串
string(const char* str = "")
{
_size = _capacity = strlen(str);
_str = new char[_size + 1];
strcpy(_str, str);
}
string(const string& s)
:_str(new char[strlen(s._str) + 1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str, s._str);
}
//返回引用因为出了函数作用域对象还在
string& operator=(const string& s)
{
if (this != &s) //避免自己给自己赋值的浪费
{
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
delete _str;
_str = newstr;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
这里还是有几点值得注意的。
1、我将默认的构造函数和含参的构造函数合并写,但是缺省值不能给nullptr,因为下面的strlen就解引用了_str,那会造成空指针的解引用。
2、拷贝构造和赋值要自己写。编译器生成的是值拷贝,也叫做浅拷贝。
3、按照effectiveC++的条款,写赋值函数要防止自己给自己赋值。
传统的写法就是开空间,拷贝数据,释放旧空间等一系列操作。我们也有新的写法。
// C++98的swap写法
/* void swap(string& s){
//加上类限定符,调用std的swap
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
} */
//拷贝构造现代写法
string(const string& s)
:_str(nullptr) //防止tmp析构随机数组
{
string tmp(s._str);
// swap(tmp);
swap(*this, tmp); //C++11因为移动语义 直接使用swap
}
//赋值的第二种写法
/*string& operator=(const string& s){
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}*/
// 赋值的现代写法,pass-by-value
string& operator=(string s) {
if(this != &s)
swap(s);
return *this;
}
//C++11 移动构造和移动赋值
//noexcept 表示不会抛异常
string(string&& s) noexcept
:_str(nullptr)
{
swap(s);
}
string& operator=(string&& s)noexcept
{
if(this != &s)
swap(s);
return *this;
}
实际上我们会发现移动赋值和赋值函数的现代写法几乎一模一样,实际上移动赋值函数不需要写。为什么呢?看看赋值函数的传值方式,如果是一个右值调用operator=,编译器会用移动构造而非拷贝构造去构造s,而这个过程是没有开空间的,然后去执行接下来的代码。也就是说,operator=现代写法的另外一个好处就是将移动赋值也合并了。
const size_t size() const{
return _size;
}
const size_t capacity()const{
return _capacity;
}
//增容 增大capacity到n
void reserve(size_t n = 0){
assert(n >= _capacity);
char* newstr = new char[n + 1]; //开空间
strcpy(newstr, _str); //拷数据
delete _str;
_str = newstr;
_capacity = n;
}
//将size变成n
void resize(size_t n, char c = '\0'){
if (n < _size) {
_size = n;
//(*this)[_size] = '\0';
_str[_size] = '\0';
}
else{
if (n > _capacity)
reserve(n);
while (_size < n){
_str[_size] = c;
++_size;
}
_str[_size] = '\0';
}
}
void clear(){
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
reserve将容量_capacity开到n,如果n小于等于_capacity,什么也不做。resize将空间_size开到n。n < _size,将string长度删除到n;n > _size ,用所给字符初始化后面多余出来的空间。实际上,reserve就是很多成员函数重复出来的新的函数,增加复用性。clear跟析构函数的实现一样,所以可以简化析构函数。
~string(){
clear();
}
char& operator[](size_t pos){
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
//迭代器实际上是char*
typedef char* iterator;
typedef const char* const_iterator;
iterator begin(){
return _str;
}
iterator end(){
return _str + _size;
}
const_iterator begin() const {
return _str;
}
const_iterator end() const {
return _str + _size;
}
string类提供了operator[ ],我们可以用数组的方式遍历字符串。string类也支持迭代器,所以string也支持范围for(范围for底层就是迭代器)。
但是我们发现上面的代码有大量重复,因为一个是const版本,一个是非const版本,需要面对不同的对象。那么有什么办法去重呢?
通常我们的办法是将相同的部分封装成一个私有(private)函数,然后让这两个函数去调用那个私有函数,在后面的代码种你将看到我的这种处理方式。但是这里我提供另外一种方式。这种方式来自EffectiveC++。这里我们不采用封装私有函数的原因是即使这样,也有重复的部分。(即调用那个函数的过程)
char& operator[](size_t pos){
return const_cast<char&>(static_cast<const string&>(*this)[pos]);
}
两次转型 用非const的operator[]去调用const的operator[],第一次将*this转型成const型的,防止无穷递归调用。是安全转型,用static_cast。第二次将const operator[]的返回值的const去掉,用const_cast转型。
注意:反过来,用const去调用非const往往不是一个好主意。
void append(const char* str){
size_t len = strlen(str);
if (_capacity < _size + len){
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
void push_back(const char c){
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 8 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
const string operator+(const string& s){
string tmp(*this);
tmp.append(s._str);
return tmp;
}
string& operator+=(const string& s){
(*this) += s._str;
return *this;
}
string& operator+=(const char c){
push_back(c);
return *this;
}
string& operator+=(const char* str){
append(str);
return *this;
}
string& insert(size_t pos, const char c){
assert(pos <= _size);
if (_size == _capacity){
size_t newcapacity = _capacity == 0 ? 8 : _capacity * 2;
reserve(newcapacity);
}
moveBack(pos, 1); //复用,封装私有函数
_str[pos] = c;
++_size;
return *this;
}
string& insert(size_t pos, const char* str){
assert(pos <= _size);
size_t len = strlen(str);
if (_capacity < len + _size){
reserve(len + _size);
}
moveBack(pos, len); //复用,封装私有函数
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos){
assert(pos < _size);
if (len == npos || pos + len > _size){
_str[pos] = '\0';
_size = pos;
}
else{
while(pos + len <= _size)
_str[pos++] = _str[pos + len];
_size -= len;
}
return *this;
}
size_t find(const char c, size_t pos = 0) const{
for (size_t i = pos; i < _size; ++i){
if (_str[i] == c)
return i;
}
return npos;
}
size_t find(const char* str, size_t pos = 0)const{
char* ptr = strstr(_str + pos, str);
if (ptr != nullptr)
return ptr - _str;
return npos;
}
const char* c_str()const{
return _str;
}
//此时新增私有成员
private:
//从pos开始的字符串向后平移len个长度
// moveBack为了让两个insert复用
void moveBack(size_t pos, size_t len){
size_t end = _size + len;
int size = _size;
while (size >= static_cast<int>(pos)){
_str[end--] = _str[size--];
}
}
static size_t npos; //在类里面声明,需要在类外部初始化
static对象的初始化:
//在类外部初始化 在类外部初始化 在类外部初始化 重要的事情说三遍
size_t string::npos = -1; //这行代码写在类外部
各种复用,未完成的函数复用已完成的函数,增加复用性。
insert函数种需要一次转型,否则在0的位置插入会有问题。实际上是moveBack函数的转型。
bool operator==(const string& s)const{
return strcmp(_str, s._str) == 0;
}
bool operator!=(const string& s)const{
return !((*this) == s);
}
bool operator>(const string& s){
return strcmp(_str, s._str) > 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);
}
较简单,不作讲解。这也算是先苦后甜,哈哈。
ostream& operator<<(ostream& out, const string& s){
for (size_t i = 0; i < s.size(); ++i){
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s){
char c;
while (1){
c = in.get();
if (c == ' ' || c == '\n')
break;
s += c;
}
return in;
}
istream& getline (istream& is, string& s){
char c;
while (1){
c = in.get();
if (c == '\n')
break;
s += c;
}
return in;
}
返回引用是为了支持连续的输入和输出。比如
cout << s1 << ‘a’ << endl; cout << s1返回一个ostream的引用,继续输出字符’a’,然后是endl。输入也一样。>>的运算符重载遇到空格或者换行符就结束,getline只遇到换行符才结束。
注意到,在operator>>种,我用到了in.get()这个函数,这是为了获得一个来自键盘的字符。为什么不直接用std的>>呢?
因为>>不会接收到空格和换行符,所以无法判断operator>>的结束。使用get函数不会有此问题。
(全文完)