C++——string类的模拟实现

  我们要模仿STL库里面的string,基本实现常用的一些操作。

目录

成员函数的接口

默认成员函数

构造函数

拷贝构造函数

析构函数

operator=赋值运算符重载

容量大小相关的函数 

size( )函数

capacity()函数

empty()函数

reserve()函数

resize()函数 

clear()函数

访问下标相关函数

operator[ ]函数

查找相关函数

find()字符

find()字符串

迭代器相关的函数

插入字符的相关函数

insert()一个字符

insert()字符串

尾插push_back( )

追加字符串append( )

operator+=加等于一个字符

operator+=加等于一个字符串

erase删除函数

关系运算符重载函数

operator< 小于 

operator== 等于 

operator<= 小于等于 

operator> 大于

operator>= 大于等于 

operator!= 不等于 

流插入 流提取 函数重载

流提取 

流插入 

其他函数

Swap( )交换函数

c形式的字符串


成员函数的接口

#pragma once
#include<iostream>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
using namespace std;
namespace sjj//自定义一个命名空间
{
	class string
	{
	public:
		typedef char* iterator;//string的迭代器就是原生指针
		typedef const char* const_iterator;
		//构造函数
		//string("hello");
		string(const char* str = "");

		//拷贝构造函数
		//s2(s1)
		string(const string& s);

		//赋值重载
		//s2=s1
		string& operator=(string s);//传值传参也是拷贝构造

		//析构
		~string();

		//容量大小相关的函数//
		size_t size()const;

		size_t capacity()const;

		bool empty()const;

		void clear();

		void reserve(size_t n);//对容量进行改变,让容量到n

		void resize(size_t n, char ch = '\0');//改变size的大小

		//访问下标相关的函数//
		char& operator[](size_t pos);

		const char& operator[](size_t pos)const;

		//查找相关的函数//
		//正向查找一个字符
		size_t find(char c, size_t pos = 0);

		//正向查找一个字符串
		size_t find(char* s, size_t pos = 0);

		///迭代器相关的函数/
		iterator begin();

		iterator end();

		const_iterator begin()const;

		const_iterator end()const;

		///插入字符串相关的函数/

		//尾插
		void push_back(char c);

		//追加字符串
		void append(const char* str);

		//加等于
		string& operator+=(char c);

		const string& operator+=(char* str);

		//插入一个字符
		string& insert(size_t pos, char c);

		//插入字符串
		string& insert(size_t pos, const char* s);

		//删除相关的函数
		//删除pos位置开始的n个字符
		string& erase(size_t pos, size_t len = npos);

		//其他函数//
		//自己写的交换函数
		void Swap(string& s);
		//C形式的字符串
		const char* c_str()const;
	private:
		char* _str;//字符串数组
		size_t _size;//有效字符的个数 不含\0
		size_t _capacity;//能存储有效字符的空间,不包含\0
		static const size_t npos;//静态成员变量,在类外部初始化
	};
	const size_t string::npos = -1; //初始化

	///关系运算符重载//
	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);


	//  operator<<   operator>>
	//  cout<<s1 --> operator<<(out,s1);
	///流插入 流提取 重载///
	ostream& operator<<(ostream& out, const string& s);
	//ostream&是系统的类型,它能够实现输出内置类型的数据

	istream& operator>>(istream& in, string& s);
}

默认成员函数

构造函数

给个空字符作为缺省值,不传参时,构造的就是空字符串,传入参数,就用传入的参数。

string(const char* str = "")//给缺省值""空字符串
	:_size(strlen(str))
	, _capacity(_size)
{
	_str = new char[_capacity + 1];//  +\0
	strcpy(_str, str);//char *strcpy( char *strDestination, const char *strSource );
}

拷贝构造函数

首先你一定要了解一下深浅拷贝相关的知识,请戳这!

第一种写法:传统写法

//传统写法     //要用自己写的
string(const string& s)
	:_size(0)
	, _capacity(0)
{
	_str = new char[s._capacity + 1];//_str申请一块和s._str一样大的空间
	strcpy(_str, s._str);//将两个指针指向交换
	_size = s._size;
	_capacity = s._capacity;
}

我们是老老实实的进行深拷贝,先new一块和原来一样大的空间,然后再深拷贝数据,这样两块空间才不会相互影响 

第二种:现代写法(推荐写)

//现代写法————投机取巧
string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str);//复用构造函数,构造一个tmp对象出来
	this->Swap(tmp);//相当于Swap(this,tmp)
}

总结

  现代写法投机取巧,复用构造函数,让他开一个临时对象出来,出了作用域,局部临时对象自动销毁,我们不用多管它,我们最后只需要把它们的指针指向交换即可,代码很简洁 。

析构函数

清理动态的一些资源

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

operator=赋值运算符重载

如果我们不写,它也会使用编译器默认生成的,但是也是浅拷贝,不符合我们的使用要求

我们知道要深拷贝,那么首先要考虑两个空间的容量大小问题

①s3的数据个数大于s1中数据个数,我们要考虑s1的扩容问题

②s1的空间远大于s3的数据个数,我们需要考虑缩容问题

这样我们不如将问题变得简单粗暴一点,直接将s1的空间给释放掉,重新开一块和s3一样大空间,最后再将数据拷贝过去

 第一种:传统写法

string& operator=(const string& s)
{
	/*if (this != &s)//防止自己给自己赋值s3=s3
	{
		delete[] _str;//释放掉s1 默认有个隐藏的this指针,实际上是这样: this->_str
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	}*/
	//优化版本
	//new可能会失败,但是却先释放s1了,我们可以先new
	if (this != &s)//防止自己给自己赋值s3=s3
	{
		char* tmp = new char[strlen(s._str) + 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
	}
	return *this;//返回s1
}

 我们可以通过调试窗口看到:

第二种:现代写法(很巧妙)

//更NB的写法
//s1=s3 这里传值传参也是一种拷贝,s充当的就是tmp
string& operator=(string s)
{
	this->Swap(s);
	return *this; //返回左值,支持连续赋值
}

总结:这里用的现代写法很巧妙,使用传值拷贝,相当于一次拷贝构造函数的调用。

容量大小相关的函数 

size( )函数

我们直接返回_size即可,不包括\0

const size_t size()const
{
	return _size;
}

capacity()函数

size_t capacity()const
{
	return _capacity;
}

empty()函数

可以利用strcmp函数,比较两个字符串指针。

//判断是否为空
bool empty()
{
	return strcmp(_str, "") == 0;
}

reserve()函数

调整容量大小到n,我们先new一个n+1的新空间,然后把原来的数据拷贝到新空间上面,再释放掉原来的空间,再交换两个指针的指向,再将_capacity置为n即可。

我们来看看动图演示:

void reserve(size_t n)//对容量进行改变,让容量到n
{
	if (n > _capacity)
	{
		char* tmpstr = new char[n + 1];
		strcpy(_str, tmpstr);
		delete[] _str;
		_str = tmpstr;
		_capacity = n;
	}
}

resize()函数 

void resize(size_t n, char ch = '\0')//改变size的大小
{
	if (n <= _size)//缩小的情况
	{
		_size = n;
		_str[n] = '\0';
	}
	else//要插入数据的情况
	{
		if (n > _capacity)
		{
			reserve(n);
		}
		//void *memset( void *dest, int c, size_t count );
		memset(_str + _size, ch, n - _size);
		_size = n;
		_str[n] = '\0';
	}
}

resize函数会出现两种情况:

第一种:给定的size的值n小于_size,这种情况数据会被截断,只保留前面的n个

第二种:给定的size的值n大于capacity容量了,要考虑扩容问题

clear()函数

将_size位置置为斜杠0,就相当于清空了字符串。

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

访问下标相关函数

operator[ ]函数

  operator[ ]下标访问元素,是我们最为熟悉的一种方式,而且非常的简洁方便,实现它是为了让我们可以像访问数组一样去访问字符串,我们可以实现普通版本和const对象版本。

char& operator[](size_t pos)
{
	assert(pos < _size);//防止越界
	return _str[pos];//返回pos位置的引用,相当于就是把那个字符给返回了
}
//const版本
const char& operator[](size_t pos)const
{
	assert(pos < _size);//防止越界
	return _str[pos];//返回pos位置的引用,相当于就是把那个字符给返回了
}

查找相关函数

find()字符

  npos是string类的一个静态成员变量,其值为整型最大值。

  我们通过遍历的方式,从头往后寻找匹配的字符串,找到返回下标,没有找到返回npos。

//正向查找一个字符
size_t find(char c, size_t pos = 0)
{
	assert(pos < _size);
	for (int i = 0; i < _size; ++i)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}
	return npos;//没有找到目标字符,返回npos
}

find()字符串

  利用的是strstr字符串查找函数,找到了返回字符串的第一个字符的指针,没有找到返回NULL。

找到了字符串,我们可以通过指针差值确定目标字符串的位置。

//正向查找一个字符串
size_t find(char* s, size_t pos = 0)
{
	assert(pos < _size);

	//const char * strstr ( const char * str1, const char * str2 );
	const char* ptr = strstr(_str + pos, s);//调用strstr字符串查找函数
	if (s)
	{
		return ptr - _str;//返回找到第一个字符的下标
	}
	else
	{
		return npos;
	}
}

迭代器相关的函数

  string类里面的迭代器就相当于原生指针,只不过这里是typedef的,这里我们把它当做指针来看就行了。

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

const_iterator begin()const
{
	return _str;
}

const_iterator end()const
{
	return _str + _size;
}

插入字符的相关函数

我们先来看看动图演示的插入原理:

insert()一个字符

  insert函数要在任意位置插入字符,首先检查下标合法性,再判断是否需要扩容,扩容可以使用reserve函数,再将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串

//插入一个字符
string& insert(size_t pos, char c)
{
	assert(pos <= _size);
	if (_size == _capacity)//判断是否需要增容
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	//从后往前挪动数据,以免被覆盖
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = c;
	++_size;
	return *this;
}

insert()字符串

原理和插入一个字符类似

//插入字符串
string& insert(size_t pos, const char* s)
{
	assert(pos <= _size);//检查有效位置
	size_t len = strlen(s);//记录要插入的字符串长度
	if (_size + len > _capacity)
	{
		reserve(_size + len);//按需扩容
	}

	size_t end = _size + len;
	while (end > pos + len)//挪动数据
	{
		_str[end] = _str[end - len];
		--end;
	}
	strncpy(_str + pos, s, len);//拷贝数据到pos位置
	_size += len;
	return *this;
}

尾插push_back( )

  push_back就是在一个字符串尾部拼接上字符,我们首先要考虑空间是否足够,若不够需要扩容,可以利用reserve函数,扩大2倍,(不一定要扩2倍),再插入数据,最后末尾填上\0即可。

void push_back(char c)
{
	if (_size == _capacity)//扩容
	{
        //这里要注意,刚开始初始化给定的_capacity=0时
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	//再插入
	_str[_size] = c;
	++_size;
	_str[_size] = '\0';
}

也可以直接复用insert函数,尾插的意思就是在pos位置为_size的位置插入

//尾插字符
void push_back(char c)
{
	insert(_size, c); //在字符串末尾插入字符ch
}

追加字符串append( )

  append其实和push_back类似,只不过append是拼接上一个字符串,这里问题就是出在字符串,我们是扩2倍?扩1.5倍?都不是很确定,这里要看的是传入的字符串个数,我们的空间至少要开到_size+len,空间刚刚好满足所有的字符。开好空间后,利用strcpy把字符串里面的数据拷贝过去即可。

//追加字符串
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		//扩容
		reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
}

  也是可以复用insert函数,在原来字符串的末尾插入即可。

//尾插字符串
void append(const char* str)
{
	insert(_size, str); //在字符串末尾插入字符串str
}

operator+=加等于一个字符

  加等于也是我们用的比较多的一个函数,因为它方便简洁,一看就懂。我们可以复用push_back,注意返回新的对象。

//加等于一个字符
string& operator+=(char c)
{
	//复用push_back
	push_back(c);
	return *this;
}

operator+=加等于一个字符串

复用append

//加等于一个字符串
const string& operator+=(char* str)
{
	//复用append
	append(str);
	return *this;
}

erase删除函数

我们要删除pos位置开始的len个字符

我们需要考虑两个问题:

第一个:pos位置之后的所有位置都要删除,将\0置于pos位置,_size-=pos即可

  第二个:pos位置之后的字符只删除一部分,剩余部分要填充前面的空位。

//删除pos位置开始的n个字符
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len == npos || pos + len > _size)//删除pos位置之后的所有字符
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else//删除pos位置之后的部分字符
	{
		strcpy(_str + pos, _str + pos + len);//用后面的剩余字符填充空位
		_size -= len;
	}
	return *this;//返回新的对象
}

关系运算符重载函数

operator< 小于 

  strcmp () 函数比较的不是字符串的长度, 而是比较字符串中对应位置上的字符的大小 (即比较的是ASCII码值,而且 还要注意区分大小写,传入c_str是一个非空的指针

bool operator<(const string& s1, const string& s2)
{
	//	size_t i1 = 0;
	//	size_t i2 = 0;
	//	while (i1 < s1.size() && i2 < s2.size())
	//	{
	//		if (s1[i1]<s2[i2])
	//		{
	//			return true;
	//		}
	//		else if (s1[i1]>s2[i2])
	//		{
	//			return false;
	//		}
	//		else
	//		{
	//			++i1;
	//			++i2;
	//		}
	//	}
	//	//while的条件是 且
	//	//一个一个的字符按照ASCII码值去比较
	//	//现在是有一个字符串已经走完了
	//	// "abcd"   "abcd"    false
	//	// "abcd"   "abcde"   true   
	//	// "abcde"  "abcd"    false
	//	return i2 < s2.size() ? true : false;

	//c_str是一个内容为字符串指向字符数组的临时指针
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}

operator== 等于 

bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;

}

operator<= 小于等于 

  实现了上面的小于和等于,下面的运算符都可以复用已经实现了的。

复用小于等于

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

operator> 大于

复用小于等于的反面


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

operator>= 大于等于 

复用小于

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

operator!= 不等于 

复用等于

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

流插入 流提取 函数重载

流提取 

  要注意,ostream,istream类是c++标准输出流中的基类,它能够输出内置类型,我们既可以把它写成全局的函数,也可以重载成为友元,但是友元破坏了程序的封装性,不推荐。

ostream& operator<<(ostream& out, const string& s)
//ostream&是系统的类型,它能够实现输出内置类型的数据
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

流插入 

我们要注意先清理s对象,以免初始化对象时给定了初值。

istream& operator>>(istream& in, string& s)
{
    s.clear();//防止对象初始化已经给了值
	char ch = in.get();
	while (ch != ' ' && ch != '\n')//遇到空格或者换行就停止
	{
		s += ch;
		ch = in.get();//再获取下一个
	}
	return in;
}

其他函数

Swap( )交换函数

  因为多处要用到swap函数,且实现都是差不多的,不妨将其封装成为一个成员函数。


//模拟string类里面的交换函数
void Swap(string& s)
{
	std::swap(_str, s._str);//用C++库里面自带的交换函数
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

  我们可以查询,C++全局库里面有个swap函数,string类里面也有个swap函数,这两个函数都能实现交换

std库里面的swap函数:

string类里面的swap函数:

int main()
{
	std::string s1("hello");
	std::string s2("C++");
	s1.swap(s2);//调用string类里面的swap函数
	swap(s1,s2);//调用全局的swap函数
	return 0;
}

这两个谁的效率更高呢?

  答案:string类里面专门的交换函数效率更高,因为仅仅是对成员变量进行交换;而std库里面的是用模板实现的,会创建临时对象,并且进行三次深拷贝,代价更高! 

c形式的字符串

返回一个非空的指针

//C语言形式的字符串
const char* c_str()const
{
	return _str;
}

谢谢观看!

猜你喜欢

转载自blog.csdn.net/weixin_57675461/article/details/123800470