C++ STL --- string类模拟实现

目录

1.C语言必备函数

2.构造模块

  (1)构造函数

    [1]构造函数一

    [2]构造函数二

  (2)析构函数 

  (3)拷贝构造函数

    [1]传统版本

    [2]简洁版本

  (4)赋值运算符重载 

    [1]传统版本

    [2]简洁版本

3.容量模块

  (1)有效字符函数 

  (2)有效字符函数

  (3)扩容函数 

  (4)设置有效字符函数 

  (5)判空函数 

  (6)清空函数 

4.迭代器模块

  (1)正向迭代器

  (2)反向迭代器

5.访问模块

  (1)重载[]

  (2)重载cout<< 

6.修改模块

  (1)尾插单个字符

  (2)重载+=

7.特殊操作模块

  (1)转为C语言字符串类型

  (2)复制函数

  (3)正向查找函数

  (4)反向查找函数

8.总体实现


         在前面的文章中已经验证了,在涉及资源管理的类中,拷贝构造函数,析构函数,赋值运算符重载函数都需要用户显示实现,如果使用编译器默认生成的,就会有浅拷贝的风险。如果有读者不理解浅拷贝,可以看下面这幅图。

        浅拷贝通俗来讲就是多个对象指向了同一块空间,例如s1与s2同时指向此空间,当s1对象中的内容修改后,s2中的内容也随之被修改了。但这不是最严重的,最严重的是如果将s1对象销毁,这块空间将被释放,可是s2还在,如果它要对自己的成员变量做修改或是销毁s2,毫无疑问程序会崩溃,因为它此时已经是个野指针了,指向了一块被释放的空间。

        在string类的模拟实现中,主要有六大模块组成:构造模块、容量模块、迭代器模块、访问模块、修改模块、特殊操作模块。因为string类的操作太多,我们只实现其中最常用的功能即可。(本文代码均在win10系统下的vs2019下运行成功)

        在六个模块中的函数均为一部分代码,不能单独运行,总体中的代码才可以运行。   

        在准备模拟string类之前,需要先来回顾一下C语言中用来操作字符串的函数:strlen、strcpy、strncpy、strcat、memset、memcpy。这些函数在模拟string类的时候都要用到。     

1.C语言必备函数

函数名称函数功能
size_t strlen(const char *str)
计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
char *strcpy(char *dest, const char *src)

src 所指向的字符串复制到 dest

注意dest指向的空间要足够大。

char *strncpy(char *dest, const char *src, size_t n)
src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。
void *memset(void *str, int c, size_t n)
复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
void *memcpy(void *str1, const void *str2, size_t n)
从存储区 str2 复制 n 个字节到存储区 str1
strcat(char* s1,char* s2)src 所指向的字符串追加到 dest 所指向的字符串的结尾。

        在回顾函数之前,需要说明的是,在vs2019中这样定义一个字符串是会报错的:char* str = "abc";解决办法如下代码一:

        代码一:

//代码一
#include "iostream"
using namespace std;
#include <string.h>

int main() {
	//方法1
	const char* s1 = "abc";
	cout << s1 << endl;

	//方法2
	char s2[] = { "1234" };
	char* p2 = s2;
	cout << p2 << endl;

	//方法3
	char* s3 = new char[100]{ "asd" };
	cout << s3 << endl;
}

        解决上述问题后,开始回顾函数,六个函数的使用都在代码二中了: 使用这些函数时,请加上#define _CRT_SECURE_NO_WARNINGS,否则编译器会因为这些函数不安全而报错。

        代码二:

//代码二
#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;
#include <string.h>

int main() {
	char* s1 = new char[100]{ "12345" };
	char* s2 = new char[100]{ "abcde" };
	char* s3 = new char[100];

	//strlen 计算s1有效元素个数
	int len = strlen(s1);
	cout << len << endl;//5

	//strcpy 把s2拷贝到s1中
	strcpy(s1, s2);
	cout << s1 << endl;//abcde

	//strcpy 把s1的前2个字符拷贝到s3中
	strncpy(s3, s1, 2);
	s3[2] = '\0';
	cout << s3 << endl;//ab

	//memset 把1个 '9'字符复制到s1的前1个位置
	memset(s1, '9', 1);
	cout << s1 << endl;//9bcde

	//memcpy 从s1中复制4个字节到s3中
	memcpy(s3, s1, 4);
	s3[4] = '\0';
	cout << s3 << endl;//9bcd

	//strcat 把s3指向的字符串追加到s1指向的字符串结尾
	strcat(s1, s3);
	cout << s1 << endl;//9bcde9bcd
}

2.构造模块

  (1)构造函数

        这里实现两个构造函数。

        String类中的成员应该有一个字符串指针,一个有效元素个数变量,一个容量大小变量,一个npos变量(因为这是原本的string类中就有的)。为了方便起见,在设计阶段先将所有成员的访问权限均设置为public。同时将构造函数定义成全缺省函数。

    [1]构造函数一

        代码三:使用字符串构造对象

//代码三
#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;

class String {
public:
	char* Str;//字符串指针
	size_t Size;//有效元素个数
	size_t Capacity;//容量
	static size_t npos;
public:
	String(const char* s = "") {
		//将指向空的指针指向空字符串
		if (s == nullptr)
			s = "";
		//注意要比申请比字符串长度多1的空间来存储 '\0'
		Str = new char[strlen(s) + 1];
		//将s指向的空间中的内容拷贝到Str指向的空间中,这个操作会把'\0'一并拷贝
		strcpy(Str, s);
		//更新有效元素个数和容量
		Size = strlen(s);
		Capacity = strlen(s);
	}
};

size_t String::npos = -1;

    [2]构造函数二

        代码四:使用n个字符构造对象

//代码四
String(size_t n, char c) {
	//申请比有效元素多一个的空间
	Str = new char[n + 1];

	//把n个c存入Str指向的空间中
	memset(Str, c, n);

	//不要忘记设置'\0'
	Str[n] = '\0';

	//更新有效元素和容量
	//容量要保持比有效元素个数多
	//用来存储'\0'
	Size = n;
	Capacity = n+1;
}

  (2)析构函数 

        代码五:析构函数一定要检查字符串指针是否指向空,如果是空就不可以再释放了。

//代码五
~String() {
	//如果指向空值,释放程序会崩溃的
	if (Str)
		delete[] Str;
	Str = nullptr;
	Size = 0;
	Capacity = 0;
}

  (3)拷贝构造函数

        这里提供两种版本的拷贝构造函数以供参考。

    [1]传统版本

        代码六:传统版本是先释放指向的旧空间,构造新空间,然后使用strcpy函数拷贝数据。

        注意这样有个缺点,虽然概率很小。万一申请空间失败呢?那样就连原有的数据都没有了。

//代码六
String(const String& s) {
	//需要释放原本的空间
	if (Str) {
		delete[] Str;
	}
	Str = new char[strlen(s.Str) + 1];//+1是为了存储'\0'
	strcpy(Str, s.Str);
	Size = strlen(Str);
	Capacity = Size+1;//+1是因为申请空间时就申请了这么多
}

    [2]简洁版本

        代码七: 简洁版本是先使用目标字符串构造一个临时对象,然后交换两个对象的字符串指针。这样做不但不用担心空间申请失败,也不用自己手动释放空间。当临时对象生命周期结束,系统会自动调用析构函数。

//代码七
String(const String& s) {
	String temp(s.Str);
	//交换temp.Str和this->Str指向的空间
	std::swap(Str, temp.Str);
	Size = strlen(Str);
	Capacity = Size+1;
}

  (4)赋值运算符重载 

        赋值运算符重载也提供了两个版本以供参考。

    [1]传统版本

        代码八:传统版本需要考虑是不是对象自己给自己赋值,若是就直接返回。一定要记得开辟空间时,要比字符串长度大,这是为了存储'\0'

//代码八
String& operator=(const String& s) {
	//判断是否是自己给自己赋值
	if (this != &s) {
		String temp(Str);
		//记得释放原本指向的空间
		delete[] Str;
		Str = new char[strlen(s.Str) + 1];
		strcpy(Str, s.Str);
		Size = strlen(s.Str);
		Capacity = Size+1;
	}
	return *this;
}

    [2]简洁版本

        代码九:简介版本不需要考虑自己给自己赋值,反正都是交换空间。

//代码九
String& operator=(const String& s) {
	String temp(s.Str);
	//交换空间
	std::swap(Str, temp.Str);
	Size = strlen(s.Str);
	Capacity = Size +1;
	return *this;
}

3.容量模块

  (1)有效字符函数 

        代码十:

//代码十
size_t SIZE() {
	return Size;
}

  (2)有效字符函数

        代码十一: 

//代码十一
size_t CAPAcity() {
	return Capacity;
}

  (3)扩容函数 

        代码十二:这里直接按照参数的两倍来扩容,记得需要释放原本的字符串空间。

//代码十二
void Reverse(size_t newCapacity) {
	//新容量大于旧容量
	if (newCapacity >= Capacity) {
		char* temp = new char[newCapacity *2];
		strcpy(temp, Str);
		delete[] Str;
		Str = temp;
		Capacity = 2 * newCapacity;
	}
}

  (4)设置有效字符函数 

        代码十三:此函数机制为:新有效元素大于旧有效元素时可能需要扩容,继续判断,当新有效元素比容量还要大时需要扩容,扩容后使用memset函数添加元素。最后不要忘了手动加'\0'。

//代码十三
void Resize(size_t newSize, char c) {
	size_t oldSize = Size;
	size_t oldCapa = Capacity;
	//新的有效元素比旧的多
	if (newSize > oldSize) {
		//新元素比容量多 扩容
		if (newSize > oldCapa) {
			Reverse(newSize);
		}
		//往Str尾部插入合适数量的c
		memset(Str + Size, c, newSize - oldSize);
	}
	Size = newSize;
	Str[Size] = '\0';
}

  (5)判空函数 

        代码十四:

//代码十四
bool Empty() {
	//容量等于0返回true
	return Size == 0;
}

  (6)清空函数 

        代码十五:注意清空的含义是清空有效元素,不对容量进行操作。

//代码十五
void clear() {
	//只改变有效元素个数,不改变容量
	Size = 0;
	Str[0] = '\0';
}

4.迭代器模块

        其实迭代器就是当作指针使用的,为了和string迭代器形式相似,我们要用这条语句给指针起个小名:typedef char* Iterator;

  (1)正向迭代器

        代码十六:

//代码十六
typedef char* Iterator;

Iterator Begin() {
	return Str;
}

Iterator End() {
	return Str + Size;
}

  (2)反向迭代器

        代码十七:反向迭代器可以直接复用正向迭代器。 

//代码十七
typedef char* Iterator;

Iterator Rbegin() {
	return End();
}

Iterator Rend() {
	return Begin();
}

5.访问模块

  (1)重载[]

        代码十八:注意一定要判断是否越界访问。同时也要多重载一份为const对象使用。使用assert来控制时要加头文件。#include<assert.h>

//代码十八
#include<assert.h>

char& operator[](size_t pos) {
	assert(pos < Size);
	return Str[pos];
}

const char& operator[](size_t pos)const {
	assert(pos < Size);
	return Str[pos];
}

  (2)重载cout<< 

        代码十九:注意<<不可以重载为成员函数,但我们还要使用私有变量,就把它设置为友元函数。至于为什么不可以重载为成员函数,我在《C++实现日期类》中已经总结了,感兴趣的可以去看看。

//代码十九
friend ostream& operator<<(ostream& _cout, const String& s) {
	_cout << s.Str;
	return _cout;
}

6.修改模块

  (1)尾插单个字符

        代码二十:尾插时需要注意扩容条件。

//代码二十
void Push_back(char c) {
	//满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
	if (Size + 1== Capacity) {
		Reverse(Capacity);
	}
	Str[Size] = c;
	Str[Size+1] = '\0';
	Size++;
}

  (2)重载+=

        代码二十一:+=有两种,+=字符串、+=对象。后者可以复用前者。这里要特别注意如何判断是否要扩容,当两个字符串有效元素加起来再加上'\0'的大小不比容量小时就应该扩容。

//代码二十一

void operator+=(const char* s) {
	size_t oldSize = Size;
	//两个空间加起来元素个数
	size_t size = Size + strlen(s);
	//元素个数接近容量
	if (size+1 >= Capacity) {
		Reverse(size);
	}
	strcat(Str + oldSize, s);
	Size = size;
}

//直接复用上面的
String& operator+=(const String& s) {
	*this += s.Str;
	return *this;
}

7.特殊操作模块

  (1)转为C语言字符串类型

        代码二十二:

//代码二十二
const char* c_str()const
{
	return Str;
}

  (2)复制函数

        代码二十三:注意参数的调整问题。

//代码二十三
String Substr(size_t pos = 0, size_t n = npos) {
    //没有传参
	if (n == npos)
		n = Size;

    //传递的参数个数大于有效元素个数就需要进行调整
	if (pos + n >= Size) {
		n =Size - pos;
	}

	char* temp = new char[n + 1];
	strncpy(temp, Str + pos, n);
	temp[Size] = '\0';

	String Stemp(temp);
	delete[] temp;
	return Stemp;
}

  (3)正向查找函数

        代码二十四:注意没找到就返回npos。npos是-1。

//代码二十四
size_t Find(char c, size_t pos = 0) {
	for (int i = pos; i < Size; i++) {
		if (Str[i] == c)
			return i;
	}
	return npos;
}

  (4)反向查找函数

        代码二十五:

//代码二十五
size_t Rfind(char c, size_t pos = npos) {

    //查找的开始位置越界就将位置定在结尾
	pos = pos < Size ? pos : Size;

	for (int i = pos; i >= 0; i--) {
		if (Str[i] == c) {
			return i;
		}
	}
	return npos;
}

8.总体实现

        以下是String类的模拟实现,在运行程序发现不足之处的,可以在评论区留言。

#define _CRT_SECURE_NO_WARNINGS //解决C语言函数报错
#include "iostream"
#include<assert.h>
using namespace std;

class String {
private:
	char* Str;//字符串指针
	size_t Size;//有效元素个数
	size_t Capacity;//容量
	static size_t npos;
public:
//构造模块
/
	//构造函数1
	String(const char* s = "") {
		//将指向空的指针指向空字符串
		if (s == nullptr)
			s = "";
		//注意要比申请比字符串长度多1的空间来存储 '\0'
		Str = new char[strlen(s) + 1];
		//将s指向的空间中的内容拷贝到Str指向的空间中,这个操作会把'\0'一并拷贝
		strcpy(Str, s);
		//更新有效元素个数和容量
		Size = strlen(s);
		Capacity = strlen(s)+1;
	}
	//构造函数2
	String(size_t n, char c) {
		//申请比有效元素多一个的空间
		Str = new char[n + 1];
		//把n个c存入Str指向的空间中
		memset(Str, c, n);
		//不要忘记设置'\0'
		Str[n] = '\0';
		//更新有效元素和容量
		//容量要保持比有效元素个数多
		//用来存储'\0'
		Size = n;
		Capacity = n+1;
	}
	//析构函数
	~String() {
		//如果指向空值,释放程序会崩溃的
		if (Str)
			delete[] Str;
		Str = nullptr;
		Size = 0;
		Capacity = 0;
	}

	//传统版拷贝构造
	/*
	String(const String& s) {
		//需要释放原本的空间
		if (Str) {
			delete[] Str;
		}
		Str = new char[strlen(s.Str) + 1];
		strcpy(Str, s.Str);
		Size = strlen(Str);
		Capacity = Size+1;
	}
	*/
	//简洁版拷贝构造
	String(const String& s) {
		String temp(s.Str);
		//交换temp.Str和this->Str指向的空间
		std::swap(Str, temp.Str);
		Size = strlen(Str);
		Capacity = Size+1;
	}

	//传统版赋值运算符重载
	/*
	String& operator=(const String& s) {
		//判断是否是自己给自己赋值
		if (this != &s) {
			String temp(Str);
			//记得释放原本指向的空间
			delete[] Str;
			Str = new char[strlen(s.Str) + 1];
			strcpy(Str, s.Str);
			Size = strlen(s.Str);
			Capacity = Size + 1;
		}
		return *this;
	}
	*/

	//简洁版赋值运算符重载
	String& operator=(const String& s) {
		String temp(s.Str);
		//交换空间
		std::swap(Str, temp.Str);
		Size = strlen(s.Str);
		Capacity = Size +1;
		return *this;
	}

//容量模块
/
	//有效元素
	size_t SIZE() {
		return Size;
	}

	//容量
	size_t CAPAcity() {
		return Capacity;
	}

	//扩容函数
	void Reverse(size_t newCapacity) {
		//新容量大于旧容量
		if (newCapacity >= Capacity) {
			char* temp = new char[newCapacity *2];
			strcpy(temp, Str);
			delete[] Str;
			Str = temp;
			Capacity = 2 * newCapacity;
		}
	}

	//设置有效元素
	void Resize(size_t newSize, char c) {
		size_t oldSize = Size;
		size_t oldCapa = Capacity;
		//新的有效元素比旧的多
		if (newSize > oldSize) {
			//新元素比容量多 扩容
			if (newSize > oldCapa) {
				Reverse(newSize);
			}
			//往Str尾部插入合适数量的c
			memset(Str + Size, c, newSize - oldSize);
		}
		Size = newSize;
		Str[Size] = '\0';
	}

	//判空函数
	bool Empty() {
		//容量等于0返回true
		return Size == 0;
	}

	//清空函数
	void clear() {
		//只改变有效元素个数,不改变容量
		Size = 0;
		Str[0] = '\0';
	}
//迭代器模块
/
	typedef char* Iterator;
	//获取正向首指针
	Iterator Begin() {
		return Str;
	}

	//获取正向尾指针
	Iterator End() {
		return Str + Size;
	}
	typedef char* Iterator;
	//获取反向首指针
	Iterator Rbegin() {
		return End();
	}

	//获取反向尾指针
	Iterator Rend() {
		return Begin();
	}
//访问模块
/
	//普通[]重载
	char& operator[](size_t pos) {
		assert(pos < Size);
		return Str[pos];
	}
	//const[]重载
	const char& operator[](size_t pos)const {
		assert(pos < Size);
		return Str[pos];
	}
	// <<重载
	friend ostream& operator<<(ostream& _cout, const String& s) {
		_cout << s.Str;
		return _cout;
	}
//访问模块
/
	//尾插
	void Push_back(char c) {
		//满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
		if (Size + 1== Capacity) {
			Reverse(Capacity);
		}
		Str[Size] = c;
		Str[Size+1] = '\0';
		Size++;
	}
	//+=重载
	void operator+=(const char* s) {
		size_t oldSize = Size;
		//两个空间加起来元素个数
		size_t size = Size + strlen(s);
		//元素个数接近容量
		if (size+1 >= Capacity) {
			Reverse(size);
		}
		strcat(Str + oldSize, s);
		Size = size;
	}
	//+=重载
	String& operator+=(const String& s) {
		*this += s.Str;
		return *this;
	}
//特殊操作模块
/
	//转换C语言字符串
	const char* c_str()const
	{
		return Str;
	}

	//复制
	String Substr(size_t pos = 0, size_t n = npos) {
		if (n == npos)
			n = Size;
		if (pos + n >= Size) {
			n =Size - pos;
		}
		char* temp = new char[n + 1];
		strncpy(temp, Str + pos, n);
		temp[Size] = '\0';

		String Stemp(temp);
		delete[] temp;
		return Stemp;
	}
	//正向查找
	size_t Find(char c, size_t pos = 0) {
		for (int i = pos; i < Size; i++) {
			if (Str[i] == c)
				return i;
		}
		return npos;
	}
	//反向查找
	size_t Rfind(char c, size_t pos = npos) {
		pos = pos < Size ? pos : Size;
		for (int i = pos; i >= 0; i--) {
			if (Str[i] == c) {
				return i;
			}
		}
		return npos;
	}
};

size_t String::npos = -1;

int main() {
	const char* sp = "abc12345566743212343455655";
	String s1(sp);
	String s2 = s1;
	auto it = s2.Begin();
	while (it != s2.End()) {
		cout << *it << " ";
		it++;
	}
}

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值