C++String类的一种简单实现

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
2
3
4
5
6
7
8
9
10
11
12
13
14
String::String(const char* str)
{
if (str == nullptr) {
m_data = new char[1];
*m_data = '\0';
cout << "Default constructor" << endl;
}
else {
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
cout << "Pass argument constructor" << endl;
}
}

当检测到传入参数字符串为空的时候,我们为 m_data 分配了一个字节的空间。为什么是一个字节呢?因为 \0 啊,因为即使为空,还是有一个字节的结束标志符的空间需要分配,这点非常非常重要!

当检测到传入参数字符串不为空,我们获取传入字符串的长度,按照此长度 + 1,进行内存空间的分配。为什么要 + 1 其实很简单,还是那个 \0 的原因。最后,将 str 拷贝到 m_data 中去。

二、析构 - 我挥一挥衣袖,真的不带走一片内存空间

析构函数的语义其实非常简单,就是为了清理好类对象的一些使用的资源。比如动态内存的分配、数据库连接之类的。

良好的类的设计,就是在它离开的时候,就像它未曾来过一样那么干净清爽。

1
2
3
4
5
String::~String()
{
delete[] m_data;
cout << "Destructor" << endl;
}

这里使用 delete[] 还是 delete 都是可以的,就我个人来说,更加喜欢清晰的语义化,使用了 delete[] 来释放 m_data 的内存空间。

三、拷贝 - 构造、赋值,这个工作并不简单

拷贝操作,顾名思义,就是通过一个已经存在的类的对象,去构造或者赋值另一个对象。

在这个操作中,我们要考虑很多方面,比如说这两个对象是不是同一个对象(自赋值问题),原来的对象的痕迹有没有被清除干净(先析构)。

1、拷贝构造函数

拷贝构造函数,就是传入参数为该类 const 引用对象的构造函数:

(1)为什么要是 const 类型
因为只有 const 类型,才能接收 const 和非 const 对象参数。
(2)为什么要是引用类型
因为只有是引用类型,才能够规避递归使用拷贝构造的死循环问题。
(3)String(const String other)
这个函数传递参数就会发生另一个参数 other 传入,默认实参匹配调用拷贝构造,然后又调用这个函数,…,直到死循环的情况。使用引用直接传对象实体进来,就不会在实参匹配时调用拷贝构造函数了。

作为拷贝构造函数,只需要处理好本对象的动态内存空间的分配,以及另一个对象的数据的拷贝即可:

1
2
3
4
5
6
7
String::String(const String& other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
cout << "Copy constructor" << endl;
}

构造构造,必然要对本对象进行一些处理,这里就是将另一个对象的数据拿来初始化了本对象的数据。

2、拷贝赋值运算符

我们可能觉得,只需要将另一个对象赋值给本对象即可。还需要考虑其他什么吗?

(1)要不要析构本对象的数据?
是的,我们必须要析构我们本对象的数据,不然赋值过来,原来的动态内存空间就是野空间了,这就是内存泄露。还有就是赋值的字符串的长度可能与本字符串的长度不相等
(2)如果是本对象赋值给本对象呢?
我们要先进行本对象的甄别操作。否则的话,我们析构了本对象的数据,再拿另一个对象的数据进行拷贝时,会发现数据已经在刚才被析构了。

1
2
3
4
5
6
7
8
9
10
11
String& String::operator=(const String& other)
{
if (this != &other) {
if (!m_data) delete[] m_data;
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}
cout << "Copy assignment" << endl;
return *this;
}

我们首先进行了自赋值的检查,this 是本对象的地址,因此比较的时候,使用的是 other 的地址进行比较。

然后,我们进行了本对象 m_data 的释放操作,这是为了避免内存泄露。

最后,我们分配 m_data 的空间,将 other 的数据拷贝到 m_data 中去,最后返回本对象即可(this 是本对象的地址,因此返回实体就是 *this)。

四、移动 - 构造、赋值,我不是归人,只是一个过客

移动,说白了就是一个对象直接接管一块临时对象的数据而已。

我们可以直接移动对象数据,而不需要进行(有时候多余的)拷贝操作。

从概念上也就决定了,移动操作的代码,必然要比拷贝操作的代码,少一些内存分配的工作,为什么呢?因为移动操作直接移动内存数据,根本不需要重新分配内存空间。

1、移动构造

根据 C++ Primer 第五版第 13 章作者的建议,我们在使用了移动构造传入对象之后,需要使得该对象不能再访问已经移动的内存区域,所以该对象的 m_data 应该置为空。

1
2
3
4
5
6
String::String(String&& other)
{
m_data = other.m_data;
other.m_data = nullptr;
cout << "Move constructor" << endl;
}

传入 String&& 代表着右值对象。我们直接接管了 other.m_data 数据,在移动过来了之后,我们将 other.m_data 置为了空,以免出现问题。

2、移动赋值

移动赋值,相比拷贝赋值来说,少了内存空间的分配操作,多了传入右值对象的 m_data 成员的置空考虑。

自赋值都是两者需要考虑的。

1
2
3
4
5
6
7
8
9
10
String& String::operator=(String&& other)
{
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
other.m_data = nullptr;
}
cout << "Move assignment" << endl;
return *this;
}

总的来说,移动操作的实现是要比拷贝操作的实现简单的,因为少了内存空间的分配工作。但是,我们需要处理好移动后传入对象对于该内存区域的访问情况,最好置为空,以免出现问题。

五、完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class String {
public:
// 构造:默认(传参)、拷贝构造、移动构造
String(const char *str = nullptr);
String(const String &other);
String(String &&other);

// 析构
~String();

// 赋值:拷贝赋值、移动赋值
String &operator=(const String &other);
String &operator=(String &&other);

private:
char *m_data;
};

String::String(const char *str)
{
if (str == nullptr) {
m_data = new char[1];
*m_data = '\0';
cout << "Default constructor" << endl;
}
else {
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
cout << "Pass argument constructor" << endl;
}
}

String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
cout << "Copy constructor" << endl;
}


String::String(String &&other)
{
m_data = other.m_data;
other.m_data = nullptr;
cout << "Move constructor" << endl;
}

String::~String()
{
delete[] m_data;
cout << "Destructor" << endl;
}

String &String::operator=(const String &other)
{
if (this != &other) {
if (!m_data) delete[] m_data;
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}
cout << "Copy assignment" << endl;
return *this;
}

String &String::operator=(String &&other)
{
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
other.m_data = nullptr;
}
cout << "Move assignment" << endl;
return *this;
}

参考原文:
让我们一步一步实现一个完整的 String 类:构造、拷贝、赋值、移动和析构

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...