【C++】string模拟实现

11 篇文章 1 订阅

在这里插入图片描述

🔥个人主页: Forcible Bug Maker
🔥专栏: STL || C++

前言

本篇博客主要内容:实现string类的基本功能

string使用很快就讲完了,但是光会用string还不够,在面试中,面试官总喜欢让我们自己来模拟实现string类,包括string类的构造、拷贝构造、赋值运算符重载以及析构函数等等内容。所以,我认为string类的自实现还是有必要讲一下的。

🔥string类的接口函数

我们本次并不会将string类的所有接口函数逐一讲解,讲一些常用的和重点的。本次string的实现分成了两个文件,一份是string.h,一份string.cpp
看看string.h的内容:

# define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using std::cout;
using std::cin;
using std::endl;
namespace ForcibleBugMaker
{
	class string 
	{
	public:
		// 交换,非成员函数
		friend void swap(string& s1, string s2);

		// 定义迭代器
		typedef char* iterator;
		typedef const char* const_iterator;
		//迭代器获取
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;

		// string默认成员函数
		string(const char* str = "");
		string(const string& s);
		string& operator=(string tmp);
		~string();

		// 获取只读字符串
		const char* c_str() const;

		// 获取容量
		size_t size() const;
		size_t capaity() const;

		// []获取元素重载
		char& operator[](size_t pos);
		const char& operator[](size_t pos) const;

		// 开辟空间
		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 = 0, size_t len = npos);

		// 查找字符或字串
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);

		// string对象比较
		bool operator>(const string& str) const;
		bool operator==(const string& str) const;
		bool operator>=(const string& str) const;
		bool operator<(const string& str) const;
		bool operator<=(const string& str) const;
		bool operator!=(const string& str) const;

		// 获取string对象字串
		string substr(size_t pos, size_t len);

		// 交换函数,成员函数
		void swap(string& str);

		// 清除串中内容
		void clear();

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
		// 常量成员
		const static size_t npos;
	};

	// 交换函数,非成员函数
	void swap(string& s1, string s2);

	// 流插入和流提取重载,非成员函数
	std::ostream& operator<<(std::ostream& so, const string& str);
	std::istream& operator>>(std::istream& is, string& str);
}

在实现string类添加和拷贝的一些函数中,使用了C语言中的一些库函数,以此方便实现,这些函数在C++中统一存放在<cstring>
如果对C语言的一些字符串函数不太了解,可以看看我之前写的一篇博客:C语言-字符串函数,相信会对你有所帮助。

🔥string类的模拟实现

接下来进入主要内容,按照string.h的接口开始实现。

以下接口都是放在命名空间里的,不同文件的相同命名空间在编译时会自动合并。

swap交换

如果你仔细观察,会发现存在两个swap交换函数,一个string中的成员函数,另一个是非成员函数。
在std的默认swap当中,是这样的:
在这里插入图片描述

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

当我们使用这个成员函数进行交换时,会造成拷贝消耗,我们提供对应的非成员函数重载是为了防止C++程序员掉坑。
成员函数的swap:

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

非成员函数的swap:

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

默认成员函数

默认成员函数,就是不提供编译器自动会生成的一些函数。我们这里实现构造函数,析构函数和赋值运算符重载

构造函数:

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

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

构造函数提供了两个,分别支持了字符串构造和string对象构造。_str指向通过new开辟空间,这个空间需要比实际的capacity大,因为需要在字符串末尾多存放一个'\0'

析构函数:

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

这个没什么好说,释放空间,指针置空,_size和_capacity置0。

赋值运算符重载:

string的赋值属于深拷贝。

拷贝分为深拷贝和浅拷贝:
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享
深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

浅拷贝:
在这里插入图片描述
深拷贝:
在这里插入图片描述
如果中规中矩的来写(传统版赋值重载):

String& operator=(const String& s)
{
	if (this != &s)
	{
		char* pStr = new char[strlen(s._str) + 1];
		strcpy(pStr, s._str);
		delete[] _str;
		_str = pStr;
	}
	return *this;
}

但如果你能对之前实现的代码进行复用,你会发现这个过程可以简化非常多(现代版赋值重载)

string& string::operator=(string tmp)
{
	// 调用成员函数的swap,交换*this和tmp
	swap(tmp);
	return *this;
}

在C++的编写中,学会复很有必要。

迭代器

之前讲过,迭代器不一定是指针,但是你可以把它想象成指针,可以通过正常的运算操作来控制其指向的元素。在string中,我们使用指针来模拟实现迭代器。

迭代器在不同编译器下的实现方式有所不同,比如VS下的迭代器就不是一个指针,而是一个类模板。

在这里插入图片描述
在类中需要typedef一下,可以看到我们所实现的string迭代器的本质:

// 定义迭代器
typedef char* iterator;
typedef const char* const_iterator;

获取迭代器的接口:

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

这时,我们已经可以使用我们自己的迭代器了:

string str("hello world!");
string::iterator it = str.begin();
while (it != str.end()) {
	cout << *it << " ";
	++it;
}
cout << endl;

在这里插入图片描述

获取容量和内容信息

这些都是返回str类型状态和内容的函数。

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

size_t string::size() const
{
	return _size;
}

size_t string::capaity() const
{
	return _capacity;
}

// 运算符重载
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];
}


都比较简单易懂。

reserve预留空间

在开始字符串增删查改之前,有必要介绍这个函数,使用它可以更好的控制我们string对象的内存管理。

void string::reserve(size_t n)
{
	if (_capacity < n) {
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

当n大于当前容量_capacity的时候,进行空间的开辟,将_str中的内容拷贝到新空间中去,同时delete释放旧空间。

尾插字符和字符串

接口函数,一个是push_back,一个是append。

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 (_capacity < _size + len) {
		reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
}

这两个成员函数都使用了reserve来预留空间。
同时还需要有 运算符重载+= 来实现尾插,用重载的运算符执行这样的操作才是最爽的。

string& string::operator+=(char ch)
{
	push_back(ch);
	return *this;
}

string& string::operator+=(const char* str)
{
	append(str);
	return *this;
}

其实不用再实现一遍了,直接复用就行。
可以简单看一下使用的效果:

string str("hello world!");
char ch = 'T';
const char* s = "hhhhhh";
cout << str << endl;

str += ch;
cout << str << endl;

str += s;
cout << str << endl;

在这里插入图片描述

字符或字符串的插入和删除

这里的逻辑稍微有些复杂,而且还有几个比较容易掉的坑。

// 字符的插入
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size >= _capacity) {
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	// 注意这里为什么要+1,size_t是无符号整型,没有负值
	//当有符号和无符号比较时,统一会被转成无符号
	size_t end = _size + 1;
	while (end > pos) {
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	++_size;
}

// 字符串的插入
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) {
		reserve(_size + len);
	}
	// 注意这里为什么要+len,size_t是无符号整型,没有负值
	//当有符号和无符号比较时,统一会被转成无符号
	size_t end = _size + len;
	while (end > pos + len - 1) {
		_str[end] = _str[end - len];
		--end;
	}
	memcpy(_str + pos, str, len);
	_size += len;
}

// 字符串的删除
void string::erase(size_t pos, size_t len) 
{
	assert(pos < _size);
	// 当pos+len过大,超过_size,则取到末尾
	if (len >= _size - pos) {
		_str[pos] = '\0';
		_size = pos;
	}
	else {
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

使用案例:

string str("hello world!");
char ch = 'T';
const char* s = "hhhhhh";
cout << str << endl;

str.insert(3, ch);
cout << str << endl;

str.insert(3, s);
cout << str << endl;

str.erase(3, strlen(s));
cout << str << endl;

在这里插入图片描述

find查找

查找C语言查找字符串的库也有提供,可以直接使用。

size_t string::find(char ch, size_t pos)
{
	for (size_t i = pos; i < _size; i++) {
		if (_str[i] == ch)
			return i;
	}
	return npos;
}

size_t string::find(const char* sub, size_t pos)
{
	const char* ptr = strstr(_str, sub);
	return ptr - _str;
}

使用案例:

string str("hello world!");
cout << str.find('w') << endl;
cout << str.find("wor") << endl;

在这里插入图片描述

缺省参数在函数声明那里。

比大小运算符重载

C语言中,有一个按能按字典序将字符串比大小的函数——strcmp

bool string::operator>(const string& str) const
{
	return strcmp(_str, str._str) > 0;
}
bool string::operator==(const string& str) const
{
	return strcmp(_str, str._str) == 0;
}
bool string::operator>=(const string& str) const
{
	return *this > str || *this == str;
}
bool string::operator<(const string& str) const
{
	return !(*this >= str);
}
bool string::operator<=(const string& str) const
{
	return !(*this > str);
}
bool string::operator!=(const string& str) const
{
	return !(*this == str);
}

只需要实现前两个,后面的复用就行。

获取子串

使用substr可以获取所需对象字串。

string string::substr(size_t pos, size_t len)
{
	if (pos + len >= _size) {
		string sub(_str + pos);
		return sub;
	}
	else {
		string sub;
		sub.reserve(len);
		for (size_t i = 0; i < len; i++) {
			sub += _str[pos + i];
		}
		return sub;
	}
}

清除clear

可以将对象内容都删除,但_capacity保持不变。

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

流插入和流提取

这两个函数之前在Date类部分实现的时候,定义为了Date类的友元。但是,流插入和流提取其实可以不定义为string类的友元。

// 流插入
std::ostream& operator<<(std::ostream& os, const string& str)
{
	for (int i = 0; i < str.size(); i++)
		os << str[i];
	return os;
}

// 流提取
std::istream& operator>>(std::istream& is, string& str)
{
	str.clear();
	char ch = is.get();
	while (ch != ' ' && ch != '\n') {
		str += ch;
		ch = is.get();
	}
	return is;
}

string对象读取的截断是空格换行,但是cin对象默认是读不到这两个字符的,这就需要使用另一种读取方式:cin.get()
使用案例:

string str;
cin >> str;
cout << str << endl;

在这里插入图片描述

常量成员npos

常量成员的定义需要实现在类的外部,像这样:

const size_t string::npos = -1;

常量的内容是整型(浮点型和字符类型等都不行)时,也可以作为缺省参数进行定义。

结语

本篇博客主要介绍了string类常用接口的实现,包括默认成员函数,迭代器,字符和字符串的插入删除等等内容。
后续博主还会继续分享与STL相关的内容,感谢大家的支持。♥

  • 113
    点赞
  • 111
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 188
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 188
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Forcible Bug Maker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值