zoukankan      html  css  js  c++  java
  • C++_类和动态内存分配6-复习各种技术及队列模拟

    知识点:

    队列:是一种抽象的数据类型(Abstract Data Type),可以存储有序的项目序列。

    新项目被添加在队尾,并可以删除队首的项目。队列有些像栈。栈是在同一端进行添加和删除。这使得栈是一种后进先出的结构,队列是先进先出的。

     

    问题:Heather银行要在Food Hea超市门口开设一个ATM机。Food Heap需要了解ATM对超市交通可能造成的影响。Heather银行希望对顾客排队等待的时间进行评估,编写一个程序模拟这种情况。

     

    设计:设计一个队列类,队列中的项目是顾客。设计一个表示顾客的类,编写一个程序来模拟顾客和队列之间的交互。

    ====================================================

    队列类

    队列的特征

    • 队列能够存储有序的项目序列;
    • 队列所能容纳的项目数有一定的限制;
    • 应当能够创建空队列;
    • 应当能够检查队列是否为空;
    • 应当能够检查队列是否是满的;
    • 应当能够在队尾添加项目;
    • 应当能够在队首删除项目;
    • 应当能够确定队列中的项目数;

     

    设计类时应当开发公有接口私有实现

     

    1、 Queue类的接口

    class Queue

    {

           enum {Q_SIZE = 10};

    private:

    // private representation to be developed later

    public:

           Queue(int qs = Q_SIZE); //create queue with a qs limit

           ~Queue();

           bool isempty() const;

           bool isfull() const;

           int queuecount() const;

           bool enqueuer(); //add item to end

           bool dequeuer()  //remove item from front

    };

    Queue line1;     //默认构造函数 10-item limit

    Queue line2(20);  //queue with 20-item limit 显示初始化

     

    2、 Queue类的实现

    如何表示队列数据?

    链表很好满足队列要求。链表由节点序列构成。每个节点都包含要保存到链表中的信息以及一个指向下一个节点的指针。

    struct Node

    {

           Item item;

           struct Node * next;

    }

    类的私有部分声明如下:

    class Queue

    {

    private:

           struct Node {Item item; struct Node * next;};

           enum {Q_SIZE = 10};

           Node * front;

           Node * rear;

           int items;    //current number

           const int qsize;  //maxium number

    public:

    }

    嵌套结构和类

    上述声明使用了一个特性,在类中嵌套结构声明或类声明;把Node声明放在Queue类中,可以使其作用域为整个类。也就是是说,Node是一种在类中声明的作用域为整个类的类型。可以用它来声明类成员,也可以将它作为类方法中的类型名称,但是注意这个Node只能在类中使用。而且不用担心Node声明和全局声明发生冲突。

           如果声明是私有部分,则只能在类中使用;如果声明是在公有部分进行的,则可以在类的外部通过作用域解析运算符使用被声明的类型。例如可以在外部使用Queue::Node类型的变量。

     

    空指针:有三种表示方式,NULL、0、nullptr(C++11);

    3、 类方法

    Queue::Queue(int qs)

    {

           front = rear = NULL;

           items =0;

           qsize = qs   //not acceptable;  qsize是const

    }

    类构造函数,初始化成员的值:

           通常调用构造函数时,对象在括号中的代码被执行之前被创建;调用构造函数将导致程序首先给这几个成员分配内存。然后,程序流程进入到括号中,使用常规的赋值方式将值存储到内存中。

           因此对于const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。 C++提供了一种特殊的语法:成员初始化列表。前面带冒号,然后是有逗号分隔的列表。例如:

    Queue::Queue(int qs) : qsize(qs)

    {

           front = rear =NULL;

           item = 0;

    }

    通常,初值可以是常量构造函数的参数列表中的参数。这种方法并不限于初始化常量。还可这样写:

    Queue::Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0)

    {

    }

    注意:只有构造函数才可使用这种语法;对于const类成员,必须使用这种语法。对于被声明为引用的成也必须使用这种语法。

    Class Agent

    {

    Private:

           Agency & belong;   //must use initializer list to initialize

    }

    Agent::Agent(Agency & a) : belong(a) {...}

     

    引用与const数据类型相似,只能在被创建时候进行初始化。而且对于本身就是类对象的数据成员来说,使用成员初始化列表效率更高

     

    //在队尾加入新的节点(入队)

    bool Queue::enqueue(const Item & item)

    {

           if (isfull())           //满了就不能插入

                  return false;

           Node * add = new Node;  //Create node

           add -> item = item;

           add -> next = NULL;

           items++    //Current number of items in queue

           if (front == NULL)      //如果这个队列就是空的,那么新插入的节点就是队首;

                  front = add;

           else

                  rear->next = add;  //把当前队尾节点的next指向新队尾节点

           rear =add;  //将rear指向新的节点add;

           return true;

    }

     

     

    // 删除队首项目节点(出队)

    bool Queue::dequeue(Item & item)

    {

           if(front == NULL)       //空了就不能取出

                  return false;

           item = front -> item;   //队首的item赋值给形参,相当于取队首item的动作;

           items--;                //项目数减少1个;

           Node * temp = front;    //用temp临时指针去指向旧队首;队首指针赋值给temp临时指针的方式

           front = front -> next;  //修改队首指针的指向,指向下一个item(新的队首);

           delete temp;            //删除旧队首;

           if (items == 0)         //如果队列空了,队尾指针被置为空指针;

                  rear = NULL;

           return true;

          

    }

     

    队列到期时,队列不为空:但是没有方法可以清除这些内存;这就需要析构函数来处理;

    虽然构造函数中没有用到new,就减少了给类带来的特殊要求;

     

    类需要一个显式的析构函数——该函数删除剩余的所有节点。

    Queue::~Queue()

    {

           Node * temp;

           while(front !=NULL)

           {

                  temp = front;

                  front =front->next;

                  delete temp;

           }

    }

     

    使用new的类通常需要包含显式复制构造函数和执行深度复制的赋值运算符

    默认的成员复制是否很是:不合适,复制新对象后,也会发现它们将修改共同的链表;浙江造成非常严重的后果。更糟的是如果执行插入的话,只有副本的尾指针得到更新。这会破坏链表。所以必须要克隆和复制队列。所以必须有复制构造函数和执行深度复制的赋值运算符,尽管目前并不需要它们,如果没有复制队列的话。

     

    还有一种小小的技巧可以避免这些额外的工作,并确保程序不会崩溃:就是定义伪私有方法(暂时不允许风险的操作,这些风险会被提示编译不通过,这就规避了犯错的可能):

    Class Queue

    {

    Private:

           Queue( ):qsize(0)  { }

           Queue & operator = (const Queue & q) {return * this;}

    }

    这样做有两个好处:一、避免了本来将自动生成的默认方法的定义。(不让用=这个操作,阻止了风险操作);二、因为这些方法是私有的,所以不能被广泛使用。即

    Queue snick(nip);  //not allowed

    tuck =nip;        //not allowed

    与其将面对无法预料的故障,不如得到一个易于跟踪的

     

           C++11还提供一种禁用方法的方式,delete关键字,在18章中会介绍。

     

    复制构造函数:

           用于对象按值传递时;

           创建临时对象时;

           创建一个对象,并初始化为已有对象;

    ====================================================

    Customer类

    设计一个客户类,确定:客户何时进入队列以及客户交易所需的时间;

    class Customer

    {

    private:

           long arrive    //arrive time for customer

           int processtime  //processing time for customer

     

    public:

           Customer() {arrive = processtime=0;}

           void set(long when);

           long when() const {return arrive};

           int ptime() const {return processtime};

    }

    ====================================================

    ATM模拟

    程序将使用循环,每次循环代表一分钟。每分钟的循环,程序将完成下列工作:

    1 判断是否来了新客人;如果来了,队列未满则将它添加到队列中,否则拒绝客户入队;

    2 wait_time是新客人所需的等待时间;

     

    模拟循环(小时为单位);

    在该循环内,模拟随机出现客人,但可以保证出现客人的频率是平均每6分钟来一个;

    bool newcustomer(double x)

    {

           Return (std::rand() * x / RAND_MAX <1);

    }

    ==================================================== 

    总结

           本章介绍了定义和使用类的许多重要方面。其中一些方面是非常微妙甚至是很难理解的。

           在类构造函数中,可以使用new为数据分配内存。然后将内存地址赋给类成员。这样,类便可以处理长度不同的字符串,而不用在类设计时提前固定数组的长度。

    在类构造函数中使用new,也可能在对象过期时引发问题。

    如果对象包含成员指针,同时它指向的内存是由new分配的。则释放用于保存对象的内存并不会自动释放对象成员指针所指向的内存。因此在构造函数中使用new来分配内存时,应在类析构函数中使用delete来释放分配的内存。这样,当对象过期时,将自动释放其指针成员指向的内存。

          

    如果对象包含指向new分配的内存的指针成员,则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象时,也会出现问题。(浅复制),在默认情况下,C++逐个对成员初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。如果原始对象的成员指向一个数据块,则副本成员将指向同一个数据块。当程序最终删除这两个对象时,类的析构函数将试图删除同一个内存数据块两次,这将出错。

    解决方法是:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符。在上述任何一种情况下,新的定义都将创建指向数据的副本,并使新的对象指向这些副本。这样,旧对象和新对象都将引用独立的,相同的数据,而不会重叠。由于同样的原因,必须定义赋值运算符。对于每一种情况,最终目的都是执行深度复制,也就是说,复制实际的数据,而不仅仅是复制指向数据的指针。

     

    对象的存储持续性为自动或外部时,在它不再存在时将自动调用其析构函数。如果使用new运算符为对象分配内存,并将其地址赋给一个指针,则当您将delete用于该指针时将自动为该对象调用析构函数。

    然而使用定位new运算符(而不是常规new运算符),为对象分配内存,比必须显式地为该对象调用析构函数。方法是使用指向该对象的指针调用析构函数的方法。

    C++允许在类中包含结构、类和枚举定义。这些嵌套类型的作用域为整个类,这意味着它们被局限于类中,不会与其他地方定义的同名结构、类和枚举发生冲突。

     

    C++为类构造函数提供了一种可用来初始化数据成员的特殊语法。这种语法包括冒号和有逗号分隔的初始化列表。被放在构造函数参数的右括号后,函数体的左括号前。每个初始化器都有被初始化的成员的名称和包含初始值的括号组成。从概念上讲,这些初始化操作是在对象创建时进行的,此时函数体中的语句还没有被执行。语法如下:

    Queue(int qs): qsize(qs), items(0), front(NULL), rear(NULL){ }

    如果数据成员是非静态const成员或引用,则必须采用这种格式,但可将C++11新增的类内初始化用于非静态const成员。

    C++允许类内初始化,即在类定义中进行初始化:

           这与使用成员初始化列表等价。然而,使用成员初始化列表的构造函数将覆盖相应的类内初始化。

    Class Queue

    {

    private:

           Node * front = NULL;

           enum {Q_SIZE = 10};

           node * rear = NULL;

           int items = 0;

           const int qsize =Q_SIZE;

    };

           与简单的C结构相比,需要注意的细节要多得多。作为回报,它们的功能也更强。

  • 相关阅读:
    Filter 和 interceptor 的区别
    JAVA基础知识|Optional
    CentOS 7安装MariaDB 10详解以及相关配置
    Linux系统zookeeper环境搭建(单机、伪分布式、分布式)
    Java设计模式——模板方法模式
    Java设计模式——装饰模式
    Java设计模式——观察者模式
    Java设计模式——代理模式
    Java设计模式——适配器模式
    Java设计模式——策略模式
  • 原文地址:https://www.cnblogs.com/grooovvve/p/10493523.html
Copyright © 2011-2022 走看看