Wrox Home  
Search
Professional C++
by Nicholas A. Solter, Scott J. Kleper
January 2005, Paperback


Excerpt from Professional C++

Writing Classes

When you write a class you specify the behaviors, or methods, that will apply to objects of that class and the properties, or data members, that each object will contain.

There are two elements to writing classes: defining the classes themselves and defining their methods.

Class Definitions

Here is a first attempt at a simple SpreadsheetCell class, in which each cell can store only a single number:

// SpreadsheetCell.h
class SpreadsheetCell
{
    public:
        void setValue(double inValue);
        double getValue();

    protected:
        double mValue;
};

Every class definition begins with the keyword class and the name of the class. A class definition is a statement in C++, so it must end with a semicolon. If you fail to terminate your class definition with a semicolon, your compiler will probably give you several errors, most of which will appear to be completely unrelated.

Class definitions usually go in a file with the name ClassName.h.

Methods and Members

The two lines that look like function prototypes declare the methods that this class supports:

void setValue(double inValue);
double getValue();

The line that looks like a variable declaration declares the data member for this class:

double mValue;

Each object will contain its own mValue variable. However, the implementation of the methods is shared across all objects. Classes can contain any number of methods and members. You cannot give a member the same name as a method.

Access Control

Every method and member in a class is subject to one of three access specifiers: public, protected, or private. An access specifier applies to all method and member declarations that follow it, until the next access specifier. In the SpreadsheetCell class, the setValue() and getValue() methods have public access, while the mValue member has protected access:

public:
    void setValue(double inValue);
    double getValue();
protected:
    double mValue;

The default access specifier for classes is private: all method and member declarations before the first access specifier have the private access specification. For example, moving the public access specifier below the setValue() method declaration gives setValue() private access instead of public:

class SpreadsheetCell
{
        void setValue(double inValue); // now has private access
    public:
        double getValue();

    protected:
        double mValue;
};

In C++, structs can have methods just like classes. In fact, the only difference between a struct and a class is that the default access specifier for a struct is public and the default for a class is private.

The following table summarizes the meanings of the three access specifiers:

Access Specification Meaning When to Use
public Any code can call a public method or access a public member of an object. Behaviors (methods) that you want clients to use.
Access methods for private and protected data members.
protected Any method of the class can call a protected method and access a protected member.
Methods of a subclass can call a protected method or access a protected member of an object.
"Helper" methods that you do not want clients to use.
Most data members.
private Only methods of the class can call a private method and access a private member.
Methods in subclasses cannot access private methods or members.
Only if you want to restrict access from subclasses.

Access specifiers are at the class level, not the object level, so methods of a class can access protected or private methods and members on any object of that class.

Order of Declarations

You can declare your methods, members, and access control specifiers in any order: C++ does not impose any restrictions such as methods before members or public before private. Additionally, you can repeat access specifiers. For example, the SpreadsheetCell definition could look like this:

class SpreadsheetCell
{
    public:
        void setValue(double inValue);

    protected:
        double mValue;

    public:
        double getValue();
};

However, for clarity it is a good idea to group public, protected, and private declarations, and to group methods and members within those declarations. In this book, we order the definitions and access specifiers in our classes as follows:

class ClassName
{
    public:
        // Method declarations
        // Member declarations

    protected:
        // Method declarations
        // Member declarations

    private:
        // Method declarations
        // Member declarations
};

Defining Methods

The preceding definition for the SpreadsheetCell class is enough for you to create objects of the class. However, if you try to call the setValue() or getValue() methods, your linker will complain that those methods are not defined. That's because the class definition specifies the prototypes for the methods, but does not define their implementations. Just as you write both a prototype and a definition for a stand-alone function, you must write a prototype and a definition for a method. Note that the class definition must precede the method definitions. Usually the class definition goes in a header file, and the method definitions go in a source file that includes that header. Here are the definitions for the two methods of the SpreadsheetCell class:

// SpreadsheetCell.cpp
#include "SpreadsheetCell.h"

void SpreadsheetCell::setValue(double inValue)
{
    mValue = inValue;
}

double SpreadsheetCell::getValue()
{
    return (mValue);
}

Note that the name of the class followed by two colons precedes each method name:

void SpreadsheetCell::setValue(double value)

The :: is called the scope resolution operator. In this context, the syntax tells the compiler that the coming definition of the setValue() method is part of the SpreadsheetCell class. Note also that you do not repeat the access specification when you define the method.

Accessing Data Members

Most methods of a class, such as setValue() and getValue(), are always executed on behalf of a specific object of that class (the exceptions are static methods, which are discussed below). Inside the method body, you have access to all the data members of the class for that object. In the previous definition for setValue(), the following line changes the mValue variable inside whatever object calls the method:

mValue = inValue;

If setValue() is called for two different objects, the same line of code (executed once for each object) changes the variable in two different objects.

Calling Other Methods

You can call methods of a class from inside another method. For example, consider an extension to the SpreadsheetCell class. Real spreadsheet applications allow text data as well as numbers in the cells. When you try to interpret a text cell as a number, the spreadsheet tries to convert the text to a number. If the text does not represent a valid number, the cell value is ignored. In this program, strings that are not numbers will generate a cell value of 0. Here is a first stab at a class definition for a SpreadsheetCell that supports text data:

#include <string>
using std::string;

class SpreadsheetCell
{
    public:
        void setValue(double inValue);
        double getValue();
        void setString(string inString);
        string getString();

    protected:
        string doubleToString(double inValue);
        double stringToDouble(string inString);

        double mValue;
        string mString;
};

This version of the class stores both text and numerical representations of the data. If the client sets the data as a string, it is converted to a double, and a double is converted to a string. If the text is not a valid number, the double value is 0. This class definition shows two new methods to set and retrieve the text representation of the cell and two new protected helper methods to convert a double to a string and vice versa. These helper methods use string streams. Here are the implementations of all the methods:

#include "SpreadsheetCell.h"

#include <iostream>
#include <sstream>
using namespace std;

void SpreadsheetCell::setValue(double inValue)
{
    mValue = inValue;
    mString = doubleToString(mValue);
}

double SpreadsheetCell::getValue()
{
    return (mValue);
}

void SpreadsheetCell::setString(string inString)
{
    mString = inString;
    mValue = stringToDouble(mString);
}

string SpreadsheetCell::getString()
{
    return (mString);
}

string SpreadsheetCell::doubleToString(double inValue)
{
    ostringstream ostr;

    ostr << inValue;
    return (ostr.str());
}

double SpreadsheetCell::stringToDouble(string inString)
{
    double temp;

    istringstream istr(inString);

    istr >> temp;
    if (istr.fail() || !istr.eof()) {
        return (0);
    }
    return (temp);
}

Note that each of the set methods calls a helper method to perform a conversion. With this technique, both mValue and mString are always valid.

The this Pointer

Every normal method call passes a pointer to the object for which it is called as a "hidden" first parameter with the name this. You can use this pointer to access data members or call methods, and can pass it to other methods or functions. It is also sometimes useful for disambiguating names. For example, you could have defined the SpreadsheetCell class such that the setValue() method took a parameter named mValue instead of inValue. In that case, setValue() would look like this:

void SpreadsheetCell::setValue(double mValue)
{
    mValue = mValue; // Ambiguous!
    mString = doubleToString(mValue);
}

That line is confusing. Which mValue do you mean: the mValue that was passed as a parameter, or the mValue that is a member of the object? In order to disambiguate the names you can use the this pointer:

void SpreadsheetCell::setValue(double mValue)
{
    this->mValue = mValue;
    mString = doubleToString(this->mValue);
}

However, if you use the naming conventions described in Chapter 7, you will never encounter this type of name collision.

You can also use the this pointer to call a function or method that takes a pointer to an object from within a method of that object. For example, suppose you write a printCell() stand-alone function (not method) like this:

void printCell(SpreadsheetCell* inCellp)
{
    cout << inCellp->getString() << endl;
}

If you want to call printCell() from the setValue() method, you must pass this as the argument to give printCell() a pointer to the SpreadsheetCell on which setValue() operates:

void SpreadsheetCell::setValue(double mValue)
{
    this->mValue = mValue;
    mString = doubleToString(this->mValue);
    printCell(this);
}

Using Objects

The previous class definition says that a SpreadsheetCell consists of two member variables, four public methods, and two protected methods. However, the class definition does not actually create any SpreadsheetCells; it just specifies their format. In that sense, a class is similar to architectural blueprints. The blueprints specify what a house should look like, but drawing the blueprints doesn't build any houses. Houses must be constructed later based on the blueprints.

Similarly, in C++ you can construct a SpreadsheetCell "object" from the SpreadsheetCell class definition by declaring a variable of type SpreadsheetCell. Just as a builder can build more than one house based on a given blueprints, a programmer can create more than one SpreadsheetCell object from a SpreadsheetCell class. There are two ways to create and use objects: on the stack and on the heap.

Objects on the Stack

Here is some code that creates and uses SpreadsheetCell objects on the stack:

SpreadsheetCell myCell, anotherCell;
myCell.setValue(6);
anotherCell.setValue(myCell.getValue());

cout << "cell 1: " << myCell.getValue() << endl;
cout << "cell 2: " << anotherCell.getValue() << endl;

You create objects just as you declare simple variables, except that the variable type is the class name. The . in lines like myCell.setValue(6); is called the "dot" operator; it allows you to call methods on the object. If there were any public data members in the object, you could access them with the dot operator as well.

The output of the program is:

cell 1: 6
cell 2: 6

Objects on the Heap

You can also dynamically allocate objects using new:

SpreadsheetCell* myCellp = new SpreadsheetCell();

myCellp->setValue(3.7);
cout << "cell 1: " << myCellp->getValue() <<
    " " << myCellp->getString() << endl;
delete myCellp;

When you create an object on the heap, you call its methods and access its members through the "arrow" operator: ->. The arrow combines dereferencing (*) and method or member access (.). You could use those two operators instead, but doing so would be stylistically awkward:

SpreadsheetCell* myCellp = new SpreadsheetCell();

(*myCellp).setValue(3.7);
cout << "cell 1: " << (*myCellp).getValue() <<
    " " << (*myCellp).getString() << endl;
delete myCellp;

Just as you must free other memory that you allocate on the heap, you must free the memory for objects that you allocate on the heap by calling delete on the objects.

If you allocate an object with new, free it with delete when you are finished with it.