C++ Programming: Understanding Classes, Inheritance, and Shared Implementation, Lecture notes of Computer science

An introduction to C++ programming, focusing on classes, inheritance, and shared implementation. It covers the concepts of public and private members, the use of classes for organizing programs, and the benefits of inheritance for sharing code. The document also discusses the importance of using assertions and the proper use of new and delete operators.

Typology: Lecture notes

2021/2022

Uploaded on 09/12/2022

ekassh
ekassh 🇺🇸

4.7

(23)

272 documents

1 / 29

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
A Quick Introduction to C++
Tom Anderson
\If programming in Pascal is like being put in a straightjacket, then program-
ming in C is like playing with knives, and programming in C++ is like juggling
chainsaws."
Anonymous.
1 Introduction
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 pro ject, 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 nd a legitimate use. It's not to o far o 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
This article is based on an earlier version written by Wayne Christopher.
1
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d

Partial preview of the text

Download C++ Programming: Understanding Classes, Inheritance, and Shared Implementation and more Lecture notes Computer science in PDF only on Docsity!

A Quick Intro duction to C++

Tom Anderson

\If programming in Pascal is like b eing put in a straightjacket, then program- ming in C is like playing with knives, and programming in C++ is like juggling chainsaws." Anonymous.

1 Intro duction

This note intro duces 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 pro ject, I b elieve it is useful to anyone learning C++. I assume that you are already somewhat familiar with C concepts like pro cedures, for lo ops, and p ointers; 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 ab out C++, if that isn't obvious already. I know several C++ purists (an oxymoron p erhaps?) who violently disagree with some of the prescriptions contained here; most of the ob jections are of the form, \How could you have p ossibly 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++ describ ed 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 ob ject-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 nd a legitimate use. It's not to o far o 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 b etween (i) those concepts that are fundamental (e.g., classes, memb er functions, constructors) { ones that everyone should know and use, (ii) those that are sometimes but rarely useful (e.g., single inheritance, templates) { ones that b eginner programmers should b e 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 b e avoided like the plague (e.g., multiple inheritance, exceptions, overloading, references, etc). Of course, all the items in this last category have their prop onents, and I will admit that, like the hated goto, it is p ossible to construct cases when the program would b e simpler

This article is based on an earlier version written by Wayne Christopher.

using a goto or multiple inheritance. However, it is my b elief that most programmers will never encounter such cases, and even if you do, you will b e much more likely to misuse the feature than prop erly apply it. For example, I seriously doubt an undergraduate would need any of the features listed under (iii) for any course pro ject (at least at Berkeley this is true). And if you nd yourself wanting to use a feature like multiple inheritance, then, my advice is to fully implement your program b oth with and without the feature, and cho ose whichever is simpler. Sure, this takes more e ort, but pretty so on you'll know from exp erience when a feature is useful and when it isn't, and you'll b e able to skip the dual implementation. A really go o d way to learn a language is to read clear programs in that language. I have tried to make the Nachos co de as readable as p ossible; it is written in the subset of C++ describ ed in this note. It is a go o d idea to lo ok over the rst assignment as you read this intro duction. Of course, your TA's will answer any questions you may have. You should not need a b o ok on C++ to do the Nachos assignments, but if you are curious, there is a large selection of C++ b o oks at Co dy's and other technical b o okstores. (My wife quips that C++ was invented to make researchers at Bell Labs rich from writing \How to Program in C++" b o oks.) Most new software development these days is b eing done in C++, so it is a pretty go o d b et you'll run across it in the future. I use Stroustrup's "The C++ Programming Language" as a reference manual, although other b o oks may b e more readable. I would also recommend Scott Meyer's \E ective C++" for p eople just b eginning to learn the language, and Coplien's \Advanced C++" once you've b een programming in C++ for a couple years and are familiar with the language basics. Also, C++ is continually evolving, so b e careful to buy b o oks that describ e the latest version (currently 3.0, I think!).

2 C in C++

To a large extent, C++ is a sup erset of C, and most carefully written ANSI C will compile as C++. There are a few ma jor caveats though:

  1. All functions must b e declared b efore they are used, rather than defaulting to typ e int.
  2. All function declarations and de nition headers must use new-style declarations, e.g.,

extern int foo(int a, char* b);

The form extern int foo(); means that foo takes no arguments, rather than argu- ments of an unsp eci ed typ e and numb er. In fact, some advise using a C++ compiler even on normal C co de, b ecause it will catch errors like misused functions that a normal C compiler will let slide.

  1. If you need to link C ob ject les together with C++, when you declare the C functions for the C++ les, they must b e done like this:
  1. Memb er functions. Here is a (partial) example of a class with a memb er function and some data memb ers:

class Stack { public: void Push(int value); // Push an integer, checking for overflow. int top; // Index of the top of the stack. int stack[10]; // The elements of the stack. };

void Stack::Push(int value) { ASSERT(top < 10); // stack should never overflow stack[top++] = value; }

This class has two data memb ers, top and stack, and one memb er function, Push. The notation class::function denotes the function memb er of the class class. (In the style we use, most function names are capitalized.) The function is de ned b eneath it. As an aside, note that we use a call to ASSERT to check that the stack hasn't over owed; ASSERT drops into the debugger if the condition is false. It is an extremely go o d idea for you to use ASSERT statements lib erally throughout your co de to do cument assumptions made by your implementation. Better to catch errors automatically via ASSERTs than to let them go by and have your program overwrite random lo cations. In actual usage, the de nition of class Stack would typically go in the le stack.h and the de nitions of the memb er functions, like Stack::Push, would go in the le stack.cc. If we have a p ointer to a Stack ob ject called s, we can access the top element as s->top, just as in C. However, in C++ we can also call the memb er function using the following syntax:

s->Push(17);

Of course, as in C, s must p oint to a valid Stack ob ject. Inside a memb er function, one may refer to the memb ers of the class by their names alone. In other words, the class de nition creates a scop e that includes the memb er (function and data) de nitions. Note that if you are inside a memb er function, you can get a p ointer to the ob ject you were called on by using the variable this. If you want to call another memb er function on the same ob ject, you do not need to use the this p ointer, 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. };

Before, given a p ointer to a Stack ob ject, say s, any part of the program could access s->top, in p otentially bad ways. Now, since the top memb er is private, only a memb er function, such as Full(), can access it. If any other part of the program attempts to use s->top the compiler will rep ort an error. You can have alternating public: and private: sections in a class. Before you sp ecify either of these, class memb ers are private, thus the ab ove example could have b een 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 b est to b e explicit, so that it is obvious what is intended. In Nachos, we make everything explicit. What is not a matter of style: all data memb ers of a class should b e private. All op erations on data should b e via that class' memb er functions. Keeping data private adds to the mo dularity of the system, since you can rede ne how the data memb ers are stored without changing how you access them.

  1. Constructors and the op erator new. In C, in order to create a new ob ject of typ e Stack, one might write:

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 sp ecify how the ob ject should b e initialized, one declares a constructor function as a memb er of the class, with the name of the function b eing 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: 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(int sz) { size = sz; top = 0; stack = new int[size]; // Let's get an array of integers. }

There are a few things going on here, so we will describ e them one at a time.

The new op erator automatically creates (i.e. allo cates) the ob ject and then calls the constructor function for the new ob ject. This same sequence happ ens even if, for instance, you declare an ob ject as an automatic variable inside a function or blo ck { the compiler allo cates space for the ob ject on the stack, and calls the constructor function on it.

In this example, we create two stacks of di erent sizes, one by declaring it as an automatic variable, and one by using new.

void test() { Stack s1(17); Stack* s2 = new Stack(23); }

Note there are two ways of providing arguments to constructors: with new, you put the argument list after the class name, and with automatic or global variables, you put them after the variable name.

It is crucial that you always de ne a constructor for every class you de ne, and that the constructor initialize every data memb er of the class. If you don't de ne your own constructor, the compiler will automatically de ne one for you, and b elieve me, it won't do what you want (\the unhelpful compiler"). The data memb ers will b e initialized to random, unrep eatable values, and while your program may work anyway, it might not the next time you recompile (or vice versa!).

The destructor for an ob ject is called when the ob ject is deallo cated. If the ob ject was created with new, then you must call delete on the ob ject, or else the ob ject will continue to o ccupy space until the program is over { this is called \a memory leak." Memory leaks are bad things { although virtual memory is supp osed to b e unlimited, you can in fact run out of it { and so you should b e careful to always delete what you allo cate. Of course, it is even worse to call delete to o early { delete calls the destructor and puts the space back on the heap for later re-use. If you are still using the ob ject, you will get random and non-rep eatable results that will b e very dicult to debug. In my exp erience, using data that has already b een deleted is ma jor source of hard-to-lo cate bugs in student (and professional) programs, so hey, b e careful out there! If the ob ject is an automatic, allo cated on the execution stack of a function, the destructor will b e called and the space deallo cated when the function returns; in the test() example ab ove, s1 will b e deallo cated when test() returns, without you having to do anything. In Nachos, we always explicitly allo cate and deallo cate ob jects with new and delete, to make it clear when the constructor and destructor is b eing called. For example, if an ob ject contains another ob ject as a memb er variable, we use new to explicitly allo cated and initialize the memb er variable, instead of implicitly allo cating it as part of the containing ob ject. C++ has strange, non-intuitive rules for the order in which the constructors and destructors are called when you implicitly allo cate and deallo cate ob jects. In practice, although simpler, explicit allo cation is slightly slower and it makes it more likely that you will forget to deallo cate an ob ject (a bad thing!), and so some would disagree with this approach. When you deallo cate an array, you have to tell the compiler that you are deallo cating an array, as opp osed to a single element in the array. Hence to delete the array of integers in Stack::~Stack:

delete [] stack;

3.2 Other Basic C++ Features

Here are a few other C++ features that are useful to know.

  1. When you de ne a class Stack, the name Stack b ecomes usable as a typ e name as if created with typedef. The same is true for enums.
  2. You can de ne functions inside of a class de nition, whereup on they b ecome inline functions, which are expanded in the b o dy of the function where they are used. The rule of thumb to follow is to only consider inlining one-line functions, and even then do so rarely. As an example, we could make the Full routine an inline.

class Stack { ... bool Full() { return (top == size); }; ... };

There are two motivations for inlines: convenience and p erformance. If overused, inlines can make your co de more confusing, b ecause the implementation for an ob ject is no longer in one place, but spread b etween the .h and .c les. Inlines can sometimes sp eed up your co de (by avoiding the overhead of a pro cedure call), but that shouldn't b e your principal concern as a student (rather, at least to b egin with, you should b e most concerned with writing co de that is simple and bug free). Not to mention that inlining sometimes slows down a program, since the ob ject co de for the function is duplicated wherever the function is called, p otentially hurting cache p erformance.

  1. Inside a function b o dy, you can declare some variables, execute some statements, and then declare more variables. This can make co de a lot more readable. In fact, you can even write things like:

for (int i = 0; i < 10; i++) ;

Dep ending on your compiler, however, the variable i may still visible after the end of the for lo op, however, which is not what one might exp ect or desire.

  1. Comments can b egin with the characters // and extend to the end of the line. These are usually more handy than the /* */ style of comments.
  2. C++ provides some new opp ortunities to use the const keyword from ANSI C. The basic idea of const is to provide extra information to the compiler ab out how a variable or function is used, to allow it to ag an error if it is b eing used improp erly. You should always lo ok for ways to get the compiler to catch bugs for you. After all, which takes less time? Fixing a compiler- agged error, or chasing down the same bug using gdb? For example, you can declare that a memb er function only reads the memb er data, and never mo di es the ob ject:

class Stack { ... bool Full() const; // Full() never modifies member data ... };

As in C, you can use const to declare that a variable is never mo di ed:

a naming convention which mimics C++, for example, naming routines StackFull() and StackPush(). However, the features I'm ab out to describ e do require a paradigm shift { there is no simple translation from them into a normal C program. The b ene t will b e that, in some circumstances, you will b e able to write generic co de that works with multiple kinds of ob jects. Nevertheless, I would advise a b eginning C++ programmer against trying to use these features, b ecause you will almost certainly misuse them. It's p ossible (even easy!) to write completely inscrutable co de using inheritance and/or templates. Although you might nd it amusing to write co de that is imp ossible for your graders to understand, I assure you they won't nd it amusing at all, and will return the favor when they assign grades. In industry, a high premium is placed on keeping co de simple and readable. It's easy to write new co de, but the real cost comes when you try to keep it working, even as you add new features to it. Nachos contains a few examples of the correct use of inheritance and templates, but realize that Nachos do es 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 nd yourself making widespread use of inheritance or templates, you should consult a C++ reference manual for the real sco op. This is meant to b e just enough to get you started, and to help you identify when it would b e appropriate to use these features and thus learn more ab out them!

4.1 Inheritance

Inheritance captures the idea that certain classes of ob jects are related to each other in useful ways. For example, lists and sorted lists have quite similar b ehavior { they b oth allow the user to insert, delete, and nd elements that are on the list. There are two b ene ts to using inheritance:

  1. You can write generic co de that do esn't care exactly which kind of ob ject it is manip- ulating. For example, inheritance is widely used in windowing systems. Everything on the screen (windows, scroll bars, titles, icons) is its own ob ject, but they all share a set of memb er functions in common, such as a routine Repaint to redraw the ob ject onto the screen. This way, the co de to repaint the entire screen can simply call the Repaint function on every ob ject on the screen. The co de that calls Repaint do esn't need to know which kinds of ob jects are on the screen, as long as each implements Repaint.
  2. You can share pieces of an implementation b etween two ob jects. For example, if you were to implement b oth lists and sorted lists in C, you'd probably nd yourself rep eating co de in b oth places { in fact, you might b e really tempted to only implement sorted lists, so that you only had to debug one version. Inheritance provides a way to re-use co de b etween nearly similar classes. For example, given an implementation of a list class, in C++ you can implement sorted lists by replacing the insert memb er function { the other functions, delete, isFull, print, all remain the same.

4.1.1 Shared Behavior

Let me use our Stack example to illustrate the rst of these. Our Stack implementation ab ove could have b een implemented with linked lists, instead of an array. Any co de using a Stack shouldn't care which implementation is b eing used, except that the linked list implementation can't over ow. (In fact, we could also change the array implementation to handle over ow by automatically resizing the array as items are pushed on the stack.) To allow the two implementations to co exist, we rst de ne an abstract Stack, containing just the public memb er 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 de nition is called a base class or sometimes a superclass. We can then de ne two di erent derived classes, sometimes called subclasses which inherit b ehavior from the base class. (Of course, inheritance is recursive { a derived class can in turn b e a base class for yet another derived class, and so on.) Note that I have prep ended the functions in the base class is prep ended with the keyword virtual, to signify that they can b e rede ned by each of the two derived classes. The virtual functions are initialized to zero, to tell the compiler that those functions must b e de ned by the derived classes. Here's how we could declare the array-based and list-based implementations of Stack. The syntax : public Stack signi es that b oth ArrayStack and ListStack are kinds of Stacks, and share the same b ehavior 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.

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 p ointers to instances of ListStack or ArrayStack to a variable of typ e Stack, and then use them as if they were of the base typ e.

Stack *s1 = new ListStack; Stack *s2 = new ArrayStack(17);

if (!stack->Full()) s1->Push(5); if (!s2->Full()) s2->Push(6);

delete s1; delete s2;

The compiler automatically invokes ListStack op erations for s1, and ArrayStack op- erations for s2; this is done by creating a pro cedure table for each ob ject, where derived ob jects override the default entries in the table de ned by the base class. To the co de ab ove, it invokes the op erations Full, Push, and delete by indirection through the pro cedure table, so that the co de do esn't need to know which kind of ob ject it is. In this example, since I never create an instance of the abstract class Stack, I do not need to implement its functions. This might seem a bit strange, but rememb er that the derived classes are the various implementations of Stack, and Stack serves only to re ect the shared b ehavior b etween the di erent implementations. Also note that the destructor for Stack is a virtual function but the constructor is not. Clearly, when I create an ob ject, I have to know which kind of ob ject it is, whether ArrayStack or ListStack. The compiler makes sure that no one creates an instance of the abstract Stack by mistake { you cannot instantiate any class whose virtual functions are not completely de ned (in other words, if any of its functions are set to zero in the class de nition). But when I deallo cate an ob ject, I may no longer know its exact typ e. In the ab ove co de, I want to call the destructor for the derived ob ject, even though the co de only knows that I am deleting an ob ject of class Stack. If the destructor were not virtual, then the compiler would invoke Stack's destructor, which is not at all what I want. This is an easy mistake to make (I made it in the rst draft of this article!) { if you don't de ne a destructor for the abstract class, the compiler will de ne one for you implicitly (and by the way, it won't b e

virtual, since you have a real ly unhelpful compiler). The result for the ab ove co de would b e a memory leak, and who knows how you would gure that out!

4.1.2 Shared Implementation

What ab out sharing co de, the other reason for inheritance? In C++, it is p ossible to use memb er functions of a base class in its derived class. (You can also share data b etween a base class and derived classes, but this is a bad idea for reasons I'll discuss later.) Supp ose that I wanted to add a new memb er function, NumberPushed(), to b oth imple- mentations of Stack. The ArrayStack class already keeps count of the numb er of items on the stack, so I could duplicate that co de in ListStack. Ideally, I'd like to b e able to use the same co de in b oth places. With inheritance, we can move the counter into the Stack class, and then invoke the base class op erations from the derived class to up date the counter.

class Stack { public: virtual ~Stack(); // deallocate data virtual void Push(int value); // Push an integer, checking for overflow. virtual bool Full() = 0; // return TRUE if full int NumPushed(); // how many are currently on the stack? 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 mo dify b oth ArrayStack and ListStack to make use the new b ehavior of Stack. I'll only list one of them here:

class ArrayStack : public Stack { public:

has an unde ned virtual functions, Push. By de ning Stack::Stack as protected, you are safe even if someone comes along later and de nes Stack::Push. Note however that I made Stack's data memb er private, not protected. Although there is some debate on this p oint, as a rule of thumb you should never allow one class to see directly access the data in another, even among classes related by inher- itance. Otherwise, if you ever change the implementation of the base class, you will have to examine and change all the implementations of the derived classes, violating mo dularity.

  1. The interface for a derived class automatically includes all functions de ned for its base class, without having to explicitly list them in the derived class. Although we didn't de ne NumPushed() in ArrayStack, we can still call it for those ob jects:

ArrayStack *s = new ArrayStack(17);

ASSERT(s->NumPushed() == 0); // should be initialized to 0

  1. Conversely, even though we have de ned a routine Stack::Push(), b ecause it is declared as virtual, if we invoke Push() on an ArrayStack ob ject, we will get ArrayStack's version of Push:

Stack *s = new ArrayStack(17);

if (!s->Full()) // ArrayStack::Full s->Push(5); // ArrayStack::Push

  1. Stack::NumPushed() is not virtual. That means that it cannot b e re-de ned by Stack's derived classes. Some p eople b elieve that you should mark al l functions in a base class as virtual; that way, if you later want to implement a derived class that rede nes a function, you don't have to mo dify the base class to do so.
  2. Memb er functions in a derived class can explicitly invoke public or protected functions in the base class, by the full name of the function, Base::Function(), as in:

void ArrayStack::Push(int value) { ... Stack::Push(); // invoke base class to increment numPushed }

Of course, if we just called Push() here (without prep ending Stack::, the compiler would think we were referring to ArrayStack's Push(), and so that would recurse, which is not exactly what we had in mind here.

Whew! Inheritance in C++ involves lots and lots of details. But it's real downside is that it tends to spread implementation details across multiple les { if you have a deep inheritance tree, it can take some serious digging to gure out what co de actually executes when a memb er function is invoked. So the question to ask yourself b efore using inheritance is: what's your goal? Is it to write your programs with the fewest numb er of characters p ossible? If so, inheritance is really useful, but so is changing all of your function and variable names to b e one letter long { "a", "b", "c" { and once you run out of lower case ones, start using upp er case, then two character variable names: "XX XY XZ Ya ..." (I'm joking here.) Needless to say, it is really easy to write unreadable co de using inheritance. So when is it a go o d idea to use inheritance and when should it b e avoided? My rule of thumb is to only use it for representing shared behavior b etween ob jects, and to never use it for representing shared implementation. With C++, you can use inheritance for b oth concepts, but only the rst will lead to truly simpler implementations. To illustrate the di erence b etween shared b ehavior and shared implementation, supp ose you had a whole bunch of di erent kinds of ob jects that you needed to put on lists. For example, almost everything in an op erating system go es on a list of some sort: bu ers, threads, users, terminals, etc. A very common approach to this problem (particularly among p eople new to ob ject- oriented programming) is to make every ob ject inherit from a single base class Object, which contains the forward and backward p ointers for the list. But what if some ob ject needs to go on multiple lists? The whole scheme breaks down, and it's b ecause we tried to use inheritance to share implementation (the co de for the forward and backward p ointers) instead of to share b ehavior. A much cleaner (although slightly slower) approach would b e to de ne a list implementation that allo cated forward/backward p ointers for each ob ject that gets put on a list. In sum, if two classes share at least some of the same memb er function signatures { that is, the same b ehavior, and if there's co de that only relies on the shared b ehavior, then there may b e a b ene t to using inheritance. In Nachos, lo cks don't inherit from semaphores, even though lo cks are implemented using semaphores. The op erations on semaphores and lo cks are di erent. Instead, inheritance is only used for various kinds of lists (sorted, keyed, etc.), and for di erent implementations of the physical disk abstraction, to re ect whether the disk has a track bu er, etc. A disk is used the same way whether or not it has a track bu er; the only di erence is in its p erformance characteristics.

4.2 Templates

Templates are another useful but dangerous concept in C++. With templates, you can parameterize a class de nition with a type, to allow you to write generic typ e-indep endent co de. For example, our Stack implementation ab ove only worked for pushing and p opping integers; what if we wanted a stack of characters, or oats, or p ointers, or some arbitrary data structure?