C++String类的一种简单实现
String 实现
一、构造 - 默认构造与传参构造的结合体
1、函数声明
当我们声明了一个类,却不声明构造函数,编译器就会生成一个默认的构造函数,其对类中的成员进行默认值初始化,并且不接受任何参数。
我们的 String 类除了默认构造函数之外,肯定还需要一个传入字符串参数的构造函数。
综合以上两个需求,可以书写一个函数就完成两个函数的功能,也就是将该字符串参数定义为默认为空(也就是默认构造函数的功能)。
1 | String(const char* str = nullptr); |
这里为什么要使用 const char* str
呢?
这是因为,如果你使用了
char str*
,而不是const char* str
,则只能向这个构造函数传递非 const 的 char 参数;当你定义为了const char* str
之后,你既可以传递const char*
的字符串,也可以传递char*
的字符串。这是因为非 const 向 const 的转化是可以的,const 向非 const 的转化却是有风险的。
2、函数定义
这个构造函数处理传递进来的字符串参数,用它来初始化 String 类中的 m_data 成员变量。又由于我们的 m_data 是一个需要动态管理的内存成员,因此我们需要一些分配空间的操作。
另外,因为我们的参数字符串可能为空,我们还需要分情况处理。
1 | String::String(const char* str) |
当检测到传入参数字符串为空的时候,我们为 m_data 分配了一个字节的空间。为什么是一个字节呢?因为 \0
啊,因为即使为空,还是有一个字节的结束标志符的空间需要分配,这点非常非常重要!
当检测到传入参数字符串不为空,我们获取传入字符串的长度,按照此长度 + 1,进行内存空间的分配。为什么要 + 1 其实很简单,还是那个 \0
的原因。最后,将 str 拷贝到 m_data 中去。
二、析构 - 我挥一挥衣袖,真的不带走一片内存空间
析构函数的语义其实非常简单,就是为了清理好类对象的一些使用的资源。比如动态内存的分配、数据库连接之类的。
良好的类的设计,就是在它离开的时候,就像它未曾来过一样那么干净清爽。
1 | String::~String() |
这里使用 delete[] 还是 delete 都是可以的,就我个人来说,更加喜欢清晰的语义化,使用了 delete[] 来释放 m_data 的内存空间。
三、拷贝 - 构造、赋值,这个工作并不简单
拷贝操作,顾名思义,就是通过一个已经存在的类的对象,去构造或者赋值另一个对象。
在这个操作中,我们要考虑很多方面,比如说这两个对象是不是同一个对象(自赋值问题),原来的对象的痕迹有没有被清除干净(先析构)。
1、拷贝构造函数
拷贝构造函数,就是传入参数为该类 const 引用对象的构造函数:
(1)为什么要是 const 类型
因为只有 const 类型,才能接收 const 和非 const 对象参数。
(2)为什么要是引用类型
因为只有是引用类型,才能够规避递归使用拷贝构造的死循环问题。
(3)String(const String other)
这个函数传递参数就会发生另一个参数 other 传入,默认实参匹配调用拷贝构造,然后又调用这个函数,…,直到死循环的情况。使用引用直接传对象实体进来,就不会在实参匹配时调用拷贝构造函数了。
作为拷贝构造函数,只需要处理好本对象的动态内存空间的分配,以及另一个对象的数据的拷贝即可:
1 | String::String(const String& other) |
构造构造,必然要对本对象进行一些处理,这里就是将另一个对象的数据拿来初始化了本对象的数据。
2、拷贝赋值运算符
我们可能觉得,只需要将另一个对象赋值给本对象即可。还需要考虑其他什么吗?
(1)要不要析构本对象的数据?
是的,我们必须要析构我们本对象的数据,不然赋值过来,原来的动态内存空间就是野空间了,这就是内存泄露。还有就是赋值的字符串的长度可能与本字符串的长度不相等
。
(2)如果是本对象赋值给本对象呢?
我们要先进行本对象的甄别操作。否则的话,我们析构了本对象的数据,再拿另一个对象的数据进行拷贝时,会发现数据已经在刚才被析构了。
1 | String& String::operator=(const String& other) |
我们首先进行了自赋值的检查,this 是本对象的地址,因此比较的时候,使用的是 other 的地址进行比较。
然后,我们进行了本对象 m_data 的释放操作,这是为了避免内存泄露。
最后,我们分配 m_data 的空间,将 other 的数据拷贝到 m_data 中去,最后返回本对象即可(this 是本对象的地址,因此返回实体就是 *this)。
四、移动 - 构造、赋值,我不是归人,只是一个过客
移动,说白了就是一个对象直接接管一块临时对象的数据而已。
我们可以直接移动对象数据,而不需要进行(有时候多余的)拷贝操作。
从概念上也就决定了,移动操作的代码,必然要比拷贝操作的代码,少一些内存分配的工作,为什么呢?因为移动操作直接移动内存数据,根本不需要重新分配内存空间。
1、移动构造
根据 C++ Primer 第五版第 13 章作者的建议,我们在使用了移动构造传入对象之后,需要使得该对象不能再访问已经移动的内存区域,所以该对象的 m_data 应该置为空。
1 | String::String(String&& other) |
传入 String&& 代表着右值对象。我们直接接管了 other.m_data 数据,在移动过来了之后,我们将 other.m_data 置为了空,以免出现问题。
2、移动赋值
移动赋值,相比拷贝赋值来说,少了内存空间的分配操作,多了传入右值对象的 m_data 成员的置空考虑。
自赋值都是两者需要考虑的。
1 | String& String::operator=(String&& other) |
总的来说,移动操作的实现是要比拷贝操作的实现简单的,因为少了内存空间的分配工作。但是,我们需要处理好移动后传入对象对于该内存区域的访问情况,最好置为空,以免出现问题。
五、完整实现
1 | class String { |
- 本文标题:C++String类的一种简单实现
- 本文作者:beyondhxl
- 本文链接:https://www.beyondhxl.com/post/cfbac8cc.html
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!