目录
与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;
- };
- }