HPlogo HP C++ Programmer's Guide: HP 9000 Series Workstations and Servers > Chapter 1 Overview of HP C++

Support for Object-Oriented Programming

» 

Technical documentation

Complete book in PDF

 » Table of Contents

 » Index

C++ supports object-oriented programming; C does not. This section describes object-oriented programming, gives a brief example of an object-oriented approach to a programming problem, and gives an overview of the language enhancements that C++ provides for object-oriented programming.

What Is Object-Oriented Programming?

The traditional approach to programming is often summarized by the equation:

PROGRAM = DATA STRUCTURES + ALGORITHMS

According to this approach, a program is a blend of data (information) and algorithms (procedures). The data is the information given in a problem that may be useful in obtaining a solution. Procedures are the steps you take in manipulating the data to obtain a solution to the problem. Procedural programming, or non-object-oriented programming, typically focuses initially on the procedures. The key to a clever procedural program is often a clever algorithm.

Object-oriented programming shifts the emphasis from algorithms, or how things get done, to object declarations, or what needs to be manipulated. The object-oriented programmer typically starts by developing a concept of an object or collection of objects whose state and functionality are independent of a particular program.

Moreover, in an object-oriented program, the concept of procedure and data is replaced by the concept of objects and messages. An object is a package containing two components: information and a description of how to manipulate the information. A message specifies one of an object's manipulations. To send a message is to tell an object what do. The object determines exactly what methods to use. For example, a message to a circle object in an object-oriented graphics program might say "draw yourself."

In other words, object-oriented programming rejects the dichotomy between data and procedures and substitutes the concepts of objects (which contain both data and procedures) and messages:

PROGRAM = OBJECTS + MESSAGES

Object-Oriented Programming: The Bank Example

For example, suppose you want to develop a program that a bank can use to keep track of its transactions. Most of its transactions have to do with money and customers. Customers can borrow, save, invest, or write checks on their money, and most of the bank's money is kept in accounts.

A programmer using a non-object-oriented approach might develop a solution to the bank's needs by analyzing the bank's various transactions and turning these transactions into program routines. For example, there might be routines with names such as calculate_interest and add_deposit that pass and return arguments containing data about money, customers, and accounts.

A programmer using an object-oriented approach, in contrast, would probably begin by thinking of the objects in the bank rather than the bank's transactions. An object-oriented language would allow an object such as an account or a customer to contain both the information needed to define the object and the functions that define operations that can manipulate the object. Thus, an account object might contain an amount of money and also a function to calculate and add interest to its amount of money.

In the banking example, this concept of an account object allows you to send a message to an account object telling the object to update its balance. Upon receiving this message, the account object manipulates its data according to its own definitions of how to carry out the operations requested in the message.

Furthermore, the programmer using an object-oriented approach might design the bank program to include a hierarchy of account objects. All account objects could be derived from account and therefore contain whatever data and operations are part of an account. Moreover, the derived objects might also have additional or slightly different data or operations. Thus, a checking_account might contain a function that sets the interest for the account at a rate lower than the interest for a savings_account.

The bank_example program in example 1-1 is intended to illustrate these object-oriented programming concepts. It is not intended to represent a realistic application. The next several sections refer to the bank_example program. The source file for this program resides in the directory /usr/contrib/CC/Examples (/opt/CC/contrib/Examples for HP-UX 10.x C++ versions).

Figure 1-1 Example 1-1. Object-Oriented Programming with C++: bank_example

//*********************************************************
//program name is "bank_example"
//*********************************************************
#include <iostream.h>  // needed for C++-style I/O
#include <string.h>    // needed for C-style string manipulation
class account
{
private:
    char* name;         // owner of the account
protected:
    double balance;     // amount of money in the account
    double rate;        // rate of interest for the account
public:
    account(char* c)    // constructor
       { 
       name = new char [strlen(c) +1]; strcpy(name,c);
       balance = rate = 0; 
       }
    ~account()          // destructor
       { delete name; }
              // add an amount to the balance
    void deposit(double amount) { balance += amount; }
              // subtract an amount from the balance
    void withdraw (double amount) { balance -= amount; }
              // show owner's name and balance
    void display() 
       { cout << name << "   " << balance << "\n"; }
              // this function is redefined for
              // checking_account, which is a derived class
    virtual void update_balance() 
       { balance += (  rate * balance ); } 
};
              // define a class derived from account
class checking_account : public account
{
private:
    double fee;  // checking accounts have a fee in 
                 // addition to name, balance, and rate
public:
         // constructor; note that checking accounts
         // pay 5% interest but charge $2.00 fee
    checking_account(char* name) : account(name) 
       { rate = .05; fee = 2.00; }
         // redefined to deduct fee for this 
         // type of account
    void update_balance() 
       { balance += (  rate * balance ) - fee; }
};

         // define a class derived from account
class savings_account : public account
{
public:
         // constructor; note that savings accounts
         // pay 10% interest and charge no fee
    savings_account(char* name) : account(name) 
       { rate = .10; }
};

main()
{
  checking_account* my_checking_acct = 
      new checking_account ("checking");
  savings_account* my_savings_acct = 
      new savings_account ("savings");
         // send a message to my_checking_acct
         // to display itself
  my_checking_acct->display();
         // send a message to my_savings_acct to
         // display itself
  my_savings_acct->display();
         // send a message to my_checking_acct 
         // to deposit $100 to itself
  my_checking_acct->deposit(100);
         // send a message to my_savings_acct 
         // to deposit $1000 to itself
  my_savings_acct->deposit(1000);
         // send a message to my_checking_acct
         // to update its balance
  my_checking_acct->update_balance();
         // send a message to my_savings_acc
         // to update its balance
  my_savings_acct->update_balance();
         // send a message to my_checking_acct
         // to display itself
  my_checking_acct->display();
         // send a message to my_savings_acct
         // to display itself
  my_savings_acct->display();
}
//***********************************************

When you compile and run the bank_example program, you get the following results:

checking     0
savings      0
checking     103
savings      1100

How Does C++ Support Object-Oriented Programming?

To support object-oriented programming, a language must support the following:

  • Encapsulation — All the functions that can access an object are in one place and data and functions can be defined that can only be accessed from within that specific class.

  • Data abstraction — You can define data types that can be used without knowledge of how they are represented in storage.

  • Inheritance — You can develop hierarchies of objects that inherit data and functionality from their parent objects.

  • Type polymorphism — A pointer to an object can point to a variety of different types, and you can use a process called dynamic binding to select and execute an appropriate function at run time based on the type of the object that is actually referenced.

C++ has all of these characteristics, which are described in more detail in the following sections.

Encapsulation

Encapsulation means that all the functions that can access an object are in one place. C++ supports the class data type, which allows you to declare all the functions that can access its data within the body of its declaration. A class is a lot like a structure in C and it is the basis for much of the support that C++ provides for object-oriented programming.

For example, suppose you are using C++ to develop the banking application described briefly in the preceding section. You could define a class to represent an account object. Its data members could represent the customer who owns the account, the balance of the account, and the rate of interest for the account. Its member functions could specify operations to be used with the data members. Your code, nearly identical to that in example 1-1, might look something like the following fragment:

class account
{ 
  private:
     char* name;        // owner of the account
     double balance;    // amount of money in the account
     double rate;       // rate of interest for the account
  public:               // add an amount to the balance
     void deposit(double amount) { balance += amount; }
                        // subtract an amount from the balance
     void withdraw (double amount) { balance -= amount; }
                        // show owner's name and balance
     void display() { cout << name << "   " << balance << "\n"; }
                        // add interest to the balance
     void update_balance() { balance += (  rate * balance ); }
};

In this example, account is a class. The keywords private and public divide the class into two parts. The members in the first part — the private part — are data members. The members in the second part — the public part — are member functions. Because they are defined to be private, the data members, specifically name, balance, and rate, can only be used by member functions of the account class. In other words, the only functions that can access name, balance, and rate are deposit, withdraw, display, and update_balance.

Figure 1-2 “Encapsulation in a C++ Class: The account class Example” illustrates this definition of an account class. The arrow in the figure indicates that the functions in the public part of the account class have access to the data in the private part of the class.

Figure 1-2 Encapsulation in a C++ Class: The account class Example

[Encapsulation in a C++ Class: The account class Example]

Note that some or all of the data members could have been public and some or all of the member functions could have been private or protected in the account class. Refer to "Inheritance" for more information on the keyword protected. The design shown in Figure 1-2 “Encapsulation in a C++ Class: The account class Example” is only one of many ways to use encapsulation in defining classes.

Data Abstraction

C++ classes allow you to hide the representation of data in storage as well as restrict access to data. In other words, classes allow you to define data types that can be used without knowledge of their representation in storage.

You can use C++ classes in the same way that you use built-in types. For example, float is a built-in type. To use a float object you do not need to know how the object is represented in storage. All you need to know is the name of the type and the operations that you are allowed to perform on that type. When you use floating-point objects, you can add or assign values to them without concern for their representation. The representation of the objects is hidden.

Similarly, C++ lets you use a class like account while ignoring the details of how an account object is represented. All you need to know is that accounts have owners, balances, and interest rates, that you can make deposits and withdrawals, that you can display the name of the account's owner and balance, and that you can update the balance.

Furthermore, you can use data abstraction to design large or complex applications with many pieces that use objects of a class in different ways. If you need to change the representation of a class, you only need to do so in one place. Also, you can add modules that use objects of the class in entirely new ways.

Finally, data abstraction means that access to the representation of data objects is restricted. Restricting access to data makes debugging easier and assists you in protecting the integrity of class objects. For instance, C++ allows you to trace an error involving the private members of a class to the limited number of functions that have access to that data. Thus, an error involving the name data in the private part of an account object probably arises from one of the account member functions (deposit, withdraw, display, and update_balance), since they are the only functions allowed to access name data. Similarly, the representation of name data is consistent for all applications using account objects, since it is only accessible to account member functions.

Inheritance

C++ supports inheritance, allowing you to derive a class from one or more base classes. For example, using the class account as a base class, you can define derived classes named checking_account and savings_account as shown in the following fragment taken from Example 1-1:

              // define a class derived from account
class checking_account : public account
{  . . .
};
              // define a class derived from account
class savings_account : public account
{  . . .
};

Figure 1-3 “Concept of Single Inheritance: The account Example” illustrates the concept of single inheritance: checking and savings accounts are each derived from a base class.

Figure 1-3 Concept of Single Inheritance: The account Example

[Concept of Single Inheritance: The account Example]

Multiple inheritance means that a class can have more than one base class. For instance, you could define a savings_account object as derived from an investment object as well as from account. Other objects derived from investment might represent stocks and real estate. This concept of multiple inheritance is shown in Figure 1-4 “Concept of Multiple Inheritance: The savings_account Example”.

Figure 1-4 Concept of Multiple Inheritance: The savings_account Example

[Concept of Multiple Inheritance: The savings_account Example]

Deriving classes allows you to define details common to many potential derived classes in a base class. Derived classes inherit all members—both data and functions—of the base class. Thus all checking_account and savings_account objects inherit name, balance, and rate data members from the base class account, as well as the public member functions deposit, withdraw, display, and update_balance. In order for the derived class to access inherited members, however, the members must be declared public or protected. The keyword protected allows the derived class to access a member of the base class, while blocking access to the rest of the program.

Inheritance allows you to write the source code for a base class, store the declarations in a header file, and then use the base class to derive new classes with additional data or functions. This means that you can write separate modules that extend a large application without affecting the header file. For instance, in the modified bank_example program, in addition to inheriting name, balance, and rate data from the base class, checking_account objects have a new data member, fee.

Furthermore, C++ allows you to redefine a base class's member functions in each class derived from it. For instance, suppose in the bank_example program you had defined the update_balance() function in account as follows:

void update_balance() { balance += ( rate * balance ); }

Since checking accounts charge fees, the checking_account version of update_balance() was redefined to deduct a fee as well as add interest as follows:

void update_balance() { balance += ( rate * balance ) - fee; }

Type Polymorphism

As was mentioned previously, type polymorphism means that a pointer to an object can point to a variety of different types, and you can select and execute an appropriate function at run time based on the type of the object actually referenced. The rest of this section discusses how C++ implements the concept of type polymorphism using dynamic binding, inheritance, and type checking.

In C++, a pointer to a derived class is type-compatible with a pointer to its base class. As a result, it is possible for a pointer declared as the address of one class type to be assigned the address of another type.

For instance, as was mentioned previously, if checking_account and savings_account are both derived from account, the following is legal:

    // account_ptr points to an account object
account* account_ptr;
    // checking_ptr points to a checking_account object
checking_account* checking_ptr;
    // savings_ptr points to a savings_account object
savings_account* savings_ptr;
    // now account_ptr points to a savings_account object
account_ptr = savings_ptr;
    // now account_ptr points to a checking_account object
account_ptr = checking_ptr;

In other words, a variable declared as a pointer to a particular class might actually point to an object of a different class at run time. In this example, account_ptr points to a checking_account rather than an account.

While this type compatibility can be convenient, it can also result in ambiguity as to which class member function should be called. For instance, after making the preceding declarations and assignments, suppose you make the following function call:

    // Does this call the account member function 
    // or the checking_account member function?
account_ptr->update_balance();  

C++ handles this ambiguity by allowing you to specify a base class member function as virtual. When you declare a function to be virtual, you tell the compiling system to select and execute the appropriate function at run time depending on an object's actual type, rather than its declared type. This is called dynamic binding.

For example, consider the update_balance function, which is defined as virtual in the following code fragment taken from bank_example:

class account
{  . . .
         // this function is redefined for checking_account,
         // which is a derived class
    virtual void update_balance() 
       { balance += ( rate * balance ); }
};
         // define a class derived from account
class checking_account : public account
{  . . . 
    void update_balance() 
       { balance +=  ( rate * balance ) - fee; }  };

Declaring update_balance as virtual means that the compiling system uses dynamic binding. Here's how it works in the bank_example:

  • Suppose you declare account_ptr as a pointer to an account object, and you assign it the address of a checking_account object. Then you make a function call: account_ptr->update_balance().

  • C++ uses the checking_account definition of update_balance. This means that the fee is deducted from the account balance.

  • If you do not declare update_balance as virtual, C++ would use the account version of update_balance, ignoring the fact that account_ptr actually points to a checking_account object.

Inline Functions

Calling small functions frequently can slow a program's execution speed. Therefore, C++ allows you to declare inline expanded functions using the keyword inline. This means that the compiling system attempts to generate the code for the function at the place where it is called. When used with small functions, inline can increase execution speed.

If a function is not a class member, you can make it inline by declaring it with the inline specifier as shown in the following example:

inline int max (int a, int b) {return a > b ? a : b;}

Inline expansion is especially useful for defining small member functions. A member function becomes inline when it is defined within the definition of its class. For example, the show_radius function in the following code is inline expanded whenever possible:

class circle                // declare a class
{
    double radius;
public:
    void show_radius()
       { cout << "radius is " << radius ;}  // an inline function
};

The new and delete Operators

You can declare a named object in C++ to be static or auto just as you can in C. A static object is created once at the start of the program and destroyed once at the termination of the program. Its scope is the block in which it is declared; if it is declared outside of a block, it has file scope. An automatic object is created each time its declaration is encountered in the execution of the program and destroyed each time the block in which it occurs is exited. Its scope is from the point of declaration to the end of the block in which it is declared.

You can also control the life span of an object and allocate storage just for the time the object is needed. The operator new creates objects and the operator delete destroys them. Using these operators, new and delete, allows you to use an object created by a function after leaving the function. The objects created by new are allocated from free storage.

Because they are built into the language, new and delete are easier to use than malloc and free, which are not built into the C language. (The functions malloc and free are UNIX library calls.) C++ also allows you to overload new and delete, which means that you can create your own memory management operators on a class by class basis. Moreover, the new and delete operators for class objects invoke constructors and destructors, which are described briefly in the next section.

Constructors and Destructors

Constructors guarantee initialization of class objects; they are member functions designed explicitly to initialize objects. A constructor sets up and assigns a value in storage when a class object is declared.

Many classes also have destructors. A destructor ensures that storage is released, that counters are reset, and that other maintenance takes place when class objects are destroyed (for example, when a variable goes out of scope).

A constructor has the same name as its class, whereas a destructor for a class is the class name preceded by a tilde (~). Thus for class account, a constructor is named account and its destructor is named ~account. These are shown in the following code fragment:

account(char* c)     //constructor
      { 
      name = new char [strlen(c) +1]; strcpy(name,c);
      balance = rate = 0; 
      }
~account()           // destructor
       { delete name; }

Note that destructors can also be declared as virtual functions, thus ensuring that the appropriate destructor is always called regardless of its apparent type. (Refer to "Type Polymorphism" above.)

Overloaded Operators

Classes can have functions that assign special user-defined meanings to most of the standard C++ operators when they are applied to class objects. These functions are called overloaded operator functions. For example, if you are designing an application using complex numbers, you could overload the plus (+) operator to handle complex addition. The following code fragment illustrates such an application:

class complex
{
         // the real and imaginary parts of the number
  double real, imag; 
public:
  complex(double r,double i) // constructor
     { real = r; imag = i; } 
         // declare overloaded "+" operator 
         // as a member function
  complex operator+(complex addend);
};

The name of an operator function is the keyword operator followed by the operator itself, such as operator+. You can declare and call an operator function in the same way you call any other function, by using its full name, or by using just the operator. When you use just the operator, C++ selects the correct overloaded operator function to perform the task. An operator function can be a member function.

Conversion Operators

Conversion operators are member functions that have the same name as a type. The type can be either user-defined or built-in. You can use conversion operators to define your own type conversions. Declare a conversion operator as an overloaded operator function with the keyword operator.

For example, the following code fragment defines a conversion operator for a conversion from circle (a user-defined type) to int (a built-in type):

class circle
{
private:
  int radius;
public:
    .
    .
    .
  operator int()   // conversion operator defines a 
                   // conversion from a circle to integer
         { return radius; }
};

Given the preceding definition, you could make the following declaration and assignment:

circle A(1);  // create a circle A with a radius of 1
int i = A;    // initialize an integer variable, i, 
              // by converting A to an int and 
              // assigning the result to i