OOP IIEncapsulation, using classes |
The big idea is this: we make certain methods and members PUBLIC - these form an **interface** - these are what can be invoked 'from the outside'; we make other methods and other members PRIVATE - these are part of the **implementation** - these are NOT visible from the outside.
By separating interface from implementation, we are able to write CLEAN, MODIFIABLE (internals) and ultimately, maintainable, reusable and evolvable code!
We have 'evolved' from thinking in terms of datatypes and control flow, to thinking in terms of function I/O, to thinking in terms of interface vs implementation.
Let's now create a vector **class**, in place of the struct vector {} that we had earlier.
class vector {
private:
float x,y,z;
public:
void setVec(float a, float b, float c);
void prtVec();
};// class vector
Note the 'class' keyword, classname, {}, ; at the end. Inside the {} body, we have a private section, and a public section (note: there can also be a 'protected' section, you can look this up after the course).
Here is how to use our new class defn (by creating OBJECTS/ INSTANCES of TYPE vector) and invoking their methods. In other words, we can create multiple vector objects, using our vector class definition; and for each vector object, we can call its methods.
Let us return to vector, a bit later. Meanwhile, consider this class to construct a chain of integers (to store the Fibonacci sequence, for example, or permutations of 1,2,3,4..10, etc.).
To use (create objects of) this class, we can write a function called twoRows(), inside which we create and populate two 'row' objects called r and s, then call twoRows() from main(). Here is the code.
The code seems to work just fine - so, what is the problem?
There are THREE problems:
Solution: provide a CONSTRUCTOR (for initializing, and auto mem alloc), and a DESTRUCTOR (for deleting the allocated memory for the ptr member). Here is the modified code. A constructor (and destructor) is a special function, that is called JUST once, when the object is constructed (or destroyed).
Note that we've also made ptr and len private, that we can't accidentally or intentionally say s.len=12000 etc. The user can supply a length (count), ONLY during construction, and can't change it afterwards.
Also, once twoRows() finishes executing, all vars+objs inside it go "out of scope" (become non-existent)- our ~row() destructor kicks in just before the function exit, to clean up, ie. free up memory (deletes the chain of ptr memory before itself gets deleted). Cool - no more memory leak!
Let us return to our vector class, and add in a constructor and an empty destructor.
What if we want to add two vectors?? Eg. we'd like to add v1 and v2, to result in v3 (without modifying v1 and v2).
Two ways:
* create a standalone sum() function
* 'overload' the + operator
A standalone sum() function can take two vectors as inputs, **grab their components** (if possible!), add, construct a new vector with the sum, and return it. Here is how we'd do this.
Note that we did an 'inline' spec for getVec() - ok to do so for __small__ functions only (bigger ones should be defined separately).
Given an 'inline' function or method, the compiler will try to ELIMINATE the function call, ie. will try to replace each occurrence of the function call, with the body of the function! This is done for efficiency reasons (to avoid the overhead of calling the function). More on 'inline' here.
What is more cool (and more natural) than sum(v1,v2)? Answer: v1+v2. In other words, we need to 'teach' + (ie. overload it) to do component-wise addition. This is what that looks like. Likewise, in a matrix class, we'd overload * to carry out matrix multiplication so that we can "simply" say 'matrix m3 = m1*m2;', for example.
We just saw an inline definition for getVec(), **inside** vector's class definition. When specified this way (inside a class defn), there is NO need for the 'inline' keyword, such specifications are always considered to be 'inline'.
Alternately, an inline function can be specified **outside** a class definition as well - but now we do NEED the 'inline' keyword:
class vector {
....
void getVec(float &a, float &b, float &c); // defn follows
};// class vector
inline void vector::getVec(float &a, float &b, float &c) {
a=x;
b=y;
c=z;
}// getVec()
Note that we'd make a function (method) inline, if we want to avoid a function call overhead. Alternately, if we don't want a method to ever get inlined, we need to declare just its prototype inside the class defn; we would define the body of it outside, and leave out the 'inline' keyword while doing so.
Given a class called 'vector', we can create vector objects as well as pointers to them:
vector v; // v is a vector object
vector *p; p is a pointer to a vector
p = &v; // we point p to a vector (similar to 'int k; int *p=&k;')
Here is syntax we haven't seen so far: we can access a pointed-to object's members and methods, in one of two ways:
(*p).prtVec(); // because *p is the object that p points to
p->prtVec(); // alternate, better, recommended syntax to access prtVec()
Rather than use (*p)., we prefer p-> to access the "pointee's" members and methods. Here is a working example.
Rather than point a pointer to an existing vector object (p=&v), we can dynamically create a new vector (or an array of vectors) "on the heap", using 'new', and assign that to our pointer:
vector *vecPtr = new vector(2,3,4);
....
vecPtr = new vector[10]; // points to an array of 10 vector objects
Run this example to see dynamic object creation in action.
Likewise, we can dynamically create a bank account, array of game characters, rectangle, song, array of images, video, web page.. and assign these to pointers. The pointers can be passed into functions or methods, where they can be used to access object data that's on the heap. Such a capability is very POWERFUL!
An object can have as its members, both PRIMITIVE types (eg. int, array of float..) as well as OBJECTS. Eg. a CoordinateSystem class would contain (be COMPOSED of) three vector objects:
class CoordSystem {
public:
vector xAxis,yAxis,zAxis;
....
}// CoordSystem
Likewise, a Scene object would be composed of a Camera object, Light objects and Geometry objects. In TRL, a Car is composed of 4 Wheels, Seats, CarRadio, Doors, etc. Those sub-objects have their own class definitions (including constructors, members, methods).
Most users of C++ don't CREATE classes; rather, they USE existing classes provided to them (eg. a Matrix class, an FFT class, a Molecule class, a Building class..) to get their work done. Oftentimes such pre-existing classes are available as a class "library" (pre-compiled, 'archived' .o files), eg. opengl.dll, libfft.a (on PCs). The linker will compile our sources and such libs together, to create a single binary (executable):
Library files are supplied/used in the form of compiled files because that keeps their sizes small, (weakly) protects against reverse engineering/unwanted code modification. Conversely, libraries are supplied and used in source form for pedagogical (teaching) purposes:
Let us illustrate using source code libraries, by using a library that contains code for creating (and reading) MIDI files.
A MIDI file contains binary (not text) data in a non-trivial file format (not easy to create with simple << outputting into an ofstream object, like we did for .pgm).
Thankfully, the creator of our MIDI library (which is provided in source form: 6 pairs of .h,.cpp files) has done all the hard work of defining MIDI-related classes, and has supplied with a simple "API" (Application Programmer Interface") for creating .mid files that can play on our machines.
Unzip this file - it will create a dir called MIDI, in which you will find:
* six .h and matching .cpp files: these make up our 'library' files (usually we'd have .h sources plus compiled (and combined) .o files)
* a driver.cpp file: this is our 'driver' source, where we have a main() inside which we employ the MIDI lib's API calls
Create a new CB project (eg. MIDIgen), delete the main.cpp that comes with it, and add our seven .h and .cpp files, turn on the 'Have g++ follow the C++11 ISO C++ language standard' compiler flag (in Settings->Compiler) [if you are compiling by hand, specify -std=c++11 as the compiler flag], compile, run, then look in the MIDIgen folder. Voila - our executable [should have] created this .mid MIDI file - NICE!!
Exercise: modify the two 'notes' arrays in driver.cpp to specify different musical instruments.
Another exercise: can you create a .mid file that plays the classic Happy Birthday tune? :) Here is an example..
What just happened (how could we create a MIDI file)? Look in driver.cpp - we are using our MIDI lib's API to create tracks, populate them with notes and output to file.
#include "MidiFile.h"
...
MidiFile outputfile;
...
outputfile.addTrack(2);
...
outputfile.addEvent(1, actiontime, midievent);
...
outputfile.write("twinkle-tmp.mid");
...
Again: in driver.cpp, we pretty much do just this: specify note values for two tracks, insert them into our MIDI object, write it out to a file - "simple" :) But HOW? We don't know/care!!!
Next, we need to look how to DERIVE/"specialize" a new class (subclass) from an existing class (base class). Remember - inheritance is the other OO principle (in addition to encapsulation).