Class Files

Separating Source Files
To reinforce our object-oriented design methodology and to create fully reusable class modules, we separate class source files from application programs. Rather than including class definitions, member definitions, and an application in a single file, we separate the class files from an application. We do this also in preparation for applications requiring multiple class types. Similar to LegosTM, our goal is to have on hand a variety of different class types that we can mix and match for a variety of different applications. In keeping with convention, we adhere to the following rules:

Put the class definition into a header file with the same name as the class
Put class member functions definitions into a source file with the same name as the class

Consider the following example with class definition, member functions, and application in a single file.

#include <iostream>
using namespace std;

class Test

{

private:

  float val;

 

public:

  Test() { val = 0.0; }

 

  void setVal( float );

 

  float getVal();

 

  void Print();

};

 

void Test::setVal( float t )

{

  val = t;

}

 

float Test::getVal()

{

  return val;

}

 

void Test::Print()

{

  cout << “val: “ << val << endl;

}

 

main()

{

  Test t;

  t.setVal( 2.0 );

  t.Print();

}

With everything in a single file, it would be difficult to reuse this Test class in another application. Instead, we separate the class into separate files apart from the application. We first place the class definition in what is referred to as a class header file. We name this file using the same name as the class (for easy identification) and use a file extension indicating a header file (.h). Note also that any header files needed by function definitions are, by convention, included in the header file. In our example the header file, iostream, needed for the Print member function is included in the class header file below.

Test.h (header file)
--------------------

#include <iostream>           // Needed for Print definition

using namespace std;

 

class Test

{

private:

  float val;

 

public:

  Test() { val = 0.0; }

 

  void setVal( float );

 

  float getVal();

 

  void Print();

};

Next, we place all class member function definitions in what is referred to as a class source file. We name this file using the same name as the class (for easy identification) and use a file extension indicating a source file (.C or .cpp). Placing the header file and source files in separate files is yet another method we use to “separate the interface from the implementation.” The messages available to use with objects, or the interface, are listed in the header file while the actual implementation of each message is in the source file. Notice also how we use the pre-processor directive to include the class header file in the class source file. This is necessary for the compiler to recognize the new class type when processing the source file.

Test.C (source file)
--------------------
#include "Test.h"       // Include class header file

 

void Test::setVal( float t )

{

  val = t;

}

 

float Test::getVal()

{

  return val;

}

 

void Test::Print()

{

  cout << “val: “ << val << endl;

}

 

Any application is written as a separate file and uses such a class simply by including the header file through the include directive as follows 

 

app1.C (application file)
-------------------------
#include "Test.h"

 

main()

{

  Test t;

  t.setVal( 2.0 );

  t.Print();

}

 

Compilation
With multiple files, a few additional steps are required to compile executable programs. Both the class source file and application program need to be compiled into object code and linked together to create the final executable. Using our example above, we proceed with the following steps:

1) Compile class source to object code. Note the –c flag to create object code. The result of this operation is an object code file (Test.o) with the instructions from the class member functions.

g++ -c Test.C                 // creates object file Test.o

2) Compile application program to object code. Note the –c flag to create object code. The result of this operation is an object code file (app.o) with the instructions from the main application.

g++ -c app1.C                 // creates object file app1.o

3) Link object code together to form executable program. Note the –o flag to specify the output file name.

g++ -o app1 Test.o app1.o     // creates executable app1

Note that we avoid shortcuts such as (g++ *.C) to prevent re-compiling code that may have not been modified.



Using Multiple Classes
By storing classes in separate files, applications can use a variety of different classes to solve a problem. Classes are reused and combined with other classes to build a variety of applications in much the same way individual LegoTM pieces are used to build structures. This process allows us to design and develop applications in a true object-oriented approach.

To illustrate, consider the availability of a second class (in addition to our class listed above) describing a simple three-dimensional array.

Array3D.h (header file)
-----------------------
#include <iostream>

using namespace std;

 

class Array3D

{

 private:

  float val[3];

 

 public:

 

  // Constructor

  Array3D( float t1=0.0, float t2=0.0, float t3=0.0 );

 

  // Access functions

  void setVals( float, float, float );

  float *getVals() { return val; }

 

  // Print function

  void Print();

}; 
 

 

Array3D.C (source file)
-----------------------
#include "Array3D.h"

 

Array3D::Array3D( float t1, float t2, float t3 )

{

  val[0] = t1;

  val[1] = t2;

  val[2] = t3;

}

 

void Array3D::setVals( float t1, float t2, float t3 )

{

  val[0] = t1;

  val[1] = t2;

  val[2] = t3;

}

 

void Array3D::Print()

{

  cout << “Array3D” << endl;

  cout << “  val: “ << val[0] << “, “ << val[1] << “, “ <<  val[2] << endl;

}

Note the addition of default values given to the parameters in the constructor declaration:

Array3D( float t1=0.0, float t2=0.0, float t3=0.0 );

As we’ll see in detail later, this allows objects to be declared in applications without requiring values for each of the input arguments.

An application (in a separate file) can be developed that uses both this and the previous class in the following example:

app2.C (test application)
-------------------------

#include "Test.h"

#include "Array3D.h"

 

main()

{

  Test t;

  t.setVal( 2.0 );

 

  Array3D a( 1.0, 1.0, 1.0 );

  a.setVals( t.getVal(), t.getVal(), t.getVal() );

 

  a.Print();

}

As before, in order to use a class (or classes) in the application, we must include its class interface (or header file) to inform (or declare) to the compiler any new class type used in the application.

#include "Test.h"
#include "Array3D.h"

Classes can also be used directly by other classes in their class files. Let’s consider adding a member function to our Array3D class that uses the Test class above. We add a new overloaded function setVals to the Array3D header and source files as follows. As we’ll see in detail later, overloaded functions in C++ are functions with the same name but different parameter arguments allowing us to “send the same message” yet with different input parameters.

Array3D.h (revised header file)
------------------------------

#include "Test.h"       // Declare Test class type

#include <iostream>

using namespace std;

 

class Array3D

{

 private:

  float val[3];

 

 public:

 

  // Constructor

  Array3D( float t1=0.0, float t2=0.0, float t3=0.0 );

 

  // Access functions

  void setVals( float, float, float );

  void setVals( Test );

  float *getVals() { return val; }

 

  // Print function

  void Print();

};

 

 

Array3D.C (revised source file)
--------------------
#include "Array3D.h"

 

Array3D::Array3D( float t1, float t2, float t3 )

{

  val[0] = t1;

  val[1] = t2;

  val[2] = t3;

}

 

void Array3D::setVals( float t1, float t2, float t3 )

{

  val[0] = t1;

  val[1] = t2;

  val[2] = t3;

}

 

void Array3D::Print()

{

  cout << “Array3D” << endl;

  cout << “  val: “ << val[0] << “, “ << val[1] << “, “ <<  val[2] << endl;

}

 

void Array3D::setVals( Test t )

{

  val[0] = t.getVal();

  val[1] = t.getVal();

  val[2] = t.getVal();

}

Note that in order to use a Test object in our new overloaded member function setVals, we must declare this class type to the compiler by including its header file in the Array3D header file. We can then use this class type in the new function definition. We can then revise our application to use this new member function:

app3.C (test application)

-------------------------

#include "Test.h"

#include "Array3D.h"

 

main()

{

  Test t;

  t.setVal( 2.0 );

 

  Array3D a( 1.0, 1.0, 1.0 );

  a.setVals( t );

 

  a.Print();

}

To compile our application, we first compile the classes used in the application to object code:

g++ -c Array3D.C (creates Array3D.o)

g++ -c Test.C (creates Test.o)

Next we compile our application to object code, but get the following compilation error:

g++ -c app3.C

 

In file included from Array3D.h:2,

                 from app3.C:3:

Test.h:5: error: redefinition of `class Test'

Test.h:5: error: previous definition of `class Test'

Test.h:10: warning: inline function `Test::Test()' used but never defined

What is the problem? The compiler has encountered the definition of the Test class multiple times during processing. Starting at the top, the compiler processes each line of the application (app3.C). First, it processes the line instructing it to include the Test class definition into the file.

#include "Test.h"

Continuing, it processes the line instructing it to include the Array3D class definition into the file.

#include "Array3D.h"

Inside this file (Array3D.h), however, is another instruction to process the Test class again. This is the line we added previously in order to use the Test class in the Array3D class.

Array3D.h (revised header file)
------------------------------

#include "Test.h"       // Declare Test class type

When the compiler encounters the same class definition twice, as in this case, it prints an error reporting that we are trying to redefine a class (Test) that has already been defined. To correct this problem, we add preprocessor directives to each header file to prevent the compiler from including a class definition multiple times. As an example, we add the following lines to our Test header file (the class definition has been excluded below for brevity). Note that these directives are added to the very top and bottom of the header file.

Test.h (revised header file)
----------------------------

#ifndef _Test_h_
#define _Test_h_

 

class Test
{
    ....
};

 

#endif

The first directive (#ifndef _Test_h_) checks to see if the symbol, _Test_h_, has not been defined. If it has not been defined, the next directive (#define _Test_h_) defines the symbol then includes everything in the file up to the last directive (#endif). By using this system of setting symbols, the preprocessor skips over entire class definitions if it has already been included previously. In this way, if the compiler processes the same include line multiple times (as our example application above), it will only include the class definition once and avoid a redefinition error. With this in mind, we begin the convention of including such directives in every class header file we write. Think of these directives as “protective bookends” in your header files to prevent redefinition errors.

Continuing our compilation process above, we add the preprocessor “bookends” to our Array3D header file:

 Array3D.h (revised header file)
----------------------------

#ifndef _Array3D_h_
#define _Array3D_h_

 

class Array3D
{
    ....
};

 

#endif

We can now compile our application file safely to object code:

g++ -c app3.C (creates app3.o)

To create our final executable, we link all class object codes together with the application code:

g++ -o app3 Test.o Array3D.o app3.o (creates executable app3)