Implementing the previous version of the Actor example in a single file allowed us to focus on the mechanics of inheritance, but putting all the classes in a single file in an object-oriented program isn't common or realistic. The second version still uses the over-simplified Person, Actor, and Star classes but adopts the more common and realistic practice of putting each class specification in a separate header file. This approach increases the potential for reusing classes, but it also increases the potential for unexpected errors.
The classes remain unchanged from the first version, so this section focuses on the syntax necessary to manage a multi-class program. If a class specification only contains member variables and function prototypes (both members and non-members), then the specification is purely a declaration. Multiple declarations don't interfere with compilation if each declaration is the same. But if the class includes any function bodies, then those bodies form a function definition. Multiple declarations don't cause a problem, but multiple definitions do.
Prototypes Only | Inline Functions |
---|---|
class Person { private: string name; public: Person(string n) : name(n); void display(); }; |
class Person { private: string name; public: Person(string n) : name(n) {} void display() { cout << name << endl; } }; |
(a) | (b) |
So far, I haven't suggested how a class specification might become included in a program more than once. The Actor2 header files and the main function demonstrate a common way that this can occur. C++ provides two preprocessor mechanisms (the ANSI standard incorporates only one) to prevent this error by ensuring that a class specification is included in each source code file exactly once.
The classic solution to the multiple file inclusion problem is called an include guard or macro guard. The include guard is implemented by the preprocessor using three directives:
#ifndef
#define
#ifndef
directive skips the definition. The more general usage requires a value to associate with the constant name. As used in the guard, the definition is significant, but the value is not, so the preprocessor automatically sets the value to 1.#endif
#ifndef
directive.#ifndef
and #define
directives must be unique throughout the program (i.e., used in only one header file). Traditionally, programmers create unique constant names from the header file name (which programmers derive from the class name) by following two or three steps:
The #pragma preprocessor directive provides additional information to the compiler system. It introduces a non-standard behavior that pragmatically solves a problem using the unique features of a vendor's hardware or software. The ANSI C and C++ standards don't cover the pragma
directive, so it may not be portable (i.e., be easily moved from one computer to another). Like the #include guard, the #pragma once
directive ensures that a block of code is included in a source code file exactly once. It is more compact (i.e., has much less code) than a guard and avoids the problem of creating unique identifiers or constant names. Although most contemporary compilers on Windows, macOS, and Linux support #pragma once, doing so is not yet required by the ANSI standard, so it is not guaranteed to work with all compilers.
Programmers typically implement C++ classes as two files: a header file and a source code file. The header file contains the class specification, prototypes for large functions, and complete definitions for small functions. The source code file contains static member variables and the definitions for large functions. This organization simplifies writing programs based on a client and supplier model. The application program is the client, while the class, represented by the header and the source files, is the supplier. For the client or application program to use the supplier's services, it's sufficient for the client to #include the supplier's header file. The compiler system compiles and links the supplier's source code file with the application.
Moving the Actor-example classes to separate header files has a significant and easily overlooked consequence. The three classes and the main function of the example use the string and iostream library classes. One set of include directives provides the necessary class specifications to all parts of the single-file version. However, naively copying the classes to new, separate header files can inadvertently create error-prone code.
#include <iostream> #include <string> using namespace std; #include "Person.h" #include "Actor.h" #include "Star.h" |
#include "Person.h" #include "Actor.h" #include "Star.h" #include <iostream> #include <string> using namespace std; |
(a) | (b) |
#ifndef _PERSON_H_ #define _PERSON_H_ #include <iostream> #include <string> using namespace std; class Person { . . . }; #endif |
#pragma once #include <iostream> #include <string> using namespace std; class Person { . . . }; |
#ifndef _ACTOR_H_ #define _ACTOR_H_ #include <iostream> #include <string> #include "Person.h" using namespace std; class Actor : public Person { . . . }; #endif |
#pragma once #include <iostream> #include <string> #include "Person.h" using namespace std; class Actor : public Person { . . . }; |
Like the Person class, Actor has a member variable and a constructor parameter that are instances of the string class, so the string header is #included in the Actor header file. Technically, doing this is unnecessary: the Actor header file includes the Person header, which includes the string header file. However, imagine an application program that uses the Actor class but does not directly deal with a Person. Following the best object-oriented programming practice, the Actor class designer should not require application programmers to "know" the implementation details of the Actor class, including that it subclasses Person. (For example, how much do you know about how C++ implements its string class?)
#ifndef _STAR_H_ #define _STAR_H_ #include <iostream> #include <string> #include "Actor.h" using namespace std; class Star : public Actor { . . . }; #endif |
#pragma once #include <iostream> #include <string> #include "Actor.h" using namespace std; class Star : public Actor { . . . }; |
Each Actor2 header file includes the C++ string header. The multiple inclusions don't cause errors because the string class employs a guarding mechanism, either an #include guard or a #pragma once. We unknowingly relied on this standard practice when we created our first multi-file program (Time, see figures 2 and 3) and placed the #include <iostream>
directive at the beginning of each source code file. This example demonstrates the value of the guarding mechanism: programmers can use existing classes and functions with little regard for their implementation details.
#include "Person.h" #include "Actor.h" #include "Star.h" int main() { Person director("Steven Spielberg"); director.display(); Actor sidekick("Harvey Korman", "Dilbert"); sidekick.display(); Star big_star("John Wayne", "Cranston Snort", 5000000); big_star.display(); return 0; }
The program demonstrates our operational rule of thumb or mantra: whenever we use a class name, we include the corresponding header file - even if some inclusions are superfluous. Including Person.h and Actor.h is superfluous because Star.h incorporates all three class specifications into the program. Nevertheless, making our classes sufficiently robust so that multiple inclusions don't cause compiler errors makes them easier for application programmers. Following this practice, it's easy to see how a program might include a class specification more than once.
Rules of thumb for using header files:
#ifndef / #define / #endif
#pragma once