C++如何设计一个类2(含指针的类)

C++如何设计一个类2(含指针的类)

本文预览:

  • BigThree:拷贝构造、拷贝复制、析构
  • Stack(栈) Heap(堆)及生命周期
  • new delete 操作符内部实现
  • 物理内存模型 in VC

BigThree:拷贝构造、拷贝复制、析构

带有指针的类必须要有拷贝构造拷贝复制

C++ STL中String的实现是典型的带指针类,成员只有一根char类型的指针,为什么不用char类型的数组,这个考量在于,我们不能确定用户创建的字符串到底有多少个字符,数组在分配内存空间的时候必须是确定的,多了造成浪费,少了空间不足,所以数组不适合这样的需求。使用指针的好处在于,我们是动态分配的内存空间,大小可控。

  • 接口设计
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;   
};
  • 构造和析构函数的实现

我们在每一个函数定义上面加了inline,这样是否合适,有人说复杂的函数加了inline多此一举,是的,因为是否是inline这是由编译器决定的,我们可以把所有的成员函数都加上inline,这样写是没有问题的,至于是不是,让编译器去决定

#include <cstring>      //使用C的函数

inline
String::String(const char* cstr)
{
   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;     //注意,析构函数delete动态分配的数组
}

  • 拷贝构造函数的实现

根据传入的字符串长度开辟相同大小的内存空间,然后执行拷贝,由于包含‘\0’结束符,所以长度需要加1

inline
String::String(const String& str)
{
   m_data = new char[ strlen(str.m_data) + 1 ];   //m_data虽然是private的,同类之间的对象互为friend
   strcpy(m_data, str.m_data);
}

  • 拷贝复制
  1. 从右值复到左值,清空左值之前的数据,然后开辟新的内存空间,执行拷贝
  2. 自检为什么是必须的,当是同一个对象的时候,而没有自检,那么会发生什么?
    两个指针指向同一块内存空间,这一块内存空间先delete掉了,再取的时候另一个就变成了野指针
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(m_data, str.m_data);
   return *this;
}

Stack(栈) Heap(堆)及生命周期

  • Stack

Stack 是存在于某作用域的一块内存空间。例如当你调用一个函数,函数本身就会形成一个stack,用来存放接收的参数以及返回地址。在函数本体内声明的任何变量,其所使用的内存块都取自上述 Stack

  • Heap

Heap *或者叫做默认System Heap, 是指由操作系统提供的一块global内存空间,程序可动态分配,从中获取若干区块(blocks) *

Stack objects的生命期

  • 分析一段代码
class Complex {...};
...
Complex a3(5,6);

int main()
{
    Complex a1(1,2);    //a1所占用的内存来之stack
    static Complex a2(1,3);
    Complex* p = new Complex(2,3); //Complex(2,3)是个临时对象,它所占用的内存是以new自Heap动态分配获得,并由p指向
}
  • Stack Objects的生命期

上述代码示例中,a1便是所谓的Stack Object, 其生命在作用域结束之际结束。这种作用域的Object,又被称为auto object, 因为他们会被自动清理。

  • Stack local Object的生命期

a2便是 static object,它的生命在作用域结束之后仍然存在,直到整个程序结束。

  • global objects的生命期

a3便是所谓的 global object,其生命在整个程序结束之后才结束,也可以把它理解为一种static,其作用域是整个程序

Heap Objects的生命期

  • 代码
class Complex {...};
...
{
    Complex* p = new Complex();
    
    delete p;
}

p所指的便是Heap Object,其生命期在被deleted之际结束。如果不写delete p,会出现内存泄漏,p所指向的Heap object仍然存在,但p的生命期结束了,作用域外再也看不到p了,也就没有机会delete p了。

new delete操作符内部实现

内存分配这块非常重要,其分析的内存模型在很多资料上都是找不到的,内存模型是基于VC的,其他编译器也应该大同小异吧。

new:先分配memory,再调用ctor()

  • 不包含指针

《C++如何设计一个类2(含指针的类)》 new的内部实现

new内部分解为三个步骤:

  1. 调用 operator new函数(内部malloc)分配内存
  2. 转型
  3. 调用构造函数,赋初始值
  • 包含指针

《C++如何设计一个类2(含指针的类)》 带有指针的new

三个步骤是相同的,在这个例子里,operator new分配了4个字节的空间给指针,然后转型,第三步调用构造的函数的时候,又动态分配了6个字节的空间给hello并把地址返回给指针ps

delete:先调用析构,再释放内存

《C++如何设计一个类2(含指针的类)》 delete的内部实现

delete内部实现分为两步:

  • 调用析构函数
  • operator delete释放内存

《C++如何设计一个类2(含指针的类)》 带有指针的delete

物理内存块模型 in VC

  • 动态内存分配的对象

《C++如何设计一个类2(含指针的类)》 实际内存分配

在实际的VC编译器中,一个Complex对象是8个字节,需要包含4*2个字节的cookie(delete回收的时候是根据cookie来进行回收的),一共是16字节,在调试模式下,需要额外的32+4个字节的信息。

  • 动态内存分配的array

《C++如何设计一个类2(含指针的类)》 动态内存分配的array

Complex数组连续分配三个对象空间,并且多了一个4字节存放数组的大小内存,在没有调试模式下,83 + 42 +4 = 36,序列化必须是16的倍数,所以,在实际的内存中是48字节。String的数组看起来会更小一点,但是还要在堆里面的内存。

  • 为什么array new 一定要搭配 array delete

《C++如何设计一个类2(含指针的类)》 new[] 搭配 delete[]

这个问题就在于delete[] 会多次调用析构函数,而不加[]只会调用一次析构函数,所以,在这个例子中,最后两个对象内部动态分配的内存是被泄漏了,这个内存模型分为两部分,对象数组部分和对象动态内存,他们都是在堆里的,那么我们调用delete p的时候到底泄漏了多少内存呢?答案是后两个对象的动态内存,对象数组本身的内存是delete根据cookie进行释放的。所以,如果我的类是没有指针的,那么我直接调用delete p是不会造成内存泄漏的。

    原文作者:alex_zhou
    原文地址: https://www.jianshu.com/p/a841fc19c4ed
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞