CIS-62 Home http://www.c-jump.com/CIS62/CIS62syllabus.htm
Consider:
void print( Printer* pr ) { if ( pr->type == 'L' ) { // compose and send print job to lazer printer } else if ( pr->type == 'I' ) { // compose and send print job to ink-jet printer } else if ( pr->type == 'F' ) { // compose and send fax } }
|
|
|
|
|
|
|
|
|
We want our main function to know as little as possible.
C++ class hierarchy is a design that supports polymorphism.
Polymorphism stands for uniform interface.
Printer example is based on run-time polymorphism,
also known as ad-hoc polymorphism:
Programmer determines a finite numbers of polymorhic behaviors.
Program delegates control to the class responsible for the implementation of each particular command.
|
|
Support re-use of common code and data,
while allowing natural class designs.
Support run-time polymorphism.
A relationship between two classes
Base/parent/superclass and Derived/child/subclass
The existence of relationship is determined by the derived class.
Some aspects of the relationship are controlled by the base class.
In a sense, the derived class is a client of the base class.
Derived classes have the data and functions of the base class.
|
|
Parents are oblivious to existence of children.
private members of parent are not visible to children.
New protection level: protected
protected members of parent are visible to children, but not visible outside of the hierarchy.
Children can override member functions of parents,
however, parent functions are still available via explicit scope qualification (this is very useful).
class Account { public: Account(); bool sell( string symbol, int shares ); bool buy( string symbol, int shares ); bool deposit( double amount ); bool withdraw( double amount ); protected: void set_cash_balance( double amount ); private: double m_cash_balance; static const int SIZE = 100; string m_holding_symbols[ SIZE ]; int m_holding_shares[ SIZE ]; };//class Account
A derived class incorporates a base part,
which must be constructed.
By default, the base part is constructed using the the base default constructor.
What if there is no default constructor for the base?
Solution: use the initializer list in the derived class constructors.
Note: base class construction happens first!
// Base
class Account {
public:
Account( int bal );
};// class Account
// Derived
class GoldAccount : public Account {
public:
GoldAccount( int bal );
private:
int m_margin_balance;
};// class GoldAccount
// Derived constructor invokes base constructor:
GoldAccount::GoldAccount( int bal )
: Account( bal ),
m_margin_balance( 0 )
{
}
|
|
|
|
|
|
|
Basically, these two ideas don't interact:
Being a friend of Base doesn't make you a friend of Derived.
Being a friend of Derived doesn't make you a friend of the Base.
|
|
|
Inheritance allows code reuse, so do templates.
Inheritance implies an asymmetric relationship between classes.
template instantiations are unrelated.
Templates support compile-time polymorphism.
Inheritance can support only run-time polymorphism.
Again, it's usually clear which one to use.
Combinations are possible, and often used in modern code.
Compiler-generated assignment and copy constructors include the base classes automatically.
However, if you define your own, they are not automatically inherited.
Thus, you have to chain to the base classes manually. For example,
Derived& Derived::operator=( Derived const& other ) { if ( &other == this ) return *this; Base::operator=( other ); }
An object of a derived type will be implicitly converted to an object of the base type when necessary:
class Base {}; class Derived : public Base {}; void e( Base b ); void f( Base* bp ); void g( Base& br ); void h( Derived* dp ); Base b; Derived d; e( d ); // OK, implicit conversion via "upcast" f( &d ); // OK, implicit conversion g( d ); // OK, implicit conversion h( &b ); // Error
class Base {}; class Derived : public Base {}; void f( Base* bp ); void g( Base b ); void h( Derived* dp ); Derived d; Base b; Base* bp = &d; // OK f( bp ); // OK g( b ); // OK h( (Derived*) bp ); // Accepted, but not advised h( (Derived*) &b ); // Accepted, but surely wrong!
Extend functionality of base class with new members.
Provide identical functionality, but enforce
additional constraints on state or inputs
use alternative algorithms, etc.
Incorporate useful behavior by privately deriving from a base class, but redefine the public interface of the derived class. Thus, derived class becomes an adaptor of the base class.
Polymorphism: using something in multiple ways
E.g. overloading operator+()
E.g. or std::vector< int >
Dynamic polymorphism defers the decision about how to do something until runtime.
C++ provides a form of function overloading that yields dynamic polymorphism:
Works (only!) through pointers or references;
Uses inheritance, meaning that polymorphism is restricted to types derived from some common base.
Steps to implement run-time polymorphism in C++:
Use inheritance, deriving classes from some common base type.
Redefine common functions in derived types.
Construct objects of the specific derived types.
Maintain these objects via pointers or references to the common base type.
Call common member functions on these pointers or references.
C++ will redirect your calls to the real types of objects.
|
|
|
|
Once a function is virtual in some base class, it is virtual in all descendant classes, whether labeled so or not:
class Shape { public: virtual void draw(); };//class Shape class Circle : public Shape { public: void draw(); // Still virtual };//class Circle
Use the virtual label in derived classes to avoid confusion!
A pure virtual function is one which must be implemented in derived classes.
A class with a pure virtual function is called an abstract base class.
A class with a pure virtual function cannot be instantiated:
class Shape { public: virtual void draw() = 0; // pure virtual };//class Shape int main() { Shape sh; // Error, cannot create instance }
Compilers are free to implement virtual functions as they see fit.
Generally, implementation is done with:
static tables of function pointers in each class with virtual functions,
and a vtbl pointer in each object.
Thus, an object of a class with virtual functions may have an unusual size.
Calling virtual functions requires a bit more time
(but is still faster than faking it!)
Destructors of base classes should be declared virtual:
class Shape { public: virtual void draw() = 0; // pure virtual virtual ~Shape(); // virtual destructor };//class Shape
Otherwise, destruction of a derived object via a base reference or pointer is (gasp!) undefined...
...so even the base part may not be properly destroyed!
This means you can't really safely inherit from classes with non-virtual destructors.
Maybe just make all destructors virtual?
Well, this is a size issue.
Any class with a virtual function should certainly have a virtual destructor.
Inheritance of implementation:
Base class provides some useful functionality (and sometimes data).
Remember: encapsulation is your friend.
Inheritance of interface:
Base class specifies common interface for a collection of types that will be used together.
Consider using abstract base class.
A good way to really hide implementation.
class IShape is a pure interface - no implementation here at all:
//ishape.h: class IShape { public: virtual void draw() = 0; // Factory functions static IShape* MakeCircle(); static IShape* MakeSquare(); };//class IShape
Consider:
int* ptr = (int*) x;
C++ inherited (type)x -style conversion from C.
The above declaration of ptr can be quite dangerous, because:
it is hard to spot or find in a large program;
the kind of converison is not explicit: the types could be
related (x is also a pointer type),
unrelated (x is not a pointer);
it can be portable;
it can be non-portable.
Is there a better way of casting?
Use static_cast in place of most C-style casts.
Use static_cast when converting from one type to another, and the types are related:
Converting to and from void*
Converting float to int
Converting enum to int
Other similar conversions when types are related (ptr to ptr).
// Trust the programmer, with compiler help: double d = 3.5; int x = static_cast< int >( d ); // NO ROUNDING: x gets 3, not 4! int* p = static_cast< int* >( malloc( sizeof( int ) * 100 ) );
Compiler will help the conversion (it may do some checking.)
Warning: using static_cast often is not portable, because types could have different sizes on different systems.
Use dynamic_cast when you are converting up or down a type hierarchy and want runtime checking:
Derived* dp; // Trust the compiler: Base* bp = dynamic_cast< Base* >( dp ); // surely safe Derived* dp = dynamic_cast< Derived* >( bp ); if ( dp != NULL ) { // cast worked fine: //... } else { // *bp wasn't really a Derived object!! //...
dynamic_cast is an operator that requires a polymorphic operand.
It comes with a small run-time cost overhead.
Safe and portable.
The reinterpret_cast handles conversions between unrelated types, such as int to ptr*, etc.
It produces value of a requested type with the same bit pattern.
Use reinterpret_cast when you know you are doing something correct, and don't need the compiler to check:
static const unsigned int IO_PORT_A = 0x00fc; Port* io_port = reinterpret_cast< Port* > IO_PORT_A;
Using reinterpret_cast most likely is not portable!
Use const_cast when you want to cast away constness on some object.
Advice: try using mutable class data members instead.
void foo( Account const& acct ) { Account& back_door = const_cast< Account& > acct; back_door.some_NONCONST_function(); }
|
|
Think about interface and implementation separately.
For real modularity, provide a pure interface and a separate function or class for producing instances.
Always make destructors virtual.
Don't redefine non-virtual functions.