next up previous contents
Next: Pointers Up: Getting Started in C++ Previous: References   Contents

Subsections

Classes

As you have seen in 7.1, structs are a very nice way to create new data types. And as you have seen in 7.1.2, it is possible to write functions like read_wizard and print_wizard to access them in a simple way.

Fortunately there is also another way to create new types in C++, which is much more powerful and simple: classes.

A class is declared just like a struct; thus, we could declare Wizard as follows:

class Wizard {
    string name;
    int age;
};
Notice that the semicolon after the closing brace is, just like in a struct, not an optional.

Member Scope

There is one big difference between the struct Wizard and the class Wizard: the scope of the members. While in the struct -- which has so-called public scope -- one could access the members using the dot operator (wizard.age, for example), this is not possible in the class, which has private scope. In fact, it is not possible to access the members at all from outside the class.

The problem is that each member of a struct or a class has either public, private or protected scope. The public members can be accessed from anywhere. The private members can be accessed only from inside the class. The protected members are somehow in between, but we'll talk about them later; you don't need them yet.

By default (if you don't specify anything else) the members of a class are private. If you wanted the class Wizard to be perfectly analogous to the struct, you'd have to declare it like this:

class Wizard {
public: // Whatever follows this is public
    string name;
    int age;
};
If you wanted to declare one member private and one public, you could do it like this:
class Wizard {
public:
    string name;
private:
    int age;
};
or, using the fact that the default is private, simply like this:
class Wizard {
    int age;
public:
    string name;
};

Member Functions

Let's go back to our good old Wizard now. Its original declaration was

struct Wizard {
    string name;
    int age;
};
As it is a struct, all members are public. This means that every programmer getting his hands on a Wizard object can change its age and name as it likes. This is of course a Bad Thing. To avoid that, we could declare Wizard as a class, like this:
class Wizard {
    string name;
    int age;
};
This way, all members being private, no function could mess with our poor wizard. The problem is that no function could access any of its data either; in fact, it wouldn't even be possible to give the name and the age meaningful values.

What we need are some privileged functions which can access the wizards interior; we know those functions won't harm our wizard, and all the other programmers could use those functions to do whatever they need to do with the wizard.

That's exactly what member functions are. They are functions which belong to the class, and can thus access all of its members -- public, private and protected -- without any restrictions. The read_wizard function could be easily implemented as a member function as follows (we call it only read because, being part of Wizard, it is obvious that it refers to wizards and not to witches):

class Wizard {
    // The default is private, so we don't need to 
    // specify
    string name;
    int age;
public:
    void read() {
        cout << "Name: ";
        cin >> name;
        cout << "Age: ";
        cin >> age;
    }
};
Before you start wondering about why read does not specify which wizard name and age refer to, look at how member functions are called:
Wizard gandalf;
gandalf.read();
Now it should be clear. The members name and age used in read automatically refer to gandalf.name and gandalf.age because read was called through gandalf. If instead of gandalf.read() you had called sauron.read() they would have referred to sauron.name and sauron.age.

Constant Member Functions

In 8.2.2 I pointed out why it is important to use a const reference (instead of a non-const one) when a function does not modify an argument. With member functions the same problem arises: some of them, as read() may modify their object, others, as for example print(), do not.

As we do not explicitly specify the reference to the object which is modified, it is not possible to use the same method anymore. Instead of that, the const keyword is appended after the closing parenthesis of the function declaration, like this:

class SomeClass {
    // ...
    void this_function_does_not_modify_the_object() const {
        // ...
    }
    void this_function_modifies_the_object() {
        // ...
    }
};
You cannot modify the members of an object in a const member function, but they are the only type of member function you can call on a constant object. If print is a const member function and read isn't, you can call print on a constant object but not read.

This means that if, for example, you have a function f taking a const reference to a wizard, you can only call the const member functions of that wizard inside the function, as follows:

#include <iostream> // for cin and cout
#include <string>
using namespace std;

class Wizard {
public:
    void read() {
        cout << "Name: ";
        cin >> name;
        cout << "Age: ";
        cin >> age;
    }
    void print() const {
        cout << "Name: " << name << "\nAge: " << age
             << '\n';
    }
private:
    string name;
    int age;
};

void f(const Wizard& wizard) {
    wizard.print(); // This is ok
    
    // wizard.read();  // ERROR!
                       // You cannot modify wizard!
}

int main() {
    Wizard some_wizard;
    some_wizard.read();
    f(some_wizard);
}

Const Correctness

Obviously, it would not be possible to declare Wizard::read() as const because it modifies the Wizard object. (Notice how I wrote Wizard::read(). In C++, that's the way to refer to a member function; it means ``the read() member function of the Wizard class''). But it would be possible to define Wizard::print() as non-const, even if it does not modify the Wizard object. If you did so, you could not even print a constant Wizard; in other words, even the line wizard.print() in the example program above would be an error.

Declaring functions which do not modify the object as const is thus important; if you don't, you won't be able to use them in const objects. This is known as const correctness, and it is much more important than you might think right now.

Encapsulation

The technique of declaring the member functions as public and the member variables as private, thus not allowing anybody to mess with them directly, is called encapsulation. Generally it is a sign of a good design; this does of course not mean that any public member variable is bad and should be wrapped into member functions. There are cases where that wouldn't make any sense.

Declaring member functions to access the content of a class has many advantages; one of them is that it is easy to change the way the data is actually stored inside the class; if you allow everyone to access the member variables directly, this is not possible any more. It may seem more work at the beginning, but trust me, in the end you'll be glad if you did so.

For example, imagine a word processor written in C++, which has class Document storing, well, documents. Imagine that the Document class has a member variable of type string called text to store the text, and some other member variables to store all the things which the text editor must know about the text (like the author, the creation date, and so on). Suppose that the Document class has also a member function called save().

Now, if you have not encapsulate the variables, everytime save() is called you have to write the whole document to the hard disk, even if it was not modified because you cannot easily know whether it was changed or not. After all, any part of the outside code could have changed something. (It would of course be possible to calculate some kind of checksum everytime the document was saved, and recalculating it when save() is called and comparing it to the previous checksum, but that would just be overkill).

If you have encapsulated the variables, everything becomes much easier. For example, you have a const member function called get_author() to retrieve the author's name, and one (non-const, of course) called set_author() to change it. All you have to do now is adding a single variable of type bool to the class, called changed. (A bool variable can only have two values: true or false). Now, if set_author() (or any other member function modifying the class) is called you set the changed to true; so in save() you can easily check whether the document was changed.

void Document::save() {
    if (changed) { // equivalent to "if (changed == true)"
        // save the document
        // ...
        changed = false;
    }
}

Or, another more realistic example. Suppose you have written a very complex algorithm to do special searches on a list of objects, which is represented by an instance of the class List, which stores all objects as an array accessible through List::objects. Your algorithm will use statements such as some_list.objects[n] to access the different objects. But as the number of objects grows larger and larger, you realize that it is not possible to keep all of them in memory at the same time. Thus you decide to change the implementation of List so that it only keeps a small set of objects in memory, and leaves the rest on disk, and automatically loads the necessary objects if the algorith requests them. Now you have a problem, because there is no way you can do that without changing the interface of List. You'll have to adapt or at least recompile the algorithm. If you had used proper encapsulation and defined a function such as List::get_object(int index) to retrieve the objects, you could just change its implementation without touching the algorithm.

Structs vs Classes

You might wonder now why I even bothered introducing structs, if everything a struct can do can be achieved through a class with a public: at the beginning.

In C++ the difference between a struct and a class is only the default scope of the members: public in structs and private in classes. They are interchangeable for all the rest: structs can have member functions and everything else a class can.

The programming language C had only structs and no classes. Those structs did not allow member functions and scope declarations; they were used only as described in 7.1. C++, which was initially only an enhanced C, kept the structs and introduced the classes, which were, at least at the beginning, not much different from the structs. Classes evolved to what they are now, and, as it was easiest for the compiler writers, struct became an alias for class, with the default scope as only difference.

Of course you can do what you like, but I'd recommend you to follow the rest of the world and use structs when you use none of the features of C++ features (as scope, and member functions), and classes for everything else. In other words structs are only for plain variable groups with no member functions.


next up previous contents
Next: Pointers Up: Getting Started in C++ Previous: References   Contents
Aaron Isotton <aaron@isotton.com>
2003-02-24