Section 3 - Deep Memory Galaxies

3B.3.1 A Deep Galaxy Class

scenic deep field galaxiesWe create a class Galaxy, which will be used by our local astronomer-programmers to do a variety of things from controlling a remote telescope to archiving newly discovered galaxies.  Currently there are only two data in the Galaxy, the name and the magnitude Magnitudes are numbers that represent the apparent brightness of the galaxy and typically have values in the range 5 to 15 (but for our purposes will always lie between -3 and 30).  So we want to protect the class data from getting incorrect values.  Likewise, we want to protect against invalid names being assigned to the private name member.

That's where the mutator methods setName() and setMagnitude() come in.  Notice how they take formal parameters from some unknown client and assume that the client is sloppy.   They will not set the private data until after the values passed in to these methods have been checked for "reasonableness."

3B.3.2 The Galaxy Class Prototype

Here is our Galaxy class prototype:

class Galaxy
{
private:
  char *name;
  double magnitude;

  static const double MAX_MAG;
  static const double MIN_MAG;
  static const int MAX_NAME;

public:
  Galaxy(const char *nm = "undefined", double mag = 0.);
  Galaxy(const Galaxy &gxy);
  ~Galaxy();
  bool setName(const char *nm = "undefined");
  const char *getName();
  bool setMag(double mag = 0);
  double getMag();
  void show();
};

This is going to be a very straightforward class, so let's make sure we understand every atom of this galaxy.

Static Class Members

We see three static members: they are shared by all objects in the class.  Also we see that they are consts, which means they cannot change.  They are the extreme values of the magnitude (brightness) of the galaxy and the length of the name.  We use these constants to determine whether a client is trying to set Galaxy data with a number or name that we deem to be too large or too small.  Since they are static class members, they will be defined right after the class in special syntax that we learned previously.

Instance Variables

There are two instance variables that make up a Galaxy, name and magnitude.  Here we see where all of the excitement will come to play this week. For name, we are using a char *, (or char pointer). This is going to point to a dynamically allocated char array that will hold the name.  This is our heap data that will have to be carefully managed. 

About the char * Member "name"

Any C++ pointer member whose data is allocated dynamically upon the construction or mutation of the object, controls what we call deep memory -- memory that is not really part of the object, proper, but which is controlled by -- and must be managed by -- the object.

Constructors

We see two constructors.  The first takes default parameters for the name and magnitude, so we can pass this constructor 0, 1 or 2 arguments with no problem.  The second takes an object (reference) of the Galaxy class itself. This constructor is used when we want to create a new Galaxy object using an existing Galaxy object as a template.  This is called a copy constructor because it copies the data from one Galaxy into the data of the Galaxy we are constructing.

Destructor

Since we are going to create heap (a.k.a. deep) memory in our constructor, we need to delete it in our destructor.

Accessors and Mutators

We see the typical accessor methods to set and get the private data.  These methods will protect the instance members from bad client input and will also manage the heap memory if need be.

Output

There is a show() method to send a Galaxy's info to the screen.

Now we discuss each group separately, starting with the most interesting first.

3B.3.3 The Accessors (and Mutators)

Focus on our private instance data. This is always the focus of attention of any class.  In a Galaxy, that data is right here:

class Galaxy
{
private:
     char *name;
  double magnitude;
  // ...

};

Setting and Getting the magnitude is not rocket science.  All we have to do is check the validity of the magnitude values. 

bool Galaxy::setMag(double mag)
{
  if (mag < MIN_MAG || mag > MAX_MAG)
    return false;
  magnitude = mag;
  return true;
}

double Galaxy::getMag()
{
  return magnitude;
}

Any questions?

The interesting part comes next.  We are going to use CStrings for the names.  And not just any CStrings but dynamically allocated ones.  Here are the accessors for the name member:

bool Galaxy::setName(const char *nm)
{
  // return if bad argument passed in
  if (nm == NULL || strlen(nm) < 2 || strlen(nm) > MAX_NAME - 1)
    return false;

  // if we came in from constructor name will be null; don't delete
  // otherwise free up memory before re-allocating
  if (name)
    delete name;

  // allocate CString and copy name
  name = new char[ strlen(nm) + 1 ];
  strcpy(name, nm);
  return true;
}

const char *Galaxy::getName()
{
  // must have a static local to persist after return
  static char buffer[MAX_NAME];

  strcpy(buffer, name);
  return buffer;
}

Let's discuss the setName() method.  First we look at the function header:

bool Galaxy::setName(const char *nm)

We note two things:

  1. The formal parameter is modified with the word "const."  This prevents the method from changing the value of the char * passed to the method.  When you want to make sure your method does not accidentally modify a client variable, use this.  The compiler will issue an error if you attempt to change it, and that is a good thing.
     
  2. setName() takes a char * as a formal parameter.  If you recall the notes in the last lesson, it means this parameter can accept a char array from the client, and that's what it will take. We will take a CString, which is nothing more than a char array, from our client, and use it to set the private data of the Galaxy object that was used to call setName().

In order to illustrate this latter point, here are two different ways we envision calling setName() from the client:

Galaxy g1, g2;
char myCstring[] = "sombrero";

g1.setName("andromeda"); // sets the name member of g1 to "andromeda"
g2.setName(myCstring);  // sets the name member of g2 to "sombrero"

As you can see, we pass it either a literal string or a CString variable, both of which represent char arrays.  The formal parameter nm captures these arrays as all array parameters do:  as the pointer to the first character in the array.

Next we look at the body of the setName() function.  In short, what we are accomplishing here is the allocation of a char array for our name, and then copying the data from the formal parameter into this memory. The new array is heap memory.

Let's take the method line-by-line:

  1. We test the CString nm for NULL and also look at its length. We always want to protect against a pointer being NULL (0).  We also want our galaxies to have names that are at least 2 and less than MAX_NAME characters.  This if statement assures that all three conditions will be satisfied, and returns false if any one is not.
  2. We first clean up old data before allocating new. This is very important.  If we omit the delete step we will have a memory leak (and lose multiple points from our evil instructor!)  If you re-assign a pointer without first dealing with the old data to which it pointed, you will have a leak.  Also, I check to see if the name pointer name is NULL. I do this as a matter of habit, but it is unnecessary as delete will ignore a NULL pointer and not attempt to delete anything, so calling delete with a NULL pointer is no crime.
  3. We allocate a dynamic array of chars just large enough to hold our string.This is the meaning of the line: name = new char[ strlen(nm) + 1 ];  Notice that we use the strlen() function to get the length of the client CString.  But why do we add one (strlen(nm) +1 )?  If you aren't sure we can talk about this in the discussion area.  Or, offer your ideas there.
  4. We use strcpy() to copy the client CString into our internal name array.  It is very important we do not do a simple assignment name = nm.  I can't say this loudly enough.  Our object must have its own private copy of name, and it cannot be pointed to the nm of the client.  If we had been using s-c strings, we could have used an assignment statement.  But, as I have explained earlier, an '=' in a CString assignment is completely different from an '=' in  an s-c string assignment. Until you understand this difference, you will not be understanding CStrings and s-c strings, so get this straight. 

     

    There are two catastrophes that would befall us if we used the assignment operator, =, rather than a strcpy().  If you really understand all the material on pointers and dynamic memory, you will be able to identify both.

Now we explore getName().  Here are three different ways we might use getName():

char galName[50], *galName_ptr;

galName_ptr = g1.getName();         // point to return value
strcpy( galName, g2.getName() );    // copy the return value
cout << g3.getName() << endl;        // display the return value

As you can see, getName() returns a char *, which is a pointer to the first char in the CString holding the name of the Galaxy.  This code shows you that we can point to that CString, copy it to a local CString or just display it.  Now, how do we define getName()?  Think about previous Get() accessor instance methods.  They are usually mind bogglingly simple.  They merely return the member, as in return name;.  But we cannot do that with a char * return type.  It will compile, but it would be a disaster.  It would result in a client pointer pointing to our private (heap) array for the name.  In that case, the client could modify or overrun that private array.  [Try to come up with a simple example of what bad thing would happen if we returned the name to the client.]

So our strategy is simple:

  1. Declare a Static Local Variable. Create a static local char array, called buffer here.  A static local variable is different from a static class member. "Static," when applied to a local variable, means that the variable retains its value between function calls.  That is, when the function returns, and the variable goes out of scope, its contents are not destroyed.  That's because static local variables are not stored on the program stack, but rather in a separate, safe part of memory that does not ever get de-allocated.
     
  2. Copy the private heap data into our static local with strcpy().  This copies the precious data to a separate location which is not part of the object.  The static buffer can be modified without danger to the private name of the Galaxy.
     
  3. Return the static buffer to the client.  We are actually returning a char *, which points to the first char in the buffer, but that's how we always return an array as a return value.  When the client receives the value, it can do anything it wants with it: copy the string to a local CString, point to it, etc.

One of my clever students pointed out that this technique is not safe in some client situations.  A good point:

if (strcmp(g1.getName(), g2.getName()) == 0)

This will always return true, because strcmp() is called only after both getName()s have been executed, at which time there would only be the last call's data in the one and only static buffer.  A client-side solution would be to copy the return value into a variable before calling strcmp(), but a better and more universally accepted solution would be to redefine getName() so that it took a reference string variable and force the client to pass the return string as a reference parameter.

3B.3.4 Deep Memory vs. Shallow Memory

Let's summarize one of the main issues of the past section: the allocation and deletion of the char *name member.  This issue materialized because we were using CStrings rather than string-class strings, however, there are many situations in C++ where we will allocate this so-called deep memory.  Remembering our recent lesson in dynamic arrays, we find that this is another place where deep memory comes into play.  Any time we have to use new to allocate memory in our constructor or mutator, we are generating deep memory for our object.  This means we have to clean that up with delete somewhere in the class.

In our next section, we'll see another important thing we must do when we define our classes using deep memory.