zoukankan      html  css  js  c++  java
  • 侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类

    积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步,驽马十驾功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇蟮之穴无可寄托者,用心躁也。是故无冥冥之志者,无昭昭之明;无昏昏之事者,无赫赫之功

    ——荀子《劝学篇》

    一、三大函数:拷贝构造、拷贝复制、析构

    (1)string类里面存在指针,属于类的两种类型之一;有指针的类必须要有析构函数;不带指针的类,其拷贝构造操作可以用编译器自带的功能但是类里面有指针,则必须自己重写

    int main()
    {
        string s1();
        string s2("hello");
        
        string s3(s1);//拷贝构造
        cout<<s3<<endl;
        s3=s2;//拷贝赋值
        cout<<s3<<endl;
    }

    (2)因为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;//指针
    }

    (3)字符串就是一个指针指向头,最后有一个结束符号(c&c++)。字符串的构造和析构函数为

    对于没有指针的类,不需要清理,因为定义的内容会随着函数结束而自动清理掉;但是含有指针的类中,过程中做了

    动态分配,占用了动态内存,不会随着函数结束而被清理,所以需要单独清理该内存,防止内存泄漏。

    inline 
    string::string(const char* cstr = 0)//构造函数
    {
        if(cstr)
        {
            m_data = new char[strlen(cstr)+1];
            strcpy (m_data, cstr);//如果构造函数已赋值,则先分配一个大小为所赋值字符串的长度的内存加1,最后的1用来存结束符号。然后将所赋值cstr拷贝到m_data中。
        }
        else
        {
            m_data = new char[1];
            *m_data = '';//如果构造函数未赋值,则自动分配一个大小为1的内存用来存结束符号
        }
    }
    
    inline
    string::~string()
    {
        delete[] m_data;//析构函数,清理,注意delete[]的写法
    }

    (4)新建字符串的几种形式。动态分配内存一定要及时delete

    //main.cpp
    {
        string s1();
        string s2("hello");
    
        string* p = new string("hello");
        delete p;//动态分配内存,新建指向”hello“的变量,用完以后一定要及时删除
    }

    (5)类里面如果有指针,则一定要有拷贝构造和拷贝赋值!

    下图中没有拷贝构造函数的b=a操作,会使得b直接指向与a同样的地址,而b原来指向的地址则成为野内容,内存泄漏,同时hello被两个指针指向也存在危险性。


    拷贝构造函数:

    inline
    string::string(const string& str)
    {
        m_data = new char[ strlen(str.m_data)+1];//深拷贝,创造一片空间容纳蓝本
        strcpy(m_data, str.m_data);//将内容拷贝进内存
    }
    
    {
        string s1("hello");
        string s2(s1);//拷贝构造
        s2=s1;//拷贝赋值
    }

    拷贝赋值函数:检测是否自我赋值 or 赋值

    {
        string s1("hello");
        s2=s1;//拷贝赋值
    }
    
    inline
    string& string::operator = (const string& str)//拷贝赋值
    {
        if(this == &str){ return *this;}//检测是不是自我赋值,这一步非常重要,如果不写,当使用者自我赋值时会出错,原因如下图
       
         //操作步骤
        delete[] m_data;//1.先清掉自己
        m_data = new char[ strlen(str.m_data)+1];//2.构造与左边值相同大小的空间
        strcpy(m_data, str.m_data);//3.把左边值复制进来
        return *this;
    }

    二、堆、栈与内存管理

    (1)所谓堆(stack)和栈(heap)

    1.1 定义

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

    heap:操作系统提供的一块global内存空间,程序可动态分配,从中获得若干区块。

    class Complex{....};
    
    {
        Complex c1(1,2);//存储在栈中,c1的存储会随着函数结束而消失.local object
        Complex *p = new Complex(3);//动态获得堆的内存,因此不会随着函数结束而消失,必须手动delete掉.static object
    
    }

    1.2 stack生命周期

    a, stack objects:离开作用域就会消失,又称为auto object(指其析构函数会自动调用)

    b.static local objects:生命在作用域结束之后仍然存在,直到程序结束。

    c.global objects: 全局对象,生命同样在整个程序结束之后消失。

    {
        Complex c1(1,2);//stack object
        static Complex c2(1,2);//static object
    }
    
    Complex m(1,2)//global object
    
    int main()
    {
        ...
        return 0;
    }

    1.3 heap objects生命周期

    class Complex {...};
    
    {
        Complex* p = new Complex;
        ....
        delete p;
    }
    //p所指的便是heap object, 其生命在它被delete之后结束
    
    //VS
    
    {
        Complex* p = new Complex;
        ....
    }
    //以上出现内存泄漏,因为当作用域结束,p所指的heap object仍然存在,但指针p的生命却结束了,作用域之外再也看不到p了,因此没办法再delete p了。

    没有delete相对应的指针所指的内容,那么就会占用内存,造成内存泄漏。而消除内存泄漏,只需delete p即可,而不用delete *p。

    1.3.1动态分配所得的array,到底有多大?--侯捷独家

    a.调试状态下:存储一个数据的内存大小是:数据本身大小+上下cookie+灰色填充(结果不是16的倍数时要向下分配内存直至满足16的倍数)

    b.非调试状态下:只存储上下cookie和数据本身大小。

    1.3.2 动态分配下的数组array存储

    array new & array delete要一起搭配。

    {
         Complex *p = new Complex[3];//指针指向复数数组头
         String *p = new String[3];//数组里面有三个指针
    }

    图片说明:对于内有3个元素的数组存储,调试模式下(左1)内存分配包括3个数组元素+上下Debugger Header+上下cookie+表数量(8*3+(32+4)+4*2+4),以及不满16倍内存,自动增加内存直至为16倍数;左二是非调试模式。

    1.3.3 delete[] 与 delete 的区别

    delete的动作分为两步,首先调用析构函数删除变量,然后删除其内存。所以如下调用delete的时候,系统会自动调用析构函数,然后根据上下cookie给出的内存大小进行整块内存删除。因此对于数组内存储的是非指针变量的array new,delete不加[],是不会造成内存泄漏的。但是!!!如果内部存储的是指针,那么没有加[],编译器会不知道需要需要调用多次析构函数,从而造成只删除第一个数组元素及其指向的数据,而不会删除后续的。内存泄漏的部分是剩余数组内指针元素指向的部分。

    .所以,为了保险起见,array new 一定要搭配 array delete[]/

    三、类模板、函数模板

    3.1  静态全局变量的作用域为单个源文件的区域;全局变量的作用域是整个工程文件的区域

    (1) 静态数据:带有this pointer;

               使用场景:对于不同类名的某一属性,只能有一份,如银行系统的利率,百人都是同一利率

    (2) 静态函数:没有this pointer;只能存取、处理静态数据

    (3) 静态的数据在类外一定要进行定义,赋不赋初值都是可以的。

    (4)调用static函数方式两种:

    a.通过类名调用;(在还没有对象被建立的时候)

    b.通过用户调用

    class Account
    {
    public:
        static double m_rate;
        static void set_rate(const double & x) { m_rate = x;}
    };
    double Account::m_rate = 8.0;//!!!静态数据在class外一定要进行定义
    
    int main()
    {
        Account::set_rate(5.0);//调用static函数方式两种:a.通过类名调用;
        Account a;
        a.set_rate(7.0);//b.通过用户调用
    }

    3.2 把ctors放在private区(Singleton,利用静态实现该种设计模式)

    初级版本一,缺陷:如果外界不需要A,那么已经被创建的a就会造成内存占用浪费

    class A
    {
    public:
        static A& getInstance {return a; };
        setup(){......}
    private:
        A();
        A(const A& rhs);
        static A a;//仅一份的创建
    ...
    };
    
    A::getInstance().setup();//通过这种方式来调用唯一的类的函数

    升级版本二

    class A
    {
    public:
        static A& getInstance {return a; };
        setup(){......}
    private:
        A();
        A(const A& rhs);
    ...
    };
    
    A& A::getInstance()//当没有人使用A时,她就不会被创建,一旦有人使用,就会被创建,并且不会随着函数消失而消失
    {
        static A a;
        return a;
    }
    
    
    A::getInstance().setup();//通过这种方式来调用唯一的类的函数

    3.3 补充类模板,class template

    template<typename T>//表明T为一个类型模板
    class complex
    {
    public:
        complex (T r=0, T i=0):re(r), im(i) {}
        complex& operator += {const complex& };
        T real() const { return re; }
        T imag() const { return im; }
    private:
        T re, im;
    
        friend complex& __doap1 (complex*, const complex& );
    };
    
    //用法
    {
        complex<double> c1(2.5, 1.5);
        complex<int> c2(2,6);
        ...
    }

    3.4 函数模板 function template

    template <class T>
    inline
    const T& min(const T& a, const T& b)
    {
        return b<a? b:a;
    }
    
    
    class stone
    {
    public:
        stone();
        bool operator < (const stone& rhs) const
        { return  _weight < rhs._weight; }
    
    private:
        int _w, _h, _weight;
    };
    
    //使用
    stone r1(2,3), r2(2,3), r3;
    r3 = min(r1, r2);

    3.5 补充 namespace

    namespace是把所有包在命名空间里,为了防止与别人定义的重名,则把你的内容包在namespace里。

    using namespace std;//一次把标准库全打开。一些常见小程序用这种
    {
    cout<<...
    }
    
    //为了防止标准库全打开,会出现混乱,那么单独打开需要的内容
    using std::cout;
    {
    cout<<..
    }
    
    //
    {
    std::cout<<...
    }
    
    
    

    3.6更多细节学习

    • Standard Library:需要好好利用
    • operator type() const;
    • explicit complex(...):initialization list {}
    • pointer-like object
    • function-like object
    • Namespace
    • template specialization
    • variadic template 
    • move ctor
    • Rvalue reference
    • auto
    • lambda
    • range-base for loop
    • unordered containers

    完!!!

    Higher you climb, more view you will see.
  • 相关阅读:
    C# 访问AD查询用户信息
    js UTC时间转本地时间
    Silverlight中的序列化和反序列化
    ASP.NET FORM认证配置排错记录
    opencv中cvSetData用法
    WS2812B-64位 8*8位 RGB LED点阵
    1602 LCDKeypad Shield
    Wemos D1 使用ESP8266 板载存储
    Wemos D1 ESP8266的网络工作模式
    Wemos D1 1602 液晶屏幕
  • 原文地址:https://www.cnblogs.com/yyfighting/p/12500646.html
Copyright © 2011-2022 走看看