🔥个人主页: Forcible Bug Maker
🔥专栏: STL || C++
目录
前言
本篇博客主要内容:实现string类的基本功能。
string使用很快就讲完了,但是光会用string还不够,在面试中,面试官总喜欢让我们自己来模拟实现string类,包括string类的构造、拷贝构造、赋值运算符重载以及析构函数等等内容。所以,我认为string类的自实现还是有必要讲一下的。
🔥string类的接口函数
我们本次并不会将string类的所有接口函数逐一讲解,讲一些常用的和重点的。本次string的实现分成了两个文件,一份是string.h
,一份string.cpp
。
看看string.h的内容:
cpp
# define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using std::cout;
using std::cin;
using std::endl;
namespace ForcibleBugMaker
{
class string
{
public:
// 交换,非成员函数
friend void swap(string& s1, string s2);
// 定义迭代器
typedef char* iterator;
typedef const char* const_iterator;
//迭代器获取
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
// string默认成员函数
string(const char* str = "");
string(const string& s);
string& operator=(string tmp);
~string();
// 获取只读字符串
const char* c_str() const;
// 获取容量
size_t size() const;
size_t capaity() const;
// []获取元素重载
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
// 开辟空间
void reserve(size_t n);
// 尾插字符或字符串
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
// 插入字符或字符串
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
//删除字符串
void erase(size_t pos = 0, size_t len = npos);
// 查找字符或字串
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
// string对象比较
bool operator>(const string& str) const;
bool operator==(const string& str) const;
bool operator>=(const string& str) const;
bool operator<(const string& str) const;
bool operator<=(const string& str) const;
bool operator!=(const string& str) const;
// 获取string对象字串
string substr(size_t pos, size_t len);
// 交换函数,成员函数
void swap(string& str);
// 清除串中内容
void clear();
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
// 常量成员
const static size_t npos;
};
// 交换函数,非成员函数
void swap(string& s1, string s2);
// 流插入和流提取重载,非成员函数
std::ostream& operator<<(std::ostream& so, const string& str);
std::istream& operator>>(std::istream& is, string& str);
}
在实现string类添加和拷贝的一些函数中,使用了C语言中的一些库函数,以此方便实现,这些函数在C++中统一存放在<cstring>
。
如果对C语言的一些字符串函数不太了解,可以看看我之前写的一篇博客:C语言-字符串函数,相信会对你有所帮助。
🔥string类的模拟实现
接下来进入主要内容,按照string.h的接口开始实现。
以下接口都是放在命名空间里的,不同文件的相同命名空间在编译时会自动合并。
swap交换
如果你仔细观察,会发现存在两个swap交换函数,一个string中的成员函数,另一个是非成员函数。
在std的默认swap当中,是这样的:
cpp
template <class T> void swap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
当我们使用这个成员函数进行交换时,会造成拷贝消耗,我们提供对应的非成员函数重载是为了防止C++程序员掉坑。
成员函数的swap:
cpp
void string::swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
非成员函数的swap:
cpp
void swap(string& s1, string& s2)
{
std::swap(s1._str, s2._str);
std::swap(s1._size, s2._size);
std::swap(s1._capacity, s2._capacity);
}
默认成员函数
默认成员函数,就是不提供编译器自动会生成的一些函数。我们这里实现构造函数,析构函数和赋值运算符重载。
构造函数:
cpp
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size + 1;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
构造函数提供了两个,分别支持了字符串构造和string对象构造。_str指向通过new开辟空间,这个空间需要比实际的capacity大,因为需要在字符串末尾多存放一个'\0'
。
析构函数:
cpp
string::~string()
{
delete[] _str;
_str = nullptr;
_capacity = 0;
_size = 0;
}
这个没什么好说,释放空间,指针置空,_size和_capacity置0。
赋值运算符重载:
string的赋值属于深拷贝。
拷贝分为深拷贝和浅拷贝:浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。可以采用深拷贝解决浅拷贝问题 ,即:每个对象都有一份独立的资源,不要和其他对象共享 。
深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
浅拷贝:
深拷贝:
如果中规中矩的来写(传统版赋值重载):
cpp
String& operator=(const String& s)
{
if (this != &s)
{
char* pStr = new char[strlen(s._str) + 1];
strcpy(pStr, s._str);
delete[] _str;
_str = pStr;
}
return *this;
}
但如果你能对之前实现的代码进行复用,你会发现这个过程可以简化非常多(现代版赋值重载):
cpp
string& string::operator=(string tmp)
{
// 调用成员函数的swap,交换*this和tmp
swap(tmp);
return *this;
}
在C++的编写中,学会复很有必要。
迭代器
之前讲过,迭代器不一定是指针,但是你可以把它想象成指针,可以通过正常的运算操作来控制其指向的元素。在string中,我们使用指针来模拟实现迭代器。
迭代器在不同编译器下的实现方式有所不同,比如VS下的迭代器就不是一个指针,而是一个类模板。
在类中需要typedef一下,可以看到我们所实现的string迭代器的本质:
cpp
// 定义迭代器
typedef char* iterator;
typedef const char* const_iterator;
获取迭代器的接口:
cpp
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::begin() const
{
return _str;
}
string::const_iterator string::end() const
{
return _str + _size;
}
这时,我们已经可以使用我们自己的迭代器了:
cpp
string str("hello world!");
string::iterator it = str.begin();
while (it != str.end()) {
cout << *it << " ";
++it;
}
cout << endl;
获取容量和内容信息
这些都是返回str类型状态和内容的函数。
cpp
const char* string::c_str() const
{
return _str;
}
size_t string::size() const
{
return _size;
}
size_t string::capaity() const
{
return _capacity;
}
// 运算符重载
char& string::operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& string::operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
都比较简单易懂。
reserve预留空间
在开始字符串增删查改之前,有必要介绍这个函数,使用它可以更好的控制我们string对象的内存管理。
cpp
void string::reserve(size_t n)
{
if (_capacity < n) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
当n大于当前容量_capacity的时候,进行空间的开辟,将_str中的内容拷贝到新空间中去,同时delete释放旧空间。
尾插字符和字符串
接口函数,一个是push_back,一个是append。
cpp
void string::push_back(char ch)
{
if (_size >= _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_capacity < _size + len) {
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
这两个成员函数都使用了reserve来预留空间。
同时还需要有 运算符重载+= 来实现尾插,用重载的运算符执行这样的操作才是最爽的。
cpp
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
其实不用再实现一遍了,直接复用就行。
可以简单看一下使用的效果:
cpp
string str("hello world!");
char ch = 'T';
const char* s = "hhhhhh";
cout << str << endl;
str += ch;
cout << str << endl;
str += s;
cout << str << endl;
字符或字符串的插入和删除
这里的逻辑稍微有些复杂,而且还有几个比较容易掉的坑。
cpp
// 字符的插入
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size >= _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 注意这里为什么要+1,size_t是无符号整型,没有负值
//当有符号和无符号比较时,统一会被转成无符号
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
// 字符串的插入
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len == 0)return;
if (_size + len > _capacity) {
reserve(_size + len);
}
// 注意这里为什么要+len,size_t是无符号整型,没有负值
//当有符号和无符号比较时,统一会被转成无符号
size_t end = _size + len;
while (end > pos + len - 1) {
_str[end] = _str[end - len];
--end;
}
memcpy(_str + pos, str, len);
_size += len;
}
// 字符串的删除
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
// 当pos+len过大,超过_size,则取到末尾
if (len >= _size - pos) {
_str[pos] = '\0';
_size = pos;
}
else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
使用案例:
cpp
string str("hello world!");
char ch = 'T';
const char* s = "hhhhhh";
cout << str << endl;
str.insert(3, ch);
cout << str << endl;
str.insert(3, s);
cout << str << endl;
str.erase(3, strlen(s));
cout << str << endl;
find查找
查找C语言查找字符串的库也有提供,可以直接使用。
cpp
size_t string::find(char ch, size_t pos)
{
for (size_t i = pos; i < _size; i++) {
if (_str[i] == ch)
return i;
}
return npos;
}
size_t string::find(const char* sub, size_t pos)
{
const char* ptr = strstr(_str, sub);
return ptr - _str;
}
使用案例:
cpp
string str("hello world!");
cout << str.find('w') << endl;
cout << str.find("wor") << endl;
缺省参数在函数声明那里。
比大小运算符重载
C语言中,有一个按能按字典序将字符串比大小的函数------strcmp
。
cpp
bool string::operator>(const string& str) const
{
return strcmp(_str, str._str) > 0;
}
bool string::operator==(const string& str) const
{
return strcmp(_str, str._str) == 0;
}
bool string::operator>=(const string& str) const
{
return *this > str || *this == str;
}
bool string::operator<(const string& str) const
{
return !(*this >= str);
}
bool string::operator<=(const string& str) const
{
return !(*this > str);
}
bool string::operator!=(const string& str) const
{
return !(*this == str);
}
只需要实现前两个,后面的复用就行。
获取子串
使用substr可以获取所需对象字串。
cpp
string string::substr(size_t pos, size_t len)
{
if (pos + len >= _size) {
string sub(_str + pos);
return sub;
}
else {
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++) {
sub += _str[pos + i];
}
return sub;
}
}
清除clear
可以将对象内容都删除,但_capacity保持不变。
cpp
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
流插入和流提取
这两个函数之前在Date类部分实现的时候,定义为了Date类的友元。但是,流插入和流提取其实可以不定义为string类的友元。
cpp
// 流插入
std::ostream& operator<<(std::ostream& os, const string& str)
{
for (int i = 0; i < str.size(); i++)
os << str[i];
return os;
}
// 流提取
std::istream& operator>>(std::istream& is, string& str)
{
str.clear();
char ch = is.get();
while (ch != ' ' && ch != '\n') {
str += ch;
ch = is.get();
}
return is;
}
string对象读取的截断是空格 和换行 ,但是cin
对象默认是读不到这两个字符的,这就需要使用另一种读取方式:cin.get()
。
使用案例:
cpp
string str;
cin >> str;
cout << str << endl;
常量成员npos
常量成员的定义需要实现在类的外部,像这样:
cpp
const size_t string::npos = -1;
当常量的内容是整型(浮点型和字符类型等都不行)时,也可以作为缺省参数进行定义。
结语
本篇博客主要介绍了string类常用接口的实现,包括默认成员函数,迭代器,字符和字符串的插入删除等等内容。
后续博主还会继续分享与STL相关的内容,感谢大家的支持。♥