目录
前言
auto
typeid
范围for
使用方法
string类的模拟实现
默认构造函数
拷贝构造函数
swap
赋值重载
析构函数
迭代器iterator
begin和end
c_str
clear
size
capacity
[]运算符重载
push_back
reserve
append
+=运算符重载
insert
erase
find
npos
substr
比较运算符重载
<<运算符重载
>>运算符重载
完整代码
string.h
string.cpp
前言
string是一个字符串类
string文档:
string - C++ Reference
string类和数据结构中的顺序表非常相似,只要我们懂了顺序表那么string类也是手拿把掐了
C数据结构:顺序表-CSDN博客
为什么要有string类?和我们平时使用的 "内容"有什么区别?
string类有非常多的接口,比如size,substr等等,这些都可以直接的帮助我们完成所需对字符串的改变
在学习STL模板之前,我们还需要了解两个前置知识
auto
auto声明的变量必须由编译器在编译时期推导而得
也就是说编译器会帮我们自动推导类型,不需要我们自己写,具体示例看下面与typeid的结合使用
typeid
格式为typeid(变量).name()
typeid的作用是打印出变量的类型
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
使用auto的注意事项
1. auto声明指针类型时,使用auto和auto*没有任何区别,但是auto声明引用类型时必须加&
2. 当同一行声明多个变量时,auto会根据第一个变量进行推导类型,所以后面的变量也必须要和第一个变量类型相同
3. auto不能作为函数的参数(自己定义编译器推导不出来),但是可以做为返回值(根据返回值推导)
4. auto不能直接用来声明数组
范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会因为粗心犯错误
因此C++11中引入了基于范围的for循环,可以把它叫做范围for
范围for的作用是在数组和容器对象上进行遍历
使用方法
for循环后的括号由冒号“:”分为两个部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束
int main()
{
int arr[] = { 0,1,2,3,4 };
for (auto x : arr)
{
cout << x << " ";
}
cout << endl;
return 0;
}
它不仅仅能在这个int数组里使用,它还能在其他各种容器使用(STL),因为这个机制所以我们不需要了解任何容器的底层即可对一个容器进行遍历和取值
先来看看string类的遍历
int main()
{
std::string str = "123456";
for (auto c : str)
{
cout << c;
}
cout << endl;
return 0;
}
其实它的底层是这样的
int main()
{
std::string str = "123456";
std::string::iterator it;
for (it = str.begin(); it != str.end(); it++)
{
cout << *it;
}
cout << endl;
return 0;
}
iterator是一个迭代器,在下面的模拟实现里可以把它看成就是一个char*的指针
每一个容器都有一个iterator迭代器,但它们的实现方式都不一样,不能把它看成简单的指针
所以在我们模拟实现的时候,如果想要让我们自己实现的string类支持范围for,首先我们需要先把begin,end,!=重载即可
具体实现看下面的模拟实现
string类的模拟实现
首先我们需要对它使用命名空间进行封装,与std中的string错开
其次我们需要三个类成员
namespace lyw
{
class string
{
public:
// ...
private:
char* _str = nullptr;
int _size = 0;
int _capacity = 0;
};
}
_str用来存储字符串
_size表示字符串大小(不包括'\0')
_capacity表示容量(不包括'\0')
这里的实现并不会把所有都实现,但会把一些相对重要的模拟实习了
默认构造函数
string(const char* str = "")
{
int len = strlen(str);
_str = new char[len+1];
strcpy(_str, str);
_size = len;
_capacity = _size;
}
使用strcpy直接拷贝到新空间即可
拷贝构造函数
string(const string& s)
{
_str = new char[s._size];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
和默认构造逻辑相同
还有另一种现代写法
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
直接利用拷贝构造函数构造一个和s一样的tmp,然后让我们的*this和tmp交换,这样最后不仅编译器会通过析构函数自动释放tmp,并且还达到了我们的目的
但是swap函数还是需要我们自己写的,不能用库里的
swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
直接利用库里的swap将内部成员交换即可
赋值重载
这个和拷贝构造函数的思路一致
string& operator=(string s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._size];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
它也有现代写法
string& operator=(string s)
{
if (this != &s)
swap(s);
return *this;
}
析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
析构函数只需要释放空间即可
迭代器iterator
迭代器分为普通迭代器和const迭代器,当然还有reverse迭代器
iterator |
const_iterator |
reverse_iterator |
const_reverse_iterator |
这里着重实现iterator和const_iterator
typedef char* iterator;
typedef const char* const_iterator;
这里的iterator我们可以把它看成一个char*的指针,把这个指针当成迭代器使用
begin和end
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
直接可以基于迭代器实现iterator版本的begin、end和const_iterator版本的begin、end
c_str
由于类内部成员变量是设置为了private的,所以如果需要使用类中的_str,可以使用该函数获取_str
const char* c_str() const
{
return _str;
}
clear
将字符串清空
void clear()
{
_str[0] = '\0';
_size = 0;
}
size
size_t size() const
{
return _size;
}
capacity
size_t capacity() const
{
return _capacity;
}
[]运算符重载
char& operator[](size_t pos) const
{
return _str[pos];
}
push_back
void string::push_back(char ch)
{
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
这里需要加上命名空间的原因是因为这个定义的地方和声明的地方不一样,类和函数声明是在.h头文件中,而实现是在.cpp文件中,所以需要加上域作用符
这个很简单,只需要直接在后面插入一个元素即可
但好像是不是少了些什么?我们的空间从哪来?
这时候我们就需要扩容,因为刚开始的capacity是0,这时候我们可以选择写一个扩容逻辑,但我们可以直接写reserve函数(扩容逻辑),这样可以直接在push_back中使用
void string::push_back(char ch)
{
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
reserve
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
n表示需要扩容的空间个数
当扩容的n大于capacity时,则会扩容,反之,由编译器自主决定(缩容或者不变)
这里每一种策略都会有对应的好处
例如缩容会比较节省空间,但是缩容的时间消耗会相对较多
若不变,则牺牲了空间,但不会消耗时间
所以两种策略各有优缺点
append
它的作用是在最后加上一段字符串
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
strcpy(_str + _size, str);
_size += len;
}
只需要在_size的位置复用strcpy即可,不要忘记_size加上str本身的长度
+=运算符重载
+=有两个版本,一个是+=一个字符,一个是+=一个字符串
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
它们的功能和push_back,append一样,所以我们只需要复用即可
insert
void string::insert(size_t pos, char ch)
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
}
这里的插入逻辑和顺序表一样,只需要把pos位置空出来即可,把pos之后的数据挪动到后面,插入ch即可
要注意的是'\0'也要挪动
void string::insert(size_t pos, const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (int i = 0; i < len; i++)
_str[i + pos] = str[i];
_size += len;
}
这是插入一个字符串的版本
和上面一样,但需要计算好需要挪动的数据个数和位置,唯一的区别就是最后需要多使用一个for循环来插入数据
要注意的是'\0'也要挪动
erase
void string::erase(size_t pos, size_t len)
{
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos; i <= _size; i++)
_str[i] = _str[i + len];
_size -= len;
}
}
和顺序表思路一样,挪动数据覆盖掉要删除的节点即可,若需要删除长度超出了字符串的长度,则全部删除即可
find
作用是从pos位置开始在字符串中寻找ch字符
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;
}
若找到则返回该位置的下标,失败则返回npos
这里出现了一个npos,npos是什么?
npos
我们可以看到它的值是-1,但它的类型确是size_t无符号整型,并且由于它在二进制中的数字为 全1,所以它是一个无穷大
下面我们需要定义它
static const size_t npos;
把它定义为了静态成员变量,那么为了定义它我们需要在类外面定义
const size_t string::npos = -1;
substr
该函数的作用是截取字符串中的子串,从pos位置开始,len长度的字符串
string string::substr(size_t pos, size_t len)
{
if (len > _size - pos)
len = _size - pos;
string sub;
sub.reserve(len);
for (int i = 0; i < len; i++)
{
sub += _str[i + pos];
}
return sub;
}
这里定义了一个sub当作子串,并将pos位置开始后面的len个字符插入到sub中即可
比较运算符重载
bool string::operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool string::operator<=(const string& s)
{
return (*this < s || *this == s);
}
bool string::operator>(const string& s)
{
return !(*this <= s);
}
bool string::operator>=(const string& s)
{
return !(*this < s);
}
bool string::operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s)
{
return !(*this == s);
}
在比较运算符重载这个家族中,我们只需要实现其中的两个函数就能够复用这两个函数将其他函数全部实现
上面的示例就是手动实现了<和==,剩下的只需复用即可
复用看似是一种偷懒行为,实则不然
我们当我们的比较逻辑改变时(例如不再根据strcmp比较,而是根据字符串的长度比较)
若我们没有复用,那么我们需要将这全部函数都重新实现
但是我们如果复用了,我们只需要修改我们手动实现的那两个函数即可
<<运算符重载
ostream& operator<<(ostream& out, const string& s)
{
for (auto x : s)
{
out << x;
}
return out;
}
<<运算符重载我们是在类外面实现的,它在类内实现不出库中的效果
如果我们在类中实现那么函数参数的第一个位置一定是this指针,那么我们在使用时就必须要把string变量放在第一个才能使用
str << cout 这种写法不是很别扭吗?
所以我们可以把它放在类外面实现,函数中的参数的位置就可以由我们自己决定了
这里的两个&是必须要有的,否则会报错
<<是输出函数,所以我们要用的是iostream中的ostream类(out)
这里只需要把out当成cout使用即可
>>运算符重载
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 128;
char buff[N];
int i = 0;
char ch;
//in >> ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
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;
}
和上面的<<一样,都是要放在类外面实现
否则会出现 str >> cin这种尴尬的写法
这里的细节就相对多一些
还是要有两个&
>>是输入函数,所以我们要用的是iostream中的istream类(in)
首先要确保s里面没有字符,所以先使用clear清空字符串
然后定义了一个buff作为缓冲区,先暂时将输入的字符
这里不能使用in >> ch,因为这样若是我们的字符串当中要有空格的时候,in会提取不到ch中,所以我们要用in中的get方法,它和in的本质区别就是它可以吸收空格,只有回车的时候才会停止
当缓冲区满时我们就可以将缓冲区的内容全部插入到s中,并重新将buff缓冲区置空
最后buff缓冲区很有可能没满但是有剩余数据在buff缓冲区中,所以循环结束后我们还要将buff中的内容全部插入到s中
这样就完成了
完整代码
string.h
#pragma once
#include<iostream>
using namespace std;
namespace lyw
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
string(const char* str = "")
{
int len = strlen(str);
_str = new char[len+1];
strcpy(_str, str);
_size = len;
_capacity = _size;
}
//string(const string& s)
//{
// _str = new char[s._size];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
//}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//string& operator=(string s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[s._size];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// }
//
// return *this;
//}
string& operator=(string s)
{
if (this != &s)
swap(s);
return *this;
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
const char* c_str() const
{
return _str;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
char& operator[](size_t pos) const
{
return _str[pos];
}
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, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string substr(size_t pos = 0, size_t len = npos);
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
private:
char* _str = nullptr;
int _size = 0;
int _capacity = 0;
static const size_t npos;
};
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
}
string.cpp
#include"string.h"
namespace lyw
{
const size_t string::npos = -1;
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void string::push_back(char ch)
{
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void string::insert(size_t pos, char ch) // 注意'\0'也要挪动
{
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);
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)
{
int len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (int i = 0; i < len; i++)
_str[i + pos] = str[i];
_size += len;
}
void string::erase(size_t pos, size_t len)
{
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos; i <= _size; i++)
_str[i] = _str[i + len];
_size -= len;
}
}
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* str, size_t pos)
{
const char* tmp = strstr(_str + pos, str);
if (tmp == nullptr)
return npos;
else
return tmp - _str;
}
string string::substr(size_t pos, size_t len)
{
if (len > _size - pos)
len = _size - pos;
string sub;
sub.reserve(len);
for (int i = 0; i < len; i++)
{
sub += _str[i + pos];
}
return sub;
}
bool string::operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool string::operator<=(const string& s)
{
return (*this < s || *this == s);
}
bool string::operator>(const string& s)
{
return !(*this <= s);
}
bool string::operator>=(const string& s)
{
return !(*this < s);
}
bool string::operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s)
{
return !(*this == s);
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto x : s)
{
out << x;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 128 ;
char buff[N];
int i = 0;
char ch;
//in >> ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
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;
}
}
完