【原创】自制string类型(已完成)

这篇文章首发于360doc http://www.360doc.com/content/21/0526/17/73755266_979099504.shtml ,其实360doc里面的那个也是我的帐号,发在那里没什么人看,就发到这里来了。

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

前后总计两个多月,总算是写完了。完成日期:2021.8.1.

若本文有错误,请大家在评论区中指出,我会尽我所能加以改正。

 

 

目录

第一章 前言和准备工作

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函数

第五章:拾遗

5.1.string.h和string

5.2.字符数组

5.3.string类的本质

 

 

第一章 前言和准备工作

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=a.operator+(b)。

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

 

 

3.5.构造函数

3.5.1.构造函数

构造函数是在定义的时候执行的函数。例如,我们初始化string类型,一定看见过这样的语句:

string s("abcd")

在定义的时候初始化。

3.5.2.写法

构造函数没有返回值,且构造函数的名称与类名一致。

String(const char *s){
        strcpy(str,s);
}

 

但是注意构造函数只要定义了,我们就需要在初始化时写上构造函数,不然会出错。
String s;//错误
String s("");//正确

为了方便,我们写一个空构造函数。
String(void){
}
这样,在不写括号的情况下,c++会自动执行空函数,也就是说,初始化时,不加括号默认执行空构造函数。
趁热打铁,还有几个构造函数,我们一起实现一下。
String(int n,char c){
        for(int i=0;i<n;i++)
            str[i]=c;
}

初始化为若干相同字符。
不过突然想到一个问题,在初始化调用构造函数时,还没有init,所以str是一个未初始化的指针,会出错,构造函数也没有意义了。我们需要把init的过程写进构造函数里面,至于size先定义为100吧,如果不够再realloc...等等!用new分配的空间无法realloc,我们还是用stdlib.h库中的malloc分配,这样还能realloc。这样一来,init函数就没用了,可以把它删掉了,而且也更像真正的string类型了。
操作非常简单,在每一个构造函数加上一句str=(char*)malloc(100);即可。运行结果一样,真不错。

 

第四章:string类函数(续)

4.1.内存动态分配

之前运算符重载的所有内容都讲完了。我们换个口味,继续说函数。

首先我们要知道,str=(int*)malloc(100)指给str分配100个空间。一开始,我们没有写成在类中直接写char str[100]而是特意在构造函数这样写,就是为了以后可以重新扩大内存。现在,我们需要在内存不够时重新扩大内存。
扩大内存,我们不能用new,必须使用realloc函数用于分配过的指针再次分配。所以,我们需要在string中新增一个成员变量size,说明现在字符串有几个空间,以及变量free,就是size-strlen(str),说明剩余多少内存可用。

class String {
public:
    char *str;
    int size;
    ...
    ...
    void insert(int ins,String t){
        int tmp=strlen(str)+strlen(t.str);
        if(tmp>size){
            str=(char*)realloc(str,tmp);
            size=tmp;
        }
        ...
    }
    String operator +=(String s){
        if(strlen(s.str)+strlen(str)>size){
            str=realloc(str,strlen(s.str)+strlen(str));
            size=strlen(s.str)+strlen(str);
        }
        ...
    }
    String operator =(String s){
        if(strlen(s.str)>size){
            str=realloc(str,strlen(s.str));
            size=strlen(s.str);
        }
        ...
    }
    
};

 

这里在每一个函数之前都加上了字符长度判断,至于构造函数只需要加上size的赋值即可,不用多说。

 

4.2.append函数

4.2.1.append函数简介

在字符串后面追加字符串。

本文在这里只简单地写两种函数重载。第一种:直接在string的后面加上string(或char*字符数组)

第二种:在string的后面加上string的前n个字符。

 

4.2.2.编写函数

介绍完了,我们开始正式编写。首先我们找到函数的结束位置,然后在结束位置后面把需要的字符串拷贝过来即可。

 

 上图是一张简图,表示函数的执行方式。

如果需要指定所加上的字符总数,那么我们使用strncpy就可以了。

void append(String s){
  int pos;
  char *t=str;
  for(pos=0;t[pos]!='\0';++pos){}
  strcpy(t+pos,s.str);
}
void append(String s,int n){
  int pos;
  char *t=str;
  for(pos=0;t[pos]!='\0';++pos){}
  strncpy(t+pos,s.str,n);
}

 

 

 

4.3.begin和end迭代器

begin指向第一个字符,end指向最后一个字符。于是本节就这样写完了。

char begin(void){
  return str[0];
}
char end(void){
  int i;
  for(i=0;str[i]!='\0';i++){}
  return str[i-1];
}

 

4.4.getline函数

getline读入一整行。

void getline(istream cin,string s){
  int i;
  for(i=0;s[i]!='\n';i++)s[i]=getchar();//读入到行末尾
}

其实函数参数中的那个cin根本没有用到,只是装装样子而已。(cin定义在iostream中,是istream类型的变量)

 

第五章:拾遗

5.1.string.h和string

有些人根本不知道下面三个头文件的区别,经常在使用的时候搞混。

#include<string.h>
#include<cstring>
#include<string>

解释:

string.h是老版本C语言的头文件,cstring是新版本C++新增加的头文件,功能等同于string.h,包含的是字符数组(char*)相关的函数(例如strcpy,strcmp,strlen等)。

string是C++的头文件,包含的是string类(即string,本文写的那些函数)只有使用这个函数,才可以写那些有关string的代码。例如:

#include<iostream>
#include<string>
string s;
int main(){
  cin>>s;
  cout<<s.size();
}

其中定义的string类型和size函数都是这个头文件里面的。

到现在我还见到很多人在这种时候#include<cstring>,在部分编译器会编译错误!

 

5.2.字符数组

5.2.1.字符数组常识

上一节我们说了string.h,那么老版本的字符数组如何使用?

字符串在这种情况下是一个数组。

char s[100];

如果需要对这个数组做输入和输出,可以使用scanf或printf的%s参数。cout可以输出字符数组,但是cin不能输入字符数组。

原因:

cout<<"hello";

用双引号括起来的都是字符数组。虽然在使用string的时候,新的string可以和字符数组混用,但是这是两个东西。(所以本文中,所有函数都要写两遍,一遍是针对string的,一遍是针对字符数组的)

5.2.2.进阶——字符数组和指针

请看两种数组的声明,有什么区别?

char *s="abc";
char s[10]="abc";

 

不妨尝试执行下面的代码:

int main(){
  s[0]='1';
  cout<<s;
}

运行结果:

上面的声明:segmentation fault(即runtime error,运行时崩溃错误)

下面的声明:输出1bc

 

可以看到,使用指针进行初始化,得到的是一个字符串常量。但是使用数组进行初始化,效果相当于拷贝字符串。(只有在这种情况下'='可以做拷贝字符串的作用)

(2021.8.8补充:

使用指针形式初始化是使得这个指针指向那个abc,也就是指向字符串常量。而对于字符串常量,内存是受到特殊保护的,只能读取不能写入,因此会发生错误。

而使用数组的形式,是对字符串进行拷贝操作,数组所在的内存区域没有受到特殊保护,因此可以写入。)

 

并且,使用指针形势初始化时,会出现一个warning。

warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]

 

5.3.string类的本质

终于写到最后一节了,最后我们来讨论string类的本质。

本质非常简单,这就是一个普通的类,和其他的stack,queue没有什么区别,就是一个数组+一堆成员函数。

因此,也是5.2中的结论,双引号括起来的不是string,而是const char*字符串。因此,string中,成员函数的参数,定义了两种形式,一种string,一种字符数组。使用上去,就像是string和字符数组完全通用。string的本质,还是一个普通的类。

当然,如果C++允许重载双引号的话(双引号也不是运算符),string估计就可以完全和字符数组通用了。就像这样:

String operator ""(const char *s){
  String Str=s;
  return Str;
}

双引号也不是运算符,上面的只是瞎写而已,如果真的要完全通用的话...就算到了宇宙毁灭,这也是不可能的吧。

 

(全文完,总字数7608字)

 

posted @ 2021-07-06 15:46  计算机知识杂谈  阅读(328)  评论(3编辑  收藏  举报