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.
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.
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";
}
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.