【C++】String类常用函数的模拟实现

目录

一、基础模板

1、默认成员函数:

二、字符串遍历:

1、[ ]符号重载:

2、迭代器iterator:

三、修改操作:

1、reserve:

2、push_back与append: 

3、insert与erase:

4、swap:

5、find与substr:

四、流插入/流提取重载:

流插入 << 重载:  

流提取 >> 重载:

getline:


一、基础模板

class string
{
public:
    // 对应的成员函数
private:

    // 成员变量
    char* _str;		 // 字符串
	size_t _size;	 // 字符串实际大小
	size_t _capacity; // 字符串容量

	// 表示 "直到字符串结尾" 与库中的npos对应  类外赋值为 -1
	static const int npos; //静态成员常量,表示size_t的最大值(Maximum value for size_t)

};

1、默认成员函数:

1、构造函数:

string(const char* str = "")
   :_size(strlen(str))
{
	_capacity = _size;

    // 多开一个空间存储'\0' 标志字符串结尾
	_str = new char[_capacity + 1];
    
    // strcpy 也会将字符串的'\0'拷贝入,所以不用手动设置'\0'
	strcpy(_str, str);
}

2、拷贝构造:

         拷贝构造就算我们不写编译器也会生成一个默认的拷贝构造函数,但是默认的拷贝构造函数进行的是浅拷贝,某些情况下(例如析构时同一块内存析构多次)会产生危害,所以需要我们手动写一个深拷贝。

// 拷贝构造得传引用,否则会产生无限递归,s2(s1)
string(const string& str)
{
	_str = new char[str._capacity + 1];	
    strcpy(_str, str._str);

	_size = str._size;
	_capacity = str._capacity;
}

3、赋值运算符重载:

         赋值运算符 = 也会涉及到浅拷贝的问题,下面提供两种方法,传统法与现代法:

传统法:先释放旧的内存空间,然后再申请新的内存,再进行复制。

// s1 = s2
string& operator=(const string& str)
{
	char* tmp = new char[str._capacity + 1];
	strcpy(_str, str._str);

	// 释放s1原来的空间
	delete[] _str;
	_str = tmp;
	_size = str._size;
	_capacity = str._capacity;

	return *this;
}

现代法:利用构造/拷贝构造创建一个临时对象再用库函数(swap)去进行交换其字符串的内容。

string& operator=(const string& str)
{
	if (this != &str)
	{
		// 创建一个临时变量,这里使用的是构造函数,也可以使用拷贝构造
		string tmp(str._str);

		// 利用库函数直接交换字符串的内容
		std::swap(_str, tmp._str);

		_size = tmp._size;
		_capacity = tmp._capacity;
	}
	return *this;
}

4、析构函数:

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

二、字符串遍历:

1、[ ]符号重载:

         string库中的字符串可以使用下标遍历,那如果想要自己实现下标遍历的话就需要先对[ ]符号进行重载,如下:

注意:对于const对象,是不能进行修改的,如果使用普通对象的[ ]重载的话会面临权限被放大的问题,所以这里可以用函数重载重载一个const对象使用的即可。

char& operator[](size_t pos)
{
	assert(pos < _size); // 引用做返回值还可以这样检查越界

	return _str[pos]; // _str这个成员是存放堆上的,出了作用域还在,所以可以用引用返回
}

// 对于const对象无法修改就利用重载:
const char& operator[](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

2、迭代器iterator:

         要写迭代器之前还要解决对应的begin() 和 end() 的问题,这里可以直接使用typedef去改名,这里也需要注意普通对象和const对象要分开写。

typedef char* iterator;

// 普通对象
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}

// const对象
typedef const char* const_iterator;
const_iterator begin() const
{
	return _str;
}
const_iterator end() const
{
	return _str + _size;
}

 关于范围for:

如果按照上面代码这样写的那么使用范围for可以不做任何修改也能跑

// 就当前模板不做任何修改,照样跑得过
for (char c : s1)
{
	cout << c << " ";
}
cout << endl;

        那是因为范围for的本质其实就是自动替换,底层还是一个迭代器,只不过这个替换比较智能,普通对象走普通对象迭代器,const对象走const对象的迭代器,但是只支持对应替换,如果将自己写的begin改成Begin就无法跑了。

三、修改操作:

1、reserve:

         reserve(n)的功能为可以自动扩容如果_capacity小于n,则进行扩容操作,如果小于n则不做修改,所以我们可以写一个reserve函数来完成我们的扩容操作。

具体操作为:开新空间 -> 拷贝数据 -> 释放旧空间 -> 指针指向新空间。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		// 开新空间  +1预留'\0'的位置
		char* tmp = new char[n + 1];

		// 拷贝数据
		strcpy(tmp, _str);

		// 释放旧空间
		delete[] _str;

		// 指针指向新空间
		_str = tmp;

		_capacity = n;
	}
}

2、push_back与append: 

push_back:  

void push_back(char ch)
{
	// 检查扩容
	if (_size == _capacity)
	{
		// 可以直接运用三目操作符,如果字符串为空则开4个空间,如果不为空则扩容成原容量的2倍
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	// 字符串原'\0'的位置变成传入的字符
	_str[_size] = ch;
	++_size;

	// 补齐字符末尾的'\0'
	_str[_size] = '\0';
}

append:

void append(const char* str)
{
	// 检查是否需要扩容
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	// 直接利用strcpy函数达到尾插字符串的目的
	strcpy(_str + _size, str);
	_size += len;
}

运算符 += 重载:

         在原string库中的 += 符号不仅可以跟字符,也可以跟字符串,以及另一个string类的对象,对于字符和字符串可以直接调用push_back和append,尾插string类对象其实也是沿用append函数的思路。

// += 尾插字符
string& operator+=(char c)
{
	push_back(c);
	return *this;
}

// += 尾插字符串
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

// += 尾插string类对象
string& operator+=(const string& str)
{
	// 检查扩容:
	if (_size + str._size > _capacity)
	{
		reserve(_size + str._size);
	}
	strcpy(_str + _size, str._str);
	_size += str._size;

	return *this;
}

3、insert与erase:

insert:

         insert函数功能实现的是指定位置插入字符/字符串,这个可以利用重载分开实现。

 指定位置插入字符:

// 指定位置插入删除字符
void insert(size_t pos, char c)
{
	// 限定pos位置在有效数据+1的范围内,当pos = _size时就是直接进行尾插
	assert(pos <= _size);

	// 检查扩容
	if (_size == _capacity)
	{
		// 利用写的reserve函数扩容
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	// 设置结尾,用于挪动数据,+1是因为对应结尾的'\0'也要进行移动
	size_t end = _size + 1;

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

	// 插入数据
	_str[pos] = c;
	_size++;
}

指定位置插入字符串: 

// 指定位置插入字符串
void insert(size_t pos, const char* str)
{
	// 限定pos位置
	assert(pos <= _size);

	size_t len = strlen(str);
	// 如果传入的是空字符串则直接返回
	if (len == 0)
	{
		return;
	}

	// 检查扩容
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	// 将要插入字符位置之后的数据往后移动,或者用循环一个个拷也可以
	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		end--;
	}

	// 利用strncpy函数将对应空缺位置填上
	strncpy(_str + pos, str, len);

	// 更新_size
	_size += len;
}

erase:

        erase(pos, n)函数功能实现的是从pos位置开始删除n个字符,如果不指定右参数,则从pos位置开始删除数据删到结尾。

void erase(size_t pos, size_t len = npos)
{
	// 限定pos位置处于有效数据下标内
	assert(pos < _size);

	// 处理未给右参数以及将pos位置往后的数据全部删除的情况
	if (len == npos || len >= _size - pos)// 隐式类型转换
	{
		_str[pos] = '\0';
		_size = pos;
	}

	// 思路: 直接利用strcpy函数将删除后pos位置后面的数据对pos位置开始往后进行覆盖
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

4、swap:

        对于swap函数,就算我们不写库中也会提供一份以swap函数的模板生成的swap函数,但这个生成的函数并不好用, 因为他是利用三次拷贝(一次拷贝构造,两次赋值) + 一次析构为代价完成两个string类对象的交换。

        我们可以通过直接交换string类对象的成员变量以此来达到交换的目的,这样就可以省去三次的拷贝和析构。

void swap(string& str)
{
	// 这里得要指定std库,因为编译器查找原则是就近原则,会优先找局部的swap函数,也就是我们写的这个
	// 不指定的话就会报参数不匹配的错误
	std::swap(_str, str._str);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}

在日常使用时为了防止误用std库中的swap函数通常会在全局再重载一个swap函数,如下:

void swap(string& str1, string& str2)
{
	// 这里调用的是我们手写的string库中的swap函数
	str1.swap(str2);
}

当模板函数和具体函数同时出现在全局的时候,调用时优先找具体函数。

5、find与substr:

find 函数:

         find(c, pos) / find(sub, pos) 函数功能为从pos位置(pos 默认为0)开始查找字符/字符串,到返回字符/字符串初始位置 的下标,找不到返回npos。

查找字符:

size_t find(char c, size_t pos = 0) const
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}
	return npos;
}

查找字符串:  

size_t find(const char* sub, size_t pos = 0)const
{
	assert(pos < _size);
	// 这里简单些可以直接用库中的strstr函数暴力匹配对应子串
	const char* p = strstr(_str + pos, sub); // 找不到返回空指针

	if (p)
	{
		return p - _str;
	}
	else
	{
		return npos;
	}

}

substr函数: 

         substr(pos, len) 函数的功能为从pos‘位置开始取len个字符,返回初始位置的下标。len默认为npos。

string substr(size_t pos, size_t len = npos)
{
	string sub;

	// 如果len 大于 剩余个数的话就表示为剩下字符有多少取多少
	if (len == npos || len >= _size - pos)
	{
		for (size_t i = pos; i < pos + len; i++)
		{
			sub += _str[i];
		}
	}
	else
	{
		for (size_t i = pos; i < pos + len; i++)
		{
			sub += _str[i];
		}
	}
	return sub;
}

四、流插入/流提取重载:

        流插入/流提取都必须重载为全局函数,否则会发生参数不匹配的问题等。

流插入 << 重载:  

ostream& operator<<(ostream& out, const string& str)
{
	// 这种写法就不需要访问内部成员,可不用友元声明
	for (char c : str)
	{
		out << c;
	}
	return out;
}

流提取 >> 重载:

// clear 不清空间只清数据
void clear()
{
	_size = 0;
	_str[_size] = '\0';
}

istream& operator>>(istream& in, string& str)
{
	// 这里得要调用clear函数将str原有的数据清空,因为in的本质是覆盖
	str.clear();

	char c;
	// 这里不能直接用in >> c 因为cin读取单个字符时是无法读取到空格和换行的
	c = in.get();
	while (c != ' ' && c != '\n')
	{
		str += c;
		c = in.get();
	}

	return in;
}

 函数优化:

        对于流提取函数,其实还可以进行一些小的优化,就是对于开空间扩容这里,因为不知道会输入多少个字符进来,所以一开始也不好直接使用reserve函数进行扩容(空间给大了浪费,少了也一样要多次扩容), 所以我们可以用下面的方式来进行改进:

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

	// buff是在栈上开的空间,且为局部变量
	char buff[128];
	size_t i = 0;
	c = in.get();

	// 将提取的字符放入buff数组中
	while (c != ' ' && c != '\n')
	{
		buff[i++] = c;

		// 输入字符少于128并不会多次开空间,大于128按 (数量 /128 + 1)次开空间
		if (i == 127)
		{
			buff[127] = '\0';
			str += buff;
		}
		c = in.get();
	}

	if (i > 0)
	{
		buff[i] = '\0';
		str += buff;
	}

	return in;
}

getline:

        getline的功能为获取一行的数据,包括空格 ,实现的方法就跟上面的流提取重载一样,只是条件去除了遇到空格停止。

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

	char c;
	c = in.get();
	while (c != '\n')
	{
		str += c;
		c = in.get();
	}

	return in;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值