Static and Dynamic Types

The static type is the type of an object variable as declared at compile time and the dynamic type is the type of an object variable determined at run-time. In most cases the static and dynamic types of an object variable are the same, however, they can differ when we are using pointers to objects. An example of this capability is demonstrated below:


  int main()
  {
    Account a = Account(35.00, 34234324); 
    Account *ptrA = &a; 1

    CurrentAccount b = CurrentAccount(50.0, 12345, 200.0);
    CurrentAccount *ptrB = &b; 2

    cout << "Displaying ptrA:" << endl;
    ptrA->display(); 3
    cout << "Displaying ptrB:" << endl;
    ptrB->display(); 4
  
    //ptrB = ptrA;   // not allowed  5

    ptrA = ptrB; 6
    cout << "Displaying ptrA again:" << endl;
    ptrA->display();  7
  }

A full source code example for this is in CurrentAccount2.cpp

1

The Account pointer ptrA is assigned to the address of the Account object a.

2

The CurrentAccount pointer ptrB is assigned to the address of the CurrentAccount object b.

3

The display() method is called on the Account object using the ptrA pointer.

4

The display() method is called on the CurrentAccount object using the ptrB pointer.

5

We are not allowed to make the ptrB CurrentAccount pointer point at the Account object.

6

The ptrA Account pointer is pointed at the CurrentAccount object. This is allowed.

7

The ptrA Account's display() method is called, which results in the display() method of the CurrentAccount object being called.

The output from this code segment is shown as in Figure 3.4, “The output from the code segment above.”

Figure 3.4. The output from the code segment above.

The output from the code segment above.

To try to explain this further, see the following figures:

Figure 3.5. The pointers ptrA and ptrB have the same static and dynamic types.

The pointers ptrA and ptrB have the same static and dynamic types.

Figure 3.5, “The pointers ptrA and ptrB have the same static and dynamic types.” shows the code segment after steps 1 and 2 have taken place. This means that the two objects are created in memory and the two pointers point at their respective objects, so an Account pointer points at the Account object and the CurrentAccount pointer points at the CurrentAccount object.

Figure 3.6. Allowed: The pointer ptrA has static type Account and dynamic type CurrentAccount.

Allowed: The pointer ptrA has static type Account and dynamic type CurrentAccount.

Figure 3.6, “Allowed: The pointer ptrA has static type Account and dynamic type CurrentAccount.” shows the state of the application after the 6th step takes place. The Account pointer is assigned to the CurrentAccount pointer and this is allowed. To understand this, you can think of the Account pointer as understanding the display(), makeLodgement() and makeWithdrawal() methods. So, we can call any of these methods on the ptrA and the object that we are calling these methods on understands all these methods (and more!). This is guaranteed in this case! why? The CurrentAccount class is a child of the Account class and so has inherited all the methods from Account. Therefore, calling the methods of the pointer to the Account ptrA works perfectly.

Figure 3.7. Not Allowed: Pointer ptrB has static type CurrentAccount and dynamic type Account.

Not Allowed: Pointer ptrB has static type CurrentAccount and dynamic type Account.

Figure 3.7, “Not Allowed: Pointer ptrB has static type CurrentAccount and dynamic type Account.” shows the assignment of ptrB = ptrA; which is not allowed. If this case was allowed then the CurrentAccount pointer ptrB would be pointing to an Account object. But the CurrentAccount pointer understands the methods display(), makeLodgement(), makeWithdrawal() and setOverdraftLimit(). So what would happen if the setOverdraftLimit() was called on the Account object a? It does not have a setOverdraftLimit() method and so it would fail.

The rule is: An assignment L=R, is legal only if the static type of R is the same or a derived type of the static type of L.

One very important point to note in the code segment above is that when the pointer ptrA of static type Account points at the CurrentAccount object and its display() method is called, it is the display() method of the object CurrentAccount is called, not the Account display() method. You can see this in the screen grab of the output, as shown in Figure 3.4, “The output from the code segment above.”, where the overdraft limit is displayed the second time ptrA->display() is called. So the method is called on the dynamic type, not the static type.

The ability of a variable to change its dynamic type during execution is a useful property of C++, allowing programs to be easier to extend and easier to debug. However, it does have some consequences: Take the last example - The base class Account and the derived class CurrentAccount have different implementations of the display() method. The dynamic type of the object determines which display() method is called. This is called Dynamic Binding. With the facility of dynamic binding, CurrentAccount::display() is called. If there was no such facility, Account::display() would always be called.

The notation that I just used for identifying the two display() methods allowed a clear indication of which display() method was being discussed, either the display() method associated with the Account class or the display() method associated with the CurrentAccount class. The "::" is an actual operator, called the scope resolution operator, and it has very important uses.

For example: The display() method that was developed for the CurrentAccount class displayed the balance, the account number and the overdraft limit. However, the code used to display the balance and the account number is the same as that in the parent class Account::display() method. So code has been replicated - This is unacceptable under the OOP paradigm as later alterations to this segment of code must also be replicated.

The scope resolution operator allow this issue to be resolved, for example, the CurrentAccount class's display() method can be modified to:


  void CurrentAccount::display()
  {
    Account::display();
    cout << "  And overdraft limit: " << overdraftLimit << endl;
  }
  

Now, the code from the Account's display() method is not being replicated using cut-and-paste, rather it is being called directly. Why is this so important?

Well, this demonstrates that we are able to call the method that we are over-riding (the Account::display() method) from within the new method (CurrentAccount::display()).

In this case, only 2 lines of code have been replicated, but it could have been 100's of lines of code, with 10 different types of derived classes.