• 第12章 类和动态内存分配


    <c++ primer plus>第六版

    12 类和动态内存分配

    12.1 动态内存和类

    12.1.1 示例和静态类成员

    //以下两行代码等价
    //  都是使用一个对象来初始化新对象,
    //  调用的构造函数为: StringBad(const StringBad &);
    StringBad sailor = sports;
    StringBad sailor = StringBad(sports);
    

    12.1.2 特殊成员函数

    有些成员函数是自动定义的, c++自动提供的成员函数有:

    1. 默认构造函数(如果没有定义).
    2. 默认析构函数(如果没有定义).
    3. 复制构造函数(如果没有定义).
    4. 赋值运算符(如果没有定义).
    5. 地址运算符(如果没有定义).

    如果类的构造函数中用到静态成员或使用动态内存分配, 则隐式的复制构造函数 和 隐式的赋值运算符 会引起一系列问题.

    1. 默认构造函数:

    1.1 定义一个类Klunk, 且没提供任何构造函数, 则编译器将提供如下默认构造函数

    Klunk::Klunk() {} //默认构造函数, 不接收任何参数, 也不执行任何操作.
    Klunk lunk; //该语句会调用默认构造函数.
    ```cpp
    
    1.2 如果定义了构造函数, 则编译器将不会定义默认构造函数, 如果需要不带参数的构造函数, 需要自己定义.
    ```cpp
    Klunk::Klunk() //定义不带参数的构造函数
    {
        klunk_ct = 0;
    }
    

    1.3 带参数的构造函数也可以是默认构造函数, 只要所有参数都有默认值.

        Klunk (int n=0)
        {
            klunk_ct = n;
        }
    

    但是只能有一个默认构造函数, 如下两个默认构造函数有二义性, 当用户使用Klunk bus语句时, 将匹配两个构造函数, 会报错.

    Klunk () { klunk_ct = 0; }
    Klunk (int n=0) { klunk_ct = n; }
    
    1. 复制构造函数: 将一个对象复制到新创建的对象中. 它用于初始化过程中, 而不是常规赋值过程中.

    2.1 复制构造函数原型:

    ClassName(const ClassName &) //接受一个指向对象的常量引用作为参数.
    

    2.2 何时调用复制构造函数

    新建一个对象, 并将其初始化为同类现有对象时, 将调用复制构造函数.
    假设motto是StringBad的对象, 则下面4语句要调用复制构造函数
    
        StringBad ditto(motto);
        StringBad metoo = motto;
        StringBad also  = StringBad(motto);
        StringBad * pStringBad = new StringBad(motto);
    
    每当程序生成了对象副本时, 编译器都将使用复制构造函数: 函数按值传递对象, 函数返回对象.
    

    2.4 默认复制构造函数的功能:
    默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制), 复制的成员的值. 静态函数不受影响, 因为它们不属于各个对象.
    比如:
    cpp StringBad sailor = sports,
    等价于:
    cpp StringBad sailor; sailor.str = sports.str; sailor.len = sports.len;

    12.1.4. 赋值运算符

    c++允许类对象赋值, 这是通过自动为类重载赋值运算符实现的.
    ClassName & ClassName::operator=(const ClassName &);
    它接受一个指向类对象的引用, 并返回一个指向类对象的引用.

    1. 赋值运算符的功能 以及 何时使用它

    将已有的对象赋值给另一个对象时, 将使用重载的赋值运算符.

    StringBad headline1("Celery Stalks at Midnight");
    StringBad knot;
    knot = headline1; //将调用赋值运算符
    

    注意: 初始化对象时, 并不一定会使用赋值运算符:
    StringBad metoo = knot; //将调用复制构造函数(实现时可能分两步: 1. 使用复制构造函数创建一个临时对象, 然后调用赋值运算符将临时对象复制到新对象).

    所以: 初始化总是会调用复制构造函数, 而使用=运算符时也可能调用赋值运算符.

    与复制构造函数相似, 赋值运算符的隐式实现也对成员进行逐个复制. 如果成员本身就是类对象, 则程序将使用为这个类定义的赋值运算符来复制该成员. 静态数据不受影响.

    12.2 改进后的新String类

    12.3 在构造函数中使用new时应注意的事项

    使用new初始化对象的指针时要特别小心:

    1. 如果在构造函数中使用new来初始化指针成员, 则应在析构函数中使用delete;
    2. new和delete必须相互兼容: new对应delete, new[]对应delete[];
    3. 如果有多个构造函数, 则必须以相同的方式使用new(要么都带中括号, 要么都不带). 因为只有一个析构函数.
    4. 应该定义一个复制构造函数, 通过深度复制将一个对象初始化为另一个对象.
      复制构造函数应该分配足够的空间来存储复制的数据, 并复制数据, 而不仅仅是数据的地址.
    5. 应该定义一个赋值运算符, 通过深度复制将一个对象复制给另一个对象.

    12.4 有关返回对象的说明

    当成员函数或独立函数返回对象时, 有几种返回方式:

    1. 返回指向对象的引用;
    2. 返回指向对象的const引用;
    3. 返回对象;
    4. 返回const对象;

    12.4.1 返回指向const对象的引用

    返回const引用的主要目的是提高效率.

    Vector force1(50, 60);
    Vector force2(10, 70);
    Vector max;
    max = Max(force1, force2);
    

    其中Max函数的以下两种实现方法都可行:

    //version 1, 返回对象
    Vector Max(const Vector &v1, const Vector &v2)
    {
        if (v1.magval()>v2.magval())
            return v1;
        else
            return v2;
    }
    
    //version 2, 返回引用
    const Vector & Max(const Vector &v1, const Vector &v2) //第一个const表示返回值是const
    {
        if (v1.magval()>v2.magval())
            return v1;
        else
            return v2;
    }
    

    注意:

    1. 返回对象将调用复制构造函数, 而返回引用则不会. 所以version 2所做的工作更少, 效率更高.
    2. 引用指向的对象应该在调用函数执行时存在.
    3. 函数参数v1和v2都被声名为const引用, 而函数返回v1或v2, 所以返回类型也必须为const, 这样才匹配.

    12.4.2 返回指向非const对象的引用

    有两种常见的情形要返回非const对象(前者旨在提高效率, 后者必须这样做):

    1. 重载赋值运算符;
    2. 重载与cout一起使用的<<运算符;

    operator=()的返回值用于连续赋值:

    String s1("Good Stuff");
    String s2, s3;
    s3 = s2 = s1;
    

    这里s2.operator=()的返回值被赋值给s3, 返回对象或返回引用都可行, 但返回引用可避免调用String的复制构造函数来创建一个新的String对象.

    operator<<()的返回值用于串接输出:

    String s1("Good Stuff");
    cout << s1 << " is coming!";
    

    这里operator<<(cout, s1)的返回值成为一个用于显示字符串" is coming!"的对象.
    返回类型必须是ostream &, 而不能是ostream. 如果返回ostream, 将会调用ostream类的复制构造函数, 但ostream没有公有的复制构造函数.

    12.4.3 返回对象

    注意: 如果被返回的对象是被调用函数中的局部变量, 则不能按引用的方式返回它. 因为函数执行完后局部变量将调用其析构函数, 引用指向的对象将不再存在.
    即: 返回局部变量时, 应该返回对象, 而不是返回引用.

    通常, 被重载的算术运算符属于这一类.

    例如:

    Vector force1(50, 60);
    Vector force2(10, 70);
    Vector net;
    net = force1 + force2;
    

    返回的不是force1也不是force2, 因此返回值不能是调用函数时已经存在的对象的引用, 而是新的临时对象.

    Vector Vector::operator+(const Vector &b) const //最后一个const表示是const函数, 不能修改类成员
    {
        return Vector(x+b.x, y+b.y);
    }
    

    这时, 存在调用复制构造函数(用来创建被返回的对象)的开销, 然而这是无法避免的.

    12.4.4 返回const对象

    以如下3个语句为例:

    net = force1 + force2; //1, 将两个对象相加, 赋值给第三个对象.
    force1 + force2 = net; //2, 将第三个对象赋值给两个对象相加.
    cout << (force1 + force2 = net).magval() << endl; //3, 在2的基础上再调用对象方法.
    

    其中第2/3条语句比较奇怪, 提三个问题:

    1. 为何编写这样的语句?
      没有要编写这种语句的理由, 但并非所有代码都是合理的.

    2. 这些语句为何可行?
      因为表达式force1+force2的结果为一个临时对象(复制构造函数将创建一个临时对象来表示返回值).
      在语句1中, 将该临时对象赋值给net.
      在语句2和3中, 将net赋值给该临时对象.

    3. 这些语句有何功能?
      使用完临时对象后, 将把它丢弃.
      比如语句2, 程序计算force1与force2之和, 将结果复制到临时变量中, 再后net的内容覆盖临时对象的内容, 然后将该临时对象丢弃, 原来的矢量全都保持不变.
      比如语句3, 程序显示临时对象的长度, 然后将其删除.

    如果担心force1 + force2 = net这种语句可能引发的误用和滥用(比如在条件判断语句中将force1+force2==net误写为force1+force2=net),
    有一种简单的解决方案: 将返回类型声明为const Vector, 则由于语句2和语句3都有对临时对象的赋值操作, 所以这两语句是非法的.

    总结:

    1. 如果方法或函数要返回局部对象, 则应该返回对象, 而不是指向对象的引用(因为局部对象在函数结束后就不存在了).
    2. 返回对象时, 将使用复制构造函数来生成返回的对象.
    3. 如果要返回一个没有公有复制构造函数的函数的类(如ostream类)的对象, 它必须返回指向这种对象的引用.
    4. 有些方法或函数(如重载的赋值运算符), 既可以返回对象也可以返回指向对象的引用, 这时应首选引用, 因为其效率更高.

    12.5 使用指向对象的指针

    如果: ClassName是类, value的类型为TypeName, 则如下语句:

    ClassName * pclass = new ClassName(value); //声明一个指向对象的指针, 将调用构造函数ClassName(TypeName);
    

    如下初始化方式:

    ClassName *ptr = new ClassName; //将调用默认构造函数
    

    12.5.1 再谈new和delete

    在构造函数中使用new为对象分配存储空间, 在析构函数中使用delete来释放这些内存.
    String * favorite = new String(sayings[choice]);

    注意: 这是为对象分配内存, 而不是为要存储的字符串分配内存. 也就是说分配的内存情况为:

    1. 保存字符串地址的str指针的内存,
    2. len成员的内存,
    3. 不给num_string成员分配内存, 因为它是静态成员, 它独立于对象被保存.

    创建对象时将调用构造函数, 在构造函数中才会分配用于保存字符串的内存, 并将字符串的地址赋值给str.
    当程序不再需要该对象时, 使用delete删除它.

    程序删除对象时, 将只释放用于保存str指针和len成员的空间, 并不释放str指向的内存.
    释放str指向的内存的任务由析构函数来完成.

    在下述情况下, 将调用析构函数:

    1. 如果对象是动态变量, 当执行完定义该对象的程序块时, 将调用该对象的析构函数.
    2. 如果对象是静态变量(外部, 静态, 静态外部, 来自名称空间), 则在结束时, 将调用该对象的析构函数.
    3. 如果对象是用new创建的, 则仅当显式地使用delete删除对象时, 才会调用该对象的析构函数.

    12.5.2 指针和对象小结

    使用对象指针时, 要注意几点:

    1. 使用常规表示法来声明指向对象的指针 : String * glamour;
    2. 将指针初始化为指向已有的对象 : String * first = &sayings[0];
    3. 使用new来初始化指针(将创建一个新对象) : String * favor = new String(sayings[choice]);
    4. 对类使用new, 将调用类构造函数初始化新建对象: String * gleep = new String; //调用默认构造函数
      String * glop = new String("My my my"); //调用相应参数类型的构造函数
    5. 通过 指针-> 运算符来访问类方法 : if (sayings[i].length() < shorted->length())
    6. 对对象指针使用解除引用运算符(*)来获得对象 : if (sayings[i] < *first) { first = &sayings[i]}

    12.5.3 再谈定位new运算符

    定位new运算符的作用: 在分配内存时能够指定内存位置.

    #include <iostream>
    #include <string>
    #include <sstream>
    #include <new>
    
    using namespace std;
    
    const int BUF = 512;
    
    class JustTesting
    {
    private:
        string words;
        int number;
    public:
        JustTesting(const string &s="Just Testing", int n=0)
        {
            words = s;
            number = n;
            cout << "construct : " << words << endl;
        }
    
        ~JustTesting()
        {
            cout << "destroy   : " << words << endl;
        }
    
        void Show() const
        {
            cout << words << ", " << number << endl;
        }
        
        string to_str()
        {
            string str;
            stringstream ss;
            ss << number;
            ss >> str;
    
            return words + ", " + str;
        }
    };
    
    int main()
    {
        char * buffer = new char[BUF]; // get a block of memory
    
        JustTesting *pc1, *pc2;
    
        pc1 = new (buffer) JustTesting;     // place object in buffer, 创建一个512字节的内存缓冲区
        pc2 = new JustTesting("Heap2", 20); // place object on heap
    
        cout << endl;
        cout << "Memory block addresses:" << endl;
        cout << "    buffer: " << (void *)buffer << endl;
        cout << "    heap  : " << pc2 << endl;
        cout << endl;
        cout << "Memory contents:" << endl;
        cout << "    " << pc1 << ": " << pc1->to_str() << endl;
        cout << "    " << pc2 << ": " << pc2->to_str() << endl;
        cout << endl;
    
        JustTesting *pc3, *pc4;
        pc3 = new (buffer) JustTesting("Bad Idea", 6); //会覆盖pc1对应的内存单元
        pc4 = new JustTesting("Heap4", 10);
    
        cout << "Memory contents:" << endl;
        cout << "    " << pc3 << ": " << pc3->to_str() << endl;
        cout << "    " << pc4 << ": " << pc4->to_str() << endl;
        cout << endl;
    
        delete pc2;       //会调用析构函数
        delete pc4;       //会调用析构函数
        delete [] buffer; //不会调用析构函数
        cout << "Done" << endl;
    
        return 0;
    }
    

    教训1:
    pc1和pc3对应的缓冲区内存单元相同, 会引发问题, 所以需要提供位于缓冲区的两个地址, 比如:
    pc1 = new (buffer) JustTesting;
    pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea", 6);

    教训2:
    如果使用定位new运算符来为对象分配内存, 必须确保其析构函数被调用.

    在堆中创建的对象可以使用delete pc2, 但缓冲区中的不能使用delete pc1.
    因为delete可以与常规new运算符配合使用, 但不能与定位new运算符配合使用.
    
    delete [] buffer释放了使用常规new运算符分配的整个内存块, 但它没有为定位new运算符在该内存块中创建的对象调用析构函数, 需要显式地调用:
    pc3->~JustTesting();
    pc1->~JustTesting();
    

    12.7 队列模拟

    队列是一种抽象的数据类型(Abstract Data Type, ADT), 可以存储有序的项目序列.
    队列: 在队尾添加项目, 在队首删除项目(FIFO).
    栈 : 在同一端进行添加和删除(LIFO).

    本节定义一个Queue类(第16单将介绍标准模板库类queue).

    12.7.1 队列类

    1. Queue类接口:
    class Queue
    {
        enum {Q_SIZE=10};
    private:
        //to be developed later
    public:
        Queue(int qs=Q_SIZE);   //构造函数, 指定队列长度, 创建一个空队列
        ~Queue();               //析构函数
        bool isempty() const;   //常函数, 队列是否为空
        bool isfull() const;    //常函数, 队列是否为满
        int queuecount() const; //?
        bool enqueue(const Item &item); //给队列添加项目, 可以使用typedef来定义Item(见第14章类模板)
        bool dequeue(Item &item);       //给队列删除项目
    }
    
    Queue line1;    //一个队列, 最多10个项目(默认值)
    Queue line1(20);//一个队列, 最多20个项目
    
    1. Queue类的实现
    1. 如何表示队列数据:
      一种方法是使用new动态分配一个数组, 但数组不适合队列操作.
      一种方法是使用链表, 每个节点都包含两个信息: 项目信息和下一节点的指针.
    struct Node
    {
        Item item;          //存储在node中的数据
        struct Node * next; //指向下一个Node的指针
    };
    
    1. 单向链表, 每个节点都只包含一个指向下一个节点的指针, 最后一个节点的指针设置为NULL/nullptr.
    2. 让Queue类的一个数据成员指向链表第一个元素, 用于跟踪链表.
    3. 让Queue类的一个数据成员指向链表最后一个元素, 方便将新项目添加到队尾.
    4. 让Queue类的数据成员来跟踪队列可存储的最大项目数以及的项目数.
    class Queue
    {
    private:
        struct Node //在class中嵌套struct声明, 使Node的作用域为整个class, 不与其它class或全局声明冲突.
        {
            Item item;
            struct Node * next;
        };
        enum {Q_SIZE = 10};
    
        Node * front;    //指向队列Queue的头
        Node * rear ;    //指向队列Queue的尾
        int items;       //队列Queue中当前项目数
        const int qsize; //队列Queue的最大项目数
        ...
    public:
        ...
    };
    
    1. 类方法
    Queue::Queue(int qs)     //构造函数, 队列开始是空的
    {
        front = rear = NULL; //队首队尾设置为NULL
        items = 0;           //项目数为0
        qsize = qs;          //最大长度从函数参数qs获取(这行代码有问题, 见后描述).
    }
    

    上述代码有个问题, qsize是常量, 只能对它初始化(在执行函数体之前, 即创建对象时进行初始化), 不能给它赋值.
    c++提供了特殊语法来应对const赋值的操作, 它叫做成员初始化列表(member initializer list).
    示例修改如下:

    Queue::Queue(int qs): qsize(qs) //构造函数, 带成员初始化列表
    {
        front = rear = NULL; //队首队尾设置为NULL
        items = 0;           //项目数为0
    }
    

    通常:
    1) 初始化对象: 可以是const成员, 也可以是非const成员,
    2) 初始化值 : 可以是参数列表中的参数, 也可以是常量(NULL/0等)
    3) 只有构造函数可以使用这种初始化列表语法.
    4) 对于const成员, 必须使用这种初始化列表语法;
    5) 对于被声明为引用的类成员, 也必须使用这种语法(因为引用也只能在创建时进行初始化);
    6) 数据成员被初始化的顺序必须与它们出现在类声明的中的顺序相同, 与初始化器中的排列顺序无关.

    //构造函数, 带成员初始化列表,
    //初始化对象: 可以是const成员, 也可以是非const成员,
    //初始化值  : 可以是参数列表中的参数, 也可以是常量(NULL/0等)
    Queue::Queue(int qs): qsize(qs), front(NULL), rear(NULL), items(0)
    {
    }
    
    //引用类型的成员必须在初始化列表中初始化
    class Agency{...};
    class Agent
    {
    private:
        Agency & belong; //一个引用, 必须在初始化列表中初始化.
    };
    
    Agent::Agent(Agency &a): belong(a){...} //在初始化列表中初始化.
    

    成员初始化列表的语法:
    Classy是一个类, mem1/mem2/mem3是这个类的成员,
    Classy::Classy(int m, int n): mem1(m), mem2(0), mem3(m*n+2)
    {
    ...
    }

    c++11可以在类内初始化, 但优先级比初始化列表低.
    class Classy
    {
    int mem1 = 10;
    const int mem2 = 20;
    };

  • 相关阅读:
    vuejs学习小结(数据处理)
    vuejs的遇到的问题小结
    ES6 对象扩展
    webpack的两个难点
    Sass入门:第二章
    Sass入门:第一章
    Effective JavaScript :第六章
    Effective JavaScript :第五章
    Effective JavaScript :第四章
    Effective JavaScript :第三章
  • 原文地址:https://www.cnblogs.com/gaiqingfeng/p/16463152.html
Copyright © 2020-2023  润新知