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

Subsections

Structs

Sometimes it is possible to store all information about something in an already available type; sometimes, it isn't. For example, a number can be easily stored in an int or a double, and a string can be stored in a string. But this is not possible for more complex objects. For example, in 3.1.1 we made the example of a type to store data about wizards in. Of course, there is no such type, and a simple type as int or string is not sufficient, because a wizard has a name, an age, and many other interesting things which we will not consider for the sake of simplicity.


struct

The simplest way to create a ``complex'' data type is using a structure, or simply struct (after the keyword struct used to declare one). Strictly speaking, in C++ it is not possible to create a new type from the ground up, but only to group already existing types.

If we want to create a Wizard data type, first we must think about what we want to store in it. To keep it simple, we will only store the wizard's name and his age. As we can use only already existing types, we'll take a string for the name and an int for the age.

Suppose we want three wizard objects: Gandalf, Sauron and Saruman. Without introducing a new data type, we'd do it somehow like this:

string gandalf_name;
int gandalf_age;

string sauron_name;
int sauron_age;

string saruman_name;
int saruman_age;
This is of course very tedious. And if we wanted to add some other data to each wizard -- for example his height -- we'd have to add it manually to each of them. This is of course no way to go. And that's why there are structs.
struct Wizard {
    string name;
    string age;
};
This defines the new type Wizard, which is a type just as int or string. Thus, we can declare objects of this new type, like this:
Wizard gandalf, sauron, saruman;
Notice that it is a good idea to use some convention to distinguish types from objects; I will always write objects like_this and types LikeThis. This has the advantage that it is possible to declare an object with the same name as a type, which would otherwise not be possible. (For example, if we had called our new type wizard instead of Wizard, we could not declare an object named wizard too.)

But what can we do with these objects? Not very much, indeed. We cannot assign to them, we cannot compare them, we cannot print them and we cannot read data into them from the console. We can do only two things: declaring them and accessing their members.

Member Access

A member of a struct is one of the objects declared inside it. The members of Wizard are name and age. They can be accessed using the dot operator (object.member), as follows:

gandalf.name = "Gandalf";
gandalf.age = 123;
cout << gandalf.name << " is " << gandalf.age << " years old.\n";
An object inside a struct is treated just as any other object; you can do exactly the same things with it as you can with a ``normal'' one.

Here a complete program doing all the things I've explained until now:

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

int main() {
    struct Wizard {
        string name;
        int age;
    };

    Wizard gandalf;
    gandalf.name = "Gandalf";
    gandalf.age = 123;

    cout << gandalf.name << " is " << gandalf.age << " years old.\n";
}


Helper Functions

In the previous example you might have wondered about why these structs are so important; in fact, you might have written the same program without them, and it would even have been a bit shorter.

The magic of objects, in fact, isn't that you can group smaller entities into new types and then access these entities using the dot operator. Its magic is modularization. This means that it is easy to split a program into different parts (called modules) which are more or less independent of each other.

Modularization is no simple business; one step towards it, though, are helper functions: functions which deal with a certain type of object. For example, we might want to print all we know about a wizard on the screen. One way is doing it the way we've done it above; another -- better -- way is to put all the printing into a separate function, and call it whenever we need it, like this:

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

struct Wizard {
    string name;
    int age;
};

void wizard_print(Wizard wizard) {
    cout << wizard.name << " is " << wizard.age << " years old.\n";
}

int main() {
    Wizard gandalf, sauron, saruman;

    gandalf.name = "Gandalf";
    gandalf.age = 123;

    sauron.name = "Sauron";
    sauron.age = 234;

    saruman.name = "Saruman";
    saruman.age = 345;

    wizard_print(gandalf);
    wizard_print(sauron);
    wizard_print(saruman);
}
As you can see, now it makes much more sense. If, for example, we want to print Gandalf, 123 and so on instead of Gandalf is 123 years old. we only need to change one function, and can leave the rest alone.

It would be nice if we could write functions to change a Wizard as well (like for example input_wizard to ask for a wizard's name and age) but for the time being we cannot do that. As I pointed out in 6.7, function arguments are passed by value; thus if we wrote a function like this:

void input_wizard(Wizard wizard) {
    cout << "Please enter the name of the wizard: ";
    getline(cin, wizard.name);
    cout << "Please enter " << wizard.name << "'s age: ";
    cin >> wizard.age;
}
would not work as expected. If you called it like this:
input_wizard(gandalf);
the compiler would make a copy of gandalf and pass it to the function. The function would then read in the data into the copy, without modifying the original. We'll talk about that again in 8.
next up previous contents
Next: References Up: Getting Started in C++ Previous: Functions   Contents
Aaron Isotton <aaron@isotton.com>
2003-02-24