【C++之STL】摸清 string 的模拟实现(中)
string
的模拟实现系列文章:
- 模拟实现上
- 模拟实现中
- 模拟实现下
文章目录
- 5. 调整操作
- 5. 1 `push_back()`
- 5. 2 `append()`
- 5. 3 `operator+=()`
- 5. 4 `insert()`
- 5. 5 `erase()`
- 5. 5. 1 `npos`
- 5. 6 `swap()`
- 5. 6. 1 为什么要实现成员函数 `swap`
- 6. 访问操作
- 6. 1 `operator[]`
- 6. 2 `front()`和`back()`
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()
有很多重载,但是这里我们只实现插入字符串的和插入多个相同字符的,其他的可以自行尝试。
- 追加字符串
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;
}
- 追加多个相同字符
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+=
有两个功能:
- 插入字符,相当于
push_back()
- 插入字符串,相当于
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
-
npos
是一个静态成员常量值,对于size_t
类型的元素具有最大可能的值。 -
当此值用作字符串成员函数中长度时,表示“直到字符串的末尾”。
-
作为返回值,它通常用于指示没有匹配项。
-
此常量定义为值
-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
,并且有两个,一个是正常的成员函数,还有一个是std
中swap
函数的重载。
我们先看成员函数这一个:
void swap(string& s)
只需要交换this
和s
的三个成员变量就可以了:
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
类型之间的交换完全不需要创建临时变量,只需要交换所有的成员变量就可以了,这样一比较,算法库提供的swap
对string
类的交换会产生无法接受的损耗,所以在库中要想方设法避免程序员使用到原本的模板生成的函数。
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];
}
下接: 模拟实现下
谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章