UNIX> cat hw.c #includeint main() { printf("Hello World\n"); } UNIX> make hw g++ -g -c hw.c g++ -g -o hw hw.o UNIX> hw Hello World UNIX>
UNIX> make concord3
g++ -I/home/parker/courses/cs302/include -c concord3.c
g++ -I/home/parker/courses/cs302/include -o concord3 concord3.o /home/parker/courses/cs302/objs/libfdr++.a
UNIX> concord3 < hw.c
int: 3
main: 3
printf: 5
world: 5
UNIX>
Note, when you link programs compiled with g++, you should use the
libraries in /home/parker/courses/cs302/objs/libfdr++.a, and not
/home/parker/courses/cs302/objs/libfdr.
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:
----------------
| |
| Mickey Mouse |
| |
----------------
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:
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:
class Rectangle {
public:
Rectangle(int x, int y, int width, int height, char *label, char *color);
~Rectangle();
void draw();
void move(int x, int y);
void resize(int width, int height);
void setLabel(char *);
void setColor(char *);
int containsPt(int x, int y);
int getLeft();
int getTop();
int getWidth();
int getHeight();
char *getLabel();
char *getColor();
protected:
int left;
int top;
int width;
int height;
char *label;
char *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 needs to free the memory pointed to by the label and color fields.
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:
We're going to write a program that performs the following functions:
We'll assume that the text file has the following format:
name left top width height color |
Where:
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());
else
printf("%4d %s\n", r->getTop(), r->getLabel());
}
}
}
A few comments. Note that this program isn't completely in the C++ style, since we're still using the C version of fields, lists, and rb-trees. We'll change that at a future date. Also 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.
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).
int Rectangle::getLeft() { return left; }
int Rectangle::getTop() { return top; }
int Rectangle::getWidth() { return width; }
int Rectangle::getHeight() { return height; }
char *Rectangle::getLabel() { return label; }
char *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 < (strlen(label) + 2))
width = strlen(label) + 2;
else
width = wd;
if (ht < 3)
ht = 3;
else
height = ht;
}
/* make a copy of name and store it in the label field. First free
the old label so that we do not have a memory leak. After setting
the label, make sure that the width of the rectangle is enough to
accommodate the new label.
*/
void Rectangle::setLabel(char *name) {
free(label);
label = strdup(name);
if (width < (strlen(label) + 2))
width = strlen(label) + 2;
}
/* make a copy of the new color and store it in the color field. First
free the old color so that we do not have a memory leak. */
void Rectangle::setColor(char *newColor) {
free(color);
color = strdup(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 method 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.
In the constructor, I do call strdup() twice -- once for label and once for color. Thus, in the destructor, I have to call free().
Rectangle::Rectangle(int l, int t, int w, int h, char *name, char *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 < (strlen(name) + 2))
width = strlen(name) + 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 = strdup(name);
color = strdup(clr);
}
Rectangle::~Rectangle() {
free(label);
free(color);
}
Ok -- put them all together (here's the makefile), and it all works:
UNIX> make HitDetect g++ -g -I/home/parker/courses/cs302/include -c HitDetect.cpp g++ -g -I/home/parker/courses/cs302/include -c rectangle.cpp g++ -g -I/home/parker/courses/cs302/include -o HitDetect HitDetect.o rectangle.o /home/parker/courses/cs302/objs/libfdr++.a UNIX> HitDetect -x 100 100 < rectangle.txt 40 Mickey Mouse 60 Florida State Seminoles UNIX> HitDetect -y 100 100 < rectangle.txt 30 Florida State Seminoles 60 Mickey Mouse UNIX>
Now, just a few more random notes. We've protected the member variables, but you should note that in order to get total protection we had to copy the strings that were stored in the label and color variables. Yes, other programs cannot access them directly, but if we'd stored the pointers to the parameter strings, they can access them indirectly. In particular, if someone would modify the memory to which the name or clr parameters point, then label and color would appear to change as well since they would point to the same memory.