美文网首页架构算法设计模式和编程理论程序员
C++如何设计一个类2(含指针的类)

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

作者: alex_zhou | 来源:发表于2017-01-16 19:49 被阅读0次

    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()

    • 不包含指针
    new的内部实现

    new内部分解为三个步骤:

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

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

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

    delete的内部实现

    delete内部实现分为两步:

    • 调用析构函数
    • operator delete释放内存
    带有指针的delete

    物理内存块模型 in VC

    • 动态内存分配的对象
    实际内存分配

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

    • 动态内存分配的array
    动态内存分配的array

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

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

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

    相关文章

      网友评论

        本文标题:C++如何设计一个类2(含指针的类)

        本文链接:https://www.haomeiwen.com/subject/ckvebttx.html