C++中string类的实现

今天来整理一下C++中一个重要的class,被侯捷老师称为含有指针的类(另一种是不含有指针的类),以字符串类作为示例。

三大函数:拷贝构造、析构函数、拷贝赋值#

这种类中最重要的三大函数包括了拷贝构造、析构函数、拷贝赋值运算符,在C++ primer中,也被称为三/五法则,除了这三大函数,C++11中还定义了移动语义,包括了移动构造函数。

对于string类型的对象,不能使用编译器默认的拷贝构造,因为这样就会造成行为不正确的浅拷贝,导致内存泄露。

下面是一个string类的经典定义:

1
2
3
4
5
6
7
8
9
10
11
12
class myString
{
public:
myString(const char* s = nullptr);
myString(const myString&);
myString&
operator=(const myString&);
~myString() { delete [] m_data; }
const char* get_data() { return m_data; }
private:
char* m_data;
};

Screen Shot 2022-01-13 at 11.51.37.png

其private的数据区域为一个char m_data,作为指针来指向之后会分配的区域,为何不是一个char array呢?因为数组大小不定,比较好的方法就是用一个指针,然后需要多大的空间,就分配多少空间,并用该指针指向该空间。

构造函数和析构函数#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
myString::myString(const char *s)
{
if (s != nullptr)
{
m_data = new char[strlen(s) + 1];
strcpy(m_data, s);
}
else
{
// 未设定初值
m_data = new char[1];
*m_data = '\0';
}
}

~myString() { delete [] m_data; }

Screen Shot 2022-01-13 at 13.25.48.png

默认的拷贝运算只有浅拷贝!会造成内存泄露。

自己定义的拷贝构造#

深拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 拷贝构造函数
myString::myString(const myString &rhs)
{
m_data = new char[strlen(rhs.m_data)+1];
strcpy(m_data, rhs.m_data);
}

// 拷贝赋值运算符
myString&
myString::operator=(const myString& rhs)
{
if (this == &rhs)
{ return *this; }
// 删除m_data
delete [] m_data;
m_data = new char[strlen(rhs.m_data) + 1];
strcpy(m_data, rhs.m_data);
return *this;
}

Screen Shot 2022-01-13 at 13.30.08.png

Screen Shot 2022-01-13 at 13.28.30.png

一定要检查是否有自我赋值#

如果不检查会出现的问题:

造成空悬指针!

1
2
3
// 一定要检查是否自赋值!
if (this == &str)
{ return *this; }

Screen Shot 2022-01-13 at 13.35.38.png

为何要返回string&#

有一个问题就是为何拷贝复制运算符要返回一个string的引用,而不是void呢?

原因在于对于下列情况:如果连续赋值,返回void就会出错:

1
myString a = b = c;

报错信息为:

1
2
candidate function not viable: cannot convert argument of incomplete type 'void' to 'const myString' for 1st argument
myString::operator=(const myString& rhs)

堆栈与内存管理#

一些例子:

c1 便是所謂 stack object,其生命在作用域 (scope) 結束之際結束。
這種作用域內的 object,又稱為 auto object,因為它會被「自動」清理。

c2 便是所謂 static object,其生命在作用域 (scope)
結束之後仍然存在,直到整個程序結束

c3 便是所謂 global object,其生命在整個程序結束之後
才結束。你也可以把它視為一種 static object,其作用域
是「整個程序」。

P 所指的便是 heap object,其生命
在它被 deleted 之際結束。

1
2
3
4
5
6
7
8
9
10
11

{
Complex c1(1,2)
static Complex c2(1,2)
}
Complex c3(1,2);
int main()
{
Complex *p = new Complex;
delete p;
}

new先分配 memory, 再調用 ctor#

调用new之后的三件事情:

1
2
3
void* mem = operator new( sizeof(Complex) ); //分配內存 
pc = static_cast<Complex*>(mem); // 类型转换
pc->Complex::Complex(1,2); // 最后才调用构造函数

Screen Shot 2022-01-13 at 17.25.44.png

delete:先調用 dtor, 再釋放 memory#

delete的动作刚好相反,首先调用析构函数,然后释放内存

Screen Shot 2022-01-13 at 17.28.14.png

分配获得的内存区块#

创建这complex和string两个类之后,编译器(VC)给两个对象分配内存如下:

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
class complex
{
public:
complex (double r = 0, double i = 0)
: re (r), im (i)
{ }
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex*,
const complex&);
};

class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};

内存分配有调试模式和非调试模式之分:

Screen Shot 2022-01-13 at 18.02.52.png

左边两个是类complex在调试模式和release模式下的编译器内存分配。在debug模式下,编译器给complex对象内存插入了头和尾(红色部分),4*8 + 4大小的信息部分(灰色部分),绿色部分是complex对象实际占用的空间,计算后只有52字节,但VC以16字节对齐,所以52最近的16倍数是64,还应该填补12字节的空缺(青色pad部分)。对于release部分的complex对象,只添加了信息头和伟部分。string类的分析基本一样。

对于数组而言:

类似的,编译器给对象增加了一些冗余信息部分,对于complex类对象,由于数组有三个对象,则存在8个double,然后编译器在3个complex对象前插入“3”用于标记对象个数(即最后加的4字节)。String类的分析方法也类似。

Screen Shot 2022-01-13 at 18.08.31.png

array new 和 array delete必须要搭配使用,不然会出错:

  • [ ] 整理为什么会发生内存泄露,以及内存泄露的位置

Screen Shot 2022-01-13 at 18.12.25.png

深入:移动语义#

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
class myString
{
public:
friend std::ostream& operator<< (std::ostream& o, const myString& s);
myString(const char* str = nullptr);
myString(const myString& other);
myString(myString&& other);
~myString();

// 移动构造函数和移动赋值运算符
myString& operator=(const myString& other);
myString& operator=(myString&& rhs) noexcept;
private:
char* m_data;
};

myString::myString(myString &&other) {
if (other.m_data != nullptr)
{
// 资源让度
m_data = other.m_data;
other.m_data = nullptr;
}
}

myString &myString::operator=(myString &&rhs) noexcept {
if (this != &rhs)
{
delete [] this->m_data;
m_data = rhs.m_data;
rhs.m_data = nullptr;
}
return *this;
}