自制string类型

文章原创,转载需注明原作者。

还未写完,见谅。

目录

第一章 前言和准备工作

1.1.前言

1.2.准备工作

第二章 string类函数——简单版

2.1.最简单的string

2.2.string类型的输入和输出

2.3.查找子串,插入,删除和替换

2.4.at函数和size函数

第三章:运算符重载和构造函数

3.1.赋值运算符

3.2.“+”和“+=”的重载

3.3.比较运算符的重载

3.4.下标运算符的重载

3.5.构造函数

第四章:string类函数(续)

4.1.内存动态分配

4.2.append函数

4.3.begin和end迭代器

4.4.getline函数

第一章 前言和准备工作

1.1.前言

1.1.1.前言

C++语言相比C语言,比较大和好用的变化就是类(class)这一功能。这次,我们尝试来根据类这一新功能,构造一些比较简单的数据结构。这次,我们挑战一下string这一类型。

1.1.2.string是什么

string是C++STL容器中自带的一个库,是关于字符串的。字符串的概念什么的这里就不细讲了,比较简单。有了string,在C++中,就可以更加方便的使用字符串了,相比较于C语言而言,不需要使用strcpy,strcmp等函数,直接使用运算符即可。它的读入和输出和cin,cout结合,也有专门的函数getline等。同时,string也可以和C语言的老版本字符串进行转换,兼容一些老的函数。

1.2.准备工作

1.2.1.工具

一台电脑,一个C++编译器(笔者这里使用的是Dev C++,各位读者可以根据自己的喜好,例如Visual C++,CodeBlocks等)

1.2.2.学习资源

如果有不懂的地方,提供一些参考的在线学习资源。实际笔者也是从这里参考了很多用法。

http://c.biancheng.net/cplus/

https://www.runoob.com/cplusplus/cpp-tutorial.html

http://www.weixueyuan.net/cpp/rumen/

第二章:string类函数——简单版

2.1.最简单的string

string是一个类型,我们构造string类型,也就需要定义一个string类型。定义类型,我们一般在C语言会使用typedef struct的方式,我们这里既然用上了C++,就用一下C++的新功能——类。

类其实是一个类似于结构体的东西,把很多东西打包在一起。类中除了可以包含变量,同时也可以包含成员函数,构造函数,析构函数以及运算符重载等。这里,我们暂且先把这个字符串本身和他的大小信息包含在整个类里面。话不多说,我们看代码。

class String{

public:

char *str;

int size;

};

这里string的s是大写为了避免和标准库的string重名。可以看到,类的定义和结构体的定义类似。public说明这些成员是公共访问的。

接下来,我们需要写成员函数。我们先写最简单的也是最重要的init函数。事实上,string是没有init的,但是在写构造函数之前,我们先用最简单的成员函数的形式写一个init,用于初始化,给指针分配空间。(str是指针,为了使得str长度可变)

class String{

char *str;

int size;

void init(void){

str=(char*)malloc(100);

}

};

首先是成员函数的位置,写在class里面。第二是写法,无需指定所属类,直接写str即可。

如果比较了解C++特性的,可能会问为什么用malloc不用new。这里用malloc是为了方便扩大空间(扩大要用realloc),这样数组就是可变的了。

本文之后为了方便和节省空间,就不把整个代码贴出来了,只写本次要写的函数,大家注意一下函数的位置。

接下来,为了让标准函数可以访问(例如printf),需要一个函数用于把这个字符串转换为C风格字符串,也就是c_str函数。

const char *c_str(void){

return str;

}

这样,以后用printf就方便了。

2.2.string类型的输入和输出

2.2.1.输入

string自带的是使用cin进行输入,但是我们这里也没法用cin,只能简单的写一个get函数来实现输入。为了方便,get可以包含多种版本。各位读者也可以根据自己的喜好来设计更多版本。

版本1 get()输入字符串,空格停止

版本2 get(字符个数)输入字符串,到达字符个数停止

版本3 get(停止字符)输入字符串,输入时遇到停止字符停止

C++有函数重载功能,多个函数只要参数不同,就会当作不同函数处理。C++编译器会根据类型来判断调用哪一个函数。

void get(void){

scanf("%s",str);

}

void get(int n){

fgets(str,n,stdin);

}

void get(char c){

int i=0;

for(;;){

str[i]=getchar();

if(str[i]==c)break;

++i;

}

}

fgets函数是从文件读入的函数,第二个参数用于指定字符总数。

多说一句,新版本的C++标准中是不支持gets函数的,因为不安全(容易数组越界),对于VC编译器新增了gets_s函数,部分编译器还是保留gets的,但是为了兼容性,建议把gets全部写成fgets的形式。

2.2.2.输出

字符串的输出也有很多种方式。这里列出比较常见的两种。

版本1 put()输出字符串,遇到'\0’结束

版本2 put(字符数量)输出字符串,到达指定数量结束

实现起来其实也很简单。

void put(void){

for(int i=0;str[i]!='\0';i++)

putchar(str[i]);

}

void put(int n){

for(int i=0;i<n;i++)

putchar(str[i]);

}

很简单。两个都是使用for循环来输出,只不过循环条件不同,一个是不等于“\0”,另一个是小于n。

多说几句,实际上,'\0’实际上就是0,所以也可以写成str[i]!=0。但是根据习惯原因,我们一般写作'\0’,表示这是一个字符串。但是,有人是这样写的:

str[i]!=NULL;

这样写就有点问题了。NULL其实也是0,但是一般用于指针较多。很多机器上面其实NULL在stdio.h是这样定义的:

#define NULL ((void*)0)

这样的话,0是void*类型的,而不是char类型的。所以,上面的代码可能会报错。C++对于类型转换是很严格的。

2.3.查找子串,插入,删除和替换

2.3.1.find函数

我们这一节来讨论查找,插入,删除和替换。想要阅读这一节的话,最好有一点线性表的功底。

先说查找。标准库的find,我们只用两种。

find(String subs)

find(int n,String subs)

当然,其实参数还可以是char*类型的c风格字符串。这里是一定要区分开来的,因为参数为c风格字符串的话,就不是sub.str而是直接写成subs了。看到这里,我们其实知道,string和c风格字符串完全不是一个东西,(一个是类,一个是数组或者说是指针)必须写两种形式。

为了节约篇幅。这里只列出参数为String的东西了。至于参数为char*的话...把后面的.str删掉即可。

int find(String subs){

for(int i=0;i<strlen(str)-strlen(subs.str);i++)

if(strcmp(str+i,subs.str)==0)return i;

return -1;

}

反复查找,直到最后一个。避免比较时候数组越界,最终位置在str长度减去subs长度。

如果一直没有,返回-1。实际上应该是string::npos,很多环境里面都定义为-1。

2.3.2.insert函数

insert函数用于往字符串里面插入一个字符串。函数的形式为:insert(插入位置,插入字串)。它将会在原字符串的插入位置后面插入字符串。例如。”abef”在第2个位置插入”cd”,结果为”abcdef”。

我们按照上面这个样例,来分析一下算法。

首先,我们根据cd的长度,来把插入点后面的东西后移。假设插入点为ins,插入字符串为subs。

for(i=strlen(str)-1;i>ins;i--){

str[i+strlen(subs)]=str[i];

}

我们注意插入的操作是从后往前的移动。

然后,需要在最后放入’\0’这一结束符。应该是在往str[strlen(s)-1+t_size+1]这个位置。

然后,我们需要把字符串subs拷贝到这个位置去。但是,不可以用一般的strcpy,这样会在最后放入一个’\0’,就结束了这个字符串,所以,我们不可以用strcpy。但是为了复制字符串,我们只能自制一个strcpy。因为没有'\0’,就叫做nz_strcpy(no zero的缩写)

void nz_strcpy(char *dest,const char *src){

while(*src!=0){

*dest=*src;++dest;++src;

}

}

(中略)

void insert(int ins,char *subs){

int i;

ins--;//数组下标从0开始

int subs_size=strlen(subs);//取得subs长度,方便s元素后移

for(i=strlen(str)-1;i>ins;i--){

str[i+subs_size]=str[i];//移动元素

}

str[strlen(str)-1+subs_size+1]='\0';//最终的结束符

nz_strcpy(str+ins+1,subs);//复制字符串

}

很简单吧。这就是线性表插入的基本操作。

2.3.3.erase操作

erase(int p,int n)

删除从p开始的n个字符。

void erase(int p,int n){

int front=p+1,rear=p+n;

while(str[rear]!=’\0’){

str[front]=str[rear];

++front;++rear;

}

str[front]=’\0’;

}

我们使用覆盖的方法,设置一头一尾两个指针,每次把尾指针的内容复制到头指针,直到尾指针指向的字符为0。如果不为0,那么就继续下一个字符。例如,把abcdefg的第三个字符到第五个字符删除。我们用列表的方式来看一下。

1 2 3 4 5 6 7

a b c d e f g

a b F d e F g

a b f G e f G

a b f g 0 f g

其中,大写字母表示头指针和尾指针所在的位置。可以看到,把后面的字符逐个放到前面,最后添上0即可。因为添上了0,最后不用删除,字符串自动结束。

2.3.4.replace操作

其实我感觉replace和insert非常类似。replace(int start,int end,char *str);把start至end的区间全部替换成str。相当于先删除start-end的区间,然后再插入str。所以,偷懒的办法如下。

void replace(int st,int en,char *str){

erase(st,en);

insert(st,str);

}

这样做即可。

2.3.5.拾遗

事实上,类似于find,erase,insert,replace等函数的实现,实际上都有很多类型。就例如insert,这里就有大约七八种。

basic_string& insert (size_type p0 , const E * s); //在p0前面插入s basic_string& insert (size_type p0 , const E * s, size_type n); //将s的前n个字符插入p0位置 basic_string& insert (size_type p0, const basic_string& str); basic_string& insert (size_type p0, const basic_string& str,size_type pos, size_type n); //选取 str

的子串 basic_string& insert (size_type p0, size_type n, E c); //在下标 p0 位置插入 n 个字符 c iterator insert (iterator it, E c); //在 it 位置插入字符 c void insert (iterator it, const_iterator first, const_iterator last); //在字符串前插入字符 void insert (iterator it, size_type n, E c) ; //在 it 位置重复插入 n 个字符 c

(参考自http://c.biancheng.net/view/1449.html)

事实上,这些都使用了函数重载功能。如果要全部写起来,比较麻烦,而且很多函数可能我们平时不会用到,本文只挑选了部分出来。下面讲述一下对函数转换的方法。

例如,

basic_string& insert (size_type p0 , const E * s, size_type n); //将s的前n个字符插入p0位置

这一个。

首先,对这类函数的编写的步骤。第一步,对已知条件进行转化,根据后面的参数得出要操作的字符串。例如,这里,根据s和n,我们需要对s取前n个字符,把结果存放入s。第二步,使用标准函数。我们把标准函数的代码搬过来。

在例如,basic_string& insert (size_type p0, size_type n, E c); //在下标 p0 位置插入 n 个字符 c

我们只需要先根据n和c,构建出要插入的字符串s,然后执行标准函数即可。

char s[n];

for(int i=0;i<n;i++)s[i]=c;

两句话即可转换。

关于其他的函数,我们可以参考笔者一开始给出的几个网站进行了解,尝试编写出更多的函数。

2.4.at函数和size函数

2.4.1.at函数

听前面的各种数组操作,有的人应该已经厌烦了吧。如果笔者这里继续写insert,erase,replace的各种新方法的话,估计各位又要犯困了。(笑)所以,这一节换换口味,讲几个简单的函数:at和size。

at函数类似于取字符串的一个字符。一般我们更加常用的方法是用下标,但是下标涉及到运算符重载,比较复杂。所以,这里我们先进行at函数的制作。

char at(int i){

assert(i<=size);

return str[i];

}

我们一般只会用到函数的第二句语句,第一句assert可能不太常用,这里我们就来讲解一下。

assert(表达式);

如果表达式为真,不做任何操作。否则,如果表达式为0,那么就输出异常。如果想看看assert效果的话,可以在程序里写一个assert(0),看程序的反应。程序应该会输出”asseration failed”一句话,然后直接终止运行。输出的文字,根据环境不同,可能结果也会不同。

这里,为了不让数组越界,这里就用了一个assert检验下标i是否小于等于size。

事实上,标准库的at就有这个功能,笔者的dev c++环境会输出这个。

terminate called after throwing an instance of 'std::out_of_range'

what():  basic_string::at: __n (which is 100) >= this->size() (which is 3)

This application has requested the Runtime to terminate it in an unusual way.

Please contact the application's support team for more information.

2.4.2.size函数

其实写到这里,笔者感觉之前的东西有问题。size之前做了成员变量,不可以做函数,所以这里必须先把size成员变量改掉。名字就叫做len吧。

class String{

int len;//这里!

char *str;

...

};

size很简单,只需要调用strlen即可。

int size(){

return strlen(str);

}

同样,还有一个功能完全相同的函数length。

int length(){

return strlen(str);

}

很简单吧。

第三章:运算符重载和构造函数

3.1.赋值运算符

3.1.1.运算符重载

到这里,我们函数就做的差不多了。

我们一般使用的运算符,都是用自己的功能了。例如,+运算符是做加法。但是,对于字符串,+的作用完全不同,是字符串连接。所以,这就涉及到一个知识点,运算符重载。

为了方便,用成员函数很麻烦,所以,我们完全可以用运算符来完成这一功能,改变运算符自己的功能,叫做运算符重载。

运算符重载,在系统内部实际上是调用函数。例如,s=a+b,实际上就是s=s.operator+(i)。

operator+就是一个函数。

3.1.2.=的重载

与其啰啰嗦嗦说一大堆,不如自己动手去做做。

String operator=( String s){

strcpy(str,s.str);

return *this;

}

String operator=(const char *s){

strcpy(str,s);

return *this;

}

运算符重载的一般格式如下:

类名 operator运算符(参数){

操作;

return *this;

}

这是二元运算符的一般重载方式。

如果一个运算符R有X个参数,那么就称R为X元运算符。例如*,/,=都是二元运算符。而+,-即可以做一元运算符(正负号),也可以做二元运算符(一般的加减法)。

例如,一个二元运算符R的参数为A,B,那么这个运算记作A.operatorR(B),当作一个成员函数使用。其实等号运算符重载的写法为s.operator=(c)。而为了简便,s.operator=(c)可以简便写做s=c。这是不是非常类似于真正的string类型了?

而return *this返回的是什么?this是一个指针,指向当前在操作的类对象。所以,我们需要执行return *this,返回当前对象,这样才能正确执行我们的操作。

这样,我们的函数就全部完成了。

3.1.3.运算符重载的广泛运用

C++的标准输入和输出流中,都是用运算符重载来做的。

cin>>a;

cout<<p[i]<<endl;

中,<<和>>都是重载的运算符。此时,cin和cout肯定不是函数(因为函数不可以做计算),其实是一个变量。

<<符和>>符是左移位运算符和右移位运算符。至于cin和cout使用它的原因不知道,大概是为了看上去方便吧。

所以,上面的语句还可以写成;

cout.operator<<(p[i]).operator<<(endl);

顺便提一句,cout是ostream类的对象,cin是istream的对象。当然,本文不是讲cin和cout的,是讲string类型的,所以这里就不详细描述了。

3.2.“+”和“+=”的重载

3.2.1.strcat函数

strcat是C语言的用于字符串连接的标准函数。

strcat(字符串1,字符串2)把字符串2连接到字符串1后面。

3.2.2.正题

String operator+(String s){

strcat(str,s.str);

return *this;

}

String operator+=(String s){

strcat(str,s.str);

return *this;

}

加号运算符和“+=”运算符都可以起到字符串连接的作用。非常简单。调用strcat即可。

3.3.比较运算符的重载

3.3.1.strcmp

惯例,我们先介绍c库标准函数。

strcmp比较两个字符串大小。

strcmp(a,b)

a>b 返回值>0

a<b 返回值<0

a=b 返回值=0

3.3.2.自制strcmp

这里先偏离正题,说说c库函数strcmp的自制。

int strcmp(char *s1;char *s2){

while(*s1==*s2){

++s1;++s2;//如果相等,就下一个字符

}

return *s1-*s2;//这里一定是不相等的字符,相减即可

}

一开始的while循环不断比较s1和s2,如果相等,指针++,指向下一个字符。

最后退出循环时,一定是不相等的,两个数相减,用作差法比较大小。

但是有一个问题,如果字符串相等,那就会比较到数组后面去,使得数组越界。我们必须保证第一个循环条件为字符不等于'\0',也就是不结束。

int strcmp(char *s1,char *s2){

while(*s1==*s2 && *(s1+1)!=0 && *(s2+1)!=0){

++s1;++s2;

}

return *s1-*s2;

}

保证下一个字符不等于0,这样做就可以了。

3.3.3.比较运算符的重载

比较运算符主要有大于,小于,等于三个。我们同样按照strcmp来进行重载,单手注意返回值变成了真和假。

bool operator>(String s){

if(strcmp(str,s.str)>0))return 1;

else return 0;

}

bool operator<(String s){

if(strcmp(str,s.str)<0))return 1;

else return 0;

}

bool operator==(String s){

if(strcmp(str,s.str)==0))return 1;

else return 0;

}

当然,我们还需要考虑参数为char*的字符串的情况,这里就略了。

制作起来非常简单,只需要调用strcmp即可。多说一下,如果是C语言用多的人可能不知道bool是什么,其实bool是一种特殊的1字节变量,只能存放1和0,表示真和假。逻辑运算符的返回就是真假。如果给bool类型赋值为任意一个非0值,那么视作赋值为1。所有不等于0的值都看作为真,这就是可以把if(a!=0)写作if(a)的原因。

3.3.4.compare函数

compare也是一个string的成员函数,也用于比较字符串大小。我们只看代码,猜一猜它的功能。

bool compare(String s){

if(strcmp(str,s.str)==0)return 1;

else return -1;

}

3.4.下标运算符的重载

3.4.1.什么是下标

我们之前在学习数组的时候,应该看到过这样的描述:

数组中每一个数都有一个编号,这个编号就是下标。

我们在引用数组元素时,经常用到s[i]这种写法,实际上i就是一个下标,而[]就是下标运算符。

3.4.2.下标运算符的重载

首先我们要搞清楚一点,下标是可以作为一个左值来进行赋值的。也就是说,我们是可以这样写的:s[i]='a’

而一般的函数是不能这样写的:s.at(i)='a’

所以,如何让这个下标运算符可以被赋值是一个问题。我们先抛开这个问题,来写程序。

char  operator[] (int i){

return str[i];

}

好像很简单...等等!如果写一个类似于s[i]='a’的语句,会报错吗?

error: lvalue required as left operand of assignment

也就是说,这里是不可以作为一个左值来赋值的。因为这样的语句,函数返回的是一个常量a,所以这个语句也是表示’a’='a’,是非法的。所以,我们需要在函数声明的地方动点手脚。

char  &operator[] (int i){

return str[i];

}

在前面加上&符号即可。&符号这里不是取地址的意思,而是表示引用。例如,交换两个变量的值,C++语言可以用引用的方法,这样写。

void swap(int &a,int &b);

这样,如果把a和b传递过去,形参就是对实参的引用,就实现了交换。同理,用引用的方式,就可以赋值了。

(0)

相关推荐