自学内容网 自学内容网

【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 之间有疑惑 或 分不清的:

可以看这篇博客:通俗讲解 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)!