String及其模拟实现

·博主博客链接——点击这里


前言

提示:本文较为硬核,学者需认真跟着我的脚步
在讲string之前,需要大家回答一下,我们以前是怎么存一段字符串的?想必绝大部分人还停留在C语言,如下:

int main()
{
   const char * tmp = "abcd";
   return 0;
}

好的,上面的代码是你的答案,那么,我将对你进行拷打,要是我们要在你上面的tmp指向的字符串中,插入数据、删除数据,求长度等等运算,单靠着上面我们之前学习的代码,能实现,但是会很麻烦,需要我们自己去写对应的函数去实现相应的功能

但是,只要我们仔细想想,就不难发现难道前辈们没有发现字符串的实现这么复杂吗?难道前辈们不去专门做一个类,这个类专门用来存放字符串,且相关函数都已经实现好(修改,插入…),等着我们直接来使用吗是的,你发现了!!

前辈为我们"造好了轮子",我们使用"现成的轮子"即可。

一、string是什么?

string是编程语言中用于便捷存储、操作字符串的类型,能自动管理内存、还提供丰富的字符串操作方法,让字符串处理更轻松。

二、string的使用

下面,我将根据string文档介绍中对string的重要部分进行讲解

1.string的底层本质

在这里插入图片描述

2.string的构造函数

default (1)	string();
copy (2)	string (const string& str);
substring (3)	string (const string& str, size_t pos, size_t len = npos);
from c-string (4)	string (const char* s);
from sequence (5)	string (const char* s, size_t n);
fill (6)	string (size_t n, char c);
range (7)	template <class InputIterator>  string  (InputIterator first, InputIterator last);

可以看到,构造函数重载了7种,我们只需重点学习当中的1、2、4

这里旨在构造空串,也就是字符串只有一个元素,那就是’\0’

#include<iostream>
#include<string>
//使用string必须包含这两个头文件
default (1)	string();
//这里旨在构造空串,也就是字符串只有一个元素,那就是'\0'
int main()
{
   string s1;
   return 0;
}
from c-string (4)	string (const char* s);
//这里就运用到我们之前的const char*指针来构造我们的string
int main()
{
   const char * s = "abcde";
   string s1(s);
   //也可以这么写:string s1("abcde");
   return 0;
}
//由代码可知,s指向了"abcde",s构造s1,即s1也为"abcde"。
copy (2)	string (const string& str);
 //大家读文档,可以连蒙带猜。这里就是用已有的一个string去构造
int main()
{
   string s1("hello zh");
   string s2(s1);
   //最终s1与s2对象指向的那块内容都是一样的
   return 0;
}

3.size与length

size_t size() const;
//Return length of string
size_t length() const;
//Return length of string

我们可以看到,这两个接口都是返回当前字符串的有效字符个数,例如"abcd",返回的就是4。

这两个接口的功能都是相同的,那为什么同样的接口要实现两个呢?原因解释:那是因为string是祖师爷第一个实现的类,他们想着字符串的大小就是长度,所以就定义了一个叫length的接口。但是另外的数据结构像链表,它是由一个一个的结点拼接而成的,它的大小称为length就有点不太合适,因此,为了耦合性,祖师爷为每个容器都定义了size()接口,用来求这些容器的大小
总的来说就是length()只适用于string,而size()可以适用于任何的容器

代码示例如下:

int main()
{
   string s1("hello czh");
   cout<<s1.size()<<endl;
   cout<<s1.length()<,endl;
   return 0;
   //打印出来的结果均为9
}

4.resize与reserve

re,就蕴含着“再”的意思,那么resize就是对string的大小size进行改变,reserve就是对string的容量进行改变。
❶resize

void resize (size_t n);
void resize (size_t n, char c);
int main()
{
   string s("hello world");
   s.resize(5);//打印出来结果为:hello
   s.resize(15,'c');//打印出来结果为:hello worldcccc,原先size为11,多出来的4个全初始化为c
   return 0;
   
}

在这里插入图片描述

❷reserve

void reserve (size_t n = 0);

string,当插入的字符足够多达到最大容量的时候,编译器也会进行扩容的。而且当我们数据的插入足够频繁时,编译器就会不断地为string扩容。
频繁扩容的后果:内存拷贝开销大,造成大量内存碎片,进而导致额外的内存浪费。
所以当我们确认好最终字符串的大小,直接给这个string确定好容量,后面也就不用频繁进行扩容了。

int main()
{
   string s;
   s.reserve(100);
}

为s先开了100个空间,确保其前期不需要因为容量低而导致频繁扩容。

5.substr与c_str

❶substr

string substr (size_t pos = 0, size_t len = npos) const;
Returns a newly constructed string object with its value initialized to a copy of a substring of this object.

拷贝从原始string的pos位置开始的len个字符
在这里插入图片描述

这里将一下第二个缺省参数npos,npos给的值为-1。假设我们不传,就使用这个npos
-1其实是整型的最大值,-1的原码为10000000 00000000 00000000 00000000。补码为
1 10000000 00000000 00000000 00000000,这是一个超出整型范畴的数字。
也就是说,假设我们不传第二个参数,也就是不知道要拷贝多少个字符,那么编译器就会靠别从pos位置开始一直到末尾的字符。

代码示例如下:

int main()
{
   string s("hello ahodhaodhao");
   cout << s.substr(3)<<endl;
   return 0;
}
//打印出来的结果就为llo ahodhaodhao

❷c_str

const char* c_str() const;
Returns a pointer to an array that contains a null-terminated sequence of characters (i.e., a C-string) representing the current value of the string object.

c_str接口返回的是指向这个字符串的指针,并且返回的这个指针类型是const char*

const char * 与一般的指针不同,在我们执行流插入语句时,假设是其他类型的指针,往往都是打印地址,而const char * 不同,打印const char * 都是直接把这个指针所指向的字符串打印出来

代码示例,如下:

int main()
{
   string s1("hello");
   cout<<s1<<endl;
   cout<<s1.c_str()<<endl;//const char *
   return 0;
   //可以得到,两者打印出来的结构均为:hello
}

6.push_back/append/operator+=

❶push_back

void push_back (char c);
Appends character c to the end of the string, increasing its length by one.

push_back(char c);
即在字符串末尾插入一个c字符。

string s1("abc");
cout<< s1<<endl;
s1.push_back('d');
cout<< s1<<endl;

两次打印的结果分别为: abd abcd。
❷append(选取重点进行讲解)

string& append (const string& str);
string& append (const char* s);

根据append示例,可在原有字符串末尾添加string型与const char * 型

代码示例,如下:

string s(" czh");
const char * tmp = " hottoday";
string s1("hello");
string s2 = s1.append(s);
cout<<s2<<endl;
string s3 = s1.append(tmp);
cout<<s3;

打印出来的结果分别为 hello czh/hello hottoday。

❸operator+=这是我最推荐大家使用的尾插接口

string& operator+= (const string& str);	
string& operator+= (const char* s);
string& operator+= (char c);

这个接口可以插入单个字符,string类型以及const char * 类型,可谓是功能齐全push_back只能插入单个字符,而append的功能实现得又异常的冗余。

代码示例如下:(为方便,只实现第一种功能)
int main()
{
   string s("abc");
   string tmp("!!!");
   string s1  = s+= tmp;
   cout<<s1;
   return 0;
   //打印出来的结果为:abc!!!
}

三.string的模拟实现

namespace zehong//为避免与库里的string起冲突,我们使用命名空间将我们自己定义的string包起来
{
    class String
    {
    public:
    private:
        char * _str
        size_t _size;
        size_t _capacity;
    };
}

1.构造的实现

根据文档,我们能为string实现以下三种构造函数

public:
    string();//空串
    string(const char * str);//内容与str指向的一样
    string(const string & s);//拷贝构造

代码实现,如下:

public:
    string()
        :_str(new char[1]('\0'));//空串表示串中只剩'\0'
        ,_size(0)
        ,_capacity(0);
    {
    }
    string(const char * str)
        :_size(strlen(str))//我的个数跟你str的个数是一致的
    {
        _str = new char[_size + 1];//+1是为了留一个位置给'\0'。
        _capacity = _size;
        strcpy(_str,str);
    }
    string(const string &s)
        :_str(new char[s._size + 1]);
        ,_size(s._size);
        ,_capacity(s._capcity)
    {
        strcpy(_str,str);
    }

答疑:为什么第二个构造不采用下面的写法?

string(const char * str)
     :_str(new char[str._size + 1])
     ,_size(strlen(str))
     ,_capacity(strlen(str))
   {
   //
   }

解答:这是因为strlen函数的原因,strlen函数是计算字符串长度的函数,在遇到’\0’之前就停下来,这本身是一种循环数数的逻辑,上面的写法调用了三次strlen逻辑,三次循环,降低了代码的运行效率与可读性。

2.operator[]

重载operator[]是为了让我们能够实现像下标[]访问数组一样访问我们string。

public:
     char & operator[](size_t i)
     {
         return _str[i];
     }
     const char & operator(size_t i)const
     {
         return _str[i];
     }

这两个函数有什么区别呢?
❶函数的常量性
char &operator[](size_t i):这是一个非const成员函数。它允许通过对象调用,并且可以用来修改对象内部存储的字符数据(_str)中对应下标的字符。意味着调用者可以通过返回值直接修改原数据。
const char & operator[](size_t i):这是一个const成员函数,函数声明最后的const表示这个函数不会修改对象的成员变量。它适用于const对象调用,比如你有一个const类型的字符串类对象时,只能调用这个const版本的operator[],而且它返回的是从const char类型的引用 ,防止通过返回值意外修改原数据。
❷使用场景

int main()
{
   string s = "hello";
   s[0] = 'H';//调用非const修改值
   cout<<s<<endl;//输出Hello
   return 0;
}
int main()
{
   const string s = "hello";
   char c = s[1];
   cout<<c<<endl;//输出e
   //s[1] = E;//此时会报错,返回值是const修饰的,无法被修改
   return 0;
}

3.插入与扩容

public:
     void reserve(size_t n);//扩容
     void push_back(char c);
     void append(const char * str);
     string & operator+=(char c);
     string & operator+=(const char * str);
public:
     void reserve(size_t n)
     {
         char * tmp = new char[n + 1];
         memcpy(tmp,_str,_size);
         delete[] _str;
         _str = tmp;
         _capacity = n;
     }
     void push_back(char c)
     {
         if(_size>=_capacity)//满了就走扩容
         {
             reserve(size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity);
         }
         _str[_size++] = c;
         _size = '\0';//这步大家容易忽略。
     }
     void append(const char * str)
     {
        size_t len = strlen(str);
        if(_size + len > _capacity)
        {
            reserve(size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity);
        }
        memcpy(_str + _size,str,len);
        _size = _size + len;
     }
     string & operator+=(char c)
     {
        push_back(c);
     }
     string & operator+=(const char * str)
     {
        append(str);
     }

4.insert和erase

public:
    insert(size_t pos,char c)
    {
         if(_size>=_capacity)//满了就走扩容
         {
             reserve(size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity);
         }
         int end = _size;
         while(end>=(int)pos)
         {
            _str[end + 1] = _str[end];
            end--;
         }
         _str[pos] = c;
         ++_size;
    }
    erase(size_t pos,size_t len)
    {
         assert(pos<_size)
         if(len == npos || len >= _size - pos)
         {
             _size = pos;
             _str[_size] = '\0';
         }
         else
         {
            size_t i = pos + len;
            memmove(_str + pos,_str + i,_size + 1 - i);
            _size -= len;
         }
    }

5.substr(getline)

string substr(size_t pos,size_t len)const
{
    if(len == npos || len >= _size - pos)
    {
        len = _size - pos;
    }
    string ret;
    ret.reserve(len);
    for(size_t i = 0;i < len;++i)
    {
        ret._str[i] = _str[pos + i];
    }
    return ret;
}

6.流提取>>

流插入<<较为简单,这里就先不讲了,来讲讲需要注意的流提取>>

int main()
{
    string s;
    cin >> s;
    return 0;
}

我们想在s输入字符串该怎么实现呢?先来给大家看一个案例。

istream & operator>>(istream& in,string &s)
{
    char ch;
    in >> ch;
    while(ch != ' '&& ch != '\n')//遇到空格或者换行符就跳出去
    {
       s+=ch;
       in>>ch;
    }
    return in;
}

在上面的代码中,如果s是想输入hello world中间有个空格的话,编译器是会自动跳过去的,也就是说,你想输入hello world的话,中间的空格会被忽略,导致的结果为输入进去的字符串为helloworld,中间没有空格隔开,这就和我们的预期不符。

想要解决这个问题,就要把’ '(空格符号)实打实取到,那就得需要get函数
代码示例,如下:

istream & operator>>(istream& in,string &s)
{
    char ch;
    get(ch);
    while(ch !='\n')
    {
        s+=ch;
        get(ch);
    }
    return in;
}

getline
getline可以对一个string进行输入,直到遇到字符delim

istream& getline (istream& is, string& str, char delim);
//我们可以自定义delim字符
istream& getline (istream& is, string& str);
//如果我们不定义delim字符,那就是默认遇到换行符'\n'就代表输入结束。

代码示例,如下:

int main()
{
    string s;
    getline(in,s,'!');
    cout<<s;
    return 0;
}

在上面的代码中,我们自定义了字符串遇到’!'就结束,假设我们输入的是,jaidhdia%¥#!
输出的结果就为jaidhdia%¥#。

7.<

字符串的比较大小都是比各自的第一位字符的ascii码值的大小,谁大谁就大,相等即比较下一个字符,以此类推。

我们这里只讲小于号(<)的重载,其他的符号与这个类似,我就不过多赘述。

bool operator<(const string & s)
{
    size_t i1 = 0,i2 = 0;
    while(i1 < _size && i2 < s._size)
    {
        if(_str[i1] < s._str[i2])
        {
            return true;
        }
        else if(_str[i1]>s._str[i2])
        {
            return false;
        }
        else
        {
            i1++;
            i2++;
        }
    }
    //跳出循环,就说明里面有一个字符串已经匹配到头了,也就是说,短的那个是长的子串
    return i1 == _size;//如果i1 == _size,就说明,第一个字符串已经匹配到头了,那就说明第一个字符串比较小,刚好返回真。如果i1!=_size那就说明第一个字符串比较长,还没到头,说明第一个字符串比较大,刚好返回false。
}

★补充:拷贝构造的现代写法

在这里插入图片描述

四.总结

本文对STL中的string进行了深度讲解,并且尝试的通过自己来模拟实现库中的string。本篇对于我们学习STL有很大的帮助,可以当作一个模板套在其他的容器上进行使用。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值