CS302 Lecture notes -- Intro to Classes

  • Brad Vander Zanden (derived from material from Jim Plank)
  • Directory: /sunshine/homes/bvz/cs302/notes/Classes
  • Lecture notes: http://www.cs.utk.edu/~bvz/cs302/notes/Classes/index.html
    Time to start writing C++. Fortunately, C is a subset of C++, so you can borrow a lot of your C knowledge to write C++ code. This won't result in prototypical C++ code because there are many C constructs that C++ programmers typically avoid, like malloc()/free(). But we'll ignore that for now.

    Compilation

    To compile C++ programs, we use the g++ compiler. It works just like gcc, and takes the same arguments such as -o, -c, -g, -I, etc. For example, we can compile our favorite Hello World program with g++, and all works as normal:
    UNIX> cat hw.c
    #include 
    
    int main()
    {
      printf("Hello World\n");
    }
    
    UNIX> make hw
    g++ -g -c hw.c
    g++ -g -o hw hw.o
    UNIX> hw
    Hello World
    UNIX> 
    

    Actually, we can compile most C programs with g++. For example, we can compile concord3.c from the last lecture using g++:
    UNIX> make concord3
    g++ -I/home/bvz/courses/302/include -c concord3.c
    g++ -I/home/bvz/courses/302/include -o concord3 concord3.o /home/bvz/courses/302/objs/libfdr.a
    UNIX> concord3 < hw.c
                int: 3
               main: 3
             printf: 5
              world: 5
    UNIX> 
    

    Classes instead of structs

    In C, you bundle data together into a struct. In C++, you bundle together data and methods into a class. The class has a declaration, which declares the data and methods (methods are functions that implement the operations supported by the class). Typically this declaration is in a header (.h) file. It also has an implementation. This is where the methods are actually implemented. Typically this is in a .cpp file.

    For example, suppose I want to create a class that allows users to display and manipulate labeled rectangles on a screen. A labeled rectangle is a rectangle with a label:

              ----------------------
              |                    |
              | Brad Vander Zanden |
              |                    |
              ----------------------
    
    First, let's think about the types of data the rectangle should store. When you see a labeled rectangle on a screen, it has a position (a left and top), a size (a width and height), a label, and a color. Of course a rectangle may have other properties but for the time being we'll limit ourselves to these six properties. Our rectangle class will need to declare variables to store each of these six properties.

    Next, let's think about the types of operations the rectangle should provide. You can probably think of many different operations that a rectangle might provide but here are a few typically supported by rectangles:

    1. draw: draw should paint a rectangle and its label on the screen.
    2. move(x,y): move should move the rectangle to the desired (x,y) coordinates.
    3. resize(width, height): resize should resize the rectangle to the desired width and height.
    4. setLabel(label): setLabel should set the rectangle's label.
    5. setColor(color): setColor should set the rectangle's color.
    6. containsPt(x,y): containsPt should return true if the rectangle contains the given (x,y) coordinates and false otherwise. This operation allows an application to determine whether the mouse cursor is in the rectangle.
    7. getLeft, getTop, getWidth, getHeight, getLabel, getColor: These operations return the current values of the indicated property.

    In C we'd use a struct to define the rectangle and global functions to define the operations that a rectangle provides. For example, here is a sample file that would declare the rectangle struct and its operations in C:

    typedef struct rectangle {
      int left;
      int top;
      int width;
      int height;
      char *label;
      char *color;
    } *Rectangle;
    
    void draw(Rectangle);
    void move(Rectangle, int x, int y);
    void resize(Rectangle, int width, int height);
    void setLabel(Rectangle, char *);
    void setColor(Rectangle, char *);
    int containsPt(Rectangle, int x, int y);
    int getLeft(Rectangle);
    int getTop(Rectangle);
    int getWidth(Rectangle);
    int getHeight(Rectangle);
    char *getLabel(Rectangle);
    char *getColor(Rectangle);
    

    We'll do the same in C++, but instead of a rectangle struct, we'll use a rectangle class. And we'll use the following two files:


    Rectangle.h

    Here's where we define the rectangle class:
    class Rectangle {
      public:
        Rectangle(int x, int y, int width, int height, string label, string color);
        ~Rectangle();
        void draw();
        void move(int x, int y);
        void resize(int width, int height);
        void setLabel(string );
        void setColor(string );
        int containsPt(int x, int y);
        int getLeft();
        int getTop();
        int getWidth();
        int getHeight();
        string getLabel();
        string getColor();
    
      protected:
        int left;
        int top;
        int width;
        int height;
        string label;
        string color;
    };
    
    This looks a little like a struct. Here's what's going on. You can define both member variables and methods. In the declaration above, left and label are variables, and draw() is a method.

    When you define a class, you can define its members as public, private or protected. We will not use private in this class (this means that you should not use private in your labs). Public means that anyone can use the member. For example, anyone can call draw(). Protected means that the only code that can use the member is the code that implements the class. I'll talk more about this later.

    The above definition is typical for C++ -- all the variables are protected, which means that if code that uses the class wants to get at or set a variable, it has to do it through method calls. For example, if I want a rectangle's label, I can't just access the label variable. Instead, I have to call getLabel(). Is this good or bad? We'll see...

    There are two special methods associated with every class. These are the constructor and destructor methods. The constructor (in this example, Rectangle()) defines special code that needs to be executed when you create an instance of this class. In this case the constructor initializes the rectangle's six variables to the values of the arguments passed to the constructor. The destructor (in this example, ~Rectangle() ) defines code that needs to be executed when you want to destroy (i.e. free) an instance of this class. In this case the destructor will not need to do anything since the string objects representing color and label are statically allocated and therefore will be automatically de-allocated when the object's storage is reclaimed.

    The name of the constructor is always the same as the name of the class and the name of the destructor is always the same as the name of the class with a ~ appended to the front.

    Note the important differences between the C and the C++ header files:

    1. The C header file declares the functions outside the struct whereas the C++ header file declares the functions inside the class. In C++ we say that the functions "belong" to the class. We also often call these functions the class's methods.

    2. The functions in the C header file take a Rectangle as an argument but the functions in the C++ header file do not. The reason is that since the C++ functions are defined inside the class, they already know which rectangle they refer to.

    3. The C++ header file declares its members either public or protected. By declaring its data members protected, the C++ header file prevents code that is not associated with the class from directly accessing or modifying the class's data members.

    4. The C++ header file uses the string data type rather than the char * data type that the C header file uses. The next section describes the string data type.


    The String Class

    C++ provides a string class that allows a programmer to treat a string like a primitive type, such as an int or float. The string class removes many of the annoying features of char *'s, including the frequent problems with memory allocation/deallocation and the need to remember various functions from the string library. You can declare string objects like any other object:
    string first_name;
    
    You can also initialize your object to a string value. Here are several example ways to do so:
    char *s = strdup("howdy");
    string first_name = s;
    string first_name("brad");
    string first_name = "brad";
    string name(first_name);
    string name = first_name;
    
    Note that it is permissable to initialize a string object with either a C-style character string, a char *, or with another string object.

    String objects support the assignment and comparison operations, including =, ==, !=, <, >, <=, and >=. The assignment operator ensures that the storage for the previous string the string object stored is de-allocated and that storage for the new string it will store is allocated properly. It also copies the string. Hence you no longer have to worry about strdup or strcpy. Similarly you do not have to worry about strcmp. Here are some valid uses of the string operators:

    string first_name, last_name, name;
    
    first_name = "brad";
    last_name = "vander zanden";
    name = first_name;
    if (first_name == last_name) { ... }
    if (first_name < last_name) { ... }
    first_name = "nancy";
    

    The + and += operators perform concatenation:

    string first_name, last_name, name;
    
    first_name = "brad";
    last_name = "vander zanden";
    name = first_name + " " + last_name; // name = "brad vander zanden"
    first_name += " " + last_name;  // first_name = "brad vander zanden"
    

    Other helpful methods for the string class include:

    If you want more documentation on the methods provided by the string class consult either a C++ book or do a web search for "C++ strings" and you should find plenty of links.

    Including the String Class in Your Program

    To include the string class in your program place the following two lines in your include section:

    #include <string>
    using namespace std; 
    
    Do not worry about the meaning of the second line, just make sure it follows the include statement for string.

    c_str()

    Sometimes you need to call a C function that is expecting a char *. C functions cannot handle the string class so you need a way to get your string object to give you a char *. c_str() does that for you. For example:

    printf("%s\n", firstname.c_str());
    


    HitDetection.cpp

    Before we look at the implementation of the rectangle class, let's see how a program might make use of the rectangle class. It may bother you that we're going to look at how to use the rectangle class before we look at how we're going to implement the rectangle class. You need to get used to doing this. In particular, you have to become comfortable with using a class even when you do not know how it is implemented. In the real world, a commercial vendor often will not let you see the source code that implements a class because it is a proprietary secret.

    We're going to write a program that performs the following functions:

    1. reads a text file that contains information about a collection of rectangles,
    2. reads the (x,y) coordinate from the command line, and
    3. prints out the labels of all the rectangles that contain that point. The program will print the rectangles ordered either by their x-coordinate or their y-coordinate. The axis on which they are ordered will be determined by a -x or -y flag.

    We'll assume that the text file has the following format:

    name              left   top   width  height  color
    

    Where:

    An example file can be found in rectangle.txt.

    Now take a look at the main procedure for HitDetect.cpp (typically, C++ files have the extension .cpp):

    main(int argc, char **argv)
    {
      Rectangle *r;
      IS is;
      JRB tree, tmp;
      int x, y;
      char sort_order;
    
      // Process the command line:
      // 1) Make sure we have the right number of arguments
      // 2) Make sure that the first argument is a -x or -y flag
      // 3) Make sure that the second and third arguments are integers
    
      if (argc != 4) {
        fprintf(stderr, "usage: HitDetect -x|-y x y\n");
        exit(1);
      }
    
      if (strcmp(argv[1], "-x") != 0 &&
          strcmp(argv[1], "-y") != 0) {
        fprintf(stderr, "usage: HitDetect -x|-y x y\n");
        exit(1);
      }
    
      if ((sscanf(argv[2], "%d", &x) != 1) ||
          (sscanf(argv[3], "%d", &y) != 1)) {
        fprintf(stderr, "usage: HitDetect x y\n");
        fprintf(stderr, "x and y must be integers\n");
        exit(1);
      }
    
      // extract the value of the -x or -y flag. argv[1] = "-x" or "-y" 
      // so argv[1][1] is equal to either 'x' or 'y' */
      sort_order = argv[1][1];
    
      // Initialize the inputstruct and the tree
    
      is = new_inputstruct(NULL);
      tree = make_jrb();
    
      // Read the file, creating rectangles for each line and inserting them
      // into the tree based on their left or top position
    
      while (get_line(is) >= 0) {
        if (is->NF == 0)
          continue; /* blank line of input */
    
        r = readRect(is);
    
        /* insert the rectangle into the tree based on either its left
           or top--the order in which rectangles are stored is determined
           by sort_order */
        switch (sort_order) {
          case 'x': jrb_insert_int(tree, r->getLeft(),   new_jval_v(r)); break;
          case 'y': jrb_insert_int(tree, r->getTop(), new_jval_v(r)); break;
          default:
            fprintf(stderr, "Internal program error -- this shouldn't happen\n");
            exit(1);
        }
      }
    
      // Traverse the tree and print out all the rectangles that contain
      // the given point. 
    
      jrb_traverse(tmp, tree) {
        r = (Rectangle *) tmp->val.v;
        if (r->containsPt(x, y)) {
          if (sort_order == 'x')
    	printf("%4d  %s\n", r->getLeft(), r->getLabel().c_str());
          else
    	printf("%4d  %s\n", r->getTop(), r->getLabel().c_str());
        }
      }
    }  
    
    

    A few comments. Note that we never explicitly use the member variables of the class. This is because we can't -- they have been defined as protected. Instead, we use methods like getLeft() and getTop(). These are called accessor methods because all that they do is access variables. Why do we do this? Because it protects our data structure. Suppose that in the future we decided to store the rectangle's position using the center of the rectangle rather than its left and top. To make this change, further suppose that we changed left and top to center_x and center_y. Since the user must access the rectangle's position through the getLeft and getTop methods, the user's code doesn't have to change when we change the way we store the position of a rectangle. However, if the user's code directly accessed left and top, we'd break the user's code. Specifically the user would now get "variable undefined" messages from the compiler. The user would then have to look at the new rectangle implementation to figure out how to fix their code to work with the new implementation. By protecting the rectangle's data structures from the user, we gain the freedom to change the rectangle's implementation without affecting the user's implementation. In effect we have decoupled the user's implementation from the rectangle's implementation.

    Note also how we call a method. When we want to get a rectangle r's left, we call r->getLeft(). This is a nice thing.

    Finally, we don't bother calling the destructor function, since we simply exit after traversing the tree.


    Creating a Rectangle

    Notice that the HitDetect's main procedure calls readRect to create a new rectangle. The code that creates this new rectangle is shown below:

    Rectangle *readRect(IS is) {
    
      int i, j;
      char label[1000];
      int left, top, width, height;
    
      if (is->NF < 6) {
        fprintf(stderr, "Line %d: Not enough info\n", is->line);
        exit(1);
      }
    
      // the last field is the color and we do not need to convert it
      // since it is already a string. Therefore decrement i by 2 so
      // that it points to the height field.
      i = is->NF - 2;
      if (sscanf(is->fields[i], "%d", &height) != 1) {
        fprintf(stderr, "Line %d: Bad height\n", is->line);
        exit(1);
      }
    
      i--;
      if (sscanf(is->fields[i], "%d", &width) != 1) {
        fprintf(stderr, "Line %d: Bad width\n", is->line);
        exit(1);
      }
    
      i--;
      if (sscanf(is->fields[i], "%d", &top) != 1) {
        fprintf(stderr, "Line %d: Bad top\n", is->line);
        exit(1);
      }
    
      i--;
      if (sscanf(is->fields[i], "%d", &left) != 1) {
        fprintf(stderr, "Line %d: Bad left\n", is->line);
        exit(1);
      }
    
      /* we've finished processing all the words to the right of the label. We
         know that any remaining words must belong to the label so we start
         at the leftmost word and work our way towards the rightmost word. We
         initialize label by copying the leftmost word into label. Thereafter
         we add additional words by repetitively concatenating a blank space and an
         additional word.
      */
      for (j = 0; j < i; j++) {
        if (j > 0) {
          strcat(label, " ");
          strcat(label, is->fields[j]);
        } else { 
          strcpy(label, is->fields[j]);
        }
      }
        
      return new Rectangle(left, top, width, height, label, is->fields[is->NF-1]);
    }
    

    Note how we create a rectangle (the very last statement in readRect). We use the new statement. This allocates a new instance of the given class, and then calls its constructor method on the given argument(s). This is like calling malloc(). If you create an instance of the class, it will exist until you specifically delete it (using the delete construct, that we'll go over later).


    Rectangle.cpp

    Now, in rectangle.cpp, we implement all the methods defined in rectangle.h. I'll go over each implementation. First, the easy ones -- the accessor methods. All they do is return a member variable:
    int Rectangle::getLeft() { return left; }
    int Rectangle::getTop() { return top; }
    int Rectangle::getWidth() { return width; }
    int Rectangle::getHeight() { return height; }
    string Rectangle::getLabel() { return label; }
    string Rectangle::getColor() { return color; }
    
    Again, a few comments. We implement a method using the syntax:
    return-type classname::method-name(arguments)
    {
        ...
    }
    
    Moreover, when we access member variables and methods from within such an implementation, we simply use their names -- for example, getLeft() simply returns left rather than r->left or anything like that (which of course, wouldn't make sense..). Why is that? Well, you can think of the method as being part of the rectangle data structure so it knows about the member variables and can reference them directly.

    Now some slightly more difficult methods--the setting methods:

     
    void Rectangle::draw() {
      // code to draw a rectangle on a screen--too complicated to show here
    }
    
    void Rectangle::move(int x, int y) {
      left = x;
      top = y;
    }
    
    /* The rectangle always needs to be big enough to accommodate the text
       label plus have one character of blank space on either side of the
       label. resize therefore needs to check whether the new width is
       less than this threshold amount, and if so, set the width to this
       threshold amount. Similarly the height needs to be at least 3--one
       character high for the label, and one character of blankspace above
       and below the string. In a real interface we would have to compute
       the pixel height of a character. For simplicity we will assume here
       that a character has a height of 1.
    */
    
    void Rectangle::resize(int wd, int ht) {
      if (wd < (label.length() + 2)) 
        width = label.length() + 2;
      else
        width = wd;
      if (ht < 3) 
        ht = 3;
      else
        height = ht;
    }
    
    /* store the name in the label field. After setting
       the label, make sure that the width of the rectangle is enough to
       accommodate the new label.
    */
    void Rectangle::setLabel(string name) {
      label = name;
      if (width < (label.length() + 2))
        width = label.length() + 2;
    }
    
    /* store the new color in the color field. */
    void Rectangle::setColor(string newColor) {
      color = newColor;
    }
    

    Several of the setting methods are straightforward--they simply set the appropriate variable to the passed in argument. However, two of the methods, resize and setLabel are a little more intricate. These two methods have to ensure that the width and height of the rectangle are big enough to accommodate the label. Note that the resize method does not blindly set the rectangle's width and height to the passed in parameter values. Instead it checks to make sure that the width and height pass minimum threshold values. If they do not, it ignores the parameter values and sets the width and height to the minimum threshold values. Similarly, note that the setLabel method checks the width variable, and if necessary, enlarges the rectangle's width so that it is wide enough to accommodate the label. The resize and setLabel methods show why it is important to prevent the user from directly setting the width, height, and label variables. Certain constraints have to be enforced with respect to these variables and the only way to enforce these constraints is to force the user to call setting methods.

    Next there are two methods that provide some functionality for the rectangle: the draw method and the containsPt method. The draw method is too complicated to show here because it depends on the windows platform (e.g., X or Windows) and requires complicated window system commands. The containsPt method checks to see whether a rectangle contains the given point:

    /* A rectangle contains a point if the x-value of the point lies between
       the left and right sides of the rectangle and the y-value of the
       point lies between the top and bottom of the rectangle */
    int Rectangle::containsPt(int x, int y) {
      return ((left <= x) && (x <= (left + width)) &&
    	  (top <= y) && (y <= (top + height)));
    }
    

    Note that we do not have to compute the result and assign it to a variable. Instead, we can compute and return the result directly.

    Now the most difficult routines: the constructor and the destructor. In the constructor, we set the rectangle's variables to the passed in parameter values and make sure that the width and height are big enough to accommodate the label. Note, in the constructor, the class instance is allocated automatically at the beginning of the method. In the destructor, it is deallocated automatically at the end of the method. Thus, malloc() and free() calls are not necessary for objects.

    Rectangle::Rectangle(int l, int t, int w, int h, string name, string clr) {
      left = l;
      top = t;
    
      /* width must be at least equal to the width of name plus one character
         of blank space on either side of name */
      if (w < (name.length() + 2))
        width = name.length() + 2;
      else
        width = w;
    
      /* height must be at least 3--one character for the label plus one
         character of blank space above and below the label */
      if (h < 3)
        height = 3;
      else
        height = h;
      label = name;
      color = clr;
    }
    
    /* The destructor does not have to do anything because statically declared
       objects, such as the string objects label and color, will be automatically
       de-allocated */
    Rectangle::~Rectangle() {
    }
    
    Ok -- put them all together, and it all works:
    UNIX> make HitDetect
    g++ -g -I/home/bvz/courses/302/libfdr -c HitDetect.cpp
    g++ -g -I/home/bvz/courses/302/libfdr -c rectangle.cpp
    g++ -g -I/home/bvz/courses/302/libfdr -o HitDetect HitDetect.o rectangle.o /home/bvz/courses/302/objs/libfdr++.a
    UNIX> HitDetect -x 100 100 < rectangle.txt
      40  Mickey Mouse
      60  Florida State
    
    UNIX> HitDetect -y 100 100 < rectangle.txt
      30  Florida State
      60  Mickey Mouse
    
    UNIX>