开发日志:
/*
* 开发日志
* 1、基本 string 类框架:string 域(自定义命名空间) + 私有成员
* 2、基本函数:一般构造 + 拷贝构造 + 析构
以下分组实现一些 string 类常见常用的函数
* 3、基本访问操作:c_str() + size() + operator[]
* 4、实现两类迭代器:begin() + end()
* 5、增加字符操作:reserve + push_back + append + operator+=
* 6、插入与删除:insert + erase
* 7、查找:find
* 8、各种重载函数:大小比较 + 赋值重载
* 9、提取子串 与 清理:substr + clear
* 10、string 的 神奇 swap 函数
* 11、流插入和流提取:<< 和 >>
*/
声明:本次模拟实现 string 类,采用 声明和定义分离 的形式
声明写在 string.h 头文件中
定义写在 string.cpp 文件中
同时 其他本项目下的 .cpp 想要使用 string.h 需要用双引号引用头文件(而不是 尖括号<>)
- 因为用 双引号 引用头文件,编译器会优先到本项目文件中找该头文件,再到库中找头文件
- 这样,自定义的 string.h 就会被优先引用(编译器找到一个就不会再找了,因此避免了和库的 string 冲突)
0. 声明:一些注意事项
1、不想指定类域,就先框定类域
由于我们自定义了一个 命名空间,string 类 声明和定义分离,则 定义部分需要指定类域:
如下:bit 时 类域,string 是 类名
bit::string::string(const char* str)
若每个都这样写,有点麻烦,可以直接 框定类域
如 string.cpp 中
namespace bit
{string::string(const char* str)
}
2、给字符串 new 空间时,一定要 + 1(给 '\0')
由于 string 类的字符串长度不包括 '\0' ,其总容量 _capacity 和 字符串有效长度 _size 都不计算 '\0'
但是我们需要开多一个字节空间来存储 '\0',因此每次给字符串 new 空间时,一定要 + 1
如下:
_str = new char[_size + 1];
1、基本 string 类框架:类域+私有成员+基本函数(构造和析构)
string.h
namespace bit
{class string{public:string(const char* str = ""); // 构造函数:全缺省(合并有参和无参)string(const string& s); // 拷贝构造函数~string(); // 析构函数private:char* _str; // 指向字符串size_t _size; // 字符串有效范围size_t _capacity; // 字符串总空间大小};
}
string.cpp
namespace bit
{// 构造函数:全缺省(合并有参和无参)string::string(const char* str):_size(strlen(str)){_str = new char[_size + 1];strcpy(_str, str);_capacity = _size;}// 拷贝构造函数string::string(const string& s){// 要开新空间,拷贝别人的字符串,若直接 str = s.str 就是浅拷贝了_str = new char[s._capacity + 1]; strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 析构函数string::~string() {delete[] _str;_str = nullptr;_size = _capacity = 0;}
}
1.1 为什么要自定义类域
因为 C++库中也有一个 string,这里定义自定义命名空间,才不会和 库函数空间 std 中的 string 冲突
1.2 将构造函数 设计成 全缺省
string 类的构造函数有两种需求:无参构造 和 带参构造
(1)无参构造:创建 string 变量时不赋初值
string s;
(2)带参构造: 则相反
string s("hello")
使用缺省值:""
当 创建 string 变量时不赋初值 ,string 会赋值为缺省值,即 空字符串,刚好满足 无参和带参的需求
——— 以下分组实现函数 ———
2、基本访问操作:c_str() + size() + operator[]
namespace bit
{const char* string::c_str() const{return _str;}size_t string::size() const{return _size;}char& string::operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& string::operator[](size_t pos) const{assert(pos < _size);return _str[pos];}}
2.1 c_str()
这个函数是将 C++ 的string,转换成 C语言的 char* 类型的字符串
2.2 两种 operator[]
这是为了满足两种需求:访问字符串且需要修改 和 不需要修改
3、实现迭代器:begin() + end()
在本次模拟实现string类中,迭代器是定义成 char* 类型,为了遍历字符串
因此这里 typedef 设置 iterator 迭代器(定义在类中)
class string
{
public:typedef char* iterator;typedef const char* const_iterator; // const_iterator 表示迭代器指向的内容不能修改,不是迭代器本身不能修改// ..... 其他函数};
若对 const_iterator 和 const iterator 之间有疑惑 或 分不清的:
可以看这篇博客:通俗讲解 const_iterator 和 const iterator 的区别
因为这里迭代器设置为 char* 指针类型,因此 begin() 和 end() 也就是获取指针位置
string.cpp
namespace bit
{string::iterator string::begin(){return _str;}string::iterator string::end(){return _str + _size;}string::const_iterator string::begin() const{return _str;}string::const_iterator string::end() const{return _str + _size;}
}
4、增加字符操作:reserve + push_back + append + operator+=
string.cpp
namespace bit {void string::reserve(size_t n){// 申请的空间大小 n 大于 当前字符串总空间大小 capacticy 才扩容if (n > _capacity) {// 手动扩容:开新空间,释放调原来的char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n; // capacity 不用 +1,不计算 '\0'}}void string::push_back(const char ch){// 一次开两倍,用 reserve,真实空间不够就扩,空间够也不缩容if (_size == _capacity) {size_t newCapacty = _capacity == 0 ? 4 : _capacity * 2;reserve(newCapacty);}// _size++ 写前面和写后面都一样//_size++;//_str[_size - 1] = ch; // 这个位置本来是 '\0'的//_str[_size] = '\0'; // 要帮别人'\0' 移动位置// 通过调试可以看到:你使用 ch 代替了 '\0' 会导致字符串无效(没有结束符了),因此需要把'\0'补上_str[_size] = ch;_str[_size + 1] = '\0';_size++;}void string::append(const char* str){size_t len = strlen(str);if (_size + len > _capacity) {// 要多少加多少reserve(_size + len);}// 可以使用 strcat 追加字符串:遍历字符串,找到 \0 ,从这个位置开始追加字符串// 遍历一遍效率较低// 使用 strcpy 指定起始位置:_str + _size,刚好是 \0 的位置strcpy(_str + _size, str);_size += len;}// operator+= 函数 直接复用之前写的函数就好string& string::operator+=(const char ch){this->push_back(ch);return *this;}string& string::operator+=(const char* str){this->append(str);return *this;} }
5、查找:find
这个就是在字符串中 寻找你要找的 字符或字符串,返回该字符或字符串第一次出现的下标位置(首位)
string.h
namespace bit {class string{// .....private:char* _str;size_t _size;size_t _capacity;//const static int npos = -1; // 特例const static size_t npos; // 定义 静态成员常量 npos (用 const 修饰变成常量)}; }
string.cpp
namespace bit {const size_t string::npos = -1; // 静态成员变量类外定义size_t string::find(char ch, size_t pos){assert(pos < _size);for (int i = pos; i < _size; ++i) {if (_str[i] == ch) return i;}return npos;}size_t string::find(const char* str, size_t pos){// 这里涉及字符串匹配问题:在实践中一般会用 BF算法,即暴力算法,而不是 KMP(至于为什么自己了解一下)// 这里直接使用 strstr(这个函数底层也是 暴力)// strstr 函数:返回指向 str1 中首次出现的 str2 的指针,如果 str2 不是 str1 的一部分,则返回空指针。const char* p = strstr(_str + pos, str);// 杭哥没写这个if (p == NULL) return string::npos; // 文档是这样写的:32位下和64位下的 npos 数值不同return p - _str; // 指针 - 指针 = 数量}}
思考问题:
1、程序中的 npos 是什么?
这个表示 一个很大的数,在我们的程序中,定义成 静态成员变量
关于 静态成员变量 的定义位置
静态成员变量声明和定义要分离,在类中声明,在类外定义
而在本章节,我们将所有函数的声明和定义分离,创建了 string.h string.cpp 两个文件
如果,你将 静态成员变量的类外定义,直接写在 string.h 文件中,就会出现程序运行的链接问题
原因:string.h 会在 string.cpp 和 test.cpp 两个文件中展开,这三个文件最后会合并成一个文件
这样导致 [ 静态成员变量的类外定义] 出现两次,会报错:重定义
因此:我们这里将 [ 静态成员变量的类外定义] 放在 string.cpp 函数中
2、为什么 const static int npos = -1 这样的写法 是 特例?
讲一个特例
之前的知识讲解过: 静态成员变量在类中声明,在类外定义,且普通成员变量可以直接给缺省值(为初始化列表服务),而 静态成员变量 不能给缺省值
但是
这样写不报错:直接给 const 修饰的 静态成员变量 赋初值
这里的赋值,也算作 静态成员变量 的定义(就不用到类外定义)
class A{ private:const static int tmp = 10; // 不报错 };
这个却会报错
const static double tmp = 10.1;
为什么一个会报错一个不会报错?
直接给结论:这个用 const 修饰静态成员,使其可以直接在类内定义 的 特例是针对于 整型类型的,浮点型不可以
(整型类型是表示整型家族:int、size_t、long、char…..)
这里讲这个是提醒你有这么一个特例,而不推荐你使用,你只要看到别人的代码出现这个,你可以看得懂就好
或者说某些奇怪的规则:各大厂商写的规则,有些甚至会为了减少可移植性故意设置的
6、各种重载函数:大小比较 + 赋值重载
string.cpp
namespace bit {bool string::operator<(const string& s) const{return _str < s._str;}bool string::operator<=(const string& s) const{return _str < s._str || _str == s._str;}bool string::operator>(const string& s) const{return !(*this > s);}bool string::operator>=(const string& s) const{return !(*this < s);}bool string::operator==(const string& s) const{return (_str == s._str && _size == s._size && _capacity == s._capacity);}bool string::operator!=(const string& s) const{return !(*this == s);}// 赋值重载string& string::operator=(const string& s){string tmp(s);swap(tmp);return *this;}}
7、提取子串 与 清理:substr + clear
string.cpp
namespace bit {string string::substr(size_t pos, size_t len){// 这个也要分长度的情况if (len >= _size - pos) {string tmp(_str + pos);return tmp; // 这里需要传值返回}else {// 写法1:老实开空间+strncpy// 写法2::reserve 开空间 + for循环拷贝string sub;sub.reserve(len);for (size_t i = pos; i < pos + len; ++i) {sub += _str[i];}return sub;}}void string::clear(){_str[0] = '\0';} }
8、string 的 神奇 swap 函数
这个 string 类的 swap 函数 是 用于帮助 string 类中的 拷贝构造函数 和 赋值重载运算符函数 写成 现代写法而发明
具体应用看这篇博客:
string.cpp
namespace bit {void string::swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);} }
9、流插入和流提取:<< 和 >>
之前我们实现 日期类 时,会将 operator<< 和 operator>> 写成类的友元函数
我们这里可以不写成友元函数
(一般 全局函数需要访问私有成员时,会写成友元函数,我们这里可以不访问私有,只需访问公公有成员)
这里也说明:流插入和流提取不一定要写成友元函数的
相关操作和优化技巧都在下面的代码中 解释了
string.h
namespace bit {class string {// ....};// 写成全局函数ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s); }
string.cpp
namespace bit {// 流插入ostream& operator<<(ostream& out, const string& s){for (size_t i = 0; i < s.size(); ++i) {cout << s[i];}return out;}// 流提取// 实现思想:到IO流中直接提取一个一个的 charistream& operator>>(istream& in, string& s){// 第一代写法:使用 cin ,但不能提取空格和换行// 不能这样写:cin 提取不了空格和换行(会被cin自动忽略),while会死循环/*char ch;cin >> ch;while (ch != ' ' && ch != '\n') {s += ch;in >> ch;}*/// 第二代写法:加入 IO流的 get 函数 和 清空函数 clear()// 使用 C++IO流的函数 get,可以获取 空格和换行// 同时这里还要一个问题:cin 是需要直接覆盖当前所有数据(而我们这里的思路是一个一个尾插的:s += ch;)// 因此 cin ,str += 之前,需要先清空原数据 使用前面的 clear函数// 代码如下://s.clear();//char ch = in.get(); // C++IO流的函数 get//while (ch != ' ' && ch != '\n') {// s += ch;// ch = in.get();//}// 第二代写法 的 问题:/*这里每次都是一个字符一个字符的相加 s += ch; 当字符非常长时,会面临频繁的扩容可以 reserve(100) 直接开大一点的空间,但如果我一次加入小几个字符,就有空间浪费了最好的办法:缓冲区思想,添加一个缓冲区数组*/// 第三代写法: 添加一个缓冲区数组s.clear();int i = 0;char buff[128];char ch = in.get(); // C++IO流的函数 getwhile (ch != ' ' && ch != '\n') {buff[i++] = ch;if (i == 127) { // 当数组满的时候,尾部添加一个 '\0' ,再尾插入 string 中buff[i] = '\0';s += buff;i = 0;}ch = in.get();}// 循环结束后,注意是否有剩余字符串未被加入 stringif (i != 0) {buff[i] = '\0';s += buff;}// 字符串很小时,没关系;字符串很大时,不用频繁地扩容return in;} }
10、总代码
string.h
#pragma once #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include <utility> #include<assert.h> using namespace std;namespace bit {class string {public:typedef char* iterator;typedef const char* const_iterator; // 构造函数string(const char* str = "");string(const string& s);~string();const char* c_str() const;size_t size() const;char& operator[](size_t pos);const char& operator[](size_t pos) const;// 实现迭代器iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;void reserve(size_t n);void push_back(const char tmp);void append(const char* s);string& operator+=(const char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str); void erase(size_t pos, size_t len = npos); // npos 的定义是一个 // 默认从零开始size_t find(char ch, size_t pos = 0); size_t find(const char* str, size_t pos = 0);bool operator<(const string& s) const;bool operator<=(const string& s) const;bool operator>(const string& s) const;bool operator>=(const string& s) const;bool operator==(const string& s) const;bool operator!=(const string& s) const;//string& operator=(const string& s); // 有返回值目的是为了支持连续赋值 string& operator=(string tmp);void swap(string& s);string substr(size_t pos, size_t len = npos); // 一般这种指定长度 len 的,就会存在取完剩下的情况,都要加一个 nposvoid clear();private://int _Buff[16]; // 暂时不实现char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;const static size_t npos;};ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s); }
string.cpp
#include"string.h"namespace bit {const size_t string::npos = -1; // 静态成员变量类外定义string::string(const char* str): _size(strlen(str)){_str = new char[_size + 1];_capacity = _size;// 拷贝过来:strcpy(目的地,源头)strcpy(_str, str); }string::string(const string& s){string tmp(s._str);swap(tmp);}string::~string(){delete[] _str; _str = nullptr;_size = 0;_capacity = 0;}const char* string::c_str() const{return _str;}size_t string::size() const{return _size;}char& string::operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& string::operator[](size_t pos) const{assert(pos < _size);return _str[pos];}string::iterator string::begin(){return _str;}string::iterator string::end(){return _str + _size;}string::const_iterator string::begin() const{return _str;}string::const_iterator string::end() const{return _str + _size;}void string::reserve(size_t n){if (n > _capacity) {// 手动扩容char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n; // capacity 不用 +1,不计算 '\0'}}void string::push_back(const char ch){// 一次开两倍,用 reserve,真实空间不够就扩,空间够也不缩容if (_size == _capacity) {size_t newCapacty = _capacity == 0 ? 4 : _capacity * 2;reserve(newCapacty);}_str[_size] = ch;_str[_size + 1] = '\0';_size++;}void string::append(const char* str){size_t len = strlen(str);if (_size + len > _capacity) {// 要多少加多少reserve(_size + len);}// 可以使用 strcat 追加字符串:遍历字符串,找到 \0 ,从这个位置开始追加字符串// 遍历一遍效率较低// 使用 strcpy 指定起始位置:_str + _size,刚好是 \0 的位置strcpy(_str + _size, str); _size += len;}string& string::operator+=(const char ch){this->push_back(ch);return *this;}string& string::operator+=(const char* str){this->append(str);return *this;}void string::insert(size_t pos, char ch){assert(pos <= _size); // 这个杭哥没写// 插入字符,会使字符串变长,要考虑扩容if (_size == _capacity) {size_t newCapacty = _capacity == 0 ? 4 : _capacity * 2;reserve(newCapacty);}// 在 pos 插入,后面的字符向后移// '\0' 不用单独处理:下面第一个处理的就是 '\0'for (int i = _size; i >= (int)pos; --i) {_str[i+1] = _str[i];}_str[pos] = ch;_size++;}void string::insert(size_t pos, const char* str){assert(pos <= _size);int len = strlen(str);// 插入字符,会使字符串变长,要考虑扩容if (_size+len >= _capacity) {reserve(_size + len);}for (int i = _size + len; i > pos+len-1; --i) {_str[i] = _str[i - len];}for (int i = 0; i < len; ++i) {_str[pos+i] = str[i];}_size += len;}void string::erase(size_t pos, size_t len){assert(pos < _size);if (len == npos || _size - pos <= len) { _str[pos] = '\0';_size = pos;}else {strcpy(_str + pos, _str + pos + len);_size -= len;}//_size -= len; 不能直接 - len,万一len很大呢?}size_t string::find(char ch, size_t pos){assert(pos < _size);for (int i = pos; i < _size; ++i) {if (_str[i] == ch) return i;}return npos;}size_t string::find(const char* str, size_t pos){const char* p = strstr(_str + pos, str);if (p == NULL) return string::npos; return p - _str;}bool string::operator==(const string& s) const{return (strcmp(_str, s._str) == 0 && _capacity == s._capacity && _size == s._size);}bool string::operator<(const string& s) const{// 字典序比较大小:这里好像可以直接比较 char* 类型的字符串// 也可以用 strcmp// return strcmp(_str, s._str) < 0;return _str < s._str;}bool string::operator<=(const string& s) const{return (*this < s || *this == s);}bool string::operator!=(const string& s) const {return !(*this == s);}bool string::operator>(const string& s) const{return !(*this < s && *this == s);}bool string::operator>=(const string& s) const{return !(*this < s);}string& string::operator=(string tmp){swap(tmp);return *this;}// 注意:这三种代码效率上没有很大差别,但是代码精简了void string::swap(string& s) // 注意:这里不能写 (const string& s),库里面的swap没有重载 const 类型的变量{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 取字符子串,不只是取字符串的 str,是搞一个新的 stringstring string::substr(size_t pos, size_t len){if (len >= _size - pos) {string sub(_str + pos); // 直接传一个 char* 有多少取多少return sub;}else {string sub;sub.reserve(len);for (size_t i = 0; i < len; ++i) {sub += _str[pos + i];}return sub;}}void string::clear(){_str[0] = '\0'; // 直接毁灭所有数据_size = _capacity = 0;}ostream& operator<<(ostream& out, const string& s){for (size_t i = 0; i < s.size(); ++i) {cout << s[i];}return out;}istream& operator>>(istream& in, string& s){s.clear();int i = 0;char buff[128];char ch = in.get(); // C++IO流的函数 getwhile (ch != ' ' && ch != '\n') {buff[i++] = ch;if (i == 127) {buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0) {buff[i] = '\0';s += buff;}// 字符串很小时,没关系;字符串很大时,不用频繁地扩容return in;} }