自学内容网 自学内容网

【C++之STL】摸清 string 的模拟实现(中)

string的模拟实现系列文章:

  1. 模拟实现上
  2. 模拟实现中
  3. 模拟实现下


5. 调整操作

5. 1 push_back()

往字符串后面加一个字符。

注意检查容量是否足够,还有添加'\0'

void string::push_back(char c)
{
    // 检查是否需要扩容
    if (_size == _capacity)
    {
        // 这里采用2倍扩容,或者你也可以1.5倍扩容
        size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newcapacity);
        _capacity = newcapacity;
    }
    // 插入字符和 \0
    _str[_size++] = c;
    _str[_size] = '\0';
}

5. 2 append()

append()有很多重载,但是这里我们只实现插入字符串的和插入多个相同字符的,其他的可以自行尝试。

  1. 追加字符串
void string::append(const char* str)
{
    // 要断言 str 不为空指针
    assert(str);
    size_t len = strlen(str);
    // 扩容
    if (_capacity < len + _size)
    {
        size_t newcapacity = len + _size > 2 * _capacity ? len + _size : 2 * _capacity;
        reserve(newcapacity);
        _capacity = newcapacity;
    }
    // 追加新的数据,strcpy会拷贝\0,所以不需要手动加
    strcpy(_str + _size, str);
    _size += len;
}
  1. 追加多个相同字符
void string::append(size_t n, char c)
{
    // 扩容
    if (_capacity < n + _size)
    {
        size_t newcapacity = n + _size > 2 * _capacity ? n + _size : 2 * _capacity;
        reserve(newcapacity);
        _capacity = newcapacity;
    }
    // 插入数据
    for (int i = 0; i < n; i++)
    {
         //push_back(c);//这里也可以对push_back()进行复用
        _str[_size++] = c;
    }
    // 如果是对push_back进行复用,这里就不用再手动加'\0'了
    _str[_size] = '\0';
}

5. 3 operator+=()

operator+=有两个功能:

  1. 插入字符,相当于push_back()
  2. 插入字符串,相当于append()

可以在不同的重载中复用不同的函数。

为了保证运算符能连续运算,还要注意返回*this

// 追加字符串,复用append()
string& string::operator+=(const char* str)
{
    append(str);
    return *this;
}
// 追加字符,复用push_back()
string& string::operator+=(char c)
{
    push_back(c);
    return *this;
}

5. 4 insert()

string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);

pos位置(下标)插入字符或者字符串,需要把pos位置之后的所有元素都向后挪动,因此效率上可能比较差。

在挪动的时候,要特别注意不要发生越界!

挪动时也要注意不要让先挪动的数据盖住还没挪动的数据,应该从后往前依次挪动来避免这一情况。

    string& string::insert(size_t pos, char c)
    {
        assert(pos <= _size);
        // 扩容
        if (_capacity < _size + 1)
        {
            size_t newcapacity = _size + 1 > 2 * _capacity ? 1 + _size : 2 * _capacity;
            reserve(newcapacity);
            _capacity = newcapacity;
        }
        for (size_t i = _size; i > pos; i--)
        {
            //不能改成_str[i + 1] = _str[i]的形式
            _str[i] = _str[i - 1];
        }
        // 记得加'\0'
        _str[++_size] = '\0';
        _str[pos] = c;
        return *this;
    }

    string& string::insert(size_t pos, const char* str)
    {
        assert(pos <= _size);
        size_t len = strlen(str);
// 扩容
        if (_capacity < len + _size)
        {
            size_t newcapacity = len + _size > 2 * _capacity ? len + _size : 2 * _capacity;
            reserve(newcapacity);
            _capacity = newcapacity;
        }
// 挪动数据
        for (size_t i = _size + len; i >= len; i--)
        {
            _str[i] = _str[i - len];
        }
// 插入字符串
        for (size_t i = 0; i < len; i++)
        {
            _str[i + pos] = str[i];
        }
        _size += len;
        // 加'\0'
        _str[_size] = '\0';
        return *this;
    }

5. 5 erase()

string& erase(size_t pos, size_t len = npos);

5. 5. 1 npos

  1. npos 是一个静态成员常量值,对于 size_t 类型的元素具有最大可能的值。

  2. 当此值用作字符串成员函数中长度时,表示“直到字符串的末尾”。

  3. 作为返回值,它通常用于指示没有匹配项。

  4. 此常量定义为值 -1,因为 size_t 是无符号整型,所以它是此类型的最大可能可表示值。\

其定义为:

    const static size_t npos = -1;

那么回到erase的模拟实现:

删除一段数据,就是把后面的直接拉到前面进行覆盖就行了。

但是要注意,要从pos位开始被覆盖,如果从最后面开始的话,可能会覆盖还没挪动的数据。

string& string::erase(size_t pos, size_t len)
{
    // 如果会把pos位置之后的所有元素全部删除,就不需要挪动数据,直接在pos位置加'\0'就行了
   if (len >= _size - pos - 1)
   {
       _str[pos] = '\0';
       _size = 0;
   }
   else
   {
       // 挪动数据
       // 从 pos + len 位开始向前挪动
       for (size_t i = pos + len; i <= _size; i++)
       {
           _str[i - len] = _str[i];
       }
       _size -= len;
       // 加'\0'
       _str[_size] = '\0';
   }
   return *this;
}

5. 6 swap()

尽管在算法库中已经有了一个swap()函数,但是string类中依然实现了swap,并且有两个,一个是正常的成员函数,还有一个是stdswap函数的重载。

我们先看成员函数这一个:

void swap(string& s)

只需要交换thiss的三个成员变量就可以了:

void string::swap(string& s)
{
    // 交换的时候还可以直接使用 std 中的 swap 进行交换
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

再看对std中的swap的重载:

注意为了防止重定义,要把声明和定义分离在.h.cpp文件中,不能在头文件中直接实现定义。

这个函数放在全局,不放入命名空间,这样当全局有匹配的函数时,就不会在命名空间中搜索函数了,更何况库中的还是函数模板。

// string.h
void swap(test::string& s1, test::string& s2);

// string.cpp
void swap(test::string& s1, test::string& s2)
{
    s1.swap(s2);  // 调用类的成员函数 swap
}

5. 6. 1 为什么要实现成员函数 swap

既然算法库中已经实现了一个swap,而且如果你尝试的话,会发现这个swap也是可以成功交换两个string类型的,但是为什么我们不使用呢?

因为算法库中的swap是通过模板实现的,一般实现为:

template <class T> 
    void swap (T& a, T& b)
{
  T c(a); 
    a=b; 
    b=c;
}

可以看到,算法库中的swap是通过创建临时变量来进行交换的,对于一个自定义类型,发生拷贝是一件很可能严重影响效率的事,如果这个string类中存储了非常多的数据,就会大大拖慢程序的运行。

并且实际上string类型之间的交换完全不需要创建临时变量,只需要交换所有的成员变量就可以了,这样一比较,算法库提供的swapstring类的交换会产生无法接受的损耗,所以在库中要想方设法避免程序员使用到原本的模板生成的函数。

6. 访问操作

6. 1 operator[]

下标访问操作符,就是直接返回_str中对应位置的元素的引用

但是这里要提供两个重载,因为string类如果被const修饰的话,直接返回引用会发生权限放大,导致报错,所以还要提供返回值也被const修饰的重载。

因为下标访问操作符本身

char& string::operator[](size_t index)
{
    return *(_str + index);
}
// 注意这里构成重载的原因是最后的那个const,它修饰的是this指针,返回值类型不同不能构成重载
const char& string::operator[](size_t index)const
{
    return *(_str + index);
}

6. 2 front()back()

分别返回第一个和最后一个元素的引用,出于和下标访问操作符相同的原因,也要提供const版本的。

char& string::front()
{
    return _str[0];
}
const char& string::front()const
{
    return _str[0];
}
char& string::back()
{
    return _str[_size - 1];
}
const char& string::back()const
{
    return _str[_size - 1];
}

下接: 模拟实现下

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章


原文地址:https://blog.csdn.net/fhvyxyci/article/details/143895229

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!