[C++] Chapter 12: Classes and Dynamic Memory Allocation

Chapter 12 Classes and Dynamic Memory Allocation

12.1 Dynamic memory and classes

In general, it is best to determine issues such as how much memory is used when the program is running, rather than at compile time.

For example, write a character array to save the names of people, how to determine the length of the array? The usual C++ approach is to use the new operator in the class constructor to allocate the required memory at runtime. The usual way to do this is to use the string class, which will handle the memory management details for you.

C++ uses the new and delete operators to dynamically control memory, allowing the program to determine memory allocation at runtime rather than at compile time.

Part of the strategy C++ adopts when allocating memory: let the program decide memory allocation at runtime, not at compile time. This allows memory to be used according to the needs of the program, rather than according to a strict set of storage type rules. C++ uses new and delete operators to dynamically control memory. Unfortunately, using these operators in classes will cause many new programming problems. In this case, the destructor will be essential, not optional.

12.1.1 Review example and static class members

The characteristic of static members in a class is that no matter how many objects are created, the program only creates a copy of the static class variable. That is, all objects of the class share the same static member, just like the telephone at home can be used by the whole family. But note that static member variables cannot be initialized in a class declaration , because the class declaration describes how to allocate memory, but does not allocate memory. Static class members can be initialized using a separate statement outside the class declaration because static class members are stored separately, not as part of the object. Do not use static when initializing static members outside the class.

The initialization is done in the method file, not in the class declaration file, because the class declaration is in the header file, which the program may include in several other files. If initialization is done in a header file, there will be multiple copies of the initialization statement, raising an error.

An exception (see Chapter 10) is when the static data member is const integer or const enumeration.

#include<iostream>
using namespace std;

class stu{
	public:
		static  int  num;//注意这里不能赋值,但是加了const以后可以赋值
};

int stu::num = 10;//注意这里不能加static
int main(){
	cout<<stu::num<<endl;
	return 0;
}

This chapter is still not as good as Hou Jie's video, so I suggest reviewing the video at that time.

The initialization is done in the method file, not in the class declaration file, because the class declaration is in the header file, which the program may include in several other files. If initialization is done in a header file, there will be multiple copies of the initialization statement, raising an error.

new and delete, new[] and delete[] must appear in pairs to be standardized.

StringBad sailor = sports;//这种形式的初始化等效于下面的语句
StringBad sailor = StringBad(sports);//因为sports类型为StringBad,因此相应的构造函数原型应该如下:
StringBad(const StringBad & sports);

When you use one object to initialize another object, the compiler will automatically generate the above constructor (called copy constructor because it creates a copy of the object). The auto-generated constructor doesn't know that the static variable num_string needs to be updated, so it messes up the counting scheme. Virtually all of the problems this example illustrates are caused by member functions automatically generated by the compiler. From this point of view, sometimes member functions automatically generated by the compiler may actually cause some harm.

12.1.2 Special member functions

C++ automatically provides the following functions:

  • default constructor, if no constructor is defined;
  • default destructor, if not defined;
  • copy constructor, if not defined;
  • assignment operator, if not defined;
  • Address operator, if not defined.

More precisely, the compiler will generate definitions for the last three functions above if the way the object is used in the program requires it to do so.

C++11 provides two other special member functions: move constructor (moveconstructor) and move assignment operator (move assignment operator).

12.1.2.1 Default constructor

If no constructor is provided, C++ will create a default constructor. C++ will not define a default constructor if a constructor is defined. The default constructor makes the defined object behave like a regular automatic variable, that is, its value is unknown at initialization time.

A parameterized constructor can also be a default constructor, as long as all parameters have default values.

A(){a = 0};
A(int n = 0){a = 0};//这两个不能同时存在。

12.1.2.2 Copy constructor

** The copy constructor is used to copy an object into a newly created object. ** That is, it is used during initialization (including passing parameters by value), not during regular assignment. The copy constructor prototype of a class is usually as follows:

classname (const classname&);

For the copy constructor, you need to know two things: when to call it and what it does.

12.1.2.3 When is the copy constructor called?

The copy constructor is called both when creating a new object and when initializing it as an existing object of the same kind.

This can happen in many situations, the most common being when a new object is explicitly initialized to an existing object. For example, assuming motto is a StringBad object, the following four declarations will all call the copy constructor:

StringBad ditto(motto);
StringBad metto = motto;
StringBad also = StringBad(motto);
String* pStringBad = new StringBad(motto);//用一个已有的对象去创建一个刚刚声明的对象

The two declarations in the middle may use the copy constructor to directly create metoo and also, or use the copy constructor to generate a temporary object, and then assign the content of the temporary object to metoo and also, depending on the specific implementation. The last declaration initializes an anonymous object using motto and assigns the address of the new object to the pstring pointer.

Whenever a program makes a copy of an object, the compiler will use the copy constructor.

Specifically, the copy constructor is used when a function passes an object by value (such as callme2() in Listing 12.3) or when a function returns an object. Remember, passing by value means creating a copy of the original variable. The copy constructor is also used when the compiler generates temporary objects.

Since passing an object by value invokes the copy constructor, objects should be passed by reference. This saves the time to call the constructor and the space to store the new object.

12.1.2.4 The default copy constructor

The default copy constructor copies non-static members one by one (member copy is also called shallow copy), and what is copied is the value of the member.

StringBad sailor = sports;//相当于以下操作,但是由于私有成员无法访问,因此这些代码是无法通过编译的
StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;

insert image description here

When defining a class, the copy constructor generated by C++ by default often causes various problems. And one of the solutions is: define an explicit copy constructor to solve the problem. The reason a copy constructor must be defined is that some class members are pointers to data initialized with new, rather than the data itself. Figure 12.3 illustrates deep copying.

insert image description here

Define an explicit copy constructor to solve the problem

The solution to this problem in class design is to do a deep copy. That is, the copy constructor should copy the string and assign the address of the copy to the str member, not just copy the address of the string.

If the class contains pointer members initialized with new, a copy constructor should be defined to copy the pointed-to data instead of the pointer. This is called deep copying. Another form of copying (member copy or shallow copy) simply copies pointer values. A shallow copy only copies the pointer information shallowly, without "digging" deep enough to copy the structure referenced by the pointer. This way each object has its own string, rather than a reference to another object's string. Each time the destructor is called, the different strings will be freed, and no attempt will be made to free the already freed strings.

The reason a copy constructor must be defined is that some class members are pointers to data initialized with new, rather than the data itself.

If the class contains pointer members initialized with new, a copy constructor should be defined to copy the pointed-to data instead of the pointer. This is called deep copying. Another form of copying (member copy or shallow copy) simply copies pointer values. A shallow copy only copies the pointer information shallowly, without "digging" deep enough to copy the structure referenced by the pointer.

12.1.3 Assignment Operators

C++ allows class object assignment, which is achieved by automatically overloading the assignment operator for the class. The prototype of this operator is as follows:

classname & classname::operator = (const classname&);//它接受并返回一个指向类对象的引用。

When assigning an existing object to another object, the overloaded assignment operator is used:

StringBad headline("Celery");
StringBad knot;
knot = headline;//调用赋值运算符

When initializing an object, it is not necessary to use the assignment operator:

StringBad metoo = knot;//调用的是复制构造函数,也有可能调用赋值运算符。

Here, metoo is a newly created object that is initialized to the value of knot, thus using the copy constructor.

However, as mentioned earlier, the implementation may also process this statement in two steps: use the copy constructor to create a temporary object, and then copy the value of the temporary object to the new object through assignment. That is, initialization always invokes the copy constructor, and the assignment operator may also be invoked when using the = operator.

Where is the problem with assignment?

As before, it is prone to the problem that two pointers point to the same address. First use one pointer to delete a content, and then use another pointer to delete a deleted content. The problem is uncertain, so it may change content in memory, causing the program to terminate abnormally.

How to solve the assignment problem?

For problems caused by inappropriate default assignment operators, the workaround is to provide assignment operator (doing deep copy) definitions. Its implementation is similar to the copy constructor, but there are some differences.

  • Since the target object may refer to previously allocated data, the function should use delete[] to release the data.
  • Functions should avoid assigning objects to themselves; otherwise, freeing memory operations may delete the contents of the object before reassigning the object.
  • The function returns a reference to the calling object.

By returning an object, the function can perform continuous assignments like normal assignment operations, that is, if S0, S1, and S2 are all StringBad objects, you can write code like this:

S0 = S1 = S2;//相当于
S0.operator = (S1.operator=(S2));

StringBad & StringBad::operator=(const StringBad & st){
    if(this == &st){
        return *this;
    }
    delete [] str;
    str = new char[len+1];
    std::strcpy(str,st.str);
    return *this;
}

The code first checks for self-replication, which is done by seeing if the address to the right of the assignment operator (&s) is the same as the address of the receiving object (this). If they are the same, the program will return *this and end. The assignment operator is one of the operators that can only be overloaded by class member functions.

If the address is different, the function will free the memory pointed to by str, because str will be assigned the address of a new string later. The above string will remain in memory if you don't use the delete operator first. This memory is wasted because the program no longer contains a pointer to the string.

The next operation is similar to the copy constructor, that is, allocate enough memory space for the new string, and then copy the string in the object on the right side of the assignment operator to the new memory unit.

After the above operations are completed, the program returns to *this and ends.

12.2 Improved new String class

Static function: static return value type type name (parameter list). Static functions are not available across files. Calling a static function does not have the overhead of pushing and popping the stack, so the efficiency is relatively high. Functions with the same name are allowed in other files. So static methods are generally declared and defined together in the header file.

12.2.1 Revised default constructor

String::String(){
    len = 0;
    str = new char[1];
    str[0] = '\0';
}

Why not new char? Both ways allocate the same amount of memory, the difference is that the former is compatible with class destructors, while the latter is not. The destructor contains the following code:

delete [] str;//delete[]与使用new[]初始化的指针和空指针都兼容。
//因此下面的代码:
str = new char[1];
str[0] = '\0';//可以修改为
str = 0;

Null pointers in C++11:

In C++98, the literal value 0 has two meanings: it can represent the numeric value zero, and it can also represent a null pointer, which makes it difficult for the reader and the compiler to distinguish. Some programmers use (void *) 0 to identify a null pointer (the internal representation of a null pointer itself may not be zero), and some programmers use NULL, which is a C language macro that represents a null pointer.

C++11 provides a better solution: a new keyword nullptr is introduced to represent a null pointer. You can still use 0 as before - otherwise a lot of existing code would be illegal, but it is recommended that you use nullptr: str = nullptr;

12.2.2 Comparing member functions

strcmp() function: the first parameter is before the second parameter, the function returns a negative value; if the two strings are the same, it returns 0; if the first parameter is after the second parameter, it returns a Positive value.

12.2.3 Accessing characters using bracket notation

For bracket operators, one operand precedes the first bracket and the other operand is between the two brackets. Thus, in the expression city[0], city is the first operand, [] is the operator, and 0 is the second operand.

char & string::operator[](int i){
    return str[i];
}

12.2.4 Static class member functions

Member functions can be declared static (a function declaration must contain the keyword static, but it cannot if the function definition is self-contained), and doing so has two important consequences.

First, you cannot call a static member function through an object; in fact, a static member function cannot even use the this pointer. If a static member function is declared in the public section, it can be called using the class name and the scope resolution operator.

Second, since static member functions are not associated with a specific object, only static data members can be used. Likewise, static member functions can be used to set class-level (classwide) flags to control the behavior of certain class interfaces.

static member function Ordinary member function
All objects share Y Y
implicit this pointer N Y
Access ordinary member variables (functions) N Y
Access static member variables (functions) Y Y
Call directly by class name Y N
Call directly by object name Y Y

12.2.5 Further overloading of assignment operators

String name;
char temp[40];
cin.getline(temp,40);
name = temp;

This is not an ideal solution if you do this too often. Let's review how the last statement works.

* 1. The program uses the constructor String(const char ) to create a temporary String object that contains a copy of the string in temp. As introduced in Chapter 11, constructors with only one parameter are used as conversion functions.

2. Use the String &String::operator=(const String &) function to copy the information in the temporary object into the name object.

3. The program calls the destructor ~String() to delete the temporary object.

In order to improve efficiency, the easiest way is to overload the assignment operator so that it can directly use regular strings, so that there is no need to create and delete temporary objects. Here's a possible implementation:

String & string::operator=(const char* s){
    delete [] str;
    len = std::strlen(s);
    str = new char[len+1];
    std::strcpy(str,s);
    return *this;
}

In general, the memory pointed to by str must be freed and enough memory allocated for the new string.

12.3 Things to note when using new in constructors

If you use new in the constructor to initialize pointer members, you should use delete in the destructor.

new and delete must be compatible with each other. new corresponds to delete, and new[] corresponds to delete[].

If there are multiple constructors, new must be used in the same way, either with brackets or none. Since there is only one destructor, all constructors must be compatible with it. However, it is possible to initialize a pointer with new in one constructor and to nothing (0 or nullptr in C++11) in another, because delete (whether with brackets or without square brackets) can be used for null pointers.

NULL, 0, or nullptr: Previously, a null pointer could be represented by 0 or NULL (in many header files, NULL is a symbolic constant defined as 0). C programmers usually use NULL instead of 0 to indicate that this is a pointer, just as they use '\0' instead of 0 to indicate the null character to indicate that this is a character. However, C++ has traditionally preferred plain 0 to the equivalent NULL. But as pointed out earlier, C++11 provides the keyword nullptr, which is a better choice.

A copy constructor should be defined to initialize one object to another by deep copying. Typically, such a constructor looks like this:

String::String(const string & st){
    num_strings++;
    len = st.len;
    str = new char [len+1];
    std::strcpy(str,st.str);
}

Specifically, the copy constructor should allocate enough space to store the copied data, and copy the data, not just the address of the data. Also, all affected static class members should be updated.

An assignment operator should be defined to copy one object to another via deep copy. Typically, the class method looks like this:

String & String::operator=(const String & st){
    if(this == &st){
        return *this;
    }
    delete [] str;
    len = st.len;
    str = new char[len+1];
    std::strcpy(str,st.str);
    return *this;
}

Specifically, the method should check for self-assignment, free the memory previously pointed to by the member pointer, copy the data rather than just the address of the data, and return a reference to the calling object.

12.4 Notes on returned objects

12.4.1 Returning a reference to a const object

A common reason to use const& is for efficiency. If a function returns the object passed to it (either by calling a method on the object or passing the object as a parameter), you can improve its efficiency by returning a reference.

Suppose now that there is a copy constructor that returns an object, and a copy constructor that uses const&:

  1. First, returning an object will invoke the copy constructor, whereas returning a reference will not. So the second version does less work and is more efficient.
  2. Second, the object the reference points to should exist at the time the calling function executes.
  3. Finally, both v1 and v2 are declared as const references, so the return type must be const for this to match.

12.4.2 Returning a reference to a non-const object

By using a reference, you can avoid the function calling the copy constructor to create a new object, thereby improving efficiency.

12.4.3 Returning Objects

If the returned object is a local variable in the called function, it should not be returned by reference, because the local object will have its destructor called when the called function finishes executing.

12.4.4 Returning a const object

If a method or function is to return a local object, it should return the object, not a reference to the object. In this case, a copy constructor is used to generate the returned object.

12.5 Using pointers to objects

Initialize the object with new:

Normally, if Class_name is a class and value is of type Type_name, the following statement:

class_name * pclass = new class_name(value);

The following constructor will be called:

class_name(Type_name);

There may also be some trivial conversions here, such as:

class_name(const Type_name &);

If no ambiguity exists, conversions resulting from prototype matching (such as from int to double) will occur. The following initialization method will call the default constructor:

class_name * ptr = new class_name;

12.5.1 Talk about new and delete again

The destructor will be called when:

If the object is a dynamic variable, the object's destructor will be called when the block in which the object is defined has been executed.

If the object is a static variable, the object's destructor will be called at the end of the program.

If an object was created with new, its destructor will only be called if you explicitly delete the object with delete.

12.5.2 Summary of Pointers and Objects

Use conventional notation to declare a pointer to an object: String * glamour;

A pointer can be initialized to point to an existing object: String * first = &sayings[0];

A pointer can be initialized with new, which will create a new object:

String *favorite = new String(sayings[choice]);

Using new on a class will call the corresponding class constructor to initialize the newly created object:

String * gleep = new String;
String * glop = new String("my");
String * favorite = new String(sayings[choice]);

insert image description here

Use new to create objects:

insert image description here

Class methods can be accessed through pointers using the -> operator:

if(sayings[i].lenth())<shortest->lenth())

The object can be obtained by applying the dereference operator (*) to the object pointer:

if(sayings[i]<*first){
    first = &sayings[i];
}

12.5.3 Revisiting the positioning new operator

The programmer must be responsible for supervising the buffer memory locations used by the positioned new operator. To use different memory locations, the programmer needs to provide two different addresses in the buffer and make sure that the two memory locations do not overlap.

pc1 = new (buffer)JustTesting;
pc3 = new (buffer + sizeof(JustTing)) JustTesting("Better Idea",6);

If you use the positioned new operator to allocate memory for an object, you must ensure that its destructor is called. But how to make sure? For objects created on the heap, you can do this:

delete pc2; //delete object pointed to by pc2

But not like this:

delete pc1; //delete object pointed to by pc1?NO!
delete pc3; //delete object pointed to by pc3?NO!

The reason is that delete works with the regular new operator, but not with the positional new operator.

For example, the pointer pc3 does not receive the address returned by the new operator, so deleting pc3 will cause a runtime error.

char * buffer = new char[BUF];
pc1 = new(buffer)JustTesting;//这种运行到最后,程序并不会调用JustTesting的析构函数。

The solution to this kind of problem is to explicitly call the destructor for objects created with the positional new operator. Normally the destructor is called automatically, this is one of the few situations where an explicit call to the destructor is required. When calling a destructor explicitly, you must specify the object to be destroyed. Since there are pointers to objects, these can be used:

pc3->JustTesting();
pc1->JustTesting();

One thing to watch out for is the correct order of deletion. Objects created with the positional new operator should be deleted in the reverse order of creation. The reason is that objects created later may depend on objects created earlier. Also, the buffers used to store these objects are only freed when all objects have been destroyed.

12.6 Review of various techniques

12.6.1 Overloading the << operator

To redefine the << operator so that it can be used with cout to display the contents of an object, define the following friend operator function:

ostream & operator << (ostream & os, const c_name & obj){
    os<<...;
    return os;
}

12.6.2 Conversion functions

To convert a single value to a class type, you need to create a class constructor whose prototype looks like this:

c_name (type_name value);//其中c_name为类名,type_name是要转换的类型的名称。

To convert a class to another type, you need to create a class member function whose prototype looks like this:

operator type_name();//虽然该函数没有声明返回类型,但应返回所需类型的值。

Be careful when using conversion functions. You can use the keyword explicit when declaring a constructor to prevent it from being used for implicit conversions.

12.6.3 A class whose constructor uses new

If a class uses the new operator to allocate memory pointed to by class members, some precautions should be taken at design time:

For all class members whose memory is allocated by new, delete should be used in the destructor of the class, which will release the allocated memory.

If the destructor frees memory by using delete on the pointer class member, every constructor should initialize the pointer with new, or set it to a null pointer.

Either use new [] or new in the constructor, and they cannot be mixed. If the constructor uses new[], the destructor should use delete[]; if the constructor uses new, the destructor should use delete.

A copy constructor should be defined that allocates memory instead of pointing a pointer to existing memory. This way the program will be able to initialize a class object to another class object. The prototype of such a constructor is usually as follows:

className(const className &)

A class member function that overloads the assignment operator should be defined, and its function is defined as follows (where c_pointer is a class member of c_name, and its type is a pointer to type_name). The following example assumes that new[] is used to initialize the variable c_pointer:

String & String::operator=(const String & st){
    if(this == &st){
        return *this;
    }
    delete [] str;
    len = st.len;
    str = new char[len+1];
    std::strcpy(str,st.str);
    return *this;
}

12.7 Queue Simulation Problems

12.7.1 ATM problems

Heather Bank intends to open an automated teller machine (ATM) at the Food Heap supermarket. Food Heap supermarket managers are concerned that queues to use ATMs will disrupt traffic at the supermarket and want to limit the number of people waiting in line. Heather Bank wants to estimate how long customers wait in line. To write a program to simulate this situation, so that supermarket managers can understand the possible impact of ATM.

A representative of Heather Bank said: Usually, one-third of customers only need one minute to get service, one-third of customers need two minutes, and another third of customers need three minutes. Also, the timing of customer arrival is random, but the number of customers using the ATM each hour is fairly constant.

Design a class to represent the customer, and simulate the interaction between the customer and the queue.

insert image description here

12.7.2 Queue class

Features of the Queue class:

  • Queues store ordered sequences of items;
  • There is a limit to the number of items the queue can hold;
  • Should be able to create empty queues;
  • It should be possible to check if the queue is empty;
  • It should be possible to check if the queue is full;
  • It should be possible to add items at the end of the queue;
  • It should be possible to remove items from the head of the queue;
  • It should be possible to determine the number of items in the queue.

12.7.3 Interface of the Queue class

Judging from the characteristics of the above classes how to define the public interface of the Queue class:

class Queue{
    enum{Q_SIZE = 10};
    public:
    Queue(int qs = Q_SIZE);
    ~Queue();
    bool isEmpty()const;
    bool isFull()const;
    int queueCount()const;
    bool enqueue(const Item &item);
    bool dequeue(Item &item);
}

12.7.4 Implementation of the Queue class

Once the interface is determined, it can be implemented. First, you need to decide how to represent the queue data. One way is to use new to dynamically allocate an array that contains the desired number of elements. However, for queue operations, arrays are not very suitable. For example, after deleting the first element of the array, you need to move all the remaining elements forward; otherwise, you need to do some more laborious work, such as treating the array as a loop. However, a linked list works well for queues. A linked list consists of a sequence of nodes. Each node contains the information to be saved in the linked list and a pointer to the next node. For the queue here, the data part is a value of Item type, so the following structure can be used to represent the node:

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

To follow a linked list, the address of the first node must be known. You can make a data member of the Queue class point to the starting position of the linked list. Specifically, this is all the information needed to find any node along a chain of nodes. However, since the queue always appends new items
to the tail, it is convenient to include a data member pointing to the last node (see Figure 12.9). In addition, data members can be used to keep track of the maximum number of items the queue can store and the current number of items. So, the private part of the class declaration looks like this:

insert image description here

class Queue{
    private:
    struct Node{ Item item; struct Node * next;};
    enum(Q_SIZE = 10);
    NODE * front;
    NODE * rear;
    int items;
    const int qsize;
}

Only constructors can use this initializer-list syntax. For non-static const class members, you must use this syntax. This syntax must also be used for class members that are declared as references. This is because references, like const data, can only be initialized when they are created.

Data members are initialized in the order in which they appear in the class declaration, regardless of the order in which they are listed in the initializer. (this point of view is doubtful)

The codes of isempty(), isfull(), and queuecount() are very simple. If items is 0, the queue is empty; if items is equal to qsize, the queue is full. To know the number of items in the queue, just return the value of items.

Adding items to the end of the queue (enqueue) is cumbersome:

bool Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node; //创建节点
	//如果失败,new将抛出std::bad_alloc的异常
	add->item = item; //设置节点指针
	add->next = NULL;
	items++;
	if (front == NULL) //如果队列为空
		front = add; //将元素放在队首
	else
		rear->next = add; //否则放在队尾
	rear = add; //设置尾指针指向新节点
	return true;
}

insert image description here

The method goes through the following stages:

1) If the queue is full, end.

2) Create a new node. If new cannot create a new node, it throws an exception, and the program terminates unless code to handle the exception is provided.

3) Put the correct value in the node. The code copies the Item value to the node's data section and sets the node's next pointer to NULL. This prepares the node to be the last item in the queue.

4) Increment the item count (items) by 1.

5) Append the node to the tail of the queue. This has two parts. First, connect the node with another node in the list. This is done by pointing the next pointer of the current tail node to the new tail node. The second part is to set the member pointer rear of the Queue to point to the new node, so that the queue can directly access the last node. If the queue is empty, the front pointer must also be set to point to the new node (if there is only one node, it is both the head and tail of the queue).

Deleting the first item in the queue (dequeuing) also requires multiple steps to complete:

bool Queue::dequeue(Item & item)
{
	if (front == NULL)
		return false;
	item = front->item; //将队首元素赋值给item
	items--;
	Node * temp = front; //保存队首元素位置
	front = front->next; //将front设置成指向下一个节点
	delete temp; //删除以前的第一个节点
	if (items == 0)
		rear = NULL;
	return true;
}

insert image description here

Need to go through the following stages:

1) If the queue is empty, end

2) The first item of the queue is provided to the calling function by copying the data part from the current front node into the reference variable passed to the method.

3) Decrement the item count (items) by 1.

4) Save the position of the front node for later deletion.

5) Dequeue the node. This is done by setting the Queue member pointer front to point to the next node, whose position is given by front->next.

6) To save memory, delete the previous first node.

7) If the linked list is empty, set rear to NULL.

Step 4 is essential because step 5 will remove information about the previous first node position.

12.7.5 Are other functions required?

The class constructor doesn't use new, so at first glance, it seems that you don't need to pay attention to the special requirements imposed on the class due to the use of new in the constructor. Because adding an object to the queue will call new to create a new node. By deleting nodes, the dequeue() method can indeed clear nodes, but this does not guarantee that the queue will be empty when it expires. Therefore, the class needs an explicit destructor - a function that deletes any remaining nodes.

The following is an implementation that starts from the head of the linked list and deletes each node in it in turn:

Queue::~Queue()
{
	Node * temp;
	while (front != NULL) //确定queue不为空
	{
		temp = front; //保存前一个元素
		front = front->next; //重置下一个元素
		delete temp; //删除前一个元素
	}
}

Classes that use new usually need to contain explicit copy constructors and assignment operators that perform deep copies, is this also the case?

The first question to answer is, is the default member replication appropriate? the answer is negative.

Copying the members of the Queue object will generate a new object that points to the original head and tail of the linked list. Therefore, adding an item to the copied Queue object modifies the shared linked list. Doing so will have very serious consequences. Worse, only the copy's tail pointer gets updated, which corrupts the linked list from the original object's point of view. Obviously, to clone or copy a queue, you must provide a copy constructor and an assignment constructor that performs a deep copy.

Of course, this raises the question: why copy the queue at all? Maybe you want to save snapshots of the queue at different stages of the simulation, or maybe you want to feed two different policies the same input. In fact, it is very useful to have an operation that splits the queue, supermarkets often do this when opening additional checkouts. Likewise, it may be desirable to combine two queues into one or to truncate a queue.

But suppose the simulation here does not implement the above functionality. Can't these problems be ignored and existing methods used? sure. However, at some point in the future, it may be necessary to use the queue again and require replication. Also, you might forget not to provide proper code for the copy. In this case, the program will compile and run, but the result will be messy and even crash. Therefore, it is better to provide a copy constructor and assignment operator, although they are not currently required.

Fortunately, there's a little trick that can avoid all this extra work and keep your program from crashing. This is all it takes to define the desired method as a pseudo-private method:

class Queue
{
private:
	Queue(const Queue &q) : qsize(0) { } //先发制人的定义 
	Queue & operator=(const Queue &q) { return *this; }
	...
};

This does two things:

1) It avoids default method definitions that would otherwise be automatically generated.

2) Because these methods are private, they cannot be widely used.

If nip and tuck were Queue objects, the compiler would not allow this:

Queue snick(nip); //错误,不被允许
tuck = nip; //错误,不被允许

C++11 provides another way to disable methods - using the keyword delete.

When an object is passed by value (or returned), the copy constructor will be called. If you follow the convention of preferring passing objects by reference, you won't have any problems. The copy constructor is also used to create other temporary objects, but there are no operations in the Queue definition that lead to the creation of temporary objects, such as overloading the addition operator.

12.7.6 Customer class

Next you need to design the client class. Typically, an ATM customer has many attributes, such as name, account, and account balance. However, the only attributes that the simulation needs to use here are when a customer enters the queue and how long it takes for a customer to transact. When the simulation generates a new customer, the program creates a new customer object and stores the customer's arrival time and a randomly generated transaction time in it. When the customer arrives at the head of the queue, the program will record the time at this time and subtract it from the time of entering the queue to obtain the customer's waiting time. The following code demonstrates how to define and implement the Customer class:

class Customer
{
private:
	long arrive; //顾客到达时间
	int processtime; //顾客进行时间
public:
	Customer() { arrive = processtime = 0; }
	void set(long when);
	long when() const { return arrive; }
	int ptime() const { return processtime; }
};
void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}

The default constructor creates an empty client. The set() member function sets the arrival time as a parameter, and sets the processing time as a random value from 1 to 3.

queue.h

#ifndef QUEUE_H_
#define QUEUE_H_
//这个队列包含Customer元素
class Customer
{
private:
	long arrive; //顾客到达时间
	int processtime; //顾客进行时间
public:
	Customer() { arrive = processtime = 0; }
	void set(long when);
	long when() const { return arrive; }
	int ptime() const { return processtime; }
};
typedef Customer Item;

class Queue
{
private:
	//类中嵌套结构声明
	struct Node { Item item; struct Node * next; };
	enum { Q_SIZE = 10 };
	//私有成员
	Node * front; //队首指针
	Node * rear; //队尾指针
	int items; //队列中当前元素个数
	const int qsize; //队列中最大元素个数
	//避免本来自动生成的默认方法定义
	Queue(const Queue &q) : qsize(0) { }
	Queue & operator=(const Queue &q) { return *this; }
public:
	Queue(int qs = Q_SIZE); //创建一个qs大小队列
	~Queue();
	bool isempty() const;
	bool isfull() const;
	int queuecount() const;
	bool enqueue(const Item &item); //在队尾添加元素
	bool dequeue(Item &item); //在队首删除元素
};
#endif // !QUEUE_H_

queue.cpp

#include "queue.h"
#include <cstdlib>

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

Queue::~Queue()
{
	Node * temp;
	while (front != NULL) //确定queue不为空
	{
		temp = front; //保存前一个元素
		front = front->next; //重置下一个元素
		delete temp; //删除前一个元素
	}
}

bool Queue::isempty() const
{
	return items == 0;
}

bool Queue::isfull() const
{
	return items == qsize;
}

int Queue::queuecount() const
{
	return items;
}

//入队
bool Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node; //创建节点
	//如果失败,new将抛出std::bad_alloc的异常
	add->item = item; //设置节点指针
	add->next = NULL;
	items++;
	if (front == NULL) //如果队列为空
		front = add; //将元素放在队首
	else
		rear->next = add; //否则放在队尾
	rear = add; //设置尾指针指向新节点
	return true;
}

//出队
bool Queue::dequeue(Item & item)
{
	if (front == NULL)
		return false;
	item = front->item; //将队首元素赋值给item
	items--;
	Node * temp = front; //保存队首元素位置
	front = front->next; //将front设置成指向下一个节点
	delete temp; //删除以前的第一个节点
	if (items == 0)
		rear = NULL;
	return true;
}

//设置处理时间为1~3的随机值
void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}

12.7.7 ATM simulation

Now you have the tools you need to simulate an ATM. The program allows the user to enter 3 numbers: the maximum length of the queue, the duration of the program simulation (in hours), and the average number of customers per hour. The program will use loops - each loop represents one minute. In the cycle of each minute, the program will complete the following work:
1) Determine whether a new customer has come. If it comes, and the queue is not full at this time, add it to the queue, otherwise reject the customer to enqueue.
2) If no customer is conducting a transaction, the first customer of the queue is picked. Determine how long this customer has been waiting, and set the wait_time counter to the processing time required for new customers.
3) If the client is being processed, decrement the wait_time counter by 1.
4) Record various data, such as the number of customers who get service, the number of customers who are rejected, the accumulated time of waiting in line and the accumulated queue length, etc.

When the simulation loop ends, the program will report various statistical results.

How the program determines whether a new customer arrives: Assuming an average of 10 customers arrive per hour, that equates to one customer every 6 minutes. The program will calculate this value and save it in the min_per_cust variable. However, exactly one customer every 6 minutes is not realistic, what we really want is a more random process - but one customer every 6 minutes on average.

The program uses the following function to determine if a customer has arrived during the loop:

bool newcustomer(double x)
{
	return (std::rand() * x / RAND_MAX < 1); 
}

It works like this: the value RAND_MAX is defined in the cstdlib file and is the maximum value that the rand() function may return (0 is the minimum value). Assuming that the average time between arrivals of customers x is 6, the value of rand()*x/RAND_MAX will be between 0 and 6. Specifically, on average every 6 times, this value will be less than 1. However, this function may cause customers to arrive at intervals of 1 minute and 20 minutes at other times. This method, although clumsy, can make the actual situation different from the regular arrival of a customer every 6 minutes. The above will not work if the average time between arrivals of customers is less than 1 minute, but the simulation is not designed for this case. If you really need to handle this situation, it's better to increase the time resolution, say each loop represents 10 seconds.

main.cpp

If you run the simulation program for a long time, you can know the long-term average; if you run the simulation program for a short time, you will only know the short-term changes.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include "queue.h"
const int MIN_PER_HR = 60;

bool newcustomer(double x);

int main()
{
	using std::cin;
	using std::cout;
	using std::endl;
	using std::ios_base;
	
	std::srand(std::time(0)); //随机初始化

	cout << "Case Study: Bank of Heather Automatic Teller\n";
	cout << "Enter maximum size of queue: ";
	int qs;
	cin >> qs;
	Queue line(qs); //队列中能装下的人数

	cout << "Enter the number of simulation hours: ";
	int hours; //模拟小时
	cin >> hours;
	//模拟将每分钟运行 1 个周期 
	long cyclelimit = MIN_PER_HR * hours;

	cout << "Enter the average number of customer per hour: ";
	double perhour; //每小时顾客到达平均个数
	cin >> perhour;
	double min_per_cust; //平均间隔时间
	min_per_cust = MIN_PER_HR;

	Item temp; //创建一个customer对象
	long turnaways = 0; //队满,拒绝入队
	long customers = 0; //加入队列
	long served = 0; //
	long sum_line = 0; //排队等候累积的队列长度
	int wait_time = 0; //等待ATM空闲时间
	long line_wait = 0; //排队等候累积的时间
	//运行这个模拟
	for (int cycle = 0; cycle < cyclelimit; cycle++)
	{
		if (newcustomer(min_per_cust))
		{
			if (line.isfull())
				turnaways++;
			else
			{
				customers++;
				temp.set(cycle); //cycle = time of arrival
				line.enqueue(temp); //加入新顾客
			}
		}
		if (wait_time <= 0 && !line.isempty())
		{
			line.dequeue(temp); //下一个顾客
			wait_time = temp.ptime(); //等待时间
			line_wait += cycle - temp.when();
			served++;
		}
		if (wait_time > 0)
			wait_time--;
		sum_line += line.queuecount();
	}
	//打印结果
	if (customers > 0)
	{
		cout << "customers accepted: " << customers << endl;
		cout << "  customers served: " << served << endl;
		cout << "        turnaways: " << turnaways << endl;
		cout << "average queue size: ";
		cout.precision(2);
		cout.setf(ios_base::fixed, ios_base::floatfield);
		cout << (double)sum_line / cyclelimit << endl;
		cout << " average wait time: "
			<< (double)line_wait / served << " minutes\n";
	}
	else
		cout << "No customers!\n";
	cout << "Done!\n";
	return 0;
}

//x = 客户到达的平均间隔时间
bool newcustomer(double x)
{
	return (std::rand() * x / RAND_MAX < 1); //如果顾客到达的平均时间间隔少于1分钟,则返回真
}

12.8 Summary

This chapter covers many important aspects of defining and using classes. Some of these aspects are very subtle or even difficult to understand concepts. Don't be afraid if some of these concepts are too complicated for you—these questions are difficult for most beginners in C++. Often, concepts such as copy constructors are understood incrementally after you run into trouble by ignoring them. Some of the material presented in this chapter may seem very difficult to understand at first, but as you gain more experience, you will understand it more thoroughly.

In the class constructor, you can use new to allocate memory for data, and then assign the memory address to the class member. In this way, the class can handle strings of different lengths without having to fix the length of the array in advance when the class is designed. Using new in class constructors can also cause problems when objects expire. If the object contains member pointers and the memory it points to is allocated by new, releasing the memory used to hold the object does not automatically free the memory pointed to by the object member pointers. Therefore, when using the new class in the class constructor to allocate memory, you should use delete in the class destructor to release the allocated memory. This way, when the object expires, the memory pointed to by its pointer member is automatically freed.

Problems can also arise when initializing one object into another object, or assigning one object to another object, if the object contains a pointer member to new-allocated memory. By default, C++ initializes and assigns members one by one, which means that the members of the object being initialized or assigned will be exactly the same as the original object. If the members of the original object point to a data block, the copy members will point to the same data block. When the program finally deletes these two objects, the destructor of the class will try to delete the same block of memory data twice, which will error out. The solution is: define a special copy constructor to redefine the initialization, and overload the assignment operator. In either case, the new definition will create copies pointing to the data, and make new objects point to those copies. That way, both the old and new objects will refer to independent, identical data without overlapping. Assignment operators must be defined for the same reason. In each case, the ultimate goal is to perform a deep copy, that is, copy the actual data, not just a pointer to the data.

When an object's storage persistence is automatic or external, its destructor is called automatically when it no longer exists. If you use the new operator to allocate memory for an object, and assign its address to a pointer, the destructor will automatically be called for the object when you use delete on the pointer. However, if you use the positioned new operator (instead of the regular new operator) to allocate memory for a class object, you must take care of explicitly calling the destructor for that object by calling the destructor method with a pointer to the object. C++ allows structures, classes, and enumeration definitions to be contained within classes. The scope of these nested types is the whole class, which means that they are confined to the class and will not conflict with structures, classes and enumerations of the same name defined elsewhere.

C++ provides a special syntax for class constructors that can be used to initialize data members. This syntax consists of a colon and a comma-separated initialization list, placed after the closing parenthesis of the constructor parameters and before the opening parenthesis of the function body. Each initializer consists of the name of the member being initialized and parentheses containing the initial value. Conceptually, these initialization operations are performed when the object is created, and the statements in the function body have not yet been executed. The syntax is as follows:

queue(int qs) : qsize(qs),item(0),front(NULL),rear(NULL){}

This format must be used if the data member is a non-static const member or a reference, but C++11's new in-class initialization can be used for non-static const members.

Guess you like

Origin blog.csdn.net/weixin_43717839/article/details/130088118