C++手写自定义实现string类

     我们知道,C++给我们提供封装好了一个强大的工具类string类
  ,在使用时只需要#include<>即可使用string类提供的各类函数。

那么我们能不能模仿string类来自己手写实现自己的MyString类呢?显然这是可以的。
在开始手写实现MyString类之前,我们要先了解一些操作运算符重载的基本概念和要注意的条件。
一:什么叫重载?
重载分为函数重载和操作运算符重载。
函数重载:函数重载是指一个函数可以和同一个作用域中的其他函数具有相同的名字
,但是这些同名的函数的形参列表必须互不相同。函数名相同但是参数列表至少满足
以下三个条件之一,就可以构成函数重载:
(1)参数的个数不同
(2)参数的个数相同但是参数类型不同
(3)参数个数相同但是参数顺序不同
但是函数的返回类型是不能够构成重载的。
例如:
(1)void func(int a,int b);
(2)void func(int a );
(3)void func(int a,string b);
(4)void func(string b,int a);
(5)int func(int a );
其中
函数(1)(2)(3)(4)均两两构成重载。(1)和(2)是参数个数的不同,(1)和(3)是参数个数相同但是参数类型不同,(3)和(4)是参数个数和参数类型相同但是参数顺序不同,这些都能构成重载,但是函数(5)和函数(2)的函数名,参数个数和参数类型 均相同,只有返回值不同,那在这时在编译时编译器无法区分这两个函数,就会报错。
操作运算符重载:运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同的类型的数据是产生不同的结果。例如用加法运算符“+”可以对整数,单精确度,双精确度的数行加法运算,如“1+2", “1.1f+2.0f”, “2.2+3.2"等。实际上计算机对整数单精确度数,双精确度数的加法运算操作的过程是不同的。这谁因为c++语言对这些运算符针对预定义的数据类型做了适当的重载,使其能在不同的数据类型中进行不同的数据操作。而这些c++中预定义的运算符操作对象只能是基本的数据类型。但是,对于很多用户自定义的数据类型或除了基本数据类之外的数据型,比如复数类,string类,分数类等,这些数据也需要进行相似的运算操作,这就需要符进行重新定义,赋予已有运算符新的功能。
运算符重载要注意的事项;
在一般情况下,重载的运算符可以既可以作为类的成员函数来定义,也可以作为类的友元函数,但是有几个运算符是必须要注意的特例:
(1)赋值运算符“=“,函数调用运算符“()“,下标运算符“[]” ,成员访问运算符“->“在重载时必须定义为类的成员变量。
(2)插入运算符”>>" 和抽取运算符“<<"不能作为类的成员函数,只能定义为类的友元函数。
(3:成员访问运算符“.“,成员指针访问运算符““,域运算符”::“,长度运算符“sizeof",条件运算符“? :“,这几个是不能重载的。这是因为成员访问运算符“.“和成员指针访问运算符““不能重载保证了C++中访问成员功能的含义不能改变,如果重载了就有可能无法访问类的成员从而导致程序出错。域运算符”::“和长度运算符“sizeof"的操作数是数据类型,而不是普通的变量或者表达式,所以不具备重载的特征。
运算符的重载有以下几条主要的规则:
(1)不能定义新的运算符。c++中除了少数的这几个运算符,其他操作符都能重载。
(2)运算符必须与当前的类有一定关系。也会是说重载的运算符的参数个数不能全部是预定义的数据类型。重载的运算符至少有一个参数的类型是与自定义的类有关的,这就要求这个参数或者是这个类的对象,或者这个类的引用或者是 这个类的指针。
(3)重载的时候,运算符的优先级,结合性以及操作数的个数是不能改变的。由于重载时不能改变运算的个数,因此定义为类的成员函数的单目运算符是没有参数的,而定义为友元类的函数时则有一个参数;同样,双目运算符如果被定义为类的成员函数,则只有一个参数,而定义为类的友元函数则有两个参数。
(4)除了赋值运算符“=“ 外,所有的其他函数都可以被派生类继承。
(5)应当使重载的运算符实现的功能类似于该运算符作用于标准的数据类型时所实现的功能。
以上便是关于运算符重载的基本要点。了解了这些要点,我们便开始对String进行手动实,在实现string类的过程中重写几个常见的运算符,从而达到 实现简易的string类。
首先,在测试中,我们要对MyString类进行测试:


#include
#include "MyString.h"
using namespace std;
int main()
{
	MyString s1("123");
	cout <<"s1:"<< s1;
	MyString s2 = "456";
	cout << "s2:" << s2;
	MyString s3(s1);
	cout << "s3:" << s3;
	MyString s4 = s2;
	cout << "s4:" << s4;
	MyString s5 = s1 + s2+s3;
	cout << "s5:" << s5;
	s5[1] ='a';	
	cout << "s5:" << s5;
	cin >> s1>>s5; //输入新的s1: 789   ,  s5:abc
	cout << "s1:" << s1 << endl<< "s5:"<

这段代码显然是会出错,这是因为我们都还没开始声明和定义这个MyString类。下面我们开始一一对这些要实现的方法进行定义。下面我们开始先使用声明和定义分离的方式来写出基本的MyString类的成员变量和成员函数。
首先声明MyString类:

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include 
#include
#include
#include
using namespace std;
class MyString
{
public:
	//默认无参构造
	MyString();
	//构造函数
	MyString(int len);
	MyString(const char* s);
	MyString(const MyString &s);
	//重载>>运算符,只能在类外实现
	friend ostream& operator<<(ostream &os,  MyString &s);
	//重载<<运算符,只能在类外实现
	friend  istream& operator>>(istream &is, MyString &s);
	//重载()运算符
	MyString& operator()(const MyString &str);
	//重载=运算符
	MyString& operator=(const MyString &str);
	//重载+运算符
	MyString operator+(const MyString &str);
	//重载[]运算符
	 char& operator[](int index);
	~MyString();
private:
	//长度
	int len;
	//S指针
	char *str;
};

至于为何要这样声明呢,下面笔者会对这些成员变量和成员函数以及相应的头文件进行讲解。

``首先是:#pragma once
#pragma once一般由编译器提供保证:同一个文件不会被包含多次。这里所说的”同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。无法对一个头文件中的一段代码作#pragma once声明,而只能针对文件。此方式不会出现宏名碰撞引发的奇怪问题,大型项目的编译速度也因此快了一些。缺点是如果某个头文件有多份拷贝,此方法不能保证它们不被重复包含。在C/C++中,#pragma once是一个非标准但是被广泛支持的方式。

#define _CRT_SECURE_NO_WARNINGS:
我们在编译老的用C++
语言的函数可能因为一些老的.cpp文件使用了strcpy,scanf,prinf ,等不安全的函数,而报警告和错误,而导致无法编译通过。在VS2017中为了防止在编译时出现报错而无法通过编译,我们此时有两种解决方案:

1.在指定的源文件的开头定义:#define _CRT_SECURE_NO_WARNINGS (只会在该文件里起作用)

2.在项目属性里设置,这会在整个项目里生效,依次选择:属性->配置属性->C/C++ ->预处理器->预处理器定义->编辑最下面加上一行:_CRT_SECURE_NO_WARNINGS (注意不需要#define)
在手动实现简易的MyString类时我们会使用到系统提供的string类中的一些strcpy等函数,所以要将这个问题解决掉,这里使用的是第一种方法。
下面这几个头文件和命名空间都是必须的
#include
#include:使用strcpy(),strlen()这些函数时需要
#include:在重写“>>"运算符时需要
#include:在重写“<<"符时需要
using namespace std;
对于MyString类的成员变量:
private:
//长度
int len;
//str指针
char *str;
我们知道string的底层就是使用char[] 实现的,所以的话我们就需要使用char *的指针来指向一个char数组的空间,用len作为char []的长度。当然,事实上string里边底层的char[] 是以‘\0’这个字符串结尾的,所以在给str这个指针开辟char[] 的空间时,char[]数组的长度len要加上1来装载‘\0’这个默认的空字符。
public:
MyString();//默认无参构造
无参构造是为了使在没有给MyString对象给出参数时不报错。如 MyString str;这时候就会调用MyString()这个无参构造从而 不会出错,否则若是没有MyString()这个无参构造的话MyString str;会找不到相应的构造函数而报错。
MyString(int len);//构造函数
传入长度为 len,给str这个指针开辟长度为 len+1的空间来给 char[]。

MyString(const char* s); 
传入一个字符指针s,这个字符指针指向的空间存储的是一个字符串,
借助这个来对str进行构造得到相应的字符串。记住这里要加const这个关键字,
防止传入的指针s指向的字符串被修改,提高程序的安全性。特别是,
当我们传的参数是	MyString s1("123");这个时,由“123"是cost char*类型的,
若我们的这个构造函数是少了一个const关键字的	String(char* s); 那么编译器
就会在编译时报错:“初始化”: 无法从“const char [4]”转换为“MyString”	,这是因
为String(char* s); 中的参数是一个安全性低的普通指针s,而传入的	“123"的指针
是一个const char*	常量指针,安全性更高,所以不能把一个安全性高的指针赋
给一个安全性的指针,因此会报错。但是却可以将一个安全性低的指针赋给一个
安全更高的指针,所以我们要使用const这个关键字。

MyString(const MyString &s); //默认的拷贝构造
	为了防止调用默认的拷贝构造函数进行浅拷贝导致已经开辟空间的str指针在释放时被重复释放造成运行出错,所以要提供一个显式示的深度拷贝构造函数。
	

friend  istream& operator>>(istream &is, MyString &s);
//重载>>运算符,只能在类外实现

根据重载的规则我们知/重载>>运算符,必须要是友元类,返回的是一个引用,这样我们就可以进行连续的使用这个>>,如 cin>>str1>>str2; 若是返回的是一个匿名对象istream ,即
friend istream operator>>(istream &is, MyString &s);那么在 cin>>str1>>str2;时编译直接出错:C++手写自定义实现string类_第1张图片
所以我们要返回的是一个istream的引用。
而对于参数MyString &s,我们必须要使用 引用,这是因为,我们通过对象MyString s
引用的地址,在函数里输入 s的值时,s对应的内存空间保存的字符串也会改变,通过按引用传递来实现输入s的值后传入的形参s对应的原目标对象的值也会改变,从而达到输入的目的。比如:
MyString s(“123”);
cin>>s;//输入的值为 456
cout< 那么修改后的s的值为s=“456”;输出的值自然就是字符串“456“。
如果我们使用的是fiend istream& operator>>(istream &is, MyString s);由于传递的MyString s是一个普通对象,根据按值传递,我们知道
,MyString s(“123”);
cin>>s;//输入的值为 456
相当于函数istream& operator>>(istream &is, MyString s);调用结束后,s的值没被改变,依旧是s=“123”.
cout< 输出的值自然就是字符串“123"。
所以我们要使用friend istream& operator>>(istream &is, MyString &s);才行。
friend ostream& operator<<(ostream &os, MyString &s); //重载<<运算符,只能在类外实现
对于这个输出运算符,与输入运算符类似,返回的是一个引用,至于参数 MyString s
这个是一个引用或者普通对象都可以。这是因为输出是不需要改变对象s的数据的,只需要读取到s的数据即可。所以friend ostream& operator<<(ostream &os, MyString &s)或者friend ostream& operator<<(ostream &os, MyString s)都可以。

//重载()运算符
MyString& operator()(const MyString &str);
拷贝函数,防止参数str被修改,所以要使用const来限定。函数的返回值可以是返回的是一个对象,也可以是一个对象的引用。MyString& operator()(const MyString &str);或者MyString  

operator()(const MyString &str),形参可以是一个MyString对象也可以是一个MyString对象的的引用。

//重载=运算符
MyString& operator=(const MyString &str);

这个赋值操作符的返回类型可以是一个MyString对象的引用也可以是一个 MyString对象。
至于参数的话可以是一个MyString对象的引用也可以是一个 MyString对象。但是要加const这个关键字防止被修改。

MyString operator+(const MyString &str);//重载+运算符

传递的参数可以是 MyString的普通对象,也可以是 MyString对象的引用,而返回类型必须是 一个匿名的MyString对象,不能是引用,否则
若返回的是一个MyString对象的引用MyString& operator+(const MyString &str); ,则在 MyString s5 = s1 + s2+s3;时运行
就会报错。
C++手写自定义实现string类_第2张图片

 char& operator[](int index);	//重载[]运算符
 传入的是一个索引,返回的是一个char引用,这样便能达到修改str中的元素的作用。如果返回的是一个char  ,	 即char operator[](int index);那么编译时  s5[1] ='9';	就会报错,提示 error C2106: “=”: 左操作数必须为左值。

~MyString();  //析构函数,用于释放str指针指向的空间

理解了MyString中的成员变量和成员函数,下面我们便开始一个一个地实现这些函数。

首先是 ;
默认无参构造:MyString();的实现

MyString::MyString()
{
	this->len = 0;
	this->str = NULL;
}

其次/构造函数:MyString(int len);的实现。
先判定传入的参数是否是合理的,如果不合理就直接
this->len = 0;
this->str = NULL;
然后return;
如果参数合理,那就给str指针开辟长度为 len+1的空间,完整代码如下:

MyString::MyString(int len)
{
	//如果传入的len不合理
	if (len<=0)
	{
		this->len = 0;
		this->str = NULL;
		return;
	}
	//如果传入的len合理
	else
	{
	this->len = len;
	this->str = new char[this->len + 1];
	}
}

MyString(const char* s)的实现:

MyString::MyString(const char * s)
{
if (s==NULL)
{
this->len = 0;
this->str = new char[0 + 1];
strcpy(this->str, “”);
}
else
{
int Len = strlen(s);
this->len = Len;
this->str = new char[this->len + 1];
strcpy(this->str, s);
}
}

MyString(const MyString &s);的实现:

MyString::MyString(const MyString & s)

{
this->len = s.len;
this->str = new char[this->len + 1];
strcpy(this->str,s.str);
}

MyString & MyString::operator()(const MyString & s)的实现:
这个相当于深拷贝
MyString & MyString::operator()(const MyString & s)
{
if (this->str!=NULL)
{
this->len = 0;
delete this->str;
this->str = NULL;
}
this->len = s.len;
this->str = new char[this->len + 1];
strcpy(this->str,s.str);
return *this;
}
在重载=运算符时注意判定传进来的参数是不是本身,如果是本身,则直接返回,若缺少是这个判定,在运行时若是传的参数是本身,那么就会在运行时报错。所以要特别注意!!!!
MyString MyString::operator=(const MyString & s)
{
if (this==&s)
{
return *this;
}
if (this->str!=NULL)
{
this->len = 0;
delete this->str;
this->str = NULL;
}
this->len = s.len;
this->str = new char[this->len + 1];
strcpy(this->str, s.str);
return *this;
}

MyString MyString::operator+(const MyString & s)的实现:、

MyString MyString::operator+(const MyString & s)
{
int Len = this->len + s.len;
MyString temp(Len);
strcpy(temp.str, this->str);
strcat(temp.str, s.str);
return temp;

}

char& MyString::operator[](int index)的实现:
char& MyString::operator[](int index)
{
return this->str[index];
}

//析构函数
MyString::~MyString()
{
if (this->str!=NULL)
{
delete str;
str = NULL;
this->len = 0;

}

}

//重写<<运算符,返回的是ostream &
ostream & operator<<(ostream & os, MyString & s)
{
//输出字符串
os<< s.str << endl;
return os;
}
//重写<<运算符,返回的是istream &
在重载这个>>运算符时注意先判定s.str是否是空的,如果是非空的,那么就要先释放s.str的内存,防止内存泄露,然后再借助一个临时的char[] temp 来作为缓冲容器先读取cin输入的内容,然后再重新给s.str开辟空间后将缓冲容器的内容复制给s.str指向的空间。
istream & operator>>(istream & is, MyString & s)
{
if (s.str != NULL)
{
delete s.str;
s.str = NULL;
s.len = 0;
}
char temp[4096] = { 0 };
is >> temp;
int len = strlen(temp);
s.str = new char[len + 1];
strcpy(s.str, temp);
s.len = len;
return is;
}
这样我们就写完了这些基本的MyString类的相关函数,当然,系统封装的string中还要很多方法,想继续写的小伙伴们也可以继续自己手动实现,笔者就不再一一实现了。下面给出完整的源代码,这是基于Vs2017的:

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include 
#include
#include
#include
using namespace std;
class MyString
{
public:
	//默认无参构造
	MyString();
	//构造函数
	MyString(int len);
	MyString(const char* s);
	MyString(const MyString &s);
	//重载>>运算符,只能在类外实现
	friend ostream& operator<<(ostream &os,  MyString &s);
	//重载<<运算符,只能在类外实现
	friend  istream& operator>>(istream &is, MyString &s);
	//重载()运算符
	MyString& operator()(const MyString &str);
	//重载=运算符
	MyString operator=(const MyString &str);
	//重载+运算符
	MyString operator+(const MyString &str);
	//重载[]运算符
	 char& operator[](int index);
	~MyString();
private:
	//长度
	int len;
	//S指针
	char *str;
};


#include "MyString.h"


//默认构造函数
MyString::MyString()
{
	this->len = 0;
	this->str = NULL;
}
//构造函数
MyString::MyString(int len)
{
	//如果传入的len不合理
	if (len<=0)
	{
		this->len = 0;
		this->str = NULL;
		return;
	}
	//如果传入的len合理
	else
	{
	this->len = len;
	this->str = new char[this->len + 1];
	}
}
//构造函数
MyString::MyString(const char * s)
{
	if (s==NULL)
	{
		this->len = 0;
		this->str = new char[0 + 1];
		strcpy(this->str, "");
	}
	else
	{
		int Len = strlen(s);
		this->len = Len;
		this->str = new char[this->len + 1];
		strcpy(this->str, s);
	}
}
//构造函数
MyString::MyString(const MyString & s)
{
	this->len = s.len;
	this->str = new char[this->len + 1];
	strcpy(this->str,s.str);
}



//重写()运算符,返回的是MyString &
MyString & MyString::operator()(const MyString & s)
{
	if (this->str!=NULL)
	{
		this->len = 0;
		delete this->str;
		this->str = NULL;
	}
	this->len = s.len;
	this->str = new char[this->len + 1];
	strcpy(this->str,s.str);
	return *this;
}
//重写=运算符返回的是MyString &
MyString  MyString::operator=(const MyString & s)
{
	if (this==&s)
	{
		return *this;
	}
	if (this->str!=NULL)
	{
		this->len = 0;
		delete this->str;
		this->str = NULL;
	}
	this->len = s.len;
	this->str = new char[this->len + 1];
	strcpy(this->str, s.str);
	return *this;
}
//重写+运算符,返回的是匿名对象
MyString  MyString::operator+(const MyString & s)
{
	int Len = this->len + s.len;
	MyString temp(Len);
	strcpy(temp.str, this->str);
	strcat(temp.str, s.str);
	return temp;
	
}
//重写[]运算符,返回的是char的引用
char& MyString::operator[](int index)
{
	return this->str[index];
}



//析构函数
MyString::~MyString()
{
	if (this->str!=NULL)
	{
	delete str;
	str = NULL;
	this->len = 0;

	}
}
//重写<<运算符,返回的是ostream &
ostream & operator<<(ostream & os, MyString & s)
{
	//输出字符串
	os<< s.str << endl;
	return os;
}
//重写<<运算符,返回的是istream &
istream & operator>>(istream & is, MyString & s)
{
	if (s.str != NULL)
	{
		delete s.str;
		s.str = NULL;
		s.len = 0;
	}
	char temp[4096] = { 0 };
	is >> temp;
	int len = strlen(temp);
	s.str = new char[len + 1];
	strcpy(s.str, temp);
	s.len = len;
	return is;
}

测试:


#include
#include "MyString.h"
using namespace std;
int main()
{
	MyString s1("123");
	cout <<"s1:"<< s1;
	MyString s2 = "456";
	cout << "s2:" << s2;
	MyString s3(s1);
	cout << "s3:" << s3;
	MyString s4 = s2;
	cout << "s4:" << s4;
	MyString s5 = s1 + s2+s3;
	cout << "s5:" << s5;
	s5[1] ='a';	
	cout << "s5:" << s5;
	cin >> s1>>s5; //输入新的s1: 789   ,  s5:abc
	cout << "s1:" << s1 << endl<< "s5:"<

运行后截图:
C++手写自定义实现string类_第3张图片

欢迎各位看官在底下评论斧正。

你可能感兴趣的:(c++技术文档)