美文网首页C++
[C++] C++面向对象高级开发:String类

[C++] C++面向对象高级开发:String类

作者: 何幻 | 来源:发表于2018-07-01 15:33 被阅读44次

    Big Three:拷贝构造函数,拷贝赋值函数,析构函数

    int main()
    {
      String s1();
      String s2("hello");
    
      String s3(s1);  // 拷贝构造
      cout << s3 << endl;
    
      s3 = s2;        // 赋值构造
      cout << s3 << endl;
    }
    
    class String 
    {
    public:
      String (const char* cstr = 0);       // 拷贝构造函数
      String (const String& str);          // 拷贝构造函数
      String& operator = (const String&);  // 拷贝赋值函数
      ~String ();                          // 析构函数
    
      char* get_c_str () const { return m_data; }
    private:
      char* m_data;
    }
    

    注:
    如果不写Big Three,编译器会自动创建一个,
    在进行拷贝和赋值时,会将class中包含的数据进行拷贝和赋值,
    对于含有指针的class,这样做是不正确的。

    当被拷贝或赋值的对象释放时,
    这个拷贝出来的指针就会指向错误的内存地址。

    对析构函数也类似,编译器自动创建的析构函数只会释放对象中的数据,
    而不会释放指针m_data指向的数据。

    所以,对于包含指针的class,我们应该自己实现Big Three。

    构造函数和析构函数

    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;
    }
    
    {
      String s1();
      String s2("hello");
    
      String* p = new String("hello");
      delete p;
    }
    

    拷贝构造函数

    inline
    String::String (const String& str)
    {
      m_data = new char [strlen(str.m_data) + 1];
      strcpy(m_data, str.m_data);
    }
    

    注:
    同类object之前互为friend,因此,str.m_data可以直接取private数据。

    {
      String s1("hello");
      String s2(s1);  // 拷贝构造
    }
    

    拷贝赋值函数

    inline String& 
    String::operator = (const String& str)
    {
      if(this == &str){  // 检测自我赋值
        return *this;
      }
    
      delete[] m_data;
      m_data = new char [strlen(str) + 1];
      strcpy(m_data, str.m_data);
      return *this;
    }
    

    注:
    检测自我赋值,并不只是效率方面的考虑,还是为了保证程序的正确性,
    如果不判断自我赋值,会首先释放掉内存delete[] m_data,导致程序出错。

    {
      String s1("hello");
      String s2;
    
      s2 = s1;  // 拷贝赋值
    }
    

    << 运算符重载

    #include <iostream.h>
    
    ostream&
    operator << (ostream& os, const String& str)
    { return os << str.get_c_str(); }
    
    {
      String s1("hello");
      cout << s1;
    }
    

    栈(stack)和堆(heap)

    (1)栈
    Stack,是存在于某作用域(scope)的一块内存空间(memory space)。

    例如,当你调用函数,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。

    在函数体(function body)内声明的任何变量,
    其所使用的内存块都取自上述stack。

    (2)堆
    Heap,或称为system heap,是指由操作系统提供的一块global内存空间,
    程序可动态分配(dynamic allocated)从其中获得若干区块(blocks)。

    {
      complex c1(1, 2);            // stack
      complex* p = new complex(3); // heap
    }
    

    注:
    栈中的对象,在作用域结束后会自动被清理,
    即自动调用其析构函数,
    因此,栈中的对象又称为auto object(自动对象)。

    而static局部变量则不会,

    {
      static complex c2(1, 2);
    }
    

    static局部变量,其生命在作用域结束后仍然存在,
    直到整个程序结束后被清理。

    全局对象

    class complex { ... };
    ...
    complex c3(1, 2);  // 全局对象
    
    int main  ()
    {
      ...
    }
    

    全局对象也是在整个程序结束后才被销毁,
    可以把它视为一种static object,其作用域是整个程序。

    heap object的生命周期

    {
      complex* p = new complex;
      delete p;  // 释放到p指向的内存
    }
    
    {
      complex* p = new complex;
    }
    

    以上会出现内存泄漏(memory leak),
    因为当作用域结束,p所指向的heap object仍然存在,
    但是指针p的生命却结束了,作用域之外再也看不到p了,
    也就没机会delete p

    new原理

    new操作符的作用原理是,先分配内存,再调用构造函数。

    complex* pc = new complex(1, 2);
    

    编译器会转化为,

    complex* pc;
    void* mem = operator new (sizeof(copmlex));  // 分配内存
    pc = static_case<complex*>(mem);  // 类型转换
    pc->complex::complex(1, 2);  // 调用构造函数
    

    其中,pc->complex::complex(1, 2)
    会将pc作为隐含的this指针传入构造函数。

    operator new内部调用了C函数malloc(n)来分配内存。

    delete原理

    complex* pc = new complex(1, 2);
    delete pc;
    

    编译器转化为,

    complex::~complex(pc);  // 调用析构函数
    operator delete(pc);  // 释放内存
    

    其中,operator delete内部调用了C函数free(pc)来释放内存。

    VC中动态分配所得的内存块

    左边第一张图为complex对象,在VC debug模式下占用的内存。

    其中,浅绿色部分是complex中的数据所占内存(2个double,2×4 bytes),
    灰色部分,是VC在debug模式下增加的调试信息所占内存(上32 bytes下4 bytes),
    红色部分,成为cookie,VC用来标志内存块的开始结束(各4 bytes),
    深绿色部分,是由于VC要求内存块大小必须为16的倍数,所进行的补齐。

    此外,红色cookie的值,表示内存块大小,
    由于内存块大小必为16的倍数,所以最后一位肯定是0,
    例如,64 bytes是写成16进制是40h

    这时VC用这一位作为标志位1表示操作系统分配出来的内存,
    0表示操作系统已回收的内存,
    于是,cookie变成了41h

    左边第二张图为complex对象,在VC release模式下占用的内存,
    去掉了调试信息,
    由于加上cookie后,内存大小已经是16的倍数,因此不需要补齐。

    右边两张图为String对象分别在VC debug和release模式下的所占用的内存。

    动态分配数组

    如果动态分配了一个数组,例如,

    m_data = new char [strlen(cstr)+1];
    

    则除了为数组分配空间之外,还要增加一块内存区域,
    用来表示数组的长度,
    图中灰色区域上面的3(4 bytes),表示了后面分配的数组长度。

    array new 一定要搭配 array delete

    由于delete[] pdelete p,都是删除p指向的内存,
    所以,以21h开头和结尾的内存块,用这两种方式调用都会被删除。

    但是,delete[] p会根据数组的长度,自动调用3次析构函数,
    delete p只会调用1次析构函数,
    因此,String对象中m_data所指向的内存区域,使用delete p就没有被完全释放。

    注:
    如果对象中不包含指针,则delete[] pdelete p的作用就是相同的,
    并不会造成内存泄漏。


    参考

    C++面向对象高级开发 - 侯捷

    相关文章

      网友评论

        本文标题:[C++] C++面向对象高级开发:String类

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