Chapter 2: Object-Oriented Design
2.1 Goals and Principles
2.1.1 Object-Oriented Design Goals
- Robustness: capable
of handling unexpected inputs that are not explicitly defined
for its application
- Adaptability: software
needs
to evolve over time in response to changing conditions in its
environment. Related to this concept is portability,
which is the
ability of software to run with minimal change on different
hardware and operating system platforms.
- Reusability: code
should reusable as a component of different systems in various
applications
2.1.2 Object-Oriented Design Principles
- Abstraction: to
distill the complicated system down to its most fundamental
parts and describe these parts in a simple, precise language.
Applying the abstraction paradigm to the design of data
structures gives rise to abstract data types (ADTs).
- Encapsulation:
different components of a software system should not reveal the
internal details of their respective implementation
- Modularity: refers
to an organizing structure in which different components of a
software system are divided into separate functional units
2.2.1 Inheritance in C++
Inheritance allows the design of generic classes that can be
specialized to more particular classes, with the specialized classes
reusing the code from the generic class.
class Person { // base class
private:
string name; // name
string ssn; // social security number
public:
//...
void print(); // print data
string getName(); // retrieve name
};
class Student : public Person { // derived class
private:
string major; // major subject
int gradYear; // graduate year
public:
//...
void print(); // print data
void changeMajor(string newMajor); // change major
};
Member Functions
Person person(...); // define a person
Student student(...); // define a student
cout << student.getName();// invokes Person::getName()
person.print(); // invokes Person::print();
student.print(); // invokes Student::print();
person.changeMajor("Math"); // ERROR!
student.changeMajor("Math"); // OK
void Person::print() // definition of Person print
{ cout << name << " " << ssn; }
void Student::print() // definition of Student print
{ Person::print(); // first print Person data
cout << major << gradYear;
}
Illustrating Class Protection
class Base {
private:
int priv;
protected:
int prot;
public:
int publ;
};
class Derived: public Base {
void someMemberFunction()
{ cout << priv; // ERROR: private member
cout << prot; // OK
cout << publ; // OK
}
};
class Unrelated {
Base X;
void anotherMemberFunction()
{ cout << X.priv; // ERROR: private member
cout << X.prot; // ERROR: protected member
cout << X.publ; // OK
}
};
Constructors and Destructors
Person::Person(const string &nm, const string &ss)
: name(nm), ssn(ss) {} // initializer list
Student::Student(const string &nm, const string &ss,
const string &maj, int year)
: Person(nm, ss), // initialize Person data members
major(maj), // initialize member
gradYear(year) {} // initialize gradYear
Person::~Person() // Person destructor
{ ... }
Student::~Student() // Student destructor
{ ... }
Student* s = new Student(...);
//...
delete s; // calls ~Student() then ~Person()
[person.cpp]
Static Binding
When determining which member function to call, C++ default action
is to consider an object's declared type, not its actual content.
Person* pp[100];
pp[0] = new Person(...);
pp[1] = new Student(...);
cout << pp[1]->getName(); // OK
pp[0]->print(); // calls Person::print()
pp[1]->print(); // calls Person::print()
pp[1]->changeMajor(...); // ERROR!
Dynamic Binding and Virtual Functions
In dynamic binding, an object's contents determine which member
function is called.
class Person { // base class
virtual void print(); // print data
//...
};
class Student : public Person { // derived class
virtual void print(); // print data
//...
};
Person* pp[100];
pp[0] = new Person(...);
pp[1] = new Student(...);
pp[0]->print(); // calls Person::print()
pp[1]->print(); // calls Student::print()
pp[1]->changeMajor(...); // OK
[person.cpp]
Virtual Destructors
If a class defines any virtual functions, it should define a virtual
destructor, even if it is empty.
delete [] pp;
When
to use virtual destructors?
In
C++, what’s a virtual destructor and when is it needed?
2.2.2 Polymorphism
The ability of a variable to take different types!
2.3.1 Function Templates
- Minimum of two integers:
int min(int a, int b)
{ return (a < b ? a : b); }
- Minimum of doubles:
double min(double a, double b)
{ return (a < b ? a : b); }
- Generic function for an arbitrary type T, called function template:
template<typename T> // parameter list
T min(T a, T b) // returns the minimum of a and b
{ return (a < b ? a : b); }
- We can invoke our template function to compute the minimum of
objects of many different types. We could use any type, provided
that the less than operator (<) is defined for this type. The
compiler looks at the argument
types, and determine which form of the function to instantiate:
cout << min(3, 4); // invokes min<int>(3,4)
cout << min(6.9, 3.5); // invokes min<double>(6.9, 3.5)
cout << min('t', 'g'); // invokes min<char>('t', 'g')
2.3.2 Class Templates
template <typename Object>
class BasicVector {
Object* a; // array storing an element
int capacity; // length of array a
public:
BasicVector(int cap = 10) // constructor
{ capacity = cap;
a = new Object[capacity];// allocate array storage
}
Object& elemAtRank(int r) // access element at index r
{ return a[r]; }
// ...
};
BasicVector<int> iv(5); // vector of 5 integers
BasicVector<double> dv(20); // vector of 20 doubles
BasicVector<string> sv(10); // vector of 10 strings
//...
iv.elemAtRank(3) = 8; // iv[3] = 8;
dv.elemAtRank(14) = 2.5; // dv[14] = 2.5
sv.elemAtRank(7) = "hello"; // sv[7] = "hello"
Exceptions are unexpected events that occur during the execution of
a program.
2.4.1 Exception Objects
Using Inheritance to Define New
Exception Types
class MathException { // generic math exception
private:
string errMsg; // error message
public:
MathException(const string& err) { errMsg = err; }
};
class ZeroDivisionException : public MathException {
public:
ZeroDivisionException(const string& err)
: MathException(err) {}
};
class NegativeRootException : public MathException {
public:
NegativeRootException(const string& err)
: MathException(err) {}
};
2.4.2 Throwing and Catching Exceptions
try {
// ...
if (divisor == 0) throw ZeroDivisionException("Division by zero");
//...
}
catch (ZeroDivisionException& zde)
{ // handle division by zero
}
catch (MathException& me)
{ // handle any math exception other than division by zero
}
Once execution of the catch block completes, control flow continues
with the first statement after the last catch block.
2.4.3 Exception Specification
void calculator() throw(ZeroDivisionException, NegativeRootException)
{
//...
}
void funct1(); // can throw any exception
void funct2() throw(); // can throw no exception
Generic Exception Class
class RuntimeException { // generic run-time exception
private:
string errorMsg;
public:
RuntimeException(const string& err) { errorMsg = err; }
string getMessage() const { return errorMsg; }
};
inline std::ostream& operator<<(std::ostream& out, const RuntimeException& e)
{ return out << e.getMessage(); }