以下是程序需要要的头文件。
#pragma once
#include
#include
#include
using namespace std;
#pragma once
是防止头文件被再次引用,是一个C语言的知识点,开始实现。
注意以下写的函数,如果没有特殊表明,都是成员函数的意思,成员函数里面是默认有this指针的
string类的是写在一个文件,测试函数写在另一个文件(main()函数)
首先我们得想想,string类的成员变量都有谁,string是一个字符数组。
_str
)_size
)_capacity
)。namespace kcc//kcc是博主的名字
{
public:
//成员函数…………
private:
char* _str;
size_t _size;
size_t _capacity;
}
string(const char* str = "")//动态内存--要解决深拷贝的问题
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);//这里就很好的复用了c库里面的库函数
}
要申请_capacity+1长的空间的原因是,我们要将\0也拷贝进来。
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
析构函数主要解决对象的清理工作,1.要完成对空间的释放 2.将维护空间的指针置为空指针,避免野指针的存在
3._size
与_capacity
也要变为0。
还记得我们之前说的,编译器默认生成的拷贝构造函数,只能完成简单的值拷贝,而像这种动态内存的,需要深拷贝,这次可不能靠编译器来帮我们了。
所以我们要实现的功能是,重新申请一块动态空间,再将其内容拷贝过来。
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
为了实现别人的东西给我们,我们先实现一个交换函数。(也是我们的string的成员函数)
void swap(string& s)
{
::swap(_str, s._str);//加了两个点是因为swap是用库里面的交换函数
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
库里面的交换函数是这样的,是一个模板函数。如果我们用库里面的,将会多次调用构造函数和拷贝构造函数,这样子效率其实比较低,所以我们可以自己实现一个对string的交换函数。
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(tmp);//在类中是有this指针的
}
string& operator= (const string& s)
{
//考虑自己给自己赋值的问题
if (this != &s)
{
delete[]_str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
如果不考虑自赋值,那么delete[]_str;
,这句代码就会让后面的程序崩掉了。
string& operator=(string s)
{
swap(s);
return *this;
}
虽然现代写法的拷贝构造觉得没有什么太大的优势,但是现代写法的赋值运算符重载,显得代码的妙处很高!很灵活。
但是现代写法没有考虑自赋值的问题,自赋值会使地址发生该改变。我的理解是:自赋值本来就是一个错误的写法,现代的编程素养不断提高,也不会在程序中写出自赋值的情况。当然了,如果你非要去实现自赋值,那还是用传统写法吧。或者把现代写法改一改。
其实面试时,面试官叫你实现一个string主要也是想看看,你会如何去实现这几个默认成员函数。
下标遍历法首先我们要实现一个成员函数size(),其可以返回成员变量_size。
size_t size() const
{
return _size;
}
温馨建议:如果一个成员函数不会改变对象内容,实现时加上const修饰,不然const对象是无法调用该成员函数的(权限放大)
关于权限的放大,缩小大家可以参考这篇文章:
(337条消息) 类与对象(下)_龟龟不断向前的博客-CSDN博客
我们想像内置类型一样,欢快的使用,我们还得实现一个运算符[]重载
char& operator[]( size_t pos) //返回引用才可修改
{
return _str[pos];//访问对应下表的内容
}
温馨提示:如果一个成员函数既可以修改对象的内容,又可以不修改对象的内容。即:可读可写,与仅可读,那么我们要实现两个成员函数
const char& operator[](size_t pos) const
{
return _str[pos];
}
这样子,const与非const对象均可调用。
下标遍历的代码实现:
int main()
{
kcc::string s = "hello world";
for(int i = 0;i< s.size();++i)
{
cout<<s[i]<<" "
}
cout<<endl;
return 0;
}
string的迭代器其实就是一个char*指针。
大家可能会说:不是说string迭代器是指针吗,那iterator是啥呀,我也不认识啊,其实iterator的原理很简单,就是使用了typedef
typedef char* iterator;
typedef const char* const_iterator
char* begin()
{
return _str;
}
返回首元素的地址,根据咱们的温馨提示,如果有可读可写和仅可读功能的,成员函数要实现两个。
const char* begin() const//const迭代器
{
return _str;
}
char* end()
{
return (_str + _size);
}
const char* end() const//const迭代器
{
return (_str + _size);
}
迭代器遍历法的代码实现:
int main()
{
kcc::string s1 = "hello world";
iterator it1 = s1.begin();
while(it1 != s1.end())
{
*it1+=1;
cout<<*it1<<" ";//可读可写
++it1;
}
const kcc::string s2 = "hello world";
iterator it2 = s2.begin();
while(it2 != s2.end())
{
cout<<*it2<<" ";//仅可读
++it2;
}
return 0;
}
代码实现:
int main()
{
string s = "hello world";
for(auto& e: s)
{
e+=1;
cout<<e<<" ";
}
cout<<endl;
return 0;
}
其实范围for的原理也很简单,就是将代码替换成了迭代器的代码,不信你可以将上面的begin()的成员函数写成Begin(),范围for就遍历不起来的。
由于增要考虑到增容的问题,为了增强代码的复用,所以咱们先要实现一个reserve(),它可以改变_capacity的值。
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_capacity = n;
_str = tmp;//让_str来维护这块空间
}
}
我们顺便也把resize给实现了把
void resize(size_t n,char c = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (_size + n > _capacity)
{
reserve(_capacity + n);
}
for (int i = _size; i < n; i++)
{
_str[i] = c;
}
_str[n] = '\0';
_size = n;
}
}
void push_back(char c)
{
//增容情况
//这里没有考虑如果_capacity是0话还是会增容失败
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = c;
_str[_size+1] = '\0';
_size++;
}
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
这句代码的意义是很重要的,因为我们实现的string的空串的_capacity
是0,如果_capacity
再怎么2倍增容,还是会增容失败。
void append(const char* str)
{
//这个增容情况就不像push_back,因为增容2倍可能也不够
//所以建议增容一个字符串的长度
size_t len = strlen(str);
if (_size == _capacity)
{
reserve(len + _capacity);
}
strcpy(_str + _size, str );//将str的内容拷贝进来
_size += len;
}
string& operator+=(const char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
很多同学可能会说,库里面为什么还要设计push_back和append啊,明明一个operator+=就够了,但是我们从实现的角度理解,
operator是复用了push_back和append。
string& insert(size_t pos,const char c)//在pos位置,插入字符
{
assert(pos <= _size);
if (_size == _capacity)//增容问题
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size + 1;//防止头插时,end越界了,因为是无符号数
//1.挪动数据
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
//2.插入字符
_str[pos] = c;
_size++;//不要忘记调正_size的值了
return *this;
}
特别注意,上面的end是无符号数,end如果减到-1了,其实是一个很大的数,并不是一个小于0的数。
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
int newcapacity = (_size + len >= _capacity) ? _capacity + len : _capacity;
reserve(newcapacity);
//2.挪动数据-这次我们使用指针
char* end = _str + _size;
while (end >= _str + pos)//pos的位置上的数据也要挪
{
*(end + len) = *end;
--end;
}
//3.填数据
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
strncpy(_str + pos, str, len);
这句代码我们得讨论以下,可千万不敢用strcpy,因为strcpy会把\0也拷贝过来,与我们的需要不一致,所以我们要设置一个拷贝的最大长度,避免\0被拷贝过来了。
namespace kcc//kcc是博主的名字
{
public:
//成员函数…………
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
}
const size_t npos = -1;
string& erase(size_t pos = 0,size_t n = npos)
{
//将数据向前挪动即可
assert(pos <= _size);
if (n < _size)
{
char* start = _str + pos + n;
while (start <= _str + _size)
{
*(start - n) = *start;
++start;
}
_size -= n;
}
else
{
_str[0] = '\0';
_size = 0;
}
return *this;
}
删除数据并不是说是真的将数据删除,我们上面的代码,仅仅只是挪动后面的数据,将前面的数据覆盖了。
温馨提示:insert和erase都需要挪动数据,极端情况下的时间复杂度是O[N^2],所以尽量少用insert和erase
找到了返回下标,找不到返回npos
size_t find(const char ch, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t find(const char* str,size_t pos = 0)const
{
assert(pos < _size);
char* ret = strstr(_str, str);//在_str中找str,返回第一个找到的下标
if (ret)
{
return (ret - _str);
}
else
{
return npos;
}
}
想必大家在c语言时期已经写过很多遍了吧。这个逆置是使用迭代器逆置的
什么是迭代器逆置,c语言时期我只学过用指针逆置啊,或者下标逆置啊。
不要慌张,上面不是讲了string的迭代器本质不就是指针嘛)
string& reverse(iterator left,iterator right)
{
while (left < right)
{
char tmp = *left;
*left = *right;
*right = tmp;
++left;
--right;
}
return *this;
}
size_t rfind(const char ch, size_t pos = npos)const
{
char* end = nullptr;
if (pos < _size)
{
end = _str + pos;
}
else
{
end = _str + _size;
}
while (end >= _str)
{
if (*end == ch)
{
return end - _str;
}
--end;
}
return npos;
}
size_t rfind(const char* ch, size_t pos = npos)const//ch是c串的首元素地址
{
size_t len = strlen(ch);
char* end = nullptr;
if (pos > _size)
{
end = _str + _size;
}
else
{
end = _str + pos;
}
while (end >= _str)
{
int ret = strncmp(end, ch, len);
if (ret == 0)//找到了就会返回0
{
return (end - _str);
}
--end;
}
return npos;
}
不过这样子实现rfind的复用性很差,大家可以思考一下,如果用find和reverse来实现rfind。
欢迎在评论区留下你的代码。
改其实咱们遍历的使用就讲了,三种遍历里面可以修改对象的数据。
为了可以更好地体现c++的字符串和c串的兼容性,我们可以写一个c_str()函数,库里面也写了
const char* c_str(const string& s)
{
return s._str;
}
####> , < ,>= ,<= ,== ,!=(可以不设置为成员函数)
bool operator>(const string& s1, const string& s2)//我们可以直接用库里面的来实现
{
return strcmp(s1.c_str(),s2.c_str()) > 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator>=(const string& s1, const string& s2)
{
return (s1 > s2) || (s1 == s2);
}
bool operator<(const string& s1, const string& s2)
{
return !(s1 >= s2);
}
bool operator<=(const string& s1, const string& s2)
{
return !(s1 > s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
如果以上函数在未写声明的时候定义,编译器找对应的函数只会从下向上找,所以先后顺序要清楚。
以前说过,operator<<,operator>>为了占位是要定义在类的外面的,getline也定义在类外面
关于运算符重载的占位可以看看博主的另一篇文章:
(337条消息) 类与对象(下)_龟龟不断向前的博客-CSDN博客
由于我们已经可以通过成员函数得到串的内容,所以不再需要将这些函数定义成类的友元了
ostream& operator<<(ostream& out, string& s)//注意字符串输出可不是遇到\0结束,而是输出所有内容
{
for (int i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
看到什么差别了吗,有些同学总是会说,C语言的字符串跟C++的字符串有什么区别啊,不都是字符数组吗,但是你输出一个C的串,它遇到\0就会结束不再输出,而C++的string则是输出至_size结束。
图解:
大家思考一下,C语言的输出上述串,输出的结果是hello
而c++输出的是helloworld
如果思考一下:如果对一个已初始化的对象进行输出,那么他原先的内容将被覆盖,所以咱们还需要一个成员函数clear()来清空其内容
void clear()
{
_size = 0;
}
不要不可思议这段代码,还是那个思想,删除并不一定就是删除!
istream& operator>>(istream& in, string& s)//这个是遇到空格或者回车就会结束了
{
s.clear();//我们得先将s的内容清空
char ch;
ch = getchar();
while ((ch != ' ') && (ch != '\n'))//用in会自动省略\n,所以咱们用scanf或者getchar
{
s += ch;//用+=就不用考虑增容了
ch = getchar();
}
return in;
}
istream& getline(istream& in, string& s)
{
s.clear();//我们得先将s的内容清空
char ch;
ch = getchar();
while (ch != '\n')//用in会自动省略\n,所以咱们用scanf或者getchar
{
s += ch;//用+=就不用考虑增容了
ch = getchar();
}
return in;
}
其实这个模拟实现string的程序写的时候真的很痛苦,写代码一时爽,测试起来真的要了我的老命,各种BUG层出不穷。
说来这些BUG也很哭笑不得。
BUG之大,一锅炖不下。
但是编程不就是这样的吗,程序员的大部分时间都在调式代码,微软搞了这么多年的操作系统不也还是会蓝屏
还记得我的一个导师说过:编程带给你的快乐往往是让你经理了多重痛苦,给你一份敞开心扉的爽快,在我实操完了string
的模拟实现,测试完了一般程序,真的是很欣慰开心,当然了上面的代码还有很多的不足之处,还有很多可以改良的地方。
欢迎大家在评论区留言指出,多谢了。
string的模拟实现的的代码也已经上传到了gitee上面:
5.2模拟实现string/5.2模拟实现string · small_sheep/cplusplus0study - 码云 - 开源中国 (gitee.com)
如果这篇文章有帮到你,请点歌免费的赞再走吧,这是对我最大的鼓励!