In this chapter the usage of C++ is further explored. The possibility
to declare functions in structs is further illustrated using
examples. The concept of a class is introduced.
Before we continue with the `real' object-oriented approach to programming, we first introduce further extensions of C++.
In analogy to C, C++ defines standard input- and output streams which are opened when a program is executed. The streams are:
cout, analogous to stdout,
cin, analogous to stdin,
cerr, analogous to stderr. Syntactically these streams are not used with functions: instead, data are
read from the streams or written to them using the operators
<< and >>. This is illustrated in the
example below:
#include <iostream.h>
int main ()
{
int
ival;
char
sval [30];
cout << "Enter a number:" << '\n';
cin >> ival;
cout << "And now a string:" << '\n';
cin >> sval;
cout << "The number is: " << ival
<< "\nAnd the string is: " << sval
<< '\n';
return (0);
}
This program reads a number and a string from the cin stream
(usually the keyboard) and prints these data to cout. Concerning
the streams and their usage we remark the following:
iostream.h.
cout, cin and cerr are
in fact `objects' of a given class (more on classes later) which processes the
input and output of a program. Note that the terminology `object' means here:
the set of data and functions which define the item in question.
cin reads data and copies the information to
variables (e.g., ival in the above example) using the right-shift
operator >>. We will describe later how operators in
C++ can perform quite other actions than what they are defined to do by
the language grammar, such as is the case here.
cin, cout and
cerr (which are >> and <<)
also manipulate variables of different types. In the above example
cout << ival stands for the
printing of an integer value. The actions of the operators therefore depend on
the type of supplied variables. The streams cin, cout and cerr are not
part of C++ grammar sec, as defined in the compiler which parses
source files. The streams are part of the definitions in the header file
iostream.h. This is comparable to the fact that functions as
printf() are not part of the C grammar, but were originally
written by people who considered such functions handy and collected them in a
run-time library.
Whether a program uses the old-style functions like printf() and
scanf() or whether it employs the new-style streams is a matter of
taste. Both styles can even be mixed. A number of advantages and disadvantages
is given below:
printf() and
scanf(), the usage of the operators << and
>> with their respective streams is more type-safe.
The format strings which are used with printf() and
scanf() can define wrong format specifiers for their arguments,
for which the compiler (usually) doesn't warn. In contrast, argument checking
with cin, cout and cerr is performed by
the compiler.
printf() and scanf(), and other
functions which use format strings, in fact implement a mini-language which is
interpreted at run-time. In contrast, the C++ compiler knows exactly
which in- or output action to perform given which argument.
The keyword const very often occurs in C++ programs, even
though it is also part of the C grammar.
This keyword is a modifier which states that the value of a variable or of an
argument may not be modified. In the below example an attempt is made to change
the value of a variable ival, which is not legal:
int main ()
{
int const // a constant int..
ival = 3; // initialized to 3
ival = 4; // assignment leads
// to an error message
return (0);
}
This example shows how ival may be initialized to a given value
in its definition; attempts to change the value later (in an assignment) are not
permitted.
Variables which are declared const can, in contrast to C,
be used as the specification of the size of an array, as in the following
example:
int const
size = 20;
char
buf [size]; // 20 chars big
A further usage of the keyword const is seen in the declaration
of pointers, e.g., in pointer-arguments. In the declaration
char const *buf;
buf is a pointer variable, which points to chars.
Whatever is pointed to by buf may not be changed: the
chars are declared as const. The pointer
buf itself however may be changed. A statement as *buf
= 'a' is therefore not allowed, while
buf++ is.
In the declaration
char *const buf;
buf itself is a const pointer which may not be
changed. Whatever chars are pointed to by buf may be
changed at will.
Finally, the declaration
char const *const buf;
is also possible; here, neither the pointer nor what it points to may be changed.
The rule of thumb for the placement of the keyword const is the
following: whatever occurs just prior to the keyword may not be changed.
Besides the normal declaration of variables, C++ allows `references' to be declared as synonyms for variables. A reference to a variable is like an alias; the variable name and the reference name can both be used in statements which affect the variable:
int
int_value;
int
&ref = int_value;
In the above example a variable int_value is defined.
Subsequently a reference ref is defined, which due to its
initialization addresses the same memory location which int_value
occupies. In the definition of ref, the reference operator
& indicates that ref is not itself an integer but
a reference to one. The two statements
int_value++; // alternative 1
ref++; // alternative 2
have the same effect, as expected. At some memory location an
int value is increased by one --- whether that location is called
int_value or ref does not matter.
References serve an important role in C++ as a means to pass arguments
which can be modified (`variable arguments' in Pascal-terms). E.g., in standard
C, a function which increases the value of its argument by five but which
returns nothing (void), needs a pointer argument:
void increase (int *valp) // expects a pointer
{ // to an int
*valp += 5;
}
int main ()
{
int
x;
increase (&x) // the address of x is
return (0); // passed as argument
}
This construction can naturally also be used in C++ but the same effect can be achieved with a reference:
void increase (int &valr) // expects a reference
{ // to an int
valr += 5;
}
int main ()
{
int
x;
increase (x); // a reference to x is
return (0); // passed as argument
}
The way in which C++ compilers implements references is by internally
using pointers: in other words, references in C++ are just ordinary
pointers -- as far as the compiler is concerned. However, the programmer does
not need to know or to bother about levels of indirection. (var is in fact
also a pointer, but the programmer needn't know.
It can be argued whether code such as the above is clear: the statement
increase (x) in the main() function
suggests that not x itself but a copy is passed. Yet the
value of x changes because of the way increase() is
defined.
Our suggestions for the usage of references as arguments to functions are therefore the following:
void some_func (int val)
{
printf ("%d\n", val);
}
int main ()
{
int
x;
some_func (x); // a copy is passed, so
return (0); // x won't be changed
}
void increase (int *valp)
{
*valp += 5;
}
int main ()
{
int
x;
increase (&x); // a pointer is passed, so
return (0); // x might be changed
}
struct, is
passed as argument: in this case the stack usage becomes significant when a
copy of the entire structure is passed.
This is illustrated below:
struct person // some large structure
{
char
name [80],
address [90];
double
salary;
};
void printperson (person &p) // printperson expects a
{ // reference to a structure
printf ("Name: %s\n"
"Address: %s\n",
p.name, p.address);
}
int main ()
{
person
boss;
.
.
printperson (boss); // no pointer is passed,
} // so variable won't be
// altered by function
An improvement is the addition of the keyword const:
void printperson (person const &p)
After such a declaration of the function printperson() no
ambiguity exists.
References also can lead to extremely `ugly' code. A function can also return a reference to a variable, as in the following example:
int &func (void)
{
static int
value;
return (value);
}
This allows the following constructions:
func () = 20;
func () += func ();
It is probably superfluous to note that such constructions should not normally be used (however, there are situations where it is useful to return a reference, this is discussed later). C++ provides as far as references are concerned a powerful tool, which however can mutilate the code.
A number of differences between pointers and references is pointed out in the list below:
int &ref;
is not allowed; what would ref refer
to? An exception to this rule are references which are declared as
external (these references are assumed to be initialized
elsewhere), references as arguments of functions (the caller then initializes
the reference), references as return types of functions (the function
determines to what the return value will refer) and references as data members
of classes (we will describe this later). In contrast, pointers are variables
by themselves. They point, whether at something concrete or just ``at
nothing''.
& is used on a reference, the
expression yields the address of the variable to which the reference applies.
In contrast, ordinary pointers are variables themselves, so the address of a
pointer variable has nothing to do with the address of the variable pointed
to. The first chapter described that functions can be part of
structs (see section FunctionInStruct
). Such functions are called `member' functions. This section discusses the
actual definition of such functions.
The code fragment below illustrates a struct in which data
fields for a name and address are present. A function print() is
included in the struct definition:
struct person
{
char
name [80],
address [80];
void
print (void);
}
The member function print() is defined using the structure name
(person) and the scope resolution operator (::):
void person::print ()
{
printf ("Name: %s\n"
"Address: %s\n", name, address);
}
In the definition of this member function, the function name is preceded by
the struct name followed by ::. The code of the
function shows how the fields of the struct can be addressed
without using the type name: in this example the function print()
prints a variable name. Since print() is a part of the
struct person, the variable name
implicitly refers to the same type.
The usage of this struct could be, e.g.:
person
p;
strcpy (p.name, "Karel");
strcpy (p.address, "Rietveldlaan 37");
.
.
p.print ();
The advantage of member functions lies in the fact that the called function
can automatically address the data fields of the structure for which it was
invoked. As such, in the statement p.print() the structure
p is the `substrate': the variables name and
address which are used in the code of print() refer to
the same struct p.
As mentioned previously (see section Pretensions ), C++ contains special syntactical possibilities to implement data hiding. Data hiding is the ability of one program part to hide its data from other parts; thus avoiding improper addressing or name collisions of data.
C++ has two special keywords which are concerned with data hiding:
private and public. These keywords can be inserted in
the definition of a struct. The keyword public defines
all subsequent fields of a structure as accessible by all code; the keyword
private defines all subsequent fields as only accessible by the
code which is part of the struct (i.e., only accessible for the
member functions) (public and private,
C++ defines the keyword protected. This keyword is not often
used and it is left for the reader to explore.struct
all fields are public, unless explicitly stated otherwise.
With this knowledge we can expand the struct
person:
struct person
{
public:
void
setname (char const *n),
setaddress (char const *a),
print (void);
char const
*getname (void),
*getaddress (void);
private:
char
name [80],
address [80];
};
The data fields name and address are only
accessible for the member functions which are defined in the
struct: these are the functions setname(),
setaddress() etc.. This property of the data type is given by the
fact that the fields name and address are preceded by
the keyword private. As an illustration consider the following code
fragment:
person
x;
x.setname ("Frank"); // ok, setname() is public
strcpy (x.name, "Knarf"); // error, name is private
The concept of data hiding is realized here in the following manner. The
actual data of a struct person are named only in the
structure definition. The data are accessed by the outside world by special
functions, which are also part of the definition. These member functions control
all traffic between the data fields and other parts of the program and are
therefore also called `interface' functions.
The data hiding which is thus realized is further illustrated in the following figure:
Also note that the functions setname() and
setaddress() are declared as having a char
const * argument. This means that the functions will
not alter the strings which are supplied as their arguments. In the same vein,
the functions getname() and getaddress() return a
char const *: the caller may not modify
the strings which are pointed to by the return values.
Two examples of member functions of the struct
person are shown below:
void person::setname (char const *n)
{
strncpy (name, n, 79);
name [79] = '\0';
}
char const *person::getname ()
{
return ( (char const *) name );
}
In general, the power of the member functions and of the concept of data
hiding lies in the fact that the interface functions can perform special tasks,
e.g., checks for the validity of data. In the above example
setname() copies only up to 79 characters from its argument to the
data member name, thereby avoiding array boundary overflow.
Another example of the concept of data hiding is the following. As an
alternative to member functions which keep their data in memory (as do the above
code examples), a runtime library could be developed with interface functions
which store their data on file. The conversion of a program which stores
person structures in memory to one that stores the data on disk
would mean the relinking of the program with a different library.
Though data hiding can be realized with structs, more often
(almost always) classes are used instead. A class is in principle
equivalent to a struct except that unless specified otherwise, all
members (data or functions) are private. As far as
private and public are concerned, a class
is therefore the opposite of a struct. The definition of a
class person would therefore look exactly as shown
above, except for the fact that instead of the keyword struct,
class would be used. Our typographic suggestion for class names is
a capital as first character, followed by the remainder of the name in lower
case (e.g., Person).
At the end of this chapter we would like to illustrate the analogy between
C and C++ as far as structs are concerned. In
C it is not uncommon to define several functions to process a
struct, which then require a pointer to the struct as
one of their arguments. A fragment of an imaginary C header file is given
below:
/* definition of a struct PERSON_ */
typedef struct
{
char
name [80],
address [80];
} PERSON_;
/* some functions to manipulate PERSON_ structs */
/* initialize fields with a name and address */
extern void initialize (PERSON_ *p, char const *nm,
char const *adr);
/* print information */
extern void print (PERSON_ const *p);
/* etc.. */
In C++, the declarations of the involved functions are placed inside
the definition of the struct or class. The argument
which denotes which struct is involved is no longer needed.
class Person
{
public:
void initialize (char const *nm, char const *adr);
void print (void);
// etc..
private:
char name [80], address [80];
};
The struct argument is implicit in C++. A function call
in C like
PERSON_
x;
initialize (&x, "some name", "some address");
is in C++:
Person
x;
x.initialize ("some name", "some address");
Next Chapter, Previous
Chapter,Home