自制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传递过去,形参就是对实参的引用,就实现了交换。同理,用引用的方式,就可以赋值了。