C++实现string类

目录

目的:

1string类的定义

2.成员函数

2.1构造函数

2.2析构函数

2.3c_str

2.4重载[]

2.5string迭代器的实现

2.6push_back(传统写法)

2.7append(传统写法)

2.8重载+=运算符

2.9insert

2.10erase—删除功能

2.11 resize

2.12 拷贝构造函数

2.13赋值运算符重载

2.14swap

2.15    find查找

2.16 substr

3 重载流输入、流提取

 4.getline


目的:

        我们知道C++中给我们提供了一个string类进行使用,那么我们为什么还要自己再实现一个string类呢?是为了写一个比编译器给我们提供的更好的吗?这不是重点,虽然我们知道string类在设计上有许多缺陷,比如功能实现太过冗余,有些功能没必要那么设计,C++的其它容器跟string相比就会好很多,这里其实包含着一些历史原因,我就不过多解释,有兴趣的朋友可以网上查询或者私信我都可以。

        那么我们自主实习一个string类的目的是为了更好地去理解底层,我们知道C++是一门重视底层的编程语言,我们如果要学好C++这门计算机语言就必须去接触底层,去感受大佬当时写这块功能是有着何种考量,接触了底层之后,在我们日后的使用时,我们就会想到这个功能的底层是如何实现的,从而能够更加合理、高效地去使用它们。

        因此,本文我会给大家讲解string大部分的内部成员函数以及一些函数重载,甚至是一些现代方法。

1string类的定义

一般我们的成员函数是设置成public属性,使其能在外部调用到,而内置成员变量我们一般是设置成私有,不允许外部随便就能修改到它。

我们知道string是字符串类,所以要有char*类型的指针,还要有它的有效元素个数_size,还有计录它空间的_capacity,因此初步来看,string类基本上的一个框架就是这样,到后面我们会慢慢完善。

通过这张图我们大致能感受到string的成员函数非常多,在此我推荐一个学习C++的网站cplusplus.com,大家直接网上搜就可以搜到,现在一开始的页面都是新版本的,不过我建议切换回老版本更好用。

框选的地方就可以切换回老版本

这里就可以搜你各种想要了解的东西,容器也好,函数也好都可以搜到,这个网站里面有非常全面的概念知识,方便大家更好地理解。

        书接上文,我们知道了它有如此多的功能,那为啥说它冗余呢?

你看,单是一个插入函数,它就重载了一大堆,我觉得当初设计的时候想得太全面了,哪中写法都考虑,都往上面去写,其实没必要,程序员使用的时候也就会用那些常用的写法,所以在后面讲解的时候我就写常见的重载方式。

2.成员函数

2.1构造函数

我们使用构造函数列表的方式更加规范一些,我们已知的框架有三个内置成员变量,我们先计算str的有效个数赋值给_size,再让我们的空间等于我们的_size,再给我们的_str开空间,注意这里要多开一个空间用来存放我们的'\0',再利用strcpy将str的内容拷贝给我们的_str。接下来我来说说我们的形参为什么要这么写,我们在char*的前面加上const修饰,在这里的意思是匹配我们传入的字符串,因为像“hello world”这串字符串是具有常属性的,不用const修饰编译上就不会通过。我们给它赋个空字符串的缺省值是因为你可能什么都不传,不传的话我要想创建一个对象就要来一个空字符串,不然我的_size,_capacity,_str就没法赋值了。

2.2析构函数

析构函数就简单得多了,只需要将_str的空间释放掉,然后给它赋个空指针,再将它的size和capacity赋0就可以了。

2.3c_str

c_str是用来得到字符串的一个函数,我们知道在C++中想要得到字符串可以直接

这是因为<<被重载了,所以可以打印s1的字符串,但是我们要知道C++是兼容C语言的,而我们熟知的C语言没有类的概念,它只有结构体strcut,而且也不支持运算符重载,所以我们的C++标准里string类里面就弄了一个这么个函数来读取字符串,所以它的名字还带着一个c字母呢。

        c_str的实现很简单(如图所示)

直接返回_str就可以了,至于为什么要给this指针加一个const修饰,这是为了适配对应的const修饰过后的类的类型,既然权限可以被缩小无法被放大,那么我们直接就将权限设置成最小就可以使两种情况都通过编译。

2.4重载[]

这里的我们就要两种分开来写了,因为c_str是读取一个字符串,不需要修改,我们为了方便,可以写成一个,但是[]不一样,没有const修饰的对象,我们一般还可能对其进行修改的操作,但是如果只写一个第二种,我们是没有办法对其进行修改的,所以这种情况就要分两次讨论。我们直接断言一下pos,pos总不能越界吧,断言完后直接返回这个字符就可以了,返回类型要加引用也是因为只有这样才能修改到对应堆上这个字符的空间。

2.5string迭代器的实现

实现迭代器之前我们先来实现两个小功能,一个是取_size的数据,一个是取_capacity的数据。

然后我们来实现.begin和.end。

        我们知道迭代器的实现方式有很多种,这里我们采用指针的方式来进行,我们先重命名一下类型。

我们这里也需要设置成两种,原因跟之前的说法大差不差。

我们begin和end都是需要两种,begin返回的就是字符串的首元素地址,而我们的end返回的是最后一个元素('\0')的地址。

2.6push_back(传统写法)

以上的push_back是传统的写法,现代写法我会在实现insert之后再讲解。push_back的功能是给字符串尾插一个字符,所以我们再尾插前得先检查一下空间是否足够,不够的话我们就需要扩容,至于空间扩大为原来的几倍我们可以自己定,我这里就扩二倍,如果我们的空间本身就是0,也就是空字符串的话,我们直接给一个4大小的空间。扩容的具体操作我们就实现一个reserve来搞定。string类中reserve的作用就是扩容。

扩容的操作我们很好理解,开n+1个空间是为了'\0',strcpy是C语言中的一个字符串拷贝函数,不要忘记我们在将tmp的空间地址给到_str之前要先将它的堆上的空间释放掉,不然会造成空间泄漏。

        回到push_back,我们完成扩容的操作之后接下来的操作就很简单了,将字符ch尾插,然后不要忘记将'\0'加回去就行了。

2.7append(传统写法)

append的功能是尾部添加一个字符串。到这里大家就能感觉到string类当时设计的一些不妥,同样是尾部插入它还设置成两个。

它的实现跟push_back相比也是大差不差,先检查一下空间是否足够,不够就扩容,不过这个的扩容有一点讲究,你试想一下,我们这次添加的是一个字符串,字符串的话我们固定的将空间扩大好像都差点意思,所以这里的一个优解就是根据其实际的长度进行扩容。扩完容之后,我们还是可以使用strcpy这个函数来将新的字符串拷贝到原字符串的末尾位置,这里我们没有再手动添加一个'\0'是因为strcpy这个函数会将'\0'也一同进行拷贝。

2.8重载+=运算符

在实际应用当中,我们尾插字符、字符串的操作最常用的就是+=,前面的那两个函数不太会去使用。+=我们也分两种,字符与字符串,欸?这不就跟我们之前那个一样吗!既然如此,我们可以直接将那两个函数搬过来。

2.9insert

insert是任意位置插入,insert函数我们也来实现两种,分别针对字符和字符串,两个代码的整体逻辑都是一样的,我们一个一个来,先看字符的。

        这个函数的两个形参代表的意思分别是索引值、字符,字符版的逻辑很简单,首先我们先断言一下pos,它最大也不能比有效元素个数还大吧,断言之后我们来进行扩容,扩容的逻辑还是一样的,因为要移动数据,所以接下来我们使用一个end来充当尾部的下标,然后将元素一个一个往后移就可以了。将pos位置的元素也移走之后我们就可以将ch放到pos位置了。

        接下来我们来看看字符串版的。首先是断言,然后扩容,这些操作与之前几乎都相差无几,重点是插入部分,这回end就不再是尾元素的位置了,而是尾元素后len个的位置,这样在将pos位置的元素移走时,刚好逻辑上就空出了len个长度的空间,在将元素移走后我们就可以使用strncpy函数来进行拷贝,同为拷贝函数,strnpy可以限制拷贝长度,这样我们就不会将要拷贝的字符串的'\0'也拷贝进去了。

————————————————————————————————————————

我们已经将insert讲完了,接下来我们来看看push_back和append的现代写法——

实现了insert之后我们就可以直接将insert函数写入对应的功能当中了,这样我们的整个添加操作本质上只用了insert的底层,大大降低了代码的冗余。

2.10erase—删除功能

删除功能也很简单,我们的形参有两个,pos是索引值,len是你想删除的字符串长度,npos是int型的-1,当它进行隐式类型转换的时候,就会变为正整数最大值。

这是我们改进后的私有成员,我们给每个成员变量都设置了一个缺省值,将npos设置为共有是因为我们希望外部也可以访问到它,给它增添常属性使其不能被修改,为了防止每开一个变量就占一块空间,我们再给它设置成静态变量。

        讲完npos之后,我们来说说函数内部,先对pos断言,然后判断要删除的字符串长度加上索引值有没有超过字符串的有效元素个数。超过了就直接在索引值对应位置加个'\0',没有的话就将不删除的部分填补空位就行了。

2.11 resize

resize的功能是增容,它的功能与reserve的区别是它既可以往小了缩,也可以往大了缩,而我们的reserve只能往大扩容,往小了没有用。   

        这个函数我们一开始先判断往那个方向缩,往小的话就直接在n位置放字符ch,使用者没写的话默认为'\0',如果往大的话就先扩容,然后再把字符ch一个一个赋值进去。

2.12 拷贝构造函数

拷贝构造函数我们也有传统版和现代版,现代版我们后面再介绍。

        拷贝构造函数也很简单,我们将被拷贝的那个对象传进来,然后就把它的数据一个一个赋值过去就行了。

2.13赋值运算符重载

赋值运算符重载我们还是有传统版和现代版,现代版我们后面再介绍。

        赋值运算符重载的思路也很简单,概括起来就是:创建一个临时字符串指针并开空间,然后将被拷贝字符串的值给到临时指针,然后释放掉自己的空间再去=tmp的值。

        这样做的好处就是空间消耗小,因为你的tmp是在局部作用域内,出了这个函数就自动销毁了。

2.14swap

std标准库里就有swap,这里我们可以直接拿过来用。

 看完swap之后我们来看看拷贝构造函数的现代写法:

本质上就是调用了构造函数:tmp进行构造函数,再用swap去交换双方的值。

赋值运算符重载的现代写法:

这里也是s调用构造和拷贝构造来完成swap交互。

所以,现代写法跟传统写法的区别在于,传统写法是靠自己,而现代写法是别人做好了你选择去使用。

2.15    find查找

find我们也要满足找字符和字符串的,我们先来看看找字符的,找字符的很简单,断言一下就可以直接遍历查找了,找到了就返回下标,找不到就返回npos。

        我们再来看看字符串版本的,同样是先断言,然后使用C语言提供的strstr函数,这个函数的功能是匹配字符串,如果找到就返回第一个匹配字符的地址,我们用p来接收后就可以直接利用地址相减返回下标。

2.16 substr

substr是提取字符串的内容,代码的逻辑很简单,要取的内容超过本身,就返回全部,否则返回部分。

3 重载流输入、流提取

重载这两个我们必须写在类的外面,不然的话,由于this指针的存在,我们的重载后的流提取将无法连续执行。

流提取,顾名思义就是获取内容,C++里标准输出流的类型是ostream,所以我们的cout就是ostream类型。提取的操作很简单,用范围for将S里的内容全部提取到cout中,最后将它返回就可以了。

流输入我们也是要定义在类的外面,原因跟流提取是一样的。

这里,我们的标准流输入是从键盘中输入信息到cin中的,cin的类型为istream。因为要输入内容,所以我们的第一步操作是先把字符串里的数据进行清空。清空完后,我们要获取一个个字符,get函数可以做到读取一个个字符,因为流输入我们是不清楚一共有多少个数据,如果一次性扩太大不好,频繁扩容也不好,这个时候就有一个大佬想了一个办法,用buff数组当中间商,临时存储数据,满了就给s,然后将下标变为0,再继续重复操作,如果没满且读完的话,就将值交给s,然后返回就可以了。

        这里要注意,我们的流输入是读到空格和回车就会停止的,所以我们这里循环终止的条件就是' '和'\n'。

 4.getline

既然我们的标准输入流无法读取我们显示器上的一行内容,那我们怎么办呢?有一个getline函数就可以解决这个问题,getline就无视空格,直到'\n'才会停止,因此,我们的代码都不需要改动,直接将循环条件的空格去除就可以了。

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
C++中的string是一个用于处理字符串的标准库。它提供了一系列成员函数和操作符重载,使得字符串的操作更加方便和高效。 C++中的string位于命名空间std中,因此在使用之前需要包含头文件< string >。 下面是一个简单的示例,展示了如何使用string来创建、初始化和操作字符串: ```cpp #include <iostream> #include <string> int main() { // 创建一个空字符串 std::string str; // 初始化字符串 std::string greeting = "Hello, world!"; // 获取字符串长度 int length = greeting.length(); std::cout << "Length: " << length << std::endl; // 连接字符串 std::string name = "Alice"; std::string message = greeting + " My name is " + name; std::cout << "Message: " << message << std::endl; // 获取子串 std::string substring = message.substr(7, 5); std::cout << "Substring: " << substring << std::endl; // 查找子串 size_t position = message.find("world"); if (position != std::string::npos) { std::cout << "Found at position: " << position << std::endl; } else { std::cout << "Not found" << std::endl; } return 0; } ``` 上述示例中,我们首先创建了一个空字符串`str`,然后使用赋值运算符将字符串"Hello, world!"赋给了变量`greeting`。接着,我们使用`length()`函数获取了字符串的长度,并使用`+`运算符将多个字符串连接起来形成新的字符串。我们还使用`substr()`函数获取了字符串的子串,并使用`find()`函数查找了子串在原字符串中的位置。 除了上述示例中的操作,string还提供了许多其他有用的成员函数和操作符重载,如插入、删除、替换、比较等。你可以参考C++的官方文档或其他相关资料来了解更多关于string的详细信息。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值