ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

第12章 类和动态内存分配

2022-07-10 14:34:17  阅读:174  来源: 互联网

标签:初始化 12 const 对象 运算符 动态内存 new 分配 构造函数


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

标签:初始化,12,const,对象,运算符,动态内存,new,分配,构造函数
来源: https://www.cnblogs.com/gaiqingfeng/p/16463152.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有