MCS 502
MCS 502
Modern software applications are intricate, dynamic and complex. The number of lines
of code can exceed the hundreds of thousands or millions. These applications evolve over
considerable time. Some applications take years of programming effort and more years to
mature. Creating such applications involves many developers with different levels of
expertise. These software projects consist of smaller stand alone and testable sub-projects; sub-
projects that are transferrable, practical, upgradeable and possibly even usable within other
projects. The principles of software engineering suggest that each component should be a highly
cohesive element and that the collection of components should be loosely coupled. Object-
oriented languages provide the tools for implementing these general principles.
Mastering Complexity
Large applications are complex. We master their complexity by identifying the most important
features of the problem domain. In very general terms, we express the features in terms of data
and activities. We identify the data objects and the activities on those objects as complementary
tasks.
Consider a course enrollment system for a program in a college or university. Each participant
If we switch our attention from these activities to the objects involved, we identify a Course and
a Hybrid Course. Focusing on a Course, we observe that it has a Course Code. We look up the
Code in the Calendar to determine when the Course is being offered.
We say that a Course has a Code and that a Code uses the Calendar to determine its
availability. The diagram below shows these relationships between the objects in the problem
domain. The connectors identify the types of relationships between the objects.
In switching our attention from the activities in the structure chart to the objects in the
relationship diagram we switch from a procedural description of the problem to an object-
oriented description.
These two distinct approaches to complexity date at least as back as the ancient
Greeks. Heraclitus viewed the world in terms of process, while Democritus viewed the world in
terms of discrete atoms.
Learning to divide a complex problem into objects and to identify the relationships amongst the
objects is the subject matter of a course on system analysis and design. The material covered in
this text introduces some of the principal concepts central to analysis and design along with the
C++ syntax for implementing these concepts in code.
Programming Languages
Eric Levenez maintains a web page that lists the major programming languages throughout the
world. TIOBE Software tracks the most popular ones and long-term trends based on world-wide
availability of software engineers as calculated from Google, Yahoo!, and MSN search
engines. Many of these languages support object orientation.
Java, C, C++ and Objective-C are currently the four most popular languages. Each is an
imperative language; that is, a language that specifies each step necessary to reach a desired
state. These languages have much syntax in common: Objective-C is a strict superset of C, C++
contains almost all of C as a subset, Java syntax is C-like, but not a superset of C.
Features of C++
Learning object-oriented programming using C++ has several advantages for a student familiar
with C. C++ is
Type Safety
A type-safe language traps syntax errors at compile-time, diminishing the amount of buggy code
that escapes to the client. The compiler uses type rules to check syntax and generates errors or
warnings if any rule is violated.
C compilers are more tolerant of type errors than C++ compilers. For example, a C compiler
will accept the following code, which may cause a segmentation fault at run-time
The prototype for foo() instructs the compiler to omit checking for argument/parameter type
mismatches. The argument in the function call is an int of negative value (-25) and the type
received in the parameter is the address of a char array. Since the parameter's value is an invalid
address, printing from that address causes a segmentation fault at run-time, but no error at
compile-time.
We can fix this easily. If we include the parameter type in the prototype as shown below, the
compiler will check for an argument/parameter type mismatch and issue an error message like
that shown on the right:
Bjarne Stroustrup, in creating the C++ language, decided to close this loophole. He mandated
that all prototypes list their parameter types, which forces all C++ compilers to check for
argument/parameter type mismatches at compile-time.
Namespaces
In applications written simultaneously by many developers, chances are high that different
developers will use the same identifiers for different variables in the application. If so, once they
assemble their code, naming conflicts will arise. We avoid such conflicts by developing each
part of an application within its own namespace and identifying its variables relative to that
namespace.
A namespace is a scope for the entities that it encloses. Scoping rules prevent identifier conflicts
across different namespaces.
We define a namespace as follows
The identifier after the namespace keyword is the name of the scope. The pair of braces encloses
and defines the scope.
For example, to define x in two separate namespaces (english and french), we write
To access a variable defined within a namespace, we precede its identifier with the
namespace's identifier and separate them with a double colon (::). We call this double colon the
scope resolution operator.
For example, to increment the x in namespace english and to decrement the x in namespace
french, we write
Namespaces hide entities. To expose an identifier to the current namespace, we insert the using
declaration before referring to the identifier.
For example, to expose one of the x's to the current namespace, we write:
After which, we can simply write:
To expose all of the identifiers within a namespace, we insert the using directive before referring
to any of them.
For example, to expose all of the identifiers within namespace english, we write:
Exposing a single identifier or a complete namespace simply adds the identifier(s) to the hosting
namespace.
From C to C++ Syntax
To compare C++ with C syntax, consider a program that displays the following phrase on the
standard output device
C - procedural code
The C source code for displaying this phrase is
The two functions - main() and printf() - identify activities. These identifiers share the global
namespace.
<cstdio> is the C++ version of the C header file <stdio.h>. This header file declares the
prototype for printf() within the std namespace. std stands for standard. The file extension for
any C++ source code is .cpp.
The directive
exposes all of the identifiers declared within the std namespace to the global namespace. The
libraries of standard C++ declare most of their identifiers within the std namespace.
C++ - hybrid code
The object-oriented C++ source code for displaying our welcome phrase is
The object-oriented syntax consists of:
1. The directive
inserts the <iostream> header file into the source code. The <iostream> library provides
access to the standard input and output objects.
2. The object
inserts whatever is on its right side into whatever is on its left side.
4. The manipulator
represents an end of line character along with a flushing of the output buffer.
Note the absence of a formatting string. The cout object handles the output formatting itself.
That is, the complete statement
inserts into the standard output stream the string "Welcome to Object-Oriented" followed by a
newline character and a flushing of the output buffer.
The following object-oriented program accepts an integer value from standard input and displays
that value on standard output:
• The object
extracts the data identified on its right side from the object on its left-hand side. Note the
absence of a formatting string. The cin object handles the input formatting itself. That is, the
complete statement
extracts an integer value from the input stream and stores that value in the variable named i The
type of the variable i defines the rule for converting the text characters in the input stream into
byte data in memory. Note the absence of the address of operator on i and the absence of the
conversion specifier, each of which is present in the C language.
This chapter defines an object and a class and introduces the concepts of encapsulation,
inheritance and polymorphism. Subsequent chapters elaborate on these concepts in detail.
Abstraction
The primary graphic in UML is the class diagram: a rectangular box with three compartments:
1. the upper compartment identifies the class by its name
2. the middle compartment identifies the names and types of its attributes
3. the lower compartment identifies the names, return types and parameter types of its
operations
For example,
Terminology
UML uses the terms attributes and operations. Each object-oriented language uses its own
terms. Equivalent terms are:
The C++ language standard uses the terms data members and member functions exclusively.
Encapsulation
Encapsulation is the primary concept of object-oriented programming. It refers to the integration
of data and logic within a class' implementation and the crisp interface between any client and
that implementation. In other words, encapsulation is a technique that supports high cohesion
within a class and low coupling between the class' implementation and any one of its clients.
The class definition declares the variables that store each object's data and the prototypes of the
functions that contain the logic that operates on that data. Clients access objects through calls to
these functions without knowlegde or any need to know the data data stored within the objects or
the logic that manipulates that data. The function prototypes provide the interface.
Encapsulation shileds the complex details of an object's implementation from its crisp external
representation. Consider the following statement from the preceding chapter:
cout refers to the standard output object. Its class defines how to store any data in memory and
how to control the processes that work with that data. The << operator copies the string to the
output object without exposing any of the implementation details. As clients, we only see the
interface that manages the output process.
A well-encapsulated class hides all of the implementation details within itself. The client does
not see the data that the class' object stores within itself or the logic that it uses to manage its
internal data. The client only sees a clean and simple interface to the object.
As long as the classes in a programming solution are well-encapsulated, any programmer can
upgrade the internal structure of any object developed by another programmer without changing
any client code.
Inheritance and Polymorphism
Two other concepts in object-oriented languages are prominent in our study of relationships
between classes:
• Inheritance - one class inherits the structure of another class
• Polymorphism - a single interface provides multiple implementations
These are special cases of encapsulation in the sense that they distinquish interface and
implementation to produce highly cohesive objects that support minimal coupling to their
clients.
Inheritance
Inheritance relates classes that share the same structure. In the Figure below, the Hybrid Course
class inherits the entire structure of the Course class. We say that the hybrid course 'is-a-kind-of'
course and depict the inheritance relationship using an arrow drawn from the more specialized
class to the more general class:
Polymorphism
Polymorphism associates the implementation appropriate to an object based on its type. In the
Figure below, the HybridCourse object uses a different mode of delivery than the Course object,
but the same assessments. Note that both objects belong to the same hierarchy: both are Course
objects.
A mode() query on a Course object reports a different result than a mode() query on a Hybrid
Course object. On the other hand, an assessments() query on a Course object reports the same
result as on the HybridCourse object. Duplicating identical code is avoided under
polymorphism.
Encapsulation, inheritance and polymorphism are the three foundational concepts of any object-
oriented programming language.
Modular programming implements modular designs and is supported by procedural and object-
oriented languages. A modular design consists of a set of modules, which were developed and
tested separately. The C programming language supports modular design through library
modules composed of functions. The stdio module provides input and output support and hides
its implementation details; typically, the implementation for scanf() and printf() ships in binary
form with the compiler. The stdio.h header file provides the interface, which is all that we need
to complete our source code.
This chapter describes how to create a module in C++, compile the source code for each module
separately and link the compiled code into a single executable binary. The chapter concludes
with an example of a unit test on a module.
Modules
A well-designed module is a highly cohesive unit that can be loosely coupled to other
modules. It addresses one aspect of a programming solution and hides as much detail as
practically possible. A compiler translates the source code for a module independently of the
source code for other modules into its own unit of binary code.
Consider the Transaction program illustrated below. The main module accesses the Transaction
module. The Transaction module accesses the iostream module. The Transaction module
contains the definitions of the transaction functions used by the program. The iostream module
contains the definitions of the cout and cin objects used by the program.
To translating the source code of any module the compiler needs information identifying the
names used in that modules but defined outside the module. To enable this, we store C++ source
code for each module in two separate files:
• a header file - contains the class definitions and the function prototypes
• an implementation file - contains the instructions that define the logic within the
functions
The file extension .h (or .hpp) identifies the header file. The file extension .cpp identifies the
implementation file.
The names of the header files for the standard C++ libraries do not include a file extension
(consider for example, the <iostream> header file for the cout and cin objects).
Example
The implementation file for the main module in the Transaction program above includes the
header files for the itself (main.h) and for the Transaction module (Transaction.h). The
implementation file for the Transaction module includes the header file for the iostream
module. Each implementation file DOES NOT include any other implementation file.
We compile each implementation (*.cpp) file separately and only once. We do not compile
header (*.h) files.
The compiled version of iostream's implementation file is part of the system library.
Stages of Compilation
Comprehensive compilation consists of three independent but sequential stages (as shown in the
figure below):
1. Preprocessor - interprets all directives and creates a single translation unit for the
compiler - (#include inserts the contents of the header files), (#define substitutes all
macros)
2. Compiler - compiles each translation unit separately and creates an independent binary
version
3. Linker - assembles the various binary units along with the system binaries to create one
complete executable binary
A Modular Example
Consider a trivial accounting application that accepts journal transaction data from the standard
input device and displays that data on the standard output device. The application does not
perform any intermediate calculation.
The design consists of two modules:
• Main - supervises the processing of all transactions
• Transaction - defines the input and output logic for a single transaction
Transaction Module
Let us define a structure for a single transaction
• Transaction - holds the information for one transaction in memory
and two global functions
• enter() - accepts transaction data from the standard input device
• display() - displays transaction data on the standard output device
Transaction.h
The header file for our Transaction module defines our Transaction type and declares the
prototypes for our two functions:
Note the UML naming convention and the extension on the name of the header file.
Transaction.cpp
The implementation file for our Transaction module defines the logic within our two
functions. It includes the system header file for access to the cout and cin objects and the header
file for access to the Transaction type.
main.cpp
The implementation file for our Main module defines the main() function. We #include the
header file for the definition of the Transaction type:
The -o option identifies the name of the executable binary. The names of the two
implementation files complete the command.
To run the executable binary, we enter
Visual Studio
To compile our application at the command-line on a Windows platform using the Visual Studio
compiler, we enter the command (To open the Visual Studio command prompt window, we press
Start > All Programs and search for the prompt in the Visual Studio Tools sub-directory.)
The /Fe option identifies the name of the executable binary. The names of the two
implementation files follow this option.
To run the executable, we enter
IDE Compilation
Integrated Development Environments (IDEs) are software development applications that
integrate features for coding, compiling, testing and debugging source code in different
languages. The IDE used in this course is Microsoft's Visual Studio.
The keystrokes for the various debugging options available in this IDE are listed next to the sub-
menu items under the Debug menu.
Unit Tests
Unit testing is an integral part of modular programming. A unit test is a code snippet that tests a
single assumption in a work unit of a complete program. Each work unit is a single logical
component with a simple interface. Typical work units are functions and classes.
We use unit tests to examine the work units in a program and rerun the tests after each
upgrade. We store the test suite in a separate module.
Calculator Example
Consider a Calculator module that raises an integer to the power of an integer exponent and
determines the integer exponent to which an integer base has been raised to obtain a given
result. The header file for the Calculator module includes the prototypes for these two work
units:
The suite of unit tests for this module checks if the implementations return the expected
results. The header file for the Tester module contains:
The implementation file for the Tester module contains:
The unit tests show that this implementation does not handle bases that are negative and should
be upgraded.
Good Programming Practice
It is good programming practice to write the suite of unit tests for the work units in a module as
soon as we have defined the header file and before starting to code the bodies of the work
units. As we fill in the implementation details, we can continue testing our module to ensure that
it produces the results that we expect.
After going through this unit, the learner will able to:
• Define the basic syntax
• Learn about keywords
• Define the Member Functions
• Learn about the Input and Output Example
• Learn about Dynamic memory
C++ augments the procedural features of C with object-oriented features. The notes entitled
"Programming Computers Using C" describe the more common syntax that these two languages
share.
This chapter introduces some basic features used in object-oriented programming, which C++
adds to C. The topics covered including types, declarations, definitions, scope, overloading and
pass by reference.
Keywords
A C++ compiler will successfully compile any C program that does not use any of these
keywords as identifiers provided that that program satisfies C++'s type safety requirements. We
call such a C program a clean C program.
Types
C++ supports the fundamental types of the core language and any compound types that we and
other programmers define.
Fundamental Types
bool
bool stores a logical value: either true and false. The ! operator reverses the value: !true is false
and !false is true.
bool to int
Conversions of boolean values to integer values and vice versa require care. true promotes to an
int of value 1, while false promotes to an int of value 0. Applying the ! operator to an int value
other than 0 produces a value of 0, while applying the ! operator to an int value of 0 produces a
value of 1. That is, the following code snippet displays 1 (not 4)
C++ treats the integer value 0 as false and any other vaue as true.
Compound Types
A compound type is a type that is composed of other types. (The C language uses the term
derived type.) To identify compound types that are object-oriented classes we use the keywords
struct or class
C++ does not require the keyword struct or class in prototypes or object definitions as shown on
the left. The C equivalent is shown on the right.
auto
The auto keyword deduces an object's type from its initializer. The initializer is necessary in any
auto definition.
For example,
auto simplifies our coding by using information that the compiler already knows. This will
prove particularly useful when working with the standard libraries.
Declarations and Definitions
Declarations
A declaration associates an entity with a type. The entity may be a variable, an object or a
function. That is, we use a declaration to tell the compiler how to interpret the entity's identifier.
For example, by writing the function prototype
we declare add() to be a function that receives two ints and returns an int. We do not specifying
its meaning.
For example, by writing
A definition is a declaration that associates a meaning with an identifier. A definition may only
appear once within its code block or translation unit. This is called the one-definition rule.
For example, the following two definitions attach meanings to Transaction and to display():
We cannot redefine Transaction or display() within the same code block or translation unit.
Definitions are Executable Statements
Each definition is an executable statement. We may embed it amongst other executable
statements.
For example, we may place a definition within an initializer:
Forward declarations and function prototypes are declarations that are not definitions. They
associate an identifier with a type, but do not attach any meaning to the identifier. We may
repeat such declarations several times within the same code block or translation unit.
Repeated declarations may occur when we include several header files in an implementation
file. However, if any header file contains a definition, the translation unit that includes that
header file must not break the one-definition rule.
A definition that appears more than once within the same translation unit violates the one-
definition rule and generates a compiler error. Consider the options shown below.
Our sample program consists of three modules: main, Transaction and iostream.
The main module's implementation file calls add(), which receives the address of a
Transaction object:
The Transaction module's header file defines the Transaction type:
If we place the prototype for the add() function in the main module's header file, main.cpp will
not compile:
The compiler will report Transaction* as undeclared. Since the compiler analyzes code
sequentially, it will not know what Transaction when it encounters the prototype for add(),
Including the Transaction.h in main.h would resolve this error but would break the one-
definition rule:
A forward declaration informs the compiler that the identifier Transaction is a valid type, without
defining the type.
This solution provides the compiler with just enough information to accept the identifer as a
valid type.
Compact Solution
The alternative is to move the prototype from main.h to the Transaction.h:
This compact solution localizes all declarations related to the Transaction type within the same
file. We call functions that support a class helper functions for that class.
Proper Order of Header File Inclusion
To avoid conflicts with system header files, we include header files in the following order:
We insert namespace declarations and directives after all header file inclusions.
Scope
The scope of a declaration is the portion of a program over which the identifier is visible.
A declaration that is visible to the entire program has global scope. A declaration in a code
block is local to its block. We say that the declaration has block scope. Its scope begins at its
declaration and ends at the end of its block. We say that the identifier is a local variable or
object. It is local to the block.
A declaration with a narrower scope can shadow a declaration with a broader scope, making the
latter temporarily invisible. For example, in the following program the second declaration
shadows the first making the scope of the first declaration of i discontinuous:
Going Out of Scope
When a declaration goes out of scope the program loses access to the variable or
object. Identifying the precise point at which a variable's or object's declaration goes out of
scope is important in object-oriented programming.
In the following code snippet, the counter i, declared within the for statement, goes out of scope
immediately after the closing brace:
The scope of j extends from its definition to just before the end of each iteration. The scope of i
extends throughout the iteration.
Function Rules
Function rules are slightly stricter rules in C++ than in C. These tighter rules enable language
features such as overloading and pass by reference, which are absent in the C language.
Prototypes
The declaration of a function prototype consists of the function's return type, its identifier and all
of its parameter types. The order of its parameter types matters. The parameter identifiers are
optional.
A declaration with no parameters identifies an empty parameter list. The keyword void, which
the C language uses with prototypes that have no parameters is redundant in C++. We omit the
keyword in in C++.
Prototypes Required
C++ enforces type safety by requiring a declaration of the prototype for a function wherever
source code calls the function before its definition. The compiler uses the prototype to check the
argument types in the call against the parameter types in the prototype.
For example, the compiler will generate an error for the following program (printf is
undeclared):
Overloading
The brackets enclose optional information. The return type and the parameter identifiers are not
part of a function's signature.
The compiler preserves uniqueness by renaming each function using a combination of its
identifier, its parameter types and the order of its parameter types. We refer to this renaming as
mangling.
We can assign default values to some or all of a function's parameters. We must however
arrange the parameters with the default values as the rightmost parameters. We specify the
default values in the first function declaration in a translation unit.
The assignment operator followed by a value identifies the default value for each paramter.
Specifying default values for function parameters reduces the need for coding multiple function
definitions where the function logic is identical in every respect except for the values of the
parameters.
For example,
Each call to display() must include enough arguments to initialize the parameters that don't have
default values. In this example, each call must include at least one argument. An argument
corresponding to a parameter that has a default value override the default value.
Pass By Reference
Pass by reference is an alternative mechanism to passing by address available in C++. Pass-by-
reference code is notably more readable than pass-by-address code.
The declaration of a function parameter that is passed by reference takes the form
The & identifies the parameter as an alias for, rather than a copy of, the corresponding argument
in the function call. The identifier is the alias name within the function definition. Any change
to the value of a parameter received by reference changes the value of the corresponding
argument in the function call.
Comparison Example
Consider a function that swaps the values stored in two different memory locations. The
programs listed below compare pass-by-address and pass-by-reference solutions. The program
on the left passes by address using pointers. The program on the right passes by reference:
Reference syntax is slightly simpler. To pass an object by reference, we attach the address of
operator to the parameter type. This operator instructs the compiler to pass by reference. The
corresponding arguments in the function call and the object names within the function definition
are not prefixed by the dereferencing operator required in passing by address.
Technically, the compiler converts each reference to a pointer with an unmodifiable address.
Encapsulation incorporates within a class the structure of data that its objects hold with the logic
that operates on that data to create a clean interface between the class and its clients and hide the
implementation details from them. In C++, we incorporate logic through member
functions. The data members of a class hold information about the structure its objects' state
while the member functions define operations that query, modify and manage that state.
This chapter describes the C++ syntax for declaring member functions in the definition of a
class, defining the member functions in the implementation file and limiting accessibility to the
data values of an object.
Member Functions
Member functions provide communication links between a client and an object. The client calls
the object's member functions to access the object's data and possibly to change its data.
Every member function has direct access to the data members of its class as well as its other
member functions. Each member function receives and passes information between the client
and its object through parameters and a return value.
Consider a Student type with the following definition
Adding a Member Function
Declaration
To declare a member function, we insert its prototype into the definition of its class.
For example, to add display() as a member to our Student type, we write:
The const qualifier identifies the member function as a query. A query cannot change the state
of its object. That is, this query cannot change the value of no or any character in grade.
As a member function, display() has direct access to the object's variables (no and grade). There
is no need to pass these values as parameters in the function prototype.
Definition
• the Student:: prefix on the function name identifies it as a member of our Student type
• the empty parameter list - this function does not receive any values from the client or pass
any values through the parameter list to the client
• the const qualifier identifies this function as a query - this function cannot change any of
the values of the data members
• the data members - the function accesses no and grade, which are defined within the
class but outside the function
Call a Member Function
The client calls a member function in the same way that an instance of a struct refers to one of
its data members. The call consists of the object's identifier, the . operator and the member
function's identifier.
For example, if harry is a Student object, we display its data by calling display() on harry:
The object part of the function call (the part before the member selection operator) identifies the
data that the function should access.
Scope of a Member Function
The scope of a member function is the scope of its class. That is, a member function can access
any other member within class scope. For example, a member function can access another
member function directly:
This global function shares the same identifier with one of the member functions. This definition
does not introduce a conflict, since the client calls each function using different syntax.
To access the global function from within the member function we apply the scope resolution
operator:
Privacy
Data privacy is important in obect-oriented programming. Data members defined using the
struct keyword are exposed to any client. To limit accessibility to any member, C++ lets us hide
member information by classify that member as private.
In an object-oriented solution, the only members that a client should need to access are the class's
communication links. The client should not need direct access to the data that describes an
object's state.
Accessibility Labels
To prohibit external access to any member (data or function), we insert the label private into the
definition of our class:
private:
private identifies all subsequent members listed in the class definition as inaccessible.
public:
public identifies all subsequent members listed in the class definition as accessible.
The class keyword is much more common in object-oriented programming than the struct
keyword. (The C language does not support privacy and a derived type in C can only take the
form of a struct).
Any attempt to access a member that is private generates a complier error:
The function foo() can only access the data stored in harry indirectly through public member
function display().
set() receives a student number and the address of a C-style string that contains the grades from a
client and stores this information in the object's data members:
Communications Links
The set() and display() member functions are the only communication links to any client. Clients
can call set() or display() on any Student object, but none can access the data stored within any
Student object directly.
Empty State
Hiding data members from clients gives us control over which data to accept, which to reject and
which data to expose to clients. Before storing any values received from a client we can validate
the incoming information. If the data is invalid, we reject it and store default values that identify
the object's state as empty.
Upgrade set()
Let us upgrade our set() member function to validate incoming data; that is, to accept incoming
data only if the student number is positive-valued and the grades are A, B, C, D or F, without
exception. If any incoming data fails to meet all of these conditions, let us ignore all of the
incoming data and store values that identify an empty state:
This validation logic ensures that the data stored in a Student object is either valid data or data
that identifies an empty state.
Design Tip
Select one data member to hold a special value that identifies an empty state. Then, to determine
if an object is in an empty state, all we need to do is interrogate that data member.
Upgrading display()
To improve this upgrade, we ensure that our display() member function executes gracefully if
our object happens to be in an empty state:
The input and output objects introduced in the first chapter include public member
functions for formatting data passing through the objects. These objects and the classes that
define their structure belong to the iostream library module and are defined in its <iostream>
header file. The public member functions report the state of each object as well as control
the formatting.
This chapter describes the input and output objects in detail along with their member
functions. This provides examples of the role that member functions play and sets the stage
for subsequent coding of member functions for our custom classes.
Streams
Data enters an application in one stream and leaves the application in another stream. A
stream is a sequence of characters without limitation. The number of characters in a stream
can be indeterminate. An input object stroes data from an input stream in the application's
memory. An output object copies data from the application's memory into an output
stream. Input and output objects operate in FIFO (First In First Out) order. The first
character entering the input object is the first character stored in memory.
The standard input and output objects of the iostream library represent the standard
peripheral devices, such as the keyboard and display.
The input object converts a sequence of characters from its attached input stream into values
of a specified type stored in system memory. The output object converts values of a
specified type stored in system memory into a sequence of characters in its associated output
stream. These objects use the data type associated with the region of memory that holds each
data value to make the appropriate conversions from or to the sequence of characters.
The data in a stream, unlike the data stored in a region of memory, is not associated with any
type. The notion of type is system memory specific.
Output Objects
An output object is an instance of the ostream type, which defines the structure of an output
device. An ostream object copies data from system memory into an output stream; in copying, it
converts the data in system memory into a sequence of characters.
The iostream module defines three standard output objects:
• cout - transfers a buffered sequence of characters to the standard output device
• cerr - transfers an unbuffered sequence of characters to the standard error output device
• clog - transfers a buffered sequence of characters to the standard error output device
Inserting Data
The expression for inserting data into an output stream takes the form
where output is the name of the ostream object. << is the insertion operator. identifier is the
name of the variable or object that holds the data.
For example,
Each expression with an ostream object as its left operand converts the data in its right operand
into a sequence of characters based on the right operand's type.
endl inserts a newline character into the stream and flushes the stream's buffer.
Cascaded Insertion
We may combine these expressions into a single statement that specifies multiple insertions:
We call such repeated use of the insertion operator cascading.
Member Functions
The ostream type supports the following public member functions for formatting conversions:
width
The width(int) member function specifies the minimum width of the next output field:
width(int) applies only to the next field. Note how the field width for the first display of
attendance is 10, while the field width for the second display of attendance is just the minimum
number of characters needed to display the value (2).
fill
The fill(char) member function defines the padding character. The output object inserts this
character into the stream wherever text occupies less space than the specified field width. The
default fill character is ' ' (space). To pad a field with '*''s, we add:
setf, unsetf
The setf() and unsetf() member functions control formatting and alignment. Their control flags
include:
The scope resolution (ios::) on these flags identifies them as part of the ios class.
setf, unsetf – Formatting
The default format in C++ is general format, which outputs data in the simplest, most succinct
way possible (1.34, 1.345E10, 1.345E-20). To output a fixed number of decimal places, we
select fixed format. To specify fixed format, we pass the ios::fixed flag to setf():
Format settings persist until we change them. To unset fixed format, we pass the ios::fixed flag
to the unsetf() member function:
To specify scientific format, we pass the ios::scientific flag to the setf() member function:
To turn off scientific format, we pass the ios::scientific flag to the unsetf() member function.
setf, unsetf – Alignment
The default alignment is right-justified.
To specify left-justification, we pass the ios::left flag to the setf() member function:
To turn off left-justification, we pass the ios::left flag to the unsetf() member function:
precision
The precision() member function sets the precision of subsequent floating-point fields. The
default precision is 6 units. General, fixed, and scientific formats implement precision
differently. General format counts the number of significant digits. Scientific and fixed formats
count the number of digits following the decimal point.
Manipulators
Manipulators are the elegant alternative to member function calls. Manipulators are operands to
the insertion operator. Manipulators that don't take arguments do not include parentheses and are
defined in <iostream>. Those that take arguments include parentheses and are defined in
<iomanip>. We must include <iomanip> whenever we use manipulators that take arguments.
The insertion manipulators include:
Manipulators (except for setw(i) which only modifies the format setting for the next object)
modify the format settings until we change them.
For example,
Reference Example
Notes:
• a double or a float rounds to the requested precision
• char data displays in either character or decimal format
to output its numeric code, we cast the value to an int (the value output for 'd' here is its ASCII
value).
Input Object
The input object is an instance of the istream type, which defines the structure of an input
device. The object extracts data from the input stream and stores it in system memory,
converting the sequence of characters in the stream into equivalent values in system memory.
Extraction
The expression for extracting characters from an input stream takes the form
where inputObject is the name of the input object. >> is the extraction operator. identifier is the
name of the destination variable.
The iostream library defines one standard input object for buffered input: cin.
For example,
Each expression with an istream object as its left operand converts the next sequence of
characters into a value stored in the data type of its right operand.
The cin object skips leading whitespace with numeric, string and character types (in the same
way that scanf("%d"...), scanf("%lf"...), scanf("%s"...) and scanf(" %c"...) skip whitespace in C).
Whitespace
cin treats whitespace in the input stream as a delimiter for numeric and string data types. For C-
style null-terminated string types, cin adds the null byte after the last non-whitespace character
stored in memory:
Cascaded Extraction
The extraction operator (>>), like the insertion operator, processes cascaded input:
Note that reading input in this manner is discouraged (see below).
Overflow
In the above two examples overflow is possible while filling s, since the extraction operator >>
does not restrict the number of characters accepted. If more than 7 characters are in the input
stream some of the data stored may corrupt other memory as shown on the right:
ignore
The ignore() member function extracts characters from the input buffer and discards
them. ignore() doesn't skip leading whitespace. Two versions of ignore() are available:
The no-argument version discards a single character. The two-argument version removes and
discards up to the specified number of characters or up to the specified delimiting character,
whichever occurs first and discards the delimiting character. The default delimiter is end-of-file
(not end-of-line).
get
The get() member function extracts either a single character or a string from the input
buffer. Three versions are available:
getline
getline() behaves like get(), but extracts the delimiter from the input buffer:
getline(), like get(), does not skip leading whitespace and appends a null byte to the sequence of
characters stored in system memory.
Format Control
Manipulators
The manipulators for input objects are listed below:
The argument to setw() should be one more than the maximum number of input characters to be
read. Note that the setw() manipulator is an alternative to get(char*, int), but setw() skips
leading whitespace unless we turn off skipping.
Once a manipulator has modified the format settings of an input object, those settings remain
modified.
We may combine manipulators with input variables directly. For example,
State
The ostream and istream types expose member functions for reporting and changing the state of
their objects. These functions include:
We should check the state of the input object every time we extract a sequence of characters
from the input buffer. If the object has encountered an invalid character, the object will fail and
leave the invalid character in the input buffer. The fail() function will return true.
Before a failed object can continue extracting data, we must clear it of its failed state. The
clear() function resets the object's state to good:
See the following section for a complete example that evaluates the state of the input object.
Robust Validation
The state functions help us validate input robustly. We check the input object's state after each
extraction of a sequence of characters to ensure that the object converted a value and that the
converted value is within valid admissible bounds. We reject invalid input and out-of-bound
values, resetting any failed state, and requesting fresh input as necessary.
getPosInt
To extract a positive int that is not greater than max from the standard input device, we write
1.5 Dynamic Memory
This chapter introduces the basic C++ syntax for allocating and deallocating memory
dynamically in preparation for designing classes with variable memory needs. The chapter
entitled Classes and Resources covers the details involved in coding classes that allocate memory
at run-time.
System Memory
After an operating system loads an executable into RAM, it transfers control to the entry point of
the executable (the main() function). The executable includes memory allocated at compile
time. Throughout execution, the application itself may request more memory from the operating
system. The system attempts to satisfy such requests by reserving more memory in RAM. After
the application terminates and returns control to the operating system, the system recovers all of
the memory that the application used.
Static Memory
The memory that the operating system allocates for the application at load time is called static
memory. Static memory includes the space allocated for program instructions, local variables
and local objects. The compiler determines the amount of static memory that each translation
unit requires. The linker sets the amount of static memory that the entire application requires.
The application's local variables and objects share static memory amongst themselves. When a
variable or object goes out of scope the space becomes available for a newly defined variable or
object. The lifetime of each local variable and object extends from its definition to the closing
brace of the code block within which it is defined:
Note that the variable y may occupy the same physical memory location in RAM as variable
x. This system of sharing memory amongst local variables and objects ensures that application
uses RAM as efficiently as possible.
Static memory is determined at compile-link time and cannot be changed during execution. This
memory is fast, fixed in its amount and allocated at load time.
Dynamic Memory
The memory that an application requests from the operating system during execution is called
dynamic memory.
Dynamic memory is completely distinct from the static memory that the operating system loads
for the application. The operating system reserves dynamic memory at run-time and the
application itself allocates and deallocates regions within this reserved memory.
To keep track of the dynamic memory currently allocated by the application, we store the
address of each region in a pointer variable. We allocate memory for this pointer in static
memory and must keep it alive as long as we need access to that region of dynamic memory.
Consider allocating dynamic memory for an array of n elements. We store the array's address in
a pointer, p, in static memory as illustrated below. We allocate the array itself dynamically and
store the data in its elements sequentially starting at address p.
Lifetime
The lifetime of any dynamic variable or object ends when the pointer that holds its address goes
out of scope. The application must explicitly deallocate the region of dynamic memory reserved
for that variable or object before this happens. If the application fails to deallocate the dynamic
memory reserved for a variable or object, the memory becomes inaccessible and survives until
the application reverts control back to the operating system.
Unlike variables and objects that have been allocated in static memory, those in dynamic
memory do not automatically go of out scope at the closing brace of the code block within which
they were defined. We must manage their deallocation explicitly ourselves.
Dynamic Allocation
The keyword new followed by [n] allocates contiguous space in dynamic memory for an array of
n elements and returns the address of the start of that array.
Dynamic Deallocation
The keyword delete followed by [] and the address of the region of dynamic memory deallocates
the memory that the new[] operator had allocated.
A dynamic array deallocation takes the form
where pointer holds the address of the start of the dynamically allocated array.
For example, to deallocate the memory allocated for the array of n Students above, we write
The nullptr assignment ensures that cpa now holds the null address. This optional assignment
eliminates the possibility of deleting the original address a second time, which is a serious run-
time error. Deleting the nullptr address has no effect.
Omitting the brackets in a deallocation expression deallocates the first element of the array and
leaves the other elements unreachable.
Deallocation does not return dynamic memory to the operating system. The deallocated memory
remains available for subsequent re-allocations. The operating system only reclaims dynamic
memory once the application reverts control back to the system.
A Complete Example
Consider a simple program in which the user enters the number of Students, the program
allocates memory for that number, the user enters data for each student, the program displays the
data accepted and finally the program terminates:
// Dynamic Memory Allocation
// dynamic.cpp
#include <iostream>
#include <cstring>
using namespace std;
class Student {
int no;
char grade[14];
public:
void set(int, const char*);
void display() const;
};
int main( ) {
int n;
Student* cpa = nullptr;
delete [] cpa;
cpa = nullptr;
}
Memory Issues
Important issues arise with dynamic memory allocation and deallocation include:
• memory leaks
• insufficient memory
Memory Leak
A memory leak occurs if an application loses the address of dynamically allocated memory that
has not deallocated. This may occur if
• a pointer to dynamic memory goes out of scope before the application has deallocated
that memory
• a pointer to dynamic memory changes its value before the application has deallocated the
memory starting at that value
Memory leaks are difficult to find because they often do not halt execution immediately. We
might only become aware of their existence indirectly through progressively slower execution or
incorrect results.
Insufficient Memory
Many platforms have sufficient hardware and operating system software to support large
allocations of dynamic memory. On those platforms where memory is severly limited, a realistic
possibility exists that the operating system might not be able to provide the amount of dynamic
memory requested.
If the operating system cannot provide the requested dynamic memory, the application may stop
executing. One method of trapping failures to allocate memory is described in the chapter
entitled The ISO/IEC Standard.
Single Instances
We can allocate dynamic memory for single instances of any type. The allocation and
deallocation syntax is similar to that for arrays.
Allocation
The keyword new without the brackets allocates dynamic memory for a single variable or
object.
A dynamic allocation statement takes the form
Deallocation
The keyword delete without the brackets deallocates dynamic memory at the address specified.
A dynamic deallocation statement takes the form
After going through this unit, the learner will able to:
• Understand the concept of Encapsulation
• Define and learn about Construction and Destruction
• Define the current object
• Learn about classes and resources
• Define member operator
• Learn about helper functions
• Define Custom I/O Operators
• Define Custom File Operators
In programming languages, encapsulation is used to refer to one of two related but distinct
notions, and sometimes to the combination thereof:
• A language mechanism for restricting access to some of the object's components.
• A language construct that facilitates the bundling of data with the methods (or other
functions) operating on that data.
Some programming language researchers and academics use the first meaning alone or in
combination with the second as a distinguishing feature of object-oriented programming, while
other programming languages which provide lexical closures view encapsulation as a feature of
the language orthogonal to object orientation.
The second definition is motivated by the fact that in many OOP languages hiding of
components is not automatic or can be overridden; thus, information hiding is defined as a
separate notion by those who prefer the second definition.
The features of encapsulation are supported using classes in most object-oriented programming
languages, although other alternatives also exist.
1.3 Construction and Destruction
In object-oriented languages, a class is the type that encapsulates state and logic. It describes the
structure of the data that objects hold and the rules under which member functions access and
change that data. A well-encapsulated class has all implementation details hidden within itself:
both its logic and its state structure. Clients communicate with objects of a well-encasulated
class only through an interface of public member functions.
This chapter describes some basic class features and the special member functions that initialize
and tidy up objects. It covers the order of memory allocation and deallocation during object
construction and destruction and overloading of the special function that initializes an object.
A class describes how to interpret the data in a region of memory and the rules for operating on
that data.
Object or Instance
Type denotes the name of the class. instance denotes the name of the object or instance of the
class.
For example, to create an object or instance of our Student class named harry, we write:
Student harry;
The compiler allocates five regions in static memory, each of which holds the data for one
object. Each region contains space for two data members - no and grade. The compiler stores
the program instructions contained in the member functions once.
Instance Variables
We call the data members declared in the class definition the object's instance
variables. Instance variables may (amongst others) be of
Logic
The logic within the member functions of a class is identical for every instance of the class and
there is no need to allocate separate memory for the logic associated with each object. The
compiler only stores the instance variables separately. At run-time each call to a member
function on an object accesses the same code, while accessing different instance variables - those
of the object on which we have called the member function.
For example, calling the same display() function on five different Student objects displays five
different sets of information in the same way:
The memory allocated by the compiler for member function code and object data is illustrated
below:
Class Privacy
C++ implements privacy at the class level. Any member function can access any private
member of its class, including any data member of any instance of its class, including any
instance other than that on which we have called the member function. In other words, there is
no privacy at the object level.
For example, we may refer to a private data member of a Student object within a member
function called on another Student object:
Here, copyIn(const Student& src) copies the values from the private data members of harry into
the private data members of backup.
Constructor
Comprehensive encapsulation requires some mechanism for initializing the data members of an
object at creation-time. Without initialization, an object's data members contain undefined
values until a client calls a modifier on the object and any client can inadvertently 'break' the
object by calling member functions in the wrong order. For instance, a client could call a
member function to read a file before having called the member function to open the file.
For example, the following code generates the output on the right
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
void set(int, const char*);
void display() const;
};
if (n < 1)
valid = false;
else
for (i = 0; g[i] != '\0' && valid; i++)
valid = g[i] >= 'A' && g[i] <= 'F' && g[i] != 'E';
if (valid) {
// accept client's data
no = n;
for (i = 0; g[i] != '\0' && i < 13; i++)
grade[i] = g[i];
grade[i] = '\0'; // set the last byte to the null byte
}
else {
// ignore client's data, set an empty state
no = 0;
grade[0] = '\0';
}
}
int main () {
Student harry;
harry.display(); 12052848
cout << endl;
harry.set(1234, "ABACA");
harry.display();
cout << endl; 1234 ABACA
}
Initially the student number of harry is undefined and the first call to display() outputs
incomprehensible results and may even produce a segmentation fault.
To avoid breaking objects, we initialize their data members to an empty state upon creation and
insert dedicated logic for objects in an empty state in each public member function.
Definition
In C++, the special member function that any object calls at creation-time is called a
constructor. This member function executes preliminary logic and we use it to initialize the
object's instance variables.
The constructor takes its name from the class itself. The prototype for a no-argument constructor
takes the form
Type();
Type is the name of the class. A constructor declarations does not include a return data type.
Example
To define a constructor for our Student class, we declare its prototype explicitly in the class
definition:
If we do not declare a constructor in the class definition, the compiler inserts a default no-
argument constructor with an empty body:
Understanding Order
Construction
The compiler constructs an object in the following order
1. allocates memory for each instance variable in the order listed in the class definition
2. executes the logic within the constructor
The constructor starts executing before any normal member function is called.
Multiple Objects
If we define multiple objects in a single declaration, the compiler creates them in the order
specified by the declaration.
For example, the following code generates the output on the right
// Constructors
// constructors.cpp
#include <iostream>
#include <cstring>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
void set(int, const char*);
void display() const;
};
int main () {
Student harry, josee;
Entering constructor
Entering constructor
harry.set(1234, "ABACA");
josee.set(1235, "BBCDC");
harry.display();
1234 ABACA
cout << endl;
josee.display();
1235 BBCDC
cout << endl;
}
The compiler construct harry first and josee afterwards.
Initializing an object's instance variables in a constructor ensures that the object has a well-
defined state from the instant of its creation. In the above example, we say that harry and josee
are in safe empty states until the set() member function changes their states. If our code calls
member functions on these objects in any 'unusual' order, the objects do not break and the results
are still as expected.
For example,
#include <iostream>
using namespace std;
int main ( ) {
Student harry, josee;
Entering constructor
Entering constructor
harry.display();
0
cout << endl;
josee.display();
0
cout << endl;
harry.set(1234,"ABACA");
josee.set(1235,"BBCDA");
harry.display();
1234 ABACA
cout << endl;
josee.display();
1235 BBCDC
cout << endl;
}
The initial values displayed for each object are their safe empty state values.
The safe empty state value is identical for all objects of the same class.
Destructor
Comprehensive encapsulation also requires some mechanism for tidying up just before the end
of an object's lifetime. An object that has written data to a file may need to flush the file's buffer
before the object going out of scope. An object that has allocated memory dynamically may
need to deallocate that memory before going out of scope. C++ lets us define a special member
function called the destructor that executes automatically at the point of an object's destruction.
Definition
In C++, the special member function that every object calls just before the end of its lifetime is
called the destructor. We code this member function with the terminal logic.
The destructor takes its name from the class itself and prefixes that name with the tilde symbol
(~). The prototype for a destructor takes the form
~Type( );
Type is the name of the class. A destructor does not have any parameters, does not return a
value and does not have a return data type.
An object's destructor
• is called automatically
• cannot be overloaded
• cannot be called explicitly
Example
To define the destructor for our Student class, we declare its prototype in the class definition:
class Student {
int no;
char grade[M+1];
public:
Student();
~Student();
void set(int, const char*);
void display() const;
};
Student::~Student() {
// insert our terminal code here
}
Default Destructor
If we don't declare a destructor in the class definition, the compiler defines the destructor with an
empty body:
Student::~Student() {
}
Understanding Order
Member Function Calls
The object's destructor starts executing only after every normal member function has completed
its execution.
The object cannot call any member function after having called its destructor.
Destruction
Multiple Objects
The compiler destroys objects in opposite order to the order of their creation.
For example, the following code generates the output on the right:
// Constructors and Destructors
// destructors.cpp
#include <iostream>
#include <cstring>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
~Student();
void set(int n, const char* g);
void display() const;
};
Student::Student() {
cout << "Entering constructor" <<
endl;
no = 0;
grade[0] = '\0';
}
int main () {
Student harry, josee; 1234 ABACA
The order of constructing and destroying the elements of an array of objects follows directly
from the order described above.
The compiler creates the elements of an array one at a time from its first element to its last. Each
object calls the no-argument constructor at creation-time. At deallocation, the compiler destroys
the last element first and proceeds sequentially through the array until it destroys the first
element last.
For example, the following code generates the output on the right:
#include <iostream>
#include <cstring>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
~Student();
void set(int, const char*);
void display() const;
};
Student::Student() {
cout << "Entering constructor" <<
endl;
no = 0;
grade[0] = '\0';
}
Student::~Student() {
cout << "Entering destructor for " <<
no << endl;
}
Overloading Constructors
Overloading a class' constructor adds communication options for clients. A client can select the
most appropriate set of arguments to specify at creation time.
For example, to let a client initialize a Student object with a student number and a set of grades,
we define a two-argument constructor with one int parameter and one const char* parameter:
// Two-Argument Constructor
// overload.cpp
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int, const char*);
~Student();
void display() const;
};
Student::Student() {
cout << "Entering 0-arg constructor" <<
endl;
no = 0;
grade[0] = '\0';
}
if (n < 1)
valid = false;
else
for (i = 0; g[i] != '\0' && valid;
i++)
valid = g[i] >= 'A' && g[i] <=
'F'
&& g[i] != 'E';
if (valid) {
// accept client's data
no = n;
for (i = 0; g[i] != '\0' && i < 13;
i++)
grade[i] = g[i];
grade[i] = '\0'; // set last byte to
null
}
else {
// ignore client's data, set empty
state
no = 0;
grade[0] = '\0';
}
}
Student::~Student() {
cout << "Entering destructor for " <<
no << endl;
} Entering 2-arg
constructor
void Student::display() const { Entering 2-arg
cout << no << ' ' << grade; constructor
}
1234 ABACA
int main () {
Student harry(1234,"ABACA"), 1235 BBCDA
josee(1235,"BBCDA"); Entering destructor for
1235
harry.display(); Entering destructor for
cout << endl; 1234
josee.display();
cout << endl;
}
This new constructor includes all of our validation logic. The compiler calls one and only one
constructor at creation. In this example, the compiler does not call the no-argument constructor.
Note that we have replaced the set() member function with the two-argument constructor.
Every member function executes on a specific object; that is, on one particular set of instance
variables. That object is the object on which the client has called the member function. We refer
to the object as the current object for that member function. In other words, the current object is
the region of memory containing the data on which a member function is currently operating.
This chapter describes the mechanism by which a member function accesses the current object
and shows how to refer to the current object from within the member function.
Member Function Access
A member function accesses non-local information through parameters and returns information
to its caller through parameters and possibly a return value. A member function's parameters are
of two distinct kinds:
• explicit - communicate with the client code
• implicit - communicate with the instance variables
Explicit parameters receive information from the client and return information to the client. We
define them explicitly in the function header. Their lifetime is the period during which the
member function is in control of execution.
Implicit parameters the member function to the current object.
The syntax of a call to a normal member function reflects this mechanism. The name of the
object on which the client calls this function represents the implicit parameters, while the
arguments that the client passes to the function initialize the explicit parameters.
Consider the constructors and the calls to the display() member function in the following code
snippet:
// ...
// ...
int main ( ) {
Student harry(1234, "ABACA"), josee(1235, "BBCDA");
harry.display();
1234 ABACA
cout << endl;
josee.display();
1235 BBCDA
cout << endl;
}
The constructor for harry receives data in its explicit parameters and copies that data through its
implicit parameters into the instance variables of its current object. The constructor for josee
receives data in its explicit parameters and copies that data through its implicit parameters into
the instance variables of its current object.
The first call to the display() member function accesses harry through its implicit
parameters. The second callaccesses josee through its implicit parameters.
This
The complete set of instance variables associated with the current object has its own
address. The keyword this returns this address. That is, this holds the address of the region of
memory that contains all of the data stored in the current object. *this refers to the current object
itself; that is, to the entire set of its instance variables.
We use this keyword within a member function to refer to the set of instance variables that the
member function is currently accessing through its implicit parameters.
For example, to upgrade the display() member function to return a copy of the object upon which
it has been called, we write:
return *this;
}
int main() {
Student harry(1234,"ABACA"), backup;
backup = harry.display();
1234 ABACA
cout << endl;
backup.display();
1234 ABACA
cout << endl;
}
The keyword this is only accessible from within a member function and has no meaning outside
member functions.
We can upgrade our definition of display() by returning an unmodifiable reference to the current
object rather than a copy of the object. This would makes a difference if the object was large,
since copying all of its instance variables would be compute intensive. Returning a reference
only involves copying the object's address, which is typically a 4-byte operation:
const Student& Student::display() const {
return *this;
}
The const qualifier on the return type prohibits client code from placing the call to the member
function on the left side of an assignment operator and thereby allowing a change to the instance
variables themselves.
To copy an object's instance variables into the current object, we dereference the keyword and
use *this as the left operand in an assignment expression:
*this = ;
Let us introduce a member function to our Student class called read() that
• construct a local Student object passing the input data to the two-argument constructor
• check the student number to determine if the local object accepted the data
• assign the local object to the current object if the data is valid
void Student::read() {
Since the local object (temp) and the current object are instances of the same class, this member
function can access each of the local object's instance variables directly.
We design and code classes independently of their client applications. In cases where a
client determines the amount of memory that an object requires, we cannot specify the memory
requirements at compile-time and must postpone allocation of that memory until run-time. Only
once the client starts instantiating the object does that object know how much memory the client
requires. To review run-time memory allocation and deallocation see the chapter entitled
Dynamic Memory.
Memory that an object allocates at run-time represents a resource of that object's class. The
management of this resource requires additional logic that was unnecessary for simpler classes
that do not access resources. This additional logic ensures proper handling of the resource and is
often called deep copying and assignment.
This chapter describes how to implement deep copying and deep assignment logic. The member
functions that manage resources are the constructors, the assignment operator and the destructor.
Resource Instance Pointers
A C++ object refers to a resource through a resource instance pointer. This pointer holds the
address of the resource. The address (that is, the resource) lies outside the object's static
memory.
Case Study
Let us upgrade our Student class to accomodate a variable number of grades. The client
specifies the number at run-time. The grades are now a resource. We allocate
• static memory for the resource instance variable (grade)
• dynamic memory for the grade string itself
In this section, we focus on the constructors and the destructor for our Student class. The client
does not copy or assign and we postpone the copying and assignment logic to later sections:
// Resources - Constructor and Destructor
// resources.cpp
#include <iostream>
#include <cstring>
using namespace std;
class Student {
int no;
char* grade;
public:
Student();
Student(int, const char*);
~Student();
void display() const;
};
Student::Student() {
no = 0;
grade = nullptr;
}
if (valid) {
// accept client data
// allocate dynamic memory
no = n;
grade = new char[strlen(g) + 1];
strcpy(grade, g);
}
else {
// set to a safe empty state
no = 0;
grade = nullptr;
}
}
Student::~Student() {
// deallocate previously allocated memory
delete [] grade;
}
void Student::display() const {
cout << no << ' ' <<
((grade != nullptr) ? grade : "");
}
int main ( ) {
Student harry(1234, "ABACA"); 1234
ABACA
harry.display();
cout << endl;
}
The no-argument constructor places the object in a safe empty state. The two-argument
constructor allocates dynamic memory for the resource only if the data received is valid. The
conditional expression in the display() query distinguishes the safe empty state. The destructor
deallocates any memory that the constructor allocated. Deallocating memory at the nullptr
address has no effect.
Deep Copies and Assignments
In a typical class design involving resources, we expect each resource associated with one object
to be independent of the resource associated with another. That is, if we change the resource
data in one object, we expect the resource data in the other object to remain unchanged. In
copying and assigning objects we ensure this resource independence through deep copies and
deep assignments. Deep copies and assignments involve copying the resource data. Shallow
copies and assignments, which involve copying the instance variables only, are only approriate
for class without resources.
Implementing deep copying and assignment logic requires separate allocation of memory. The
resource instance pointer in each object points to a different location in dynamic memory.
For each deep copy, we allocate memory for a new resource and copy the contents of the original
resource into that new memory. We shallow copy only those instance variables that are NOT
resource instance variables. For example, in our Student class, we shallow copy the student
number, but not the address stored in the grade pointer.
The two special member functions that manage allocations and deallocations associated with
deep copies and assignments are:
If we do not declare a copy constructor, the compiler inserts code that implements a shallow
copy. If we do not declare an assignment operator, the compiler inserts code that implements a
shallow assignment.
Copy Constructor
The copy constructor defines the logic for copying from a source object to a newly created object
of the same type. The compiler calls this constructor whenever we
Declaration
Type(const Type&);
For example, we insert the declaration into the definition of our Student class:
// Student.h
class Student {
int no;
char* grade;
public:
Student();
Student(int, const char*);
Student(const Student&);
~Student();
void display() const;
};
Definition
For example, the following code performs a deep copy on objects of our Student class:
// Student.cpp
#include <iostream>
#include <cstring>
using namespace std;
#include "Student.h"
// ...
// shallow copy
no = src.no;
Assignment Operator
The assignment operator defines the logic for copying data from an existing object to another
existing object. The compiler calls this member operator whenever we write expressions of the
form
identifier = identifier
where the left Type is the return type and the right Type is the type of the source operand.
For example, we insert the declaration into the definition of our Student class:
// Student.h
class Student {
int no;
char* grade;
public:
Student();
Student(int, const char*);
Student(const Student&);
Student& operator=(const Student&);
~Student();
void display() const;
};
Definition
For example, the following code performs a deep assignment on objects of our Student class:
// Student.cpp
// ...
To trap a self-assignment (a = a), we compare the address of the current object to the address of
the source object. If the addresses match, we skip the assignment logic altogether. If we
neglected to check for self-assignment, the deallocation statement would deallocate the memory
holding the resource data and we would lose access to the resource resulting in our logic failing
at the call to std::strlen().
Localization
The code in our definition of the copy constructor is identical to most of the code in our
definition of the assignment operator. To avoid duplication we can:
• localize the common code in a private member function and call that member function
from the copy constructor and the assignment operator
• call the assignment operator directly from the copy constructor
Private Member Function
In the following example, we localize the common code is in a private member function named
init() and call this function from our copy constructor and assignment operator call:
void Student::init(const Student& source) {
no = source.no;
if (source.grade != nullptr) {
grade = new char[strlen(source.grade) + 1];
strcpy(grade, source.grade);
}
else
grade = nullptr;
}
Assigning grade to nullptr in the copy constructor ensures that the assignment operator does not
deallocate any memory if called by the copy constructor.
Copies Prohibited
Certain class designs may require that we prohibit any client from copying any instance of a
class. To prohibit copying and/or assigning, we declare the copy constructor and/or the
assignment operator as deleted members of our class:
class Student {
int no;
char* grade;
public:
Student();
Student(int, const char*);
Student(const Student& source) = delete;
Student& operator=(const Student& source) = delete;
~Student();
void display() const;
};
Use of the keyword delete in this context has no relation to its use in deallocating dynamic
memory.
1.6 Member Operators
• binary arithmetic (+ - * / %)
• assignment (= += -= *= /= %=)
• unary pre-fix post-fix plus minus (++ -- + -)
• relational (== < > <= >= !=)
• logical (&& || !)
• insertion, extraction (<< >>)
C++ DOES NOT ALLOW overloading of the following operators (amongst others):
Classifying Operators
We define operators that change the state of their left operand as member operators and operators
that do not change the state of their operands as helper operators. The next chapter covers helper
operators.
// ...
Signature
Every overloaded member operator has its own signature consisting of:
• the operator keyword
• the operation symbol
• the type of its right operand, if any
• the const status of the operation
The compiler evaluates expressions consisting of the operator and the operand type by calling the
member function with the signature that matches the operator symbol, the operand type and the
const status.
Promotion or Narrowing of Arguments
If the compiler cannot find an exact match for an operation's signature, the compiler will attempt
a rather complicated selection process to find an optimal fit, promoting or narrowing the operand
value into a related type if necessary.
Good Design Practice
Programmers expect an operator to perform its operation in a way similar if not identical to the
way that the operator performs on any fundamental type as defined by the core language. +
implies addition of two values in a binary operation (not subtraction). In defining an member
operator we code its logic to be consistent with that of other types.
Binary Operations
A binary operation consists of one operator and two operands. The left operand is the current
object. The operator takes one explicit parameter: the right operand.
The header for a binary member operator takes the form
Type operator symbol (type identifier)
where Type is the type of the evaluated expression. operator identifies some operation. symbol
specifies the operation. type is the type of the right operand. identifier is the right operand's
name.
For example, let us overload the += operator for a char as the right operand, in order to add a
single grade to a Student object:
// Overloading Operators
// operators.cpp
#include <iostream>
#include <cstring>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int, const char*);
void display() const;
Student& operator+=(char g);
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
int main () {
Student harry(975,"BCADB");
Unary Operations
A unary operation consists of one operator and one operand. The left operand is the current
object. The operator does not take any explicit parameters (with one exception - see post-fix
operators below).
The header for a unary member operator takes the form
Type operator symbol()
where Type is the type of the evaluated expression. operator identifies an operation. symbol
identifies the kind of operation.
Pre-Fix Operators
We overload the pre-fix increment/decrement operators to increment/decrement the current
object and return the updated value. The header for a pre-fix operator takes the form
Type& operator++() or Type& operator--()
For example, let us overload the pre-fix increment operator for our Student class so that a pre-fix
expression increases all of the Student's grades by one grade letter, if possible:
// Pre-Fix Operators
// preFixOps.cpp
#include <iostream>
#include <cstring>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int, const char*);
void display() const;
Student& operator++();
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student& Student::operator++() {
for (int i = 0; grade[i] != '\0'; i++)
if (grade[i] == 'F') grade[i] = 'D';
else if (grade[i] != 'A') grade[i]--;
return *this;
}
int main () {
Student harry(975,"BCADB");
harry.display(); 975 BCADB
backup = ++harry;
harry.display(); 975 ABACA
}
Post-Fix Operators
We overload the post-fix operators to increment/decrement the current object after returning its
value. The header for a post-fix operator takes the form
Type operator++(int) or Type operator--(int)
The int type in the header distinguishes the post-fix operators from their pre-fix counterparts.
For example, let us overload the incrementing post-fix operator for our Student class so that a
post-fix expression increases all of the Student's grades by one grade letter, if possible:
// Post-Fix Operators
// postFixOps.cpp
#include <iostream>
#include <cstring>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int, const char*);
void display() const;
Student& operator++();
Student operator++(int);
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student& Student::operator++() {
for (int i = 0; grade[i] != '\0'; i++)
if (grade[i] == 'F') grade[i] = 'D';
else if (grade[i] != 'A') grade[i]--;
return *this;
}
Student Student::operator++(int) {
Student s = *this; // save the original
++(*this); // call the pre-fix operator
return s; // return the original
}
int main () {
Student harry(975,"BCADB");
Student backup;
harry.display(); 975 BCADB
backup = harry++;
backup.display(); 975 BCADB
harry.display(); 975 ABACA
}
We avoid duplicating logic by calling the pre-fix operator from the post-fix operator.
The return values of the pre-fix and post-fix operators differ. The post-fix operator returns a
copy of the current object as it was before any changes took effect. The pre-fix operator returns
a reference to the current object after the changes have taken effect.
Type Conversions
Member operators include type conversion operators, which define implicit conversions to
different types, including fundamental types.
For the following code to compile, the compiler must know how to convert a Student object into
a bool value:
Student harry;
if (harry)
harry.display();
To this effect, we define a conversion operator that returns true if the Student object has valid
data and false if the object is in a safe empty state.
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int, const char*);
void display() const;
operator bool() const;
};
#include "Student.h"
// ...
Design Consideration
Conversion operators easily lead to ambiguities. Good design uses them quite sparingly and
keeps their implementations trivial.
Single-Argument Constructors
A single-argument constructor defines the rule for promoting the value of its parameter to its
class type. This type of constructor defines not only how to construct an object using only a
single argument but also how to convert an argument of that type into an object of the class' type.
.
The following program demonstrates both uses of a single-argument constructor that has been
overloaded to receive an int argument:
// One-Argument Constructor
// oneArgCtor.cpp
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
no = n;
grade[0] = '\0';
}
int main () {
Student harry(975), nancy;
harry.display(); 975
cout << endl;
nancy = (Student)428;
nancy.display(); 428
cout << endl;
}
The first use converts 975 to the Student object harry. The second use casts 428 to a Student
object containing the number 428. Each resultant object holds an empty grade list.
Promotion
For the same result we could omit the cast and let the compiler promote the 428 to a Student
object in the assignment itself:
int main () {
Student harry(975), nancy;
harry.display();
975
cout << endl;
nancy = 428; // promotes an int to a Student
nancy.display();
428
cout << endl;
}
The compiler inserts code that creates a temporary Student object using the single-argument
constructor. The constructor receives the value 428 and initializes no to 428 and grade to an
empty string. Then, the assignment operator performs a shallow copy from the temporary object
to nancy. Finally, the compiler inserts code that destroys the temporary object and removes it
from memory.
Explicit
Limiting the number of single-argument constructors in a class definition avoids potential
ambiguities in automatic conversions of one data type to another.
To prohibit the compiler from using a single-argument constructor for any implicit conversion,
we declare the constructor explicit:
class Student {
int no;
char grade[M+1];
public:
Student();
explicit Student(int);
Student(int, const char*);
void display() const;
};
The second invocation in the example above (nancy = 428) would generate a compiler error
under this class definition.
Temporary Objects
A temporary object has no name and goes out of scope at the end of same statement as the one
within which it is created. For example,
int main () {
Student harry(975), nancy;
harry.display(); 975
cout << endl;
nancy = Student(428); // temporary Student object
nancy.display(); 428
cout << endl;
}
A temporary object provides a compact way of calling the constructor on the object's type.
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
};
Student::Student() {
// safe empty state
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, ""); // assign temporary to current
}
if (valid) {
// accept client data
no = n;
for (i = 0; g[i] != '\0' && i < M; i++)
grade[i] = g[i];
grade[i] = '\0'; // set the last byte to the null
byte
}
else {
// set to a safe empty state
*this = Student(); // assign temporary to current
}
}
harry.display(); 1235
cout << endl;
josee.display(); 0
cout << endl;
empty.display();
cout << endl;
}
The two-argument constructor creates a temporary object in a safe empty state if the validation
fails and assigns that temporary object to the current object.
The single-argument constructor creates a temporary object using the two-argument constructor,
which validates the data, and then assigns the temporary object to the current object.
Classes with Resources
Assigning a temporary object to the current object requires additional code if the object manages
resources. To prevent the assignment operator from releasing not-as-yet-acquired resources we
initialize all resource instance variables to empty values (nullptr).
For example, if our Student object stores its grades in dynamic memory, then we need to add the
highlighted code:
class Student {
int no;
char* grade;
public:
// ...
};
Student::Student() {
// safe empty state
no = 0;
grade = nullptr;
}
Student::Student(int n) {
grade = nullptr;
*this = Student(n, "");
}
if (valid) {
// accept client data
no = n;
grade = new char[std::strlen(g) + 1];
std::strcpy(grade, g);
}
else {
// set to a safe empty state
grade = nullptr;
*this = Student();
}
}
A well-encapsulated class can accept external support in the form of global functions that contain
additional logic. We call these supporting functions helper functions. They access objects solely
through their parameters, all of which are explicit. Since helpers are not members of any class,
they have no implicit parameters. In a typical helper function at least one parameter receives an
object of the class that the function is supporting.
This chapter describes how to define helper functions, including operators, and how to grant
select helpers privileged access to the private members of a class.
Free Helpers
A free helper function is a function that does not need access to the private members of the class
that it supports. Public member functions on the object provide whatever information the helper
function requires. Coupling between a free helper function and the supported class is minimal.
Comparison
Consider a helper function that compares two objects of the same class. The helper returns true
if their data values are identical and false otherwise.
Example
Let us add two queries (getNo() and getGrades()) to our class definition and introduce a free
helper function named areIdentical() to support our Student class. We insert the prototype for
our helper function into the header file after the class definition:
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
int getNo() const { return no; }
const char* getGrades() const { return grade; }
};
#include <iostream>
#include <cstring>
using namespace std;
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
#include <iostream>
using namespace std;
#include "Student.h"
int main () {
Student harry(975,"AAAAA"), josee(975,"AAAAA");
if (areIdentical(harry, josee))
cout << "are identical" << endl;
are identical
else
cout << "are different" << endl;
}
Comparison
To improve readability, let us replace our areIdentical() function with an overloaded == operator
that also takes two Student operands. The header file for the Student class now contains:
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
int getNo() const { return no; }
const char* getGrades() const { return grade; }
};
This helper operator accesses the private data of each operand through queries:
bool operator==(const Student& lhs, const Student& rhs) {
return lhs.getNo() == rhs.getNo() &&
strcmp(lhs.getGrades(), rhs.getGrades()) == 0;
}
Addition
Let us overload the + operator to add a single grade to a Student object and return a copy. The
left left operand is a Student and the right operand is a char. The header file for the Student class
now contains:
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
int getNo() const { return no; }
const char* getGrades() const { return grade; }
};
bool operator==(const Student&, const Student&);
Student operator+(const Student&, char);
Our implementation avoids accessing private data by initializing a new Student object to the left
operand and calling the += member operator on that object to add the right operand:
Student operator+(const Student& s, char grade) {
Student copy = s; // makes a copy
copy += grade; // calls the += operator on copy
return copy; // return updated copy
}
For symmetry, we overload this operator for identical operand types in reverse order. The
complete header file contains:
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
int getNo() const { return no; }
const char* getGrades() const { return grade; }
};
Our implementation calls the original version with the arguments switched:
// Student.cpp
#include <iostream>
#include <iomanip>
using namespace std;
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
#include <iostream>
using namespace std;
#include "Student.h"
int main () {
Student harry(975,"AAAAA");
harry.display();
975 AAAAA
cout << endl;
harry = harry + 'B';
harry.display();
975 AAAAAB
cout << endl;
}
Friendship
Friendship grants access to all private members of a class. By granting a helper function
friendship status, a class allows that helper function access to any of its private members: data
members or member functions. Friendly helper functions minimize class bloat.
To grant a helper function friendship status, we declare the function a friend. A friendship
declaration takes the form
friend Type identifier(...);
where Type is the return type of the function and identifier is the function's name.
For example:
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
friend bool operator==(const Student&, const Student&);
};
#include <iostream>
#include <cstring>
using namespace std;
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
The following client code that uses this implementation produces the results shown on the right:
// Friends
// friends.cpp
#include <iostream>
using namespace std;
#include "Student.h"
int main () {
Student harry(975,"AAAAA"), backup = harry;
Friendly Classes
A class can grant access to its private members to all of the member functions of another
class. A class friendship declaration takes the form
friend class Identifier;
where Identifier is the name of the class to which the host class grants friendship privileges.
For example, an Administrator class needs access to all of the information held within each
Student object. To grant such access, we simply include a class friendship declaration within the
Student class definition
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
friend bool areIdentical(const Student&, const Student&);
friend class Administrator;
};
No Reciprocity or Transitivity
Friendship is neither reciprocal nor transitive. Just because one class is a friend of another class
does not mean that the latter is a friend of the former. Just because a class is a friend of another
class and that other class is a friend of yet another class does not mean that the latter class is a
friend of either of them.
Consider three classes: a Student, an Administrator and an Auditor.
• Let the Auditor be a friend of the Administrator and the Administrator be a friend of the
Student
• Just because the Auditor is a friend of any Administrator and the Administrator is a
friend of any Student, the Administrator is not necessarily a friend of the Auditor and the
Student is not necessarily a friend of the Administrator (lack of reciprocity)
• Just because the Auditor is a friend of any Administrator and the Administrator is a
friend of any Student, the Auditor is not necessarily a friend of any Student (lack of
transitivity)
1.8 Custom I/O Operators
To associate our own classes with those of the iostream library stream classes we overloading
the insertion and extraction operators as helper operators that take iostream objects as left
operands and objects of our class type as right operands.
This chapter describes how to overload the insertion and extraction operators for objects of our
own class type. Thechapter concludes by introducing the standard library's string class, which is
quite useful in managing character stream input of user-defined length.
Design Considerations
The C++ operators for inserting values into an output stream and extracting values from an input
stream are:
The iostream library overloads these operators for std::ostream/std::istream objects as left
operands and fundamental types as right operands. The library also defines the standard output
and input objects (cout, cin).
We adopt scope resolution notation to refer to these entities each of which is defined in the
standard namespace (std::):
#include <iostream>
int main() {
int x;
int main() {
Student harry;
Enter number : 1234
std::cin >> harry;
Enter grades : ABACA
std::cout << harry << std::endl;
1234 ABACA
}
Good Design
In overloading the insertion and extraction operators for our class types, we intend to:
• provide flexibility in the selection of output objects
• use scope resolution on classes and objects defined in the std namespace
• enable cascading as implemented for fundamental types
Selection of Output Objects
To enable selection of the output object, we upgrade our display() member function to receive a
reference to an object of std::ostream type:
// Student.h // Student.cpp
With this code the client chooses the output object (cout, cerr, clog).
Scope Resolution
Scope resolution on the display() function's parameter exposes the class that we actually use,
without exposing any other name defined in the std namespace.
This convention is important in coding header files. Exposing all of the names in any namspace
may lead to unnecessary conflicts with new names or conflicts when several header files are
included in an implementation file.
The preferred method of coding header files is shown on the right:
// Student.h // Student.h
Cascading
Cascading support enables concatenation of operations where the leftmost operand serves as the
left operand for every operation in a compound expression.
std::cout << x;
std::cout << y << z << std::endl;
std::cout << y;
std::cout << z << std::endl;
Finally, the cascaded sub-expression
std::cout << z;
std::cout << std::endl;
Returning a modifiable reference from a function enables the client to use the return value as the
left operand for the operator on its right. The call to an operator that returns a modifiable
reference takes the following form after returning from the function:
The next right operand may be a compound expression with more operators as shown in the
above example.
The header file for our Student class upgraded for these custom operators is:
// Student.h
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display(std::ostream& os) const;
operator bool() const { return no == 0; }
};
The bool conversion operator lets the client know if the object is in a safe empty state.
The implementation file for our upgraded Student class contains:
// Student.cpp
#include <cstring>
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
// student number
std::cout << "Number : ";
is >> no;
// student grades
std::cout << "Grades : ";
is.ignore(); // swallow newline in the buffer
is.getline(grade, M+1); // read string with whitespace
The getline() member function receives in its second parameter the size of the C-style string that
accepts the input data, which including room for the null byte terminator.
The following client uses our upgraded Student class to produce the results shown on the right:
// Custom I/O Operators
// customIO.cpp
#include "Student.h"
int main () {
Student harry;
Number : 1234
std::cin >> harry;
Grades : ABACA
std::cout << harry << std::endl;
1234 ABACA
}
In this solution we assume that the user does not enter characters that would cause a stream
failure. To process failures, we include validation logic that tests the state of the input object and
requests corrected input as required (see the Robust Validation section in the chapter on Input
and Output Examples).
String Class
The Problem
The solution in the above example can fail if the user enters more than M grades. Determining
the amount of memory needed to hold the grades that the user chooses to input requires a
dynamic solution. Since we do not know how much memory to allocate before receiving all of
the user's input, we cannot predict at compile time the maximum size of memory that any client
will require.
The Solution
The string class of the standard library provides a solution that allocates the required amount of
memory at run-time. A string object can accept as many characters as the user enters. The
helper function getline() extracts them from the input stream.
The prototype for this helper function is
std::istream& getline(std::istream&, std::string&, char);
The first parameter receives a modifiable reference to the istream object, the second parameter
receives a modifiable reference to the string object and the third parameter receives the character
delimiter for terminating the extraction (newline by default).
The <string> header file contains the class definition with this prototype. The class definition
includes two member functions for converting its internal data into a C-style null-terminated
string:
• std::string::length() - returns the number of characters in the string
• std::string::c_str() - returns the address of the C-style null-terminated version of the
string
Preliminary Example
The following client extracts an unknown number of characters from the standard input stream
and displays the characters on the standard output object. The extraction involves five steps:
• define a string object to accept the input
• extract the input using the getline() helper function
• query the memory required for a C-style null terminated string
• allocate dynamic memory for the C-style null-terminated string
• copy the input data from the string object into the allocated memory
#include <iostream>
#include <string>
int main( ) {
char* s;
std::string str;
// Student.h
#include <iostream>
class Student {
int no;
char* grade;
public:
Student();
Student(int, const char*);
Student(const Student&);
Student& operator=(const Student&);
~Student();
operator bool() const { return no == 0; }
void display(std::ostream&) const;
};
#include <cstring>
#include <string>
#include "Student.h"
Student::Student() {
// safe empty state
no = 0;
grade = nullptr;
}
Student::Student(int n) {
grade = nullptr;
*this = Student(n, "");
}
Student::Student(int n, const char* g) {
int i;
bool valid = true; // assume valid input, check invalidity
if (valid) {
// accept client data
no = n;
grade = new char[std::strlen(g) + 1];
std::strcpy(grade, g);
}
else {
// set to a safe empty state
grade = nullptr;
*this = Student();
}
}
Student::~Student() {
delete [] grade;
}
// student number
std::cout << "Number : ";
is >> number;
// student grades
std::cout << "Grades : ";
is.ignore();
if (std::getline(is, grade)) {
Student temp(number, grade.c_str());
if (!temp)
s = temp;
}
else {
is.clear();
is.ignore(2000, '\n');
}
return is;
}
The extraction operator only stores the input in the right operand if the getline() function
successfully reads the grade input.
// String Class
// string.cpp
#include <iostream>
#include "Student.h"
int main ( ) {
Student harry;
File stream classes share much of their structure with the standard input and output classes
described in the chapter entitled Input and Output Examples. The iostream library associates its
file objects with the fundamental types by overloading the extraction and insertion operators for
those types. We can overload these operators for objects of our own class type as right operands
in the way that we did in the preceding chapter for the standard input and output objects as left
operands.
This chapter introduces the stream classes that manage communication with file objects. The
chapter describes how to create file objects from these classes, how to read and write data of
fundamental type and how to overload the operators for file objects as left operands and objects
of our own class type as right operands.
File Stream Classes
The iostream library defines three classes for managing communication between file streams
containing 8-bit characters and system memory:
• ifstream - processes input from a file stream
• ofstream - processes output to a file stream
• fstream - processes input from and output to a file stream
These classes provide access to a file stream through separate input and output buffers.
The fstream system header file defines these classes in the std namespace:
#include <fstream>
State Methods
If a file object is not in a good() state, we must reset its state. To reset state we call the modifier:
These member functions operate in the same way as described in the chapter entitled Input and
Output Examples for the standard input and output objects.
File Objects
A file object is an instance of one of the file stream classes. When used with the insertion or
extraction operators, a file object processes data in formatted form. The object uses the host
platform's encoding sequence (ASCII, EBCDIC, Unicode) in converting from stream bytes to
data stored in system memory and vice versa.
File Connection
We can connect a file object to a file for reading, writing or both. The object's destructor closes
the file connection.
Input Objects
We create a file object for reading by defining an instance of the ifstream class. This class
defines a no-argument constructor and one that receives the address of a C-style null-terminated
string holding the name of the file.
For example,
#include <fstream>
std::ifstream fin("input.txt"); // connects fin to input.txt for reading
To connect a file to an existing file object, we call the open() member function on the object.
For example,
#include <fstream>
Output Objects
We create a file object for writing by defining an instance of the ofstream class. This class
defines a no-argument constructor and one that receives the address of a C-style null-terminated
string holding the name of the file.
For example,
#include <fstream>
To connect a file to an existing file object, we call the open() member function on the object.
For example,
#include <fstream>
if (!fout.is_open()) {
std::cerr << "File is not open" << std::endl;
} else {
// file is open
}
Fundamental Types
We use the same syntax to read from a file and write to a file as we use to read from the
standard input object and write to a standard output object (see the chapter entitled Input and
Output Examples for a review).
The standard library contains overloads of the extraction and insertion operators for each
fundamental type with objects of the file stream classes as left operands.
A file object reads from a file under format control using the extraction operator in the same way
as the standard input object (cin) does using the operator.
For example, consider a file with a single record: 12 34 45 abc The output from the following
program is shown on the right:
// Reading a File
// readFile.cpp
#include <iostream>
#include <fstream>
int main() {
int i;
std::ifstream f("input.txt");
if (f.is_open()) {
while (f.good()) {
f >> i;
if (f.good())
std::cout << i << ' ';
12 34 45
else if (!f.eof())
std::cout << "\n**Bad input**\n";
**Bad input**
}
}
}
Writing to a File
A file object writes to its connection under format control using the insertion operator in the
same way as the standard output objects (cout, cerr and clog) do using the operator.
For example, the contents of the file created by the following program are shown on the right
// Writing to a File
// writeFile.cpp
#include <iostream>
#include <fstream>
int main() {
int i;
std::ofstream f("output.txt");
if (f.is_open()) {
f << "Line 1" << std::endl; // record 1
Line 1
f << "Line 2" << std::endl; // record 2
Line 2
f << "Line 3" << std::endl; // record 3
Line 3
}
}
Custom Types
Insertion and extraction operators that have been overloaded for standard output and input
objects respectively as left operands and a custom type as the right operand work without
modification with file objects as left operands. This flexibility has to do with inheritance, which
is described later in the chapter entitled Functions in a Hierarchy. Neither the header file nor the
implementation file require any modification.
Since accepting input from a file does not involve the interaction that we expect across a
standard input device, we typically overload the extraction operator to work differently with file
objects. We overload the operator for an ifstream object as the left operand. Our overload of the
ostream operator holds for output to a file stream as well as to the standard output stream.
For example, we add the prototype for the file extraction helper to the definition of our Student
class:
// Student.h
The implementation file contains the definition of the file extraction operation for our Student
class:
// Student.cpp
#include <cstring>
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
Student& Student::operator+=(char g) {
int i = strlen(grade);
if (i < M) {
// add validation logic here
grade[i++] = g;
grade[i] = '\0';
}
return *this;
}
// student number
std::cout << "Number : ";
is >> no;
// student grades
std::cout << "Grades : ";
is.ignore(); // swallow newline in the buffer
is.getline(grade, M+1); // read string with whitespace
// student number
is >> no;
is.ignore(); // skip whitespace
// student grades
is.getline(grade, M+1); // read string with whitespace
#include "Student.h"
int main ( ) {
Student harry(975, "AABD"), josee(976, "BAAA");
std::ofstream oufile("Student.txt");
oufile << harry << std::endl;
oufile << josee << std::endl;
oufile.close();
std::cout << harry << std::endl; 975 AABD
std::cout << josee << std::endl; 976 BAAA
std::ifstream infile("Student.txt");
infile >> harry;
infile >> josee;
harry += 'B';
josee += 'C';
std::cout << harry << std::endl; 975 AABDB
std::cout << josee << std::endl; 976 BAAAC
}
The records written to the Student.txt file by this program are:
975 AABD
976 BAAA
Nice To Know
Open-Mode Flags
We customize a file object's connection mode through combinations of flags passed as an
optional second argument to the object's constructor or its open() member function.
The flags defining the connection mode are:
• std::ios::in open for reading
• std::ios::out open for writing
• std::ios::app open for appending
• std::ios::trunc open for writing, but truncate if file exists
• std::ios::ate move to the end of the file once the file is opened
Practical combinations of these flags include
• std::ios::in|std::ios::out open for reading and writing (default)
• std::ios::in|std::ios::out|std::ios::trunc open for reading and overwriting
• std::ios::in|std::ios::out|std::ios::app open for reading and appending
• std::ios::out|std::ios::trunc open for overwriting
The vertical bar (|) is the bit-wise or operator.
The Defaults
The standard library overloads the logical negation operator (!) as an alternative to the fail()
query. This operator reports true if the latest operation has failed or if the stream has
encountered a serious error.
We can invoke this operator on any stream object to check the success of the most recent
activity:
if (fin.fail()) { if (!fin) {
std::cerr << "Read error"; std::cerr << "Read error";
fin.clear(); fin.clear();
} }
The operator applied directly to a file object returns the state of the connection:
#include <iostream>
#include <fstream>
if (!fout) {
std::cerr << "File is not open" << std::endl;
} else {
// file is open
Rewinding a Connection
istream, fstream
ostream, fstream
Premature Closing
To close a file connection before the file object has gone out of scope, we call the close()
member function on the object:
// Concatenate Two Files
// concatenate.cpp
#include <fstream>
int main() {
std::ifstream in("src1.txt"); // open 1st source file
std::ofstream out("output.txt"); // open destination file
while (!in.eof())
out << in.get(); // byte by byte copy
in.clear();
in.close(); // close 1st source file
in.open("src2.txt"); // open 2nd source file
while (!in.eof())
out << in.get(); // byte by byte copy
in.clear();
}
#include <iostream>
#include <fstream>
int main() {
std::fstream f("file.txt",
std::ios::in|std::ios::out|std::ios::trunc);
f << "Line 1" << std::endl; // record 1
f << "Line 2" << std::endl; // record 2
f << "Line 3" << std::endl; // record 3
f.seekp(0); // rewind output
f << "****"; // overwrite
char c;
f.seekg(0); // rewind input
f << std::noskipws; // don't skip whitespace
while (f.good()) {
f >> c; // read 1 char at a time
if (f.good())
std::cout << c; // display the character
**** 1
}
Line 2
f.clear(); // clear failed (eof) state
Line 3
}
Carl Linnaeus earned himself the title of Father of Taxonomy after developing this
hierarchy. He grouped the genera of Biology into higher taxa based on shared
similarities. Using his taxa with its modern refinements, we say that the genus Homo, which
includes the species sapiens, belongs to the Family Hominidae, which belongs to the Order
Primates, which belongs to the Class Mammalia, which belongs to the Phylum Chordata, which
belongs to the Kingdom Animalia. For more details see the University Of Michigan Museum Of
Zoology's Animal Diversity Site.
Inheritance is a transitive relationship. A human inherits the structure of a homo, which inherits
the structure of a hominoid, which inherits the structure of a primate, which inherits the structure
of a mammal, which inherits the structure of a chordata, which inherits the structure of an
animal.
The relative position of two classes in a hierarchy identifies their inheritance relationship. A
class lower in the hierarchy is a kind of the class that is higher in the hierarchy. For example, a
dog is a kind of canis, a fox is a kind of vulpes and a human is a kind of homo. In our course
example from the first chapter, a Hybrid Course is a kind of Course. We depict inheritance by
an arrow pointed towards the inherited class.
The Hybrid Course class inherits the entire structure of the Course class.
We depict an instance of a derived class by drawing the instance variables of the derived class in
the direction of increasing addresses with respect to the instance variables of its base class:
A derived class object contains the instance variables of the base class and those of the derived
class, while a base class object only contains the instance variables of the base class.
The terms base class and derived class are C++ specific. Equivalent terms for these object-
oriented concepts include:
• base class - super class, parent class
• derived class - subclass, heir class, child class
Inherited Structure
A derived class contains all of the instance variables and all of the normal member functions of
its base class. The derived class does not inherit these special functions: constructors, destructors
and assignment operators. Normal member functions exclude these special member functions.
Definition of a Derived Class
The definition of a derived class takes the form
class Derived : access Base {
// ...
};
where Derived is the name of the derived class and Base is the name of the base class. access
identifies the access that member functions of the derived class have to the non-private members
of the base class. The default access is private. The most common access modifier is public.
Consider a Student ss a kind of Person. Every Person has a name. Accordingly, we derive our
Student class from a Person class.
The header file for our Student class contains the definitions of the base and derived classes:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
// Student.cpp
#include <cstring>
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
The following client uses this implementation to produce the results on the right:
// Derived Classes
// derived.cpp
#include <iostream>
#include "Student.h"
int main() {
Student harry(975, "ABBAD");
harry.set("Harry"); // inherited
harry.displayName(std::cout); // inherited
harry.display(std::cout); // not inherited
Harry 975 ABBAD
std::cout << std::endl;
}
The main() function refers to the Student type, without mention of the Person type. That is, the
hierarchy itself is invisible to the client. We can upgrade the hierarchy without having to alter
the client code in any way.
Access
A derived class can have different levels of access to the members of its base class. C++
supports three access modifiers:
The member functions of our Student class cannot access the data member of the Person class,
since that member is private to the base class. On the other hand, the main() function and the
member functions of the Student class can access the two member functions of Person, since
those functions are public.
Limiting Access to Derived Classes
The keyword protected limits access to members of the derived classes.
For example, let us limit access to displayName() to derived classes. Then, the main() function
cannot call this member function and we must call it directly from Student::display(). The
header file limits the access:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
void set(const char* n);
protected:
void displayName(std::ostream&) const;
};
// Student.cpp
#include <cstring>
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
We refer to displayName() directly without any scope resolution as if this function is a member
of our Student class.
// Protected Access
// protected.cpp
#include <iostream>
#include "Student.h"
int main() {
Student harry(975, "ABBAD");
harry.set("Harry");
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
}
Avoid Granting Protected Access to Data Members
Granting data members protected access introduces a security hole. If a derived class has
protected access to any data member of its base class, any member function of the derived class
can circumvent any validation procedure in the base class. If the base class in the above example
granted us access to the person data member, we could change its contents from our Student
class to a string of more than N characters, which would probably break our Student object.
Inheritance treats normal member functions and the special member functions that
manage objects differently. A derived class inherits the normal member functions of its base
class. A derived class' destructor automatically calls the base class' destructor and no additional
coding is required. A derived class' default assignment operator automatically calls the base
class' assignment operator and no additional coding is required. Constructors in a class hierarchy
are different because they are involved in the creation of objects: each constructor creates part of
the final object.
This chapter examines how member functions shadow one another in a hierarchy, describes the
order in which the compiler calls constructors and destructors, shows how to define a derived
class' constructor to access a specific base class constructor and finally, describes how to
overload a helper operator for a derived class.
Shadowing
A member function of a derived class shadows the base class member function with the same
name. The compiler binds a call to the member function defined in the derived class, if one
exists.
We use scope resolution to access the base class version of a member function that the derived
class version has shadowed. A call to a shadowed function takes the form
Base::identifier(arguments)
where Base identifies the class that defines the shadowed function.
Example
Consider the following hierarchy. The base and derived classes define separate versions of the
display() member function. The Student class version shadows the Person class version for any
object of Student type:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
void set(const char* n);
void display(std::ostream&) const;
};
#include <cstring>
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
#include <iostream>
#include "Student.h"
int main() {
Person jane;
Student harry(975, "ABBAD");
harry.set("Harry");
harry.display(std::cout);
std::cout << std::endl;
jane.set("Jane Doe");
Harry 975 ABBAD
jane.display(std::cout);
std::cout << std::endl;
Jane Doe
}
Design Tip
C++ shadows member functions on their name and not their signature. To expose a member
function in the base class other than the function with the same signature we insert a using
declaration into the definition of the derived class. A using declaration takes the form
using Base::identifier;
where Base identifies the base class and identifier is the name of the shadowed function.
Example
Let us overload the display() member function in the Person class to take no arguments. We
insert the using declaration in the definition of the derived class to expose the member function
for objects of the derived class.
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
void set(const char* n);
void display(std::ostream&) const;
void display() const;
};
// Student.cpp
#include <cstring>
#include "Student.h"
Student::Student() {
no = 0;
grade[0] = '\0';
}
#include <iostream>
#include "Student.h"
int main() {
Person jane;
Student harry(975, "ABBAD");
harry.set("Harry");
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
harry.display();
Harry
std::cout << std::endl;
jane.set("Jane Doe");
jane.display(std::cout);
Jane Doe
std::cout << std::endl;
}
Constructors
A derived class does not inherit a base class constructor by default. That is, if we don't declare a
constructor in our definition of the derived class, the compiler inserts an empty no-argument
constructor by default.
The compiler constructs an instance of the derived class in four steps in two distinct stages:
In our example, let us define a no-argument constructor for the base class. The header file
declares the no-argument constructor:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
void set(const char* n);
void display(std::ostream&) const;
};
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Student::Student() {
no = 0;
grade[0] = '\0';
}
The following client uses this implementation to produce the result shown on the right:
#include <iostream>
#include "Student.h"
int main() {
Person jane;
Student harry(975, "ABBAD");
harry.set("Harry");
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
jane.set("Jane");
jane.display(std::cout);
Jane
std::cout << std::endl;
}
A call to the base class constructor from a derived class constructor that forwards values takes
the form
where Derived is the name of the derived class and Base is the name of the base class. The
single colon separates the header of the derived-class constructor from the call to the base class
constructor. Omitting this call defaults to a call to the no-argument base class constructor.
Example
Let us replace the set() member function in the base class with a one-argument constructor and
upgrade the Student's two-argument constructor to receive the student's name. The header file
declares a single-argument base class constructor and a triple-argument derived class
constructor:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
The implementation of the single-argument constructor copies the name to the instance variable:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Student::Student() {
no = 0;
grade[0] = '\0';
}
The following client uses this implementation to produce the output shown on the right:
int main() {
Person jane("Jane");
Student harry("Harry", 975, "ABBAD");
harry.display(std::cout); Harry
std::cout << std::endl; 975
ABBAD
jane.display(std::cout);
std::cout << std::endl;
} Jane
Inheriting Base Class Constructors
Consider cases where a derived class constructor forwards values to the base class
constructor without executing any logic on the instance variables of the derived
class. To avoid coding the almost empty derived class constructor, C++ lets us
inherit the base class constructor directly.
The declaration for inheriting a base class constructor takes the form:
using Base::Base;
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
void set(const char* n);
void display(std::ostream&) const;
};
The implementation file remains unchanged. The following client uses this new class definition
to produce the output shown on the right:
// Inherited Constructors
// inheritCtors.cpp
#include <iostream>
#include "Student.h"
int main() {
Instructor john("John");
Person jane("Jane");
Student harry("Harry", 975, "ABBAD");
john.display(std::cout);
John
std::cout << std::endl;
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
jane.display(std::cout);
Jane
std::cout << std::endl;
}
Destructors
A derived class does not inherit the destructor of its base class. Destructors execute in opposite
order to the order of their object's construction. That is, the derived class destructor always
executes before the base class destructor.
Example
Let us define destructors for our base and derived classes that insert messages to standard
output. We declare the destructors in the class definitions:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void set(const char* n);
void display(std::ostream&) const;
};
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Person::~Person() {
std::cout << "Leaving " << person << std::endl;
}
void Person::display(std::ostream& os) const {
os << person << ' ';
}
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::~Student() {
std::cout << "\nLeaving " << no << std::endl;
}
The following client uses this implementation to produce the output shown on the right:
#include <iostream>
#include "Student.h"
int main() {
Person jane("Jane");
Student harry("Harry", 975, "ABBAD");
Helper Operators
A derived class does not support the helper functions of its base class. Each helper function is
dedicated to the class that it supports. The compiler binds a call to a helper function on the basis
of its parameter type(s).
Example
Let us upgrade our Student class to include overloads of the insertion and extraction operators
for both base and derived classes. The header file contains:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
std::istream& operator>>(std::istream&, Person&);
std::ostream& operator<<(std::ostream&, const Person&);
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Student::Student() {
no = 0;
grade[0] = '\0';
}
The following client uses this implementation to produce the output shown on the right:
// Helpers to Derived Classes
// drvdHelpers.cpp
#include <iostream>
#include "Student.h"
int main() {
Person jane;
Student harry; Name: Jane Doe
Name: Harry
std::cin >> jane; Number: 975
std::cin >> harry; Grades: ABBD
std::cout << jane << std::endl;
std::cout << harry << std::endl; Jane Doe
} Harry 975 ABBD
#include <iostream>
const int N = 30;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
Our two-argument constructor forwards the student's name to the single-argument constructor of
the base class and then allocates memory for the grades. Our destructor deallocates that
memory.
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Parson::Person() { }
Student::Student() {
no = 0;
grade = nullptr;
}
#include <iostream>
#include "Student.h"
int main() {
Person jane("Jane");
Student harry("Harry", 975, "ABBAD");
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
jane.display(std::cout);
Jane
std::cout << std::endl;
}
Copy Constructor
The copy constructor of a derived class with a resource calls a constructor of the base class. By
default, that constructor is the no-argument constructor. To override this default, we explicitly
call the base class constructor of our choice.
The header in the definition of the copy constructor for a derived class takes the form
Derived(const Derived& identifier) : Base(identifier) {
// ...
}
The parameter receives an unmodifiable reference to an object of the derived class. The
argument in the call to the base class' constructor is the parameter's identifier.
Copying occurs in two distinct stages and four steps altogether:
1. copy the base class part of the existing object
1. allocate memory for the instance variables of the base class in the order of their
declaration
2. execute the base class' copy constructor
2. copy the derived class part of the existing object
1. allocate memory for the instance variables of the derived class in the order of their
declaration
2. execute the derived class' copy constructor
Example
Let us declare our own definition of the copy constructor for our Student class, but use the
default definition for the Person class:
// Student.h
#include <iostream>
const int N = 30;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
The default copy constructor for the base class performs a shallow copy. The copy constructor
for the derived class calls the base class copy constructor and performs the deep copy itself:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Person::~Person() {}
Student::Student() {
no = 0;
grade = nullptr;
}
Student::~Student() {
delete [] grade;
}
The Student copy constructor executes its logic after the Person copy constructor has executed its
logic.
The following client uses this implementation to produce the output shown on the right:
// Derived Class with a Resource
// drvdResrce.cpp
#include <iostream>
#include "Student.h"
int main() {
Student harry("Harry", 975, "ABBAD"),
harry_ = harry; // calls copy constructor
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
harry_.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
}
Any custom copy assignment operator of a derived class with a resource DOES NOT by default
call the copy assignment operator of the base class. Only the default copy assignment operator
of a derived class calls its base class counterpart. Accordingly in a custom copy assignment
operator of a derived class with a resource, we need to call the base class copy assignmnet
operator explicitly.
We call the base class copy assignment operator from within the body of the derived class
assignment operator (unlike the copy constructor). The call can take one of the following forms:
• a functional expression
• an assignment to the base class part of the current object
The functional expression takes the form
Base::operator=(identifier);
(Base&)*this = identifier;
Base is the name of the base class and identifier is the name of the right operand, which is the
source object for the assignment. Note that the address of the derived object is the same as the
address of the base class part of that object. The compiler distinguishes the call to the base class
operator and a call to the derived class operator by the type of the left operand.
Example
The derived class definition declares a private member function for initializing along with the
assignment operator:
// Student.h
#include <iostream>
const int N = 30;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
The private init() contains the copying logic shared by the constructors and the assignment
operator:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Person::~Person() {}
Student::Student() {
no = 0;
grade = nullptr;
}
Student::~Student() {
delete [] grade;
}
#include <iostream>
#include "Student.h"
int main() {
Student harry("Harry", 975, "ABBAD"), backup;
harry.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
backup = harry;
backup.display(std::cout);
Harry 975 ABBAD
std::cout << std::endl;
}
Direct Call Copy Constructor
The alternative to sharing a member function is a direct call from the copy constructor to the
assignment operator (as in the chapter entitled Classes and Resources). In a direct call design,
we do not need to call the base class copy constructor since the assignment operator will copy
the base class part of the object.
Student::Student(const Student& src) { // calls no-argument base class
constructor
grade = nullptr;
*this = src;
}
After going through this unit, the learner will able to:
• Understand the basic concepts of Polymorphism
• Learn about the virtual functions
• Define abstract base classes
• Learn about function templates
1.2 Introduction to Polymorphism
Uses of virtual function enable run time polymorphism. We can use base class pointer to point
any derived class object. When a base class contains a virtual function and base class pointer
points to a derived class object as well as the derived class has a redefinition of base class virtual
function, then the determination of which version of that function will be called is made on run
time. Different versions of the function are executed based on the different type of object that is
pointed to.
The following example shows polymorphism through virtual function.
class Player {
public:
virtual void showInfo() {
cout << "Player class info" << endl;
}
};
class Footballer : public Player {
public :
void showInfo() {
cout << "Footballer class info" << endl;
}
};
class Cricketer : public Player {
public :
void showInfo() {
cout << "Cricketer class info" << endl;
}
};
int main() {
Player *pPl,p1;
pPl = &p1;
Footballer f1;
Cricketer c1;
pPl->showInfo();
pPl = &f1;
pPl->showInfo();
pPl = &c1;
pPl->showInfo();
system("pause");
return 0;
}
In the above example, Player is the base class, Footballer and Cricketer are the derived class
from Player. Virtual function showInfo is defined in Player class. Then it is redefined in
Footballer and Cricketer class. Here, pPl is the Player class pointer, p1, f1 and c1 are Player,
Footballer and Cricketer class object.
At first, pPl is assigned the address of p1, which is a base class object. If showInfo is now called
using pPl, showInfo function of base class is executed. Next, pPl points to address derived class
(Footballer & Cricketer) . If showInfo is called now, the redefined showInfo function of
Footballer & Cricketer class are executed. The key point is, which version of the showInfo
function will be executed depends on which object is currently pointed by base class pointer.
This decision is taken in run time, so it is an example of a run time polymorphism. This type of
runtime polymorphism using virtual function is achieved by the base class pointer.
Function overloading
One way of achieving polymorphism is function overloading. When two or more functions share
the same name with different parameter list, then this procedure is called function overloading
and the functions are called overloaded function.
The following example shows polymorphism using function overloading.
class Player {
string mName;
int mAge;
string mGameType;
public:
void setInfo(string str) {
mName = str;
cout << "Name :" << mName << endl;
}
void setInfo(string str, int age) {
mAge = age;
mName = str;
cout << "Name :" << mName << " " << "Age :" << mAge << endl;
}
void setInfo(string str, int age, string game) {
mAge = age;
mName = str;
mGameType = game;
cout << "Name :" << mName << " " << "Age :" << mAge << " " <<
"Game Type:" << mGameType << endl;
}
};
int main() {
Player p1;
p1.setInfo("John Sena");
p1.setInfo("C Ronaldo",25);
p1.setInfo("J Kallis",38,"Cricket");
return 0;
}
In this example, three functions have same name setInfo but different parameter list is defined
for each. One function takes only one string parameter, another takes one string and one integer
parameter and the last one takes two string and one integer as parameter. When we call setinfo
function from Player class object p1, compiler looks at the argument list. It matches the
argument list with the signature of three different funciton named setinfo, then one of the
function is called according to the match. For example, when p1.setInfo("John Sena") is used,
out of the three setinfo fucntion, the one with signature void setInfo(string str) is called and this
one is executed. When p1.setInfo("C Ronaldo",25)is used, out of the three setinfo function, the
one with signature void setInfo(string str, int age)is called and this one is executed.
1.3 Overview of Polymorphism
Cardelli and Wegner (1985) expanded Strachey's distinction to accommodate the object-oriented
languages. They distinguished functions that work on
• a finite set of different and potentially unrelated types
o coercion
o overloading
• a potentially infinite number of types across some common structure
o inclusion
o parametric
Ad-Hoc Polymorphism
Coercion may
• narrow the argument type (narrowing)
• widen the argument type (promotion)
For example,
// Ad-Hoc Polymorphism - Coercion
// polyCoercion.cpp
#include <iostream>
int main( ) {
display(10);
One argument (10)
std::cout << std::endl;
display(12.6); // narrowing
One argument (12)
std::cout << std::endl;
display('A'); // promotion
One argument (63)
std::cout << std::endl;
}
Most programming languages support coercion to some extent. For instance, C narrows and
promotes argument types in function calls so that the same function will accept a variety of
argument types, albeit limited.
Overloading
Overloading covers accepted variations in a function's definition to match the argument types to
corresponding parameter types. It is a syntactic abbreviation that associates the same function
identifier with a variety of function definitions by distinguishing its parameter sets. The same
function name can be used with a variety of unrelated argument types. Each set of argument
types has its own function definition. The compiler binds the function call to the matching
function definition.
Unlike coercion, overloading does not involve any common logic shared by the function
definitions with the same identifer. Uniformity is a coincidence rather than the rule. The
definitions may contain totally unrelated logic. Each definition works only on its set of
types. The number of overloaded functions is limited by the number of definitions implemented
in the source code.
C++ compilers implement overloading at compile time by converting the function definitions
into functions with different identifiers: the language mangles the original identifier with the
paramter types to generate an unique name. The linker uses the mangled name to bind the
function call to the appropriate function definition.
For example,
// Ad-Hoc Polymorphism - Overloading
// polymorphismOverloading.cpp
#include <iostream>
int main( ) {
display();
No arguments
std::cout << std::endl;
display(10);
One argument (10)
std::cout << std::endl;
}
The C language does not admit overloading and requires a unique name for each function
definition.
Universal Polymorphism
Universal polymorphism is true polymorphism. Its polymorphic character survives at closer
scrutiny.
Universal polymorphism imposes no restriction on the admissible types. The same function
(logic) applies to a potentially unlimited range of different types.
Inclusion
Inclusion polymorphism covers selection of a member function definition from a set of
definitions based on an object's type. The type is one of the types belonging to an inheritance
hierarchy. The term inclusion refers to one type including another type within the hierarchy. All
function definitions share the same name throughout the hierarchy.
In the hierarchy illustrated below, a HybridCourse uses one mode of delivery while a Course
uses another mode. That is, a mode() query on a Course object reports a different result from a
mode() query on a HybridCourse object.
Operations that are identical for all of the types within the hierarchy require only single
definitions. The assessments() query on a HybridCOurse object invokes the same logic as one
on the Course object. Defining a query for the HybridCourse class would only duplicate
existing code. Inclusion polymorphism eliminates duplicate logic across a hierarchy.
Parametric
Parametric polymorphism covers definitions that share identical logic independently of
type. The logic is common to all possible types, without restriction. The types need not be
related in any way. For example, a function that sorts ints uses the same logic as a function that
sorts doubles. If we have already written a function to sort ints, what would we do to create a
function that sorts doubles. C++ implements parametric polymorphism at compile-time using
template syntax.
1.4 Virtual Functions
Function Bindings
The compiler uses an object's type to bind a function call to a function definition. The object's
type determines which member function in the inheritance hierarchy to call.
Function binding takes either of two forms:
• early binding - based on the object's static type
• dynamic dispatch - based on the object's dynamic type
Early Binding
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
char grade[M+1];
public:
Student();
Student(const char*, int, const char*);
void display(std::ostream&) const;
};
The implementation file is also the same as in the chapter entitled Functions in a Hierarchy:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Student::Student() {
no = 0;
grade[0] = '\0';
}
The main() function in the client code listed below calls the global show() function twice, first
for a Student object and second for a Person object. The compiler binds both calls to the Person
version of display() irrespective of argument type in the call itself. That is, the compiler uses the
parameter type in show() to determine which member function definition to call. We call this an
early binding. The client program produces the output shown on the right:
// Early Binding
// earlyBinding.cpp
#include <iostream>
#include "Student.h"
int main() {
Student harry("Harry", 975, "ABBAD");
Person jane("Jane Doe");
show(harry);
Harry
show(jane);
Jane Doe
}
This is the most efficient binding of a call to a member function's definition, since it occurs at
compile-time. Early binding is the default in C++.
Note that no shadowing occurs inside the global show() function. show() has no way of knowing
which version of display() to select beyond the type of its parameter. (To demonstrate
shadowing, add the statements harry.display() and jane.display() to the main() function.)
Dynamic Dispatch
To call the member function associated with an object's dynamic type, we must wait until run-
time, when the executable code is aware of the object's dynamic type. We call this dynamic
dispatching.
C++ provides the keyword virtual for overriding the default early binding. If this keyword is
present, the compiler inserts code that binds the call to most derived version of the member
function based on the object's dynamic type.
For example, the keyword virtual in the following class definition instructs the compiler to
postpone calling the display() member function definitions until run-time:
// Student.h
#include <iostream>
const int N = 30;
const int M = 13;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
virtual void display(std::ostream&) const;
};
Although the implementation file and the client program have not changed, the show() function
now calls the most derived version of display() based on the type of the argument passed to it
and produces the output shown on the right:
// Late Binding
// lateBinding.cpp
#include <iostream>
#include "Student.h"
int main() {
Student harry("Harry", 975, "ABBAD");
Person jane("Jane Doe");
show(harry);
Harry 975 ABBAD
show(jane);
Jane Doe
}
Polymorphic Objects
A polymorphic object's static type identifies the hierarchy to which the object belongs, regardless
of its current dynamic type. We specify the object's static type through
• a pointer declaration
• a receive-by-address parameter
• a receive-by-reference parameter
For example, the highlighted code specifies the static type pointed to by person:
// Polymorphic Objects - Static Type
#include <iostream>
#include "Student.h"
int main() {
Person* person = nullptr;
// ...
We specify the object's dynamic type on the constructor that allocates memory for it. The region
of memory that the object occupies depends on that type.
The highlighted code specifies the dynamic type. The results produced by this code are listed on
the right:
// Polymorphic Objects - Dynamic Type
// dyanmicType.cpp
#include <iostream>
#include "Student.h"
int main() {
Person* person = nullptr;
Implementing inclusion polymorphism produces efficient and flexible code. That is,
virtual functions reduce code size considerably. Our show() function works on objects of any
type within the Person hierarchy. We only define member functions (display()) for those classes
requiring distinct processing.
During the lifecycle of a client application that uses our hierarchy, we may add several classes to
the hierarchy. Our original coding, without any alteration, selects the most derived version of the
member function in each upgrade of the hierarchy.
The assignment to 0 identifies the function as pure. A pure function must be a virtual member
function.
For example, let us specify that the Person hierarchy provides some implementation of a display
function with the signature display() const. The pure virtual function that exposes this
functionality is:
class iPerson {
public:
virtual void display(std::ostream&) const = 0;
};
Set of Implementations
The definitions of the member functions throughout the class hierarchy with the same signature
as a pure virtual function provide the different implementations available to the client.
Abstract Base Classes
An abstract base class is a class that contains or inherits a pure virtual function that has yet to be
defined. Any attempt to create an instance of an abstract base class generates a compiler error.
Definition
An abstract base class is a set of pure virtual member functions. The class definition contains
their declarations. We call an abstract base class without data members an interface.
Example
Let us define an abstract base class named iPerson for our Person hierarchy and expose the
display() member function to any client that accesses the interface.
The iPerson.h header file contains the definition of our abstract class:
// Abstract Base Class for the Person Hierarchy
// iPerson.h
#include <iostream>
class iPerson {
public:
virtual void display(std::ostream&) const = 0;
};
We derive our Person class from this interface. The header file for our Person and Student
class definitions includes the header file that defines our abstract base class:
// Student.h
#include "iPerson.h"
const int N = 30;
const int M = 13;
Declaring display() a member function of both Person and Student informs the compiler that
each concrete class will implement a version of this member function or access a previously
implemented version.
Unit Tests on an Interface
It is good programming practice to code unit tests for an interface rather than any
implementation. This approach assumes that the interface does not change. We can then
perform unit tests on the interface at every upgrade throughout an object's lifecycle.
Sorter Classes
Consider an interface that exposes the sort() member function of a hierarchy of Sorter
classes. The Sorter module contains all of the implemented algorithms. The interface and the
tester module remain unchanged. With every upgrade to the Sorter module, we can rerun the test
suite on the interface.
The header file for our Sorter module contains:
// iSorter.h
class iSorter {
public:
virtual void sort(float*, int) = 0;
};
class iSorter;
#include <iostream>
#include "iSorter.h"
The implementation file for the Sorter module defines the sort() member functions for the
SelectionSorter class and the BubbleSorter class:
// Sorter.cpp
#include "iSorter.h"
The following program uses this Sorter implementation to produce the output shown on the
right:
// Test Main for Sorter Interface
// test_main.cpp
#include <iostream>
#include <ctime>
#include "Tester.h"
#include "iSorter.h"
int main() {
int n;
std::cout << "Enter no of elements : ";
Enter no of elements : 1000
std::cin >> n;
float* array = new float[n];
delete [] array;
}
1.6 Function Templates
The keyword template identifies the subsequent code block as a template. The less-than greater-
than angle bracket pair (< >) encloses the template's parameter definitions. The ellipsis stands
for more comma-separated parameters.
Each parameter declaration consists of a type and an identifier. Type may be any of
• typename - to identify a type (fundamental or compound)
• class - to identify a type (fundamental or compound)
• int, long, short, char - to identify a non-floating-point fundamental type
• a template parameter
identifier is a placeholder for the argument specified by the client.
For example,
Consider a function that swaps values in two different memory locations. The code for two int
variables may be defined using references:
void swap(int& a, int& b) {
int c;
c = a;
a = b;
b = c;
}
The template for all functions that swap values in this way follows from replacing the specific
type int with the type variable T and inserting the template header:
// Template for swap
// swap.h
template<typename T>
void swap(T& a, T& b) {
T c;
c = a;
a = b;
b = c;
}
#include <iostream>
#include "swap.h" // template
definition
int main() {
double a = 2.3;
double b = 4.5;
long d = 78;
long e = 567;
The arguments in each call are unambiguous in their type and the compiler can specialize the
template appropriately. If the arguments are ambiguous, the compiler reports an error.
Explicit Specialization
A template definition may have exceptions for certain arguments. We define separate
specializations for each exception not covered by the template definition.
Example
The following template definition return the maximum of two arguments and applies to any
fundamental type:
// Maximum Function
// maximum.h
template<typename T>
T maximum(T a, T b) {
return a > b ? a : b;
}
To accomodate the const char* type, we specialize the template explicitly. An explicit
specialization has an empty paramter list:
// Maximum Function
// + explicit specialization for const char*
// maximum.h
template<typename T>
T maximum(T a, T b) {
return a > b ? a : b;
}
#include <iostream>
#include "maximum.h"
int main() {
double a = 2.3;
double b = 4.5;
const char d[4] = "abc";
const char e[4] = "def";
Class Template
The syntax for class templates is similar to that for function templates.
The following template defines Array classes of specified size in static memory. The template
parameters are the element type (T) and the size of the array (N):
// Template for Array Classes
// Array.h
// Class Template
// Template.cpp
#include <iostream>
#include "Array.h"
int main() {
Array<int, 5> a, b;
b = a;
Type Casting
Type safety is an important feature of any strongly typed language. Bypassing the type system
introduces ambiguity to the language itself and is best avoided. Type casting a value from one
type to another type circumvents the type system's type checking facilities. We implement casts
only where absolutely unavoidable and localize them as much as possible.
C++ supportsconstrained type casting through template syntax using one of the following
keywords:
• static_cast<Type>(expression)
• reinterpret_cast<Type>(expression)
• const_cast<Type>(expression)
• dynamic_cast<Type>(expression)
Type refers to the destination type. expression refers to the value being cast to the destination
type.
Related Types
The static_cast<Type>(expression) keyword converts the expression from its evaluated type to
the specified type. By far, this is the most common form of constrained cast.
For example, to cast minutes to a float type, we write:
// Cast to a Related Type
// static_cast.cpp
#include <iostream>
int main() {
double hours;
int minutes;
#include <iostream>
int main() {
int x = 2;
int* p;
std::cout << p;
}
Unrelated Types
The reinterpret_cast<Type>(expression) keyword converts the expression from its evaluated
type to an unrelated type. This cast may produce a value that has the same bit pattern as the
evaluated expression.
For example, to cast an int type to a pointer to an int type, we write:
// Cast to an Unrelated Type
// reinterpret_cast.cpp
#include <iostream>
int main( ) {
int x = 2;
int* p;
std::cout << p;
}
reinterpret_cast<Type>(expression) performs minimal type checking. It rejects conversions
between related types.
For example, the following cast generates a compile-time error:
#include <iostream>
int main( ) {
int x = 2;
double y;
std::cout << y;
}
Unmodifiable Types
The const_cast<Type>(expression) keyword removes the const status from an expression.
A common use case is a function written by another programmer that does not receive a const
parameter but should receive one. If we cannot call the function with a const argument, we
temporarily remove the const status and hope that the function is truly read only.
// Strip const status from an Expression
// const_cast.cpp
#include <iostream>
void foo(int* p) {
std::cout << *p << std::endl;
}
int main( ) {
const int x = 3;
const int* a = &x;
int* b;
int main( ) {
const int x = 2;
double y;
y = const_cast<double>(x); // FAILS
std::cout << y;
}
Inherited Types
The dynamic_cast<Type>(expression) keyword converts the value of an expression from its
current type to another type within the same class hierarchy.
For example, to cast a pointer to derived object d to a pointer to its base class part, we write:
// Cast to a Type within
the Hierarchy
// dynamic_cast.cpp
#include <iostream>
class Base {
public:
void display() const
{ std::cout << "Base\n"; }
};
class Derived : public
Base {
public:
void display() const
{ std::cout <<
"Derived\n"; }
};
int main( ) {
Base* b;
Derived* d = new
Derived;
Base
b =
dynamic_cast<Base*>(d); Derived
// in the same hierarchy
b->display();
d->display();
delete d;
}
dynamic_cast<Type>(expression) performs some type checking. It rejects conversions from a
base class pointer to a derived class pointer if the object is monomorphic.
For example, the following cast generates a compile-time error:
#include <iostream>
class Base {
public:
void display() const { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void display() const { std::cout << "Derived\n"; }
};
int main( ) {
Base* b = new Base;
Derived* d;
d = dynamic_cast<Derived*>(b); // FAILS
b->display();
d->display();
delete d;
}
Note that a static_cast works here and may produce the result shown on the right. However, the
Derived part of the object would then be incomplete. static_cast does not check if the object is
complete, leaving the responsibility to the programmer.
#include <iostream>
class Base {
public:
void display() const
{ std::cout << "Base\n"; }
};
class Derived : public
Base {
public:
void display() const
{ std::cout <<
"Derived\n"; }
};
int main( ) {
Base* b = new Base;
Derived* d;
Base
d =
static_cast<Derived*>(b); Derived
// OK
b->display();
d->display();
delete d;
}
Old-Style Casts
C++ inherited its original casting facilities from C and built directly on them. The constrained
syntax described above is more discriminating than the older syntax, which remains in the
language for legacy reasons. The availability of these older features allows programmers to
bypass the type system and directly weaken the compiler's ability to identify type errors.
For example, even though converting from an int to a pointer to an int is most probably a typing
mistake, C and hence C++ allow:
int x = 2;
int* p;
p = (int*)(x); // MOST PROBABLY A TYPING ERROR (& missing)!
Such syntax is nearly always an error that we expect the type-checking system to trap. Errors
that result from such casts are very difficult to find when embedded within thousands of lines of
code.
C++ supports old-style casting in two distinct forms - plain C-style casts and C++-function-style
casts:
(Type) identifier and Type (identfier)
These forms are interchangeable for fundamental types, but not pointer types. For conversions to
pointer types, only the C-style cast is available.
C-Style Casts
To cast a value from one type to another using a C-style cast, we preface the identifier with the
name of the target type enclosed in parentheses:
// C-Style Casting
// c_cast.cpp
#include <iostream>
int main() {
double hours;
int minutes;
std::cout << "Enter minutes : ";
std::cin >> minutes;
hours = (double) minutes / 60; // C-Style Cast
std::cout << "In hours, this is " << hours;
}
Function-Style Casts
To cast a value from one type to another using a function-style cast, we enclose in parentheses
the variable or object whose value we wish to cast to the target type:
// Function Style Casting
// functionStyleCast.cpp
#include <iostream>
int main() {
double hours;
int minutes;
std::cout << "Enter minutes : ";
std::cin >> minutes;
hours = double(minutes) / 60; // Function-Style Cast
std::cout << "In hours, this is " << hours;
}
Comparison
The old-style casts (for example, (int)x) apply without regard to the category of the
conversion. This syntax does not convey the programmer's intent. An old-style cast can mean
any of the following:
• static_cast
• const_cast
• static_cast + const_cast
• reinterpret_cast
• reinterpret_cast + const_cast
The constrained casts on the other hand by distinguishing the different categories improve the
degree of type checking.
It is always safer type-wise to code a static_cast rather than a C-style cast.