Featured image of post C++面向对象高级开发-String字符串类的设计与实现及堆栈与内存管理

C++面向对象高级开发-String字符串类的设计与实现及堆栈与内存管理

拷贝构造函数与拷贝赋值函数,堆和栈,动态分配内存块分析等

C++面向对象高级开发-String字符串类的设计与实现及堆栈与内存管理

Notes

三大函数:

  • 拷贝构造函数
  • 拷贝赋值函数
  • 析构函数

若编写带有指针成员的类,拷贝构造函数,拷贝赋值函数要重写,不可使用编译器自动生成的拷贝构造和拷贝赋值。

拷贝构造函数与拷贝赋值函数

带指针的类必须包含拷贝构造和拷贝赋值

如上图所示,上半部分为深拷贝,下半部分为浅拷贝。若使用编译器自动生成的拷贝构造和拷贝赋值,因为a和b本身都是指针的缘故,如当执行拷贝赋值b=a;时,既让指针b指向a,造成b原来所指的空间无指针指向,造成内存泄漏。即为浅拷贝。而深拷贝则是通过编写拷贝赋值函数或拷贝构造函数执行操作。 在拷贝赋值函数中,例A拷贝到B,通常的做法是:

  1. 清空B的内容
  2. 创建一个比A大1长度的新空间
  3. 把新空间拷贝到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_realm_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。计算原理同上。

数组new搭配数组delete

执行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 */