CtoC++
CtoC++: Upgrading to Object Oriented C
Introduction
This tutorial carries on where StartingC left off.
To get the material, cut and paste the contents of the box below onto your command line.
svn co http://source.ggy.bris.ac.uk/subversion-open/CtoC++/trunk ./CtoC++
In this tutorial we will assume basic linux skills as outlined in Linux1.
Cutting to the Chase: Classes and Encapsulation
So, he we are contemplating C++. We've got to grips with most of the C language in StartingC and it looked alright. Definitely serviceable. What's all the fuss about C++? Well, I believe that most of the fuss is about encapsulation. We saw the benefit of collecting together related variables into structures in C, true? Well, C++ goes further and allows us to collect together not only related variables, but also functions which use those variables too. An instance of a class is called an object and it comes preloaded with all the variables and functions (aka methods) that you'll need when considering said object.
What may have seemed like the relatively small enhancement of adding methods to the encapsulation has, in fact, resulted in a sea-change. No longer are we thinking about a program in terms of the variables and the functions, but instead we're thinking about objects (planets, radios, payrolls and the like) and how they interact with other objects. The whole thing has become far more modular, and so easier to work with. Indeed, this is no accident as object oriented programming (OOP) arose in response to programs written in the functional style getting larger and unwieldy and hard to work with. In short it arose to swap spaghetti for lego.
OK, enough of the spiel, let's get going with an example:
cd CtoC++/examples/example1 make
The first chunk of code to greet you inside class.cc (we'll use .cc to denote C++ source code files) is:
// // This is a C++ comment line // #include <iostream> // A useful C++ library #include <cmath> // The standard C math library // declare a namespace in which to keep // some handy scientific constants namespace scientific { const double pi = 3.14159265; // note the use of 'const' const double grav_constant = 6.673e-11; // uinversal graviational constant (m3 kg-1 s-2) const int sec_per_day = 86400; // number of seconds in 24 hours } // avail ourselves of a couple of namespaces // via the 'using' directive using namespace std; // allows us to use 'cout', for example using namespace scientific;
What's new? Well, first up, we see that the comment syntax has changed and that we can use just a leading double forward slash (//) to signal a note from the author. #include is familiar, except that we've dropped the .hs from inside the angle brackets.
The next block is a namespace declaration. The concept of a namespace is common to a number of programming languages and here we're setting one up called scientific and using it to store some handy constants. We can enclose anything we like in a namespace. We access the contents of a namespace via the using directive. In this case we're accessing an intrinsic one called std (standard)--we'll be doing that a lot!--and also our scientific one. The idea behind namespaces is to reduce the risk of a clash of names when programs get large. They're handy.
Next up in the source code is the class declaration (and definition, as it heppens) itself:
class satellite { private: // Private members of a class class cannot be accessed // from outside the class. double period; // time taken to orbit e.g. earth (s) double sma_of_orbit; // semi-major axis of satellite's orbit (m) public: // Public members of the class are visible to the // rest of the program. // Method to assign values to private variables. void set(const double prd, const double sma) { period = prd; sma_of_orbit = sma; } // Method to compute mass of a celestial body // given the period of a satellite which orbits // it and the semi-major axis of that orbit. // See Kepler's laws of planetary motion. double mass_of_attractor(void) { return (4.0 * pow(sma_of_orbit,3) * pow(pi,2)) / (pow(period,2) * grav_constant); } };
You can see that the class called satellite contains some variables and also some methods. The contents of the class is also separated into two sections by the keywords private and public. We've declared our variables to be private (cannot be seen from outside the class) and our methods to be public (are visible from outside). In doing so, we've set up an interface (i.e. the public methods) through which other parts of the program can interact with this class. In this case, the program at large can call set(), providing information about the satellite's orbit as it does so, and also mass_of_attractor() in order to discover the mass of whatever the satellite is orbiting.
The existence of an interface simplifies the ways in which the object interacts with the rest of the program and means that any alterations to the program are much easier to make. For example, any you can make changes to the internals of a class without fear that you will unwittingly break some aspect of the program outside of the interface. Indeed, we could entirely re-write the contents of a (perhaps complex) class and as long as the interface remains unchanged, the rest of the program need never know! This is quite a boon for scientific software, which has a more rapid schedule of alterations that other kinds of software.
Last up is our glue code, or main function:
int main (void) { // Declare an 'instance' of the satellite class, // called 'moon'. satellite moon; cout << "== Welcome to the intro to classes program! ==" << endl << endl; // Set some values pertaining to the moon. moon.set((27.322*sec_per_day),384399e3); // Call a method of the satellite class // and report results to the 'stdout' stream. cout << "Mass of the Earth (kg) is: "<< moon.mass_of_attractor() << endl; return EXIT_SUCCESS; }
in which we declare in instance of our satellite class, the moon object, call set() and finally mass_of_attractor(), noting the dot (.) operator for accessing members of the class.
The way in which we print to stdout is also different in C++. Here we have used the left shift operator (<<) together with the cout I/O stream and also the endline (endl) operator.
You can run the program--and weigh the Earth!--by typing:
./class.exe
(The eagle-eyed amongst you will note that we have a small error in our calculation of mass. The intrigued amongst the cohort of eagles may be relieved to see that Kepler's law gives the combined mass of the moon and Earth in this case, and that if we subtract off the mass of the moon, we get closer to the actual mass of the Earth--phew!)
Exercises
- Add a new method to the satellite class to compute the mean orbital speed of the satellite, and perhaps another to compute the satellites speed at various points along it's orbit?
- Add a whole new class to the program. One for a planet orbiting a star, such as our sun, would be a good one.
More on Methods
OK, so far so good. We've bundled up some methods and variables into a class. This is all to the good. However, we haven't delved too deeply into all the features that C++ provides with regards to methods. Let's rectify that right now. We'll make a start by typing:
cd ../example2 make
In this directory, you'll see that we've split our program over the files;
- methods.h, containing the declarations for our enhanced satellite class,
- methods.cc, containing the 'meat' of the methods and,
- main.cc, containing the main function inside which we run our class through it's paces.
Looking inside the header file, you'll see our scientific namespace again, as well as the class declaration:
class satellite { private: char *name; // name of satellite unsigned int iNameLen; // length of name string double period; // time taken to orbit e.g. earth (s) double sma_of_orbit; // semi-major axis of satellite's orbit (m) // copy method void copy(const satellite& _stllt); public: // default constructor // Note same name as class satellite(); // constructor with arguments satellite(const char *nm, const double prd, const double sma); // copy construcor satellite(const satellite& _stllt); // assignment operator satellite& operator=(const satellite& _stllt); // previous mass calculation method double mass_of_attractor(void); // previous set method void set(const char *nm, const double prd, const double sma); // display method void display(void); // default destructor ~satellite(); };
This time around we have some extra class members:
- We have a character pointer called name, along with an integer to store the length of the character array, once some memory has been allocated.
- We have a number of constructors methods, which we immediately see are special since their shared name matches the name of the class.
- We have a destructor, where it's name has a leading twiddle (~) and also matches the class name.
- We have a private method called copy,
- a display method and also
- an assignment operator (=).
Let's go through these in turn.
Constructors are invoked when a new object is created. The two relevant lines in 'main.cc are:
satellite moon1; // default construcor satellite moon2("moon2",(27.322*sec_per_day),384399e3); // construcor with args
Here we've declared two instances of the satellite class, and called them moon1 and moon2. We created moon1 using the default constructor (no arguments follow the variable name). The internals of which we can find inside methods.cc:
// default constructor satellite::satellite() { iNameLen = 0; name = new char[iNameLen + 1]; (*name) = '\0'; // empty string period = 0.0; sma_of_orbit = 0.0; }
As it's name suggests, this method sets up a default object (zero values, null strings etc.) in lieu of any specific information.
moon2 was created using a constructor which takes arguments:
// constructor with arguments satellite::satellite(const char *nm, const double prd, const double sma) { set(nm, prd, sma); }
This method accepts the name of the satellite instance, together with values for the period and the semi-major axis. Given these, it merely calls the set() method, which is sensible since this method has all the functionality that we desire, and it's a bad idea to duplicate the code.
We can see that these two methods have exactly the same save and differ only in their associated argument lists. This is an example of what's called overloading, which can be highly desirable when designing clear and simple class interfaces. We can overload both methods and operators.
Exercises
- Method arguments can have defaults attached, e.g. satellite(const double period = 0.0, ...). Rewrite our constructor with arguments, so that we no longer need a default constructor.
Templates
Inheritance
Not multiple inheritance.