In this section, we will make a short tour passing a number of common programming paradigms, finally arriving at object-oriented techniques. Suitable background for this section is the ground-breaking book by B. Stroustrup [1], the original creator of the C++ programming language (see also [2]).
Each of the paradigms embodies a set of architectural rules. The rules constrain the activities of design and programming, but it is these constraints that keep us from going awry in our long-term goals.
The design techniques outlined here are not formal; the human mind is the best ``tool'' to support and distinguish between them. (You could e.g. make contact with ``literate programming'' a real tool to help us to leverage the output of a design idea across an entire development community.)
Our basic assumption will be that other than the change from one high-level language like FORTRAN, C or PASCAL to another, Object-oriented programming [OOP] requires a new mind-set.
The paradigms are hierarchical, i.e. if a particular language supports a paradigm, it will in general support all paradigms leading to it as well. Note that we shall speak of support of a paradigm only if the language not merely enables its use but provides suitable mechanisms to allow for elegant, effortless programming employing the paradigm. In a smallish example below, you will see that one can write FORTRAN programs with data hiding (see sect. 2.3) and object-oriented programs using the C language, but these languages do not really provide the necessary means to do this without making the result look unnatural. Nevertheless, at times the borders between realizations of one or the other paradigm may appear fuzzy.
To physicists this is a common style since the advent of FORTRAN, which comes from FORmula TRANslator. ALGOL and C are later inventions in the same direction. A simple example of good procedural programming style is the implementation of the square root function as sqrt(x) in a FORTRAN program.
The procedural programming paradigm is supported by all modern programming languages by facilities for passing arguments to and returning values from functions. It is quite a minimalistic style and we consider it an elementary condition of programming rather than a method of choice.
We recognize the need to encapsulate functions or data subsets. This is not just extreme procedural style, but an emphasis shift from the procedures to partition a program in its task to the organization of data defined and manipulated by it. Thus, modularity is also known as the ``data-hiding principle''.
When the names of variables, constants, functions and types are made local to a module, we can also speak more generally of ``information hiding''. In C++, the class concept does provide support for this concept. In the C language, the possibility to separately compile code subfiles (using e.g. the #include compiler directive can pose as an example of modular programming).
Note: When there is no grouping of procedures with related data, the procedural style suffices and there is no need for modular programming.
prepend()
, first()
, print()
, delete()
, release()
(written
with brackets to indicate their function character).
Singling out these operations and implementing them as separate
functions is an example of modular programming paradigm use.
The modular style leads us to the centralization of data of a type. The next step is to transcend the built-in types, like integer, real etc. and create new user-defined types designed to suit our specific needs. This is called Data Abstraction. By a data structure, we mean an ordered collection of data objects and a set of operations on these objects.
A simple example is the definition of a type Complex of number pairs in the C or C++ programming languages (C and C++ do not have a built-in type for complex numbers, as FORTRAN has, for example), or a type Matrix as a field of numbers.
Another name for user-defined types is ``abstract data types'' [ADT], a collection of data and a set of allowed operations (actions, methods) that are used to define and manipulate the data.
A priori, the variables of an ADT do not have a name known to the compiler. They do not obey the usual rules saying when they go in or out of value (this is called the ``scope'' of a variable) etc. The whole machinery available to the not-so-simple built-in types must be defined by us to allow to manipulation of instances of the new type.
DA is to a limited extent supported in FORTRAN 90 (through the MODULE statement) and in C (through the struct statement). In the C++ programming language, the class keyword is reserved for the definition of ADTs.
class List { private: Listelement * listHead; public: List() {listHead = NULL;} ~List() { release(); } void prepend(char data); void print(); void release(); // ... };} What you see here is our definition of a C++ class named
List
between a pair of curly brackets closed by a
semicolon. The body of the definition is partioned in two
groups labelled private and public. The first
group contains only a data member (a pointer variable, more
specifically), while the methods needed to manipulate the list
are all in the second group. In addition, there are two definitions
with the same name as the class, itself, namely List()
,
and ~List()
.
The first is called a (default) constructor and
provides us with an empty list whose ``head'' pointer points nowhere
(to the NULL
address). The second is a destructor
calling the release()
method in order to ``destroy'' the
elements of the list one by one - i.e. free the storage space they
are occupying.
It is convenient to keep the class definition consisting
of method declarations only in its own file,
e.g. List.hh
(the extension indicates that it contains
a ``header'' to the code), and the definitions of the methods in
a file List.cc
which includes the header file(s) using the
#include
compiler directive.
Sometimes you can also see files with the
extension *.icc
containing methods which are inline expanded
the compiler (C++ has an extra keyword, inline
, reserved for
such functions). The inline method file is included likewise at the
end of the header file.
The meaning of the partitioning becomes obvious only when trying to
use or instantiate the definitions: outside of the class
body, only the use of methods in the public part is allowed
and recognized. Thus, anything in this group is truly private
to the user-defined type List
. On the contrary,
the public methods and variables can be used anywhere -- in
other ADT definitions or in client programs. The fragment below is
part of a sample client. At this point, it shall only show how the
ADT might be used in a program called main()
:
main() { List w; w.prepend('A'); w.prepend('B'); w.print(); // ...The file which contains
main()
must include the class definition.
The output, facilitated by the print()
method, might simply
result in printing the letters added to the (initially empty) list
named w. You can see that a period is used as an operator to
access the member functions.
Note: When there is no need for more than one object of a type, the modular programming style suffices and there is no need for data abstraction.
The solution presented in the C++ language is called inheritance. It is the primary characteristic of any object-oriented programming language. A collection of classes or user-defined types exhibits an inheritance hierarchy as one class derives from another (inherits members) in a hierarchical fashion.
A common example is that of a Shapeclass. OOP is defined by expressing the distinction between the general properties of a any shape: it has color (a class data member), it can be drawn (a class method) - and the properties of a specific shape: a Circle is a Shape that has a radius, is drawn by a circle-drawing function etc. In C++, the class Circle is said to be derived from the class Shape, which is said to be a base class of Circle (in another terminology, Shape is called a superclass and Circle a subclass).
Note: When there is no commonality between types selected to become classes, the data abstraction paradigm suffices and there is no need for OOP.
There is another way to express the notion of object-orientation - message passing: Objects are a priori independent of each other. An object of the same or different class can send a message to any object, and the object receiving the message can only act upon that message. We have met an implicit example of such a message above - here, the request for action was to create an empty list. The use of a method like print() is an obvious example for the request to print something - as specified in the implementation of this function (which we did not present).
call <function>
statements are passing the
message to either set or get a value to anObject
There is no way to use aValue
outside of
anObject
.
As you go along learning C++, you will have to decide which of the paradigms you need to employ in your program design. It may turn out that you only want to have a ``better C''. It may be that you want to dive deep down in the glittering world of OOP and discover relations among the parts of your application which justify to consider parametric polymorphism - C++ can become arbitrarily complicated and you are well advised to start small and only apply those parts of the language which you have well understood and know how to use.