【C++修炼之路 第五章】模拟实现 string 类
开发日志:
/*
* 开发日志
* 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 之间有疑惑 或 分不清的:
因为这里迭代器设置为 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流中直接提取一个一个的 char istream& 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流的函数 get while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 127) { // 当数组满的时候,尾部添加一个 '\0' ,再尾插入 string 中 buff[i] = '\0'; s += buff; i = 0; } ch = in.get(); } // 循环结束后,注意是否有剩余字符串未被加入 string if (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 的,就会存在取完剩下的情况,都要加一个 npos void 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,是搞一个新的 string string 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流的函数 get while (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; } }
原文地址:https://blog.csdn.net/2301_79499548/article/details/140655870
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!