类与对象

string类的成员变量:
我们今天主要模拟实现下string类的一些常用函数接口,来进一步的理解string类。我们首先需要一个动态开辟的指针指向这个字符串,然后还需要容量和存储的个数,并且我们不能和标准库的string进行冲突所以我们需要写在我们自己的类域中,并且我们库中还有一个静态的变量是npos,就是无符号的-1,代表整形的最大值:

namespace huyang
{
    class string
    {
    public:
        //成员函数
    private:
        char *_str;
        size_t size;
        size_t capaticy;
    public:
        const static size_t npos = -1;
    };
};


这其中有一点是非常奇怪的,首先我们的static成员变量应该在类中声明,在类外部定义,但是在类的内部如果是上面的写法那么就相当于是直接定义了,这点是C++的特例。因为我们的const  静态变量 npos后面需要访问,所以我们设置为共有。

string的构造函数:
我们的构造函数其中有很多的细节,我们慢慢来讲解:

一般的我们指针初始化时,我们都是给nullptr初始化,但是我们的string是不可以的,因为我们有一个c_str函数的接口要提供,我们如果给成nullptr后,调用这个函数就会崩溃。根本原因就是我们默认的_str给的是空指针,我们调用c_str后会直接解引用访问它,导致崩溃

我们的无参的函数应该也开辟一个空间,来存放 '\0' ,并且我们使用new [] 进行开辟空间。正常我们应该使用new 就可以,但是为了后面的析构函数对应,那么我们应该使用new []来和delete [] 配合使用。

//无参的构造
string()
    :_str(new char[1])
    , _size(0)
    , _capaticy(0)
{
    _str[0] = '\0';
}


 如果有参数的构造呢?我们应该先计算出字符串长度,然后我们在开辟空间,开辟空间时,我们要多开一个字节用来存放 ' \0 ',接着把size和capaticy共同的赋初值,最后把这个字符串拷贝到我们开辟好的空间上。

string(const char* str)
    :_str(new char[strlen(str) + 1])
    , _size(strlen(str))
    , _capaticy(strlen(str))
{
    strcpy(_str, str);
}


 这个代码我们还可以进行优化,因为我们只用计算出一次strlen就可以啦,比如我们size是用strlen计算得出的,那么我们开辟的空间就是size+1,capaticy就是size。这样我们减少了计算,毕竟我们的strlen接口是O(N)的接口。

//error 错误的示范
string(const char* str)
    :_str(new char[_capaticy + 1])
    , _size(_capaticy)
    , _capaticy(strlen(str))
{
    strcpy(_str, str);
}


但是这样我们还要有点需要注意的,那就是初始化链表的顺序,这个初始化的顺序和我们写的顺序无关,和我们的声明顺序有关的,就是我们是先声明的就先初始化,后声明就后初始化,我们是先声明的_str,再声明的size,最后声明的capaticy,那么我们如果是按照上面的逻辑就是下面的情况:

我们这其中有5个有效字符了,但是我们的size还是为0,原因就是我们的成员变量之间是有关系的,我们在初始化时先初始化的是_str,_str是用一个capaticy的随机值来进行初始化的,而我们的size则是使用_capaticy初始化的,最后容量才是我们的strlen的值,这样是不合理的,我们的解决办法就是不使用初始化列表,而在函数体内初始化,函数体就不会出现谁先初始化导致的错误。

//带参数的构造
string(const char* str)
{
    _size = strlen(str);
    _str = new char[_size + 1];
    _capaticy = _size;
    strcpy(_str, str);
}


   

全缺省的构造函数:
我们搞定了无参和有参数的,最后我们使用最多最好用的还是全缺省的。我们一般缺省的_str提供的是一个空串,size和capaticy是0。
//全缺省的
string(const char* str = "")
{
    _size = strlen(str);
    _capaticy = _size;
    _str = new char[_size + 1];
 
    strcpy(_str, str);
}


string的拷贝构造,赋值(opeartor = ) 和析构:(深浅拷贝)
我们string类默认生成的拷贝构造是浅拷贝,但是我们的浅拷贝在析构是会出错,所以我们要实现出深拷贝。其中我们分为了传统写法和新派写法,我们分别来看看吧

关于拷贝构造的传统写法:

我们首先肯定是开辟一个和 s._capaticy 一样的空间,把size和capapticy设置为一样,然后把数据拷贝过来就好了。

//传统写法的深拷贝
string(const string& s)
    :_str(new char[s._capaticy + 1])
    , _size(s.size())
    , _capaticy(s._capaticy)
{
    strcpy(_str, s._str);
}


我们深拷贝的现代写法中更多的是复用构造函数的部分,减少拷贝直接交换。在写之前我们还要支持一下string的swap函数,我们swap函数中也是使用std标准库中提供的,所以我们需要在前面加上 ::类作用限定符,空白表示的是全局域。我们要使用一个临时对象进行构造,然后再把构造好的对象进行交换到我们引用传参的对象中。就完成了拷贝。 

void swap(string& s)
{
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capaticy, s._capaticy);
}
 
//现代写法的拷贝构造
string(const string& s)
    :_str(nullptr)
    , _size(0)
    , _capaticy(0)
{
    string tmp(s._str);
    swap(tmp);
}


我们的赋值操作符也要进行深拷贝,也要进行就交换。赋值的传统写法:首先我们先判断是否自己和自己交换,如果是的话就直接返回,如果不是,我们首先要开辟空间,然后我们进行拷贝字符,这样其实防止了new出错之后破坏原来的字符串的情况,最后进行交换返回。

//传统写法赋值
string& operator=(const string& s)
{
    if (this != &s)
    {
        char* tmp = new char[s._capaticy + 1];
        strcpy(_str, s._str);
        delete[] _str;
        _str = tmp;
        _size = s._size;
        _capaticy = s._capaticy;
    }
 
    return *this;
}


现代写法中我们也要先判断,接着我们直接进行构造一个对象,然后在交换。

//现代写法1.0
string& operator=(const string& s)
{
    if (this != &s)
    {
        string(tmp);
        swap(tmp);
    }
}


 然后我们对现代写法在优化下:我们直接使用传值传参,这个形参值也会进行拷贝,然后我们直接利用参数进行构造,在进行交换,这样也防止了我们会自己拷贝自己的情况。

//现代写法2.0
string& operator=(string s)
{
    string(s);
    return *this;
}


string的遍历方式:
我们在string中遍历字符串,一般无外乎以下三中方式:

1. 下标遍历( operator []) 2. 迭代器(iterator ) 3.范围for(底层还是迭代器)

我们先说下标遍历吧:(operator [])

我们的_str是一个指针,那么我们可以通过数组的方式来访问,只需要重载operator []即可。我们还是要重载两个版本的,因为普通变量和const变量的访问权限不一样。

//普通变量,可读可写
char& operator[](size_t pos)
{
    assert(pos < _size);
 
    return _str[pos];
}
 
//const变量,只读属性
char& operator[](size_t pos) const
{
    assert(pos < _size);
    return _str[pos];
}

2.迭代器(iteator)

在string中,迭代器就是一个指针,只不过我们进行了封装,typedef一下就可以啦,同样我们也要实现两个版本的,const和非const的。

typedef char* iterator;
typedef const char* const_iterator;
 
iterator begin()
{
    return _str;
}
 
const_iterator begin()const
{
    return _str;
}
 
iterator end()
{
    return _str +_size;
}
 
const_iterator end() const
{
    return _str +_size;
}

3. 我们范围for的底层就是迭代器,所以我们不用实现,只要实现了迭代器,那么我们就可以直接使用范围for,我们可以一会儿看下底层调用。

我们来看下例子:

void test_string2()
{
    string s1("world");
    for (size_t i = 0; i < s1.size(); i++)
    {
        cout << s1[i] << " ";
    }
    cout << endl;
 
    string::iterator it = s1.begin();
    while (it != s1.end())
    {
        (*it)++;
        cout << *it << " ";
        it++;
    }
    cout << endl;
 
    for (auto& e : s1)
    {
        e--;
        cout << e << " ";
    }
    cout << endl;
}

  

我们看下范围for和迭代器:

string类的申请空间:
一般是我们原空间容量满了,需要申请空间扩容,我们的扩容函数还是要先申请空间,然后在进行拷贝,接着我们delete原来的空间,把申请的空间的指针和 容量 赋值过去即可。 

void reserve(size_t n)
{
    if (n > _capaticy)
    {
        char* tmp = new char[n + 1];
        strcpy(tmp, _str);
        delete[] _str;
 
        _str = tmp;
        _capaticy = n;
    }
}

我们来实现下吧:

void resize(size_t n, char ch = '\0')//缺省参数给'\0'
{
    if (n > _size)//如果比较大,那么我们申请空间,初始化
    {
        reserve(n);
        for (size_t i = _size; i < n; i++)
        {
            _str[i] = ch;
        }
        _str[n] = '\0';
        _size = n;
    }
    else
    {
        _str[n] = '\0';
        _size = n;
    }
}

我们看下测试例子:

void test_string9()
{
    string s0;
    s0.resize(10);
    cout << s0 << endl;
    string s1;
    s1.resize(16, 'x');
    cout << s1 << endl;
    string s2("11111111111111111111111111111111111111111111111111111111111111111");
    s2.resize(10);
    cout << s2 << endl;
}

        

第一行是10个空间被初始化为 ' \0 ',没有显示打印。

第二行是,我们传入的是16和 ' x ',所以就初始化为全 ' x '.

第三行虽然我们构造是传入的参数很长,但是我们resize时传入的是10,所以我们的只有前9个1,还有一个是 ' \0 '。

string的增删查改:
我们上面实现了扩容了,下面我们就实现下我们的增删查改吧。

push_back(char ch) 尾部插入字符,首先我们在扩容时,判断下string的对象是否是空串,接着就插入数据,++_size ,再把_size位置的值赋值为' \0 '。

void push_back(char ch)
{
    if (_size == _capaticy)
    {
        //防止刚开始传的是空串。
        reserve(_capaticy == 0 ? 4 : 2 * _capaticy);
    }
    _str[_size] = ch;
    ++_size;
    _str[_size] = '\0';//一定要有'\0',这是C形式字符串结束的标志
}
 append(const char* str) 这个是插入一个字符串,这个我们需要先提前开辟len字符的空间,然后我们进行拷贝,在把_size+=len。

void append(const char* str)
{
    size_t len = strlen(str);
 
    if (_size + len > _capaticy)
    {
        reserve(_size + len);
    }
 
    //从_str+_size位置插入字符串str
    strcpy(_str + _size, str);
    _size += len;
}
然后我们重载operator+=就可以直接复用上面的代码:

string& operator+=(char ch)
{
    push_back(ch);
 
    return *this;
}
 
string& operator+=(const char* str)
{
    append(str);
 
    return *this;
}


我们还要实现任意位置的插入字符或者字符串:

string& insert(size_t pos, char ch),对于这个函数我们首先要先进行断言,不能使pos超过_size,这个我们进行开辟空间,还要注意是否刚开始时空串这个问题,最后我那么进行挪动数据和放置数据,我们画图理解下这个挪动数据的过程。

string& insert(size_t pos, char ch)
{
    assert(pos <= _size);
 
    if (_size == _capaticy)
    {
        reserve(_capaticy == 0 ? 4 : 2 * _capaticy);
    }
 
    size_t end = _size + 1;
 
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        --end;
    }
    _str[pos] = ch;
    ++_size;
 
    return *this;
}

代码如下:

string& insert(size_t pos, const char* str)
{
    assert(pos <= _size);
    size_t len = strlen(str);
 
    if (_size+len > _capaticy)
    {
        reserve(_size + len);
    }
    
    size_t end = _size + len;
 
    while (end >= pos+len)
    {
        _str[end] = _str[end-len];
        --end;
    }
 
    strncpy(_str + len, str, len);
    _size += len;
 
    return *this;
}

 我们实现了insert之后就可以把push_back和append进行修改,复用我们的insert就好了。

删除pos位置之后的n个字符:

void erase(size_t pos, size_t len = npos),我们先进行断言pos<_size,此时不能是 = size,因为_size处不可删除是 ' \0 ',接着我们判断下 pos + len是否大于size 或者 len是否是npos。如果是上面的两种情况那么直接把pos位置放入' \0 ',然后把_size = pos即可。

否则就是我们的pos+len小于size,那我们就把pos+len处的字符依次拷贝到pos后面即可。

代码如下:

void erase(size_t pos, size_t len = npos)
{
    assert(pos < _size);
    if (len == pos || pos + len >_size)
    {
        _str[pos] = '\0';
        _size = pos;
    }
    else
    {
        strcpy(_str + pos, _str + len + pos);
        _size -= len;
    }
}


我们在实现下查找算法:

size_t find(char ch, size_t pos = 0) const  

从pos位置找一个字符,返回字符的下标。我们先断言pos<_size,再直接遍历就可以啦。

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

在pos位置查找子串,然后我们返回最开始的下标。还是先断言,sub不能为空,并且下标要正常的pos<_size,我们可以使用strstr帮助我们实现。strstr找到时返回一个指针,我们获取下标直接让返回的指针减去_str即可。(因为我们是char类型)

size_t find(const char* sub, size_t pos = 0) const
{
    assert(sub);
    assert(pos < _size);
              
    const char *ptr = strstr(_str + pos, sub);
    if (ptr == nullptr)
    {
        return npos;
    }
    else
    {
        //返回的要是下标,ptr和_str是指针,指针相减就是下标(char类型的)
        return ptr - _str;
    }
}

我们还要提供一个函数获取pos位置后len个字符的函数,就是获取子串。

string substr(size_t pos, size_t len = 0)

和我们删除算法类似,首先我们断言pos<_size,如果len = pos 或者 len+pos >_size时,我们就从pos位置获取到最后即可。否则就是获取pos后len个字符组成子串。

string substr(size_t pos, size_t len = 0)
{
    assert(pos < _size);
    size_t realLen = len;
 
    if (len == npos || pos + len >_size)
    {
        realLen = _size - pos;
    }
    string sub;
    for (size_t i = 0; i < realLen; i++)
    {
        sub += _str[pos + i];
    }
 
    return sub;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值