C++——string常见函数的使用和模拟实现

        在C语言中只提供了一系列str的库函数,字符串是以'\0'为结尾的一些字符的集合,这些库函数和字符串是分离的,在C++中添加了一个类——string,用来管理字符串以及一系列的函数。

        在string类中一般来说有三个成员函数,分别是_str、_size和_capacity,_str是一个指向我们存放字符串内容的指针,_size表示这个字符串的的长度(不包括结尾的'\0'),_capacity当前这个string对象能够存储的字符串的长度,这个长度是能够动态增长的。

        一、构造函数

        二、string类对象的访问及遍历操作

        三、string类对象的修改操作

        四、string类的非成员函数

        五、在指定位置的插入和删除


        一、构造函数

        string类常见的实例化对象的方式共有以下几种

    //当不传入参数时,会构造出一个空字符串
    string()
    //传入一个字符串时,会根据这个字符串构造一个函数
    string(const char* s)
    //传入的参数是一个已经存在的string对象时,要进行深拷贝
    string(const string&s)

        首先我们知道字符串都是以'\0'为结尾的,所以我们在使用这个默认构造函数的时候构造出来的“空”字符串并不是真的没有东西,里面至少有一个'\0';同样是这个原因,因为字符串是要以'\0'为结尾的,一个string对象能存储的字符的个数是_capacity-1个,为了让string对象能够存下_capacity个字符并且最后以'\0'结尾,我们在申请空间的时候就要多申请一个。

        

//这里把传字符串和无参的糅合到一起成默认构造函数了
string(const char* str="")
{
    if(str == nullptr)
        return;
    //为了存下'\0'多开了一个位置
    _str = new char[strlen(str)+1];
    strcpy(_str,str);
}

string(const string& s)
{
    //因为是成员函数,所以可以直接取得s对象的_str
    //用s对象的_str直接调用默认构造函数,这样就可以直接拷贝出一份我们想要的空间
    //再把这个空间交换给要实例化的对象
    string tmp(s._str);
    //这里的swap并不是类模版里的swap函数
    swap(str);
}

string& operator=(const sting& s)
{
    //这里要判断是否是用自己来初始化自己
    //如果是自己初始化自己,深拷贝之后释放空间以后就会出错
    if(this!=&s)
    {
        //这里不仅tmp构造好了三个成员变量,因为tmp是局部变量,所以出了这个作用域
        //tmp就会自动销毁,就会调用析构函数,会把this交换过去的_str所指向的那块空间自动释放掉,省去了自己释放空间的过程
        string tmp(s._str);
        swap(tmp);
    }
    return *this;
}

//类模版里的swap是实例化出一个c对象,借助c对象把我们要交换的a,b对象的内容进行交换
//这里的string类中的_str指向了堆上的资源,所以在进行拷贝构造和赋值重载的时候都要进行深拷贝
//这里如果用默认的swap要进行三次深拷贝,效率很低
//我们把重载一个swap的成员函数,手动对这三个成员变量进行交换,因为这三个成员变量都是内置类型
//虽然_str指向一块堆上的资源,但是它本身只是一个指针,所以在交换的时候都不涉及深拷贝
void swap(string& s)
{
    swap(_str,s._str);
    swap(_size,s._size);
    swap(_capacity,s._capacity);
}

        二、string类对象的访问及遍历操作

        在string中多有一个组成部分叫做迭代器——iterator。有了迭代器,我们就可以用类似于指针的形式访问string对象里的内容string类中的迭代器有两种分别是iterator和const_iterator,iterator的权限是可读可写,而const_iterator的权限是只能读不能写。string类中的两个成员函数begin()和end()分别能取得string对象开始位置的迭代器和string对象存储的最后一个字符的下一个位置的迭代器。

//这里我们就用指针来模拟实现迭代器的功能
typedef char* iterator
typedef const char* const_iterator

iterator begin()
{
    return _str;
}

iterator end()
{
    //_size表示存储的字符的个数,下标就是位数-1,所以_size就是最后一个字符的下一个位置,也就是'\0'的位置
    return _str+_size;
}

//才有引用返回可以实现类似s[1]=‘s’的功能
char& operator[](int i)
{
    return _str[i];
}

//比如出现const char& x = s1[0]的情况时x被const修饰,如果只有上面的几种函数就会报错
//因为参数x被const修饰,而返回值是可读可写的,权限被放大了,所以还有一系列const修饰的迭代器的函数

const_iterator begin() const
{
    return _str;
}

const_iterator end() const
{
    return _str+_size;
}

const char& operator[](int i) const
{
    return _str[i];
}

         还有两个函数rbegin 和 rend也是用来获取迭代器的,不过这个迭代器是从尾部开始获取,rbegin获取的是最后一个字符所在位置的迭代器,rend获取的是第一个字符之前位置的迭代器,对rbegin获取的迭代器进行++操作是是向前遍历的。

        因为有了迭代器的存在所以在遍历的时候就多了两种方式,如果想要遍历string对象中的数据,就可以采用下面的迭代器方式和范围for。其中范围for里的参数auto是让编译器自动识别变量的类型,在把s里的数据轮流赋值给ch,其实本质上在编译以后,编译器会把范围for的形式转化为上面的迭代器的形式,所以如果我们在模拟实现的过程中想要使用范围for,就一定要写好begin和end函数。

string::iterator it=s.begin();
while(it!=s.end())
{
    cout<<*it;
}


for(auto ch : s)
{
    cout<<ch;
}

        三、string类对象的修改操作

        这个类型的函数主要有push_back,append,operator+=,c_str、find以及resize。

        push_bakc就是在字符串的尾部添加一个字符c,但是这里就会涉及到一个扩容的问题,当一个string类里数据存满了以后我们要堆容量进行扩容,string类里也提供了一个功能类似的函数reserve。

        reserve函数是用来设置对象的容量,当我们想要设置的容量大于当前对象已有的容量时,会另外开辟一个我们想要的大小的空间,然后再把对象中原有的数据拷贝过去,之后再回首原本的空间。当我们想要设置的空间是小于当前对象的容量的时候,就不会对对象进行操作。

        reserve是对容量进行操作,而resize是对对象存储数据的个数进行操作,当传入的n要大于_capacity时,把对象的容量扩容至n同时用c把剩余的空间填满,当n大于_size并且小于_capacity时,则只进行填充操作不扩容,当n小于_size时,则把对象的内容调整至n个,但是不改变对象的容量。

void reserve(size_t n)
{
    //扩容的空间比原来大才扩容,比原来小不操作
    if(n>_capacity)
    {
        //多开一个位置存放'\0'
        char* tmp=new char[n+1];
        strcpy(tmp,_str);
        delete[] _str;
        _str=tmp;
        _capacity=n;
    }
}

void resize(size_t n, char c = '\0')
{
    if (n > _size)
    {
	    if (n > _capacity)
		    reserve(n);
	    memset(_str + _size, c, n - _size);
    }

    _size = n;
    _str[_size] = '\0';
}

        利用这个函数就可以很方便的进行扩容

void push_back(const char& c)
{
    //_size和_capacity相等的时候表示存满了 多开的那一个位置需要用来存'\0'
    if(_size == _capacity)
    {
        //如果当前容量为0就一次性给4个空间
        reseve(_capacity == 0 ? 4 : _capacity * 2);
    }
    _str[_size++]=c;
    //我们只是给'\0'开了一块空间,这里是一个字符一个字符的进行赋值,所以原来最后一个位置的'\0'被这个字符覆盖了,所以在赋值以后要自己添加上一个'\0'
    _str[_size]='\0';
}

        append函数是在对象的末尾加上一段字符串,这里的逻辑和push_back的逻辑差不多,只是这里扩容就不能进行简单的二倍扩容,有可能存在想要加入的字符串的长度比二倍扩容还有长的情况,同时因为字符串的结尾一定有'\0',所以赋值以后不用自己加上。

void append(const char* str)
{
    //这里计算出来的长度并不包括'\0'但是我们在给对象开空间的时候已经给'\0'预留过空间了,所以不需要加上
    size_t len=strlen(str);
    //判断剩下的空间是否足够存下字符串str,如果不够则进行扩容
    if(len > _capacity - _size)
    {
        //如果二倍扩容以后的空间还不能存下str,则存下str需要多少,我们就扩容多少   
        reserve(len > 2 * _capacity ? len : 2 *_capacity);
    }
    strcpy(_str+_size,str);
    _size += len;
}

        对string对象进行+=操作也有两种形式,可以+=一个字符,也可以+=一个字符串,都是在对象的尾部添加数据,这里和我们上面实现的两个函数功能相同,就可以进行复用了

//这里才有传引用返回的形式就可以支持连续+=的操作
string& operator(const char c)
{
    push_back(c);
    return *this;
}

string& operator(const char* str)
{
    append(str);
    return *this;
}

        当然我们还可以支持查看string对象中_str里的数据,同时我们对对象进行了封装,我们不希望在类外,使用者能够自行对_str里的数据进行操作,只能通过我们给的接口对对象进行操作,所以这样。

const char* c_str() const
{
    return _str;
}

          find函数也支持两种查找,find(char c, size_t pos),从pos位置开始查找c第一次出现的位置,如果找到则返回这个位置的下标,没有找到则返回npos(对象中存储数据的结束位置,也就是'\0'这里不属于这个对象),find(const char* s, size_t pos)同样是从pos位置开始查找字符串s第一次出现的位置,找到则返回s的第一个字符的下标,如果没找到也返回npos;这两个函数中的pos如果没有指定,则会从头开始搜索。

size_t find(char c, size_t pos = 0) const
{
    for(size_t i=pos;i<_size;++i)
    {
        if(_str[i] == c)
            return i;
    }
    return npos;
}

size_t find(const char* s,size_t pos = 0)const
{
    char* tmp = strstr(_str+pos,s);
    if(tmp)
        return tmp-_str;
    return npos;
}

        四、string类的非成员函数

        在string类的非成员函数中最重要的就是operator>>和operator<<,对这两个运算符进行重载以后就可以直接堆string类的对象进行插入和提取操作了。

        要注意的是在进行operator>>重载的时候有几个要注意的点。一是:如果我们直接使用cin对内容进行提前的话,cin会默认跳过' '和'\n'这样函数就一直提取不到这两个字符,就不能用这个两个字符作为提取的结束标志,所以我们要采用cin.get()的方式,这样就可以提取到' '和'\n';二是:如果对一个已经有内容的对象进行插入操作时,并不会把原先的内容清除,所以我需要有一个函数来清除对象内原本存在的内容,但是不回收空间,这样能提供效率;三是:如果在插入字符串的时候是一个字符一个字符的进行插入,那这样扩容的成本会很高,因为我们每次都只进行二倍的扩容,假如我们对象开始的容量很小,但是要插入的字符串很长,这里就会浪费很多效率在扩容上,如果我们一开始就给对象一个很大的容量,当插入的字符串很短的时候,又会浪费空间;所以这里我们采用自己设计一个缓存区,当这个缓存区满的时候我们才给对象赋值,这样对象一次就能开比较大的空间同时,如果要插入的内容很短的时候,也不会浪费空间。

//这里是要进行流提取不会对对象s进行操作,所以可以用const修饰并且采用传引用传参能减少拷贝
ostream& operator<<(ostream& out,const string& s)
{
    for(auto e : s)
    {
        out<<e;
    }
    return out;
}

void clear()
{
    //只要把_size指向第一个位置,同时第一个位置置成'\0'就完成清除数据的工作了
    _str[0]-'\0';
    _size=0;
}

istream& operator>>(isteram& in,string& s)
{
    s.claer();
    char ch;
    const int N = 256;
    char buff[N]={ 0 };
    ch = in.get();
    int i=0;
    while(ch != '\0')
    {
        buff[i++] = ch;
        if(i == N - 1)
        {
            buff[i] = '\0';
            s+=buff;
            i = 0;
        }
        ch = in.get();
    }
    if( i != 0)
    {
        buff[i] = '\0';
        s+=buff;
    }
    return in;
}

        五、在指定位置的插入和删除

        insert函数可以实现在指定的位置插入数据,同样有插入字符和插入字符串两个版本,而erase则是删除从pos位置开始长度为len的字符串。

    void insert(size_t pos, char c)
    {
	    if (_size == _capacity)
	    {
		    reserve(_capacity == 0 ? 4 : 2 * _capacity);
	    }
        //_size存的是'\0'所以实际上空的位置是_size + 1
	    size_t end = _size + 1;
        while(end > pos)
        {
            _str[end] = _str[end - 1];
            --end;
        }
        _str[pos] = c;
        ++_size;
    }    
	
    void insert(size_t pos, const char* str)
	{
		int len = strlen(str);
		if (len + _size > _capacity)
		{
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		size_t end = _size + len;
		while (end>pos)
		{
			_str[end] = _str[end - len];
			--end;
		}
		memcpy(_str + pos, str,len);
		_size += len;
	}


    void erase(size_t pos, size_t len)
	{
        //当要删除的长度超过_size时,和清空数据一样操作
        //直接在要清空的位置置成'\0'再把_size指向这个位置就完成清除操作了
		if (pos + len > _size)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			strcpy(_str + pos, _str + pos + len);
			_size -= len;
		}
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值