目录
一、模拟实现中类的组织
二、默认成员函数
1.默认构造函数
2.拷贝构造函数
(1)传统写法——循规蹈矩
(2)现代写法——偷天换日
3.析构函数
4.赋值运算符重载
二、元素访问
三、容量操作
1.容量与有效数据
2.改变空间容量
(1)reserve函数
(2)resize函数
四、迭代器
五、数据尾插
1.尾插一个字符
2.尾插一个字符串
3.+=的重载
六、字符串的比较
七、在特定位置插入数据
1.pos位置插入一个字符
2.pos位置插入一个字符串
八、清除数据
1.erase函数
2.清除数据
九、其他成员函数
1.返回string对象转C语言字符串
2.查找字符和子串
十、非成员函数
1.<<流插入重载
2.>>流提取重载
十一、测试代码
一、模拟实现中类的组织
我们使用两个文件,一个是string_class.h,另一个是test.c,头文件用于定义string类,cpp文件编写测试代码。
string类的底层是顺序表,顺序表的内容可以看这里:
string_class.h
#pragma once
#include<iostream>
#include<assert.h>
namespace my_string//用一个命名空间包起来
{
class string//我们实现的string类
{
public:
//这里定义成员函数,包括下面第二至九部分
private:
char* _str;//存放字符的顺序表
size_t _size;
size_t _capacity;
const static size_t npos = -1;//用于缺省值,-1对size_t是无符号整形最大值
};
//这里定义非成员函数,包括下面的第十部分
}
test.c
#include"string_class"
void test()
{
//测试代码
}
int main()
{
test();
return 0;
}
二、默认成员函数
1.默认构造函数
用缺省参数输入参数就构造对应字符串,不输入参数就构造空字符串
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;//容量与元素量一致
_str = new char[_capacity + 1];//留个位置给\0
strcpy(_str, str);//复制过来
}
2.拷贝构造函数
(1)传统写法——循规蹈矩
开同样的空间,赋同样的值,拷数据
string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s.c_str());
}
(2)现代写法——偷天换日
这个写法我愿称之为挂路灯写法。(群资本家,时不时来群里看看剩余价值.jpg)
string(const string& s)
{
string temp(s._str);//先通过构造函数构造一个与str一样的temp对象
std::swap(_str, temp._str);//swap是std中提供的一个交换函数,可以交换任意类型的数据
std::swap(_size, temp._size);
std::swap(_capacity, temp._capacity);//然后将所有的内部数据全部交换,temp的内容就直接给了*this对象
//出了作用域temp被销毁,但是*this的换来的temp构造的内容被保存了下来
}
3.析构函数
释放内存,指针和变量置零
~string()
{
delete[] _str;
_str = nullptr;//指针必须置空,否则可能存在野指针
_size = 0;
_capacity = 0;
}
4.赋值运算符重载
string& operator=(const string& s)
{
if (&s != this)//避免给自己赋值(s = s),我等于我自己
{
char* p = new char[s._capacity + 1];//开新空间
strcpy(p, s._str);//拷数据
delete[] _str;//将原数据的空间释放掉
_str = p;//指向新空间
_size = s._size;
_capacity = s._capacity;//将变量拷过去
}
return *this;
}
二、元素访问
[]的重载分为可读可写和只读的两个,其实at的实现也很类似,不写实现了。
//不可修改,返回的const char和const修饰的this是不可修改的
const char operator[](size_t pos) const
{
assert(pos < _size);//加上越界检查
return _str[pos];
}
//可修改,不加const修饰就行了
char operator[](size_t pos)
{
assert(pos < _size);//加上越界检查
return _str[pos];
}
三、容量操作
1.容量与有效数据
size_t size() const;——————返回string有效数据的长度
size_t length() const;——————返回string有效数据的长度
size_t capacity() const;——————返回开辟空间的大小
bool empty() const;——————检测当前string是否是空字符串
每一个都给this加上const最好,保证不会修改数据
//返回有效数据个数
size_t size() const
{
return _size;
}
//返回容量大小
size_t capacity() const
{
return _capacity;
}
//判断是否为空
bool empty() const
{
if (_str[0] == '\0')
return true;
else
return false;
}
2.改变空间容量
(1)reserve函数
由于硬件设计的一些特征,我们在内存管理时用指针开空间时并不能缩容。所以在我们设计程序时,是要尽力避免缩容的。
在这个函数中,我们可以分为两种情况:
reserve的空间大于_capacity:扩容到新空间大小
reserve的空间小于等于_capacity:不进行操作
我们在学习内存管理时,知道申请空间的扩容分为两种:原地括容和异地扩容。如果我们允许两种情况同时存在,情况就太复杂了,所以只用异地扩容就好了。
//改变空间大小,但不能小于字符串的长度
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//申请异地的新空间
strcpy(tmp, _str);//拷数据
delete[] _str;//归还原空间
_str = tmp;
_capacity = n;//更新空间和容量
}
}
(2)resize函数
在这个函数中,我们也可以分为两种情况:
reserve的空间大于_capacity:扩容到新空间大小,再在后面的空位置补上对应字符
reserve的空间小于等于_capacity:将对应的下标位变为\0
在大于_capaciity时,先扩容,然后补全空位,扩大_size的大小,最后在后面加上\0;小于等于_capacity时,也分为两种情况,一种是小于_size,我们直接在_str[n]处(数组元素下标由0开始)加上\0,另一种是大于_size,小于_capacity,我们在_str[n]加上\0对整体数据也没有影响,就不再多加一个判断了。同样,函数也只用异地扩容,不缩容。
//改变空间大小,若新空间大于旧空间插入对应字符,小于则直接截断
void resize(size_t n, char ch = '\0')
{
if (n > _capacity)
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[n] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
四、迭代器
我这里只实现了正向迭代器,string的迭代器直接使用指针就可以了,反向是一样的底层组织,具体的迭代器实现还需要我们以后去学习。
//迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
五、数据尾插
1.尾插一个字符
和顺序表一样,先检查容量,不够就扩容,够就直接插数据
//插入字符
void push_back(char c)
{
if (_size == _capacity)//容量等于有效字符个数需要扩容
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
//用一个三目操作符,设置新空间大小,如果容量位0设置新容量为4,反之新容量在原基础上乘2
reserve(newcapacity);//开新的空间
}
_str[_size++] = c;//尾插数据同时加一
_str[_size] = '\0';//补上\0
}
2.尾插一个字符串
这个和顺序表的扩容就有一些区别了,因为我们不知道需要插入的字符串的长度,所以不能用原来的2倍或1.5倍扩容,而是直接扩容到能直接容纳新字符串。
//插入字符串
void append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)//不够长度
{
reserve(len + _size);//扩容
}
strcpy(_str + _size, str);//在尾部将数据拷贝过去
_size += len;//有效数据长度也需要加上
}
3.+=的重载
我之前说过,+=可以尾插字符和字符串,其实就是通过这上面两个函数实现的
//尾插字符
string& operator+=(char c)
{
push_back(c);
return *this;
}
//尾插字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
六、字符串的比较
底层使用C语言的strcmp就可以了,而且只需要实现小于和等于就可以了,其他的大于、大于等于、小于等于和不等于都可以通过逻辑操作符实现
bool operator<(const string& s)
{
if (strcmp(c_str(), s.c_str())>0)
return true;
else
return false;
}
bool operator==(const string& s)
{
if (strcmp(c_str(), s.c_str()) == 0)
return true;
else
return false;
}
bool operator<=(const string& s)
{
if (!(*this > s))
return true;
else
return false;
}
bool operator>(const string& s)
{
if (!(*this < s && *this != s))
return true;
else
return false;
}
bool operator>=(const string& s)
{
if (!(*this < s))
return true;
else
return false;
}
bool operator!=(const string& s)
{
if (!(*this == s))
return true;
else
return false;
}
七、在特定位置插入数据
1.pos位置插入一个字符
与之前尾插的扩容原则一致,不过需要将包括pos后的数据向后挪动一位然后插入数据,有效数据加一。
string& insert(size_t pos, char c)
{
assert(pos<=_size);//断言输入的pos不能越界
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size;
while (end > pos)//挪动数据
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
2.pos位置插入一个字符串
直接扩容到能直接容纳新字符串,同样需要一定数据不过这次是每一次移动字符串长度位,然后插入数据增加有效数据数。
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);//断言输入的pos不能越界
size_t len = strlen(str);
if (_size+len > _capacity)//计算剩余空间够不够用
{
reserve(_size + len);
}
_size += len;
size_t end = _size;
while (end > pos)//挪动数据
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);//用strncpy可以避免拷贝\0
return *this;
}
八、清除数据
1.erase函数
erase用于从对应位置清除后面定量长度的数据,同样需要移动数据。
有两种情况:
pos后有效数据的个数小于要删除的数据长度:直接将pos放上\0
pos后有效数据的个数大于等于要删除的数据长度:按字符串长度向前挪动数据,我们也可以使用strcpy,这样就不用关注后续字符串的长度了
//清除特定位置后面的有限个字符(包括pos)
string& erase(size_t pos = 0, size_t len = npos)//pos和len都用缺省值
{
assert(pos < _size);//断言输入的pos不能越界
if (len == npos || pos + len >= _size)
//不传len参数时len默认是npos,由于||中前面的表达式为真后面也就不计算的特点,可以单独放前面判断len==npos
{
_str[pos] = '\0';
_size = pos;//长度直接就是pos
}
else
{
strcpy(_str + pos, _str + pos + len);//直接拷到\0停止,不需要关心循环的次数
_size -= len;//减掉相应长度
}
return *this;
}
2.清除数据
这里的清除数据将string变为空字符串,但不会释放空间,在开始加个\0,再将size置零。
//清空但不释放空间
void clear()
{
_str[0] = '\0';
_size = 0;
}
九、其他成员函数
1.返回string对象转C语言字符串
//把一个string类型的变量返回它的字符串首元素地址
const char *c_str() const
{
return _str;
}
2.查找字符和子串
find函数的两个重载函数,一个通过一个一个比较找到字符,另一个通过strstr找到子串,它们都返回对应的下标位,找不到则返回npos
// 返回某个字符在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == c)
return i;//直接返回下标
}
return npos;//没找到,返回npos
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
char* pi = strstr(_str, s);
if (pi == nullptr)
return npos;//没找到,返回npos
else
return pi - _str;//找到了,通过指针减指针找下标
}
十、非成员函数
1.<<流插入重载
返回类型循环遍历即可,返回类型为ostream保证连续输出
std::ostream& operator<<(std::ostream& out, const my_string::string& s)//返回类型为cout的类型,可以实现连续赋值
{
for (int i = 0; i < s.size(); i++)
{
std::cout << s[i];
}
return out;
}
2.>>流提取重载
std::istream& operator>>(std::istream& in, my_string::string& s)//返回类型为cin的类型,可以实现连续输入
{
s.clear();//把原来的内容清除掉
char buffer[128];//定义一个数组,存储临时数据
size_t i = 0;
char ch = in.get();//cin也是一个类变量,内部有一个函数get专门从输入缓冲区拿取一个字符
while (ch != ' ' && ch != '\n')//读取到空字符就结束了
{
if (i == 127)//如果buffer满了,就尾插整个字符串
{
s += buffer;
i = 0;//更新i
}
buffer[i++] = ch;//临时储存字符
ch = in.get();//再次获取
}
if (i > 0)//将剩余没满的部分插入
{
buffer[i] = '\0';
s += buffer;
}
return in;
}
十一、测试代码
我写了一段代码,尽量包括了所有的接口,定义在test.c里
void test()
{
string s1 = "hello world";
std::cout << s1 << std::endl;
string s2;
s2 += 'b';
std::cout << s2 << std::endl;
s2 += "onjour";
std::cout << s2 << std::endl;
string s3;
std::cin >> s3;
std::cout << s3 << std::endl;
s1.erase(1);
std::cout << s1 << std::endl;
s1 = s2;
std::cout << s1 << std::endl;
printf("%d\n",s1.find('o'));
printf("%d\n", s1.find('o', 2));
s1.insert(1, "def");
std::cout << s1 << std::endl;
s1.insert(6, 'x');
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
std::cout << (s1 < s2) << std::endl;
std::cout << (s1 <= s2) << std::endl;
std::cout << (s1 > s2) << std::endl;
std::cout << (s1 >= s2) << std::endl;
std::cout << (s1 == s2) << std::endl;
std::cout << (s1 != s2) << std::endl;
s1.clear();
std::cout << s1 << std::endl;
string::iterator it1 = s2.begin();
while (it1 != s2.end())
{
std::cout << *it1;
it1++;
}
std::cout << std::endl;
string s4;
std::cin >> s4;
std::cout << s4;
}
到这里string的主要接口就都实现了。