经过上一次的string类学习, 想必大家都对string容器有一定的了解了吧. 大家都有在用string吗? 个人觉得是非常好用的…无论是平时写代码还是刷题, 都是非常好用的容器~
当面试的时候有非常大的概率面试官就会让你当场实现一个string类, 那我们今天就来看看怎么简单实现一个有基本功能的string类~, 耐心看完, 你一定有所收获.
一. 深浅拷贝的问题
其实定义一个类, 无非就是写出他的几个成员函数, 比如构造函数, 析构函数.
拷贝构造和赋值运算符重载编译器会自动生成.
但是大家还记不记得前面说过的深浅拷贝的问题呢?
string类的拷贝构造和赋值运算符重载就涉及到深浅拷贝的问题,
接下来我会向大家说明深浅拷贝的问题
涉及深浅拷贝, 首先这个类中必须有资源.
那问题又来了, 什么是资源?
资源啊, 资源就是不属于类模型中的东西, 都叫资源
比如类A中有一个成员变量是指针char*, char指向了堆上的一片空间,
那么很显然, char 指向的空间并不属于A类, A类只占4个字节, 也就是一个指针的大小
那么当A类调用拷贝构造的时候, 我们知道编译器自动生成的是浅拷贝, 浅拷贝就是我啥也不干, 就把这个地址也给你的指针, 然后创建了一个对象
这时没有什么问题, 但是当程序结束时, 两个对象都要调用析构函数释放资源, 在这里也就是释放空间, 那么现在, 问题来了: 这两个对象指向的空间是一块空间, 释放第一次没问题, 但是第二个对象析构要释放空间的时候就会产生二次释放的问题
举个例子: 你写完了作业, 我想借鉴一下你的, 但是我没写, 我直接把你的作业名字一改, 老师要收的时候我先交了, 那你不就没作业交了? 那这出大问题啊~
这下大家都能明白了吧?
然后我再画个图就更清晰了:
OK, 理解了深浅拷贝的问题, 下面我们就进行string类的简单实现吧~
二. string类的简单实现
首先要说的是, string类的本质就是一个指针char* 的封装
1. 构造函数
构造函数分为两种情况:
1. 空的string类: 并不是不做任何操作, 而是要开一个空间放一个'\0'
2. 给一个字符串: 给指针开一个空间, 把字符串拷贝即可
下面给出代码 :
String()
:_ptr(new char[1])
{
*_ptr = '\0';
}
String(char* ptr)
:_ptr(new char[strlen(ptr) + 1])
{
strcpy(_ptr, ptr);
}
这里我们其实可以把他俩合并一下, 给一个缺省值为空 ""
代码如下:
//全缺省的构造函数
String(const char* ptr = "")
:_ptr(new char[strlen(ptr) + 1])
{
strcpy(_ptr, ptr);
}
2. 析构函数
上来我们先把两个最简单的写了, 析构函数不用多说, 为了释放资源, 资源就是char* 所指向的空间, 释放这个空间就可
为了代码的安全性, 要进行指针判空
//析构
~String() {
if (_ptr) {
delete[] _ptr;
_ptr = nullptr;
}
}
3. 拷贝构造
首先我们要知道, 拷贝构造是用一个已经存在的对象去创建另一个对象
这里我们要显式定义拷贝构造: 就要拷贝对象的内容+资源
即给新的对象开辟自己的空间, 然后吧内容拷贝进去~
注意: 拷贝构造参数一定要给引用类型, 不然会引发无穷递归, 详细见我之前的博客, 有兴趣的小伙伴可以去看看
C++六大成函数
代码如下:
String(const String& str)
:_ptr(new char[strlen(str._ptr) + 1])
{
//深拷贝: 开自己的空间, 拷贝str的内容
strcpy(_ptr, str._ptr);
}
4. 赋值运算符重载函数
赋值运算符重载函数: 用一个已经存在的对象的值给另外一个已经存在的对象赋值
实现深拷贝要把内容拷贝到自己的空间中
这时为了防止空间不够, 我们直接给他开辟新的合适的空间, 释放原有的空间
这里有一个小小的优化就是判断是否给自己赋值, 是的话直接跳过
代码如下:
String& operator=(const String& str) {
if (this != &str) {
//开空间
//因为原来内容不用了, 为了保证空间足够, 所以要新开空间
char* tmp = new char[strlen(str._ptr) + 1];
//拷贝资源
strcpy(tmp, str._ptr);
//释放原有空间
delete[] _ptr;
//指向新的空间
_ptr = tmp;
}
return *this;
}
到这里我们就已经实现了一个简单的string类了~
下面给出整体的代码:
class String {
public:
//全缺省的构造函数
String(const char* ptr = "")
:_ptr(new char[strlen(ptr) + 1])
{
strcpy(_ptr, ptr);
}
//系统默认的拷贝构造
//浅拷贝, 会造成析构时二次释放的问题
//String(const String& str)
// :_ptr(strcpy(_ptr, str._ptr))
//{}
//显式定义的拷贝构造, 完成深拷贝: 拷贝对象的内容 + 资源
String(const String& str)
:_ptr(new char[strlen(str._ptr) + 1])
{
//深拷贝: 开自己的空间, 拷贝str的内容
strcpy(_ptr, str._ptr);
}
//赋值运算符重载函数
String& operator=(const String& str) {
if (this != &str) {
//开空间 因为原来内容不用了, 为了保证空间足够, 所以要新开空间
char* tmp = new char[strlen(str._ptr) + 1];
//拷贝资源
strcpy(tmp, str._ptr);
//释放原有空间
delete[] _ptr;
//指向新的空间
_ptr = tmp;
}
return *this;
}
//析构
~String() {
if (_ptr) {
delete[] _ptr;
_ptr = nullptr;
}
}
private:
char* _ptr;
};
当你以为这篇到此结束了, 你就大错特错了…
接下来给大家来一钵秀的, 拷贝构造和赋值运算符重载的"现代"写法
5. 拷贝构造和赋值运算符的现代写法
不多说, 先直接上代码,
//拷贝构造 现代写法
String(const String& str)
:_ptr(nullptr)
{
String tmp(str._ptr);
swap(_ptr, tmp._ptr);
}
下面说明为什么这样子做可以
- 创建一个局部对象tmp: 调用构造函数 --> 开空间 + 内容拷贝
此时内容已经拷贝完成- 交换了一下指针, 把nullptr给了局部对象tmp, 而把我们需要的字符串交换了过来, 函数结束后局部对象带着交换给他的nullptr就被销毁了, 留下了我们需要的东西
这钵 ===> 杀人诛心
要注意的是: 一定要给_ptr初始化, 如果不初始化的话, _ptr是一个随机值, 交换给tmp之后, tmp调用析构释放空间时, 随机值的释放是错误的..程序会崩掉
这个, 说实话, 操作一般, 下面这个才是真的秀
来, 上菜
//赋值运算符重载 现代写法
String& operator=(String tmp) {
swap(_ptr, tmp._ptr);
return *this;
}
我们可以看到, 代码非常的简短, 但是细节很到位, 我们来品一下
- 首先是参数, 给的是值传递而不是引用传递, 这里就调用拷贝构造创建了临时对象tmp, 此时我们就已经完成了拷贝操作
- 然后直接一波交换, 把你的指针拿来, 此时我的指针就指向了我们需要的字符串, 赋值的任务完成. 临时变量tmp就带着原来对象的内容被销毁…
这钵直接窃取劳动果实...不可谓不狠啊....tmp老工具人了...呜呜心疼1秒钟
下面给出现代写法的完整代码:
//简单的实现.
class String {
public:
//全缺省的构造函数
String(const char* ptr = "")
:_ptr(new char[strlen(ptr) + 1])
{
strcpy(_ptr, ptr);
}
//拷贝构造 现代写法
String(const String& str)
:_ptr(nullptr)
{
String tmp(str._ptr);
swap(_ptr, tmp._ptr);
}
//赋值运算符重载 现代写法
String& operator=(String tmp) {
swap(_ptr, tmp._ptr);
return *this;
}
//析构
~String() {
if (_ptr) {
delete[] _ptr;
_ptr = nullptr;
}
}
private:
char* _ptr;
};
这就是string类的简单实现了…下一次我们实现完整的string类~