String类的实现

     C++中为我们提供了字符串类型(string),使得我们在处理字符串时非常方便。在此将实现我们常用的方法。

      头文件:

#pragma once

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
#include <assert.h>

using namespace std;

inline void SafeRelease(char* s){
	if (s != nullptr)
	{
		delete[]s;
		s = nullptr;
	}
}

class MyString{
public:
	typedef char* Iterator;

public:
	MyString::MyString(const char* str = "")
	{
		if (nullptr == str){
			assert(false);
			return;
		}

		_size = myStrlen(str);
		_capacity = _size;
		_str = new char[_capacity + 1];
		myStrcpy(_str, str);
	}
	MyString::MyString(const MyString& str)
		:_str(new char[str._capacity + 1])
		, _size(str._size)
		, _capacity(str._capacity)
	{
		myStrcpy(_str, str._str);
	}
	MyString& operator=(const MyString& str);
	MyString& operator+(const MyString& str);
	MyString::~MyString()
	{
		if (nullptr != _str){
			delete[] _str;
			_str = nullptr;
		}

	}
	Iterator Begin();
	Iterator End();

	void Push_back(char c);
	void Pop_back();
	void Append(size_t n, char c);
	void Append(const char* str);

	MyString& operator+=(char c);
	MyString& operator+=(const char* str);
	void Clear();
	void Swap(MyString& str);
	const char* C_str()const;
	size_t Size()const;
	size_t Capacity()const;
	bool Empty()const;
	void Resize(size_t newSize, char c = char());
	void Reserve(size_t newCapacity);

	char& operator[](size_t index);
	const char& operator[](size_t index)const;
	bool operator<(const MyString& s);
	bool operator<=(const MyString& s);
	bool operator>(const MyString& s);
	bool operator>=(const MyString& s);
	bool operator==(const MyString& s);
	bool operator!=(const MyString& s);
	
	size_t Find(const char c, size_t pos = 0) const;
	// 返回子串s在string中第一次出现的位置
	size_t Find(const char* s, size_t pos = 0) const;
	// 截取string从pos位置开始的n个字符
	MyString SubStr(size_t pos, size_t n);
	// 在pos位置上插入字符c/字符串str,并返回该字符的位置
	MyString& Insert(size_t pos, char c);
	MyString& Insert(size_t pos, const char* str);
	// 删除pos位置上的元素,并返回该元素的下一个位置
	MyString& Erase(size_t pos, size_t len);
private:
	friend ostream& operator<<(ostream& o, MyString& str);
	friend istream& operator >> (istream& is, MyString& ss);
	friend int IndexKMP(const char* S, const char* T);
	friend void GetNext(string T, int* next);

private:
	int myStrlen(const char* str);
	char* myStrcpy(char* dest, const char* src);

private:
	char* _str;
	size_t _capacity;
	size_t _size;

};

为方便查看代码已传至github。相关实现代码:https://github.com/Timecur/STL.git

在这里主要就介绍一些实现string类时遇到的问题。

  • 浅拷贝问题

      浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生非法访问。

       解决方法:深拷贝。

       如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

 

  • delete释放问题
inline void SafeRelease(char* s){
	if (s != nullptr)
	{
		delete[]s;
		s = nullptr;
	}
}	
MyString& MyString::Insert(size_t pos, char c){
		assert(pos <= _size);
			
		char* str = new char[_size+1]; // size一定要足够大
		myStrcpy(str, _str);

		if (_size + 1 > _capacity)
			Reserve(_size + 1);

		_str[pos] = c;
		_str[pos+1] = '\0';
		strcat(_str, str+pos);

		SafeRelease(str);
		return *this;
}

      最初为str申请了size大小的空间,但是在strcopy时,由于还要写入 '\0' 所以拷贝后的str实际上是size+1的大小,因此破坏了原来的指针,所以在delete释放时会出错。

      参考博客:

            

            

 

  • KMP算法

      在实现find方法中的字符串查找时,使用了KMP算法。

      KMP算法:是在每次模式串与目标串失配地方,找寻模式串最合适位置继续匹配。

      例如: 

String类的实现

       可以看出在下标为2的位置失配, 从图可以看出T[1] = T[2] = 'b',所以T[2]处失配,T[1]也一定失配,所以最好方法就是从T[0]处去匹配(T[0]为当前合适位置)。

String类的实现

         当T[0]与S[2]匹配发现依然失配,而T[0]为模式串首位元素,则只能从S串下一元素进行匹配。

String类的实现

         以此方式进行直至匹配成功或匹配完目标串的所有字符。

size_t MyString::Find(const char* s, size_t pos) const{
	assert(pos >= 0);
	return IndexKMP(_str + pos, s);
}


int IndexKMP(const char* S,const char* T)
{
	int i = 0;
	int j = 0;
	int s_len = strlen(S);
	int t_len = strlen(T);
	int *next = new int[t_len + 1];

	GetNext(T, next);

	while (i < s_len && j < t_len){
		if (-1 == j || S[i] == T[j]){
			++i;
			++j;
		}
		else{
			j = next[j];
		}
	}

	delete[] next;
	if (j == t_len){
		return i - t_len;
	}
	else{
		return 0;
	}
}

      在了解KMP算法后,我们还需要一个next数组来辅助我们。next数组作用是记录当前元素失配后,下一合适元素的下标。

String类的实现

       构建方法:

  • 初始化0号下标位置存 -1。
  • 前缀:i , 后缀:j   

       举例来理解前缀后缀:

                 字符串:a b b a

                 

  前缀 后缀 共有元素
a 0
ab a b 0
abb a,ab bb,b 0
abba

a,ab,abb

bba,ba,a 1
  • i == j 时 next[j] = next[i]+1,前后缀向下匹配;
  • i != j 时 则在前缀从头再与后缀比较,直到 i == -1(next[j] = -1) 或 字符串匹配(如上步)。

        String类的实现

void GetNext(string T, int* next)
{
	int i = 0, j = -1;
	next[0] = -1;

	// 前缀是固定的,后继是相对的
	while (i < T.size()){
		if (-1 == j || T[i] == T[j]){
			if (T[++i] != T[++j]){
				next[i] = next[j];
			}
			else{
				next[i] = j;
			}
		}
		else{
			j = next[j];
		}
	}
}

   

  参考KMP算法博客: 

                                    https://zhuanlan.zhihu.com/p/24274982

  前缀后缀详解博客:

 

;