上一次我们实现了string的基本功能, 为了加深对string容器的理解, 这次我们要实现他的很多基本功能, 虽说是实现, 但也不可能和库中的string完全相同, 只能说尽量向库中的容器靠拢.
那么话不多说, 我们直接开始吧. 可能会有点多, 耐心看完, 你一定有所收获~
文章目录
一. 成员变量
上次string的简单实现只有一个char*, 这次的话, 会加入size和capacity
成员变量如下:
private:
char* _ptr;
size_t _size; //有效字符个数
size_t _capacity; //可以存放的最大有效字符个数
注: 前四个函数因为上次实现的时候已经做过详细解释, 这里就不多做说明, 有兴趣的朋友可以去看看上一篇的简单实现, 链接奉上~
string容器的简单实现
二. 基本功能
1. 构造函数
构造函数, 没什么好说的, 想比之前唯一要注意的点就是:
别忘了给_size和_capacity初始化
代码如下:
//全缺省的构造函数
String(const char* str = "")
:_ptr(new char[strlen(str) + 1])
,_size(strlen(str))
,_capacity(_size)
{
strcpy(_ptr, str);
}
2. 拷贝构造
直接现代写法奉上: 跟上面一样, 别忘了给新的成员变量初始化
还有就是这里要自定义一个交换函数, 内容就是交换三个成员变量
代码如下:
//交换函数
void Swap(String& str) {
swap(_ptr, str._ptr);
swap(_size, str._size);
swap(_capacity, str._capacity);
}
//现代写法
String(const String& str)
:_ptr(nullptr) //这里指针一定要初始化, 否则交换完释放的时候空间有问题
, _size(0)
, _capacity(0)
{
String tmp(str._ptr);
Swap(tmp);
}
3. 赋值运算符重载函数
也是一样, 直接现代写法奉上
//现代写法
String& operator=(String str) {
Swap(str);
return *this;
}
4. 析构函数
析构就是释放资源, 别忘了给_size和_capacity赋0, 养成良好的习惯
//析构
~String() {
if (_ptr) {
delete[] _ptr;
_ptr = nullptr;
_size = _capacity = 0;
}
}
三. 插入删除相关
1. reserve增容函数
这里的reserve函数只实现增容的逻辑,
基本逻辑就是: 当n > _capacity时进行增容逻辑
增容的话大家也不陌生了, 无非就是开空间, 拷贝, 然后释放原有空间, 最后更新成员变量
这里开空间的时候直接开了n + 1个空间, 因为无法确定_capacity和n的具体关系, 为了保证空间够用, 直接新开n + 1个空间, 多开一个存放'\0'
//增容, 不实现缩容
void reserve(size_t n) {
if (n > _capacity) {
//开空间, 拷贝
//这里无法保证_capacity和n的具体大小关系, 所以直接开够n个空间即可
char* tmp = new char[n + 1];
strcpy(tmp, _ptr);
//释放空间, 改变指向, 更新容量
delete[] _ptr;
_ptr = tmp;
_capacity = n;
}
}
2. 尾插字符 push_back
尾插一个字符: 首先, 只要是插入接口就必须要判断是否要增容
然后进行尾插操作, 最后一定不要忘了新增一个’\0’
这里增容之前我创建了一个newCap表示新的容量, 运用三目运算符给他赋值, 如果当前_capacity 为 0, 就像库中一样, 给一个初值15, 如果不是, 那么以2倍的规则增容
//尾插字符
void push_back(const char ch) {
if (_size == _capacity) {
//增容
//如果是空的话, 给初值为15(模拟库中的string), 否则以2倍增容
size_t newCap = _capacity == 0 ? 15 : 2 * _capacity;
reserve(newCap);
}
//尾插
_ptr[_size++] = ch;
//最后一定不要忘了补一个'\0'~
_ptr[_size] = '\0';
}
3. 尾插字符串append
尾插字符串, 首先也要考虑增容, 这里先获取要插入的字符串长度len
当 _size + len > _capacity, 进行增容. 由于无法确定要插入的字符串到底有多长, 所以这里直接以_size + len作为参数进行增容
然后进行尾插: 直接用strcpy把字符串拷贝进来即可, 这里直接把’\0’也拷贝进来, 所以不用手动添加, 不过想手动添加也没什么问题
最后别忘了更新成员变量_size
//尾插字符串 append
void append(const char* str) {
//获取字符串长度
size_t len = strlen(str);
//判断增容
if (_size + len > _capacity) {
//直接增容到_size + len, 因为无法确定_size + len有多大
reserve(_size + len);
}
//尾插字符串, 这里把str的'\0'也拷贝过来了
strcpy(_ptr + _size, str);
_size += len;
}
4. +=运算符重载函数
+=运算符其实才是我们最常用的尾插方法
上面已经完整的写出了尾插的接口, 那么+=只需要复用上面的代码即可
这里写了三个重载函数, 分别是+=一个字符, +=一个字符串, +=一个String对象
能复用的代码尽量复用, 既能方便我们代码的书写, 也方面后期的代码维护
//+=运算符重载函数
//+= char ch
String& operator+=(const char ch) {
push_back(ch);
return *this;
}
//+= char* str
String& operator+= (const char* str) {
append(str);
return *this;
}
//+= String
String& operator+=(const String& str) {
append(str._ptr);
return *this;
}
接下来我们测试一下上面的代码是否正确, 测试代码如下:
void test() {
String s = "abcde";
String s2("xyz");
s2 = s;
s.clear();
s.push_back('1');
s.push_back('2');
s.push_back('3');
s.append("abc");
s2 += '0';
s2 += "QAQ";
s2 += s;
}
调试窗口如下:
这里建议大家把测试代码拿去自己调试一下, 因为每一行都涉及变化, 截图过于繁琐, 故不在此展示
5. 在任意位置插入 insert
- 插入一个字符
首先这里使用了一个断言, 保证代码运行下去时位置pos一定是小于有效字符_size的
插入依然是要考虑增容, 和上面大同小异
这里的核心就是移动元素了, 插入的话要从后往前移动元素, 不然数据会被覆盖移动的写法有很多, 可以的话最好自己画一个图演示一下, 这是最清晰的
最后就是插入和增加’\0’
下面给出一个图解:
//任意位置插入
void insert(size_t pos, const char& ch) {
assert(pos < _size);
//判断增容
if (_size == _capacity) {
size_t newCap = _capacity == 0 ? 15 : 2 * _capacity;
reserve(newCap);
}
//移动元素 --> 从后向前, 不然会覆盖
size_t end = _size;
while (end > pos) {
_ptr[end] = _ptr[end - 1];
--end;
}
//插入新字符
_ptr[pos] = ch;
//添加新的结束标志
_ptr[++_size] = '\0';
}
下面演示一个经常遇到的错误写法:
while (end >= pos) {
_ptr[end + 1] = _ptr[end];
--end;
}
当pos = 0时, 会无限循环, 因为end是size_t类型, 无符号, 永远大于等于0
- 插入一个字符串
与上面基本一致, 增容的逻辑和上面的append一致, 这里不再多说
最大的不同就是元素的移动了, 下面会给出图解
最后插入字符串不用strcpy是因为strcpy会把最后的’\0’拷贝进来, 所以选择使用memsetmemset是字节拷贝, 最后一个参数是字节大小
给出图解:
void insert(size_t pos, const char* str) {
assert(pos <= _size);
int len = strlen(str);
//增容
if (_size + len > _capacity) {
reserve(_size + len);
}
//移动元素
size_t end = _size + len;
// end: [pos + len, _size + len] '\0'也一起移动到末尾了
while (end >= pos + len) {
_ptr[end] = _ptr[end - len];
--end;
}
//插入字符串
memcpy(_ptr + pos, str, len * sizeof(char));
_size += len;
}
6. 删除erase
这里删除接口就不写那么多了, 直接一个接口搞定
要注意的是: 删除时移动元素是从前向后移动, 不然会数据会被覆盖
至于元素的移动, 跟上面插入大同小异, 画个图即可…写法很多, 下面是我的一种
代码如下:
//删除
void erase(size_t pos, size_t len) {
//移动元素 ---> 从前向后移动, 不然会覆盖
size_t start = pos + len;
while (start <= _size) {
_ptr[start - len] = _ptr[start];
start++;
}
//更新_size
_size -= len;
}
四. 迭代器相关(重点)
迭代器: 访问容器元素的一种机制
类似于指针
操作: 移动, 解引用, 判断
迭代器实现: char*
typedef char* iterator;
typedef const char* const_iterator;
1. begin()
//begin: 第一个元素的位置
iterator begin() {
return _ptr;
}
const_iterator begin() const {
return _ptr;
}
2. end()
//end: 最后一个元素的下一个位置
iterator end() {
return _ptr + _size;
}
const_iterator end() const {
return _ptr + _size;
}
3. operator[]
//[]重载
//可读可写
char& operator[](size_t pos) {
assert(pos < _size);
return _ptr[pos];
}
//只读
const char& operator[](size_t pos) const {
assert(pos < _size);
return _ptr[pos];
}
有了这些我们就可以进行遍历了
下面给出3种遍历方式的代码:
void test4() {
String s = "12345";
//1. []
for (int i = 0; i < s.size(); i++) {
cout << s[i] << " ";
}
cout << endl;
//2. 迭代器
String::iterator it = s.begin();
while (it != s.end()) {
cout << *it << " ";
it++;
}
cout << endl;
//范围for
for (const auto& ch : s) {
cout << ch << " ";
}
cout << endl;
}
运行结果:
五. 逻辑运算符
bool operator<(const String& s) {
return !(*this >= s);
}
bool operator<=(const String& s) {
return !(*this > s);
}
bool operator>(const String& s) {
if (strcmp(_ptr, s.c_str()) > 0)
return true;
return false;
}
bool operator>=(const String& s) {
return (*this > s) || (*this == s);
}
bool operator==(const String& s) {
if (strcmp(_ptr, s.c_str()) == 0)
return true;
return false;
}
bool operator!=(const String& s) {
return !(*this == s);
}
测试代码:
void test5() {
String s = "123";
String s2 = "123";
String s3 = "02";
String s4 = "9";
cout << (s == s2) << endl;
cout << (s != s2) << endl;
cout << (s > s3) << endl;
cout << (s <= s4) << endl;
cout << (s >= s2) << endl;
cout << (s4 < s2) << endl;
}
运行结果:
六. 其他操作
1. 获取size
const成员函数, 普通成员和const成员都能调用
//获取size
int size() const {
return _size;
}
2. 获取capacity
//获取capacity
size_t capacity() const {
return _capacity;
}
3. 判空
//判空
bool empty() const {
if (_size == 0)
return true;
return false;
}
4. 清空字符串
注意只影响size, 不影响capacity
void clear() {
erase(0, strlen(_ptr));
_size = 0;
}
5. 返回C风格字符串
const char* c_str() const {
return _ptr;
}
6. 查找find函数
这里要加一个静态成员变量 npos, 值为-1, 表示没找到
static const size_t npos;
静态成员要在类外初始化
//查找字符
size_t find(const char& ch, size_t pos = 0) {
for (int i = pos; i < _size; i++) {
if (_ptr[i] == ch) {
//找到了
return i;
}
}
//没找到
return npos;
}
//查找字符串
size_t find(const char* str, size_t pos = 0) {
char* start = strstr(_ptr + pos, str);
if (start) {
//找到了, 返回偏移量
return start - _ptr;
}
//没找到
return npos;
}
7. 输入输出运算符重载
输入输出要声明成友元函数, 为了访问类中的私有成员
不定义为成员函数是因为写出来的顺序与我们的习惯相反(之前文章也有说到)
最后选择友元函数为最优解
类中声明:
friend ostream& operator<<(ostream& _cout, const String& str);
friend istream& operator>>(istream& _cin, String& str);
类外定义:
ostream& operator<<(ostream& _cout, const String& str) {
//_cout << str._ptr;
for (const auto& ch : str) {
cout << ch;
}
return _cout;
}
istream& operator>>(istream& _cin, String& str) {
char ch;
str.clear();//先清空
while ((ch = getchar()) != EOF) {
if (ch == '\n')
break;
str += ch;
}
return _cin;
}
测试代码:
void test2() {
string s = "abc";
cout << s << endl;
cin >> s;
cout << s << endl;
}
结果如下:
到这里string的实现就完成了…
下面给出完整的代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
class String {
public:
//全缺省的构造函数
String(const char* str = "")
:_ptr(new char[strlen(str) + 1])
,_size(strlen(str))
,_capacity(_size)
{
strcpy(_ptr, str);
}
//交换函数
void Swap(String& str) {
swap(_ptr, str._ptr);
swap(_size, str._size);
swap(_capacity, str._capacity);
}
//拷贝构造
//传统写法
//String(const String& str)
// :_ptr(new char[strlen(str._ptr) + 1])
// ,_size(str._size)
// ,_capacity(str._capacity)
//{
// //拷贝
// strcpy(_ptr, str._ptr);
//}
//现代写法
String(const String& str)
:_ptr(nullptr) //这里指针一定要初始化, 否则交换完释放的时候空间有问题
, _size(0)
, _capacity(0)
{
String tmp(str._ptr);
Swap(tmp);
}
//赋值
//传统写法
//String& operator=(const String& str) {
// if (this != &str) {
// //开空间, 拷贝, 释放原有空间
// char* tmp = new char[strlen(str._ptr) + 1];
// strcpy(tmp, str._ptr);
// delete[] _ptr;
// //指向新的空间, 并更新size和cap
// _ptr = tmp;
// _size = str._size;
// _capacity = str._capacity;
// }
// return *this;
//}
//现代写法
String& operator=(String str) {
Swap(str);
return *this;
}
//析构
~String() {
if (_ptr) {
delete[] _ptr;
_ptr = nullptr;
_size = _capacity = 0;
}
}
//增容, 不实现缩容
void reserve(size_t n) {
if (n > _capacity) {
//开空间, 拷贝
//这里无法保证_capacity和n的具体大小关系, 所以直接开够n个空间即可
char* tmp = new char[n + 1];
//strcpy(tmp, _ptr);
//strcpy 遇到'\0'就会停止, 可能出现bug, 这里直接循环拷贝
for (int i = 0; i < _size; i++) {
tmp[i] = _ptr[i];
}
//释放空间, 改变指向, 更新容量
delete[] _ptr;
_ptr = tmp;
_capacity = n;
}
}
//尾插字符
void push_back(const char ch) {
if (_size == _capacity) {
//增容
//如果是空的话, 给初值为15(模拟库中的string), 否则以2倍增容
size_t newCap = _capacity == 0 ? 15 : 2 * _capacity;
reserve(newCap);
}
//尾插
_ptr[_size++] = ch;
//最后一定不要忘了补一个'\0'~
_ptr[_size] = '\0';
}
//尾插字符串 append
void append(const char* str) {
//获取字符串长度
size_t len = strlen(str);
//判断增容
if (_size + len > _capacity) {
//直接增容到_size + len, 因为无法确定_size + len有多大
reserve(_size + len);
}
//尾插字符串, 这里把str的'\0'也拷贝过来了
strcpy(_ptr + _size, str);
_size += len;
}
//+=运算符重载函数
//+= char ch
String& operator+=(const char ch) {
push_back(ch);
return *this;
}
//+= char* str
String& operator+= (const char* str) {
append(str);
return *this;
}
//+= String
String& operator+=(const String& str) {
append(str._ptr);
return *this;
}
//任意位置插入
void insert(size_t pos, const char& ch) {
assert(pos < _size);
//判断增容
if (_size == _capacity) {
size_t newCap = _capacity == 0 ? 15 : 2 * _capacity;
reserve(newCap);
}
//移动元素 --> 从后向前, 不然会覆盖
size_t end = _size;
while (end > pos) {
_ptr[end] = _ptr[end - 1];
--end;
}
/*
常见的错误写法:
当pos = 0时,无限循环, 因为end是size_t类型, 无符号, 永远大于等于0
while (end >= pos) {
_ptr[end + 1] = _ptr[end];
--end;
}
*/
//插入新字符
_ptr[pos] = ch;
//添加新的结束标志
_ptr[++_size] = '\0';
}
void insert(size_t pos, const char* str) {
assert(pos <= _size);
int len = strlen(str);
//增容
if (_size + len > _capacity) {
reserve(_size + len);
}
//移动元素
size_t end = _size + len;
// end: [pos + len, _size + len] '\0'也一起移动到末尾了
while (end >= pos + len) {
_ptr[end] = _ptr[end - len];
--end;
}
//插入字符串
memcpy(_ptr + pos, str, len * sizeof(char));
_size += len;
}
//删除
void erase(size_t pos, size_t len) {
//移动元素 ---> 从前向后移动, 不然会覆盖
size_t start = pos + len;
while (start <= _size) {
_ptr[start - len] = _ptr[start];
start++;
}
//更新_size
_size -= len;
}
//获取size
int size() const {
return _size;
}
//获取capacity
size_t capacity() const {
return _capacity;
}
//判空
bool empty() const {
if (_size == 0)
return true;
return false;
}
//[]重载
//可读可写
char& operator[](size_t pos) {
assert(pos < _size);
return _ptr[pos];
}
//只读
const char& operator[](size_t pos) const {
assert(pos < _size);
return _ptr[pos];
}
//迭代器: 访问容器元素的一种机制
//类似于指针
//操作: 移动, 解引用, 判断
//迭代器实现: char*
typedef char* iterator;
typedef const char* const_iterator;
//begin: 第一个元素的位置
iterator begin() {
return _ptr;
}
//end: 最后一个元素的下一个位置
iterator end() {
return _ptr + _size;
}
const_iterator begin() const {
return _ptr;
}
const_iterator end() const {
return _ptr + _size;
}
//resize(n, ch)
// 1. n <= size: 只需要修改size
// 2. size < n <= capacity: 赋值ch --> [size, capacity) , 修改size
// 3. n > capacity: 增容, 赋值, 修改size
void resize(size_t n, char ch = '\0') {
if (n > _capacity) {
//增容
reserve(n);
}
if (n > _size) {
//赋值
//默认要赋值'\0', 所以不能用strcpy, 只能逐个赋值或者用memset (字节拷贝)
memset(_ptr + _size, ch, n - _size);
}
//修改size
_size = n;
_ptr[_size] = '\0';
}
//查找
size_t find(const char& ch, size_t pos = 0) {
for (int i = pos; i < _size; i++) {
if (_ptr[i] == ch) {
//找到了
return i;
}
}
//没找到
return npos;
}
size_t find(const char* str, size_t pos = 0) {
char* start = strstr(_ptr + pos, str);
if (start) {
//找到了, 返回偏移量
return start - _ptr;
}
//没找到
return npos;
}
const char* c_str() const {
return _ptr;
}
void clear() {
erase(0, strlen(_ptr));
_size = 0;
}
bool operator<(const String& s) {
return !(*this >= s);
}
bool operator<=(const String& s) {
return !(*this > s);
}
bool operator>(const String& s) {
if (strcmp(_ptr, s.c_str()) > 0)
return true;
return false;
}
bool operator>=(const String& s) {
return (*this > s) || (*this == s);
}
bool operator==(const String& s) {
if (strcmp(_ptr, s.c_str()) == 0)
return true;
return false;
}
bool operator!=(const String& s) {
return !(*this == s);
}
friend ostream& operator<<(ostream& _cout, const String& str);
friend istream& operator>>(istream& _cin, String& str);
private:
char* _ptr;
size_t _size; //有效字符个数
size_t _capacity; //可以存放的最大有效字符个数
static const size_t npos;
};
const size_t String::npos = -1;
ostream& operator<<(ostream& _cout, const String& str) {
//_cout << str._ptr;
for (const auto& ch : str) {
cout << ch;
}
return _cout;
}
istream& operator>>(istream& _cin, String& str) {
char ch;
str.clear();//先清空
while ((ch = getchar()) != EOF) {
if (ch == '\n')
break;
str += ch;
}
return _cin;
}
void test() {
String s = "abcde";
String s2("xyz");
s2 = s;
s.clear();
s.push_back('1');
s.push_back('2');
s.push_back('3');
s.append("abc");
s2 += '0';
s2 += "QAQ";
s2 += s;
}
void test2() {
string s = "abc";
cout << s << endl;
cin >> s;
cout << s << endl;
}
void test3() {
String s = "0123456789";
s.insert(7, 'a');
s.insert(3, "xyz");
s.erase(2, 1);
size_t pos = s.find('4', 0);
pos = s.find("xyz", 0);
pos = s.find("aaaaa", 0);
}
void test4() {
String s = "12345";
for (int i = 0; i < s.size(); i++) {
cout << s[i] << " ";
}
cout << endl;
String::iterator it = s.begin();
while (it != s.end()) {
cout << *it << " ";
it++;
}
cout << endl;
for (const auto& ch : s) {
cout << ch << " ";
}
cout << endl;
}
void test5() {
String s = "123";
String s2 = "123";
String s3 = "02";
String s4 = "9";
cout << (s == s2) << endl;
cout << (s != s2) << endl;
cout << (s > s3) << endl;
cout << (s <= s4) << endl;
cout << (s >= s2) << endl;
cout << (s4 < s2) << endl;
}
int main() {
//test();
test2();
//test3();
//test4();
//test5();
return 0;
}