Al is a contributing editor and columnist for DDJ and the author of a number of books on C. He can be reached at DDJ, 501 Galveston Drive, Redwood City, CA 94063.
C++ is an object-oriented superset of C that Bjarne Stroustrup developed at AT&T Bell Laboratories beginning in 1980 and continuing with successive releases. Although C++ goes back almost ten years, its recent attention in the PC community is largely due to the growing interest in object-oriented programming and the availability of new C++ language development environments.
In this article, I will discuss some C++ extensions that make life easier for C programmers and how C++ brings the object-oriented programming paradigm to C. This article is not a comprehensive treatment of the subject; I neither describe all the features of C++ nor explain every nuance of every feature. Such coverage would fill a book, and C++ books and articles are appearing with increased regularity as the language gains momentum.
Dr. Stroustrup developed the C++ language system as a preprocessor that translates C++ code into C code, which is then compiled by a traditional C compiler. Newer C++ language systems use native code compilers that translate directly from the C++ source language to the native language of the target machine. I will refer to all C++ language systems as "compilers," but these references apply as well to preprocessing translators.
Forget, for the moment, object-oriented programming. If you never define a class or declare an object -- things I'll discuss shortly -- you can still benefit from the improvements that C++ offers. Many of these improvements were important enough that the ANSI X3J11 committee incorporated them into the C language-standard specification, and you might know them now as ANSI C additions rather than as the legacy of C++. Among the ANSI adoptions are the function prototype, the const type qualifier, and the void type. There are other C++ features that you can use in your traditional function-oriented C programming, ones not included by ANSI, but definite enhancements, nonetheless. I'll discuss some of these advantages before exploring the object-oriented facets of C++. So, for now, let's look at C++ as an improved C.
Mandatory Prototypes -- One of the C++ contributions to ANSI C was the new-style function definition and declaration blocks. ANSI calls the definition block a "function prototype." Both blocks contain the parameters' type descriptions in the parenthesized parameter list.
ANSI C allows for both the old and new-style function blocks to protect existing code, and you can use a mix of both styles in an ANSI C program. With C++, however, the old style is not supported. C++ requires you to code with the new style, and that is a strong advantage, because the new style enforces stronger type checking of parameter and return data types.
Comments -- C++, as a superset of C, recognizes the standard C comment style that delimits comments with the / * and */ tokens. But C++ has another comment format. Wherever the // token appears, everything to the end of the line is a comment.
Because C++ recognizes both formats, many programmers use the new format for comments and the old format to temporarily disable blocks of code. Comments do not nest in C, so disabling code by surrounding it with the /* and */ delimiters does not work if the code to be disabled itself contains comments. In C++, old-style comments can include the new style comment format, so the / * ... */ format is an effective way to "comment out" code. If you use the old format exclusively for this purpose, finding all such commented-out code is a simple matter of using a grep utility program or your editor to find all instances of the /* token.
Default Function Arguments -- You can declare a default value for arguments in a C++ function prototype in the following way:
void func(int = 5, double = 1.23);
The expressions declare default values for the arguments. The C++ compiler substitutes the default values if you omit the arguments when you call the function. You can call the function by using any of these ways:
func(12, 3.45); // overrides both defaults func(3); // effectively func(3, 1.23); func( ); // effectively func(5, 1.23);
Variable Declaration Placement -- In C, you must declare all variables at the beginning of the block in which they have scope. You may not intermix variable declaration and procedural expressions. C++ removes that restriction, allowing you to declare a variable anywhere before you reference it and making expressions such as this one possible:
for(int ctr = 0; ctr < MAXCTR; ctr++) // ...
This feature allows you to code the declaration of a variable closer to the code that uses it.
Stand-alone Structure Names -- In C, you refer to structures with the struct keyword as a prefix to the name. In C++, the structure is closely related to the class (discussed later), and its name is similar to a keyword for as long as the definition is in scope. You can code the following:
struct linkedlist {
/* -- whatever -- */
linkedlist *previous_node;
linkedlist *next_node;};Observe that the two pointers are declared without the struct keyword. Later declarations of the structure may use the linkedlist word only, as shown here:
linkedlist mylist;
The effect here is as if you had used this typedef in C:
typedef struct linkedlist linkedlist;
The typedef would not have worked for the pointers in the structure because the typedef declaration is not complete when the pointer declarations occur, so the C++ notation offers a distinct improvement.
Inline Functions -- You can tell the C++ compiler that a function is "inline." This causes a new copy of it to be compiled in line each time it is called. The inline nature of the individual copies eliminates the function-calling overhead of a traditional function. Obviously you should use the inline function qualifier only when the function itself is relatively small.
Global Scope Resolution - In C, if a local variable and a global variable have the same name, all references to that name from within the block where the local variable is declared will refer to the local variable. The local variable name overrides the global name. C++ adds the "::" global scope resolution operator with which you can explicitly reference a global variable from a scope where a local variable has the same name. Consider the code in Example 1.
int amount = 123;
main ()
{
int amount = 456;
printf("%d", ::amount);
printf("d", amount);;
}
In this example, the output would be 123456 because the first printf refers to the hidden global amount variable by virtue of the "::" global scope resolution operator.
asm ( string ); -- C++ offers, as a standard way to incorporate assembly language into a C++ program, the "asm" keyword. Many C compilers have ways to do this, but there is no standard. ANSI sidestepped the issue as being implementation dependent. C++ faces it head-on and provides a standard way to pass the string of your choice to the assembler. Assuming all C++ compilers use the standard, only the contents of the string are implementation dependent. If you define the strings separately, the assembly language components of your programs are more manageable.
Of course, this is not a perfect solution. Whenever you use assembly language, you must be ready to deal with it when you undertake a port. Just because assembly language was appropriate in your original program does not mean you will be able to use it in the ported version. But at least the C++ approach moves the layer of non-portable implementation dependence to a more distant platform.
The Free Store -- C++ introduces an improved heap management facility called the "free store." It is implemented with two operators named "new" and "delete." The new operator is analogous to malloc in that it allocates memory and assigns its address to a pointer. Its argument, however, is more descriptive than the integer value that you pass to malloc. Its purpose is to allow you to allocate memory for a semipermanent variable, one that remains in scope beyond the block in which is declared. Here are some examples:
char *cp = new char[strlen(whatever)+1]; linkedlist *first_node = new linkedlist;
Observe that the array-like expression in the first example contains a variable dimension. The second example assumes a structure (or class) named "linkedlist," such as the one used earlier.
When you are ready to destroy the variable, you use the delete operator in much the same way that you used the free function in traditional C.
delete cp; delete first_node;
C++ allows you to override the new and delete operators by coding your own new and delete functions. It also provides a global function pointer that the system calls when any new operation cannot get the memory it needs. By calling the set_new_handler function with the address of your heap exhaustion error function, you can issue new operations without testing the pointer values for a NULL return every time.
References -- C++ includes a derived data type called a "reference." It is a form of alias and will seem oddly like a pointer to the C programmer. Here's what a simple reference looks like:
int sam; int& george = sam;
The & reference-to operator tells the C++ compiler that george is an alias for sam. Wherever you say george you could have said sam. If you only use it that way, you might just as well use a #define, but simple substitution is not the strength of a reference.
If you pass a variable as an argument to a function that is expecting a reference, the compiler actually passes the variable's address. The called function acts upon the caller's copy of the variable through its address rather than upon a local copy. This feature shows its promise when used with structures. An example:
void setheight(struct cube& box)
{
box.height = 5;
}This function will change the value of the structure owned by the calling function rather than the value of a local copy. This procedure eliminates unnecessary copying of data values back and forth between functions while preserving the notation of a local variable, which is simpler and more readable than de-referenced pointer notation.
A function can return a reference, as shown in this code:
int& getwidth( )
{
/*...*/
return newwidth;
}The caller of the getwidth function does not need to know that the called function returns a reference. You can code the receiving variable as a reference itself or an actual variable. Either way works. Some C++ experts believe that you should never return a reference. Others disagree.
Overloaded Function Names -- In C++, you can have several functions with the same name in the same scope. The compiler distinguishes the functions based on their argument types. This feature, called "overloading," lets you use the same name to represent the same generic operation on different data types.
Here is an example of an overloaded function name's prototypes:
void display(int); // display an int void display(char); // display a char void display(long); // display a long void display(double); // display a double
These prototypes represent four distinct functions with the same name. When you call the name, the argument you supply tells the compiler which one you want.
Support for overloading is one reason why C++ needs prototypes for everything. The compiler must see the argument types so it can distinguish functions that have the same name.
Structures with Functions - C++ allows you to code functions as members of structures. This practice binds a function call to an instance of a structure. Consider the code in Example 2.
struct cube {
int length;
int width;
int height;
int volume();
};
You have defined a structure that has three integers and a function. You must now complete the structure's definition by declaring the function:
int cube::volume(void)
{
return length * width * height;
}Observe that you declare the function as a member of the cube structure by attaching the cube:: prefix to the function name. Notice also that the function does not need to name the instance of the structure when it refers to one of the structure's members. The compiler knows that the function will be called on behalf of an instance of the structure in which the function is a member, and the compiler automatically makes the association for you.
Next you declare a cube object and get some values into the cube's dimensions:
struct cube box = {5,4,3};That looks like and is traditional C. You have declared a structure of type cube and named it box. You have initialized the dimensions of the structure. Finally, you call the cube's volume function to compute the volume of the cube.
printf("Volume: %d", box.volume( ));Observe the form of the volume function call. The function name is prefixed with the identifier of the declared structure in standard C structure member addressing format.
This little exercise is a sneak preview of the C++ class, a more complex version of the enhanced structure used here. It is also your introduction to object-oriented programming in that the structure just shown is an abstract data type including a method, and the structure declaration named box is an object.
So far we've concentrated on the additive features of C++ that can enhance C language programming. But, in doing so, we brushed against some new object-oriented programming ideas. Now let's consider what those and other new things mean when viewed from the perch of the object-oriented programming platform.
C++, we have said, adds the object-oriented programming paradigm to the C language. C++ has "data abstraction," "encapsulation," "objects," "methods," "inheritance," and "polymorphism." These ingredients, we are told, are what embody an object-oriented programming platform. Every paradigm wants its own parlance, and you will find that these strange new terms have unexpected parallels in your C experience, parallels that, once revealed, make the new things easier to understand. Before getting into the object-oriented details of C++, though, let's try to liken some of these new object-oriented concepts to their counterparts in traditional function-oriented C.
Data Abstraction, Encapsulation, and Objects -- "Data abstraction" is the ability to describe new data types in terms of their format and the processes that act upon them. "Encapsulation" is the process by which you combine the component data and function parts of an abstract data type into one encapsulated definition. An abstract data type, called a "class" in C++, is thus a collection of other data items and functions. The data items describe the new class's format, and the functions describe how it behaves. An instance of an abstract data type is called an "object."
C has its own built-in data types. In C, you use char, int, float, and double and qualified variations. When you declare one of these, you are declaring an instance of the type or an object. The developers of your C compiler encapsulated these data types into the compiler by describing their formats and by including the methods that act upon them. When you say:
int answer = 10 + variable_integer;
you have declared the int object named answer and invoked the C compiler's int method that sums the values from two integer data type objects and places the result into the answer object. The int is not an abstract data type because it is built into the compiler, but it is, nonetheless, an object.
In object-oriented programming, you add to the language's vocabulary of data types with abstract data types encapsulated by you and, perhaps, by developers of third-party data type libraries. So, in addition to the int and the float, you can have the string, the blivot, the Bach_two_part_invention, and whatever else you dream up as a new data type.
Other derived types in C are pointers, which are derived directly from the base types to which they point; constants, which are derived from the types assigned to them by the compiler; and structures, which are early examples of derived types with multiple inheritance in that they are derived from the various types of their multiple elements.
Function Overriding and Polymorphism -- Function overriding is the ability for a type or method in a derived type to override a similarly defined type or method in its base type. The different types up and down a type hierarchy can define member types and methods according to their individual needs. The programmer who is using an abstract data type does not need to know which method will process the message being sent or in which abstract type the referenced member type appears.
If you have a base data type and a derived data type, you send messages to an object of the derived data type to get it to do what you want. Because the derived type has inherited the attributes of the base, you can send messages that are defined as belonging to the base data type, and the derived type will accept them and use the methods of the base to act upon them.
With function overriding, you can define a member type or method in a derived data type that resembles one in its base. Send a message via that method or refer to that member type, and the one defined in the derived type gets used. Not all of the base's derived types will duplicate the original. If you send a message to an object way down the hierarchical ladder, the compiler will select the method from the first type higher in the hierarchy where the method is defined.
If, however, you address the derived type through the base type, perhaps by a base-type pointer that contains the address of the derived type, then the base function gets used, and overriding does not occur. When you do not want this to happen, when you want the derived overriding function to be used regardless of how you address the type, then you must invoke "polymorphism."
Polymorphism is a nuance of function overriding. If you declare the base function as a "virtual" function, then it will always be overridden by functions with the same name and characteristics in the derived types.
These, then, are the fundamentals of object-oriented programming, and we have considered how you might associate your function-oriented programming practices with them. Some of our analogies stretch the point to its limit, but their purpose is to assure you that the paradigm called "object-oriented programming" is largely a different way of looking at what you have been doing all along.
Now let's look at some of the details of C++ and see how those principles are applied.
The basic unit of encapsulation in C++ is the class. What we called the abstract data type before, we will now call the "class" because they are the same thing. C++ uses the class keyword to describe its version of the programmer-defined data type. When you encapsulate an abstract data type, you define a class. When you declare an object, you declare an instance of a class. Example 3 illustrates what a definition of a class looks like.
class date {
int day;
int month;
int year;
public:
date (void)
{day = month = year = 0;}
date (int da, int mo, int yr);
date() { /* null destructor */ }
void display (void);
};
This definition describes a class named date. It resembles a structure in that it has members consisting of data variables and functions. The members prior to the public keyword are private parts. The rest are public. The difference between the class and the struct shown earlier is that all the members of the struct are visible to all parts of your program while only the public members of a class can be seen by your program's functions. The private parts of a class can be read and changed only by functions that are themselves members of the class. (For an exception, see the discussion on friends.)
Usually, the private parts are variables and the public parts are functions. Nothing says that this must be so, but it seems to be a good convention t follow and works for most class definitions.
In addition to the public keyword, you can declare that certain members are "private" and others are "protected." Members in a class are private by default as are our day, month, and year members. Members in a structure are public by default unless you declare them as private or protected.
The access control of a member indicates what kinds of functions can access the members. A private member can be accessed by member functions and friends only. Public members can be accessed by any function that declares an object of the class or has the structure in scope. Protected members of classes are private except that member functions and friends of derived classes can access them. We will discuss member functions, friends, and derived classes soon.
Our date class's encapsulation is in complete because all the methods are not there yet. Remember, methods are functions.
Classes usually have at least two member functions and often more. The usual two are the "constructor" member function and the "destructor" member function, although it is not required that you define them. The others are the methods.
Constructors and Destructors -- When you declare an object as an instance of a class, you do it in much the same way that you declare any other data type. Compare this integer and this date:
int days_on_board; date date_hired;
They look the same, and to the programmer using them, they are. But when you design the class, you usually provide for one constructor function and one destructor function. The constructor function executes when you declare the object, and the destructor function executes when the object goes out of scope. If you omit these functions, then no special processing occurs when an object enters and leaves scope.
A constructor function has the same name as the class itself and has no return value. In the date example above, we have defined two constructor functions, both named date. They are distinguished by their different argument types; the first constructor has no arguments, and the second constructor has three integer arguments. These are overloaded constructor functions because they have the same name and different parameters.
Observe next that the first constructor definition is not terminated with a semicolon but has a brace-surrounded block of code following it. This format is the member function's version of an inline function. By including the code with the definition in this manner, you build an inline function, in this case one that merely initializes the three date variable members to zero. This constructor function executes when you declare a date object with no initializing values as shown here:
date date_hired;
The second constructor function is not an inline function (although it could be), so you must provide its code somewhere. The function might look like that in Example 4.
date:: date (int da, int mo, int yr)
{
day = da;
month = mo;
year = yr;
}
Observe that the function declaration is prefixed with date:: to associate it with the date class. Observe also that the function has free access to the private members of the class. This particular constructor executes when you declare a date object with three integer initializer values as shown here:
date date_retired(25, 5, 88);
The destructor function has the same name as the class but with a tilde prefix. In the date example class used here, the destructor function does nothing, so it is coded as a null inline function. More complex classes will require things to be done when the object goes out of scope. Perhaps some free store memory needs to be deleted, for example, and the destructor function would take care of that.
Member Functions -- Besides the constructor and destructor, a class can have other member functions. These are the methods of the class. In our date example, we show a member function named "display." To use this method, we must code it something like this:
void date::display(void) { printf("%d/%d/%d", month, day, year); }(Experienced C++ programmers might wonder why I use the old-fashioned printf when the C++ stream facility is available. Because I haven't described the stream classes yet, their use would tend to confuse those who are not familiar with them. I'll discuss streams later.)
The program that declares a date object can then use a member function related to the date class such as this:
date_hired.display( );
In the parlance of object-oriented programming, we have sent a message to the date_hired object to tell it to use its display method. It looks a lot like a traditional function call, doesn't it?
Friends -- The private members of a class are visible only to the class's member functions. The constructor, destructor, and display member functions in the date class can read and write the month, day, and year integers, but no outside function has that access. From time to time you will find a need to provide outside access to the innards of one of your classes. A named function or class can be a "friend" of the class being defined. You can make the assignment of friend status only from within the definition of the class that grants access as shown in Example 5.
class time;
class date {
// ...
friend void now (date&, time&);
};
class time {
// ...
friend void shownow (date&, time&);
};
These are two classes named date and time (with the details omitted). We need the extra time declaration at the top because the friend statement in the date class has a reference to it. The two classes share a friend function named shownow, which might display the current date and time like this:
void shownow (date& d, time& t)
{
printf("\n%d/%d/%d", d.day, d.month, d.year);
printf("\n%d:%d:%d", t.hr, t.min, t.sec);
}Because the shownow function is a friend to both classes, it can read the private members of both.
A class can have another class as its friend with this statement:
class date {// ... friend class time;};
Operator overloading is one of the neater tricks you can do with C++. It is what makes the language so extensible. You've already seen how you can add data types by defining classes. Next, you might want to perform arithmetic, relational, and other operations on your classes the same way that you do with int and float variables and the like. We built a simple date class. Consider Example 6.
date retirement_date, today;
// ...
if (retirement_date < today)
// Keep working ...
How about this?
date date_married, today;
// ...
if (date_married + 365 == today)
// Happy Anniversary ...
Both these forms are possible with C++. You can build class member functions that the compiler associates with C operators. This feature is called "operator overloading." When you use an operational expression such as the ones just shown, your operator overloading member functions get called. Here's an example:
class date {
// ...
int operator<(date&);
}The date class definition says that a member function overloads the less-than operator. When you code this expression:
(retirement_date < today)
the function that associates the less-than operator to a date class with another date class as the argument is called. It returns a true or false integer. The member function might look like that in Example 7.
int date::operator<(date& dt)
{
if (year < dt .year)
return TRUE;
if (year == dt .year) {
if (month < dt .month)
return TRUE;
if (month == dt.month)
if (day < dt .day)
return TRUE;
}
return FALSE;
}
The function refers to the class on the left side of the expression by naming its private parts without qualification (day, month, year). It refers to the argument's private parts by way of the reference variable (dt.day, dt.month, dt.year).
You can overload any C operator in this manner. You cannot create your own operators such as")("or anything the compiler would choke on, and you may not use overloaded operators in ways that the compiler cannot parse, such as A [B. You could, however, use this feature to create some really confusing code. You might, for example, use the + operator to logically subtract two classes. Try to steer clear of such nonsense.
Operator overloading is a powerful feature. You can overload the [] array operator to create your own array processing. You can overload the ( ) function call operator to make a class look like a function call. And, of course, you can have several different functions overloading the same operator with different argument types. You might want separate functions for adding floats and longs to your class, for example.
The this Pointer -- Every member function has a built-in pointer named this. It points to the object being processed by the member function. When a member function wants to make a copy of the object or return it after perhaps modifying it, the member function can reference the object as *this.
To illustrate the use of the this pointer, let's take another look at the overloaded addition operator. Our less-than overloaded operator returned an integer, but an arithmetic expression needs to return a copy of the class. Consider the example we saw earlier:
if (date_married + 365 = = today) //
Happy Anniversary ...There are two overloaded operations implied by this expression. One is the equality = = operator, which would look a lot like the less-than operator we already built. The other is the addition operator. Here they are in the class definition.
class date {
// ...
date& operator+(int);
int operator= =(date &);
}The overloaded + operator must return a class that has the result of the addition as its value. It does not add the integer to the class called out in the expression. It performs the addition and returns the result, which is itself a date class. Without the complex date arithmetic that checks for month overflow, leap years, and all that, here is what the overloaded + operator function looks like.
date& date::operator+(int n)
{
date dt;
dt = * this;
//add n to dt.month,dt.day,dt.year
// ...
return dt;
}You will see that we copy the object being added to into a temporary date named dt. The this pointer provides a way for us to do this.
If you wanted to overload the + = operator, the function would look like this:
date& date::operator+=(int n)
{
//add n to month,day,year
// ...
return *this;
}
Observe in the overloaded + operator above that the object pointed to by this is assigned to the object named dt. C++ includes a built-in assignment operator for every class you define. It simply copies all the members from one object to the other much the way that structure assignment works in C. You can overload the assignment operator if you need to. You would do that if you wanted to assign something other than another object of the same class. Consider this, for example:
#include <time.h>
// ...
date_hired = time(NULL);
Throughout the examples so far, we have used printf to display data. C++ includes stream input/output classes already defined in stream.h. They have several advantages over the traditional printf/scanf pair. To write something to the console you say:
cout << "Hello, Dolly";
The stream.h file not only includes the ostream and istream classes, it includes external declarations of the standard objects, cout and cin, which are assigned to the console. The example just given shows how the ostream class has overloaded the << operator. This is not a bitwise shift operation in this usage. The notation is meant to represent the direction of data flow. The << operator is overloaded several times to allow you to send different data types to the console without worrying about their format. Here are examples:
cout << "Blossom";
cout <<' \ n';
cout << 123;The overloaded operator functions return references to the object being processed, so you can write the above example in this way:
cout << "Blossom" <<' \ n'<< 123;
The istream class works the other way with >> as the operator to signify data flowing from the object to the argument variable as shown here:
void main( )
{
char mystring[80];
// ...
cin >> mystring;
}You can declare streams and associate them with file buffers as well. There are several standard methods that support file stream input/output included in stream.h.
Conversion Functions -- Conversion functions allow you to provide for the conversion of a class to another type, either a standard C data type or another class as shown here:
class date {
// ..
operator long( );
};You would write the overloaded function such as the one shown in Example 8 and can call the function one of several ways as shown in Example 9.
long date::operator long () (void)
{
long days_since_creation;
// compute the number of days...
// ...
return days_since_creation;
}
void main ()
{
date today;
long eversince;
// ...
eversince = (long) today;
eversince = today;
eversince = long (today);
}
The notation you choose would depend on how you are using the conversion. The first format looks like a cast, and the second implies that a normal type conversion is going on. You might use the last format if you want the code to remind you that you are invoking a class conversion function.
This example shows how you convert a class to a standard C or C++ data type. You can use the same technique to build class-to-class conversion functions, but some programmers prefer to use specific constructor functions to perform conversions of classes to other classes. When the target class comes into scope, the constructor conversion function gets the data values from the object that is its parameter. For example:
date today(25, 12, 89);
// ...
Julian julian_today(today); // a class named Julian
Inheritance is a vital ingredient for object-oriented programming. Many C programmers will use the advanced features of C++ classes to add data types and never concern themselves with class hierarchies. Others will explore the murky depths of inheritance and will build huge, exotic class systems, making every new class a new wrinkle on an old. Somewhere in between is the probable ideal.
A class in C++ can be defined as a derivative of another class. Given the generic date class we've used so far, we might need a more specific date, one that has all the properties of other dates but that has a few unique ones of its own. Here is how you would code a derived class:
class workday: date
{
int shift;
public:
workday(int da, int mo,int yr, int sh);
// ...
};We have defined a class named workday that is derived from the class named date. The workday class is called the "derived" class, and the date class is called the "base" class. The derived workday class has its own private member, the shift variable. What you do not see is that the class also has variables named day, month, and year because it is derived from the date class which has those variables, and that is how inheritance works. The workday constructor function has a shift variable, one more variable than the corresponding date constructor. Here is the constructor function:
workday::workday(int da,int mo,int yr,int sh)
:date( da, mo, yr)
{
shift = sh;
}The expression following the colon after the argument list shows what the workday constructor function will pass to the date constructor function. The values do not have to be taken from the argument list as we have done here, they can be any valid expressions that match the argument types of the base class's constructor function.
Member functions in a derived class can read and write the public members of the base class if the derived class has not reused the member name as shown in Example 10.
class workday : date {
public:
// ...
int isXmas (void);
};
int workday;;isXmas (void)
{
return month == 12 && day == 25;
}
A derived class can reuse a member name that is used in a base. This is function overriding. If the derived class has reused the base member's name, any unqualified references to that name will point to the member in the derived class. But if the member wants to access the base class's member, the derived member function qualifies the name such as this:
x = date:: month;
Non-member functions that declare objects of the derived class cannot access the public members of the base class unless the public keyword qualifies the inheritance as shown in Example 11.
class workday : public date {
// ...
};
void main ()
{
workday proj_dt;
// ...
int yr_comp = proj_dt.year;
}
A derived class cannot access the private parts of its base unless it is declared by the base as a friend. If you are adding a new class to a class hierarchy and find that a derived class needs to get at a private member of a base class somewhere up the line, you will need to rummage around in the class definitions to find and modify the base class. You must either declare the derived class (or one of its functions) as a friend of the base or move the desired element into the public part of the base class.
The object-oriented world traditionally describes its type in what they call a "class hierarchy." This is often a misnomer. In object-oriented design, a base type can have many derived types. If you were to stop there, you would have a hierarchy; in a true hierarchy each lower element is subordinate to only one superior. Versions of C++ prior to 2.0, which is the latest, adhered to the hierarchical model in that a derived C++ class could have only one base class. In Version 2.0, as in many other object-oriented languages, a derived class can have multiple base classes. This model is called "multiple inheritance," and it is not a hierarchy -- it is a network. Programmers love to mix, confuse, and abuse metaphors. No doubt the "type hierarchy" metaphor is destined to remain with us even though it misuses the base from which it derives.
A C++ class can inherit the attributes of multiple base classes as shown in Example 12.
class date {
// ...
};
class time {
// ...
};
class datetime : date, time {
// ...
public:
determine (int da,int mo,int yr,
int hr,int min, int sec);
};
We have defined a derived class named datetime that inherits the attributes of two base classes named date and time. The constructor for the datetime class would look like this:
datetime:: datetime(int da, int mo, int yr, int hr, int min, int sec)
: date (da, mo, yr), time (hr, min, sec)
{
// ...
}There are many things to consider when you build a network of multiple-inheritance classes. You must be aware of the order in which base constructors are called and you must guard against ambiguities when you refer to members of the derived and base classes. The taller the family tree, the harder it is to keep track of what is involved with distant, unseen relatives, ancestors, and friends.
C++ uses the virtual function qualifier to declare a base-member function as one that is always overridden by a derived class regardless of how the derived class is addressed. If you declare a function in the derived class with the same name and argument types, then any reference to that function -- even through a reference to the base -- will invoke the derived class's copy of the virtual function. Example 13 illustrates some of these concepts.
class date {
// ...
public:
virtual int isXmas (void);
};
class workday : date {
public:
int isXmas (void);
};
void main ()
{
date dt;
workday wd;
date = dat = &dt;
// ...
wd.isXmas (); // workday::isXmas
dt.isXmas(); // date::isXmas
dat->isXmas(); // workday::isXmas (!)
wd.date::isXmas (); // date::isXmas
}
This ability to redefine methods up and down the class network is what gives your C++ class definitions their polymorphic characteristics.
Some of the features I discussed in this article are new to C++ 2.0. But the language has not finished growing. For example, Dr. Stroustrup is working to add parameterized data types and exception handling as intrinsic parts of the language. And ANSI is about to field a committee whose task it will be to write a standard description of C++.
With Microsoft and Borland moving in the object-oriented language direction and with rumors that both will introduce C++ compilers in the '90s, the future of C++ seems secure. Given the extensive improvements that C++ makes to the C language, we can safely predict that C++ will eventually replace C as the language of choice.