C++ Classes

Part II
The material discussed thus far can best be described as an "improved" C. After all, just in time declarations, new comments, overloading and the like, while certainly convenient, don't really require a different outlook in how you program.

Object oriented programing, of which C++ is an example, suggests a different paradigm. The four buzz words are abstraction, encapsulation, hierarchy and polymorphism. The basic mechanism used by C++ to achieve these properties is the notion of a "class".

Roughly speaking, a C++ class is a C struct with functions. To have a concrete example in front of us, let us consider writing a C++ class to handle quaternions. The data part is inspired by C struct's:


class quat {
     float r;
     float i;
     float j;
     float k;
     };

One can declare a class just like any other data type:
quat Q;
makes "Q" the name of a quaternionic variable. This is still just the sort of thing one would do in C, but in C++ one also includes the functions used to manipulate quaternions in the class. Examples where would include functions to add, subtract, multiply and divide quaternions. In our example


class quat {
public:
     quat add(quat, quat);
     quat sub(quat, quat);
     quat mul(quat, quat);
     quat div(quat, quat);

private:
     float r;
     float i;
     float j;
     float k;
     };

Functions are declared just as you would in C; "public" and "private" are described below, as is how to write the code for these functions. Using functions in a class has a syntax which mimics C's conventions for getting at the elements of a struct. Let us assume that we have three variables, "Q1", "Q2", and "Q3", all previously declared to be of class "quat". Then
Q3=Q3.add(Q1,Q2);
adds the values from "Q1" and "Q2" and puts the sum in "Q3". No experienced C++ programmer would set things up like this however and the notation gives a clue to the elegant solution. Why did we write "Q3.add" instead of "Q2.add" or "Q1.add"?

The answer is that we could have written any of these and gotten the same behavior. The more elegant solution is to have "add" return a void and have the code for "Q3.add" automatically put the sum in "Q3", whereas "Q2.add" would put the sum in "Q2" and so on. We should think of functions in a class as functions we write once but every time we declare a new instance of the class, we get a new set of functions. Before continuing, let us change our declaration of our class to reflect these ideas:


class quat {
public:
     void add(quat, quat);
     void sub(quat, quat);
     void mul(quat, quat);
     void div(quat, quat);

private:
     float r;
     float i;
     float j;
     float k;
     };

The time has come to explain "public" and "private". C++ makes it easy to hide details of how your class works from other code. The buzz word here is "encapsulation". The only data and functions to which users of your class have access are the "public" ones. It is not necessary to have all the public's first followed by all the private's. A "public:" causes everything after it to be public until a "private:" is encountered, and then everything is private until the next "public:" is encountered and so on.

In our "quat" example, the user has access to the four functions, "add", "sub", "mul", div", but not to the actual data. If "Q1", "Q2" and "Q3" are quat's, then in your program
Q3.add(Q1,Q2)
is legal and adds "Q1" to "Q2" and puts the sum in "Q3". The statement
Q2.i=3.89
is illegal, the "i" part of "Q2" is private. This suggests an immediate problem: how are we ever going to get any values into our variables? The answer is simple once we absorb the paradigm: we need a new function in our class,
void set(float, float, float, float)
so "Q1.set(1.0,8.4,2.3,0)" should produce the quaternion "1.0+8.4i+2.3j+0.0k" as the value of "Q1". Of course this function better be public!

Encapsulation does make life more difficult for the writer of classes, but the goal is to make life easier and safer for the user of classes. In this example, suppose the maintainer of the code decides for reasons of efficiency or accuracy or whatever that it would be better to store a quaternion as a norm, a non-negative real number, times a unit quaternion, so now the data would look like


     float norm;
     float r;
     float i;
     float j;
     float k;

If we just used C struct's, any code using quaternions would certainly break under this change, but if the data can only be accessed through the public functions, the maintainer will have to rewrite all the class functions, but the user will have to rewrite nothing, so even if this code has been included in thousands of different programs, as soon as the new class code is made available, all these programs will continue to work and will be able to take advantage of the improvements.

The down-side is that classes have to be thought out carefully. A certain amount of this can be done piecemeal, as we did above when we decided we needed a "set" function. As we go along we will certainly think of other functions we will need. C++ makes it easy to add them IF we have access to the "quat" class, but C++ also makes it IMPOSSIBLE to add functions to the class if we cannot re-declare it. If the class has been poorly thought out it will not be very useful. If we shipped this class as we have discussed it so far, there would be no way to print any answers. As a start, let's declare our class


class quat {
public:
     void set(float, float, float, float);
     void print(quat);
     void add(quat, quat);
     void sub(quat, quat);
     void mul(quat, quat);
     void div(quat, quat);

private:
     float r; // real part
     float i;
     float j;
     float k;
     };

Next let's describe how to functions in classes using "set" as an example. Anytime AFTER the class is declared we write the code:


quat::set(float real, float i, float j, float k) {
     this->r=real;
     this->i=i;
     this->j=j;
     this->k=k;
     }

Name_of_class::name_of_function(variables...) {      CODE      }

When you actually invoke an instance of the function, "this" will be a pointer to the variable, so when we do the code for "Q1.set(1.0,8.4,2.3,0)", "this" points to "Q1", so "Q1->r" is set to "real" just as we want. More experienced C++ programmers would take advantage of the help the compiler supplies: "this" is usually unnecessary. In most instances where it seems needed, the complier will add it, so the "set" code would most likely be written


quat::set(float real, float i, float j, float k) {
     r=real;
     i=i;
     j=j;
     k=k;
     }

The "add" code is equally simple:


quat::add(quat Q, quat R) {
     r=Q.r+R.r;
     i=Q.i+R.i;
     j=Q.j+R.j;
     k=Q.k+R.k;
     }


Math 211 homepage.
C++ First Steps.
C++ Overloading.
C++ Derivation.