【C++】模拟实现string类

发布于:2024-09-18 ⋅ 阅读:(61) ⋅ 点赞:(0)

通过上篇我们已经学习到了string类的基本使用,这里我们就试着模拟实现一些,我们主要实现一些常用到的函数。

注意:我们在此实现的和C++标准库中实现的有所不同,其目的主要是帮助大家大概理解底层原理。本篇中涉及许多的字符串函数,大家如果不清楚具体功能的可以先看一下这篇文章 -> 常见的字符串函数

我们模拟string类的大致框架是:

class string
{
public:
    //...

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

_str是在堆上动态开辟的空间,_size是元素个数,_capacity是容量大小。 接下来主要是实现一些public下的一些内容。

一、构造函数

在这里我们只实现两个常用的构造函数,一个是默认构造,一个是带参构造。

string()
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{

}
string(const char* str)
{
	_size = strlen(str);
	_capacity = _size; //_capacity不包含'\0',所以不用+1
	_str = new char[_capacity + 1]; //+1是为了存放'\0'
	strcpy(_str, str);
}

大家仔细看看,默认构造写的对吗?

 乍一看,没有任何问题,如果打印可以看到的是空串就说明我们写的是对的。

因为我们还没实现重载流插入和流提取,所以我们可以简单的写一个c_str()来帮助我们打印。

const char* c_str() const
{
	return _str;
}

在主函数中调用test_string1():

void test_string1()
{
	string s1;
	string s2("hello world");
	cout << s1.c_str() << endl;
	cout << s2.c_str() << endl;
}

运行结果

我们发现程序崩了,这说明我们写的有问题,其原因是因为在默认构造函数中_str初始化为空指针nullptr, 在打印过程中,在字符串必须要找到'\0'才终止,而对nullptr进行解引用程序就会出现崩溃。

我们可以这样写:

string()
	:_str(new char[1]{ '\0' })
	,_size(0)
	,_capacity(0)
{

}

运行结果: 

注意: _str(new char[1]{ '\0' })  和 _str(new char('\0'))这两种写法的效果一样,但要写成前者的形式,其目的是为了适应析构函数。

我们也可以将上边两个构造函数合并成一个:

string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size; //_capacity不包含'\0',所以不用+1
	_str = new char[_capacity + 1]; //+1是为了存放'\0'

	strcpy(_str, str);
}

二、析构函数

直接看代码即可:

~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

三、赋值重载

string& operator=(const string& str)
{
    if(this != &str) //防止自己给自己赋值
    {
	    delete[] _str; //先释放旧空间,否则会造成内存泄漏
	    _str = new char[str._size + 1];
	    strcpy(_str,str._str);
	    _size = str._size;
	    _capacity = str._capacity;
    }
    return *this;
}

四、拷贝构造

拷贝构造在"五(11)"引出。

五、成员函数

(1)size()/capacity()
//返回字符串中元素个数
size_t size() const
{
	return _size;
}

//返回所开空间的容量大小(比实际空间少1,1就是'\0')
size_t capacity() const
{
	return _capacity;
}

这种简单的代码,大家一看便知,不过多赘述。

(2)重载[]
//返回下标为pos位置上的字符
char& operator[](size_t pos)
{
	assert(pos >= 0 && pos < _size); //越界直接报错
	return _str[pos];
}

这里需要注意的是返回值要用引用返回,其一,它的空间申请在堆上,调用[]后,对象不销毁,所用可以使用引用返回。其二,我们可以修改pos下标的值,即可读可写。

还有一种写法:

//返回const类型字符串下标为pos位置上的字符
const char& operator[](size_t pos) const
{
	assert(pos >= 0 && pos < _size); //越界直接报错
	return _str[pos];
}

这种写法只能读不能写,也就是不能修改pos下标的值。

调用assert函数必须包含头文件<assert.h>。 

(3)迭代器

我们在上篇文章中提到迭代器的功能像指针,所以在这里可以用指针的方式定义迭代器。

typedef char* iterator;

//返回起始位置指针
iterator begin()
{
	return _str;
}

//返回最后一个有效字符位置的下一位置的指针
iterator end()
{
	return _str + _size;
}

在主函数中调用test_string2():

void test_string2()
{
	string s1("good morning");
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

 

 我们这里用原生指针来typedef迭代器,根本原因是string的底层是数组,我们才可以这样玩,如果底层是链表这样定义的方式就不行了。

迭代器是一种封装的体现。它屏蔽了底层结构和实现细节,提供了统一的类似访问容器的方式。

const迭代器:

typedef const char* const_iterator;

//返回const类型字符串的起始位置指针
const_iterator cbegin() const
{
	return _str;
}

//返回const类型字符串的最后一个有效位置的下一位置的指针
const_iterator cend() const
{
	return _str + _size;
}
(4)reserve()
//设置容量
void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1]; //加1是为了存放'\0'
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

n比原先容量大就扩容,在C++中我们尽量不要去使用C语言中的realloc去扩容,要使用new这个关键字来扩容。

n比原先容量小是否缩容是不确定的,我们这里就不处理这种情况了。

(5)push_back()
//在字符串最后位置上插入一个字符
void push_back(char ch)
{
	if (_size == _capacity)
	{
		//扩容
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

	_str[_size] = ch;
	_size++;
}

这里只实现了尾插一个字符这一个重载函数。

在主函数中调用test_string3():

void test_string3()
{
	string s1("hello world");
	s1.push_back('x');
	s1.push_back('x');
	cout << s1.c_str() << endl;
}

运行结果: 

这结果怎么和我们想象的不太一样? 怎么出现乱码了

这是因为我们没有考虑'\0',我们在尾插时覆盖掉了末尾的'\0',导致出现乱码。

更改后的代码:

void push_back(char ch)
{
	if (_size == _capacity)
	{
		//扩容
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

	_str[_size] = ch;
	_size++;

	_str[_size] = '\0';  //防止'\0'被覆盖
}
(6)重载+=
//在字符串最后位置上接一个字符
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

这里直接复用push_back,就可以达到效果。

在主函数中调用test_string4():

void test_string3()
{
	string s1("hello world");
	s1 += 'x';
	s1 += 'x';
	cout << s1.c_str() << endl;
}

运行结果: 

这里我们调用了修改后的push_back(),所以没有出现乱码,如果我们自己实现就要提防出现乱码的情况。 

这里通常也会+=一个字符串(我们可以复用下面append函数):

//在字符串最后位置上接一个字符串
string& operator+=(const char* str)
{
	append(str);
	return *this;
}
(7)append()
//在字符串最后位置上追加一个字符串
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		//扩容
		//大于2倍,需要多少开多少,不足2倍,按二倍扩
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	strcpy(_str + _size, str); //这里不用担心'\0'问题,因为strcpy也会把'\0'拷贝进去
	_size += len;
}

append在扩容时不能直接固定2倍扩,因为追加的字符串的长度可能大于capacity的2倍,我们这里采用了如果_size + len大于2倍_capacity,需要多少开多少,不足2倍,按二倍扩。

(8)insert()
//在pos位置前插入一个字符
void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		//扩容
		reserve(_capacity * 2 == 0 ? 4 : _capacity * 2);
	}

	//挪动数据
	size_t end = _size;
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
}

代码写起来飞快,但有没有错误呢?

我们来验证一下, 在主函数中调用test_string5():

void test_string5()
{
	string s("hello world");
	cout << s.c_str() << endl;

	s.insert(0, 'x'); //头插
	cout << s.c_str() << endl;

}

运行结果:

 

这里程序崩了,这又是哪里出问题了呢?

这是循环的跳出条件是end>=pos,正常情况下end==0是最后一次进入循环,然后end-1 == -1后大于0,就跳出循环,但由于这里end是size_t无符号整形,所以它永远不可能是-1,所以程序崩溃。那我们把end类型改为int不就行了 ,答案也是不行,因为pos是size_t类型,一个int类型和size_t类型进行比较时,会隐式类型提升,将int默默提升为size_t类型进行比较,所以我们要将pos强制转换成int类型进行比较。

修改后的代码:

//在pos位置前插入一个字符
void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		//扩容
		reserve(_capacity * 2 == 0 ? 4 : _capacity * 2);
	}

	//挪动数据
	int end = _size; //必须将end类型改为int
	while (end >= (int)pos)  //比较时必须将pos类型强制转换为int
	{
		_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
}

运行结果:

还有另外一种写法:

//在pos位置前插入一个字符
void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		//扩容
		reserve(_capacity * 2 == 0 ? 4 : _capacity * 2);
	}

    //挪动数据
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
    ++_size;
}

这种写法会更好一点。 

上面写的是插入一个字符,接下来我们实现插入一个字符串:

//在pos位置前插入一个字符串
void insert(size_t pos, const char* str)
{
	assert(pos < _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		//扩容
		//大于2倍,需要多少开多少,不足2倍,按二倍扩
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	size_t end = _size + len;
	while (end >= pos + len)
    {
		_str[end] = _str[end - len];
		--end;
	}

	for (size_t i = 0;i < len;++i)
	{
		_str[pos + i] = str[i];
	}
		
	_size += len;
}

这段代码有一种情况也会出现问题,就是pos和len同时为0,按理说这是没意义的,但这种情况确实存在,故我们要预防这种情况,添加一个if判断即可:

void insert(size_t pos, const char* str)
{
	assert(pos < _size);
	size_t len = strlen(str);
	if (len == 0)  //这里判断一下特殊的情况
		return;
	if (_size + len > _capacity)
	{
		//扩容
		//大于2倍,需要多少开多少,不足2倍,按二倍扩
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	size_t end = _size + len;
	while (end >= pos + len)
	{
		_str[end] = _str[end - len];
		--end;
	}

	for (size_t i = 0;i < len;++i)
	{
		_str[pos + i] = str[i];
	}
		
	_size += len;
}
(9)erase()
//从pos位置开始,向后删除len个字符
void erase(size_t pos, size_t len)
{
    assert(pos < _size); 
	if (len > _size - pos) //删除pos往后的所有字符
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		for (size_t i = pos + len;i <= _size; ++i)
		{
			_str[i - len] = _str[i];
		}
		_size -= len;
	}
}

在主函数中调用test_string6():

void test_string6()
{
	string s("hello world");
	cout << s.c_str() << endl;

	s.erase(0, 6);
	cout << s.c_str() << endl;
}

运行结果:

(10)find()
//从pos位置向后查找,找到第一个字符ch返回其下标,否则返回npos
size_t find(char ch, size_t pos)
{
    assert(pos < _size);
	for (size_t i = pos;i < _size;i++)
	{
		if (_str[i] == ch)
			return i;
	}

	return npos; //static const size_t npos = -1
}

 下面是上面的一个重载函数:

//从pos位置向后查找,找到第一个字串str返回其首个字符的下标,否则返回npos
size_t find(const char* str, size_t pos)
{
	assert(pos < _size);

	const char* ptr = strstr(_str + pos, str); //如果找到字串,返回母串中子串的起始位置的指针
	if (ptr== nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}
(11)substr() 
//取子串
string substr(size_t pos, size_t len)
{
	assert(pos < _size);
		
	//len大于剩余字符长度,更新一下len
	if (len > _size - pos)
	{
		len = _size - pos;
	}

	string sub; //用来存放字串
	sub.reserve(len); //提前预留出空间,避免频繁扩容

	for (size_t i = 0; i < len; ++i)
	{
		sub += _str[pos + i];
	}

	return sub;
}

 在主函数中调用test_string7():

void test_string7()
{
	string s("Test.txt");
	size_t pos = s.find('.');

	string tmp = s.substr(pos,npos);  //static const size_t npos = -1;

	cout << tmp.c_str() << endl;
}

运行结果:

程序崩溃,打印了一串乱码,肯定是我们的程序写的有点问题。

从代码中看,因为不能写传引用返回,所以我们写的是传值拷贝,传值返回会调用拷贝构造,我们此刻没有写拷贝构造,所以会调用默认生成的拷贝构造完成浅拷贝,问题就出在这个浅拷贝身上,如果是浅拷贝,调用结束后sub指向的空间就被收回了,而由于是浅拷贝tmp的_str又指向被收回的空间,所以会造成非法访问内存空间。导致打印一串乱码,程序崩溃。

所以我们要想解决这个问题就需要单独写一个拷贝构造,来进行深拷贝。

//拷贝构造 -- 深拷贝
string(const string& str)
{
	_str = new char[str._capacity + 1];
	strcpy(_str, str._str);
	_size = str._size;
	_capacity = str._capacity;
}

再次运行: 

这次代码正常运行。

(12)swap()

swap函数写起来非常简单,只需要调用库中swap即可。

库中的swap是一个模板,我们成员函数swap实现如下:

void swap(string& tmp)
{
	std::swap(_str, tmp._str);
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}

很容易明白。 

六、非成员函数

(1)重载关系运算符
bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}	
bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<=(const string& s1, const string& s2)
{
	return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

bool operator!=(const string& s1, const string& s2)
{
	return !(s1 == s2);
}

我们只需重载<和==,其余运算符重载直接复用它们两个即可实现。 

在主函数中调用test_string8():

void test_string8()
{
	string s1("Hello");
	string s2("Hello");
		
	cout << (s1 == s2) << endl;
	cout << (s1 > s2) << endl;
	cout << (s1 < s2) << endl;

	cout << (s1 == "Hello") << endl; //隐式类型转换
	cout << ("world" == "Hello") << endl; //重载的条件必须满足至少一个是自定义类型,这里是两个const类型的char指针在比较
}

运行结果:

 

(2) 重载流插入(<<)和流提取(>>)
ostream& operator<<(ostream& out, const string& str)
{
	for (auto e : str)
	{
		out << e;
	}
	return out;
}
istream& operator>>(istream& in, string& str)
{
	char ch;
	in >> ch;
	while (ch != ' ' && ch != '\n')
	{
		str += ch;
		in >> ch;
	}

	return in;
}

在主函数中调用test_string9():

void test_string9()
{
	string s1;

	cin >> s1;

	cout << s1;
}

运行结果: 

 

我们发现遇到空格或换行,程序并没有终止,也就是cin还在进行,初步判断问题出在重载流提取>>时出现了错误。

原因是cin在控制台拿数据时,会将空格和换行认为是分隔符,会读空格和换行但仅仅认为它们是分隔符,也就是取不到空格和换行。所以,界面就会一直等待输出,循环出不来。换成get可以解决问题,get是什么字符都可以取到。在C语言中scanf和cin一样,也是取不到空格和换行,可以换成getchar来解决问题。

修改后代码:

istream& operator>>(istream& in, string& str)
{
	char ch;
	ch = in.get(); //调用get是什么字符就接收什么字符,不会过滤掉空格
	while (ch != ' ' && ch != '\n')
	{
		str += ch;
		ch = in.get();
	}

	return in;
}

在主函数中调用test_string10(): 

void test_string10()
{
	string s1("hello world");

	cin >> s1;

	cout << s1;
}

运行结果: 

不对呀,这结果怎么是这样的?

因为s1是有内容的,所以在调用流提取>>时,要先将对象中的内容清空,代码如下:

void clear()
{
	_str[0] = '\0';
	_size = 0;
}

istream& operator>>(istream& in, string& str)
{
	str.clear();

	char ch;
	ch = in.get(); //调用get是什么字符就接收什么字符,不会过滤掉空格
	while (ch != ' ' && ch != '\n')
	{
		str += ch;
		ch = in.get();
	}

	return in;
}

运行结果: 

这样就完成了重载流提取>>的工作。 

优化流提取>>:

调用>>,当我们插入大量字符时,会不断的进行+=,会造成频繁扩容,会损耗性能。

这里进行优化:

istream& operator>>(istream& in, string& str)
{
	str.clear();

	const int N = 256;
	char buff[N]; //作为缓冲
	int i = 0;

	char ch;
	ch = in.get(); //调用get是什么字符就接收什么字符,不会过滤掉空格
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == N - 1)
		{
			buff[i] = '\0';
			str += buff;

			i = 0;
		}
		ch = in.get();
	}

	if (i > 0) //没进if语句
	{
		buff[i] = '\0';
		str += buff;
	}

	return in;
}

这个代码的好处是:如果输入很长的字符串不会频繁扩容,如果输入很短的字符串,影响也不大,因为我的buff是在栈中开辟的,调用完就销毁了,对程序的性能影响不大。 

 (3)getline()

getline的功能和流提取>>差不多,区别是getline遇到空格不结束,遇到换行才结束。

我们稍作修改即可实现:

istream& getlien(istream& in, string& str)
{
	str.clear();

	char ch;
	ch = in.get(); //调用get是什么字符就接收什么字符,不会过滤掉空格
	while (ch != '\n')
	{
		str += ch;
		ch = in.get();
	}

	return in;
}
(4)swap()

在上面有一个swap成员函数,但在这里它不是成员函数,它是针对string类型全局函数。为什么又有了一个?

我们先看一个例子:

void test_string12()
{
	string s1("hello");
	string s2("world");

	//方式1
	s1.swap(s2);

	//方式2
	swap(s1, s2);
}

方式1会调用这个:

void swap(string& tmp)
{
	std::swap(_str, tmp._str);  //这里的sawp也是全局的模板swap,但交换的是内置类型
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}

方式2会调用这个:

template <class T> void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

在C++98下,肯定是方式1好些,因为string类型的对象调用方式2,首先完成一次拷贝构造,接着两次赋值重载,相当于比方式1多了3次深拷贝,这代价是很大了,每次都要开空间,效率是很低的。

为什么方式1没有拷贝呢?虽然方式1中也调用了模板swap,但参数都是内置类型,所以没有拷贝,效率比较高,所以方式1会更好。

其实,C++也考虑到了这点,他也写了一个全局swap函数:

它是将模板实例化了,变成具体的函数。

我们以方式2去写,它其实调的是void swap (string& x, string& y)这个函数,不再去单独调用模板了,而这个函数内部调用的x.swap(y),相当于方式1的用法,也不会有拷贝了。也解决了3次深拷贝的问题。

七、形式

(1)拷贝构造又一形式

我们在实现拷贝构造的时候是我们手动来进行开辟空间,赋值等一系列操作完成深拷贝,但还有一种方法可以帮助我们达到目的,而且不用我们手动开辟空间:

void swap(string& tmp)
{
    //交换成员变量
	std::swap(_str, tmp._str);  //这里需要用到标准库中的swap来进行交换
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}
string(const string& str)
{
	string tmp(str.c_str());
	swap(tmp);
}

我们在拷贝构造中,先定义一个临时的string对象tmp,tmp相当于str的一份拷贝,再将当前的对象的成员变量与tmp的成员变量进行交换,即可实现间接拷贝,来达到目的。

这里需要注意一个点,就是要保证当前对象的_str的值为nullptr,因为一会要交换给tmp,tmp出作用域会销毁,所以要保证当前的_str不是随机值,我们可以在声明成员函数是给一个缺省值:

char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
 (2)赋值重载又一形式

赋值重载与拷贝构造的思路差不多,我们直接看代码:

void swap(string& tmp)
{
    //交换成员变量
	std::swap(_str, tmp._str);  //这里需要用到标准库中的swap来进行交换
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}
string& operator=(const string& str)
{
	if (this != &str)
	{
		string tmp(str.c_str());
		swap(tmp);
	}
	return *this;
}

这里就很巧妙了,赋值重载,假设s2 = s1,我们之前在实现赋值重载时,是先把s2所占空间释放掉,再开辟和s1一样大的空间,再调用strcpy进行复制,现在我们这样写就不用手动释放s2的空间了,交换之后,tmp就是之前s2,出了作用域,tmp自动调用析构函数,进行空间释放,所以这段代码的妙处就在于,既完成了赋值,又完成了对之前空间的释放。

再简洁:

string& operator=(string tmp)
{
	swap(tmp);
	return *this;
}

我们也可以直接进行交换,以s2 = s1为例,tmp是s1的拷贝构造出来的对象,它们的_str是不一样的,但_str指向的内容是一样的。然后交换,和上面一样的思路。这种写法更简洁,也能达到目的。 

(3)优点

以上这些写法,在效率上与之前的相比没有优劣之分,它们思路不同,之前的是我们手动设置容量,现在这些活全部交给编译器去完成了,减少了我们出错的机会。

我们可以写一段代码来检验我们说的到底对不对,到底能不能实现功能。

在主函数中调用test_string11():

void test_string11()
{
	string s1("hello world");
	string s2 = s1; //调用拷贝构造
	string s3("hah");

	s1 = s3;//调用赋值重载

	cout << s2 << endl;
	cout << s1 << endl;
}

运行结果: 

结果没有任何问题。 当然大家可以多测试几组(下面有源码),这里我就不多测了,应该是没问题的。

八、写时拷贝(了解)

我们在浅拷贝时会遇到一些问题:

  1. 析构两次
  2. 一个修改会影响另外一个

那么如何解决这些问题?

这里引出来一个引用计数的概念:

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该
资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,
如果减1后计数为0,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

通过引用计数我们就解决了析构多次的问题。

如果引用计数是1,那么我可以任意修改,因为空间只有我一人掌管。

如果引用计数不是1,那么我还是要进行深拷贝,"引用计数和写时拷贝",也就是说谁写,谁拷贝。

这时候,有些人会认为直接深拷贝就行了,不用引用计数不是也可以嘛。

这里有一个"博弈"的思想:如果拷贝后,不写就是"赚"。

如果博弈成功,将会提高代码执行的效率。一般在vs上用的是深拷贝,在g++上用的是写时拷贝。

九、源码

(1)string.h
#pragma once
#include <string>
#include <assert.h>
#include <iostream>
using namespace std;

namespace blue
{
	class string
	{
	public:
		typedef char* iterator;

		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		iterator begin() const
		{
			return _str;
		}
		iterator end() const
		{
			return _str + _size;
		}

		typedef const char* const_iterator;

		const_iterator cbegin() const
		{
			return _str;
		}
		const_iterator cend() const
		{
			return _str + _size;
		}

		//短小频繁调用的函数可以直接定义到类中,默认是inline
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size; //_capacity不包含'\0',所以不用+1
			_str = new char[_capacity + 1]; //+1是为了存放'\0'

			strcpy(_str, str);
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

		/*string(const string& str)
		{
			_str = new char[str._capacity + 1];
			strcpy(_str, str._str);
			_size = str._size;
			_capacity = str._capacity;
		}*/


		void swap(string& tmp)
		{
			std::swap(_str, tmp._str);
			std::swap(_size, tmp._size);
			std::swap(_capacity, tmp._capacity);
		}
		string(const string& str)
		{
			string tmp(str.c_str());
			swap(tmp);
		}

		//赋值重载
		//string& operator=(const string& str)
		//{
		//	if (this != &str)
		//	{
		//		delete[] _str; //先释放旧空间,否则会造成内存泄漏
		//		_str = new char[str._size + 1];
		//		strcpy(_str, str._str);
		//		_size = str._size;
		//		_capacity = str._capacity;
		//	}
		//	return *this;
		//}

		//string& operator=(const string& str)
		//{
		//	if (this != &str)
		//	{
		//		string tmp(str.c_str());
		//		swap(tmp);
		//	}
		//	return *this;
		//}

		string& operator=(string tmp)
		{
			swap(tmp);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

		char& operator[](size_t pos)
		{
			assert(pos >= 0 && pos < _size); //越界直接报错
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
			assert(pos >= 0 && pos < _size); //越界直接报错
			return _str[pos];
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(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);

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

		//static const size_t npos = -1;
		//static const double d = 1.1;//err
		static const size_t npos;
	};

	bool operator<(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);


	ostream& operator<<(ostream& out, const string& str);
	istream& operator>>(istream& in, string& str);

}


(2)string.cpp
#include "string.h"

namespace blue
{
	const size_t string::npos = -1;

	//设置容量
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	//在字符串最后位置上插入一个字符
	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			//扩容
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

		_str[_size] = ch;
		_size++;

		_str[_size] = '\0';
	}
	//在字符串最后位置上追加一个字符串
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			//扩容
			//大于2倍,需要多少开多少,不足2倍,按二倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		strcpy(_str + _size, str);
		_size += len;
	}
	//在字符串最后位置上接一个字符
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	//在字符串最后位置上接一个字符串
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

	//在pos位置前插入一个字符
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			//扩容
			reserve(_capacity * 2 == 0 ? 4 : _capacity * 2);
		}

		//挪动数据
		//int end = _size; //必须将end类型改为int
		//while (end >= (int)pos)  //比较时必须将pos类型强制转换为int
		//{
		//	_str[end + 1] = _str[end];
		//	--end;
		//}

		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		_str[pos] = ch;
		++_size;
	}

	//在pos位置前插入一个字符串
	void string::insert(size_t pos, const char* str)
	{
		assert(pos < _size);
		size_t len = strlen(str);
		if (len == 0)  //这里判断一下特殊的情况
			return;
		if (_size + len > _capacity)
		{
			//扩容
			//大于2倍,需要多少开多少,不足2倍,按二倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		size_t end = _size + len;
		while (end >= pos + len)
		{
			_str[end] = _str[end - len];
			--end;
		}

		for (size_t i = 0;i < len;++i)
		{
			_str[pos + i] = str[i];
		}
		
		_size += len;
	}

	//从pos位置开始,向后删除len个字符
	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len > _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len;i <= _size; ++i)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;
		}
	}


	//从pos位置向后查找,找到第一个ch返回其下标,否则返回npos
	size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);
		for (size_t i = pos;i < _size;i++)
		{
			if (_str[i] == ch)
				return i;
		}

		return npos; //static const size_t npos = -1
	}
	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str + pos, str); //如果找到字串,返回母串中子串的起始位置的指针
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

	//取子串
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		
		//len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}

		string sub; //用来存放字串
		sub.reserve(len); //提前预留出空间,避免频繁扩容

		for (size_t i = 0; i < len; ++i)
		{
			sub += _str[pos + i];
		}

		return sub;
	}


	bool operator<(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}	
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}

	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}


	ostream& operator<<(ostream& out, const string& str)
	{
		for (auto e : str)
		{
			out << e;
		}
		return out;
	}


	//istream& operator>>(istream& in, string& str)
	//{
	//	str.clear();

	//	char ch;
	//	//in >> ch;
	//	ch = in.get(); //调用get是什么字符就接收什么字符,不会过滤掉空格
	//	while (ch != ' ' && ch != '\n')
	//	{
	//		str += ch;
	//		//in >> ch;
	//		ch = in.get();
	//	}

	//	return in;
	//}

	istream& operator>>(istream& in, string& str)
	{
		str.clear();

		const int N = 256;
		char buff[N]; //作为缓冲
		int i = 0;

		char ch;
		ch = in.get(); //调用get是什么字符就接收什么字符,不会过滤掉空格
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				str += buff;

				i = 0;
			}
			ch = in.get();
		}

		if (i > 0) //没进if语句
		{
			buff[i] = '\0';
			str += buff;
		}

		return in;
	}
}

(3)Test.cpp 
#include "string.h"

namespace blue
{
	void test_string1()
	{
		//string s1;
		//string s2("hello world");
		//cout << s1.c_str() << endl;
		//cout << s2.c_str() << endl;

		string s3("good morning");
		string::iterator it = s3.begin();
		while (it != s3.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
	void test_string2()
	{
		string s1("good morning");
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	void test_string3()
	{
		string s1("hello world");
		s1.push_back('x');
		s1.push_back('x');
		cout << s1.c_str() << endl;
	}

	void test_string4()
	{
		string s1("hello world");
		s1 += 'x';
		s1 += 'x';
		cout << s1.c_str() << endl;
	}

	void test_string5()
	{
		string s("hello world");
		cout << s.c_str() << endl;

		s.insert(0, 'x');
		cout << s.c_str() << endl;

	}

	void test_string6()
	{
		string s("hello world");
		cout << s.c_str() << endl;

		s.erase(0, 6);
		cout << s.c_str() << endl;
	}

	void test_string7()
	{
		string s("Test.txt");
		size_t pos = s.find('.');

		string sub = s.substr(pos); 

		cout << sub.c_str() << endl;
	}

	void test_string8()
	{
		string s1("Hello");
		string s2("Hello");
		
		cout << (s1 == s2) << endl;
		cout << (s1 > s2) << endl;
		cout << (s1 < s2) << endl;

		cout << (s1 == "Hello") << endl; //隐式类型转换
		cout << ("world" == "Hello") << endl; //重载的条件必须满足至少一个是自定义类型,这里是两个const类型的char指针在比较
	}

	void test_string9()
	{
		string s1;

		cin >> s1;

		cout << s1;
	}
	void test_string10()
	{
		string s1("hello world");

		cin >> s1;

		cout << s1;
	}


	void test_string11()
	{
		string s1("hello world");
		string s2 = s1; //调用拷贝构造
		string s3("hah");

		s1 = s3;//调用赋值重载


		cout << s2 << endl;
		cout << s1 << endl;
	}

	void test_string12()
	{
		string s1("hello");
		string s2("world");

		//方式1
		s1.swap(s2);

		//方式2
		swap(s1, s2);
	}
}

int main()
{
	//blue::test_string1();
	//blue::test_string2();
	//blue::test_string3();
	//blue::test_string4();
	//blue::test_string5();
	//blue::test_string6();
	//blue::test_string7();
	//blue::test_string8();
	//blue::test_string9();
	//blue::test_string10();
	blue::test_string11();

	return 0;
}

在外面套了一个命名空间blue是为了防止与C++标准库中string发生冲突。 

十、结语

本篇内容到这里就结束了,主要讲了string的模拟实现的大致思路,希望帮助到大家,祝大家天天开心!