美文网首页
C++笔记二(Boolan网——面向对象编程上)

C++笔记二(Boolan网——面向对象编程上)

作者: 小小出大炮 | 来源:发表于2017-04-21 16:33 被阅读0次

    七 三大函数(Big Three)

    上文提到class有两种经典分类:
    1.class without pointer members(complex)
    2.class with pointer members(string)
    根据string代码解析:

    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;
    };
    

    m_data指向一串字符,动态分配内存空间来存放字符,而不是在类里面放数组,这样不易确定所要放数组的大小。
    三个特殊的函数(Big Three):拷贝构造函数、拷贝赋值函数、析构函数。

    7.1 析构函数

    构造函数和析构函数:

    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;
    }
    

    Complex类不需要清理,String是动态分配内存,析构函数用来清理这块内存,否则将会造成内存泄漏。

    7.2 拷贝构造

    带有指针的类必须要自己写拷贝构造函数和拷贝赋值函数,原因如下:
    如果使用编译器给的默认拷贝构造函数,只是将a的地址拷贝到b中去,并没有拷贝a的m_data指向的那个内容,使得a的m_data和b的m_data指向同一个地方,b的m_data原本指向的“World”就成了一个孤立的内容,这样会造成内存泄漏。而且因为a和b都指向一块内存,更改a就会影响b,也是一个隐患,这个被称为浅拷贝。

    所以要使用深拷贝的方法,即自己写一个拷贝构造函数。

    inline
    String::String(const String& str)
    {
        m_data = new char[ strlen(str.m_data + 1) ];
        strcpy(m_data,str.m_data);
    } //直接取另一个object的private data(兄弟之间互为friend)
    
    {
        String s1("Hello");
        String s2(s1);    
     // String s3 = s1; 
    }
    

    7.3 拷贝赋值

    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;
    }
    {
        String s1("hello");
        String s2(s1);
        String s2=s1;         //拷贝赋值使用
    }
    

    自我赋值检测是必要的,如果没有自我赋值检测,delete[] m_data这一步骤会把自己的内容删掉,产生不确定行为。

    八 堆,栈与内存管理

    8.1 output函数

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

    不能写成成员函数,只能写成全局函数,这样cout才会在左边,符合使用习惯。

    8.2 stack(栈)和heap(堆)

    (1)Stack:是存在于某作用域的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个stack用来放置他所接受的参数,以及返回地址。在函数本体(function body)内声明的任何变量,其所使用的内存块都取自上述stack。
    (2)Heap:或谓system heap,是指向操作系统提供的一块global内存空间,程序可动态分配(dynamic allocated)从某中获得若干块区(blocks)。

    class Complex{...};
    ...
    Complex c3(1,2);
    {
         Complex c1(1,2);
         static Complex c2(1,2);
         Complex* p = new Complex(3);
         delete p;
    }
    

    上述代码中:
    (1).c1所占用的空间来自stack,它叫做stack object,其生命在作用域(scope)结束之际结束,这种作用域内的object,又称为auto object,因为它会被自动清理;
    (2).c2就是所谓static object,其生命再作用域(scope)结束之后仍然存在,直到整个程序结束;
    (3).c3就是所谓global object,其生命在整个程序结束之后才结束。也可以把它视为一种static object,其作用于是整个程序。
    (4).Complex(3)是个临时对象,其所占用的空间乃是new以heap动态分配而得,并由p指向。p指向的是heap object,其生命在它被delete之际结束,如果没有delete,将会出现内存泄漏,因为当作用域结束,p所指的heap object仍然存在,但指针p的生命却结束了,作用域之外再也看不到p(也就没机会delete p)。

    8.3 new和delete

    1.Complex的new和delete编译分析:
    new:先分配memory,再调用ctor。new调用的C的malloc函数。


    1.png

    delete:先调用dtor,再释放memory。delete调用C的free函数。


    2.png
    2.String的new和delete编译分析:
    new:先分配memory,再调用ctor。
    3.png

    delete:先调用dtor,再释放memory。


    4.png
    动态分配所得内存块(in vc):
    5.png
    红色部分被称为cookie(小甜饼干)用来记所分配内存块的大小,大小是16bit的倍数,末位表示输入(0)和输出(1);灰色部分表示debug代码区,不需要调试可以不要;浅绿色表示对象所存在的内存区域;深绿色是为了使分配的内存为16的倍数所填补的,称为pad。

    动态分配所得的array:


    6.png

    array new一定要搭配array delete,否则也会造成内存泄漏:


    7.png

    九 String类完整代码

    
    #ifndef __MYSTRING__
    #define __MYSTRING__
    
    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;
    };
    
    #include <cstring>
    
    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;
    }
    
    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;
    }
    
    inline
    String::String(const String& str)
    {
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
    }
    
    #include <iostream>
    using namespace std;
    
    ostream& operator<<(ostream& os, const String& str)
    {
       os << str.get_c_str();
       return os;
    }
    
    #endif
    

    十 类模板、函数模板及其他补充

    10.1 static

    8.png

    1.非静态成员变量中,使用者如图创建了三个对象,在内存模型中,每个对象都有一份成员变量,通过this指针(每个对象的地址)来区别是哪个对象调用对应的成员变量。同样的一份成员函数处理多个对象的需求也是通过this指针实现的。
    2.在成员变量或者成员函数前面加上关键字static,那么就变成静态的成员变量和静态的成员函数,脱离了对象,只有一份。应用比如设计一种银行账户体系,用户的账号是每一个对象,银行的利率和对象无关,这时把利率设计成静态的成员变量。静态成员函数没有this pointer,不能像一般的成员函数一样去访问、处理对象里面的数据,静态函数只能处理静态的数据。
    静态的数据在class的外头必须要定义,初值可以给也可以不给,如下图黄色部分:


    9.png
    int main()
    {
        Account::set_rate(5.0);  //通过class name调用
        Account a;
        a.set_rate(7.0);         //通过object调用
    }
    

    调用static函数的方式有两种:
    (1)通过object调用;
    (2)通过class name调用。

    10.2 singleton补充

    class A
    {
        public:
            static A& getInstance();
            setup() {...}
        private:
            A();
            A(const A& rhs);
            static A a; 
            ... 
    };
    
    A& A::getInstance()
    {
        return a;
    }
    

    如上述代码所示,singleton通过static实现,外界不能创建A的对象,只能通过A::getInstance().setup这样的方式调用函数。当外界不需要调用这个函数,a仍然存在,造成浪费,更好的设计如下:

    class A
    {
        public:
            static A& getInstance();
            setup() {...}
        private:
            A();
            A(const A& rhs);
            ... 
    };
    
    A& A::getInstance()
    {
        static A a;   
        return a;
    }
    

    10.3 cout

    cout能接受不同类型的参数是因为大量作了<<的重载,cout是一种ostream。


    10.png

    10.4 类模板和函数模板

    类模板示例:


    11.png

    函数模板示例:


    12.png
    关键字class和typename是相通的。函数模板用的时候,编译器会对它进行实参推导,不用加尖括号。函数模板里面<的重载由设计stone类的人去设计,这是合理的,编译器是不知道如何比大小的。

    10.5 namespace

    所有的东西被包装一个命名空间里面,防止不同公司相同函数名调用的混乱。
    有三种打开这个命名空间的方式:


    13.png

    相关文章

      网友评论

          本文标题:C++笔记二(Boolan网——面向对象编程上)

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