C++面向对象高级开发-String字符串类的设计与实现及堆栈与内存管理
Notes
三大函数:
- 拷贝构造函数
- 拷贝赋值函数
- 析构函数
若编写带有指针成员的类,拷贝构造函数,拷贝赋值函数要重写,不可使用编译器自动生成的拷贝构造和拷贝赋值。
拷贝构造函数与拷贝赋值函数
如上图所示,上半部分为深拷贝,下半部分为浅拷贝。若使用编译器自动生成的拷贝构造和拷贝赋值,因为a和b本身都是指针的缘故,如当执行拷贝赋值b=a;
时,既让指针b指向a,造成b原来所指的空间无指针指向,造成内存泄漏。即为浅拷贝。而深拷贝则是通过编写拷贝赋值函数或拷贝构造函数执行操作。
在拷贝赋值函数中,例A拷贝到B,通常的做法是:
- 清空B的内容
- 创建一个比A大1长度的新空间
- 把新空间拷贝到B
参考拷贝赋值函数代码:
inline String& String::operator = (const String& str) {
if (this == &str) {
return *this;
} //检测自我赋值
delete[] m_data;
mdata = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}
检测自我赋值段是必须写的,首先,如果是自我赋值,那么,不用做任何操作,直接返回this,大幅提升效率。其次,若不增加自我赋值检测,那么,因为this与str都为指针,若this与str指向同一块存放字符串的内存区域,如下图中上面所示。那么当执行到下面的delete行时,会将指针所指的区域释放,导致两个指针指向的内存区域均为无效区域,则此时下面无论执行什么,返回的结果必为错误结果。
堆和栈
栈(Stack)存在于某作用域的一块内存空间。如当调用函数时,函数本身会在内存中形成stack用来放置接收参数,返回地址以及Local object。 堆(Heap)为操作系统提供的全局内存空间,程序可动态分配获取若干块。
void foo() {
Complex c1(1,2);
}
上面的代码中的c1即为栈对象,在作用域内的对象stack object又被称作auto object,在作用域结束时自动调用析构函数清理。
void foo() {
static Complex c2(1,2);
}
c2是static object,在作用域结束后仍然存在,直到整个程序结束。
class Complex {...};
Complex c3(1,2);
int main() {...}
c3即为全局对象,作用域为整个程序,生命周期为程序结束后结束。
class Complex {...};
...
void foo() {
Complex* p = new Complex;
delete p;
}
p所指即为堆对象,生命周期在调用delete时结束。若delete p;
不执行,即出现内存泄漏。当作用域结束,p所指的堆对象仍然存在,指针p生命结束,原p所指的堆对象无指针指向,无法进行delete操作,即产生内存泄漏。
在使用new关键字创建新的堆对象时,语句Complex* pc = new Complex(1,2);
被编译器转换为图中的三个步骤:
- 首先分配内存调用C++函数
operator new
,内部调用malloc分配内存,得到sizeof(Complex)
的大小8,保存在void*
型的指针中,得到了一个指向堆对象新申请的空间的指针。 - 使用static_cast对mem进行转型,由
void*
型的mem得到Complex*
型的内存空间,并赋值给pc。 - 通过得到的指针调用Complex类的构造函数。构造函数实为
Complex::Complex(pc,1,2);
,其中调用该函数的为pc,即pc为this。
delete pc;
语句转换为图中的两个步骤:
- 首先调用析构函数,由于Complex占用为栈空间,故不需要释放
m_real
和m_imag
,在生命周期结束时自动销毁。 - 然后调用C++函数
operator delete
内部调用free释放pc的空间
与Complex类原理相同,语句String* ps = new String("Hello");
被编译器转换为图中的三个步骤:
- 首先分配内存调用C++函数
operator new
,内部调用malloc分配内存,得到sizeof(String)
的大小,保存在void*
型的指针中,得到了一个指向堆对象新申请的空间的指针。 - 使用static_cast对mem进行转型,由
void*
型的mem得到String*
型的内存空间,并赋值给ps。 - 通过得到的指针调用Complex类的构造函数。构造函数实为
String::String(ps,"Hello");
,其中调用该函数的为ps,即ps为this。
delete ps;
语句转换为图中的两个步骤:
- 首先调用析构函数,字符串class中的m_data进行
delete[]
操作,即指针ps所指向的字符串的存放“Hello”的内存区域。 - 然后调用C++函数
operator delete
内部调用free释放ps指针的空间。
动态分配内存块分析
图中分别为Debug模式下和Release模式下的对象在内存中的空间占用。每一条为4字节。
Complex在Debug模式下,除Complex对象(double m_real, double m_imag)占用8个字节外,上面有8个灰色的部分以及下面的一个灰色的部分为Debug Header,红色的为Debug Cookie,其作用为记录所占内存整块空间大小,用于系统空间回收。Complex类所占8
+Debug Header所占32+4=36
+Debug Cookie所占4*2=8
,加起来和为52。由于编译器分配内存块为16的倍数,故需填补绿色的pad至16的倍数最小的值64。已知所占空间为64,16进制状态下为0x40。借用其最后一位0/1表示是当前程序释放或获得空间的状态值。
Complex在Release模式下,无Debug Header,即只包含Complex类所占8个字节+Cookie所占4*2=8=16个字节,故不需添加pad填充。16的16进制为0x10,获取空间,最后一位比特位置1,即为0x11。右侧字符串类同理。
设计规范8:
在构造函数和析构函数中,array new要搭配array delete使用。如:
inline String::String(const char* cstr = 0) {
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data,cstr);
} else {
m_data = new char[1];
*m_data = '\0';
}
}
inline String::~String() {
delete[] m_data;
}
若不搭配使用array delete,则会造成内存泄漏。
使用数组分配时,Complex类在三个double数组的上方标注的数字3即为编译器记录数组大小的空间,记为Counter。计算原理同上。
执行delete[] p;
语句分两步操作,与上述相同。由于Cookie中记录了所占内存空间的大小,是否在语句中包含中括号不影响内存占用空间的析构。若语句中不含中括号,则语句只唤起一次析构函数,即数组中只有第一个元素被析构,其余元素指向的内存空间仍然不变,待第二步释放整个内存空间时,数组除第一个元素外的指针均被清空,此时指针指向的原内存空间无指针指向,即造成内存泄漏。
String函数完整源码
String-Test.cpp
//
// main.cpp
// String
//
// Created by Fa1c0n on 2020/2/25.
// Copyright © 2020 Fa1c0n. All rights reserved.
//
#include <iostream>
#include <cstdlib>
#include "String.h"
using namespace std;
int main(int argc, const char * argv[]) {
String s1("hello");
String s2("world");
String s3(s2);
cout << s3 << endl;
s3 = s1;
cout << s3 << endl;
cout << s2 << endl;
cout << s1 << endl;
return EXIT_SUCCESS;
}
String.hpp
//
// String.h
// String
//
// Created by Fa1c0n on 2020/2/25.
// Copyright © 2020 Fa1c0n. All rights reserved.
//
#ifndef __STRING_H__
#define __STRING_H__
#include <cstring>
using namespace std;
class String {
public:
String(const char* cstr = 0);
String(const String& str);
String& operator = (const String&);
~String();
inline char* get_c_str() const { return m_data; }
private:
char* m_data;
};
inline String::String(const String& str) {
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
inline String::String(const char* cstr = 0) {
if (cstr) {
m_data = new char[strlen(cstr) + 1];
strcpy(m_data, cstr);
} else {
m_data = new char[1];
*m_data = '\0';
}
}
inline String::~String() {
delete[] m_data;
}
inline String& String::operator = (const String& str) {
if (this == &str) {
return *this;
}
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1];
strcpy(this->m_data, str.m_data);
return *this;
}
inline ostream& operator << (ostream& os, const String& str) {
return os << str.get_c_str();
}
#endif /* String_h */