Header
file etiquette
When
you create a
struct
containing member functions, you are creating a new data type. Generally, you
want this type to be easily accessible to yourself and others. In addition, you
want to separate the interface
(the declaration) from the implementation
(the definition of the member functions) so the implementation can be changed
without forcing a re-compile of the entire system. You achieve this end by
putting the declaration for your new type in a header file.
When
I first learned to program in C, the header file was
a mystery to me. Many C books don’t seem to emphasize it, and the
compiler didn’t enforce function declarations, so it seemed optional most
of the time, except when structures were declared. In C++ the use of header
files becomes crystal clear. They are almost mandatory for easy program
development, and you put very specific information in them: declarations. The
header file tells the compiler what is available in your library. You can use
the library even if you only posess the header file along with the object file
or library file – you don’t need the source code for the
cpp
file. The header file is where the interface specification is stored.
Although
it is not enforced by the compiler, the best approach to building large
projects in C is to use libraries: collect associated functions into the same
object module or library, and use a header file to hold all the declarations
for the functions. It is
de
rigueur
in C++: you could throw any function into a C library, but the C++ abstract
data type determines the functions that are associated by dint of their common
access to the data in a
struct.
Any member function must be declared in the
struct
declaration;
you cannot put it elsewhere. The use of function libraries was encouraged in C
and institutionalized in C++.
Importance
of header files
When
using a function from a library, C allows you the option of ignoring the header
file and simply declaring the function by hand. In the past, people would
sometimes do this to speed up the compiler just a bit by avoiding the task of
opening and including the file (this is usually not an issue with modern
compilers). For example, here’s an extremely lazy declaration of the C
function
printf( )
(from
<stdio.h>): The
ellipses specify a
variable
argument list
[24],
which says:
printf( )
has some arguments, each of which has a type, but ignore that. Just take
whatever arguments you see and accept them. By using this kind of declaration,
you suspend all error checking on the arguments.
This
practice can cause subtle problems. If you declare functions by hand, in one
file you may make a mistake. Since the compiler only sees your hand-declaration
in that file, it may be able to adapt to your mistake. The program will then
link correctly, but the use of the function in that one file will be faulty.
This is a tough error to find, and is easily avoided by using a header file.
If
you place all your function declarations in a header file, and include that
header everywhere you use the function and where you define the function, you
ensure a consistent declaration across the whole system. You also ensure that
the declaration and the definition match by including the header in the
definition file.
If
a
struct
is declared in a header file in C++, you
must
include the header file everywhere a
struct
is used and where
struct
member functions are defined. The C++ compiler will give an error message if
you try to call a regular function, or call or define a member function,
without declaring it first. By enforcing the proper use of header files, the
language ensures consistency in libraries, and reduces bugs by forcing the same
interface to be used everywhere.
The
header is a contract between you and the user of your library. The contract
describes your data structures, and states the arguments and return values for
the function calls. It says, “Here’s what my library does.”
The user needs some of this information to develop the application and the
compiler needs all of it to generate proper code. The user of the
struct
simply includes the header file, creates objects (instances) of that
struct,
and links in the object module or library (i.e.: the compiled code).
The
compiler enforces the contract by requiring you to declare all structures and
functions before they are used and, in the case of member functions, before
they are defined. Thus, you’re forced to put the declarations in the
header and to include the header in the file where the member functions are
defined and the file(s) where they are used. Because a single header file
describing your library is included throughout the system, the compiler can
ensure consistency and prevent errors.
There
are certain issues that you must be aware of in order to organize your code
properly
and write effective header files. The first issue concerns what you can put
into header files. The basic rule is “only declarations,”
that is, only information to the compiler but nothing that allocates storage by
generating code or creating variables. This is because the header file will
typically be included in several translation units in a project, and if storage
for one identifier is allocated in more than one place, the linker will come up
with a multiple definition error (this is C++’s
one
definition rule
:
you can declare things as many times as you want, but there can be only one
actual definition for each thing).
This
rule isn’t completely hard and fast. If you define a variable that is
“file static”
(has visibility only within a file) inside a header file, there will be
multiple instances of that data across the project, but the linker won’t
have a collision
[25].
Basically, you don’t want to do anything in the header file that will
cause an ambiguity at link time.
The
multiple-declaration problem
The
second header-file issue is this: when
you put a
struct
declaration in a header file, it is possible for the file to be included more
than once in a complicated program. Iostreams are a good example. Any time a
struct
does I/O it may include one of the iostream headers. If the
cpp
file
you are working on uses more than one kind of
struct
(typically including a header file for each one), you run the risk of including
the istreams header more than once and re-declaring streams.
The
compiler considers the redeclaration of a structure (this includes both
structs
and
classes)
to
be an error, since it would otherwise allow you to use the same name for
different types. To prevent this error when multiple header files are included,
you need to build some intelligence into your header files using the
preprocessor (Standard C++ header files like
<iostream>
already have this “intelligence”).
Both
C and C++ allow you to redeclare a function, as long as the two declarations
match, but neither will allow the redeclaration of a structure.
In C++ this rule is especially important because if the compiler allowed you to
redeclare a structure and the two declarations differed, which one would it use?
The
problem of redeclaration comes up quite a bit in C++ because each data type
(structure with functions) generally has its own header file, and you have to
include one header in another if you want to create another data type that uses
the first one. In any
cpp
file
in your project, it’s very likely that you’ll include several files
that include the same header file. During a single compilation, the compiler
can see the same header file several times. Unless you do something about it,
the compiler will see the redeclaration of your structure and report a
compile-time error. To solve the problem, you need to know a bit more about the
preprocessor.
The
preprocessor directives
#define,
#ifdef and #endif
The
preprocessor directive
#define
can be used to create compile-time flags. You have two choices: you can simply
tell the preprocessor that the flag is defined, without specifying a value:
or
you can give it a value (which is the typical C way to define a constant):
In
either case, the label can now be tested by the preprocessor to see if it has
been defined: will
yield a true result, and the code following the
#ifdef
will be included in the package sent to the compiler. This inclusion stops when
the preprocessor encounters the statement
Any
non-comment after the
#endif
on the same line is illegal, even though some compilers may accept it. The
#ifdef/#endif
pairs may be nested within each other.
The
complement of
#define
is
#undef
(short for “un-define”), which will make an
#ifdef
statement
using the same variable yield a false result.
#undef
will also cause the preprocessor to stop using a macro. The complement of
#ifdef
is
#ifndef,
which will yield a true if
the label has not been defined (this is the one we will use in header files).
There
are other useful features in the C preprocessor. You should check your local
documentation for the full set.
A
standard for header files
In
each header file that contains a structure, you should first check to see if
this header has already been included in this particular
cpp
file. You do this by testing a preprocessor flag. If the flag isn’t set,
the file wasn’t included and you should set the flag (so the structure
can’t get re-declared) and declare the structure. If the flag was set
then that type has already been declared so you should just ignore the code
that declares it. Here’s how the header file should look:
#ifndef HEADER_FLAG
#define HEADER_FLAG
// Type declaration here...
As
you can see, the first time the header file is included, the contents of the
header file (including your type declaration) will be included by the
preprocessor. All the subsequent times it is included – in a single
compilation unit – the type declaration will be ignored. The name
HEADER_FLAG can be any unique name, but a reliable standard to follow is to
capitalize the name of the header file and replace periods with underscores
(leading underscores are reserved for system names). Here’s an example:
//: C04:Simple.h
// Simple header that prevents re-definition
#ifndef SIMPLE_H
#define SIMPLE_H
struct Simple {
int i,j,k;
initialize() { i = j = k = 0; }
};
Although
the
SIMPLE_H
after the
#endif
is commented out and thus ignored by the preprocessor, it is useful for
documentation.
These
preprocessor statements that prevent multiple inclusion are often referred to as
include
guards
.
Namespaces
in headers
You’ll
notice that
using
directives
are present in nearly all the
cpp
files in this book, usually in the form:
Since
std
is
the namespace that surrounds the entire Standard C++ library, this particular
using directive allows the names in the Standard C++ library to be used without
qualification. However, you’ll virtually never see a using directive in a
header file (at least, not outside of a scope). The reason is that the using
directive eliminates the protection of that particular namespace, and the
effect lasts until the end of the current compilation unit. If you put a using
directive (outside of a scope) in a header file, it means that this loss of
“namespace protection” will occur with any file that includes this
header, which often means other header files. Thus, if you start putting using
directives in header files, it’s very easy to end up “turning
off” namespaces practically everywhere, and thereby neutralizing the
beneficial effects of namespaces.
Using
headers in projects
When
building a project in C++, you’ll usually create it by bringing together
a lot of different types (data structures with associated functions).
You’ll usually put the declaration for each type or group of associated
types in a separate header file,
then define the functions for that type in a translation unit. When you use
that type, you must include the header file to perform the declarations properly.
Sometimes
that pattern will be followed in this book, but more often the examples will be
very small, so everything – the structure declarations, function
definitions, and the
main( )
function – may appear in a single file. However, keep in mind that
you’ll want to use separate files and header files in practice.
[24]
To write a function definition for a function that takes a true variable
argument list, you must use
varargs.
This is avoided in C++; you can find details about the use of varargs in your C
manual.
[25]
However, in Standard C++ file static is a deprecated feature.
Contact: webmaster@codeguru.com
CodeGuru - the website for developers.