Starting with Objects
A general rule of good program design (not always true, but worth considering)
is that you should identify the objects in the real world, and make sure that
each has a proper representation in the program. In the Goldfish Club
(discussed presiously), the real world objects
are people, goldfish, the person-goldfish relationship, and the club's
membership list.
People and Goldfish are
solid unarguable objects, but some might argue about the relationship between
a goldfish and a person. We are not concerned philosophical arguments about
what makes an object. For us, an object is anything, concrete or abstract,
that has some kind of existence and is of importance to our program. The
connection between members of the Goldfish Club and their goldfish certainly
qualifies.
So we have four kinds
of real world objects. None of them correspond to software objects in our
program. Each person is certainly represented in the arrays, but there is
no one thing that can be indentified as the representation of a person.
We could say that the 73rd person in the club's records
is represented by the collection of ten items <memnum[73],
title[73], fname[73], lname[73], straddr[73], city[73], state[73],
zip[73], phone[73], birthday[73]>, but that is not a single object, and it isn't
even true anyway. If one person owns three goldfish, they will appear
in the list three times, so their "representation" is spread over a
number of ten-item groups. Imagine having to change a member's name
(when he or she gets married or joins a new religion); there is no one place
where the change could be recorded, and that is very strong evidence that
there is no one object representing the person.
All the other kinds of
objects are in the same position. The information that is required for the
object is present, but not in the form of a proper object. The main thing
that C++ and other object oriented languages give us, is the ability to
define proper objects.
In C++, we could define
new kinds of objects to represent people and goldfish quite easily. This is
the syntax:
class Person
{ public:
char *memnum;
char *title;
char *fname;
char *lname;
char *straddr;
char *city;
char *state;
char *zip;
char *phone;
char *birthday; };
| class Goldfish
{ public:
char *name;
char *species;
char *birthday;
char *birthyear;
char *favcolour; };
|
The introduction: "class Person" simply says that we are defining
a new class. Class is the C++ term for a "kind of object". The class called
Person will be the representation for all persons, not just one.
After these declarations, Person and Goldfish become new
types, just like int and float, and we will be able to
define variables that contain Persons just as we can define variables that
contain integers: "int x;" and "Person x;" are
both valid declarations, and mean more-or-less the same thing.
The curly brackets
are required to surround the parts that make up the class, otherwise
they would look like normal declarations of global variables. The word
"public" indicates that all of the parts about to be defined
are generally accessible. It is possible to protect parts against accidental
(or deliberate) changes by saying protected or private
instead, but that will come later. The rest of the definition of the class
is simply a list of normal variable declarations. They could have any types,
and any names. In this example they are all char * because
that's how they were in the previous example. It would be much more sensible
to make some of them (such as memnum, birthday, and
birthyear) into ints instead, and soon we will.
Those class definitions
do not define any variables at all. They do not create variables called
title or birthday or any of the others. They simply specify
that any time you create a variable of type Person, it will
contain within it, sub-variables called title, fname,
and so on. This little example shows what that means:
Person a, b;
a.title="Mrs";
a.fname="Semolina";
a.lname="Pudding";
a.straddr="221b Baker St";
a.city="Origami";
a.state="MT";
a.zip="83741";
a.phone="2839981243";
a.birthday="1225"; // i.e. 25th December
b=a;
b.title="Mr";
b.fname="Poindexter";
b.birthday="1031";
The only variables created in this example are a and b. Each
of them have the nine defined parts that can be accessed individually as
variables in their own right, but the object as a whole also has a real existence,
as illustrated by the assignment b=a; which copies all of a's
parts into b.
It is absolutely not
a problem that both Person and Goldfish have sub-variables
called birthday. We are only allowed to access a subvariable in
a context that makes the whole object obvious (e.g. in b.birthday="1031";
it is completely obvious that we are talking about b's
birthday and nobody else's), so no confusion can ever occur.
We can also have arrays of
objects:
Person people[100];
people[1]=a;
people[2]=b;
An object in an array is no different from an object in a variable of its own.
We can access its sub-variables, assign it, or do anything else:
for (i=1; i<=N; i+=1)
printf("Person %d is %s %s\n", i, people[i].title, people[i].lname);
We can even have pointers to objects. In fact, objects are nearly always accessed
through pointers, for reasons that will become clear later.
Person *p;
p=&a;
The normal syntax for accessing a sub-variable
through a pointer is quite ugly. First we have to apply * to the
pointer to find the object that it refers to, then we have to apply
. to the object to reach the correct sub-variable:
(*p).zip="65432";
so C gives us a combined notation
for accessing a sub-variable through a pointer:
p->zip="65432";
-> is an operator that combines
*'s pointer-following and .'s subvariable-accessing
into one.
Correct Terminology:
An Object is
a single self-contained lump of data, whose real existence is totally internal
to a program. a, b, and p in the above examples,
are not objects. a and b are variables that contain objects
(it is a very subtle distinction, but significant nonetheless);
p is a variable that points to an object.
A Class is
a data type that represents all possible objects of a given kind. The
relationship between Object and Class is the same as the relationship
between the number 3 and the type int.
A Member
is what I have been informally calling a sub-variable. It is simply one of
the named individual parts that make up an object or a class.
Now that we have good solid software objects to represent at least two of
our real world objects, we would be sensible to start designing some
functions to make programming with them more convenient. There are certain
operations that are definitely going to be performed very frequently.
For example, Printing a person's or a goldfish's details is probably going to be
a very common task, so we would probably want to write:
void print(Person p)
{ printf("%s %s %s of %s\n", p.title, p.fname, p.lname, p.city); }
void print(Goldfish g)
{ printf("%s, a %s born is %s\n", g.name, g.species, g.birthyear); }
And we would instantly see that there is going to be some competition for
the most popular function names. They can't both be called print
(actually in C++ they can, but that is beside the point). C++ solves this
problem by allowing functions to be members of classes, just like
variables. A function that is a member of a class is properly called
a Method of that class. To illustrate this, we'll redefine
the two classes:
class Person
{ public:
char *memnum;
char *title;
char *fname;
char *lname;
char *straddr;
char *city;
char *state;
char *zip;
char *phone;
char *birthday;
void print(void); };
| class Goldfish
{ public:
char *name;
char *species;
char *birthday;
char *birthyear;
char *favcolour;
void print(void); };
|
void Person::print(void)
{ printf("%s %s %s of %s\n",
title, fname, lname, city); }
| void Goldfish::print(void)
{ printf("%s, a %s born is %s\n",
name, species, birthyear); }
|
Whenever you want to put a method in a class, just put a prototype
for the function inside the class definition, and a proper complete
definition somewhere outside. The prototype "void print(void);"
inside Person's definition is simply a promise that Person's
own version of print will be defined at some point in the future.
When that point in the future is reached, and we are ready to define the
function, we have to make it clear which version of print is
being defined. That is why the Person:: appeared in front of the
name.
In fact, Person::
isn't in front of the name, it is really part of the name. All members
(and methods) of a class can only be accessed in a context that makes it clear
exactly which object they are members of. Remember we have to say
"a.fname"; just "fname" would not be allowed. Similarly,
to print something, we aren't allowed to just say "print()", we must
make it clear whose print method is being used: "a.print()".
Whenever there is no object handy to make the distinction clear, we can use
the member's full name instead; fname is just an abbreviation
for the full name Person::fname.
A method can only
be called when it is clear which object's method is meant. That means that
a method will only ever run when it is known which object it belongs to.
That means that there is no need specify an object again inside
a method. It is ok to say fname and lname alone.
Here's how it works:
Person her;
her.title="Miss";
her.fname="Frogalina";
her.lname="Sprocket";
her.city="Ouagadougou";
her.print();
The statement her.print() means call Person::print, and give
it the object her to work on, so inside print, any access
to fname is automatically considered to be an access to
her.fname, and everything works nicely.
Methods are always called
in connection with a particular object, and they always have direct access
to that object's members. That is the real point of a class method.
Another important
concern is that all members of an object should be properly initialised before
they are used. Not all members necessarily need initialisation, but some do.
Suppose we failed to assign a string to her.city before calling
her.print(). The function would have no way of knowing that anything
is amiss, and it would attempt to print city as a string. Anything
could happen, an uninitialised string is just a random pointer somewhere
in memory.
C++ gives us a way
to ensure that all important members are initialised automatically every
time a new object is created. This is done by defining a Constructor.
A constructor is just a normal method or function with two special
attributes. One is that it must have exactly the same name as the class
that it belongs to; the other is that it must not have a return type, not
even void. To add constructors to our two classes, we would
add simple prototypes to the class definitions. Goldfish has
this line added to it:
Goldfish(void);
and Person has this line added to it:
Person(void);
Then outside the class definitions, we provide the complete definitions
for the two constructors. Remember that all we have to do is ensure that
all the members are put in safe initial states; we don't need to put any
useful information anywhere:
Goldfish::Goldfish(void)
{ name=""; species="";
birthday=""; birthyear="";
favcolour=""; }
Person::Person(void)
{ memnum="0"; title="Mr"; fname="No"; lname="Name";
birthday=""; straddr=""; city=""; state=""; zip="";
phone=""; }
These are very boring functions. The point is that they make programs
safe. Every time an object is created, no matter how, its constructor is
called. There is nothing you can do to prevent the constructor from
being called. In fact, you are not allowed to create objects if their
class hasn't got a constructor to call.
So if we say:
Person him;
him.title="Prof";
him.fname="Wally";
him.lname="Drab";
him.print();
even though we forgot to give a value to city, nothing would go
wrong, because the constructor has already given it a default safe value.
It is a bit wasteful
to do all those assignments in the constructor if we haven't forgotten
anything, because the very next statements will be assigning new values to
those same variables. We could define an alternative constructor that is
provided with parameters to tell it useful initial values to give to the members.
This is how it would
look:
Person::Person(char *mn, char *tt, char *fn, char *ln)
{ memnum=mn; title=tt; fname=fn; lname=ln;
birthday=""; straddr=""; city=""; state=""; zip="";
phone=""; }
This constructor has four parameters, all strings. It can only
be called if four string parameters are provided. That's no problem. When
this sort of constructor is in use, declarations look like this:
Person a("1231","Ms","Henrietta","Chicken");
Person b("1232","Mr","Ivan","Itch");
The declarations look like function calls, which is exactly what they are.
Variable declarations that look like this work just like any other
variable declaration; they just allow parameters to be passed to the
constructor.
Surprisingly, we can
have both kinds of constructor, and even more. So long as all the constructors
for any class can be distinguished by their parameters (either by having
a different number of parameters, or by having the same number but with
different types) a class can have as many different constructors as you like,
For an example, I'll
give complete definitions for Person and Goldfish, showing
exactly how they would appear in a program. We'll have three constructors
for a Person, one to be used when we have no useful information
yet, one for when we already know the person's name, and one to be used
when we know their address too.
class Person
{ public:
char *memnum, *title, *fname, *lname;
char *straddr, *city, *state, *zip;
char *phone, *birthday;
void print(void);
void print_address_label(void);
Person(void);
Person(char *mn, char *tt, char *fn, char *ln);
Person(char *mn, char *tt, char *fn, char *ln, char *sa,
char *ci, char *st, char *zi, char *ph, char *bi); };
class Goldfish
{ public:
char *name, *species, *birthday, *birthyear, *favcolour;
void print(void);
Goldfish(void);
Goldfish(char *nm);
Goldfish(char *nm, char *sp, char *bd, char *by, char *fc); };
void Person::print(void)
{ printf("%s %s %s of %s\n", title, fname, lname, city); }
void Person::print_address_label(void)
{ printf("%s %c %s\n", title, fname[0], lname);
printf("%s\n", straddr);
printf("%s, %s %s\n", city, state, zip); }
Person::Person(void)
{ memnum="0"; title=""; fname="No"; lname="Name";
straddr=""; city=""; state=""; zip="";
phone=""; birthday=""; }
Person::Person(char *mn, char *tt, char *fn, char *ln)
{ memnum=mn; title=tt; fname=fn; lname=ln;
straddr=""; city=""; state=""; zip="";
phone=""; birthday=""; }
Person(char *mn, char *tt, char *fn, char *ln, char *sa,
char *ci, char *st, char *zi, char *ph, char *bi)
{ memnum=mn; title=tt; fname=fn; lname=ln;
straddr=sa; city=ci; state=st; zip=zi;
phone=ph; birthday=bi; }
void Goldfish::print(void)
{ printf("%s, a %s, born in %s\n", name, species, birthyear); }
Goldfish::Goldfish(void)
{ name=""; species="";
birthday=""; birthyear=""; favcolour=""; }
Goldfish::Goldfish(char *nm)
{ name=nm; species="";
birthday=""; birthyear=""; favcolour=""; }
Goldfish(char *nm, char *sp, char *bd, char *by, char *fc)
{ name=nm; species=sp;
birthday=bd; birthyear=by; favcolour=fc; }
With these definitions in force, a program could correctly use all of
the following. In every case, it can tell which constructor to call by
the type of the variable being created and the number and types of the
parameters provided:
Person one("8239","Dr","Vera","Crooked");
Person two("12312","Miss","Jellybean","Jones","11 A Av","Aack","AK","12345","1234567");
Person three;
Person four("9239","Uncle","Horace","Horse");
Goldfish x("Goldie");
Goldfish y("Yellowie");
Goldfish z("Fluffy","Himalayan Blue","0231","1865","Purple");
Person *p;
p=&two;
One final observation about the last two lines. Constructors are called
only when an object is created. The declaration Person *p creates
a pointer that can point to an object, but it does not create an object. It is
exactly the same as with all other pointer declarations. Declaring a pointer
never creates an object, so it never causes a constructor to be called, so
you are never allowed to provide parameters. The very last statement makes
p point to an object that has already been brough into existence
(seven lines before), so of course no constructor is called.
This is just the beginning. If things don't seem quite right, that may be
because you haven't seen everything yet. Don't worry about it. This is
quite enough new stuff for one week; we'll get to the rest soon enough.