














Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
An overview of c++ classes, their usage, and the concept of inheritance. It covers the basics of classes, member functions, constructors, and data hiding. Additionally, it explains how to create derived classes and the benefits of inheritance. This guide is intended for students and developers looking to deepen their understanding of c++ programming.
Typology: Study notes
1 / 22
This page cannot be seen from the preview
Don't miss anything!















“If programming in Pascal is like being put in a straightjacket, then programming in C is like playing with knives, and programming in C++ is like juggling chainsaws.” Anonymous.
This note introduces some simple C++ concepts and outlines a subset of C++ that is easier to learn and use than the full language. Although we originally wrote this note for explaining the C++ used in the Nachos project, I believe it is useful to anyone learning C++. I assume that you are already somewhat familiar with C concepts like procedures, for loops, and pointers; these are pretty easy to pick up from reading Kernighan and Ritchie’s “The C Programming Language.” I should admit up front that I am quite opinionated about C++, if that isn’t obvious already. I know several C++ purists (an oxymoron perhaps?) who violently disagree with some of the prescriptions contained here; most of the objections are of the form, “How could you have possibly left out feature X?” However, I’ve found from teaching C++ to nearly 1000 undergrads over the past several years that the subset of C++ described here is pretty easy to learn, taking only a day or so for most students to get started. The basic premise of this note is that while object-oriented programming is a useful way to simplify programs, C++ is a wildly over-complicated language, with a host of features that only very, very rarely find a legitimate use. It’s not too far off the mark to say that C++ includes every programming language feature ever imagined, and more. The natural tendency when faced with a new language feature is to try to use it, but in C++ this approach leads to disaster. Thus, we need to carefully distinguish between (i) those concepts that are fundamental (e.g., classes, member functions, constructors) – ones that everyone should know and use, (ii) those that are sometimes but rarely useful (e.g., single inheritance, templates) – ones that beginner programmers should be able to recognize (in case they run across them) but avoid using in their own programs, at least for a while, and (iii) those that are just a bad idea and should be avoided like the plague (e.g., multiple inheritance, exceptions, overloading, references, etc). Of course, all the items in this last category have their proponents, and I will admit that, like the hated goto, it is possible to construct cases when the program would be simpler using a goto or multiple inheritance. However, it is my belief that most programmers will never encounter such cases, and even if you do, you will be much more likely to misuse the feature than properly apply it. For example, I seriously doubt an undergraduate would need any of the features listed under (iii) for any course project (at least at Berkeley this is true). And if you find yourself wanting to use a feature like multiple inheritance, then, my advice is to fully implement your program both with and without the feature, and choose whichever is simpler. Sure, this takes more effort, but pretty soon you’ll know from experience when a feature is useful and when it isn’t, and you’ll be able to skip the dual implementation. A really good way to learn a language is to read clear programs in that language. I have tried to make the Nachos code as readable as possible; it is written in the subset of C++ described in this note. It is a good idea to look over the first assignment as you read this introduction. Of course, your TA’s will answer any questions you may have. You should not need a book on C++ to do the Nachos assignments, but if you are curious, there is a large selection of C++ books at Cody’s and other technical bookstores. (My wife quips that C++ was invented
This article is based on an earlier version written by Wayne Christopher.
to make researchers at Bell Labs rich from writing “How to Program in C++” books.) Most new software development these days is being done in C++, so it is a pretty good bet you’ll run across it in the future. I use Stroustrup’s ”The C++ Programming Language” as a reference manual, although other books may be more readable. I would also recommend Scott Meyer’s “Effective C++” for people just beginning to learn the language, and Coplien’s “Advanced C++” once you’ve been programming in C++ for a couple years and are familiar with the language basics. Also, C++ is continually evolving, so be careful to buy books that describe the latest version (currently 3.0, I think!).
To a large extent, C++ is a superset of C, and most carefully written ANSI C will compile as C++. There are a few major caveats though:
extern int foo(int a, char* b);
The form extern int foo(); means that foo takes no arguments, rather than arguments of an unspecified type and number. In fact, some advise using a C++ compiler even on normal C code, because it will catch errors like misused functions that a normal C compiler will let slide.
extern "C" int foo(int a, char* b);
Otherwise the C++ compiler will alter the name in a strange manner.
Before giving examples of C++ features, I will first go over some of the basic concepts of object-oriented languages. If this discussion at first seems a bit obscure, it will become clearer when we get to some examples.
(a) What to do when you create a new object (the constructor for that object) – in other words, initialize the object’s data.
Inside a member function, one may refer to the members of the class by their names alone. In other words, the class definition creates a scope that includes the member (function and data) definitions. Note that if you are inside a member function, you can get a pointer to the object you were called on by using the variable this. If you want to call another member function on the same object, you do not need to use the this pointer, however. Let’s extend the Stack example to illustrate this by adding a Full() function.
class Stack { public: void Push(int value); // Push an integer, checking for overflow. bool Full(); // Returns TRUE if the stack is full, FALSE otherwise. int top; // Index of the lowest unused position. int stack[10]; // A pointer to an array that holds the contents. };
bool Stack::Full() { return (top == 10); }
Now we can rewrite Push this way:
void Stack::Push(int value) { ASSERT(!Full()); stack[top++] = value; }
We could have also written the ASSERT:
ASSERT(!(this->Full());
but in a member function, the this-> is implicit. The purpose of member functions is to encapsulate the functionality of a type of object along with the data that the object contains. A member function does not take up space in an object of the class.
class Stack { public: void Push(int value); // Push an integer, checking for overflow. bool Full(); // Returns TRUE if the stack is full, FALSE otherwise.
private: int top; // Index of the top of the stack. int stack[10]; // The elements of the stack. };
Before, given a pointer to a Stack object, say s, any part of the program could access s->top, in potentially bad ways. Now, since the top member is private, only a member function, such as Full(), can access it. If any other part of the program attempts to use s->top the compiler will report an error. You can have alternating public: and private: sections in a class. Before you specify either of these, class members are private, thus the above example could have been written:
class Stack { int top; // Index of the top of the stack. int stack[10]; // The elements of the stack. public: void Push(int value); // Push an integer, checking for overflow. bool Full(); // Returns TRUE if the stack is full, FALSE otherwise. };
Which form you prefer is a matter of style, but it’s usually best to be explicit, so that it is obvious what is intended. In Nachos, we make everything explicit. What is not a matter of style: all data members of a class should be private. All operations on data should be via that class’ member functions. Keeping data private adds to the modularity of the system, since you can redefine how the data members are stored without changing how you access them.
struct Stack *s = (struct Stack *) malloc(sizeof (struct Stack)); InitStack(s, 17);
The InitStack() function might take the second argument as the size of the stack to create, and use malloc() again to get an array of 17 integers. The way this is done in C++ is as follows:
Stack *s = new Stack(17);
The new function takes the place of malloc(). To specify how the object should be initialized, one declares a constructor function as a member of the class, with the name of the function being the same as the class name:
class Stack { public: Stack(int sz); // Constructor: initialize variables, allocate space. void Push(int value); // Push an integer, checking for overflow. bool Full(); // Returns TRUE if the stack is full, FALSE otherwise. private:
delete s2;
This will deallocate the object, but first it will call the destructor for the Stack class, if there is one. This destructor is a member function of Stack called ˜Stack():
class Stack { public: Stack(int sz); // Constructor: initialize variables, allocate space. ˜Stack(); // Destructor: deallocate space allocated above. void Push(int value); // Push an integer, checking for overflow. bool Full(); // Returns TRUE if the stack is full, FALSE otherwise. private: int size; // The maximum capacity of the stack. int top; // Index of the lowest unused position. int* stack; // A pointer to an array that holds the contents. };
Stack::˜Stack() { delete [] stack; // delete an array of integers }
The destructor has the job of deallocating the data the constructor allocated. Many classes won’t need destructors, and some will use them to close files and otherwise clean up after themselves.
The destructor for an object is called when the object is deallocated. If the object was created with new, then you must call delete on the object, or else the object will continue to occupy space until the program is over – this is called “a memory leak.” Memory leaks are bad things – although virtual memory is supposed to be unlimited, you can in fact run out of it – and so you should be careful to always delete what you allocate. Of course, it is even worse to call delete too early – delete calls the destructor and puts the space back on the heap for later re-use. If you are still using the object, you will get random and non-repeatable results that will be very difficult to debug. In my experience, using data that has already been deleted is major source of hard-to-locate bugs in student (and professional) programs, so hey, be careful out there!
If the object is an automatic, allocated on the execution stack of a function, the destructor will be called and the space deallocated when the function returns; in the test() example above, s1 will be deallocated when test() returns, without you having to do anything.
In Nachos, we always explicitly allocate and deallocate objects with new and delete, to make it clear when the constructor and destructor is being called. For example, if an object contains another object as a member variable, we use new to explicitly allocated and initialize the member variable, instead of implicitly allocating it as part of the containing object. C++ has strange, non-intuitive rules for the order in which the constructors and destructors are called when you implicitly allocate and deallocate objects. In practice, although simpler, explicit allocation is slightly slower and it makes it more likely that you will forget to deallocate an object (a bad thing!), and so some would disagree with this approach.
When you deallocate an array, you have to tell the compiler that you are deallocating an array, as opposed to a single element in the array. Hence to delete the array of integers in Stack::˜Stack:
delete [] stack;
Here are a few other C++ features that are useful to know.
class Stack { ... bool Full() { return (top == size); }; ... };
There are two motivations for inlines: convenience and performance. If overused, inlines can make your code more confusing, because the implementation for an object is no longer in one place, but spread between the .h and .c files. Inlines can sometimes speed up your code (by avoiding the overhead of a procedure call), but that shouldn’t be your principal concern as a student (rather, at least to begin with, you should be most concerned with writing code that is simple and bug free). Not to mention that inlining sometimes slows down a program, since the object code for the function is duplicated wherever the function is called, potentially hurting cache performance.
for (int i = 0; i < 10; i++) ;
Depending on your compiler, however, the variable i may still visible after the end of the for loop, however, which is not what one might expect or desire.
class Stack { ... bool Full() const; // Full() never modifies member data ... };
Nachos contains a few examples of the correct use of inheritance and templates, but realize that Nachos does not use them everywhere. In fact, if you get confused by this section, don’t worry, you don’t need to use any of these features in order to do the Nachos assignments. I omit a whole bunch of details; if you find yourself making widespread use of inheritance or templates, you should consult a C++ reference manual for the real scoop. This is meant to be just enough to get you started, and to help you identify when it would be appropriate to use these features and thus learn more about them!
Inheritance captures the idea that certain classes of objects are related to each other in useful ways. For example, lists and sorted lists have quite similar behavior – they both allow the user to insert, delete, and find elements that are on the list. There are two benefits to using inheritance:
4.1.1 Shared Behavior
Let me use our Stack example to illustrate the first of these. Our Stack implementation above could have been implemented with linked lists, instead of an array. Any code using a Stack shouldn’t care which implemen- tation is being used, except that the linked list implementation can’t overflow. (In fact, we could also change the array implementation to handle overflow by automatically resizing the array as items are pushed on the stack.) To allow the two implementations to coexist, we first define an abstract Stack, containing just the public member functions, but no data.
class Stack { public: Stack(); virtual ˜Stack(); // deallocate the stack virtual void Push(int value) = 0; // Push an integer, checking for overflow. virtual bool Full() = 0; // Is the stack is full? };
// For g++, need these even though no data to initialize. Stack::Stack {} Stack::˜Stack() {}
The Stack definition is called a base class or sometimes a superclass. We can then define two different derived classes , sometimes called subclasses which inherit behavior from the base class. (Of course, inheri- tance is recursive – a derived class can in turn be a base class for yet another derived class, and so on.) Note that I have prepended the functions in the base class is prepended with the keyword virtual, to signify that they can be redefined by each of the two derived classes. The virtual functions are initialized to zero, to tell the compiler that those functions must be defined by the derived classes. Here’s how we could declare the array-based and list-based implementations of Stack. The syntax : public Stack signifies that both ArrayStack and ListStack are kinds of Stacks, and share the same behavior as the base class.
class ArrayStack : public Stack { // the same as in Section 2 public: ArrayStack(int sz); // Constructor: initialize variables, allocate space. ˜ArrayStack(); // Destructor: deallocate space allocated above. void Push(int value); // Push an integer, checking for overflow. bool Full(); // Returns TRUE if the stack is full, FALSE otherwise. private: int size; // The maximum capacity of the stack. int top; // Index of the lowest unused position. int *stack; // A pointer to an array that holds the contents. };
class ListStack : public Stack { public: ListStack(); ˜ListStack(); void Push(int value); bool Full(); private: List *list; // list of items pushed on the stack };
ListStack::ListStack() { list = new List; }
ListStack::˜ListStack() { delete list; }
void ListStack::Push(int value) { list->Prepend(value); }
bool ListStack::Full() { return FALSE; // this stack never overflows! }
The neat concept here is that I can assign pointers to instances of ListStack or ArrayStack to a variable of type Stack, and then use them as if they were of the base type.
protected: Stack(); // initialize data private: int numPushed; };
Stack::Stack() { numPushed = 0; }
void Stack::Push(int value) { numPushed++; }
int Stack::NumPushed() { return numPushed; }
We can then modify both ArrayStack and ListStack to make use the new behavior of Stack. I’ll only list one of them here:
class ArrayStack : public Stack { public: ArrayStack(int sz); ˜ArrayStack(); void Push(int value); bool Full(); private: int size; // The maximum capacity of the stack. int *stack; // A pointer to an array that holds the contents. };
ArrayStack::ArrayStack(int sz) : Stack() { size = sz; stack = new int[size]; // Let’s get an array of integers. }
void ArrayStack::Push(int value) { ASSERT(!Full()); stack[NumPushed()] = value; Stack::Push(); // invoke base class to increment numPushed }
There are a few things to note:
ArrayStack::ArrayStack(int sz) : Stack()
The same thing applies to destructors. There are special rules for which get called first – the construc- tor/destructor for the base class or the constructor/destructor for the derived class. All I should say is, it’s a bad idea to rely on whatever the rule is – more generally, it is a bad idea to write code which requires the reader to consult a manual to tell whether or not the code works!
ArrayStack *s = new ArrayStack(17);
ASSERT(s->NumPushed() == 0); // should be initialized to 0
Stack *s = new ArrayStack(17);
if (!s->Full()) // ArrayStack::Full s->Push(5); // ArrayStack::Push
void ArrayStack::Push(int value) { ... Stack::Push(); // invoke base class to increment numPushed }
private: int size; // The maximum capacity of the stack. int top; // Index of the lowest unused position. T *stack; // A pointer to an array that holds the contents. };
To define a template, we prepend the keyword template to the class definition, and we put the param- eterized type for the template in angle brackets. If we need to parameterize the implementation with two or more types, it works just like an argument list: template <class T, class S>. We can use the type parameters elsewhere in the definition, just like they were normal types. When we provide the implementation for each of the member functions in the class, we also have to declare them as templates, and again, once we do that, we can use the type parameters just like normal types:
// template version of Stack::Stack template
// template version of Stack::Push template
Creating an object of a template class is similar to creating a normal object:
void test() { Stack
s1.Push(5); s2->Push(’z’); delete s2; }
Everything operates as if we defined two classes, one called Stack
debuggers don’t really understand templates very well. Nevertheless, it is easier to debug a template than two nearly identical implementations that differ only in their types. So the best advice is – don’t make a class into a template unless there really is a near term use for the template. And if you do need to implement a template, implement and debug a non-template version first. Once that is working, it won’t be hard to convert it to a template. Then all you have to worry about code explosion – e.g., your program’s object code is now megabytes because of the 15 copies of the hash table/list/... routines, one for each kind of thing you want to put in a hash table/list/... (Remember, you have an unhelpful compiler!)
Despite the length of this note, there are numerous features in C++ that I haven’t explained. I’m sure each fea- ture has its advocates, but despite programming in C and C++ for over 15 years, I haven’t found a compelling reason to use them in any code that I’ve written (outside of a programming language class!) Indeed, there is a compelling reason to avoid using these features – they are easy to misuse, resulting in programs that are harder to read and understand instead of easier to understand. In most cases, the features are also redundant – there are other ways of accomplishing the same end. Why have two ways of doing the same thing? Why not stick with the simpler one? I do not use any of the following features in Nachos. If you use them, caveat hacker.
Was the intent really x == y? After all, it’s pretty easy to mistakenly leave off the extra equals sign. By never using assignment within a conditional, you can tell by code inspection whether you’ve made a mistake.
Even if you follow the approach I’ve outlined above, it is still as easy to write unreadable and undebuggable code in C++ as it is in C, and perhaps easier, given the more powerful features the language provides. For the Nachos project, and in general, we suggest you adhere to the following guidelines (and tell us if you catch us breaking them):
#ifndef STACK_H #define STACK_H
class Stack { ... };
#endif
Sometimes this will not be enough, and you will have a circular dependency. For example, you might have a .h file that uses a definition from one .h file, but also defines something needed by that .h file.
In this case, you will have to do something ad-hoc. One thing to realize is that you don’t always have to completely define a class before it is used. If you only use a pointer to class Stack and do not access any member functions or data from the class, you can write, in lieu of including stack.h:
class Stack;
This will tell the compiler all it needs to know to deal with the pointer. In a few cases this won’t work, and you will have to move stuff around or alter your definitions.
The Makefiles we will give you works only with the GNU version of make, called “gmake”. You may want to put “alias make gmake” in your .cshrc file. You should use gdb to debug your program rather than dbx. Dbx doesn’t know how to decipher C++ names, so you will see function names like Run__9SchedulerP6Thread. On the other hand, in GDB (but not DBX) when you do a stack backtrace when in a forked thread (in homework 1), after printing out the correct frames at the top of the stack, the debugger will sometimes go into a loop printing the lower-most frame (ThreadRoot), and you have to type control-C when it says “more?”. If you understand assembly language and can fix this, please let me know.
We’ve provided the complete, working code for the stack example. You should read through it and play around with it to make sure you understand the features of C++ described in this paper. To compile the simple stack test, type make all – this will compile the simple stack test (stack.cc), the inherited stack test (inheritstack.cc), and the template version of stacks (templatestack.cc).