【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;
	}
}

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值