| |
| Main Menu for the Essentials of C++ |
| Next Section: Operator Overloading |
Another way to extend the C++ type system is through the use of
class declarations. The class system is C++'s way
of implementing object-oriented programming . The
class system therefore allows for encapsulation, inheritance,
and polymorphism.
A. Encapsulation
Both data (the properties of instances of a class) and functions
(the operations that instances can perform) can be declared as
integral parts of a class type. They then become parts of each
instance of that class type. Data and functions that are
appropriately declared can be made inaccessible to the rest of
the program. Such class elements are said to be encapsulated
. Other data and functions can be declared as public,
in which case they are available to the rest of the program. Careful
design of classes, based on encapsulation, supports data abstraction
and the software engineering principles widely recognized
as important to software re-use and bug-free code.
B. Inheritance
Inheritance involves the ability to create a new class based on
an already existing class (or classes in the case of multiple inheritance) that has a subset of the properties
of the desired class. Both data (properties) and functionality
can be inherited.
C. Polymorphism
Polymorphism allows instances of different classes to respond
to the same message or function call in the way appropriate
for that instance type. It is related to the idea of dynamic
binding where the system does not decide until run-time which
function code will be executed for a given function call.
D. Class Declarations
Ignoring inheritance for the moment, a class declaration takes the following general form:
The placement of the public and private sections can be swapped.
Everything placed in the private part is encapsulated. Variables
declared inside a class represent the data or properties of instances
of the class and usually are declared in the private part of the
class. Such variables are often referred to as data members
or member data.
Functions declared as part of a class are often referred to as
member functions. Member functions that other parts
of the program must access for any purpose, including to get access
to or manipulate the private data members of instances of the class,
must be declared in the public part of the class. Functions used
only by the member functions themselves and which are otherwise
unavailable to the program should be declared in the private part.
Aside from their placement inside a class, member data and function declarations are the same as non-member declarations. There are, however, two special types of member functions. First, all classes need one or more constructors.
These are functions automatically called when an instance of a
class is declared. Their purpose is to create or 'construct' an
instance of the class. The data members of the
instance are initialized according to the instructions in the
constructor function.
C++ will create a default constructor that simply creates the
instance, but, usually, the programmer creates one or more constructors
for a class to perform initializations unique to instances of
that class. Constructors always have the same name as the class
they are members of and they have no return type - not even 'void'.
Their parameter list looks the same as any other function declaration
with the parameters themselves usually being used to provide the
initial property values for the instance being created. For example,
if one is declaring a 'person' class, the constructor declaration
might be:
If we assume that the 'person' class includes the data members:
then one could interpret the parameter list above to indicate
that the parameter 'nm' holds the value to be given to the data
member 'name; 'ag' holds the value to be given to 'age', 'streetAddr'
holds the value for streetAddress, etc. As with any other set
of variables, the parameter names and data member names must be
different. For that reason, 'stte', for example, is used in the
parameter list so that 'state' could be the data member name.
All constructors for a specific class have the same name and the
same return type. It is via function overloading
that multiple constructors are created.
Unlike other member functions, constructors cannot be inherited.
It is possible, however, for a constructor of an inheriting class to call the inherited class's constructor.
Since constructors are used in instance declarations (see Part D below), they should be declared in the public section. Note: constructors
are not called except indirectly as part of instance declarations
and as part of constructors for inheriting classes.
When initializing an instance with another instance (as might
happen when declaring an instance and immediately initializing
it with the values from another instance) and when passing an
instance as a value parameter to a function, the property values
of one instance are copied to another instance. Such copying requires
a copy constructor. C++ automatically
provides one that works as long as the class declaration does
not include pointer, class, struct, or array-based data members.
In these situations it may be necessary for programmers to declare
their own copy constructors.
The second special type of member function declaration is the destructor. Any instance for which special code
must be executed when the instance disappears must have a destructor
containing that special code. While there can be multiple constructors
- based on the idea of function overloading, there can
be only one destructor per class. It is called whenever the duration
of a class instance ends.
The format for the destructor is:
Note that destructors always start with a tilde (~),
use the class name (as with constructors), have no return type, and no parameters. They
cannot be inherited but they can be explicitly called.
A simple declaration for the 'Person' class might be:
class Person
{public:
Person(char* nm, int ag, char* streetAddr, char* cty, char*
stte, char* zip);
~Person();
char* ProvideName();
int ProvideAge();
// 'Provide...' functions for other properties
void ChangeName(char* nm);
void ChangeAge(int ag);
// 'Change...' functions for the other properties
private:
char* name;
int age;
char* streetAddress;
char* city;
char* state;
char* zipCode // zip codes are not considered to be integers
// so they are declared as strings.
};
Discussion in "The Story of C++
E. Class Definitions
Member functions are defined the same way other functions are
defined. That definition can be part of an in-line function declaration (See Part 'F' below) in the class declaration or it can be
part of a separate definition as in standard function definitions.
Usually the class declaration is in a header file and the definitions
are in a separate file that contains an 'include' directive for
the corresponding header file.
Since the system must know that a specific definition belongs
to a specific class, the scope resolution operator, '::',
is used with member function definitions. To define the member
function that returns the age of a person one might write:
To define the member function 'ChangeAge', one might write:
Note that the first line of each definition is the same as the
corresponding declaration except that the class name and scope
resolution operator are inserted. The same approach is used to
define constructors and destructors. Below are incomplete definitions
for each:
Person :: Person(char* nm, int ag, char* streetAddr,
char* cty, char* stte, char* zip)
{
// the code for the constructor would go here
}
Person :: ~Person()
{
// the code for the destructor would go here
}
Discussion of the scope resolution operator in "The Story of C++"
F. Inline Member Functions
The section describing functions defined
inline functions. To declare a member function as inline
one can use the procedure described there or one can use a form
specific to class member functions. The general form (inside the
class declaration) is:
One line member functions are ideal candidates for inline functions.
To re-code 'ChangeAge' as an inline function one would write the
following in the class declaration and remove the definition code:
The code that was in the definition has been moved into the declaration.
Note that the declaration does NOT end with a semi-colon.
G. Creating Class Instances
Once a class declaration has been written, the class name can
be used as a type name in any file that includes the class declaration.
In other words, since class declarations are type definitions,
class names can be used anywhere type names are used. The general
format for declaring an instance of a class (called instantiating
an instance) is:
Note that the instantiation process automatically calls a constructor.
If a class declaration has multiple constructors, the one called
is determined using the same rules used in other function overloading.
If the constructor to be
used has no parameters, the parentheses are dropped. To declare
an instance of class 'Person' one might write:
This instantiation process can be seen as declaring and initializing
the identifier 'pablo' as a variable of type 'Person' with the
property values shown. A variable such as 'pablo', which represents
an instance of a specific class, is ofter referred to as an instance
variable.
One must be careful when creating an array of instances of a class
type because one is instantiating as many instances of the class
as there are elements in the array and a constructor must be called
for each instance. The following code will not work:
because there is no constructor for the class 'Person' that has
no parameters. One can fix this by declaring and defining a constructor
with no parameters or by defining a constructor with default values (Section III.A)
for all parameters. Either way, the properties for each instance
of 'Person' in the array may not be what the user wants. If so,
the program must include explicit code for giving values to these
properties.
H. Using Instances
Instance variables are like any other structured variables. They
can be passed as parameters, assigned to other instances of the
same class, and returned by functions. They can also be used with
appropriately overloaded operators.
The private members of an instance are simply unaccessable from
outside the instance. To access the public members of an instance
one can use the dot operator:
When the public member is a function (which is most likely to
be the case since data members tend to be private), the part of
the code to the right of the 'dot' is written the same as any
other function call. For example:
or
In the language of object-oriented programming, function calls
like this are often referred to as messages. Using this
terminology one can interpret the above as:
or
In other words, the code is explicitly asking that something be done by 'person1' or 'person[3]'. This is how the system determines which instance is being referred to. Both these function calls really have an implicit or hidden argument - the specific instance under consideration. At times one needs to refer explicitly to this instance. Therefore, each member function has a built-in local variable referred to as this which is a pointer to the instance "sent the message". A common use for 'this' is in member functions that need to return the instance itself.
When using pointer references to class instances,
the dot operator notation can be cumbersome because of the precedence
rules. Consider the example:
The parentheses must be used because the dot operator has higher
precedence than the indirection operator. To simplify matters
C++ provides another operator for accessing members, ->.
Using this operator the above code becomes:
I. The Syntax of Inheritance
To indicate that a class inherits from another class, the first
line of the declaration includes a colon followed by the name
of the class (or classes) inherited from. The simplest form of
this is:
This declares 'some-new-class' as a derived class
type inheriting from base class, 'the class-inherited-from'.
Since the new class only inherits from one class, we have an example
of single inheritance.
To provide some control over what gets inherited and the kinds
of access users of the derived class have to members of the base
class, C++ includes additional keywords both for the base class
declaration and the inheritance declaration.
In addition to the public and private sections of a class declaration,
there can be a protected section. Private members of a
base class are not directly accessible to member functions of
derived classes. They can only be accessed through the base class's
interface. To allow member data and functions to be directly accessible
to a derived class but not accessible outside the class, such
member data and functions should be placed in the 'protected'
section.
This level of access control is handled by the base class. The
declaration of the derived class can further refine access by
adding the words 'public', 'private' or protected in front of
each base class inherited from. The word 'public' signifies that
there is no change in access either by users or classes that inherit
from this derived class. On the other hand, 'private' signifies
that even 'public' and 'protected' members of the base class are
now treated as if they were private. When 'protected' is used, the public or protected
members of the base class become protected. Note that in all cases,
private members in the base class remain private.
The default is 'private' so 'public' must be used if one wishes
to keep access rights the same. For documentation purposes it
is recommended that the desired option be included in the declaration.
The basic form of a declaration involving single inheritance now
becomes:
Thus, to declare a new class 'Student' that inherits from 'Person' and allows the same level of access to the protected and public members of 'Person', one would write:
class Person
{public:
Person(char* nm, int ag, char* streetAddr, char* cty,
char* stte, char* zip);
~Person();
char* ProvideName();
int ProvideAge();
// 'Provide...' functions for other properties
void ChangeName(char* nm);
void ChangeAge(int ag);
// 'Change...' functions for the other properties
protected:
char* name;
int age;
char* streetAddress;
char* city;
char* state;
char* zipCode // zip codes are not handled as integers so
// they are declared as strings
};
class Student : public Person
{
// member declarations new to class Student
}
Although not used as often, it is possible to have multiple
inheritance - where one class inherits from more than one
class. There are a number of issues involved in this and you are
encouraged to check other references for a more thorough discussion. Syntactically, it is easy to accomplish. You simply add
a comma followed by the access type (public, protected, or private)
and the class name for each additional base class. If class 'X'
is to inherit from classes 'Y' and 'Z' one might write:
J. Friend Functions
C++ allows a breach in the encapsulation rules by allowing one
to declare functions or classes as friends of a class.
Friend functions or classes have full access to the public, protected,
and private members of any class for which they have been declared
as friends. It is as if the friend functions or member functions
of the friend class were members of the class.
To declare that a function is a friend of (has full access to)
a class, one includes the declaration of that function in the class
declaration with the word 'friend' in front of the function declaration.
By putting the word friend here, you are declaring that this
function is NOT a member function of the class but it does have
access to the private and protected members of that class. The
definition of the friend function may or may not be included in
the file containing the definitions for the class's member functions.
One can also declare a whole class to be a friend of another class.
This allows ALL member functions of the friend class to have full
access to all the members of the other class. The general form
for this is:
where this line is placed inside a class declaration and 'some-class-name'
is the name of a class known in the file holding the class declaration.
If the class 'Teacher' was to be declared as a friend of 'Student'
one could write:
The first line of this example is an incomplete declaration of
the 'Teacher' class. It acts as a promise that the class is declared
somewhere. (The other way to accomplish the same goal would be
to use the 'include' preprocessor directive to copy in the code
for the declaration of 'Teacher'.) By including
'Teacher' as a friend of 'Student', any instance of 'Teacher'
has full access to ALL the members of instances of the 'Student'
class. (Sounds right - don't students feel they have no privacy
from teachers and administrators!)
K. Static Members
As we have declared data members so far, whether (public, protected,
or private), there is a unique copy of each data member in each
instance. Sometimes, however, one wants a data member that says
something about the whole class. To declare such a data member
in C++ one puts the keyword static in front of the declaration.
Consider a data member for the class 'Person' that held the age
of the oldest person. This is not a property of any specific person
but of the class. To declare it as a 'static' data member one
would include the following line inside the class declaration
for 'Person'.
This data member exists whether or not any instances of 'Person'
exists - which is not true of non-static data members because
they belong to specific instances. This means that a static variable
requires both a declaration (just given) and a definition. The
definition must be outside the class declaration, usually in the
file for the member functions. The form is:
as in:
This form can be extended to include an initialization as in:
One can access static variables in the same way one can access
other data members - following the same rules about public, protected,
and private membership. In addition, since these variables exist
independent of any specific instance, one can declare static member
functions to access them. Such functions are declared the same
as member functions except that they too have the keyword 'static'
placed before the declaration. To declare a static member function
to update the 'oldestAge' the following code would be placed in
the public section of the class declaration:
To call this function one uses the class name instead of an instance
name as in:
Such functions, since they do not refer to a specific instance,
do not have access to the data members of the class.
L. Implementing Polymorphism
All member functions declared so far are linked to their definitions
at link-time. In other words, at run-time, the system already
knows what code to execute if a function is called. To implement
polymorphism and the dynamic binding (Part 'C' above) it requires, this link between the declaration and definition
must be postponed until the moment the function is called. The idea is that there are multiple (polymorphic) functions with the same name and each time a polymorphic function is called, the system stops for a moment to figure out which version of the function to actually call. Since not all functions are polymorphic, one must let the compiler and linker know which set of functions are polymorphic and require run-time linkage.
In C++ polymorphic functions can only be those belonging to classes involved
in an inheritance structure. Such a structure represents
the one or more base classes and all the classes that derive directly
or indirectly from this (these) base classes. (See Chapter 11, Section 1, Figure 2 for an example of an inheritance structure.) To state which functions are to have
their linkage postponed until run-time, one adds the word virtual
in front of the highest declaration of this function in the
hierarchy.
This text does not deal further with polymorphism and you should
check elsewhere if you want to include polymorphic functions in
your code.
Links to 'The Story of C++" and other documents
| |
Main Menu of Essentials of C++ | Next Section |