目录
[3.string::string(const char* s)的模拟实现](#3.string::string(const char* s)的模拟实现)
1.模拟实现string类的原因
在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。而且我们之后需要用到string类的地方还是比较多的,所以这里就相当于更加深入的了解它如何实现的了,总之是有益无害的!
2.string类模拟实现注意的点
在模拟实现时,我们需要学习一部分底层,只是把比较重要的函数实现了其他不重要的函数就不做过多讲解了,函数实现的时候可能顺序并不是按照C++官网的分类来的了。
由于string类可能会有命名冲突问题,我们需要把string类的模拟实现和测试函数都放到一个命名空间里面,而且我们可能不能把这个std的命名空间展开,之后我们需要比较二者的不同之处的。
cpp
//.h文件
namespace td
{
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
这是基本结构了(少了npos和之前额外补充的buff,之后我们都会围绕这个结构来进行讲解,当然这个是放到.h文件中的,因为之后我们实现的时候也需要放到两个不同的文件中,所以可能需要声明在.h文件中,而定义在.cpp文件中,但是有些比较短小的函数我们可以在.h文件中定义,使它成为内联函数。
3.string::string(const char* s)的模拟实现
3.1初始版本
我们用初始化列表来进行初始化,并且我们需要用strlen函数来计算它的字符个数。由于还要申请空间,所以我们还需要用new函数来申请内存空间,并把所有的数据拷贝到_str里面去,所以得到了这个:
cpp
//.h
#include<string.h>
namespace td
{
class string
{
public:
string(const char* str = " ");
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
cpp
//.cpp
#include"test.h"
#define _CRT_SECURE_NO_WARNINGS 1
namespace td
{
string::string(const char* str)
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
}
如果按照声明顺序进行初始化的情况下,那么这个代码是没有任何问题的,但是这样写起来很麻烦,因为总是用strlen(str)函数,我们知道这个函数的时间复杂度是O(n)的,很影响效率,所以我们要用_size或者_capacity进行初始化。
3.2进阶版本
在.h文件中_size要在第一个位置声明,其他的随意,则:
cpp
//.cpp
#include"test.h"
#define _CRT_SECURE_NO_WARNINGS 1
namespace td
{
string::string(const char* str)
:_str(new char[_size+1])
,_size(strlen(str))
,_capacity(_size)
{
strcpy(_str, str);
}
}
但是这样的方式会有问题:

所以我们如果这样写就会导致这个运行出错,至于原因可能是声明的顺序必须是开始的顺序那样,但是那样会导致在_str定义时,而_size未初始化,可能会导致用这个函数时崩溃(因为随机值的话如果空间没有开够,就会越界)。那么如何解决呢?
3.3最终版本
cpp
//.cpp
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
我们可以声明的时候和开始版本一样,但是我们不在初始化列表里面初始化,我们直接在函数体里面初始化,这样就不会报错了。之前我们是建议在初始化列表初始化,但是这只是一个建议,我们在函数体内初始化一些就不会有这么多事情了!
4.string::c_str()的模拟实现
这个比较简单,我们直接在那个.h文件中实现这个函数就可以了:
cpp
//.h
const char* c_str() const
{
return _str;
}
5.string::string()的模拟实现
5.1初始版本
cpp
//.cpp
string::string()
:_str(nullptr)
,_size(0)
,_capacity(0)
{
}
我们需要把之前的那个默认构造函数注释掉,因为不能有多个默认构造函数(在using namespace std的情况下),然后测试如下代码:
cpp
//.cpp
void test1()
{
string s2;
cout << s2.c_str() << endl;
}
这个不是放在成员函数里面的,而是只在命名空间td里面的,因为如果用在主函数里面那么就还要那个指定命名空间,所以这样更好一些,则运行结果为:

这样是因为它崩溃了。
为什么会出现这种情况?
cout打印char*的时候比较特殊,因为char*被认为是字符串,所以这样打印出来的结果不是指针类型,而是按字符串打印。但是这样打印就会直接对char*的指针进行解引用,直到遇到'\0',但我们在实例化对象时,这个是空指针(_str),空指针解引用会崩溃。
但是如果我们改为如下形式:
cpp
//.cpp
void test1()
{
std::string s2;
cout << s2.std::string::c_str() << endl;
}
那么运行结果如下:

我们发现如果用库里面的函数就不会崩溃,之前我们如果不指定类域默认用的是本类域里面的函数,所以会崩溃。因为库里面不是用nullptr去初始化_str,而是开了一个字符大小的空间(一个字节)。
5.2最终版本
cpp
//.cpp
string::string()
:_str(new char[1]{'\0'})
,_size(0)
,_capacity(0)
{
}
那么运行结果如下:

当然这是在已经写了析构函数的情况下运行的结果,之前都没有写析构函数,所以运行出来就会报错。
6.string::~string()的模拟实现
这个函数很简单,只要先释放内存后再把_str置为nullptr且_size,_capacity都置为0即可,代码如下:
cpp
//.cpp
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
用之前的测试代码,调试有:


运行结果是没有问题的。
但是之后我会把这个string()的构造函数直接删掉,因为之后用的也不多,就用const char*的即可。
7.总结
这里就先把那些比较重要的构造函数(除拷贝构造外)已经讲了,下一节将进行讲解:string类的增删查改的模拟实现,那些才是比较重要的部分,理解起来也比较难,所以下一节需要重点关注,喜欢的可以一键三连哦!
