Dynamic Memory Allocation via new and delete

So far, we've encountered two storage classes: static and auto. As you might recall from the discussion in Chapter 5, static variables are allocated memory when the program is linked, while the memory for auto variables is assigned to them at entry to the block where they are defined. However, both mechanisms have a major limitation; the amount of memory needed is fixed when the program is compiled. In the case of a string, we need to allocate an amount of memory that in general cannot be known until the program is executed, so we need another storage class.

As you will be happy to learn, there is indeed another storage class called dynamic storage that enables us to decide the amount of memory to allocate at run time.[6] To allocate memory dynamically, we use the new operator, specifying the data type of the memory to be allocated and the count of elements that we need. In the member initialization expression m_Data(new char [m_Length]), the type is char and the count is m_Length. The result of calling new is a pointer to the specified data type; in this case, since we want to store chars, the result of calling new is a pointer to a char; that is, a char*. This is a good thing, because char* is the type of the variable m_Data that we're initializing to the address that is returned from new. So the result of the member initialization expression we're examining is to set m_Data to the value returned from calling new; that value is the address of a newly assigned block of memory that can hold m_Length chars. In the case of the default constructor, we've asked for a block of 1 byte, which is just what we need to hold the contents of the zero-length C string that represents the value of our empty string.

[6] This terminology doesn't exactly match the official nomenclature used by Bjarne Stroustrup to describe dynamic memory allocation. However, every C++ programmer will understand you if you talk about dynamic storage, and I think this terminology is easier to understand than the official terminology.

It may not be obvious why we need to call new to get the address where we will store our data. Doesn't a char* always point to a byte in memory? Yes, it does; the problem is which byte. We can't use static (link time) or auto (function entry time) allocation for our string class, because each string can have a different number of characters. Therefore, we have to assign the memory after we find out how many characters we need to store the value of the string. The new operator reserves some memory and returns the address of the beginning of that memory. In this case, we assign that address to our char* variable called m_Data. An important point to note here is that in addition to giving us the address of a section of memory, new also gives us the right to use that memory for our own purposes. That same memory area will not be made available for any other use until we say we're done with it by calling another operator called delete.

Susan had some questions about how (and why) we use new. Here's the discussion:

Susan: OK, so all Figure 7.3 does is lay the foundation to be able to acquire memory to store the C string "" and then copy that information that will go into m_Data that starts at a certain location in memory?

Steve: Right. Figure 7.6 is the code for the constructor that accomplishes that task.

Susan: When you say that "the amount of memory needed is fixed when the program is compiled" that bothers me. I don't understand that in terms of auto variables, or is this because that type is known such as a short?

Steve: Right. As long as the types and the quantity of the data items in a class definition are known at compile time, as is the case with auto and static variables, the compiler can figure out the amount of memory they need. The addresses of auto variables aren't known at compile time, but how much space they use is.

Susan: OK, I understand the types of the data items. However, I am not sure what you mean by the quantity; can you give me an example?

Steve: Sure. You might have three chars and four shorts in a particular class definition; in that case, the compiler would add up three times the length of a char and four times the length of a short and allocate that much memory (more or less). Actually, some other considerations affect the size of a class object that aren't relevant to the discussion here, but they can all be handled at compile time and therefore still allow the compiler to figure out the amount of memory needed to store an object of any class.

The statement inside the {} in the default constructor, namely memcpy(m_Data,"",m_Length);, is responsible for copying the null byte from the C string "" to our newly allocated area of memory. The function memcpy (short for "memory copy") is one of the C standard library functions for C string and memory manipulation; it is declared in <cstring>. As you can see, memcpy takes three arguments. The first argument is a pointer to the destination, that is, the address that will receive the data. The second argument is a pointer to the source of the data; this, of course, is the address that we're copying from (i.e., the address of the null byte in the "", in our example). The last argument is the number of bytes to copy.

In other words, memcpy reads the bytes that start at the address specified by its input argument (in this case, "") and writes a copy of those bytes to addresses starting at the address specified by its output argument (in this case, m_Data). The amount copied is specified by the length argument (in this case, m_Length). Effectively, therefore, memcpy copies a certain amount of data from one place in memory to another. In this case, it copies 1 byte (which happens to be a null byte) from the address of the C string literal "" to the address pointed to by m_Data (that is, the place where we're storing the data that make up the value of our string).

This notion of dynamic allocation was the subject of some more discussion with Susan.

Susan: This stuff with operator new: I have no idea what you are talking about. I am totally gone, left in the dust. What is this stuff? Why do you need new to point to memory locations? I thought that is what char* did?

Steve: You're right that char* points to a memory location. But which one? The purpose of new is to get some memory for us from the operating system and return the address of the first byte of that memory. In this case, we assign that address to our char* variable called m_Data. Afterward, we can store data at that address.

Susan: I am not getting this because I just don't get the purpose of char*, and don't just tell me that it points to an address in memory. I want to know why we need it to point to a specific address in memory rather than let it use just any random address in memory.

Steve: Because then there would be no way of guaranteeing that the memory that it points to won't be used by some other part of the program, or indeed some other program entirely in a multitasking system that doesn't provide a completely different memory space for each program. We need to claim ownership of some memory by calling new before we can use it.

Susan: I think I understand now why we need to use new, but why should the result of calling new be a pointer? I am missing this completely. How does new result in char*?

Steve: Because that's how new is defined: it gives you an address (pointer) to a place where you can store some chars (or whatever type you requested).

Susan: OK, but in the statement m_Data = new char [m_Length], why is char in this statement not char*? I am so confused on this.

Steve: Because you're asking for an address (pointer) to a place where you can store a bunch of chars.

Susan: But then wouldn't it be necessary to specify char* rather than char in the statement?

Steve: I admit that I find that syntax unclear as well. Yes, in my opinion, the type should be stated as char*, but apparently Bjarne thought otherwise.

Susan: OK, so then m_Data is the pointer address where new (memory from the free store) is going to store data of m_Length?

Steve: Almost right. The value assigned to m_Data in the constructor is the value returned from operator new; this value is the address of an area of memory allocated to this use. The area of memory is of length m_Length.

Susan: Well, I thought that the address stored in m_Data was the first place where you stored your chars. So is new just what goes and gets that memory to put the address in m_Data?

Steve: Exactly.

Susan: Here's what I understand about the purpose of char*. It functions as a pointer to a specific memory address. We need to do that because the computer doesn't know where to put the char data, therefore we need char* to say "hey you, computer, look over here, this is where we are going to put the data for you to find and use".

Steve: That's fine.

Susan: We need to use char* for variable length memory. This is because we don't know how much memory we will need until it is used. For this we need the variable m_Data to hold the first address in memory for our char data. Then we need the variable m_Length that we have set to the length of the C string that will be used to get the initial data for the string. Then we have to have that nifty little helper guy new to get some memory from the free store for the memory of our C string data.

Steve: Sounds good to me.

Susan: Now about memcpy: This appears to be the same thing as initializing the variable. I am so confused.

Steve: That's correct. Maybe you shouldn't get unconfused!

As the call to memcpy is the only statement in the constructor proper, it's time to see what we have accomplished. The constructor has initialized a string by:

  1. Setting the length of the string to the effective length of a null C string, "", including the terminating null byte (i.e., 1 byte).

  2. Allocating memory for a null C string.

  3. Copying the contents of a null C string to the allocated memory.

The final result of all this work is a string with the value "", whose memory layout might look like Figure 7.4.

Figure 7.4. An empty string in memory


Using the default constructor is considerably easier than defining it. As we have seen in Chapter 6, the default constructor is called whenever we declare an object without specifying any data to initialize it with (for example, in the line string s; in Figure 7.5).

Although this program doesn't do anything useful, it does illustrate how we can use the member functions of our string class, so you should pay attention to it.

Figure 7.5. Our first test program for the string class (codestrtst1.cpp)
#include "string1.h"

int main()
{
   string s;
   string n("Test");
   string x;

   s = n;
   n = "My name is Susan";

   x = n;
   return 0;
}

I should point out here that the only file the compiler needs to figure out how to compile the line string s; is the header file, string1.h. The actual implementation of the string class in string1.cpp isn't required here, because all the compiler cares about when compiling a program using classes is the contract between the class implementer and the user; that is, the header file. The actual implementation in string1.cpp that fulfills this contract isn't needed until the program is linked to make an executable; at that point, the linker will complain if it can't find an implementation of any function that we've referred to.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset