In object-oriented programming, we design classes to behave independently of their client applications. Wherever client code dictates the amount of memory that an object requires, the memory that needs to be allocated is unknown at compile-time. Only once the client has instantiated the object will the 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 to its class. Management of this resource requires additional logic that was unnecessary in simpler designs. This additional logic ensures proper handling of the resource and is often called deep copying and deep 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 lies outside the object’s static memory.
Case Study
Let us upgrade our Student class to accommodate a variable number of grades. The client code specifies the number at run-time. The array of grades is now a dynamically allocated resource. We allocate
· static memory for the resource instance variable (grade)
· dynamic memory for the grade array itself
In this section, we focus on the constructors and the destructor for our Student class. Let us assume that the client does not copy or assign objects of this class. We shall cover the copying and assignment logic in subsequent sections:
// Resources – Constructor and Destructor
// resources.cpp
#include
using namespace std;
class Student {
int no;
float* grade;
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
~Student();
void display() const;
};
Student::Student() {
no = 0;
ng = 0;
grade = nullptr;
}
Student::Student(int sn) {
float g[] = {0.0f};
grade = nullptr;
*this = Student(sn, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_;
// allocate dynamic memory
if (ng > 0) {
grade = new float[ng];
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
grade = nullptr;
}
} else {
grade = nullptr;
*this = Student();
}
}
Student::~Student() {
delete [] grade;
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.display();
}
1234:
89.40
67.80
45.50
The no-argument constructor places the object in a safe empty state. The three-argument constructor allocates dynamic memory for the resource only if the data received is valid. The pre-initialization of grade is a precaution that ensures no inadvertent destruction of memory (see the assignment operator section below). The destructor deallocates any memory that the constructor allocated. Deallocating memory at the nullptr address has no effect.
DEEP COPIES AND ASSIGNMENTS
In designing a class with a resource, we expect the resource associated with one object to be independent of the resource associated with another object. 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 resource independence through deep copying and deep assigning. Deep copying and deep assigning involve copying the resource data. Shallow copying and assigning involve copying the instance variables only and are only appropriate for non-resource instance variables.
Implementing deep copying and assigning requires dynamic allocation and deallocation of memory. The copying process includes not only the non-resource instance variables but also the resource data itself.
In each deep copy, we allocate memory for the underlying resource and copy the contents of the source resource into the destination memory. We shallow copy the instance variables that are NOT resource instance variables. For example, in our Student class, we shallow copy the student number and number of grades, but not the address stored in the grade pointer.
Two special member functions manage allocations and deallocations associated with deep copying and deep copy assigning:
· the copy constructor
· the copy assignment operator
If we do not declare a copy constructor, the compiler inserts code that implements a shallow copy. If we do not declare a copy assignment operator, the compiler inserts code that implements a shallow assignment.
COPY CONSTRUCTOR
1. perform a shallow copy on all resource instance variables
Declaration
The declaration of a copy constructor takes the form
Type(const Type&);
where Type is the name of the class.
To define a copy constructor, we insert its declaration into the class. For example, we insert the following into the definition of our Student class:
// Student.h
class Student {
int no;
float* grade;
int ng;
public:
Student();
Student(int, const char*);
Student(const Student&);
~Student();
void display() const;
};
Definition
The definition of a copy constructor contains logic to
1. perform a shallow copy on all non-resource instance variables
2. allocate memory for each new resource
3. copy data from the source resource to the newly created resource
For example, the following code implements a deep copy on objects of our Student class:
// Student.cpp
#include
using namespace std;
#include “Student.h”
// …
Student::Student(const Student& src) {
// shallow copies
no = src.no;
ng = src.ng;
// allocate dynamic memory for grades
if (src.grade != nullptr) {
grade = new float[ng];
// copy data from the source resource
// to the newly allocated resource
for (int i = 0; i < ng; i++)
grade[i] = src.grade[i];
}
else {
grade = nullptr;
}
}
Since the source data was validated on its original receipt from the client code and privacy constraints have ensured that this data has not been corrupted in the interim, we do not need to revalidate the data in the copy constructor logic.
COPY ASSIGNMENT OPERATOR
The copy assignment operator contains the logic for copying data from an existing object to an existing object. The compiler calls this member operator whenever for client code expressions of the form
identifier = identifier
identifier refers to the name of an object.
Declaration
The declaration of an assignment operator takes the form
Type& operator=(const Type&);
the left Type is the return type and the right Type is the type of the source operand.
To define the copy assignment operator, we insert its declaration into the class definition. For example, we insert the following declaration into the definition of our Student class:
// Student.h
class Student {
int no;
float* grade;
int ng;
public:
Student();
Student(int, const float*, int);
Student(const Student&);
Student& operator=(const Student&);
~Student();
void display() const;
};
Definition
The definition of the copy assignment operator contains logic to:
1. check for self-assignment
2. shallow copy the non-resource instance variables to destination variables
3. deallocate any previously allocated memory for the resource associated with the current object
4. allocate a new memory for the resource associated with the current object
5. copy resource data from the source object to the newly allocated memory of the current object
For example, the following code performs a deep copy assignment on objects of our Student class:
// Student.cpp
// ...
Student& Student::operator=(const Student& source) {
// check for self-assignment
if (this != &source) {
// shallow copy non-resource variables
no = source.no;
ng = source.ng;
// deallocate previously allocated dynamic memory
delete [] grade;
// allocate new dynamic memory, if needed
if (source.grade != nullptr) {
grade = new float[ng];
// copy the resource data
for (int i = 0; i < ng; i++)
grade[i] = source.grade[i];
}
else {
grade = nullptr;
}
}
return *this;
}
To trap a self-assignment from the client code (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 neglect to check for self-assignment, the deallocation statement would release the memory holding the resource data and we would lose access to the source resource resulting in our logic failing at grade[i] = source.grade[i].
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 such duplication and thereby improve maintainability we can localize the logic in a:
· private member function - localize the common code in a private member function and call that member function from both the copy constructor and the copy assignment operator
· direct call - call the assignment operator directly from the copy constructor
Private Member Function
The following solution localizes the common code in a private member function named init() and calls this function from the copy constructor and the copy assignment operator:
void Student::init(const Student& source) {
no = source.no;
ng = source.ng;
if (source.grade != nullptr) {
grade = new float[ng];
for (int i = 0; i < ng; i++)
grade[i] = source.grade[i];
}
else {
grade = nullptr;
}
}
Student::Student(const Student& source) {
init(source);
}
Student& Student::operator=(const Student& source) {
if (this != &source) { // check for self-assignment
// deallocate previously allocated dynamic memory
delete [] grade;
init(source);
}
return *this;
}
Direct Call
The following solution initializes the resource instance variable in the copy constructor to nullptr and calls the copy assignment operator directly:
Student::Student(const Student& source) {
grade = nullptr;
*this = source; // calls assignment operator
}
Student& Student::operator=(const Student& source) {
if (this != &source) { // check for self-assignment
no = source.no;
ng = source.ng;
// deallocate previously allocated dynamic memory
delete [] grade;
// allocate new dynamic memory
if (source.grade != nullptr) {
grade = new float[ng];
// copy resource data
for (int = 0; i < ng; i++)
grade[i] = source.grade[i];
}
else {
grade = nullptr;
}
}
return *this;
}
Assigning grade to nullptr in the copy constructor ensures that the assignment operator does not deallocate any memory if called by the copy constructor.
Assigning Temporary Objects
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 each resource instance variable to an empty value (nullptr).
For example, in the constructors for our Student object, we add the highlighted code:
class Student {
int no;
float* grade;
int ng;
public:
// ...
};
Student::Student() {
no = 0;
ng = 0;
grade = nullptr;
}
Student::Student(int n) {
float g[] = {0.0f};
grade = nullptr;
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_;
// allocate dynamic memory
if (ng > 0) {
grade = new float[ng];
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
grade = nullptr;
}
} else {
grade = nullptr;
*this = Student();
}
}
COPIES PROHIBITED
Certain class designs require prohibiting client code from copying or copy assigning any instance of a class. To prohibit copying and/or copy assigning, we declare the copy constructor and/or the copy assignment operator as deleted members of our class:
class Student {
int no;
float* grade;
int ng;
public:
Student();
Student(int, const float*, int);
Student(const Student& source) = delete;
Student& operator=(const Student& source) = delete;
~Student();
void display() const;
};
The keyword delete used in this context has no relation to deallocating dynamic memory.
SUMMARY
· a class with resources requires custom definitions of a copy constructor, copy assignment operator and destructor
· the copy constructor copies data from an existing object to a newly created object
· the copy assignment operator copies data from an existing object to an existing object
· initialization, pass by value, and return by value client code invokes the copy constructor
· the copy constructor and copy assignment operator should shallow copy only the non-resource instance variables
· the copy assignment operator should check for self-assignment
An important feature of object-oriented programs is support for expressions composed of objects. An expression consists of an operator and a set of operands. The expression evaluates to a value of specific type. In languages like C++, all operators are built-in. The core language defines the logic for the operands of fundamental type. To support expressions with operands of class type, we need to overload the built-in operators for those operands. Overloading an operator entails declaring a corresponding function in the class definition and defining its logic in the implementation file.
This chapter lists the C++ operators that we may overload and describes the syntax for overloading operators using member functions. These functions cover unary and binary operations on the current object. This chapter also describes how to define casting operations and how to use temporary object effectively.
OPERATIONS
In the C++ language, the keyword operator identifies an overloaded operation. We follow the keyword by the operator's symbol. The signature of a member function that overloads an operator consists of the keyword, the symbol and the type of the right operand, if any, within parentheses. The left operand of any member operator is the current object.
For example, an overloaded assignment operator for a Student right operand takes the form
Student& operator=(const Student&);
Candidates for Overloading
C++ lets us overload the following operators (amongst others):
· binary arithmetic (+ - * / %)
· assignment - simple and compound (= += -= *= /= %=)
· unary - pre-fix post-fix plus minus (++ -- + -)
· relational (== < > <= >= !=)
· logical (&& || !)
· insertion, extraction (<< >>)
C++ DOES NOT ALLOW overloading of the following operators (amongst others):
· the scope resolution operator (::)
· the member selection operator (.)
· the member selection through pointer to member operator (.*)
· the conditional operator (?:)
C++ DOES NOT let us introduce or define new operators.
Classifying Operators
We classify operators by the number of operands that they take:
· unary – one operand – post-fix increment/decrement, pre-fix increment/decrement, pre-fix plus, pre-fix minus
· binary – two operand – assignment, compound assignment, arithmetic, relational, logical
· ternary – three operands – conditional operator
Members and Helpers
We overload operators in either of two ways, as:
· member operators – part of the class definition with direct access to the class representation
· helper operators – supporting the class, without direct access to its representation
We prefer to declare operators that change the state of their left operand as member operators. Helper operators are described separately in the chapter entitled Helper Functions.
Overloading a Member Operator
Signature
The signature of an overloaded member operator consists of:
· the operator keyword
· the operation symbol
· the type of its right operand, if any
· the const status of the operation
The compiler binds an expression to 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 to 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.
Type of the Evaluated Expression
The return type of the member function declaration identifies the type of the evaluated expression.
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 its operation on any fundamental type as defined by the core language. For instance, + implies addition of two values in a binary operation (not subtraction). In defining a member operator we code its logic to be consistent with operations on other types.
BINARY OPERATORS
A binary operation consists of one operator and two operands. In a binary member operator, the left operand is the current object and the member function takes one explicit parameter: the right operand.
The declaration of a binary member operator takes the form
return_type operator symbol (type [identifier])
return_type is the type of the evaluated expression. operator identifies the function as an operation. symbol specifies the kind of operation. type is the type of the right operand. identifier is the right operand’s name.
Example
Let us overload the += operator for a float as the right operand, in order to add a single grade to a Student object:
// Overloading Operators
// operators.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
void set(int, const float*, int);
public:
Student();
Student(int, const float*, int);
void display() const;
Student& operator+=(float g);
};
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int sn, const float* g, int ng_) {
set(sn, g, ng_);
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
Student& Student::operator+=(float g) {
if (no != 0 && ng < NG && g >= 0.f && g <= 100.f)
grade[ng++] = g;
return *this;
}
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.display();
harry += 78.23f;
harry.display();
}
1234:
89.40
67.80
45.50
1234:
89.40
67.80
45.50
78.23
UNARY OPERATORS
A unary operation consists of one operator and one operand. The left operand of a unary member operator 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
return_type operator symbol()
return_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 a reference to its updated value. The header for a pre-fix operator takes the form
Type& operator++() or Type& operator--()
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 mark, if possible:
// Pre-Fix Operators
// preFixOps.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
void set(int, const float*, int);
public:
Student();
Student(int, const float*, int);
void display() const;
Student& operator++();
};
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int sn, const float* g, int ng_) {
set(sn, g, ng_);
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
Student& Student::operator++() {
for (int i = 0; i < ng; i++)
if (grade[i] < 99.0f) grade[i] += 1.f;
return *this;
}
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), backup;
harry.display();
backup = ++harry;
harry.display();
backup.display();
}
1234:
89.40
67.80
45.50
1234:
90.40
68.80
46.50
1234:
90.40
68.80
46.50
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
return_type operator++(int) or Type operator--(int)
The int type in the header distinguishes the post-fix operators from their pre-fix counterparts.
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 mark, if possible:
// Post-Fix Operators
// postFixOps.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
void set(int, const float*, int);
public:
Student();
Student(int, const float*, int);
void display() const;
Student& operator++();
Student operator++(int);
};
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int sn, const float* g, int ng_) {
set(sn, g, ng_);
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
Student& Student::operator++() {
for (int i = 0; i < ng; i++)
if (grade[i] < 99.0f) grade[i] += 1.f;
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 () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), backup;
harry.display();
backup = harry++;
harry.display();
backup.display();
}
1234:
89.40
67.80
45.50
1234:
90.40
68.80
46.50
1234:
89.40
67.80
45.50
We avoid duplicating logic by calling the pre-fix operator from the post-fix operator.
Return Types
The return types 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, which accesses the data after the changes have taken effect.
TYPE CONVERSION OPERATORS
Type conversion operators define implicit conversions to different types, including fundamental types.
For the following code to compile, the compiler needs information on how to convert a Student object to a bool value:
Student harry;
if (harry)
harry.display();
bool operator
Let us 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.
We add the following declaration to the class definition:
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
void set(int, const float*, int);
public:
Student();
Student(int, const float*, int);
void display() const;
operator bool() const;
};
We define the conversion operator in the implementation file
#include "Student.h"
// ...
Student::operator bool() const { return no != 0; }
Good Design Tip
Conversion operators easily lead to ambiguities. Good design uses them quite sparingly and keeps their implementations trivial.
CAST OPERATOR
C++ defines the casting operation for a class type in terms of a single-argument constructor. This overloaded constructor defines the rule for casting a value of its parameter type to the class type, as well as constructing an object from an argument of the parameter type.
The following program demonstrates both uses of a single-argument constructor on an int argument:
// Casting
// casting.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
void set(int, const float*, int);
public:
Student();
Student(int);
Student(int, const float*, int);
void display() const;
};
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int sn) {
float g[] = {0.0f};
set(sn, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
set(sn, g, ng_);
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
Student harry(975), nancy;
harry.display();
nancy = (Student)428;
nancy.display();
}
975:
428:
The first use converts 975 to the Student object harry. The second use casts 428 to a Student object containing the number 428. Both objects hold empty grade lists.
Promotion (Optional)
For the same result as the above cast, we may omit the cast operator and defer to the compiler promoting the int value 428 to a Student object before assigning the object to nancy:
int main () {
Student harry(975), nancy;
harry.display();
cout << endl;
nancy = 428; // promotes an int to a Student
nancy.display();
cout << endl;
}
975
428
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 ng to 0. Then, the assignment operator copies the temporary object to nancy. Finally, the compiler inserts code that destroys the temporary object removing it from memory.
Explicit (Optional)
Declaring several single-argument constructors raise the possibility of potential ambiguities in automatic conversions form one type to another. Limiting the number of single-argument constructors in a class definition helps avoid such potential ambiguities.
To prohibit the compiler from using a single-argument constructor for any implicit conversion, we declare that constructor explicit:
class Student {
int no;
char grade[M+1];
void set(int, const float*, int);
public:
Student();
explicit Student(int);
Student(int, const float*, int);
void display() const;
};
With such a declaration, the second invocation in the example at the start of the section above (nancy = 428) would generate a compiler error.
TEMPORARY OBJECTS
C++ compilers create temporary objects in a variety of situations. A temporary object has no name and is destroyed as the last step in evaluating the expression that contains its creation point.
Consider the assignment expression below:
int main () {
Student harry(975), nancy;
harry.display();
nancy = Student(428); // temporary Student object
nancy.display();
}
975:
428:
Localizing Constructor Logic
We can use temporary objects to access validation logic localized within one constructor. Note the temporary object assignments to the current object (*this) in the one-argument and three-argument constructors below:
// Localized Validation
// localize.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display() const;
};
Student::Student() {
// safe empty state
no = 0;
ng = 0;
}
Student::Student(int sn) {
float g[] = {0.0f};
*this = Student(sn, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), josee(1235), empty;
harry.display();
josee.display();
empty.display();
}
1234:
89.40
67.80
45.50
1235:
no data available
The three-argument constructor validates all data received from client code. If the validation fails, this constructor creates a temporary object in a safe empty state and assigns that temporary object to the current object. Note that the single-argument constructor uses the temporary object created by the three-argument constructor to initialize the current object.
Good Design Tip
Using temporary objects to avoid repeated logic is good programming practice. If we update the logic later, there is no chance that we will update the logic in one part of the source code and neglect to update identical logic in another part of the code.
SUMMARY
· C++ allows overloading of most of the operators for operands of class type
· we cannot define new operators or redefine operations on the fundamental types
· the keyword operator followed by a symbol identifies an operation
· the left operand in an overloaded member operator is the current object
· we use member operators to overload operations that modify the left operand
· the int keyword in the signature for increment/decrement operator identifies the post-fix operation distinguishing it from the pre-fix operation
· we use temporary objects to localize logic, which improves maintainability
Once client code calls a member function on an object of its class and before that function returns control to the client code, that member function may need to refer to its host object. We refer to the host object from within a member function as the current object for that function. In other words, the current object is the region of memory that contains the data on which a member function currently operates.
This chapter describes the mechanism by which a member function accesses its current object and shows how to refer to that current object from within the function.
MEMBER FUNCTION PARAMETERS
Member functions receive information through parameters and return information through a return value and possibly some of their parameters. The parameters of any member function are of two distinct kinds:
· explicit - access the client code
· implicit - access the instance variables
Explicit parameters receive information from the client code and return information to the client code. We define them explicitly in the header of a member function. Their lifetime extends from entry to the function to exit from the function. They have function scope.
Implicit parameters tie the member function to the current object.
The syntax of a normal member function call reflects this two-part mechanism. The name of the object on which the client code invokes the function identifies the implicit parameters, while the arguments that the client code passes to the function initialize the explicit parameters.
Consider the constructors and calls to the display() member function in the following code snippet:
// ...
Student::Student(int s, const float* g, int n)
{
cout << "Entering 3-arg constructor\n";
set(s, g, n);
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
float gj[] = {83.4f, 77.8f, 55.5f};
Student harry(1234, gh, 3),
josee(1235, gj, 3);
harry.display();
josee.display();
}
Entering 3-arg constructor
Entering 3-arg constructor
1234:
89.40
67.80
45.50
1235:
83.40
77.80
55.50
Entering destructor for 1235
Entering destructor for 1234
The constructor for harry receives data in its explicit parameters and copies that data to the arguments in the call to the set() member function on the current object, which consists of the instance variables for harry. The constructor for josee receives data in its explicit parameters and copies that data to the arguments in the call to the set() member function on the current object, which consists of the instance variables for josee.
The first client call to the display() member function accesses harry through its implicit parameters. The second client call accesses josee through its implicit parameters.
THIS
The keyword this returns the address of the current object. That is, this holds the address of the region of memory that contains all of the data stored in the instance variables of current object. *this refers to the current object itself; that is, to the complete set of its instance variables.
We use the this keyword within a member function to refer to the complete set of instance variables that that member function is currently accessing through its implicit parameters.
The figure below illustrates usage of the keyword. The top memory map shows two member function and three Student objects. The question mark indicates that the data on which the member function logic operates is unknown. The middle memory map identifies the data when the member functions are called on object b. The bottom memory map identifies the data when the member functions are called on object a.
For example, for the display() member function to return a copy of its host object, we write:
Student Student::display() const {
// ...
return *this;
}
int main() {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), backup;
backup = harry.display();
backup.display();
}
Entering 3-arg constructor
1234:
89.40
67.80
45.50
Entering destructor for 1234
Entering destructor for 1234
Entering destructor for 1234
Entering destructor for 1234
The keyword this has no meaning outside a member function.
Reference to the Current Object
We can improve this definition of display() by returning an unmodifiable reference to the current object rather than a copy of the object. This would improve performance if the object was large, since copying all of its instance variables would be compute intensive. Returning a reference only copies 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 prevents client code from placing the call to the member function on the left side of an assignment operator and thereby enabling a change to the instance variables themselves.
Assigning to the Current Object
To copy the values of the instance variables of one object into those of the current object, we dereference the keyword and use *this as the left operand in an assignment expression:
*this = ;
Example - Validated Input
Let us introduce a member function to our Student class called read() that
· extracts data from standard input
· stores that data in a temporary Student object
· copies the temporary object to the current object only if the temporary object is not empty
· leaves the current object unchanged if the temporary object is empty
To avoid duplicating validation logic, we
· construct a local Student object passing the input data to the three-argument constructor
· let the internal logic determine whether to accept the data or place the object in a safe empty state
· assign the local object to the current object if the temporary object accepted the data
void Student::read() {
int no; // will hold the student number
int ng; // will hold the number of grades
float grade[NG]; // will hold the grades
cout << "Enter student number : ";
cin >> no;
cout << "Enter number of grades : ";
cin >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
cout << "Enter student grade : ";
cin >> grade[i];
}
// construct the temporary object
Student temp(no, grade, ng);
// if data is valid, the student number is non-zero
if (temp.no != 0)
// copy the temporary object into the current object
*this = temp;
}
Since the temporary object (temp) and the current object are instances of the same class, this member function can access each object’s instance variables directly.
SUMMARY
· the current object is the current host object for the member function
· a member function’s parameters consist of implicit and explicit parameters
· a member function’s explicit parameters receive information from the client code
· a member function’s implicit parameters bind the function to the instance variables of current object
· the keyword this holds the address of the current object
· *this refers to the current object itself
· the keyword this is meaningless outside a member function
Object-oriented languages encapsulate the state and logic of a type using a class. A class describes the structure of the data that its objects hold and the rules under which its member functions access and change that data. The implementation of a well-encapsulated class has all details hidden within itself. Client code communicates with the objects of the class solely through its public member functions.
This chapter describes some basic features of classes and introduces the special member functions that initialize and tidy up objects of a class. This chapter covers the order of memory allocation and deallocation during object construction and destruction as well as overloading of the special function that initializes objects.
CLASS FEATURES
Instance of a Class
Each object or instance of a class occupies its own region of memory. The data for the object is stored in that region of memory.
A definition of an object takes the form
Type identifier;
Type is the name of the class. indentifier is the name of the object.
Consider the following class definition
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
void set(int, const float*, int);
void display() const;
};
To create an object of our Student class named harry, we write:
Student harry;
To create five objects of our Student class, we write:
Student a, b, c, d, e;
The compiler allocates five regions in static memory, each of which holds the data for one of the five objects. Each region stores the values of three data members – no, the array grade and ng. The compiler stores the member function instructions separately and only once for all objects of the class.
Instance Variables
We call the data members in the class definition the object’s instance variables. Instance variables may be of
· fundamental type (int, double, char, etc.)
· compound type
· class type (struct or class)
· pointer type (to instances of data types – fundamental or compound)
· reference type (to instances of data types – fundamental or compound)
Logic
The member function instructions apply to all objects of the class and there is no need to allocate separate logic memory for each object. At run-time each call to a member function on an object accesses the same code, but different instance variables – those of the object on which the client code has called the member function.
Consider the following client code. This code calls the same member function (display()) on five different Student objects and displays five different sets of information in the same format:
Student a, b, c, d, e;
// different data for each object – same logic
a.display(); // displays the data stored in a
cout << endl;
b.display(); // displays the data stored in b
cout << endl;
c.display(); // displays the data stored in c
cout << endl;
d.display(); // displays the data stored in d
cout << endl;
e.display(); // displays the data stored in e
cout << endl;
The memory allocated for member function code is shown on the left. The memory allocated for the instance variables is shown on the right:
Class Privacy
C++ compilers apply 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. In other words, privacy is not implemented at the individual object level.
In the following example, we refer to private data members of a Student object within a member function called on a different Student object:
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
void copyFrom(const Student& src);
void set(int, const float*, int);
void display() const;
};
// ...
void Student::copyFrom(const Student& src) {
no = src.no; // copy data from one object to another
ng = src.ng; // copy data from one object to another
for (int i = 0; i < NG; i++)
grade[i] = src.grade[i]; // copy from one object to another
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main() {
Student harry, backup;
float grade[] = {78.9f, 67.5f, 45.5f, 64.35f};
harry.set(975, grade, 4);
backup.copyFrom(harry);
backup.display();
}
975:
78.90
67.50
45.50
64.35
The copyFrom(const Student& src) member function copies the values of the private data members of harry to the private data members of backup.
CONSTRUCTOR
Complete encapsulation requires a mechanism for initializing data members at creation-time. Without initialization at creation-time, an object's data members contain undefined values until client code calls a modifier that sets that data. Before any modifier call, client code can inadvertently 'break' the object by calling a member function that assumes valid data. For instance, client code could call display() before ever calling set().
The following code generates the spurious output on the right
// Calling an Object with Uninitialized Data
// uninitialized.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
void set(int, const float*, int);
void display() const;
};
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main() {
Student harry;
harry.display();
float grade[] = {78.9f, 67.5f, 45.55f};
harry.set(975, grade, 3);
harry.display();
}
12052848
975:
78.90
67.50
45.55
Initially harry's student number, grades and their number are undefined. If the value stored in ng is negative, the first call to display() outputs an unrecognizable student number and no grades. After the call to set(), the data values are defined and the subsequent call to display() produces recognizable results.
To avoid undefined behavior or broken objects, we need to initialize each object to an empty state at creation-time.
Definition
The special member function that any object invokes at creation-time is called its class' constructor. We use the default constructor to execute any preliminary logic and set the object to an empty state.
The default constructor takes its name from the class itself. The prototype for this no-argument constructor takes the form
Type();
Type is the name of the class. Its declaration does not include a return type.
Example
To define a default constructor for our Student class, we declare its prototype explicitly in the class definition:
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
void set(int, const float*, int);
void display() const;
};
We define the constructor in the implementation file:
Student::Student() {
no = 0;
ng = 0;
}
Default Behavior
If we don't declare a constructor in the class definition, the compiler inserts a default no-argument constructor with an empty body:
Student::Student() {
}
Note that this default constructor leaves the instance variables uninitialized.
Understanding Order
Construction
The compiler assembles 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, if any, within the constructor's definition
Member Function Calls
Since the constructor starts executing at instantiation, no normal member function is called before the constructor. Every normal member function is called after instantiation.
Multiple Objects
The compiler creates multiple objects defined in a single declaration in the order specified by the declaration.
For example, the following code generates the output on the right
// Constructors
// constructors.cpp
#include
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
void set(int, const float*, int);
void display() const;
};
// initializes the data members
//
Student::Student() {
cout << "In constructor" << endl;
no = 0;
ng = 0;
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
Student harry, josee;
float gh[] = {89.4f, 67.8f, 45.5f};
float gj[] = {83.4f, 77.8f, 55.5f};
harry.set(1234, gh, 3);
josee.set(1235, gj, 3);
harry.display();
josee.display();
}
In constructor
In constructor
1234:
89.40
67.80
45.50
1235:
83.40
77.80
55.50
The compiler assembles harry and calls its constructor first and assembles josee and calls its constructor afterwards.
Safe Empty State
Initializing an object's instance variables in a constructor ensures that the object has a well-defined state from the time of its creation. In the above example, we say that harry and josee are in safe empty states until the set() member function changes those states. If client code calls member functions on objects in safe empty states, the objects do not break and behave as expected.
For example, the following client code produced the no data available message listed on the right:
// Safe Empty State
// safeEmpty.cpp
#include
using namespace std;
int main ( ) {
Student harry, josee;
harry.display();
josee.display();
float gh[] = {89.4f, 67.8f, 45.5f};
float gj[] = {83.4f, 77.8f, 55.5f};
harry.set(1234, gh, 3);
josee.set(1235, gj, 3);
harry.display();
josee.display();
}
In constructor
In constructor
no data available
no data available
1234:
89.40
67.80
45.50
1235:
83.40
77.80
55.50
The safe empty state is identical for all objects of the same class.
DESTRUCTOR
Complete encapsulation also requires a mechanism for tidying up at the end of an object’s lifetime. An object with dynamically allocated memory needs to deallocate that memory before going out of scope. An object that has written data to a file needs to flush the file’s buffer and close the file before going out of scope.
Definition
The special member function that every object invokes before going out of scope is called its class’ destructor. We code all of the terminal logic in this special member function.
The destructor takes its name from the class itself, prefixing it with the tilde symbol (~). The prototype for a destructor takes the form
~Type();
Type is the name of the class. Destructors have no parameters or return values.
An object’s destructor
· is called automatically
· cannot be overloaded
· should not be called explicitly
Example
To define the destructor for our Student class, we declare its prototype in the class definition:
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
~Student();
void set(int, const float*, int);
void display() const;
};
We define the member function in the implementation file:
Student::~Student() {
// insert our terminal code here
}
Default Behavior
If we don’t declare a destructor in the class definition, the compiler inserts a destructor with an empty body:
Student::~Student() {
}
Understanding Order
Member Function Calls
An object’s destructor starts executing only after every normal member function has completed its execution.
Client code cannot call any member function on an object after the object has called its destructor and gone out of scope.
Destruction
Object destruction proceeds in the following order
1. execute the logic of the object’s destructor
2. deallocate memory for each instance variable in opposite order to that listed in the class definition
Multiple Objects
The compiler destroys sets of objects in opposite order to that of their creation.
For example, the following code generates the output on the right:
// Constructors and Destructors
// destructors.cpp
#include
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
~Student();
void set(int, const float*, int);
void display() const;
};
Student::Student() {
cout << "In constructor" << endl;
no = 0;
ng = 0;
}
// executed before object goes out of scope
//
Student::~Student() {
cout << "In destructor for " << no
<< endl;
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
Student harry, josee;
float gh[] = {89.4f, 67.8f, 45.5f};
float gj[] = {83.4f, 77.8f, 55.5f};
harry.set(1234, gh, 3);
josee.set(1235, gj, 3);
harry.display();
josee.display();
}
In constructor
In constructor
1234:
89.40
67.80
45.50
1235:
83.40
77.80
55.50
In destructor for 1235
In destructor for 1234
The compiler destroys josee first followed by harry.
CONSTRUCTION AND DESTRUCTION OF ARRAYS
The order of construction and destruction of elements of an array of objects follows the order described above.
The compiler creates the elements of an array one at a time sequentially starting from the first element and ending with the last. Each object calls the default constructor at creation-time. When the array goes out of scope, the last element calls its destructor first and the first element calls its destructor last.
For example, the following code generates the output on the right:
// Constructors, Destructors and Arrays
// ctorsDtorsArrays.cpp
#include
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
~Student();
void set(int, const float*, int);
void display() const;
};
Student::Student() {
cout << "In constructor" << endl;
no = 0;
ng = 0;
}
Student::~Student() {
cout << "In destructor for " << no
<< endl;
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
Student a[3];
float g0[] = {89.4f, 67.8f, 45.5f};
float g1[] = {83.4f, 77.8f, 55.5f};
float g2[] = {77.8f, 83.4f, 55.5f};
a[0].set(1234, g0, 3);
a[1].set(1235, g1, 3);
a[2].set(1236, g2, 3);
for (int i = 0; i < 3; i++)
a[i].display();
}
In constructor
In constructor
In constructor
1234:
89.40
67.80
45.50
1235:
83.40
77.80
55.50
1236:
77.80
83.40
55.50
In destructor for 1236
In destructor for 1235
In destructor for 1234
The destructor for element a[2] executes before the destructor for a[1], which executes before the destructor for a[0]. The order of destruction is based on order of construction and not on order of usage.
OVERLOADING CONSTRUCTORS
Overloading a class' constructor adds communication options for client code. Client code can select the most appropriate set of arguments at creation time.
For example, to let client code initialize a Student object with a student number and a set of grades, let us define a three-argument constructor similar to our set() function:
// Overloaded Constructor
// overload.cpp
#include
using namespace std;
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int, const float*, int);
~Student();
void set(int, const float*, int);
void display() const;
};
Student::Student() {
cout << "In constructor" << endl;
no = 0;
ng = 0;
}
Student::Student(int sn, const float* g, int ng_) {
cout << "In 3-arg constructor" << endl;
set(sn, g, ng_);
}
Student::~Student() {
cout << "In destructor for " << no
<< endl;
}
void Student::set(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
no = 0;
ng = 0;
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
float gj[] = {83.4f, 77.8f, 55.5f};
Student harry(1234, gh, 3), josee(1235, gj, 3);
harry.display();
josee.display();
}
In 3-arg constructor
In 3-arg constructor
1234:
89.40
67.80
45.50
1235:
83.40
77.80
55.50
In destructor for 1235
In destructor for 1234
This new constructor includes the validation logic by calling set(). The compiler calls only one constructor at creation-time. In this example, the compiler does not call the default constructor.
No-argument constructor is not always implemented
If the class definition includes the prototype for a constructor with some parameters but does not include the prototype for a no-argument default constructor, the compiler DOES NOT insert an empty-body, no-argument default constructor. The compiler only inserts an empty-body, no-argument default constructor if the class definition does not declare ANY constructor.
If we define a constructor with some parameters, we typically also define a no-argument default constructor. This is important in the creation of arrays of objects. The creation of each element in the array requires a no-argument default constructor.
SUMMARY
· we refer to the data members of an object as its instance variables
· privacy operates at the class level, not at the object level
· the constructor is a special member function that an object invokes at creation time
· the name of the constructor is the name of the class
· the destructor is a special member function that an object invokes at destruction time
· the name of the destructor is the name of the class prefixed by a ~
· the constructor and destructor do not have return types
· the compiler inserts an empty body constructor/destructor into any class definition that does not declare a constructor/destructor
· the compiler does not insert an empty-body, no-argument constructor into a class definition that declares any form of constructor
The primary concept of object-oriented programming is class encapsulation. Encapsulation incorporates within a class the structure of data that its objects store and the logic that operates on that data. In other words, encapsulation creates a clean interface between the class and its clients while hiding the implementation details from its clients. The C++ language describes this logic in the form of functions that are members of the class. The data members of a class hold the information about the state of its objects, while the member functions define the operations that query, modify and manage that state.
This chapter describes the C++ syntax for declaring member functions in a class definition, for defining the member functions in the implementation file and for limiting accessibility to an object's data.
MEMBER FUNCTIONS
The member functions of a class provide the communication links between client code and objects of the class. Client code calls the member functions to access an object's data and possibly to change that data.
We classify member functions into three mutually exclusive categories:
· queries - also called accessor methods - report the state of the object
· modifiers - also called mutator methods - change the state of the object
· special - also called manager methods - create, assign and destroy an object
Every member function has direct access to the members of its class. Each member function receives information from the client code through its parameters and passes information to the client code through its return value and possibly its parameters.
Adding a Member Function
Consider a Student type with the following definition
const int NG = 20;
struct Student {
int no; // student number
float grade[NG]; // grades
int ng; // number of grades filled
};
Function Declaration
To declare a member function to a class, we insert its prototype into the class definition.
For example, to add display() as a member to our Student type, we write:
struct Student {
int no;
float grade[NG];
int ng;
void display() const; // member function
};
The const qualifier identifies the member function as a query. A query does not change the state of its object. That is, this query cannot change the value of no or any grade.
As a member function, display() has direct access to the data members (no and grade). There is no need to pass their values as arguments to the function.
Function Definition
We define display() in the implementation file as follows:
void Student::display() const {
cout << no << ": \n";
for (int i = 0; i < ng; i++)
cout << grade[i] << endl;
}
The definition consists of four elements:
· 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 code or return any values through the parameter list to the client code
· the const qualifier identifies this function as a query - this function cannot change any of the values of the object's data members
· the data members - the function accesses no and grade are defined outside the function's scope but within the class' scope, which encompasses the function's scope
Calling a Member Function
Client code 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, followed by the . operator and then followed by the member function's identifier.
For example, if harry is a Student object, we display its data by calling display() on harry:
Student harry = {975, 78.9f, 69.4f};
harry.display(); // <== client call to the member function
cout << endl;
The object part of the function call (the part before the member selection operator) identifies the data that the function accesses.
Scope of a Member Function
The scope of a member function lies within the scope of its class. That is, a member function can access any other member within its class' scope. For example, a member function can access another member function directly:
struct Student {
int no;
float grade[NG];
int ng;
void display() const;
void displayNo() const;
};
void Student::displayNo() const {
cout << no << ": \n";
}
void Student::display() const {
displayNo(); // calls the member function defined above
for (int i = 0; i < ng; i++)
cout << grade[i] << endl;
}
Accessing Global Functions
A member function can also access a function outside its class' scope. Consider the following global function definition:
void displayNo() {
cout << "Number...\n";
}
Note that this definition does not include any scope resolution identifier. This global function shares the same identifier with one of the member functions, but does not introduce any conflict, since the client code calls each function using different syntax.
displayNo(); // calls the global display function
harry.displayNo(); // calls the member function on harry
To access the global function from within the member function we apply the scope resolution operator:
void Student::display() const {
::displayNo(); // calls the global function
displayNo(); // calls the member function
for (int i = 0; i < ng; i++)
cout << grade[i] << endl;
}
PRIVACY
Data privacy is central to encapsulation. Data members defined using the struct keyword are exposed to client code. Any client code can change the value of a data member. To limit accessibility to any member, the C++ language lets us hide that member within the class by identifying it as private.
Well-designed object-oriented solutions expose to client code only those members that are the class's communication links. In a good design, the client code should not require direct access to any data that describes an object's state or any member function that performs internally directed operations.
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.
To enable external access, we insert the label public:
public:
public identifies all subsequent members listed in the class definition as accessible.
For example, in order to
· hide the data members of each Student object
· expose the member function(s) of the Student type
we insert the accessibility keywords as follows
struct Student {
private:
int no;
float grade[NG];
int ng;
public:
void display() const;
};
Note that the keyword struct identifies a class that is public by default.
class
The keyword class identifies a class that is private by default.
We use the keyword class to simplify the definition of a Student type:
class Student {
int no;
float grade[NG];
int ng;
public:
void display() const;
};
The class keyword is the common keyword in object-oriented programming, much more common than the struct keyword. (The C language does not support privacy and a derived type in C can only be a struct).
Any attempt by the client code to access a private member generates a complier error:
void foo(const Student& harry) {
cout << harry.no; // ERROR - this member is private!
}
The function foo() can only access the data stored in harry indirectly through public member function display().
void foo(const Student& harry) {
harry.display(); // OK
}
Modifying Private Data
If the data members of a class are private, client code cannot initialize their values directly. We use a separate member function for this specific task.
For example, to store data in Student objects, let us introduce a public modifier named set():
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
void set(int, const float*, int);
void display() const;
};
set() receives a student number, the address of an unmodifiable array of grades and the number of grades in that array from the client code and stores this information in the data members of the Student object:
void Student::set(int sn, const float* g, int ng_) {
ng = ng_ < NG ? ng_ : NG;
no = sn; // store the Student number as received
// store the grades as received within the available space
for (int i = 0; i < ng; i++)
grade[i] = g[i];
}
Communications Links
The set() and display() member functions are the only communication links to client code. Clients can call set() or display() on any Student object, but no client code can access the data stored within any Student object directly.
For example, the compiler traps the following privacy breach:
Student harry;
float g[] = {78.9f, 69.4f};
harry.set(975, g, 2);
harry.display();
cout << harry.no; // ERROR .no IS PRIVATE!
EMPTY STATE
Hiding all data members from client code gives us control over which data to accept, which data to reject and which data to expose. We can validate information incoming from client code before storing it in an object. If the data is invalid, we can reject it and store default values that identify the object's state as an empty state.
Upgrading set()
Let us upgrade our set() member function to validate incoming data only if
· the student number is positive-valued
· the grades are between 0 and 100 inclusive
If any incoming data fails to meet one of these conditions, let us ignore all incoming data and store a value that places the object in an empty state. For instance, let us use a student number of 0 to identify an empty state:
void Student::set(int sn, const float* g, int ng_) {
int n = ng_ < NG ? ng_ : NG;
bool valid = true; // assume valid input, check for invalid values
if (sn < 1)
valid = false;
else
for (int i = 0; i < n && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = n;
for (int i = 0; i < n; i++)
grade[i] = g[i];
}
else {
no = 0; // ignore the client's data, set an empty state
}
}
This validation logic ensures that either the data stored in any Student object is valid data or the object is in an empty state.
Design Tip
Select one data member to hold the special value that identifies an empty state. Then, to determine if an object is in an empty state, we only need to interrogate that data member.
Upgrading display()
To match this upgrade, we ensure that our display() member function executes gracefully if our object is in an empty state:
void Student::display() const {
if (no != 0) {
cout << no << ":\n";
for (int i = 0; i < ng; i++)
cout << grade[i] << endl;
} else {
cout << "no data available";
}
}
Looking Forward
Although this upgrade validates data incoming from client code, our class definition still leaves the data in a Student object uninitialized before the first call from client code to set(). To address this deficiency, we will introduce a special member function in the chapter entitled Construction and Destruction.
INPUT AND OUTPUT EXAMPLES
The iostream type that represents the standard input and output objects, like cin and cout, provides member functions for controlling the conversion of characters from the input stream into data types stored in system memory and the conversion of data types stored in system memory into characters sent to the output stream.
cin
The cin object is an instance of the istream type. This object extracts a sequence of characters from the standard input stream, converts that sequence into a specified type and stores that type in system memory.
The general expression for extracting characters from the standard input stream takes the form
cin >> identifier
where >> is the extraction operator and identifier is the name of the destination variable.
For example,
int i;
char c;
double x;
char s[8];
cout << "Enter an integer,\n"
"a character,\n"
"a floating-point number and\n"
"a string : " << flush;
cin >> i;
cin >> c;
cin >> x;
cin >> s; // possible overflow
cout << "Entered " << i << ' '
<< c << ' ' << x << ' ' << s << endl;
Enter an integer,
a character,
a floating-point and
a string : 6 - 9.75 Harry
Entered 6 - 9.75 Harry
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).
// Leading Whitespace
// leading.cpp
#include
using namespace std;
int main() {
char str[11];
cout << "Enter a string : " << endl;
cin >> str;
cout << "|" << str << "|" << endl;
}
Note: _ denotes space
Enter a string :
__abc
|abc|
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:
// Trailing Whitespace
// trailing.cpp
#include
using namespace std;
int main() {
char str[11];
cout << "Enter a string : " << endl;
cin >> str;
cout << "|" << str << "|" << endl;
}
Note: _ denotes space
Enter a string :
__abc__
|abc|
The istream type supports the following member functions:
· ignore(...) - ignores/discards character(s) from the input buffer
· get(...) - extracts a character or a string from the input buffer
· getline(...) - extracts a line of characters from the input buffer
For detailed descriptions of get() and getline(), see the chapter entitled More on Input and Output.
ignore
The ignore() member function extracts characters from the input buffer and discards them. ignore() does not skip leading whitespace. Two versions of ignore() are available:
cin.ignore();
cin.ignore(2000, '\n');
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).
cout
The cout object is an instance of the ostream type. 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 general expression for inserting data into the standard output stream takes the form
cout << identifier
where << is the insertion operator and identifier is the name of the variable or object that holds the data.
For example,
int i = 6;
char c = ' ';
double x = 9.75;
char s[] = "Harry";
cout << i;
cout << c;
cout << x;
cout << c;
cout << s;
cout << endl;
cout << "Data has been written";
6 9.75 Harry
Data has been written
endl inserts a newline character into the stream and flushes the stream's buffer.
We may combine these expressions into a single statement that specifies multiple insertions:
int i = 6;
char c = ' ';
double x = 9.75;
char s[] = "Harry";
cout << i << c << x << c << s << endl;
cout << "Data has been written";
6 9.75 Harry
Data has been written
We call such repeated use of the insertion operator cascading.
The ostream type supports the following public member functions for formatting conversions:
· width(int) - sets the field width to the integer received
· fill(char) - sets the padding character to the character received
· setf(...) - sets a formatting flag to the flag received
· unsetf(...) - unsets a formatting flag for the flag received
· precision(int) - sets the decimal precision to the integer received
width
The width(int) member function specifies the minimum width of the next output field:
// Field Width
// width.cpp
#include
using namespace std;
int main() {
int attendance = 27;
cout << "1234567890" << endl;
cout.width(10);
cout << attendance << endl;
cout << attendance << endl;
}
1234567890
27
27
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:
// Padding
// fill.cpp
#include
using namespace std;
int main() {
int attendance = 27;
cout << "1234567890" << endl;
cout.fill('*');
cout.width(10);
cout << attendance << endl;
}
1234567890
********27
The padding character remains unchanged, until we reset it.
setf, unsetf
The setf() and unsetf() member functions control formatting and alignment. Their control flags include:
Control Flag
Result
ios::fixed
ddd.ddd
ios::scientific
d.ddddddEdd
ios::left
align left
ios::right
align right
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():
// Fixed Format
// fixed.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.width(10);
cout.setf(ios::fixed);
cout << pi << endl;
}
1234567890
3.141593
Format settings persist until we change them. To unset fixed format, we pass the ios::fixed flag to the unsetf() member function:
// Unset Fixed Format
// unsetf.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.width(10);
cout.setf(ios::fixed);
cout << pi << endl;
cout.unsetf(ios::fixed);
cout << pi << endl;
}
1234567890
3.141593
3.14159
To specify scientific format, we pass the ios::scientific flag to the setf() member function:
// Scientific Format
// scientific.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "12345678901234" << endl;
cout.width(14);
cout.setf(ios::scientific);
cout << pi << endl;
}
12345678901234
3.141593e+00
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:
// Left Justified
// left.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.width(10);
cout.fill('?');
cout.setf(ios::left);
cout << pi << endl;
}
1234567890
3.14159???
To turn off left-justification, we pass the ios::left flag to the unsetf() member function:
cout.unsetf(ios::left);
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.
For a precision of 2 under general format, we write
// Precision
// precison.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.setf(ios::fixed);
cout.width(10);
cout.precision(2);
cout << pi << endl;
}
1234567890
3.14
The precision setting applies to the output of all subsequent floating-point values until we change it.
Student Example
The code snippet listed on the left produces the output listed on the right
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
975:
78.90
69.40
SUMMARY
· object-oriented classes may contain both data members and member functions
· the keyword private identifies subsequent members as inaccessible to any client
· the keyword public identifies subsequent members as accessible to any client
· data members hold the information about an object's state
· member functions describe the logic that an object performs on its data members
· a query reports the state of an object without changing its state
· a modifier changes the state of an object
· an empty state is the set of data values that identifies the absence of valid data in an object
· a field width setting only holds for the next field
· all settings other than a field width setting persist until changed
· precision has different meanings under general, scientific, and fixed formats
Various components of object-oriented programs can be reused by other applications. An important aspect of object designing is including flexibility in their memory requirements to enhance reusability. Objects are more reusable by different clients if they account for their own memory needs internally. These memory requirements may depend on problem size, which might not even be known approximately at compile-time. Programming languages address this aspect of reusability by supporting dynamic memory allocation.
This chapter introduces the C++ syntax for allocating and deallocating memory dynamically. The chapter entitled Classes and Resources augments this material with the details required to code classes that manage dynamic memory internally.
STATIC AND DYNAMIC MEMORY
The memory accessible by a C++ program throughout its execution consists of static and dynamic components. After the user starts an application, the operating system loads its executable into RAM and transfers control to that executable's entry point (the main() function). The loaded executable only includes the memory allocated at compile time. During execution, the application may request more memory from the operating system. The system satisfies such requests by allocating 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 has used.
Static Memory
The memory that the operating system allocates for the application at load time is called static memory. Static memory includes the memory allocated for program instructions and program data. The compiler determines the amount of static memory that each translation unit requires. The linker determines the amount of static memory that the entire application requires.
The application's variables and objects share static memory amongst themselves. When a variable or object goes out of scope its memory becomes available for newly defined variables or objects. The lifetime of each local variable and object concludes at the closing brace of the code block within which it has been defined:
// lifetime of a local variable or object
for (int i = 0; i < 10; i++) {
double x = 0; // lifetime of x starts here
// ...
} // lifetime of x ends here
for (int i = 0; i < 10; i++) {
double y = 4; // lifetime of y starts here
// ...
} // lifetime of y ends here
Since the variable x goes out of scope before the variable y is declared, the two variables may occupy the same memory location. This system of sharing memory amongst local variables and objects ensures that each application minimizes its use of RAM.
Static memory requirements are determined at compile-link time and do not change during execution. This memory is fast, fixed in its amount and allocated at load time.
Dynamic Memory
The memory that an application obtains from the operating system during execution is called dynamic memory.
Dynamic memory is distinct from the static memory. While the operating system allocates static memory for an application at load time, the system reserves dynamic memory, allocates it and deallocates it at run-time.
Scope Considerations
To keep track of an application's dynamic memory, we store the address of each allocated region in a pointer variable. We allocate memory for this pointer itself in static memory. This pointer variable must remain in scope as long as we need access to the data in the allocated 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 memory for the elements of the array dynamically and store the data in those elements starting at address p.
Lifetime
The lifetime of any dynamically allocated memory ends when the pointer holding its address goes out of scope. The application must explicitly deallocate the allocated region of dynamic memory within this scope. If the application neglects to deallocate the allocated region, that memory becomes inaccessible and irrecoverable until the application returns control 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 memory dynamically for an array of n elements and returns the address of the array's first element.
Dynamic allocation of arrays takes the form
pointer = new Type[size];
where Type is the type of the array's elements.
For example, to allocate dynamic memory for an array of n Students, we write
int n; // the number of students
Student* student = nullptr; // the address of the dynamic array
cout << "How many students in this section? ";
cin >> n;
student = new Student[n]; // allocates dynamic memory
The nullptr keyword identifies the address pointed to as the null address. This keyword is an implementation constant. Initialization to nullptr ensures that student is not pointing to any valid dereferencable address. The size of the array is a run-time variable and not an integer constant or constant expression. Note that the size of an array allocated in static memory must be an integer constant or constant expression.
DYNAMIC DEALLOCATION
The keyword delete followed by [] and the address of a dynamically allocated region of memory deallocates the memory that the corresponding new[] operator had allocated.
Dynamic deallocation of arrays takes the form
delete [] pointer;
where pointer holds the address of the dynamically allocated array.
For example, to deallocate the memory allocated for the array of n Students above, we write
delete [] student;
student = nullptr; // optional
The nullptr assignment ensures that student 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.
Note that omitting the brackets in a deallocation expression deallocates the first element of the array, leaving the other elements inaccessible.
Deallocation does not return dynamic memory to the operating system. The deallocated memory remains available for subsequent dynamic allocations. The operating system only reclaims all of the dynamically allocated memory once the application has returned control to the system.
A Complete Example
Consider a simple program in which the user enters a number and the program allocates memory for that number of Students. The user then enters data for each student. The program displays the data stored, deallocates the memory and terminates:
// Dynamic Memory Allocation
// dynamic.cpp
#include
#include
using namespace std;
struct Student {
int no;
float grade[2];
};
int main( ) {
int n;
Student* student = nullptr;
cout << "Enter the number of students : ";
cin >> n;
student = new Student[n];
for (int i = 0; i < n; i++) {
cout << "Student Number: ";
cin >> student[i].no;
cout << "Student Grade 1: ";
cin >> student[i].grade[0];
cout << "Student Grade 2: ";
cin >> student[i].grade[1];
}
for (int i = 0; i < n; i++) {
cout << student[i].no << ": "
<< student[i].grade[0] << ", " << student[i].grade[1]
<< endl;
}
delete [] student;
student = nullptr;
}
MEMORY ISSUES
Issues regarding dynamic memory allocation and deallocation include:
1. memory leaks
2. insufficient memory
Memory Leak
Memory leaks are one of the most important bugs in object-oriented programming. A memory leak occurs if an application loses the address of dynamically allocated memory before that memory has been deallocated. This may occur if
· the pointer to dynamic memory goes out of scope before the application deallocates that memory
· the pointer to dynamic memory changes its value before the application deallocates the memory starting at the address stored in that pointer
Memory leaks are difficult to find because they often do not halt execution immediately. We might only become aware of their existence indirectly through subsequently incorrect results or progressively slower execution.
Insufficient Memory
On small platforms where memory is severely 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 dynamically allocate the requested memory, the application may throw an exception and stop executing. The topic of exception handling is beyond the scope of these notes. One method of trapping a failure to allocate memory is described in the chapter entitled The ISO/IEC Standard.
SINGLE INSTANCES (OPTIONAL)
Although dynamic memory is often allocated for data structures like arrays, we can also allocate dynamic memory for single instances of any type. The allocation and deallocation syntax is similar to that for arrays. We simply remove the brackets.
Allocation
The keyword new without brackets allocates dynamic memory for a single variable or object.
A dynamic allocation statement takes the form
pointer = new Type;
For example, to store one instance of a Student in dynamic memory, we write
Student* harry = nullptr; // a pointer in static memory
harry = new Student; // points to a Student in dynamic memory
// we must deallocate harry later!
Deallocation
The keyword delete without brackets deallocates dynamic memory at the address specified.
A dynamic deallocation statement takes the form
delete pointer;
delete takes the address that was returned by the new operator.
For example, to deallocate the memory for harry, we write
delete harry;
harry = nullptr; // good programming style
SUMMARY
· the memory available to an application at run-time consists of static memory and dynamic memory
· static memory lasts the lifetime of the application
· the linker determines the amount of static memory used by the application
· the operating system provides dynamic memory to an application at run-time upon request
· the keyword new [] allocates a contiguous region of dynamic memory and returns its starting address
· we store the address of dynamically allocated memory in static memory
· delete [] deallocates contiguous memory starting at the specified address
· allocated memory must be deallocated within the scope of the pointer that holds its address
Object-oriented languages inherit from their non-object-oriented predecessors the concepts of variable declarations, data types, data structures, logic constructs, and modular programming. The C++ language inherits these features from the C language (see IPC Notes for a detailed exposition).
This chapter elaborates on these inherited concepts and introduces the new concepts of references and overloading, which the C++ language adds to its C core. This chapter concludes with a section on arrays of pointers, which is important later in the design of polymorphic objects.
TYPES
The built-in types of the C++ language are called its fundamental types. The C++ language, like C, admits struct types constructed from these fundamental types and possibly other struct types. The C++ language standard refers to struct types as compound types. (The C language refers to struct types as derived types.)
Fundamental Types
The fundamental types of C++ include:
· Integral Types (store data exactly in equivalent binary form and can be signed or unsigned)
· bool - not available in C
· char
· int - short, long, long long
· Floating Point Types (store data to a specified precision - can store very small and very large values)
· float
· double - long double
bool
The bool type stores a logical value: true or false.
The ! operator reverses that value: !true is false and !false is true.
! is self-inverting on bool types, but not self-inverting on other types.
bool to int
Conversions from bool type to any integral type 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. Note that the following code snippet displays 1 (not 4)
int x = 4;
cout << !!x;
1
Both C and C++ treat the integer value 0 as false and any other value as true.
Compound Types
A compound type is a type composed of other types. A struct is a compound type. An object-oriented class is also a compound type. To identify a compound type we use the keywords struct or class. We cover the syntax for classes in the following chapter.
For example,
// Modular Example
// Transaction.h
struct Transaction {
int acct; // account number
char type; // credit 'c' debit 'd'
double amount; // transaction amount
};
The C++ language requires the keyword identifying a compound type only in the declaration of that type. The language does not require the keyword struct or class in a function prototype or an object definition. Note the code snippets listed on the left. Recall that the C language requires the keyword struct throughout the code as listed on the right.
// Modular Example - C++
// Transaction.h
struct Transaction {
int acct;
char type;
double amount;
};
void enter(Transaction*);
void display(const Transaction*);
// ...
int main() {
Transaction tr;
// ...
}
// Modular Example - C
// Transaction.h
struct Transaction {
int acct;
char type;
double amount;
};
void enter(struct Transaction*);
void display(const struct Transaction*);
// ...
int main() {
struct Transaction tr;
// ...
}
auto Keyword
The auto keyword was introduced in the C++11 standard. This keyword deduces the object's type directly from its initializer's type. We must provide the initializer in any auto declaration.
For example,
auto x = 4; // x is an int that is initialized to 4
auto y = 3.5; // y is a double that is initialized to 3.5
auto is quite useful: it simplifies our coding by using information that the compiler already has.
DECLARATIONS AND DEFINITIONS
Modular programming can result in multiple definitions. To avoid conflicts or duplication, we need to design our header and implementation files accordingly. The C++ language distinguishes between declarations and definitions and stipulates the one-definition rule.
Declarations
A declaration associates an entity with a type, telling the compiler how to interpret the entity's identifier. The entity may be a variable, an object or a function.
For example, the prototype
int add(int, int);
declares add() to be a function that receives two ints and returns an int. This declaration does not specify what the function does; it does not specify the function's meaning.
For example, the forward declaration
struct Transaction;
declares Transaction to be of structure type. A forward declaration is like a function prototype: it tells the compiler how to interpret the entity's identifier. It tells the compiler that the entity is a valid type, but does not specify the entity's meaning.
Although a declaration does not necessarily specify meaning, it may specify it. Specifying a meaning is an optional part of any declaration.
Definitions
A definition is a declaration that associates a meaning with an identifier.
For example, the following definitions attach meanings to Transaction and to display():
struct Transaction {
int acct; // account number
char type; // credit 'c' debit 'd'
double amount; // transaction amount
};
void display(const Transaction* tr) { // definition of display
cout << "Account " << tr->acct << endl;
cout << (tr->type == ‘d’ ? ” Debit $” : ” Credit $”) << endl;
cout << tr->amount << endl;
}
In C++, each definition is an executable statement. We may embed it amongst other executable statements.
For example, we may place a definition within an initializer:
for (int i = 0; i < n; i++)
//...
One Definition Rule
In the C++ language, a definition may only appear once within its scope. This is called the one-definition rule.
For example, we cannot define Transaction or display() more than once within the same code block or translation unit.
Declarations are not necessarily Definitions
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 that identifier. We may repeat such declarations several times within the same code block or translation unit.
Header files consist of declarations. When we include several header files in a single implementation file, multiple declarations may occur. If some of the declarations are also definitions, this may result in multiple definitions within the same translation unit. Any translation unit must not break the one-definition rule. We need to design our header files to respect this rule.
Designing Away Multiple Definitions
A definition that appears more than once within the same translation unit generates a compiler error. Two solutions are shown below.
The program listed below consists of three modules: main, Transaction and iostream.
In the main module's implementation file we have introduced a new function called add(), which receives the address of a double and the address of a Transaction object. This function update the value stored in the first address:
// One Definition Rule
// one_defintion_rule.cpp
#include
#include “main.h” // prototype for add()
#include “Transaction.h” // prototypes for enter() and display()
using namespace std;
int main() {
int i;
double balance = 0.0;
Transaction tr;
for (i = 0; i < NO_TRANSACTIONS; i++) {
enter(&tr);
display(&tr);
add(&balance, &tr);
}
cout << "Balance " << balance << endl;
}
void add(double* bal, const Transaction* tr) {
*bal += (tr->type == ‘d’ ? -tr->amount : tr->amount);
}
The Transaction module’s header file defines the Transaction type:
// Modular Example
// Transaction.h
struct Transaction {
int acct; // account number
char type; // credit ‘c’ debit ‘d’
double amount; // transaction amount
};
void enter(Transaction* tr);
void display(const Transaction* tr);
Design Question
Into which header file should we insert the prototype for this add() function?
If we insert the prototype into the main module’s header file, main.cpp will not compile:
// main.h
#define NO_TRANSACTIONS 3
void add(double*, const Transaction*);
The compiler will report Transaction* as undeclared. Note that the compiler analyzes code sequentially and does not yet know what Transaction is when it encounters the prototype for add().
If we insert Transaction.h into this header file (main.h), we resolve this issue but break the one-definition rule in main.cpp:
// main.h
#define NO_TRANSACTIONS 3
#include “Transaction.h” // BREAKS THE ONE-DEFINITION RULE!
void add(double*, const Transaction*);
The main.cpp translation unit would contain TWO definitions of Transaction.
Possible designs are possible include:
· Forward Declaration Solution – insert the prototype into main.h
· Compact Solution – insert the prototype into Transaction.h
Forward Declaration Solution
Inserting the prototype into main.h along with a forward declaration of Transaction informs the compiler that this identifier in the prototype is a valid type.
// main.h
#define NO_TRANSACTIONS 3
struct Transaction; // forward declaration
void add(double*, const Transaction*);
This design provides the compiler with just enough information to accept the identifer, without exposing the type details.
Compact Solution
Inserting the prototype into the Transaction.h header file is a more compact solution:
// Modular Example
// Transaction.h
struct Transaction {
int acct; // account number
char type; // credit ‘c’ debit ‘d’
double amount; // transaction amount
};
void enter(Transaction* tr);
void display(const Transaction* tr);
void add(double*, const Transaction*);
This design localizes all declarations related to the Transaction type within the same header file. We call functions that support a compound type the helper functions for that type.
Proper Header File Inclusion
To avoid contaminating system header files, we include header files in the following order:
· #include < ... > – system header files
· #include ” … ” – other system header files
· #include ” … ” – your own header files
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 that declaration is visible. Scopes include
· global scope – visible to the entire program
· file scope – visible to the source code within the file
· function scope – visible to the source code within the function
· class scope – visible to the member functions of the class
· block scope – visible to the code block
The scope of a non-global declaration begins at the declaration and ends at the closing brace for that declaration. A non-global declaration is called a local declaration. We say that an identifier that has been locally declared is a local variable or object.
Going Out of Scope
Once a declaration is out of its scope, the program has lost access to the declared variable or object. Identifying the precise point at which a variable’s or object’s declaration goes out of scope is important in memory management.
Iterations
In the following code snippet, the counter i, declared within the for statement, goes out of scope immediately after the closing brace:
for (int i = 0; i < 4; i++) {
cout << "The value of i is " << i << endl;
} // i goes out of scope here
We cannot refer to i after the closing brace.
A variable or object declared within a block goes out of scope immediately before the block's closing brace.
for (int i = 0; i < 3; i++) {
int j = 2 * i;
cout << "The value of j is " << j << endl;
} // j goes out of scope here
The scope of j extends from its definition to just before the end of the current iteration. j goes out of scope with each iteration. The scope of i extends across the complete set of iterations.
Shadowing
An identifier declared with an inner scope can shadow an identifier declared with a broader scope, making the latter temporarily inaccessible. For example, in the following program the second declaration shadows the first declaration of i:
// scope.cpp
#include
using namespace std;
int main() {
int i = 6;
cout << i << endl;
for (int j = 0; j < 3; j++) {
int i = j * j;
cout << i << endl;
}
cout << i << endl;
}
6
0
1
4
6
FUNCTION OVERLOADING
In object-oriented languages functions may have multiple meanings. Functions with multiple meanings are called overloaded functions. C++ refers to functions first and foremost by their identifier and distinguishes different meanings by differing parameter lists. For each identifier and parameter list combination, we implement a separate function definition. C++ compilers determine the definition to select by matching the argument types in the function call to the parameters types in the definition.
Function Signature
A function's signature identifies an overloaded function uniquely. Its signature consists of
· the function identifier
· the parameter types (ignoring const qualifiers or address of operators as described in references below)
· the order of the parameter types
type identifier ( type identifier [, ... , type identifier] )
The square brackets enclose optional information. The return type and the parameter identifiers are not part of a function's signature.
C++ compilers preserve identifier uniqueness by renaming each overloaded function using a combination of its identifier, its parameter types and the order of its parameter types. We refer to this renaming as name mangling.
Example
Consider the following example of an overloaded function. To display data on the standard output device, we can define a display() function with different meanings:
// Overloaded Functions
// overload.cpp
#include
using namespace std;
// prototypes
void display(int x);
void display(const int* x, int n);
int main() {
auto x = 20;
int a[] = {10, 20, 30, 40};
display(x);
display(a, 4);
}
// function definitions
//
void display(int x) {
cout << x << endl;
}
void display(const int* x, int n) {
for (int i = 0; i < n; i++)
cout << x[i] << ' ';
cout << endl;
}
20
10 20 30 40
C++ compilers generate two one definition of display() for each set of parameters. The linker binds each function call to the appropriate definition based on the argument types in the function call.
Prototypes
A function prototype completes the function's signature by specifying the return type. However, the parameter identifiers are also optional in the prototype. The prototype provides sufficient information to validate a function call.
A prototype without parameter types identifies an empty parameter list. The keyword void, which the C language uses to identify no parameters is redundant in C++. We omit this keyword in C++.
Prototypes Required
A programming language may require a function declaration before any function call for type safety. The declaration may be either a prototype or the function definition itself. The compiler uses the declaration to check the argument types in the call against the parameter types in the prototype or definition. The type safety features of C++ require a preceding declaration.
For example, the following program will generate a compiler error (note that the absence of any printf declaration):
int main() {
printf("Hello C++\n");
}
To meet type safety requirements, we include the prototype:
#include
using namespace std;
int main() {
printf(“Hello C++\n”);
}
Default Parameter Values
We may include default values for some or all of a function’s parameters in the first declaration of that function. The parameters with default values must be the rightmost parameters in the function signature.
Declarations with default parameter values take the following form:
type identifier(type[, …], type = value);
The assignment operator followed by a value identifies the default value for each parameter.
Specifying default values for function parameters reduces the need for multiple function definitions if the function logic is identical in every respect except for the values received by the parameters.
Example
For example,
// Default Parameter Values
// default.cpp
#include
using namespace std;
void display(int, int = 5, int = 0);
int main() {
display(6, 7, 8);
display(6);
display(3, 4);
}
void display(int a, int b, int c) {
cout << a << ", " << b << ", " << c << endl;
}
6, 7, 8
6, 5, 0
3, 4, 0
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 passed to a parameter that has a default value overrides the default value.
REFERENCES
A reference is an alias for a variable or object. Object-oriented languages rely on referencing. A reference in a function call passes the variable or object rather than a copy. In other words, a reference is an alternative to the pass by address mechanism available in the C language. Pass-by-reference code is notably more readable than pass-by-address code. To enable referencing, the C++ rules on function declarations are stricter than those of the C language.
The declaration of a function parameter that is received as a reference to the corresponding argument in the function call takes the form
type identifier(type& identifier, ... )
The & identifies the parameter as an alias for, rather than a copy of, the corresponding argument. The identifier is the alias for the argument 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 Examples
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:
// Swapping values by address
// swap1.cpp
#include
using namespace std;
void swap ( char *a, char *b );
int main ( ) {
char left;
char right;
cout << "left is ";
cin >> left;
cout << "right is ";
cin >> right;
swap(&left, &right);
cout << "After swap:"
"\nleft is " <<
left <<
"\nright is " <<
right <<
endl;
}
void swap ( char *a, char *b ) {
char c;
c = *a;
*a = *b;
*b = c;
}
// Swapping values by reference
// swap2.cpp
#include
using namespace std;
void swap ( char &a, char &b );
int main ( ) {
char left;
char right;
cout << "left is ";
cin >> left;
cout << "right is ";
cin >> right;
swap(left, right);
cout << "After swap:"
"\nleft is " <<
left <<
"\nright is " <<
right <<
endl;
}
void swap ( char &a, char &b ) {
char c;
c = a;
a = b;
b = c;
}
Clearly, reference syntax is 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.
ARRAY OF POINTERS
Arrays of pointers are data structures like arrays of values. Arrays of pointers contain addresses rather than values. We refer to the object stored at a particular address by dereferencing that address. Arrays of pointers play an important role in implementing polymorphism in the C++ language.
An array of pointers provides an efficient mechanism for processing the set. With the objects' addresses collected in a contiguous array, we can refer to each object indirectly through the pointers in the array and process the data by iterating on its elements.
In preparation for a detailed study of polymorphic objects later in this course, consider the following preliminary example:
// Array of Pointers
// array_pointers.cpp
#include
using namespace std;
const int N_CHARS = 31;
struct Student {
int no;
double grade;
char name[N_CHARS];
};
int main() {
const int NO_STUDENTS = 3;
Student john = {1234, 67.8, “john”};
Student jane = {1235, 89.5, “jane”};
Student dave = {1236, 78.4, “dave”};
Student* pStudent[NO_STUDENTS]; // array of pointers
pStudent[0] = &john;
pStudent[1] = &jane;
pStudent[2] = &dave;
for (int i = 0; i < NO_STUDENTS; i++) {
cout << pStudent[i]->no << endl;
cout << pStudent[i]->grade << endl;
cout << pStudent[i]->name << endl;
cout << endl;
}
}
1234
67.8
john
1235
89.5
jane
1236
78.4
dave
Here, while the objects are of the same type, the processing of their data is done indirectly through an array of pointers to that data.
KEYWORDS
The 84 keywords of the C++11 standard are listed below. We cannot use any of these keywords as identifiers. Those shaded in grey are also C keywords. The italicized keywords are alternative tokens for operators.
alignas alignof and and_eq asm auto bitand
bitor bool break case catch char char16_t
char32_t class compl const constexpr const_cast continue
decltype default delete do double dynamic_cast else
enum explicit export extern false float for
friend goto if inline int long mutable
namespace new not not_eq noexcept nullptr operator
or or_eq private protected public register reinterpret_cast
return short signed sizeof static static_assert static_cast
struct switch template this thread_local throw true
try typedef typeid typename union unsigned using
virtual void volatile wchar_t while xor xor_eq
C++ compilers 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.
SUMMARY
· a bool type can only hold a true value or a false value
· C++ requires the struct or class keyword only in the definition of the class itself
· a declaration associates an identifier with a type
· a definition attaches meaning to an identifier and is an executable statement
· a definition is a declaration, but a declaration is not necessarily a definition
· the scope of a declaration is that part of the program throughout which the declaration is visible
· we overload a function by changing its signature
· a function's signature consists of its identifier, its parameter types, and the order of its parameter types
· a C++ function prototype must include all of the parameter types and the return type
· the & operator on a parameter type instructs the compiler to pass by reference
· pass by reference syntax simplifies the pass by address syntax in most cases
· an array of pointers is a data structure that provides an efficient way for iterating through a set of objects based on their current type
A modular design consists of a set of modules, which are developed and tested separately. Modular programming implements modular designs and is supported by both procedural and object-oriented languages. The C programming language supports modular design through library modules composed of functions. The stdio module provides input and output support, while hiding 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 an object-oriented languages using C++, how to compile the source code for each module separately and how to 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 couples loosely to other modules. The module addresses one aspect of the programming solution and hides as much detail as practically possible. A compiler translates the module's source code independently of the source code for other modules into its own unit of binary code.
Consider the schematic of the Transaction application shown below. The main module accesses the Transaction module. The Transaction module accesses the iostream module. The Transaction module defines the transaction functions used by the application. The iostream module defines the cout and cin objects used by the application.
To translate the source code of any module the compiler only needs certain external information. This information includes the names used within the module but defined outside the module. To enable this in C++, we store the source code for each module in two separate files:
· a header file - defines the class and declares the function prototypes
· an implementation file - defines the functions and contains all of the logic
The file extension .h (or .hpp) identifies the header file. The file extension .cpp identifies the implementation file.
Note, however, that the names of the header files for the standard C++ libraries do not include a file extension (consider for example, the
Example
The implementation file for the main module includes the header files for itself (main.h) and the Transaction module (Transaction.h). The main.h file contains definitions specific to the main module and the Transaction.h file contains definitions specific to the Transaction module.
The implementation file for the Transaction module includes the header files for itself (Transaction.h) and the iostream module. The Transaction.h file contains definitions specific to the Transaction module and the iostream file contains definitions specific to the iostream module.
An implementation file can include several header files but DOES NOT include any other implementation file. Note the absence of any direct connections between the implementation files.
We compile each implementation (*.cpp) file separately and only once. We do not compile header (*.h) files.
A 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 creating a single translation unit for the compiler – (inserts the contents of all #include header files), (substitutes all#define macros)
2. Compiler – compiles each translation unit separately and creates a corresponding 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 transactions from the standard input device and displays them on the standard output device. For presentation simplicity, the application does not perform any intermediate calculation.
The application design consists of two modules:
· Main – supervises the processing of each transaction
· Transaction – defines the input and output logic for a single transaction
Transaction Module
The transaction module defines a structure and functions for a single transaction
· Transaction – holds the information for a single transaction in memory
The related functions are 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:
// Modular Example
// Transaction.h
struct Transaction {
int acct; // account number
char type; // credit ‘c’ debit ‘d’
double amount; // transaction amount
};
void enter(struct Transaction* tr);
void display(const struct Transaction* tr);
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 our two functions. This file includes the system header file for access to the cout and cin objects and the header file for access to the Transaction type.
// Modular Example
// Transaction.cpp
#include
#include “Transaction.h” // for Transaction
using namespace std;
// prompts for and accepts Transaction data
//
void enter(struct Transaction* tr) {
cout << "Enter the account number : ";
cin >> tr->acct;
cout << "Enter the account type (d debit, c credit) : ";
cin >> tr->type;
cout << "Enter the account amount : ";
cin >> tr->amount;
}
// displays Transaction data
//
void display(const struct Transaction* tr) {
cout << "Account " << tr->acct;
cout << ((tr->type == ‘d’) ? ” Debit $” : ” Credit $”) << tr->amount;
cout << endl;
}
Note the .cpp extension on the name of this implementation file
Main Module
The main module defines a Transaction object and accepts input and displays data for each of three transactions.
main.h
The header file for our Main module #defines the number of transactions:
// Modular Example
// main.h
#define NO_TRANSACTIONS 3
main.cpp
The implementation file for our Main module defines the main() function. We #include the header file to provide the definition of the Transaction type:
// Modular Example
// main.cpp
#include "main.h"
#include "Transaction.h"
int main() {
int i;
struct Transaction tr;
for (i = 0; i < NO_TRANSACTIONS; i++) {
enter(&tr);
display(&tr);
}
}
Command Line Compilation
Linux
To compile our application on a Linux platform at the command line, we enter the following
g++ -o accounting main.cpp Transaction.cpp
The -o option identifies the name of the executable. The names of the two implementation files complete the command.
To run the executable, we enter
accounting
Legacy Linux
To compile an application that includes a C++11 feature on a legacy Linux installation, we may need to specify the standard option. For example, to access C++11 features on the GCC 4.6 installation on our matrix cluster, we write
g++ -o accounting -std=c++0x main.cpp Transaction.cpp
Options for versions 4.7 and later include c++11 and gnu++11.
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. )
cl /Fe accounting main.cpp Transaction.cpp
The /Fe option identifies the name of the executable. The names of the two implementation files follow this option.
To run the executable, we enter
accounting
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.
Build and Execute
The following steps build and execute a modular application in Visual Studio 2013 or newer:
· Start Visual Studio
· Select New Project
· Select Visual C++ -> Win32 -> Console Application
· Enter Transaction Example as the Project Name | Select OK
· Press Next
· Check Empty Project | Press Finish
· Select Project -> Add New Item
· Select Header .h file | Enter Transaction as File Name | Press OK
· Select Project -> Add New Item
· Select Implementation .cpp file | Enter Transaction as File Name | Press OK
· Select Project -> Add New Item
· Select Header .h file | Enter main as File Name | Press OK
· Select Project -> Add New Item
· Select Implementation .cpp file | Enter main as File Name | Press OK
· Select Build | Build Solution
· Select Debug | Start without Debugging
The input prompts and the results of execution appear in a Visual Studio command prompt window.
UNIT TESTS
Unit testing is integral to modular programming. A unit test is a code snippet that tests a single assumption in a module or work unit of a complete application. Each work unit is a single logical component with a simple interface. Functions and classes are typical examples.
We use unit tests to examine the work units in an application 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:
// Calculator.h
// …
int power(int, int);
int exponent(int, int);
The suite of unit tests for this module checks if the implementations return the expected results. The header file for the Tester module contains:
// Tester.h
int testSuite(int BASE, int EXPONENT, int RESULT);
The implementation file for the Tester module contains:
// Tester.cpp
#include
using namespace std;
#include “Calculator.h”
int testSuite(int BASE, int EXPONENT, int RESULT) {
int passed = 0;
int result;
result = power(BASE, EXPONENT);
if (result == RESULT) {
cout << "Raise to Power Test Passed" << endl;
passed++;
}
else {
cout << "Raise to Power Test Failed" << endl;
}
result = exponent(RESULT, BASE);
if (result == EXPONENT) {
cout << "Find Exponent Test Passed" << endl;
passed++;
}
else {
cout << "Find Exponent Test Failed" << endl;
}
return passed;
}
A first attempt at implementing the Calculator module might look like:
// Calculator.cpp
#include "Calculator.h"
int power(int base, int exp) {
int i, result = 1;
for (i = 0; i < exp; i++)
result *= base;
return result;
}
int exponent(int result, int base) {
int exp = 0;
while(result >= base) {
exp++;
result /= base;
}
return exp;
}
The following test main produces the results shown on the right:
// Test Main
// testmain.cpp
#include
using namespace std;
#include “Tester.h”
int main() {
int passed = 0;
passed += testSuite(5, 3, 125);
passed += testSuite(5, -3, 0.008);
cout << passed << " Tests Passed" << endl;
}
Raise to Power Test Passed
Find Exponent Test Passed
Raise to Power Test Failed
Find Exponent Test Failed
2 Tests Passed
The tester shows that this implementation does not handle negative-valued bases correctly and needs upgrading.
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 coding the bodies of the work units. As we complete implementation details, we continue testing our module to ensure that it produces the results that we expect and to identity the work that needs to be done.
DEBUGGING TECHNIQUES
Programming Errors
Programming errors that require debugging skills are of two kinds:
· syntactic
· semantic
Syntactic Errors
Syntactic errors are errors that break the rules of the programming language. The most common syntactic errors in C++ language programs are:
· missing semi-colon after a struct or class definition
· unnecessary semi-colon terminator in a #define directive
· undeclared variable name or missing header file
· mismatched parentheses
· left-side of an assignment expression is not a defined memory location
· return statement is missing
Semantic Errors
Semantic errors are errors that fail to implement the intent and meaning of the program designer. The more common semantic errors are:
· = instead of ==
· iteration without a body (for/while followed by a semi-colon)
· uninitialized variable
· infinite iteration
· incorrect operator order in a compound expression
· dangling else
· off-by-one iteration
· integer division and truncation
· mismatched data types
· & instead of &&
Idenfiying Errors
Syntactic Errors
Techniques for identifying syntactic errors include
· IDE intellisense
· compiler error messages (compiler output)
· comparing error messages from different compilers - some are more cryptic than others
· reading code statements (walkthroughs)
Semantic Errors
Techniques for identifying semantic errors include:
· vocalization - use your sense of hearing to identify the error (compound conditions)
· intermediate output - cout statements at critical stages
· walkthrough tables
· interactive debugging using
· Visual Studio IDE - integrated debugger for Windows OSs
· Eclipse IDE - integrated debugger for Linux OSs
· gdb - GNU debugger for gcc
IDE Tracing Example
The following steps trace through the execution of our Transaction application using the Visual Studio IDE debugger
· Select the file named main.cpp
· Move the cursor to the left-most column of the for statement in the main() function and left-click | This places a red dot in that column, which identifies a breakpoint
· Move the cursor to the left-most column of the closing brace for the function and left-click | This places a red dot in the column, which identifies another breakpoint
· Select Debug -> Start Debugging | Execution should pause at the first executable statement
· Observe the values under the Locals tab in the Window below the source code
· Press F10 and answer the three input prompts
· Select the source code Window
· Observe the values under the Locals tab in the Window below the source code
· Press F10 3 times and note the value of i
· Press F5, note where execution pauses and observe the value of i
· Press F5 again to exit
The keystrokes for the various debugging options are listed next to the sub-menu items under the Debug menu.
SUMMARY
· a module consists of a header file and an implementation file
· a module’s header file declares the names that are exposed to client modules
· a module’s implementation file defines the module’s logic
· a module’s implementation file needs the header files of those modules that define classes or functions used in the implementation file
· the three stages of creating an executable are preprocessing, compiling, and linking
· it is good practice to write the suite of unit tests for each module of an application before coding the module’s implementation
Object-oriented programming reflects the way in which we manage day-to-day tasks. Professor Miller of Princeton University demonstrated that most of us can only comprehend about seven chunks of information at a time. As children, we learn to play with small sets of chunks at an early age. As we grow, we learn to break down the problems that we face into sets of manageable chunks.
A chunk in object-oriented programming is called an object. The shared structure of a set of similar objects is called their class. This shared structure includes the structure of the data in the similar objects as well as the logic that works on that data.
This chapter introduces the concepts of object, class, encapsulation, inheritance and polymorphism. Subsequent chapters elaborate on each concept in detail.
ABSTRACTION
Programming solutions to application problems consist of components. The process of designing these solutions involves abstraction. In the C programming language, we abstract common code, store it in a structure or function and refer to that structure or function from possibly multiple places in our source code, thereby avoiding code duplication.
An object-oriented programming solution to an application problem consists of components called objects. The process of designing an object-oriented solution likewise involves abstraction. We distinguish the most important features of the object, identify them publicly and hide the less important details within the object itself.
Each object has a crisp conceptual boundary and acts in ways appropriate to itself. Compare a book with a set of notes. A book has pages that are bound and can be flipped. The page order is fixed. A set of notes consists of loose pages that can be rearranged in any order. We represent the book as an object and the set of notes as another object; each object has a different structure.
The cout and cin objects introduced in the preceding chapter are examples. They have different structures. cout represents the standard output device, which may be a monitor or a printer. The abstraction – the standard output device – is simple and crisp. Internally, the cout object is quite complex. On the other hand, cin represents the standard input device, which may be a keyboard, a tablet or a touch screen. The abstraction – the standard input device – is also simple and crisp. Internally, the cin object is also quite complex.
CLASSES
We describe the structure of similar objects in terms of their class. Objects of the same class have the same structure, but possibly different states. The variable types that describe their states are identical, but generally have different values. For example, all of the books in the figure above have a title and an author, but each book has a different title and a different author.
We say that each object is an instance of its class.
UML
The Unified Modelling Language (UML) is a general-purpose modeling language developed for describing systems of objects and relationships between their classes. This language defines standard symbols for classes and their relationships. The connectors shown in the relationship diagram below are UML connectors. We use these symbols in this text.
The Class Diagram
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,
Code
number : int
title : char*
getCode() : int
getTitle() : const char*
setCode(int) : void
setTitle(const char*) : void
The naming conventions include:
· begin each class name with an upper case letter
· begin each member name with a lower case letter
· use camel case throughout all names – capitalize the first letter of every word after the first word
Terminology
UML uses the terms attributes and operations. Some object-oriented languages use different terms. Equivalent terms are:
· attributes (UML) -> fields, data members, properties, member variables
· operations (UML) -> methods (Java), procedures, messages, member functions
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 that establishes the crisp interface between the implementation and any client. Encapsulation maintains high cohesion within a class and low coupling between the class’ implementation and any one of its clients.
A class definition declares the variables and the function prototypes. The variables store each object’s data and the functions contain the logic that operates on that data. Clients access objects through calls to these functions without knowledge of the data stored within the objects or the logic that manipulates that data.
Encapsulation shields the complex details of a class’ implementation from its interface; that is, its crisp external representation. Consider the following statement from the preceding chapter:
cout << "Welcome to Object-Oriented";
cout refers to the standard output object. Its class defines how to store the object's data in memory and how to control the operations that work with that data. The << operator copies the string to the output object without exposing any of the implementation details. As client programmers, we only see the interface that manages the output process.
A well-encapsulated class hides all 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 object-oriented concepts are prominent in our study of the 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 of distinguishing 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 and adds some further structure. 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:
Inheriting one class from another allows us to utilize existing technology. We only provide the code that implements additional structure.
Polymorphism
Polymorphism relates the implementation for an object based on its type. In the Figure below, the HybridCourse object involves a different mode of delivery than the Course object, but the same assessments. Both objects belong to the same hierarchy: both are Course objects.
A mode() query on a Course type reports a different result than a mode() query on a Hybrid Course type. On the other hand, an assessments() query on a Course type reports the same result as on an HybridCourse type.
Polymorphic programming allows us to minimize the duplication of code amongst objects that belong to the same inheritance hierarchy.
The Three Musketeers
Encapsulation, inheritance and polymorphism are the cornerstones of any object-oriented programming language.
SUMMARY
· An object is a chunk of information with a crisp conceptual boundary and a well-defined structure.
· Objects are abstractions of the most important chunks of information from a problem domain. They distinguish the different feature sets in the problem domain.
· A class describes the structure common to a set of similar objects. Each object in the set is a single instance of its class.
· Encapsulation hides the implementation details within a class - the internal data and internal logic are invisible to client applications that use objects of that class.
· We can upgrade the structure of a well-encapsulated class without altering any client code.
· The cornerstones of object-oriented programming are encapsulation, inheritance and polymorphism.
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 time. Some take years of programming effort to mature. Creating such applications involves many developers with different levels of expertise. Their work consists of smaller stand alone and testable sub-projects; sub-projects that are transferrable, practical, upgradeable and possibly even usable within other future applications. The principles of software engineering suggest that each component should be highly cohesive and that the collection of components should be loosely coupled. Object-oriented languages provide the tools for implementing these principles.
C++ is an object-oriented programming language specifically designed to provide a simple, comprehensive feature set for programming modern applications without loss in performance. C++ combines the efficiencies of the C language with the object-oriented features necessary for the development of large applications.
ADDRESSING COMPLEXITY
Large applications are complex. We address their complexity by identifying the most important features of the problem domain; that is, the area of expertise that needs to be examined to solve the problem. 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
· enrolls in several face-to-face courses
· enrolls in several hybrid courses
· earns a grade in each course
The following structure diagram identifies the activities.
If we switch our attention to the objects involved, we find a Course and a Hybrid Course. Focusing on a Course, we observe that it has a Course Code. We lookup the Code in the institution's Calendar to determine when that Course is offered.
We say that a Course has a Code and uses a Grading Scheme and that a Hybrid Course is a kind of Course. The diagram below shows these relationships between the objects in this problem domain. The connectors identify the types of relationships. The closed circle connector identifies a has-a relationship, the open circle connector identifies a uses-a relationship and the arrow connector identifies an is-a-kind-of relationship.
In switching our attention from the activities in the structure chart to the objects in the relationship diagram we have switched from a procedural description of the problem to an object-oriented description.
These two complementary approaches to mastering complexity date at least as far 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 system analysis and design courses. The material covered in this course introduces some of the principal concepts of 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 are object orientated.
Java, C, C++, Python and C# are currently the five most popular languages. Java, C, C++ and C# have much syntax in common: Java syntax is C-like, but not a superset of C, C++ contains almost all of C as a subset, C# syntax is C++-like but not a superset of C. Each is an imperative language; that is, a language that specifies every step necessary to reach a desired state.
The distinguishing features of C, C++ and Java are:
· C has no object-oriented support. C leaves us no choice but to design our programming solutions in terms of activity-oriented structures.
· C++ is hybrid. It augments C with object-oriented features. C++ lets us build our solutions partly from activities and partly from objects. The main function in any C++ program is a C function, which is not object-oriented. C++ stresses compile-time logic.
· Java is purely object-oriented. It excludes all non-object-oriented features. Java leaves us no choice but to design our solutions using an object-oriented structures.
Features of C++
Using C++ to learn object-oriented programming has several advantages for a student familiar with C. C++ is
· nearly a perfect superset of C
· a multi-paradigm language
·
· procedural (can focus on distinct activities)
· object-oriented (can focus on distinct objects)
· realistic, efficient, and flexible enough for demanding projects
·
· large applications
· game programming
· operating systems
· clean enough for presenting basic concepts
· comprehensive enough for presenting advanced programming concepts
Type Safety
Type safety in central to C++.
A type-safe language traps syntax errors at compile-time, diminishing the amount of buggy code that escapes to the client. C++ compilers use type rules to check syntax and generate errors or warnings if any type rule has been 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
#include
void foo();
int main(void)
{
foo(-25);
}
void foo(char x[])
{
printf(“%s”, x); /* ERROR */
}
Segmentation Fault (coredump)
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:
#include
void foo(char x[]);
int main(void)
{
foo(-25);
}
void foo(char x[])
{
printf(“%s”, x);
}
Function argument assignment between
types “char*” and “int” is not allowed.
Bjarne Stroustrup, in creating the C++ language, decided to close this loophole. He mandated that in C++ all prototypes list their parameter types, which has forced all C++ compilers to check for argument/parameter type mismatches at compile-time.
NAMESPACES
In applications written simultaneously by several developers, chances are high that some developers will use the same identifier 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 scoping variables within each namespace.
A namespace is a scope for the entities that it encloses. Scoping rules avoid identifier conflicts across different namespaces.
We define a namespace as follows
namespace identifier {
}
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
namespace english {
int x = 2;
// …
}
namespace french {
int x = 3;
// …
}
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
english::x++;
french::x–;
Each prefix uniquely identifies each variable’s namespace.
Namespaces hide their entities. To expose an identifier to the current namespace, we insert the using declaration into our code before referring to the identifier.
For example, to expose one of the x’s to the current namespace, we write:
using french::x;
After which, we can simply write:
x++; // increments french::x but not english::x
To expose all of a namespace’s identifiers, we insert the using directive into our code before referring to any of them.
For example, to expose all of the identifiers within namespace english, we write:
using namespace english;
Afterwards, we can write:
x++; // increments english::x but not french::x
Exposing a single identifier or a complete namespace simply adds the identifier(s) to the hosting namespace.
Common Usage
By far the most common use of namespaces is for classifying
· struct-like types
· function types
FIRST EXAMPLES
From C to C++ Syntax
To compare C++ syntax with C syntax, consider a program that displays the following phrase on the standard output device
Welcome to Object-Oriented
C – procedural code
The C source code for displaying this phrase is
// A Language for Complex Applications
// welcome.c
//
// To compile on linux: gcc welcome.c
// To run compiled code: a.out
//
// To compile on windows: cl welcome.c
// To run compiled code: welcome
//
#include
int main(void)
{
printf(“Welcome to Object-Oriented\n”);
}
The two functions – main() and printf() – specify activities. These identifiers share the global namespace.
C++ – procedural code
The procedural C++ source code for displaying the phrase is
// A Language for Complex Applications
// welcome.cpp
//
// To compile on linux: g++ welcome.cpp
// To run compiled code: a.out
//
// To compile on windows: cl welcome.cpp
// To run compiled code: welcome
//
#include
using namespace std;
int main ( ) {
printf(“Welcome to Object-Oriented\n”);
}
The file extension for any C++ source code is .cpp.
The directive
using namespace std;
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
// A Language for Complex Applications
// welcome.cpp
//
// To compile on linux: g++ welcome.cpp
// To run compiled code: a.out
//
// To compile on windows: cl welcome.cpp
// To run compiled code: welcome
//
#include
using namespace std;
int main ( ) {
cout << "Welcome to Object-Oriented" << endl;
}
The object-oriented syntax consists of:
1. The directive
#include
inserts the
2. The object
cout
represents the standard output device.
3. The operator
<<
inserts whatever is on its right side into whatever is on its left side.
4. The manipulator
endl
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
cout << "Welcome to Object-Oriented" << endl;
inserts into the standard output stream the string "Welcome to Object-Oriented" followed by a newline character and a flushing of the output buffer.
First Input and Output Example
The following object-oriented program accepts an integer value from standard input and displays that value on standard output:
// Input Output Objects
// inputOutput.cpp
//
// To compile on linux: g++ inputOutput.cpp
// To run compiled code: a.out
//
// To compile on windows: cl welcome.cpp
// To run compiled code: welcome
//
#include
using namespace std;
int main() {
int i;
cout << "Enter an integer : ";
cin >> i;
cout << "You entered " << i << endl;
}
Enter an integer : 65
You entered 65
The object-oriented input statement includes:
· The object
cin
represents the standard input device.
· The extraction operator
>>
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
cin >> i;
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.
SUMMARY
· object-oriented languages are designed for solving large, complex problems
· object-oriented programming focuses on the objects in a problem domain
· C++ is a hybrid language that can focus on activities as well as objects
· C++ provides improved type safety relative to C
· cout is the library object that represents the standard output device
· cin is the library object that represents the standard input device
· << is the operator that inserts data into the object on its left-side operand
· >> is the operator that extracts data from the object on its left-side operand
EXERCISES
· Read Wikipedia on the C++ Programming Language
· Read this interview with Bjarne Stroustrup
“Be adventurous in your experimentation and somewhat more cautious in your production code. On my home pages, I have a “Technical and Style FAQ” that gives many practical hints and examples. However, to really develop significant new skills, one must read articles and books. I think of C++ as a multi-paradigm programming language. That is, C++ is a language that supports several effective programming techniques, where the best solution to a real-world programming problem often involves a combination of these techniques. Thus, I encourage people to learn data abstraction (roughly, programming using abstract classes), object-oriented programming (roughly, programming using class hierarchies) and generic programming (roughly, programming using templates). Furthermore, I encourage people to look for combinations of these techniques rather than becoming fanatical about one of these paradigms because it happens to be a great solution to a few problems.
It is important to remember that a programming language is only a tool. Once you master the basic concepts of a language, such as C++, it is far more important to gain a good understanding of an application area and of the problem you are trying to solve than it is to study the minute technical details of C++. Good luck, and have fun with C++! ”
Extract from the Linux Journal: Interview with Bjarne Stroustrup
Posted on Thursday, August 28, 2003 by Aleksey Dolya
· Install Visual Studio on your local computer
· Ensure that your remote Linux account is operational
The relationships between classes that object-oriented languages support include compositions and associations. Both of these relationships are more loosely coupled than friendship. A composition is a relationship in which one class has another class, while an association is a relationship in which one class uses another class. In relationship diagrams, associations appear as open circles and compositions as filled circles. The diagram below shows that Code uses the Calendar (to determine availability), while each Course has a Course Code class.
Associations are more loosely coupled than compositions. Typical examples of associations are relationships between the stream-based input and output library classes that support the C++ language and our own custom classes. To create these relationships, we overload the insertion and extraction operators as helper operators that take stream objects as their left operands and objects of our class type as their right operands.
This chapter describes the stream-based input/output library and shows how to overload the insertion and extraction operators for objects of our custom classes. This chapter shows how to define file objects and use them to store and retrieve variables of fundamental type. The chapter concludes by introducing the standard library’s string class, which manages character stream input of user-defined length.
STREAM LIBRARY OVERVIEW
The stream-based input/output library that supports the core C++ language overloads two operators for extracting values from an input stream and inserting values into an output stream:
· >> (extract from an input stream)
· << (insert into an output stream)
The library embeds its class definitions in the standard namespace (std). To access those instances of these classes that the library predefines, we prefix their identifiers with the namespace identifier and the scope resolution operator (std::).
For example,
#include
int main() {
int x;
std::cout << "Enter an integer : ";
std::cin >> x;
std::cout << "You entered " << x << std::endl;
}
Enter an integer : 3
You entered 3
Standard I/O and File I/O
Standard I/O
The iostream system header file contains the definitions for streaming from and to standard devices.
#include
This header file includes the definitions of the
· std::istream class – for processing input from the standard input device
· std::ostream class – for processing output to the standard output devices
This header file also predefines the standard input and output objects:
· std::istream
· std::cin – standard input
· std::ostream
· std::cout – standard output
· std::cerr – standard error
· std::clog – standard log
We use these objects directly and don’t need to redefine them.
File I/O
The fstream system header file contains the definitions for streaming from and to files.
#include
This header file includes the definitions of the
· std::ifstream class – for processing input from a file stream
· std::ofstream class – processing output to a file stream
· std::fstream class – processing input from and output to a file stream
These three classes manage communication between file streams containing 8-bit characters and system memory. They provide access to a file stream through separate input and output buffers.
Design Considerations
In overloading the insertion and extraction operators for our class types, good design suggests:
· providing flexibility in the selection of stream objects
· resolving scope on classes and objects defined in header files
· enabling cascading as implemented for fundamental types
Selection of Stream Objects
To enable selection of the stream objects by the client code, we upgrade our display() member function to receive a modifiable reference to an object of std::ostream type. The changes to the header file are shown on the left and the implementation file on the right:
// Student.h
#include
// std::ostream
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream&)
const;
};
// Student.cpp
#include
#include “Student.h”
using namespace std;
// …
void Student::display(ostream&
os) const {
if (no > 0) {
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available"
<< endl;
}
}
With this upgrade the client code can choose the destination stream (cout, cerr, or clog).
Header Files
A header files may be included alongside other header files written by other developers. To avoid conflicts between the header files included in an implementation file, we follow certain guidelines:
· include system header files before custom header files
· insert namespace directives after all header files
· resolve the scope of any identifier in a header file at the identifier itself
The preferred method of coding header files is shown on the right:
// Student.h
#include
using namespace std; // POOR DESIGN
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(ostream& os) const;
};
// Student.h
#include
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream& os)
const;
};
Exposing all of the names in any namespace as on the left may lead to unnecessary conflicts with new names or conflicts when several header files are included in an implementation file. Resolving scope in the display() function’s parameter list identifies the class used with its namespace, without exposing any name in that namespace.
Cascading
The following expression is a cascaded expression
std::cout << x << y << z << std::endl;
Cascading support enables concatenation of operations where the leftmost operand serves as the left operand for every operation in a compound expression.
The cascaded expression above expands to two simpler sub-expressions executed in the following order:
std::cout << x;
std::cout << y << z << std::endl;
The cascaded sub-expression
std::cout << y << z << std::endl;
expands to two simpler sub-expressions executed in the following order:
std::cout << y;
std::cout << z << std::endl;
Finally, the cascaded sub-expression
std::cout << z << std::endl;
expands into two simpler sub-expressions executed in the following order:
std::cout << z;
std::cout << std::endl;
Enabling cascading requires that we return a modifiable reference to the left operand.
Returning a modifiable reference from a function lets the client code use the return value as the left operand for the operator on its right.
STANDARD I/O OPERATORS
The prototypes for the overloaded insertion and extraction operators for standard input and output on objects of our own classes take the form
std::istream& operator>>(std::istream&, Type&);
std::ostream& operator<<(std::ostream&, const Type&);
where Type is the name of the class.
The header file for our Student class that includes their declarations is:
// Student.h
#include
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void read(std::istream&);
void display(std::ostream& os) const;
};
std::istream& operator>>(std::istream& is, Student& s);
std::ostream& operator<<(std::ostream& os, const Student& s);
The implementation file for our upgraded Student class contains:
// Student.cpp
#include "Student.h"
using namespace std;
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
*this = Student(n, nullptr, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::read(istream& is) {
int no; // will hold the student number
int ng; // will hold the number of grades
float grade[NG]; // will hold the grades
cout << "Student Number : ";
is >> no;
cout << "Number of Grades : ";
is >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
cout << "Grade " << i + 1 << " : ";
is >> grade[i];
}
// construct a temporary Student
Student temp(no, grade, ng);
// if data is valid, the temporary object into the current object
if (temp.no != 0)
*this = temp;
}
void Student::display(ostream& os) const {
if (no > 0) {
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
std::ostream& operator<<(ostream& os, const Student& s) {
s.display(os);
return os;
}
std::istream& operator>>(istream& is, Student& s) {
s.read(is);
return is;
}
The following client code uses our upgraded Student class accepts the input shown on the right and produces the results shown below:
// Standard I/O Operators
// standardIO.cpp
#include “Student.h”
int main () {
Student harry;
std::cin >> harry;
std::cout << harry;
}
Student Number : 1234
Number of Grades : 2
Grade 1 : 56.7
Grade 2 : 78.9
1234:
56.70
78.90
FILE I/O OPERATORS
The stream library does not predefine any file objects as instances of the file stream classes. To create file objects, we need to define them ourselves and connect them to a named file. A file object is an instance of one of the file stream classes. When used with the insertion or extraction operators on a connected file, a file object streams the data in formatted form.
File Connections
We can connect a file object to a file for reading, writing or both. The object's destructor closes the connection.
Input File Objects
To create a file object for reading we define an instance of the std::ifstream class. This class includes a no-argument constructor as well as one that receives the address of a C-style null-terminated string containing the file name.
For example,
// Create a File for Reading
// createFileReading.cpp
#include
int main() {
std::ifstream f(“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,
// Connect to a File for Reading
// connectFileReading.cpp
#include
int main() {
std::ifstream fin; // defines a file object named fin
fin.open(“input.txt”); // connects input.txt to fin
// …
}
Output File Objects
To create a file object for writing we define an instance of the std::ofstream class. This class includes a no-argument constructor as well as one that receives the address of a C-style null-terminated string containing the name of the file.
For example,
// Writing to a File
// writeFile.cpp
#include
int main() {
std::ofstream fout(“output.txt”); // connects fout to output.txt for writing
// …
}
To connect a file to an existing file object, we call the open() member function on the object.
For example,
// Connect to a File for Writing
// connectFileWriting.cpp
#include
int main() {
std::ofstream fout; // create a file object named fout
std::ofstream fout(“output.txt”); // connects fout to output.txt for writing
// …
}
Confirming a File Connection
The is_open() member function called on a file object returns the current state of the object’s connection to a file:
#include
#include
std::ofstream fout(“output.txt”); // connects output.txt to fout for output
if (!fout.is_open()) {
std::cerr << "File is not open" << std::endl;
} else {
// file is open
// ...
}
Streaming Fundamental Types
The standard input/output library overloads the extraction and insertion operators for each fundamental type for the file stream classes with a file objects as left operands.
Reading From a File
A file object reads from a file under format control using the extraction operator in the same way as the standard input object (cin) reads using the extraction operator.
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
#include
int main() {
int i;
std::ifstream f(“input.txt”);
if (f.is_open()) {
while (f) {
f >> i;
if (f)
std::cout << i << ' ';
else
std::cout << "\n**Bad input**\n";
}
}
}
12 34 45
**Bad input**
The file stream class definition overload the bool conversion operator to return false if the object is not ready for further streaming. A stream object is not ready for further streaming if it has encountered an error and has not been cleared. the topic of error states and clearing errors is covered later in the chapter entitled Input and Output Refinements.
Writing to a File
A file object writes to a file under format control using the insertion operator in the same way as the standard output objects (cout, cerr and clog) write using the insertion 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
#include
int main() {
int i;
std::ofstream f(“output.txt”);
if (f.is_open()) {
f << "Line 1" << std::endl; // record 1
f << "Line 2" << std::endl; // record 2
f << "Line 3" << std::endl; // record 3
}
}
Line 1
Line 2
Line 3
STRING CLASS (OPTIONAL)
The examples in these notes have been limited to input data that fits within pre-allocated memory. In the case of character string input, the user determines the number of characters to enter and pre-allocation of the required memory is not possible. A user entering more characters than allocated memory can accept may cause a stream failure.
The Problem
Consider the user inputting a comment on a student's transcript. Since we only know how much memory to allocate for the comment after receiving the complete text, we cannot allocate that memory at compile-time or run-time before accepting the comment.
The Solution
The standard library's string class allocates the required amount of memory dynamically during the input process itself. A std::string object can accept as many characters as the user enters. The helper function std::getline() extracts the characters 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 std::istream object, the second parameter receives a modifiable reference to the std::string object and the third parameter receives the character delimiter for terminating extraction (newline by default).
The
· 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
C-Style Example
The following client code extracts an unknown number of characters from the standard input stream, stores them in a C-style null-terminated string and displays the character string on the standard output object in five steps:
1. define a string object to accept the input
2. extract the input using the std::getline() helper function
3. query the string object for the memory required
4. allocate dynamic memory for the requisite C-style null-terminated string
5. copy the data from the string object to the allocated memory
6. deallocate the allocated memory
// String Class example
// string.cpp
#include
#include
int main( ) {
char* s;
std::string str;
std::cout << "Enter a string : ";
if (std::getline(std::cin, str)) {
s = new char [str.length() + 1];
std::strcpy(s, str.c_str());
std::cout << "The string entered is : >” << s << '<' << std::endl;
delete [] s;
}
}
Student Class Example
Let us upgrade our Student class to store a comment using a string object as a data member.
The header file for our Student class contains:
// Student.h
#include
#include
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
std::string comment;
public:
Student();
Student(int);
Student(int, const float*, int, const std::string&);
void read(std::istream&);
void display(std::ostream&) const;
};
std::istream& operator>>(std::istream& is, Student& s);
std::ostream& operator<<(std::ostream& os, const Student& s);
The implementation file contains:
// Student.cpp
#include "Student.h"
using namespace std;
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
*this = Student(n, nullptr, 0, "");
}
Student::Student(int sn, const float* g, int ng_, const string& c) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
comment = c;
} else {
*this = Student();
}
}
void Student::read(std::istream& is) {
int no; // will hold the student number
int ng; // will hold the number of grades
float grade[NG]; // will hold the grades
string comment; // will hold comments
cout << "Student Number : ";
is >> no;
cout << "Number of Grades : ";
is >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
cout << "Grade " << i + 1 << " : ";
is >> grade[i];
}
is.ignore(); // extract newline
cout << "Comments : ";
getline(is, comment, '\n');
// construct a temporary Student
Student temp(no, grade, ng, comment);
// if data is valid, the temporary object into the current object
if (temp.no != 0)
*this = temp;
}
void Student::display(std::ostream& os) const {
if (no > 0) {
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
os << "Comments:\n" << comment << endl;
} else {
os << "no data available" << endl;
}
}
std::ostream& operator<<(std::ostream& os, const Student& s) {
s.display(os);
return os;
}
std::istream& operator>>(std::istream& is, Student& s) {
s.read(is);
return is;
}
The client code on the left receives the input on the right and produces the output listed below:
// String Class
// string.cpp
#include
#include “Student.h”
int main ( ) {
Student harry;
std::cin >> harry;
std::cout << harry << std::endl;
}
Student Number : 1234
Number of Grades : 2
Grade 1 : 56.7
Grade 2 : 78.9
Comments : See Coordinator
1234:
56.70
78.90
Comments:
See Coordinator
SUMMARY
· we associate our own classes with the iostream classes by overloading the extraction and insertion operators as helpers to those classes
· the first parameter in the declaration of each overloaded operator is a modifiable reference to the stream object
· the return type of each overloaded operator is a modifiable reference to the stream object, which enables cascading
· the standard library includes overloaded extraction and insertion operators for file objects as left operands and fundamental types as right operands
· an input file object is an instance of an ifstream class
· an output file object is an instance of an ofstream class
· the string class of the standard library manages the memory requirements for storing a user-defined character string of any length
Object-oriented languages implement reusability of coding structure through inheritance. Inheritance is the second most prominent concept next to encapsulation. It refers to the relationship between classes where one class inherits the entire structure of another class. Inheritance is naturally hierarchical, a tighter relationship than composition and the most highly coupled relationship after friendship.
This chapter introduces the terminology used to describe an inheritance relationship and the syntax for defining a class that inherits the structure of another class. This chapter includes specification of accessibility privileges between classes within a hierarchy.
HIERARCHIES
A comprehensive example of inheritance relationships is the Linnaean Hierarchy in Biology (a small portion is shown below). The Linnaean hierarchy relates all biological species in existence to one another. Proceeding from the bottom of the hierarchy, we identify a human as a Homo, which is a Hominidae, which is a Primate, which is a Mammal, which is a Chordata, which is an Animal. Similarly a dog is a Canis, which is a Canidae, which is a Carnivora, which is a Mammal, which is a Chordata, which is an Animal.
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 along with 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 in Hierarchies
Inheritance is a transitive structural 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.
Inheritance is not commutative. A Primate is an Animal, but an Animal is not necessarily a Primate: dogs and foxes are not Primates. Primates have highly developed hands and feet, shorter snouts and larger brains than dogs and foxes.
Terminology
is a kind of
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 an inheritance relationship by an arrow pointed to the inherited class.
The Hybrid Course class inherits the entire structure of the Course class.
Derived and Base Classes
We call the child in an is-a-kind-of relationship the derived class and we call the parent in the relationship the base class; that is, the Hybrid Course class is a derived class of the Course base class. A derived class is lower in the hierarchy, while its base class is higher in the hierarchy. The derived class inherits the entire structure of its base class.
The inheritance arrow extends from the derived class to the base class:
We depict an object of a derived class by placing its instance variables after the instance variables of its base class in the direction of increasing addresses in memory:
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 in addition to its own instance variables and member functions. A derived class does not inherit the base class' special functions: constructors, destructors or assignment operators. The term normal member function excludes 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 is public.
Example
A Student is a kind of Person. Every Person has a name. Accordingly, let us derive our Student class from a Person class, where the Person class includes an instance variable that holds a name in the form of a character string.
The header file for our Student class contains our definitions of the base and derived classes:
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person { // start of Base Class Definition
char name[NC+1];
public:
void set(const char* n);
void displayName(std::ostream&) const;
}; // end of Base Class Definition
class Student : public Person { // start of Derived Class Definition
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream&) const;
}; // end of Derived Class definition
The implementation file defines the member functions:
// Student.cpp
#include
#include “Student.h”
using namespace std;
void Person::set(const char* n) {
strncpy(name, n, NC);
name[NC] = ‘\0’;
}
void Person::displayName(std::ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(std::ostream& os) const {
if (no > 0) {
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client code uses this implementation to produce the results on the right:
// Derived Classes
// derived.cpp
#include
#include “Student.h”
int main() {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.set(“Harry”); // inherited
harry.displayName(std::cout); // inherited
harry.display(std::cout); // not inherited
}
Harry 1234:
89.40
67.80
45.50
Note that the main() function refers to the Student type, without referring to the Person type. Here, the hierarchy itself is invisible to the client code. We can upgrade the hierarchy without having to alter the client code in any way.
ACCESS
The C++ language supports three modifiers for granting access to the members of class:
· private – bars all access
· protected – limits access to derived classes only
· public – unlimited access
Since the data member of the Person class is private, the member functions of our Student class and the client code cannot access that data member. Since the member functions of the Person and Student classes are public, the main() function can access all of them.
Limiting Access to Derived Classes
The keyword protected limits access to members of a derived class.
For example, let us limit access to displayName() to classes derived for the Person class. 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
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
void set(const char* n);
protected:
void displayName(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream&) const;
};
Our implementation of Student::display() calls displayName() directly:
// Student.cpp
#include
#include “Student.h”
using namespace std;
void Person::set(const char* n) {
strncpy(name, n, NC); // validates length
name[NC] = ‘\0’;
}
void Person::displayName(std::ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(std::ostream& os) const {
if (no > 0) {
displayName(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
We refer to displayName() directly without any scope resolution as if this function is a member of our Student class.
The following client code produces the output shown on the right:
// Protected Access
// protected.cpp
#include
#include “Student.h”
int main() {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.set(“Harry”); // inherited
harry.display(std::cout); // not inherited
}
Harry 1234:
89.40
67.80
45.50
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 client code access to the person data member, we could change its contents from our Student class to a string of more than NC characters, which would probably break our Student object.
Good Design Tip
Granting protected access to any data member exposes that member to potential corruption and is considered poor design. A protected read-only query is a preferable alternative to protected access to a data member. The query does not allow any modification of the value in the data member.
SUMMARY
· inheritance is a hierarchical relationship between classes.
· a derived class inherits the entire structure of its base class
· the access modifier protected grants access to member functions of the derived class
· any member function of a derived class may access any protected or public member of its base class
· keeping a data member private and accessing it through a protected query is good design
The logic that a derived class inherits from its base class is limited to the normal member functions of the base class. A derived class does not by default inherit the constructors, the destructor or the copy assignment operator – that is, the special member functions – of the base class. The special member functions in a class hierarchy define the logic for the creation, destruction and copying of different parts of an object and are necessarily different. A derived class’ constructor automatically calls the base class’ default constructor. A derived class’ destructor automatically calls the base class’ destructor. A derived class’ copy assignment operator automatically calls the base class’ copy assignment operator.
This chapter describes how member functions shadow one another in a hierarchy and the order in which constructors and destructors call one another. This chapter shows how to define a derived class’ constructor to access a specific base class constructor and 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 identifier. The C++ compiler binds a call to the member function of the derived class, if one exists.
To access the base class version of a member function that a derived class version has shadowed, we use scope resolution. A call to a shadowed function takes the form
Base::identifier(arguments)
where Base identifies the class to which the shadowed function belongs.
Example
Consider the following hierarchy. The base class and the derived class define distinct versions of the display() member function. The Student class version shadows the Person class version for any object of Student type:
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
void set(const char* n);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream&) const;
};
We access the base class version from the derived version using scope resolution:
// Student.cpp
#include
#include “Student.h”
using namespace std::
void Person::set(const char* n) {
strncpy(name, n, NC);
name[NC] = ‘\0’;
}
void Person::display(std::ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client code produces the output shown on the right:
// Shadowing
// shadowing.cpp
#include
#include “Student.h”
int main() {
Person jane;
jane.set(“Jane Doe”);
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.set(“Harry”); // inherited
harry.display(std::cout); // not inherited
jane.display(std::cout);
std::cout << std::endl;
}
Harry 1234:
89.40
67.80
45.50
Jane Doe
harry.display(std::cout) calls Student::display(), which calls the shadowed Person::display(), while jane.display() calls Person::display() directly. The derived version shadows the base version when called on harry.
Good Design Tip
By calling Person::display() within Student::display(), we hide the hierarchy from the client code. The main() function is hierarchy agnostic.
Exposing an Overloaded Member Function(Optional)
The C++ language shadows member functions on their identifier and not on their signature. To expose an overloaded member function in the base class with the same identifier but a different 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 two arguments: a modifiable reference to the output stream and the address of a C-style null-terminated character string containing a prefix message. We insert the using declaration in the definition of the derived class to expose this member function and any other with the same identifier for objects of the derived class.
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
void set(const char* n);
void display(std::ostream&) const;
void display(std::ostream&, const char*) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream&) const;
using Person::display;
};
We define the overloaded display() function for the base class:
// Student.cpp
#include
#include “Student.h”
using namespace std;
void Person::set(const char* n) {
strncpy(name, n, NC);
name[NC] = ‘\0’;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
void Person::display(ostream& os, const char* msg) const {
os << msg << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student(n, nullptr, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client produces the result shown on the right:
// Overloading and Shadowing
// overloading.cpp
#include
#include “Student.h”
int main() {
Person jane;
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.set(“Harry”);
harry.display(std::cout);
harry.display(std::cout, “Name is “);
std::cout << std::endl;
jane.set("Jane Doe");
jane.display(std::cout);
std::cout << std::endl;
}
Harry 1234:
89.40
67.80
45.50
Name is Harry
Jane Doe
CONSTRUCTORS
A derived class does not inherit a base class constructor by default. That is, if we do not 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:
1. construct the base class portion of the complete object
1. allocate memory for the instance variables in the order of their declaration
2. execute the base class constructor
2. construct the derived class portion of the object
2. allocate memory for the instance variables in the order of their declaration
2. execute the derived class constructor
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
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
void set(const char* n);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display(std::ostream&) const;
};
The implementation file defines the base class constructor:
// Student.cpp
#include
#include “Student.h”
using namespace std;
Person::Person() {
cout << "Person()" << endl;
name[0] = '\0';
}
void Person::set(const char* n) {
cout << "Person(const char*)" << endl;
strncpy(name, n, NC);
name[NC] = '\0';
}
void Person::display(ostream& os) const {
os << name << ' ';
}
Student::Student() {
cout << "Student()" << endl;
no = 0;
ng = 0;
}
Student::Student(int n) {
cout << "Student(int)" << endl;
float g[] = {0.0f};
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
cout << "Student(int, const float*, int)" << endl;
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client uses this implementation to produce the result shown on the right:
// Derived Class Constructors
// derivedCtors.cpp
#include
#include “Student.h”
int main() {
Person jane;
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry.set(“Harry”);
harry.display(std::cout);
jane.set(“Jane”);
jane.display(std::cout);
}
Person()
Person()
Student(int, const float*, int);
Person(const char*);
Harry 1234:
89.40
67.80
45.50
Person(const char*);
Jane
In this example, the compiler constructs the two objects as follows:
1. allocates memory for jane
1. allocates memory for person
2. the base class constructor initializes person to an empty string
2. allocates memory for harry
1. allocates memory for name
2. the base class constructor initializes name to an empty string
3. allocates memory for no, grade and ng
4. the derived class constructor initializes
· no to 1234
· grade to {89.40f, 67.80f, 45.50f}
· ng to 3
Passing Arguments to a Base Class Constructor
Each constructor of a derived class, other than the no-argument constructor, receives in its parameters all of the values passed by the client. Each constructor forwards the values for the base class part of the object to the base class constructor. The base class constructor uses the values received to build the base class part of the object. The derived class constructor uses the values received to complete building the derived class part of the object.
A call to the base class constructor from a derived class constructor that forwards values takes the form
Derived( parameters ) : Base( arguments )
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 its call to the base class constructor. If we omit this call, the compiler inserts a call to the default 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 three-argument constructor to receive the student’s name. The header file declares a single-argument base class constructor and a four-argument derived class constructor:
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void display(std::ostream&) const;
};
The implementation of the single-argument constructor copies the name to the instance variable:
// Student.cpp
#include
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client uses this implementation to produce the output shown on the right:
// Derived Class Constructors with Arguments
// drvdCtorsArgs.cpp
#include
#include “Student.h”
int main() {
Person jane(“Jane”);
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(“Harry”, 1234, gh, 3);
harry.display(std::cout);
jane.display(std::cout);
}
Harry 1234:
89.40
67.80
45.50
Jane
Inheriting Base Class Constructors (Optional)
C++11 introduced syntax for inheriting a base class constructor in cases where the derived class constructor does not execute any logic on the instance variables of the derived class and only passes to the base class constructor values received from the client. In such cases, the derived class may inherit the base class constructors.
The declaration for inheriting a base class constructor takes the form:
using Base::Base;
where Base is the name of the base class.
Example
Let us derive an Instructor class from the Person base class and inherit all of the constructors of the base class. The header file overrides the no-inheritance default:
// Student.h
// compiles with GCC 4.8 or greater or equivalent
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void display(std::ostream&) const;
};
class Instructor : public Person {
public:
using Person::Person;
};
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
#include “Student.h”
int main() {
Instructor john(“John”);
Person jane(“Jane”);
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(“Harry”, 1234, gh, 3);
john.display(std::cout);
std::cout << std::endl;
harry.display(std::cout);
jane.display(std::cout);
}
John
Harry 1234:
89.40
67.80
45.50
Jane
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 tracking messages to standard output. We declare each destructor in its class definition:
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
~Student();
void display(std::ostream&) const;
};
We specify the messages in the destructor definitions:
// Student.cpp
#include
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
Person::~Person() {
std::cout << "Leaving " << name << std::endl;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
Student::~Student() {
std::cout << "\nLeaving " << no << std::endl;
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client uses this implementation to produce the output shown on the right:
// Derived Class Destructors
// drvdDtors.cpp
#include
#include “Student.h”
int main() {
Person jane(“Jane”);
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(“Harry”, 1234, gh, 3);
harry.display(std::cout);
jane.display(std::cout);
}
Harry 1234:
89.40
67.80
45.50
Jane
Leaving 1234
Leaving Harry
Leaving Jane
HELPER OPERATORS (OPTIONAL)
Helper functions support the classes identified by their parameter types. 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). That is, the helper functions of a base class do not directly support classes derived from the supported base class.
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
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
std::istream& operator>>(std::istream&, Person&);
std::ostream& operator<<(std::ostream&, const Person&);
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void read(std::istream&);
void display(std::ostream&) const;
};
std::istream& operator>>(std::istream&, Student&);
std::ostream& operator<<(std::ostream&, const Student&);
The implementation file defines the helper operators:
// Student.cpp
#include
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
istream& operator>>(istream& is, Person& p) {
char name[NC+1];
cout << "Name: ";
is.getline(name, NC+1);
p = Person(name);
return is;
}
std::ostream& operator<<(ostream& os, const Person& p) {
p.display(os);
return os;
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(std::ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
void Student::read(istream& is) {
char name[NC + 1]; // will hold the student's name
int no; // will hold the student's number
int ng; // will hold the number of grades
float grade[NG]; // will hold the grades
std::cout << "Name: ";
is.getline(name, NC+1);
cout << "Student Number : ";
is >> no;
cout << "Number of Grades : ";
is >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
cout << "Grade " << i + 1 << " : ";
is >> grade[i];
}
// construct a temporary Student
Student temp(name, no, grade, ng);
// if data is valid, the temporary object into the current object
if (temp.no != 0)
*this = temp;
}
istream& operator>>(istream& is, Student& s) {
s.read(is);
return is;
}
ostream& operator<<(ostream& os, const Student& s) {
s.display(os);
return os;
}
The following client uses this implementation to produce the output shown on the right:
// Helpers to Derived Classes
// drvdHelpers.cpp
#include
#include “Student.h”
int main() {
Person jane;
Student harry;
std::cin >> jane;
std::cin >> harry;
std::cout << jane << std::endl;
std::cout << harry << std::endl;
}
Name: Jane Doe
Name: Harry
Student Number : 1234
Number of Grades : 3
Grade 1 : 89.40
Grade 2 : 67.80
Grade 3 : 45.50
Jane Doe
Harry 1234
89.40
67.80
45.50
SUMMARY
· a member function of a derived class shadows an identically named member function of a base class
· a derived class does not inherit the destructor, assignment operators or helper functions of a base class
· a derived class does not by default inherit the constructor of a base class, but we may add syntax to allow inheritance where the derived class constructor does not contain logic to set its instance variables
· constructors in an inheritance hierarchy execute in order from the base class to the derived class
· destructors in an inheritance hierarchy execute in order from the derived class to the base class
Object-oriented languages support selection of behavior across related types through polymorphism. Polymorphism is the third principal concept that these languages implement (alongside encapsulation and inheritance). Polymorphism refers to the multiplicity of meanings attached to a single identifier. Polymorphic stands for 'of many forms'. A polymorphic language selects an operation on an object based on the type associated with the object.
Virtual functions are an example of inclusion polymorphism. Object-oriented languages implement inclusion polymorphism through member functions in a hierarchy. The type of a polymorphic object can change throughout its lifetime to any type in the same inheritance hierarchy. We distinguish between the static and dynamic type associated with a polymorphic object. Its static type is the type of the object's hierarchy, its dynamic type is the object's actual type.
This chapter describes how C++ implements inclusion polymorphism. The chapter describes the concept of types, the options for binding a function call to its definition and how polymorphic objects are implemented in C++.
TYPES
Raw memory stores information in the form of bit strings. These bit strings represent variables, objects, addresses, instructions, constants, etc. Without knowing what a bit string represents, the compiler cannot interpret the bit string. By associating a type with a region of memory, we tell the compiler how to interpret the bit string in that region of memory.
For example, if we associate a region of memory with a Student and define the structure of a Student, the compiler knows that the first 4 bytes holds an int stored in equivalent binary form, the next 12 bytes holds an array of 3 floats and the remaining 4 bytes hold an int.
C++ Pointers
C++ implements a polymorphic object using pointer syntax. The pointer type identifies the static type of the inheritance hierarchy to which the object belongs. This static type is known at compile time. The pointer holds the address of the polymorphic object.
To dereference the object's address, the compiler needs to know its dynamic type. The dynamic type is the referenced type of the object. Initially, we specify the dynamic type at object creation time through the constructor that we invoke.
In the following example, we instantiate a Person* object by dynamically allocating memory once for a Person type and one for a Student type.
void show(const Person*);
// Polymorphic Objects
Person jane("Jane");
float g[] = {54.6f, 67.7f, 89.6f};
Student john("John", 1234, g, 3);
Person* pJane = &jane;
Person* pJohn = &john;
// possibly different behaviors
show(pJohn);
show(pJane);
...
By implementing different behaviors for different types in the same hierarchy, we enable different execution paths in show() for different dynamic types.
FUNCTION BINDINGS
The compiler binds a function call to a function definition using an object's type. The object's type determines the member function to call in the inheritance hierarchy.
The binding of a member function can take either of two forms:
· early binding - based on the object's static type
· dynamic dispatch - based on the object's dynamic type
Early Binding
Consider the following definition of our Student class from the chapter entitled Functions in a Hierarchy:
// Early Binding
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void display(std::ostream&) const;
};
The implementation file is also the same as in the chapter entitled Functions in a Hierarchy:
// Student.cpp
#include
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
Note that this hierarchy has two distinct definitions of the member function named display().
The main() function listed below defines a global function named show(). This client code calls that global function twice, first for a Student object and second for a Person object. The global function show() in turn calls the display member function on p. The compiler binds the call to this member function to its Person version. C++ applies this convention irrespective of the argument type in the call to show(). That is, the compiler uses the parameter type in definition of show() to determine the kind of binding to implement. We call this binding an early binding.
The client program produces the output shown on the right:
// Function Bindings
// functionBindings.cpp
#include
#include “Student.h”
void show(const Person& p) {
p.display(std::cout);
std::cout << std::endl;
}
int main() {
Person jane("Jane Doe");
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry("Harry", 1234, gh, 3);
harry.display(std::cout);
jane.display(std::cout);
std::cout << std::endl;
show(harry);
show(jane);
}
Harry 1234:
89.40
67.80
45.50
Jane Doe
Harry
Jane Doe
Early binding occurs at compile time and is the most efficient binding of a member function call to that function's definition. Early binding is the default in C++.
Note that shadowing does not occur inside the global function show(). show() has no way of knowing which version of display() to select aside from the type of its parameter p. The statements harry.display() and jane.display() in the main() function demonstrate shadowing. The call to display() on harry shadows the base version of display().
Dynamic Dispatch
The output in the above example omits the details for the Student part of harry. To output these details, we need to postpone the binding of the call to display() until run-time when the executable code is aware of the dynamic type of object p. We refer to this postponement as dynamic dispatch.
C++ provides the keyword virtual for dynamic dispatching. If this keyword is present, the compiler inserts code that binds the call to most derived version of the member function based on the 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:
// Dynamic Dispatch
// Student.h
#include
const int NC = 30;
const int NG = 20;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
virtual void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void display(std::ostream&) const;
};
Note that the implementation file and the client program have not changed. Because the keyword is present, the compiler overrides the early binding of display() so that the show() function will call the most derived version of display() for the type of the argument passed to it. The following client code (identical to that above) then produces the output shown on the right:
// Function Bindings
// functionBindings.cpp
#include
#include “Student.h”
void show(const Person& p) {
p.display(std::cout);
std::cout << std::endl;
}
int main() {
Person jane("Jane Doe");
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry("Harry", 1234, gh, 3);
harry.display(std::cout);
jane.display(std::cout);
std::cout << std::endl;
show(harry);
show(jane);
}
Harry 1234:
89.40
67.80
45.50
Jane Doe
Harry 1234:
89.40
67.80
45.50
Jane Doe
Each call to show() passes a reference to an object of different dynamic type:
· show(harry) passes an unmodifiable reference to a Student
· show(jane) passes an unmodifiable reference to a Person
In each case, the executable code binds at run time the version of display() that is the most derived version for the dynamic type referenced by the parameter in show().
Note that if we pass the argument to the show() function by value instead of by reference, the show() function would still call the most derived version of display(), but that most derived version would be for the Person version, since the copied object would be a Person in all cases.
Overriding Dynamic Dispatch
To override dynamic dispatch with early binding, we resolve the scope explicitly:
void show(const Person& p) {
p.Person::display(std::cout);
}
Documentation
Some programmers include the qualifier virtual in derived class declarations as a form of documentation. This improves readability but has no syntactic effect.
We can identify a member function as virtual even if no derived class exists. This clarifies the intent of the original developer for subsequent developers of the hierarchy
POLYMORPHIC OBJECTS
A polymorphic object is an object that can change its dynamic type throughout its lifetime. Its static type identifies the hierarchy of types to which the object belongs. Its dynamic type identifies the rule for interpreting the bit string in the region of memory currently allocated for the object.
We specify the static type of a polymorphic object 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
#include “Student.h”
void show(const Person* p) {
// …
}
void show(const Person& p) {
// …
}
int main() {
Person* p = nullptr;
// …
}
We specify the dynamic type of a polymorphic object by allocating memory dynamically using the appropriate constructor from the inheritance hierarchy.
The highlighted code in the example below identifies the dynamic type. The results produced by this code are listed on the right:
// Polymorphic Objects – Dynamic Type
// dyanmicType.cpp
#include
#include “Student.h”
void show(const Person& p) {
p.display(std::cout);
std::cout << std::endl;
}
int main() {
Person* p = nullptr;
p = new Person("Jane Doe");
show(*p);
delete p;
float g[] = {89.4f, 67.8f, 45.5f};
p = new Student("Harry", 1234, g, 3);
show(*p);
delete p;
}
Jane Doe
Harry 1234:
89.40
67.80
45.50
In the main() function:
· p initially points to nothing (holds the null address). The object's dynamic type is undefined.
· after the first allocation, p points to a Student type (dynamic type).
· after the second allocation, p points to a Person type (the new dynamic type).
The static and dynamic types are related to one another through the hierarchy.
Note that we only need one show() function to display both dynamic types.
p holds the address a polymorphic object throughout its lifetime. That address may change with deallocations and fresh allocations of memory. The dynamic type may be of any type in the Person hierarchy.
show() is a polymorphic function. Its parameter receives an unmodifiable reference to any type in the Person hierarchy.
Good Design
It is good programming practice to dynamically dispatch the destruction of any object in an inheritance hierarchy as virtual. If an object of a derived class acquires a resource, typically the destructor of that class releases the resource. To ensure that any object in the hierarchy calls the destructor of its most derived class at destruction time, we declare the base class destructor virtual. Since the destructor of any derived class automatically calls the destructor of its immediate base class, all destructors in the object's hierarchy will be called in turn.
Good design codes the destructor in a base class as virtual, even if no class is currently derived from that base class. The presence of a virtual base class destructor ensures that the most derived destructor will be called if and when a class is derived from the base class without requiring an upgrade to the definition of the base class.
REUSABILITY AND FLEXIBILITY
Implementing inclusion polymorphism improves reusability and flexibility of code.
Virtual functions reduce code size considerably. Our show() function works on references of any type within the Person hierarchy. We only define member functions (display()) for those classes that require specialized processing.
Consider a client application that uses our hierarchy. During the life cycle of the hierarchy, we may add several classes. Our original client code, without any alteration, will selects the most derived version of the member function for each upgrade of the hierarchy. We will only need to add client code to create objects of new derived classes.
SUMMARY
· polymorphism refers to the multiplicity of logic associated with the same name.
· static type is the type of the object's hierarchy and is available at compile-time
· dynamic type is the type of the object referenced and may change with different calls to the same function
· early binding of a call to a member function's definition occurs at compile-time
· the keyword virtual on a member function's declaration specifies dynamic dispatch
· a polymorphic object's pointer type identifies the object's static type
· a polymorphic object's constructor identifies the object's dynamic type
· declare a base class destructor virtual even if there are no derived classes
Object-oriented languages use interfaces to define the single identifier to multiple meanings that polymorphism provides. Separating the interface from its various implementations promotes low coupling between the client code and an object's class hierarchy. The interface specifies what any object in the hierarchy offers to a client, while each implementation specifies how the interface provides what it has offered to its clients. This separation of concerns is central to software engineering. The interface effectively hides the hierarchy from its clients. We can upgrade the hierarchy by adding derived classes without having to change the client code. We can upgrade the client code without having to change the hierarchy.
C++ supports the distinction between an interface and its implementations through abstract and concrete classes. An abstract class is a base class that defines an interface, while a concrete class is a derived class that implements that interface. The abstract class identifies the member functions that the class hierarchy exposes to its clients and is the gateway to testing the derived classes in its own inheritance hierarchy. Each concrete class gives a specific meaning to the interface.
This chapter describes pure virtual functions, which are the principal components of an abstract base class. The chapter shows how to define an abstract class and use it with an array of pointers in client code. This chapter concludes with an example of a unit test on an abstract base class.
PURE VIRTUAL FUNCTION
The principal component of an abstract base class is a pure virtual member function. Pure refers to the lack of any implementation detail. That is, a pure virtual function is a signature without a definition. The client code only requires access to the signature.
Declaration
The declaration of a pure virtual function takes the form
virtual Type identifier(parameters) = 0;
The assignment to 0 identifies the virtual function as pure. A pure function must be a virtual member function.
Example
We define the pure virtual function for the signature display(std::ostream&) const using
virtual void display(std::ostream&) const = 0;
Implementations
A pure virtual member function typically has multiple definitions within its hierarchy. Each definition has the same signature as the pure virtual function but a different meaning. That is, it provides the client with the implementation that suits a specific dynamic type.
Note that the separation between the client and the hierarchy's implementation is crisp. The client code does not need any access to the variety of implementations available within the hierarchy. The implementation code has no access to the client codes that use the hierarchy.
ABSTRACT CLASSES
An abstract class is a class that contains or inherits a pure virtual function. Because the class provides no implementation(s) for its pure virtual function(s), the compiler cannot instantiate the class. Any attempt to create an instance of an abstract base class generates a compiler error.
Definition
The definition of any abstract base class contains or inherits at least one pure virtual member function. The class definition contains the declaration of the pure virtual function. We call an abstract base class without any data members a pure interface.
Example
Let us define an abstract base class named iPerson for our Person hierarchy and use this class to expose the hierarchy's display() function to any client code.
The iPerson.h header file defines our abstract class:
// Abstract Base Class for the Person Hierarchy
// iPerson.h
#include
class iPerson {
public:
virtual void display(std::ostream&) const = 0;
};
We derive our Person hierarchy from this interface. The header file that defines our Person and Student class includes the header file that defines our abstract base class:
// Late Binding
// Student.h
#include
#include “iPerson.h”
const int NC = 30;
const int NG = 20;
class Person : public iPerson {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void display(std::ostream&) const;
};
The class definitions for the Person and Student classes inform the compiler that each concrete class implements its own version of the display() member function.
ARRAY OF POINTERS
A systematic technique for accessing objects of different dynamic type within the same hierarchy is through an array of pointers of their static type. The executable code dereferences each pointer at run time based on its object’s dynamic type.
Person Example
The following code demonstrates the use of an array of pointers to Person objects. The objects pointed to by the array elements may be of differing dynamic type, but are of the same static type. The CreatePerson() global function creates a Person object and returns its address.
Client Code
The following client code manages Person objects through the array of pointers p. The output generated for the input provided is listed on the right:
// Array of Pointers
// array_of_pointers.cpp
#include
#include “iPerson.h”
const int NP = 5;
int main() {
iPerson* p[NP];
for (int i = 0; i < NP; i++)
p[i] = nullptr;
int n = 0;
bool quit = false;
do {
iPerson* ptemp = CreatePerson();
if (ptemp != nullptr) {
p[n++] = ptemp;
} else {
quit = true;
}
} while(n < NP && !quit);
for (int j = 0; j < n; j++) {
p[j]->display(std::cout);
std::cout << std::endl;
}
for (int j = 0; j < n; j++)
delete p[j];
}
Type (0,1,2) : 1
Name: Jane Doe
Type (0,1,2) : 2
Name: Harry
Student Number : 1234
Number of Grades : 3
Grade 1 : 45.6
Grade 2 : 67.8
Grade 3 : 89.5
Type (0,1,2) : 0
Jane Doe
Harry 1234:
45.60
67.80
89.50
Interface
The interface to a Person object includes the prototype for a global function that creates the object:
// Abstract Base Class for the Person Hierarchy
// iPerson.h
#include
class iPerson {
public:
virtual void display(std::ostream&) const = 0;
};
iPerson* CreatePerson();
Concrete Class Definitions
The concrete class definitions specify the various implementations of the iPerson interface:
// Person and Student Concrete Classes
// Student.h
#include “iPerson.h”
const int NC = 30;
const int NG = 20;
class Person : public iPerson {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
void display(std::ostream&) const;
};
iPerson* CreatePerson();
The prototype for CreatePerson() is repeated for documentation.
Implementations
Each concrete class that declares the display() member function in its definition defines its own version of the display() function. The implementation file also defines the global CreatePerson() function:
// Person Hierarchy – Implementation
// person.cpp
#include
#include
#include “Student.h”
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
std::strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(std::ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
*this = Student("", n, nullptr, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display(std::ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(std::ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << std::endl;
}
os.unsetf(std::ios::fixed);
os.precision(6);
} else {
os << "no data available" << std::endl;
}
}
iPerson* CreatePerson() {
iPerson* p = nullptr;
int type, no, ng;
float grade[NG];
char name[NC+1];
bool repeat;
do {
std::cout << "Type (0,1,2) : ";
std::cin >> type;
std::cin.ignore();
repeat = false;
switch(type) {
case 0:
break;
case 1:
std::cout << "Name: ";
std::cin.getline(name, NC+1);
p = new Person(name);
break;
case 2:
std::cout << "Name: ";
std::cin.getline(name, NC+1);
std::cout << "Student Number : ";
std::cin >> no;
std::cout << "Number of Grades : ";
std::cin >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
std::cout << "Grade " << i + 1 << " : ";
std::cin >> grade[i];
std::cin.ignore();
}
p = new Student(name, no, grade, ng);
break;
default:
repeat = true;
std::cout << "Invalid type. Try again\n";
}
} while(repeat);
return p;
}
Multiple definitions would be unnecessary if the definitions of display() were identical.
UNIT TESTS ON AN INTERFACE
Good programming practice suggests that we code unit tests for an interface rather than a specific implementation. This practice requires that the interface does not change during the life cycle of the software. With a constant interface we can perform unit tests at every upgrade throughout an object's lifecycle without changing the test code.
Example
To illustrate a unit test on the interface of a hierarchy, consider a module of Sorter classes. The number of implementations changes throughout the life cycle.
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 execute the unit tester on the interface.
Sorter Module
Each Sorter class sorts in a different way. The interface to the hierarchy exposes the sort() and name() member functions of each class.
The header file for the interface contains:
// Sorter Interface
// iSorter.h
class iSorter {
public:
virtual void sort(float*, int) = 0;
virtual const char* name() const = 0;
};
iSorter* CreateSorter(int);
int noOfSorters();
The header file for the Sorter concrete classes contains:
// Sorter Concrete Classes
// Sorter.h
#include "iSorter.h"
class SelectionSorter : public iSorter {
public:
void sort(float*, int);
const char* name() const;
};
class BubbleSorter : public iSorter {
public:
void sort(float*, int);
const char* name() const;
};
iSorter* CreateSorter(int);
int noOfSorters();
The implementation file for the Sorter module defines the sort() and name() member functions for the SelectionSorter class and the BubbleSorter class as well as the global CreateSorter() and noOfSorters() functions:
// Sorter Hierarchy - Implementation
// Sorter.cpp
#include "Sorter.h"
void SelectionSorter::sort(float* a, int n) {
int i, j, max;
float temp;
for (i = 0; i < n - 1; i++) {
max = i;
for (j = i + 1; j < n; j++)
if (a[max] > a[j])
max = j;
temp = a[max];
a[max] = a[i];
a[i] = temp;
}
}
const char* SelectionSorter::name() const {
return “Selection Sorter”;
}
void BubbleSorter::sort(float* a, int n) {
int i, j;
float temp;
for (i = n – 1; i > 0; i–) {
for (j = 0; j < i; j++) {
if (a[j] > a[j+1]) {
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
const char* BubbleSorter::name() const {
return “Bubble Sorter”;
}
iSorter* CreateSorter(int i) {
iSorter* sorter = nullptr;
switch (i) {
case 0:
sorter = new SelectionSorter();
break;
case 1:
sorter = new BubbleSorter();
break;
}
return sorter;
}
int noOfSorters() {
return 2;
}
Unit Tester
The unit tester generates the test data and reports the test results:
// Test Main for the iSorter Interface
// Test_Main.cpp
#include
#include
#include “iSorter.h”
void populate(float* a, int n) {
srand(time(nullptr));
float f = 1.0f / RAND_MAX;
for (int i = 0; i < n; i++)
a[i] = rand() * f;
}
void test(iSorter* sorter, float* a, int n) {
sorter->sort(a, n);
bool sorted = true;
for (int i = 0; i < n - 1; i++)
if (a[i] > a[i+1]) sorted = false;
if (sorted)
std::cout << sorter->name()
<< " is sorted" << std::endl;
else
std::cout << sorter->name()
<< " is not sorted" << std::endl;
}
int main() {
int n;
std::cout << "Enter no of elements : ";
std::cin >> n;
float* array = new float[n];
for (int i = 0; i < noOfSorters(); i++) {
iSorter* sorter = CreateSorter(i);
populate(array, n);
test(sorter, array, n);
delete sorter;
}
delete [] array;
}
Enter no of elements : 200
Selection Sorter is sorted
Bubble Sorter is sorted
Note that we do not need to change this test code if we derive another class from the iSorter interface.
SUMMARY
· a pure virtual function is a member function declaration without an implementation
· an abstract base class contains or inherits at least one pure virtual function
· an interface is an abstract base class with no data members
· good programming practice performs unit tests on an interface rather than any specific implementation
Polymorphism is not restricted to related types in object-oriented languages. Many languages also support selection across unrelated types. This polymorphism, which perfects the separation of interfaces from implementations, is called parametric or generic polymorphism. In parametric polymorphism the type and the logic executed on that type are independent of one another. Different clients can access the same logic using different totally unrelated types.
The C++ language implements parametric polymorphism using template syntax. The compiler generates the implementation for each client type at compile-time from the template defined by the developer.
This chapter describes how to implement parametric polymorphism using template syntax with reference to functions and classes. This chapter also describes the templated keywords available for casting values from one type to another.
FUNCTION TEMPLATE
Template Syntax
A template definition resembles that of a global function with the parentheses replaced by angle brackets. A template header takes the form
template
The keyword template identifies the subsequent code block as a template. The less-than greater-than angle bracket pair (< >) encloses the parameter definitions for the template. The ellipsis denotes more comma-separated parameters. identifier is a placeholder for the argument specified by the client.
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
The following examples are equivalent to one another:
template
// … template body follows here
T value; // value is of type T
template
// … template body follows here
T value; // value is of type T
The compiler replaces T with the argument specified by the client code.
Complete Definition
Consider the following function that swaps values in two different memory locations. This code is defined using references to two int variables:
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
void swap(T& a, T& b) {
T c;
c = a;
a = b;
b = c;
}
We place template definitions in header files; in this case, in swap.h.
Calling a Templated Function
A call to a templated function determines the specialization that the compiler generates. The compiler binds the call to that specialization.
For example, to call the swap() function for two doubles and two longs, we write the following and leave the remaining work to the compiler:
// Calling a Templated Function
// swap.cpp
#include
#include “swap.h” // template definition
int main() {
double a = 2.3;
double b = 4.5;
long d = 78;
long e = 567;
swap(a, b); // compiler generates
// swap(double, double)
std::cout << "Swapped values are " <<
a << " and " << b << std::endl;
swap(d, e); // compiler generates
// swap(long, long)
std::cout << "Swapped values are " <<
d << " and " << e << std::endl;
}
Swapped values are 4.5 and 2.3
Swapped values are 567 and 78
If the arguments in each call are unambiguous in their type, the compiler can specialize the template appropriately. If the arguments are ambiguous, the compiler reports an error.
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 type (T) of each element in the array and the number of elements in the array (N):
// Template for Array Classes
// Array.h
template
class Array {
T a[N];
public:
T& operator[](int i) { return a[i]; }
};
For the following code, the compiler generates the class definition for an array of element type int and size 5 from the Array template definition. The output from executing this client program is shown on the right:
// Class Template
// Template.cpp
#include
#include “Array.h”
int main() {
Array
for (int i = 0; i < 5; i++)
a[i] = i * i;
b = a;
for (int i = 0; i < 5; i++)
std::cout << b[i] << ' ';
std::cout << std::endl;
}
0 1 4 9 16
CONSTRAINED CASTS
Constrained casts improve type safety. 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. Casting a value from one type to another type circumvents the type system's type checking facilities. It is good programming practice to implement casts only where absolutely unavoidable and localize them as much as possible.
C++ supports constrained type casting through template syntax using one of the following keywords:
· static_cast
· reinterpret_cast
· const_cast
· dynamic_cast
Type specifies the destination type. expression refers to the value to be cast to the destination type.
Related Types
The static_cast
For example, to cast minutes to a float type, we write:
// Cast to a Related Type
// static_cast.cpp
#include
int main() {
double hours;
int minutes;
std::cout << "Enter minutes : ";
std::cin >> minutes;
hours = static_cast
std::cout << "In hours, this is " << hours;
}
static_cast
For example, the following constrained cast generates a compile-time error:
#include
int main() {
int x = 2;
int* p;
p = static_cast
std::cout << p;
}
Some static casts are portable across different platforms.
Unrelated Types
The reinterpret_cast
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
int main( ) {
int x = 2;
int* p;
p = reinterpret_cast
std::cout << p;
}
reinterpret_cast
For example, the following constrained cast generates a compile-time error:
#include
int main( ) {
int x = 2;
double y;
y = reinterpret_cast
std::cout << y;
}
Few reinterpret casts are portable. Uses include
· evaluating raw data
· recovering data where types are unknown
· quick and messy calculations
Unmodifiable Types
The const_cast
A common use case for this constrained cast 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
void foo(int* p) {
std::cout << *p << std::endl;
}
int main( ) {
const int x = 3;
const int* a = &x;
int* b;
// foo expects int* and not const int*
b = const_cast
foo(b);
}
const_cast
For example, the following code generates a compile-time error:
#include
int main( ) {
const int x = 2;
double y;
y = const_cast
std::cout << y;
}
Inherited Types
The dynamic_cast
Downcasts
dynamic_cast
For example:
// Downcast within the Hierarchy
// downcast.cpp
#include
class Base {
public:
virtual void display() const { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void display() const { std::cout << "Derived\n"; }
};
int main( ) {
Base* b1 = new Base;
Base* b2 = new Derived;
Derived* d1 = dynamic_cast
Derived* d2 = dynamic_cast
if (d1 != nullptr)
d1->display();
else
std::cerr << "d1 is not derived" << std::endl;
if (d2 != nullptr)
d2->display();
else
std::cerr << "d2 is not derived" << std::endl;
delete b1
delete d2;
}
d1 is not derived
Derived
Upcasts
dynamic_cast
For example, to cast a derived class pointer to a base object d to a pointer to its base class part, we write:
// Upcast within the Hierarchy
// upcast.cpp
#include
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;
b = dynamic_cast
if (b != nullptr)
b->display();
else
std::cerr << "Mismatch" << std::endl;
d->display();
delete d;
}
Base
Derived
Note that here the display() member function is not virtual. If it were, both calls to it would produce the same result.
Compile-Time Checking
dynamic_cast
For example, the following constrained cast generates a compile-time error:
// Dynamic Cast – Compile Time Checking
// dynamic_cast.cpp
#include
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
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.
// Static Cast – Compile Time Checking
// static_cast.cpp
#include
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 = static_cast
b->display();
d->display();
delete d;
}
Base
Derived
Note that if display() is declared virtual the output may be the same for both calls to display().
SUMMARY
· a template header consists of the keyword template followed by the template parameters
· the compiler generates the template specialization based on the argument types in the function call
· avoid type casting that completely bypasses the language’s type-checking facilities
· if type casting is necessary, use one of the four type cast keywords (usually static_cast)
Polymorphism was perfected in object-oriented languages, but has roots in procedural languages. Polymorphism relies on a language’s type system to distinguish different meanings for the same identifier. This ambiguity introduces flexibility and enables reusability of code.
This chapter describes the difference between monomorphic and polymorphic languages and the use of a type system to ensure consistency. This chapter also identifies the categories of polymorphism supported by object-oriented languages and reviews how C++ implements each category.
LANGUAGES
Programming languages evolved from untyped origins through monomorphic languages to polymorphic ones. Untyped languages support words of one fixed size. Assembly languages and BCPL are examples. Typed languages support regions of memory of different sizes distinguished by their type. Typed languages can be monomorphic or polymorphic. In a monomorphic language the type of an object, once declared, cannot change throughout the object’s lifetime. Polymorphic languages relax this relation between the object’s type and a region of memory by introducing some ambiguity. The type of a polymorphic object may change during its lifetime. This ambiguity brings object descriptions closer to natural language usage.
Monomorphic languages require separate code for each type of object. For instance, a monomorphic language requires the programmer to code a separate sort() function for each data type, even though the logic is identical across all types. Polymorphic languages, on the other hand, let the programmer code the function once. The language applies the programming solution to any type.
The C++ language assumes that an object is monomorphic by default, but lets the programmer override this default by explicitly identifying the object as polymorphic.
TYPE SYSTEMS
A type system introduces consistency into a programming language. It is the first line of defense against coding relationships between unrelated entities. Typically, the entities in the expressions that we code have some relation to one another. The presence of a type system enables the compiler to check whether such relations follow well-defined sets of rules. Each type in a type system defines its own set of admissible operations in forming expressions. The compiler rejects all operations outside this set. Breaking the type system exposes the underlying bit strings and introduces uncertainty in how to interpret the contents of their regions of memory.
A strongly typed language enforces type consistency at compile-time and only postpones type-checking to run-time for polymorphic objects.
The C++ language is a strongly typed language. It checks for type consistency on monomorphic objects at compile-time and on polymorphic objects at run-time.
Role of Polymorphism
A polymorphic language provides the rules for extending the language’s type system. Compilers apply their language’s type system to identify possible violations of that system. Not all type differences between entities are necessarily errors. Those differences that the language allows expose its polymorphism. That is, the polymorphic features of a language represent the admissible set of differences between types that the language as a polymorphic language supports.
CATEGORIES
The polymorphic features that an object-oriented language supports can be classified into four categories. The C++ language supports all of these categories.
Classifications
Christopher Strachey (1967) introduced the concept of polymorphism informally into procedural programming languages by distinguishing functions
· that work differently on different argument types
· that work uniformly on a range of argument types
He defined the former as ad-hoc polymorphism and the latter as parametric polymorphism:
“Ad-Hoc polymorphism is obtained when a function works, or appears to work, on several different types (which may not exhibit a common structure) and may behave in unrelated ways for each type. Parametric polymorphism is obtained when a function works uniformly on a range of types; these types normally exhibit some common structure.” (Strachey, 1967)
Cardelli and Wegner (1985) expanded Strachey’s distinction to accommodate object-oriented languages. They distinguished functions
· that work on a finite set of different and potentially unrelated types
· coercion
· overloading
· that work on a potentially infinite number of types across some common structure
· inclusion
· parametric
Inclusion polymorphism is specific to object-oriented languages.
Ad-Hoc Polymorphism
Ad-hoc polymorphism is apparent polymorphism. Its polymorphic character disappears at closer scrutiny.
Coercion
Coercion addresses differences between argument types in a function call and the parameter types in the function’s definition. Coercion allows convertible changes in the argument’s type to match the type of the corresponding function parameter. It is a semantic operation that avoids a type error.
If the compiler encounters a mismatch between the type of an argument in a function call and the type of the corresponding parameter, the language allows conversion from the type of the argument to the type of the corresponding parameter. The compiler inserts the code necessary to perform the coercion. The function definition itself only ever executes on one type – the type of its parameter.
Coercion has two possible variations
· narrow the argument type (narrowing coercion)
· widen the argument type (promotion)
For example,
// Ad-Hoc Polymorphism – Coercion
// coercion.cpp
#include
// One function definition:
void display(int a) {
std::cout << "One argument (" << a << ')';
}
int main( ) {
display(10);
std::cout << std::endl;
display(12.6); // narrowing
std::cout << std::endl;
display('A'); // promotion
std::cout << std::endl;
}
One argument (10)
One argument (12)
One argument (65)
Coercion eliminates type mismatch or missing function definition errors. C++ implements coercion at compile time. If the compiler recognizes a type mismatch that is a candidate for coercion, the compiler inserts the conversion code immediately before the function call.
Most programming languages support some coercion. For instance, C narrows and promotes argument types in function calls so that the same function will accept a limited variety of argument types.
Overloading
Overloading addresses variations in a function's signature. Overloading allows binding of function calls with the same identifier but different argument types to function definitions with correspondingly different parameter types. It is a syntactic abbreviation that associates the same function identifier with a variety of function definitions by distinguishing the bindings based on function signature. 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 for functions with the same identifier. Uniformity is a coincidence rather than a 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.
For example,
// Ad-Hoc Polymorphism - Overloading
// overloading.cpp
#include
// Two function definitions:
void display() {
std::cout << "No arguments";
}
void display(int a) {
std::cout << "One argument (" << a << ')';
}
int main( ) {
display();
std::cout << std::endl;
display(10);
std::cout << std::endl;
}
No arguments
One argument (10)
Overloading eliminates multiple function definition errors. C++ implements overloading at compile time by renaming each identically named function as a function with its own distinct identifier: the language mangles the original identifier with the parameter types to generate an unique name. The linker uses the mangled name to bind the function call to the appropriate function definition.
Note that a procedural language like 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.
Unlike ad-hoc polymorphism, 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 addresses the multiplicity of definitions available for a function call. Inclusion polymorphism allows the multiplicity of member function definitions by selecting the definition from the set of definitions based on the object's type. The type is a type that belongs to an inheritance hierarchy. The term inclusion refers to the base type including the derived types within the hierarchy. All member function definitions share the same name throughout the hierarchy.
In the figure below, both a HybridCourse and a Course belong to the same hierarchy. 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 types within the hierarchy require only one member function definition. The assessments() query on a HybridCourse object invokes the same function definition as a query on the Course object. Defining a query for the HybridCourse class would only duplicate existing code and is unnecessary.
For example,
// Universal Polymorphism - Inclusion
// inclusion.cpp
#include
#include “Course.h”
using std::cout;
using std::endl;
int main( ) {
Course abc123(“Intro to OO”)
HybridCourse abc124(“Intro to OO*”);
cout << abc123.assessments() << endl;
cout << abc123.mode() << endl;
cout << abc124.assessments() << endl;
cout << abc124.mode() << endl;
}
Intro to OO 12 assessments
Intro to OO lecture-lab mode
Intro to OO* 12 assessments
Intro to OO* online-lab mode
Inclusion polymorphism eliminates duplicate logic across a hierarchy without generating missing function definition errors. C++ implements inclusion polymorphism at run-time using virtual tables.
Parametric
Parametric (or generic) polymorphism addresses differences between argument types in a function call and the parameter types in the function's definition. Parametric polymorphism allows function definitions that share identical logic independently of type. Unlike coercion, the logic is common to all possible types, without restriction. The types do not need to be related in any way. For example, a function that sorts ints uses the same logic as a function that sorts Students. If we have already written a function to sort ints, we only need to ensure that the Student class includes a comparison operator identical to that used by the sort function.
For example,
// Universal Polymorphism - Parametric
// parametric.cpp
#include
template
void sort(T* a, int n) {
int i, j;
T temp;
for (i = n – 1; i > 0; i–) {
for (j = 0; j < i; j++) {
if (a[j] > a[j+1]) {
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
class Student {
int no;
// other data – omitted here
public:
Student(int n = 0) : no(n) {}
bool operator>(const Student& rhs) const {
return no > rhs.no;
}
void display(std::ostream& os) const {
os << no << std::endl;
}
};
int main( ) {
int m[] = {189, 843, 321};
Student s[] = {Student(1256), Student(1267), Student(1234)};
sort(m, 3);
for (int i = 0; i < 3; i++)
std::cout << m[i] << std::endl;
sort(s, 3);
for (int i = 0; i < 3; i++)
s[i].display(std::cout);
}
189
321
843
1234
1256
1267
Parametric polymorphism eliminates duplicate logic across all types without generating a missing function definition error. C++ implements parametric polymorphism at compile-time using template syntax.
SUMMARY
· a polymorphic language allows type differences that a monomorphic type system would report as type errors.
· polymorphic features are classified into four distinct categories.
· ad-hoc polymorphism is only apparent - its polymorphic character disappears at closer scrutiny
· coercion modifies an argument's type to suit the parameter type in the function definition
· overloading associates the same function name with different and unrelated function definitions
· universal polymorphism is true polymorphism - its polymorphic character survives at closer scrutiny
· inclusion polymorphism selects a member function definition within an inheritance hierarchy based on an object's dynamic type
· parametric polymorphism generates identical logic to match any object's type
The chapter entitled Member Functions and Privacy covered the public member functions that format data passing through the standard library's iostream objects. The chapter entitled Input and Output Operators covered the design of custom input and output operators and introduced the standard library's fstream classes for processing file data.
This chapter describes in more detail the input and output objects along with their public member functions reviews the material covered in those preceding chapters. This chapter introduces manipulators as a simplifying alternative to member function calls on input and output objects.
Stream and Stream Objects
A stream is a sequence of items without limitation. More specifically, in C++ a stream is a sequence of bytes. An executing application accepts data in one stream and outputs data in another stream. The number of bytes in a stream can be indeterminate. Input objects store data from an input stream in the application's memory. Output objects copy data from the application's memory into an output stream. Both input and output objects operate in FIFO (First In First Out) order.
The standard input and output objects of the iostream library represent the standard peripheral devices, such as the keyboard and display.
An input object converts a sequence of bytes from its attached input stream into values of specified type, which are stored in system memory. An output object converts values of specified type, which have been stored in system memory, into a sequence of bytes in its associated output stream. Both types of objects use the data type associated with the region of memory to make the appropriate conversions from or to the sequence of bytes in each stream.
The data in a stream, unlike the data stored in a region of memory, is not associated with any particular type. The notion of type is programming language specific.
INPUT OBJECTS
An input object is an instance of the istream class. The istream class defines the structure of an input device. The object extracts data from the input stream and stores it in system memory, converting each sequence of bytes in the input stream into an equivalent value in system memory based on the specified variable's type.
Extraction
The expression for extracting bytes from an input stream takes the form
inputObject >> identifier
where inputObject is the name of the input object, >> is the extraction operator and identifier is the name of the destination variable.
The standard iostream library defines one input object for buffered input: cin.
For example,
int i;
char c;
double x;
char s[8];
cout << "Enter an integer,\n"
"a character,\n"
"a floating-point number and\n"
"a string : " << flush;
cin >> i;
cin >> c;
cin >> x;
cin >> s; // possible overflow
cout << "Entered " << i << ' '
<< c << ' ' << x << ' ' << s << endl;
Enter an integer,
a character,
a floating-point and
a string : 6 - 9.75 Harry
Entered 6 - 9.75 Harry
Each expression that takes an istream object as its left operand converts the next sequence of bytes in the attached input stream into a value stored in the type of the expression's 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).
// Leading Whitespace
// leading.cpp
#include
using namespace std;
int main() {
char str[11];
cout << "Enter a string : " << endl;
cin >> str;
cout << "|" << str << "|" << endl;
}
Note: _ denotes space
Enter a string :
__abc
|abc|
Whitespace
The input object treats whitespace in its input stream as a delimiter for numeric and string data types. In converting input bytes into a C-style null-terminated string, the input object adds the null byte after the last non-whitespace character stored in memory:
// Trailing Whitespace
// trailing.cpp
#include
using namespace std;
int main() {
char str[11];
cout << "Enter a string : " << endl;
cin >> str;
cout << "|" << str << "|" << endl;
}
Note: _ denotes space
Enter a string :
__abc__
|abc|
Cascading
We can compress a sequence of extraction operations into a single compound expression:
int i;
char c;
double x;
char s[8];
cout << "Enter an integer,\n"
"a character,\n"
"a floating-point number and\n"
"a string : " << flush;
cin >> i >> c >> x >> s;
cout << "Entered " << i << ' '
<< c << ' ' << x << ' ' << s << endl;
Enter an integer,
a character,
a floating-point and
a string : 6 - 9.75 Harry
Entered 6 - 9.75 Harry
We call such repeated use of the extraction operator cascading.
Note that reading a sequence of bytes in this manner is discouraged (see below).
Overflow
In the above two examples, overflow may occur while filling s. The extraction operator >> does not restrict the number of bytes accepted. If more than 7 bytes are in the input stream the data stored for the string may corrupt other data that has been stored in memory as shown on the right:
// Overflow
// overflow.cpp
#include
using namespace std;
int main() {
int i;
char c;
double x;
char s[8];
cout << "Enter an integer,\n"
"a character,\n"
"a floating-point number and\n"
"a string : \n";
cin >> i >> c >> x >> s;
cout << "Entered " << endl;
cout << i << ' '
<< c << ' ' << x << ' ' << s << endl;
}
Enter an integer,
a character,
a floating-point and
a string :
6 - 9.75 Constantinople
Entered
6 - 2.04952 Constantinople
The corruption varies from platform to platform.
Member Functions
The istream type supports the following member functions:
· ignore(...) - ignores/discards character(s) from the input buffer
· get(...) - extracts a character or a string from the input buffer
· getline(...) - extracts a line of characters from the input buffer
ignore
The ignore() member function extracts bytes from the input buffer and discards them without skipping whitespace. The iostream hierarchy defines two overloaded versions of ignore():
cin.ignore();
cin.ignore(2000, '\n');
The no-argument version discards a single byte. The two-argument version removes and discards up to the number of bytes specified by the first argument or up to the specified delimiting character, whichever occurs first and discards the delimiting character. The default delimiter is the end-of-file character (not the newline character).
get
The get() member function extracts either a single character or a string from the input buffer. Three versions are available:
// Input Extraction Using get()
// get.cpp
#include
using namespace std;
int main() {
char c, d, t[8], u[8], v;
c = cin.get(); // extracts a single character
cin.get(d); // extracts a single character
cin.get(t, 8); // newline delimiter – accepts up to 7 chars
// and adds a null byte
cin.ignore(2000, ‘\n’); // extracts the ‘j’ and the newline
cin.get(u, 8, ‘\t’); // tab delimiter – accepts up to 7 chars and
// adds a null byte
cin.ignore(); // extracts the tab
cin.get(v); // extracts a single character
cout << "c = " << c << endl;
cout << "d = " << d << endl;
cout << "t = " << t << endl;
cout << "u = " << u << endl;
cout << "v = " << v << endl;
}
The above program produces the following results for the input shown (the character _ refers to the horizontal tab character):
Input stream : abcdefghij
klmn_opqr
Output:
-------
c = a
d = b
t = cdefghi
u = klmn
v = o
get() does not skip leading whitespace. get(,) leaves the delimiting character in the input buffer. In using get(,) we need to remove the delimiting character, if there is one. Both string versions - get(char*, int) and get(char*, int, char) - append a null byte to the sequence of characters stored in memory.
getline
getline() behaves like get(), but extracts the delimiting character from the input buffer:
// Input Extraction using getline()
// getline.cpp
#include
using namespace std;
int main() {
char t[8], u[8], v;
cin.getline(t, 8); // newline delimiter – accepts up to 7 chars
// and adds a null byte
cin.getline(u, 8, ‘\t’); // tab delimiter – accepts up to 7 chars and
// adds a null byte
cin.get(v); // extracts a single character
cout << "t = " << t << endl;
cout << "u = " << u << endl;
cout << "v = " << v << endl;
}
The above program produces the following results for the input shown (the character _ refers to the horizontal tab character):
Note: _ denotes '\t' character here
Input stream : cdefghi
jklmn_opqr
Output:
-------
t = cdefghi
u = jklmn
v = o
getline(), like get(), does not skip leading whitespace and appends a null byte to the sequence of characters stored in system memory.
OUTPUT OBJECTS
An output object is an instance of the ostream class. The ostream class 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 standard iostream library defines three distinct 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
output << identifier
where output is the name of the ostream object, << is the insertion operator and identifier is the name of the variable or object that holds the data.
For example,
int i = 6;
char c = ' ';
double x = 9.75;
char s[] = "Harry";
cout << i;
cout << c;
cout << x;
cout << c;
cout << s;
cout << endl;
cerr << "Data has been written";
6 9.75 Harry
Data has been written
Each expression that takes an ostream object as its left operand converts the data in its right operand into a sequence of characters based on the type of the expression's right operand.
endl inserts a newline character into the output stream and flushes the stream's buffer.
Cascading
We may combine a sequence of insertion operations into a compound insertion expression:
int i = 6;
char c = ' ';
double x = 9.75;
char s[] = "Harry";
cout << i << c << x << c << s << endl;
cerr << "Data has been written";
6 9.75 Harry
Data has been written
Member Functions
The ostream class supports the following public member functions for formatting control:
· width(int) - sets the field width to the integer received
· fill(char) - sets the padding character to the character received
· setf(...) - sets a formatting flag to the flag received
· unsetf(...) - unsets a formatting flag for the flag received
· precision(int) - sets the decimal precision to the integer received
width
The width(int) member function specifies the minimum width of the next output field:
// Field Width
// width.cpp
#include
using namespace std;
int main() {
int attendance = 27;
cout << "1234567890" << endl;
cout.width(10);
cout << attendance << endl;
cout << attendance << endl;
}
1234567890
27
27
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:
// Padding
// fill.cpp
#include
using namespace std;
int main() {
int attendance = 27;
cout << "1234567890" << endl;
cout.fill('*');
cout.width(10);
cout << attendance << endl;
}
1234567890
********27
The padding character remains unchanged, until we reset it.
setf, unsetf - Format control
The setf() and unsetf() member functions control formatting and alignment. Their control flags include:
Control Flag
Result
ios::fixed
ddd.ddd
ios::scientific
d.ddddddEdd
ios::left
align left
ios::right
align right
The scope resolution (ios::) on these flags identifies them as part of the ios class.
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():
// Fixed Format
// fixed.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.width(10);
cout.setf(ios::fixed);
cout << pi << endl;
}
1234567890
3.141593
Format settings persist until we change them. To unset fixed format, we pass the ios::fixed flag to the unsetf() member function:
// Unset Fixed Format
// unsetf.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.width(10);
cout.setf(ios::fixed);
cout << pi << endl;
cout.unsetf(ios::fixed);
cout << pi << endl;
}
1234567890
3.141593
3.14159
To specify scientific format, we pass the ios::scientific flag to the setf() member function:
// Scientific Format
// scientific.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "12345678901234" << endl;
cout.width(14);
cout.setf(ios::scientific);
cout << pi << endl;
}
12345678901234
3.141593e+00
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 switch to left-justification, we pass the ios::left flag to the setf() member function:
// Left Justified
// left.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.width(10);
cout.fill('?');
cout.setf(ios::left);
cout << pi << endl;
}
1234567890
3.14159???
To switch off left-justification, we pass the ios::left flag to the unsetf() member function:
cout.unsetf(ios::left);
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.
For a precision of 2 under general format, we write
// Precision
// precison.cpp
#include
using namespace std;
int main() {
double pi = 3.141592653;
cout << "1234567890" << endl;
cout.setf(ios::fixed);
cout.width(10);
cout.precision(2);
cout << pi << endl;
}
1234567890
3.14
The precision setting applies to the output of all subsequent floating-point values until we change it.
MANIPULATORS (OPTIONAL)
The C++ language defines manipulators that are elegant alternatives to member function calls. These manipulators are operands for the extraction and insertion operators. Manipulators that don't take any argument do not include parentheses and are defined in
Input Manipulators
The manipulators of input objects are listed below:
Manipulator
Effect
skipws
skip whitespace
noskipws
turn off skip whitespace
setw(int)
set the field width for next input (strings only)
The argument to setw() should be one more than the maximum number of input bytes 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 to form compound expressions. For example,
// Input Manipulators
// manipulator.cpp
#include
#include
using namespace std;
int main( ) {
char a[5], b[2], c, d[7];
cout << "Enter : ";
cin >> setw(5) >> a >>
setw(2) >> b >> noskipws >>
c >> skipws >> d;
cout << "Stored '" << a <<
"' & '" << b <<
"' & '" << c <<
"' & '" << d << "'" << endl;
}
Enter : abcde fgh
Stored 'abcd' & 'e' & ' ' & 'fgh'
Output Manipulators
The manipulators of output objects are listed below:
Manipulator
Effect
fixed
output floating-point numbers in fixed-point format
scientific
output floating-point numbers in scientific format
left
left justify
right
right justify
endl
output end of line and flush the buffer
setprecision(int)
set the precision of the output
setfill(int)
set the fill character for the field width
setw(int)
set the field width for the next output operand only
setbase(int)
set the base of the number system for int output
flush
flush the output buffer
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,
cout << fixed << left << setw(5) <<
setprecision(1) << 12.376 <<
setprecision(5) << 12.376 <<
endl;
12.4 12.37600
Reference Example (Optional)
The following program produces the output listed on the right
#include
#include
using namespace std;
int main( ) {
/* integers */
cout << "\n* ints *\n"
<< "1234567890\n"
<< "----------\n"
<< 4321 << '\n'
<< setw(7) << 4321 << '\n'
<< setw(7) << setfill('0') << 4321 << setfill(' ')<<'\n'
<< setw(7) << left << 4321 << right << '\n';
/* floats */
cout << "\n* floats *\n"
<< "1234567890\n"
<< "----------\n"
<< 4321.9876546F << '\n';
/* doubles */
cout << "\n* doubles *\n"
<< "1234567890\n"
<< "----------\n"
<< fixed << 4.9876546 << '\n'
<< setw(7) << setprecision(3) << 4.9876546 << '\n'
<< setw(7) << setfill('0') << 4.9876546 << '\n'
<< setw(7) << left << 4.9876546 << right << '\n';
/* characters */
cout << "\n* chars *\n"
<< "1234567890\n"
<< "----------\n"
<< 'd' << '\n'
<< int('d') << '\n';
}
* ints *
1234567890
----------
4321
4321
0004321
4321
* floats *
1234567890
----------
4321.99
* doubles *
1234567890
----------
4.987655
4.988
004.988
4.98800
* chars *
1234567890
----------
d
100
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).
STATE
The ios base class defines public member functions that report or change the state of istream and ostream objects. These member functions include:
· good() - the next operation might succeed
· fail() - the next operation will fail
· eof() - end of data has been encountered
· bad() - the data may be corrupted
· clear() - reset the state to good
For user-friendly input processing, we should check the state of the input object every time it extracts a sequence of bytes from the input buffer. If the object has encountered an invalid character, the object will fail and leave that invalid character in the input buffer and the fail() member function will return true.
Before a failed object can continue extracting data from the input buffer, we must clear the object of its failed state. The clear() function resets the state of the object to good:
if(cin.fail()) { // checks if cin is in a failed state
cin.clear(); // clears state to allow further extraction
cin.ignore(2000, '\n'); // clears the input buffer
}
The following section provides a complete example.
ROBUST VALIDATION
Robust validation enhances the friendliness of any application that processes input. The state functions of the iostream classes help us validate input robustly. Robust validation checks the input object's state after each extraction to ensure that the object has converted the sequence of bytes into a value and that that converted value is valid and within admissible bounds. Robust validation rejects invalid input and out-of-bound values, resetting any failed state and requesting fresh input as necessary from the user.
getPosInt
To extract a positive int that is not greater than max from the standard input device, we write
// getPosInt extracts a positive integer <= max
// from standard input and returns its value
//
int getPosInt(int max) {
int value;
int keepreading;
keepreading = 1;
do {
cout << "Enter a positive integer (<= " << max << ") : ";
cin >> value;
if (cin.fail()) { // check for invalid character
cerr << "Invalid character. Try Again." << endl;
cin.clear();
cin.ignore(2000, '\n');
} else if (value <= 0 || value > max) {
cerr << value << " is outside the range [1," <<
max << ']' << endl;
cerr << "Invalid input. Try Again." << endl;
cin.ignore(2000, '\n');
// you may choose to omit this branch
} else if (char(cin.get()) != '\n') {
cerr << "Trailing characters. Try Again." << endl;
cin.ignore(2000, '\n');
} else
keepreading = 0;
} while(keepreading == 1);
return value;
}
FILE STREAM CLASSES (OPTIONAL)
The ios inheritance hierarchy includes three derived classes specifically designed for processing file streams. These classes manage the communications between file streams containing 8-bit bytes and system memory.
The fstream system header file defines its classes in the std namespace:
#include
The fstream classes include:
· 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 access a file stream through separate input and output buffers.
Extraction and Insertion Operator Overloads
The standard library overloads the extraction and insertion operators for the fundamental types. We overload these operators for fstream objects as left operands and custom types as right operands.
Fundamental Types
For fundamental types see the chapter entitled Input and Output Operators.
Custom Types
Typicall’, custom types require separate overloads of both extraction and insertion operators.
While reading standard input involves prompting the suer, reading a file does not require any prompts. The extraction operator for file input objects excludes prompts. Since writing to a file matches the convention for subsequent reading from that file, the output to a file generally differs from the more decorated output to be read by the user. Moreover, since the insertion operator that takes an ostream object as its left operand class is a templated function and the ofstream class is a class derived from the ostream class, ambiguities arise with direct overloading of the operator for a custom type. One way to avoid these ambiguities is to define a separate file class for the custom type and overload the insertion operator for that file class.
The file related additions are highlighted in the listings below.
The header file for the Student class includes the definition of a StudentFile class that overloads the insertion operators for an ofstream object that receives the Student class:
// Student.h
#include
#include
const int NG = 13;
class StudentFile;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void read(std::istream&);
void read(std::ifstream&);
void display(std::ostream& os) const;
void display(StudentFile& os) const;
};
std::istream& operator>>(std::istream& is, Student& s);
std::ostream& operator<<(std::ostream& os, const Student& s);
std::ifstream& operator>>(std::ifstream& is, Student& s);
class StudentFile {
public:
std::ofstream f;
StudentFile(const char*);
StudentFile& operator<<(char);
StudentFile& operator<<(int);
StudentFile& operator<<(float);
void close();
};
StudentFile& operator<<(StudentFile& os, const Student& s);
The implementation file overloads the file extraction and insertion operators for our Student class and defines the insertion operator for StudentFile objects as left operands and fundamental types as right operands:
// Student.cpp
#include "Student.h"
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
*this = Student(n, nullptr, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::read(std::istream& is) {
int no; // will hold the student number
int ng; // will hold the number of grades
float grade[NG]; // will hold the grades
std::cout << "Student Number : ";
is >> no;
std::cout << "Number of Grades : ";
is >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
std::cout << "Grade " << i + 1 << " : ";
is >> grade[i];
}
// construct a temporary Student
Student temp(no, grade, ng);
// if data is valid, copy temporary object into current object
if (temp.no != 0)
*this = temp;
}
void Student::read(std::ifstream& is) {
int no; // will hold the student number
int ng; // will hold the number of grades
float grade[NG]; // will hold the grades
is >> no;
is >> ng;
if (ng > NG) ng = NG;
for (int i = 0; i < ng; i++) {
is >> grade[i];
}
// construct a temporary Student
Student temp(no, grade, ng);
// if data is valid, copy temporary object into current object
if (temp.no != 0)
*this = temp;
}
void Student::display(std::ostream& os) const {
if (no > 0) {
os << no << ":\n";
os.setf(std::ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << std::endl;
}
os.unsetf(std::ios::fixed);
os.precision(6);
} else {
os << "no data available" << std::endl;
}
}
void Student::display(StudentFile& os) const {
os << no << '\n';
os << ng << '\n';
for (int i = 0; i < ng; i++)
os << grade[i] << '\n';
}
std::ostream& operator<<(std::ostream& os, const Student& s) {
s.display(os);
return os;
}
std::istream& operator>>(std::istream& is, Student& s) {
s.read(is);
return is;
}
std::ifstream& operator>>(std::ifstream& is, Student& s) {
s.read(is);
return is;
}
StudentFile& operator<<(StudentFile& f, const Student& s) {
s.display(f);
return f;
}
StudentFile::StudentFile(const char* filename) : f(filename) {}
StudentFile& StudentFile::operator<<(char c) {
f << c;
return *this;
}
StudentFile& StudentFile::operator<<(int i) {
f << i;
return *this;
}
StudentFile& StudentFile::operator<<(float v) {
f << v;
return *this;
}
void StudentFile::close() {
f.close();
}
Note the definitions of the read() and display() member functions overloaded for file input and output respectively.
The client file that uses this upgraded Student class creates the file objects, writes to them and reads from them:
// Custom File Operators
// customFile.cpp
#include
#include “Student.h”
int main ( ) {
Student harry;
std::cin >> harry;
std::cout << harry;
StudentFile studentFile("Student.txt");
studentFile << harry;
studentFile.close();
std::ifstream inFile("Student.txt");
inFile >> harry;
std::cout << harry;
}
Student Number : 1234
Number of Grades : 3
Grade 1 : 56.7
Grade 2 : 78.9
Grade 3 : 85.4
1234:
56.70
78.90
85.40
1234:
56.70
78.90
85.40
The records written to the Student.txt file by this program are:
1234
3
56.7
78.9
85.4
Nice to Know (Optional)
Open-Mode Flags
To customize a file object's connection mode we use 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 default combinations for no-argument and one-argument constructors are:
· ifstream - std::ios::in - open for reading
· ofstream - std::ios::out - open for writing
· fstream - std::ios::in|std::ios::out - open for reading and writing
The Logical Negation Operator
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()) {
std::cerr << "Read error";
fin.clear();
}
if (!fin) {
std::cerr << "Read error";
fin.clear();
}
The operator applied directly to a file object returns the state of the connection:
#include
#include
int main() {
std::ofstream fout(“output.txt”); // connects fout to output.txt
// for writing
if (!fout) {
std::cerr << "File is not open" << std::endl;
} else {
std::cout << "File is open" << std::endl;
}
}
Rewinding a Connection
istream, fstream
To rewind an input stream we call:
· istream& seekg(0) - sets the current position in the input stream to 0
ostream, fstream
To rewind an output stream we call:
· ostream& seekp(0) - sets the current position in the output stream to 0
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
int main() {
std::ifstream in(“src1.txt”); // open 1st source file
std::ofstream out(“output.txt”); // open destination file
if (in) {
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
if (in) {
while (!in.eof())
out << in.get(); // byte by byte copy
in.clear();
}
}
Writing to and Reading from the Same File
The fstream class supports both reading and writing operations. An instance of this class can write to a file and read from that same file.
For example, the following program produces the output shown on the right
// File Objects - writing and reading
// fstream.cpp
#include
#include
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
}
f.clear(); // clear failed (eof) state
}
**** 1
Line 2
Line 3
SUMMARY
· the extraction and insertion operators support cascading
· get() and getline() read strings with whitespace
· a field width setting only holds for the next field
· all non-field width settings persist until changed
· precision has different meanings under general, scientific, and fixed formats
· manipulators are the elegant alternative to member function based format settings
· manipulators that take arguments are defined in #include
· a failed state must be cleared before processing can continue
· the extraction and insertion operators are overloaded for file objects as left operands and fundamental types as right operands
· an input file object is an instance of an ifstream class
· an output file object is an instance of an ofstream class
· we may overload the extraction and insertion operators for file objects as left operands and our class types as right operands
Inheritance hierarchies that access resources at multiple levels require intervention. Managing relationships between the special member functions in a hierarchy with multiple resources involves ensuring that the appropriate calls between these functions are made. The definitions of some copy constructors and copy assignment operators in the hierarchy may require explicit coding of the connections to their base class counterparts.
This chapter describes how to define the constructors and the copy assignment operators in a hierarchy that access multiple resources and how to call their base class counterparts.
CONSTRUCTORS AND DESTRUCTOR
Each constructor of a derived class calls a constructor of its base class. By default, that constructor is the no-argument constructor. To override this default, we insert an explicit call to the base class constructor.
Destructors in an inheritance hierarchy do not require any intervention, since each class in the hierarchy has but one destructor and each destructor calls its sole base class counterpart automatically.
Example
Let us upgrade the definition of our Student class to accommodate a client-defined number of grades. We store the grades in dynamic memory and store the address of that memory in a resource instance pointer.
The upgraded definition of our Student class contains a resource instance pointer:
// Student.h
#include
const int NC = 30;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float* grade;
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
~Student();
void display(std::ostream&) const;
};
Our four-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
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
grade = nullptr;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_;
if (ng > 0) {
grade = new float[ng_];
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
grade = nullptr;
}
} else {
grade = nullptr;
*this = Student();
}
}
Student::~Student() {
delete [] grade;
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
The following client uses this implementation to produce the output shown on the right:
// Derived Class with a Resource Constructors
// dclassResourceCtor.cpp
#include
#include “Student.h”
int main() {
Person jane(“Jane”);
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(“Harry”, 1234, gh, 3);
harry.display(std::cout);
jane.display(std::cout);
}
Harry 1234:
89.40
67.80
45.50
Jane
COPY CONSTRUCTOR
The copy constructor of a derived class 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
2. 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
const int NC = 30;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float* grade;
int ng;
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
Student(const Student&);
~Student();
void display(std::ostream&) const;
};
We implement the copying steps as follows:
1. shallow copy the Person part of the source object
· allocate static memory for name in the base class part of the newly created object
· copy into name the string at address src.name
2. copy the Student part of the source object
· allocate static memory for no, *grade and ng in the derived part of the newly created object
· shallow copy src.no into no
· shallow copy src.ng into ng
· allocate dynamic memory for a copy of src.grade
· deep copy the elements at src.grade into grade
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
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(ostream& os) const {
os << name << ' ';
}
Student::Student() {
no = 0;
ng = 0;
grade = nullptr;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_;
if (ng > 0) {
grade = new float[ng_];
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
grade = nullptr;
}
} else {
grade = nullptr;
*this = Student();
}
}
Student::Student(const Student& src) : Person(src) {
no = src.no;
ng = src.ng;
if (src.grade != nullptr && ng > 0) {
grade = new float[ng];
for (int i = 0; i < ng; i++)
grade[i] = src.grade[i];
}
else
grade = nullptr;
}
Student::~Student() {
delete [] grade;
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
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 Copy Constructor
// dclassResourceCopyCtor.cpp
#include
#include “Student.h”
int main() {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(“Harry”, 1234, gh, 3);
Student harrz = harry; // calls copy constructor
harry.display(std::cout);
harrz.display(std::cout);
}
Harry 1234:
89.40
67.80
45.50
Harry 1234:
89.40
67.80
45.50
COPY ASSIGNMENT OPERATOR
The default copy assignment operator of a derived class calls the copy assignment operator of its base class. However, any custom copy assignment operator of a derived class DOES NOT by default call the copy assignment operator of its base class. Accordingly, a custom copy assignment operator of a derived class with a resource requires an explicit call to the base class copy assignment operator.
We call the base class copy assignment operator from within the body of the definition of the derived class assignment operator. The call takes one of the following forms:
· the functional form
· the cast assignment form
The functional expression takes the form
Base::operator=(identifier);
The assignment expression takes the form
(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 from a call to the derived class operator by the type of the left operand.
Example
The derived class definition declares the assignment operator and a private member function for the copying operations:
// Student.h
#include
const int NC = 30;
class Person {
char name[NC+1];
public:
Person();
Person(const char*);
void display(std::ostream&) const;
};
class Student : public Person {
int no;
float* grade;
int ng;
void init(int, int, const float*);
public:
Student();
Student(int);
Student(const char*, int, const float*, int);
Student(const Student&);
Student& operator=(const Student& src);
~Student();
void display(std::ostream&) const;
};
The private init() contains the copying logic shared by the constructors and the assignment operator:
// Student.cpp
#include
#include “Student.h”
using namespace std;
Person::Person() {
name[0] = ‘\0’;
}
Person::Person(const char* nm) {
strncpy(name, nm, NC);
name[NC] = ‘\0’;
}
void Person::display(std::ostream& os) const {
os << name << ' ';
}
void Student::init(int no_, int ng_, const float* g) {
no = no_;
ng = ng_;
if (g != nullptr && ng > 0) {
grade = new float[ng_];
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
grade = nullptr;
}
}
Student::Student() {
no = 0;
ng = 0;
grade = nullptr;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student("", n, g, 0);
}
Student::Student(const char* nm, int sn, const float* g, int ng_) : Person(nm) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
init(sn, ng_, g);
} else {
grade = nullptr;
*this = Student();
}
}
Student::Student(const Student& src) : Person(src) {
init(src.no, src.ng, src.grade);
}
Student& Student::operator=(const Student& src) {
if (this != &src) {
// Base class assignment
// 1 - functional expression
// Person::operator=(src);
// 2 - assignment expression
(Person&)*this = src; // call base class assignment operator
delete [] grade;
init(src.no, src.ng, src.grade);
}
return *this;
}
Student::~Student() {
delete [] grade;
}
void Student::display(ostream& os) const {
if (no > 0) {
Person::display(os);
os << no << ":\n";
os.setf(ios::fixed);
os.precision(2);
for (int i = 0; i < ng; i++) {
os.width(6);
os << grade[i] << endl;
}
os.unsetf(ios::fixed);
os.precision(6);
} else {
os << "no data available" << endl;
}
}
Sharing a private member function is one way of coding the copy constructor and assignment operator for the derived class.
The following client uses this implementation to produce the output shown on the right:
// Derived Class with a Resource Copy Assignment
// dclassResourceCopyAssmnt.cpp
#include
#include “Student.h”
int main() {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(“Harry”, 1234, gh, 3), harrz;
harrz = harry; // calls copy assignment
harry.display(std::cout);
harrz.display(std::cout);
}
Harry 1234:
89.40
67.80
45.50
Harry 1234:
89.40
67.80
45.50
Direct Call Copy Constructor
The alternative to sharing a private member function is a direct call from the copy constructor to the copy assignment operator (as in the chapter entitled Classes and Resources). In a direct call, the assignment operator copies the base class part of the object and any call to the base class copy constructor is redundant.
Student::Student(const Student& src) { // calls no-argument base constructor
grade = nullptr;
*this = src;
}
Student& Student::operator=(const Student& src) {
if (this != &src) {
// Base class assignment
// 1 – functional expression
// Person::operator=(src);
// 2 – assignment expression
Person& person = *this; // only copies address
person = src; // call base class operator
delete [] grade;
no = src.no;
ng = src.ng;
if (src.ng > 0) {
grade = new float[ng];
for (int i = 0; i < ng; i++)
grade[i] = src.grade[i];
}
else
grade = nullptr;
}
return *this;
}
SUMMARY
· a derived class with a resource requires explicit definitions of its special member functions - constructors, copy assignment operator and destructor
· an explicitly defined derived class copy constructor without a call to the base class' copy constructor calls the base class' no-argument constructor
· the derived class' copy constructor executes the logic in the base class' copy constructor first
· an explicitly defined derived class copy assignment operator does NOT automatically call the base class assignment operator.
· the derived class assignment operator executes the base class assignment operator entirely within the scope of the copy derived class assignment operator
· the destructor of a derived class automatically calls the destructor of the base class
Object-oriented languages have evolved since the concept of objects was formally incorporated into the Simula language by Ole-Johan Dahl and Kristen Nygaard in the 1960s. International standards have documented their evolution. The most recent standard for the C++ language is formally known as ISO/IEC 14882:2014 and extends over 1300 pages. ISO stands for the International Organization for Standardization. IEC stands for the International Electrotechnical Commission.
This chapter reviews the milestones in the evolution of C++, highlights some of the features introduced during its evolution with respect to the original version of the language, and briefly discusses a few of the topics that have evolved with the C++ standards.
MILESTONES
C++ was originally designed as a synthesis of C and object-orientation Simula-style. C had and still has no object-oriented capabilities. Simula introduced the terms class, object, inheritance, virtual methods and subclasses (derived classes) formally to the programming community.
Bjarne Stroustrup created C++ at Bell Labs (AT&T Research Labs) by augmenting C with the object-oriented features of Simula. He released C++ officially in October 1985. His web site includes a quite useful and up-to-date glossary of technical terms.
The ISO/IEC Standards
At the time of printing, three standard definitions have been approved by the international programming community.
· C++98
· C++11
· C++14
C++98
The first official standard that defined the C++ language is formally known as ISO/IEC 14882:1998 and less formally as C++98. The international programming community ratified this definition in 1998 and published it in a document that contains about 800 pages. The definition is based in part on the ISO/IEC 9899:1990 standard for the C language (informally known as C89).
C++98 augmented pre-standard C++ with
"additional data types, classes, templates, exceptions, namespaces, inline functions, operator overloading, function name overloading, references, freestore management operators, and additional library facilities."
The library facilities included a newly re-written iostream library and the string class.
C++11
The second official standard that re-defined the language is formally known as ISO/IEC 14882:2011 and less formally as C++11. The international programming community ratified this definition on August 12 2011. The definition is based on C++98 and C99 and includes several major additions to the core language as well as several major extensions of the standard library.
The objectives of the C++11 committee had included:
· making C++ easier to teach and to learn through increased uniformity
· making C++ better for systems programming and library construction
· improving the type safety of the language
The features that C++11 added to C++98 included (amongst others):
· the nullptr keyword replacing the NULL macro
· the auto keyword inferring the type of a left operand implicitly from the type of the right operand in an assignment expression
· inherited constructors
· features covered in the next volume of this series of notes
· move constructors and assignment operators
· lambda expressions (anonymous functions)
· library support for multi-threading classes
· range based for loops
· strongly typed enumerations
· uniformity amongst initializers
· initializers for class members
C++14
The third official standard that re-defined the language is formally known as ISO/IEC 14882:2014 and less formally as C++14. The international programming community ratified this definition on August 18 2014. The definition expanded the application of the auto keyword to return types, the application of templates to variables. The changes made the language safer and more convenient.
These notes align with this standard.
C++17
The ISO/IEC 14882 standards committee is working on the next iteration scheduled for ratification in July 2017. The committee intended this revision to be a major amendment to the C++14 standard, but several new features did not make the cut.
Compiler Support Status
A language standard is a specification for compiler writers. Different writers introduce different features adopted in a standard at different times. The support status for the features approved in C++11 and C++14 is tabulated at http://en.cppreference.com/w/cpp/compiler_support. Links to the individual compiler web sites are included there.
SOME FEATURES THAT HAVE CHANGED
Facilities that C++98 and C++11 introduced included inline functions, member function deletion, constrained casting and changes to freestore management.
Inline Functions
Inlining is a technique for improving a function's execution time by replacing the function call with the function logic proper and thereby removing the overhead associated with parameter passing. The primary cost of inlining is an increase in the size of the executable code.
An inline request directs the compiler to insert the body of the function at every call to the function, if possible. The compiler, instead of storing the function's definition once in its own dedicated region of memory and transferring control to that region for each call, inserts a copy of the body at each and every call. Inlining is particularly useful with member functions that contain small blocks of code. Member functions that do not contain iterations are candidates for inlining.
The compiler determines whether or not to implement an inline request. If the function contains too many statements or an iteration, the compiler ignores the request and calls the function in the usual way.
Example
To inline a member function, we embed its definition within the class definition as shown on the left or alternatively add the keyword inline to the definition as shown on the right.
For example,
// Inline Functions - Embedded
// inline_1.h
const int NG = 20;
struct Student {
private:
int no;
float grade[NG];
int ng;
public:
void set(int n, const char* g);
const float* getGrades() const {
return grade;
}
};
// Inline Functions - Separate
// inline_2.h
const int NG = 20;
struct Student {
private:
int no;
float grade[NG];
int ng;
public:
void set(int n, const char* g);
const float* getGrades() const;
};
inline const float* Student::getGrades()
const {
return grade;
}
Note that we place the implementation of an inline function in the header file that includes the class definition.
Function Deletion
C++11 introduced use of the keyword delete to inform the compiler that we do not want any default definition of a member function created. An example of this usage is the Copies Prohibited Section of the chapter entitled Classes and Resoures.
The Legacy Way
Prior to C++11, one way to prohibit a client from copying or copy assigning an instance of a class was to declare both the copy constructor and copy assignment operator as private members:
class Student {
int no;
float* grade;
int ng;
Student(const Student& source);
Student& operator=(const Student& source);
public:
Student();
Student(int, const float*);
~Student();
void display() const;
};
Since these special member functions were defined as private members, no definition was practically necessary.
C-Style Casts
C++ inherited its original casting facilities from C directly. The constrained casting syntax described in the chapter entitled Templates is more discriminating than the inherited syntax. The standards support the inherited syntax for legacy reasons. The availability of these older features allows programmers to bypass the type system and directly weaken a compiler's ability to identify type errors.
For example, consider code that converts an int to a pointer to an int. Such code is most probably a typing mistake, C and hence C++ allow this code to slip through the type checking system:
int x = 2;
int* p;
p = (int*)(x); // MOST PROBABLY A TYPING ERROR (& missing)!
Nevertheless, in applications built from many thousands of lines of code, we expect the compiler's type-checking system to flag such code. Errors that result from such casts are very difficult to find if they are embedded within many 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 simply preface the identifier with the name of the target type enclosed in parentheses:
// C-Style Casting
// c_cast.cpp
#include
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 to be cast to the target type:
// Function Style Casting
// functionStyleCast.cpp
#include
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 C-style casts (for example, (int)x) apply without regard to the nature of the conversion. Such syntax does not convey the programmer's intent.
A C-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 distinguish the different categories and thereby improve the degree of type checking available from the compiler.
For example, it is always safer type-wise to code a static_cast rather than a C-style cast.
Freestore Management
C++98 introduced exception handling for dynamic memory allocation. By default, the new operator throws an exception if the operator encounters an error. The topic of exception handling is covered in the next volume of this series of notes.
The Legacy Way
Prior to C++98, the new operator returned the null address if it encountered an error (for example, insufficient memory).
The following legacy code checks for such an error:
// Prior to C++98
#include
int main() {
char* p;
int i = 0;
do {
p = new char[100001];
i++;
} while (p != NULL);
cout << "Out of space after " << i << " attempts!\n";
}
One Alternative
Since C++98, we can instruct the new operator to return the null address by passing the nothrow argument to the operator. nothrow is defined in the new header file:
// After C++98 - Null Address Alternative
#include
#include
int main() {
char* p;
int i = 0;
do {
p = new (std::nothrow) char[100001];
i++;
} while (p != NULL);
std::cout << "Out of space after " << i << " attempts!\n";
}
With C++11, we can improve type safety by replacing the macro NULL with the nullptr keyword:
// After C++11 - Null Address Alternative
#include
#include
int main( ) {
char* p;
int i = 0;
do {
p = new (std::nothrow) char[100001];
i++;
} while (p != nullptr);
std::cout << "Out of space after " << i << " attempts!\n";
}
A Technical Note on Inclusion Polymorphism
Dynamic Dispatch or Late Binding
The terms dynamic dispatch and late binding arise in descriptions of inclusion polymorphism. These terms have similar definitions and are sometimes used interchangeably. Technically, dynamic dispatch is the more precise term in regard to C++.
Dynamic dispatch is the process of selecting which implementation of a member function in a class hierarchy to call on a polymorphic object. The name of the operation may be bound to a polymorphic operation at compile time and the implementation identified at run time. The object's dynamic type determines which implementation to call.
Late binding associates a method's name with an object based on its dynamic type. The name of the operation is bound to a polymorphic operation at run time when the implementation is identified. Late binding implies dynamic dispatch.
C++ uses early binding and static or dynamic dispatch. Static dispatch is the default. The virtual keyword implements dynamic dispatch.
Virtual Table
Most C++ compilers implement dynamic dispatch by adding an instance pointer to the object's data members. This pointer redirects to a table that associates the member function implementations with object types. The run-time code uses this table to select the implementation corresponding to the object's dynamic type. This table is called the virtual table for the class.
The compiler creates the virtual table at compile time. Introducing a virtual table (by inserting the keyword virtual) results in the equivalent of a single indirection every time a client calls a virtual member function on an instance of its class; that is, it does not introduce a significant overhead. Since the compiler constructs the table at compile time, it is unmodifiable at run-time and we cannot add a new member function to the class at that time.
The prototypes for the some common functions in the C++ standard library are listed below. These prototypes and the functions definitions are declared within the std namespace. To access one of these functions, we #include its parent header file and either expose the entire namespace (using namespace std;) or add the std:: prefix to the function identifier.
C LIBRARY
· int scanf(const char* format, …) – read data from standard input
· int printf(const char* format, …) – send data to standard output
· int fscanf(FILE* stream, const char* format, …) – read data from a file stream
· int fprintf(FILE* stream, const char* format, …) – send data to a file stream
· size_t – non-negative integer type
· size_t strlen(const char* str) – the number of characters in a C-style null-terminated string
· char* strcpy(char* destination, const char* source) – copy C-style null-terminated string from source address to destination address
· char* strcat(char* destination, const char* source) – concatenate C-style null-terminated string from source address to the string at the destination address
· int strcmp(const char* str1, const char* str2) – compare C-style null-terminated strings at two addresses
· int abs(int i) – absolute value of i
· int atoi(const char* str) – convert C-style null-terminated string (str) to an integer
· int rand() – generate a random number
· double abs(double x) – absolute value of x
· float abs(float x) – absolute value of x
· double pow(double base, double exponent) – raises base to the power of exponent
· double sqrt(double x) – square root of x
· int toupper(int c) – converts c to upper case
· int tolower(int c) – converts c to lower case
· int isupper(int c) – is c upper case?
· int islower(int c) – is c lower case?
· int isalpha(int c) – is c alphabetic?
· int isdigit(int c) – is c a numeric digit?
INPUT OUTPUT LIBRARY
· streamsize – integer type
· fmtflags – format flags type
· char fill(char c) – set fill character to c
· fmtflags setf(fmtflags f) – set format to f
· fmtflags unsetf(fmtflags f) – unset format for f
· streamsize width(streamsize w) – set field width to w
· streamsize precision(streamsize p) – set floating-point precision to p
· bool good() const – goo bit is set
· bool eof() const – end of file bit is set
· bool fail() const – fail file bit is set
· bool bad() const – bad bit is set
· bool eof() const – end of file bit is set
· void clear() – set error state to good
· setfill(char c) – set fill character to c
· setw(int w) – set field width to w
· setprecision(int p) – set floating-point precision to p
creates an object by initializing
· void open(const char* f, …) – open file named f and associate it with the current object
· void close() – close file associated with current object