What if, instead of using a header file, you actually copied the text of the class definition and pasted it manually into each source file? Then any modification to the class would require you to change the definition in each file seperately. This would be time-consuming and prone to errors.
So far we’ve shown class definitions with no external member-function definitions. Where can external member functions be defined? Like ordinary functions, they can go in any source file, and the linker will connect them as needed. The class definition serves to declare the member functions in each file. As within a single file, the member function definition must include the class name and scope resolution operator.
//fileH.h |
|
class someClass |
//class definition |
{ |
|
private: |
|
int memVar; |
|
public: |
|
int memFunc(int, int); |
//member-function declaration |
}; |
|
; |
|
//fileA.cpp |
|
#include “fileH.h” |
|
int someClass::memFunc(int n1, int n2) |
//member function definition |
{ return n1 + n2; } |
|
//fileB.cpp |
|
#include “fileH.h” |
|
someClass anObj; |
//create an object |
int answer = anObj.memFunc(6,7); |
//use the member function |
The Multiple-Includes Hazard
We’ve mentioned that you can’t define a function or variable in a header file that will be shared by multiple source files. Doing so causes multiple-definition errors. A similar problem arises if you include the same header file twice in a source file. How could such a thing happen? You probably would not make a mistake this obvious:
//file app.cpp #include “headone.h” #include “headone.h”
But suppose you have a source file APP.CPP and two header files, HEADONE.H and HEADTWO.H. Further suppose that HEADONE.H includes HEADTWO.H. Unfortunately you forget this and include them both in APP.CPP:
Chapter 13
646
//file headtwo.h int globalVar;
//file headone.h #include “headtwo.h”
//file app.cpp #include “headone.h” #include “headtwo.h”
Now what happens when you compile APP.CPP? Once the #include directives have pasted the text of the header files into APP.CPP, we end up with
//file app.cpp
. . .
int globalVar; //from head2.h via headone.h
. . .
int globalVar; //from head2.h directly
This will cause the compiler to complain that globalVar is defined twice.
Preventing Multiple Includes
Here’s how to prevent multiple-definition errors even when a header file is included more than once in your source file. You precede the definitions in the header file with the preprocessor directive
#if !defined( HEADCOM )
(You can use any identifier, not just HEADCOM.) This statement says that if HEADCOM is not defined (the exclamation point is a logical NOT), all the text that follows this directive, up to a closing #endif directive, will be pasted into the source file normally. But if HEADCOM is defined, which can be accomplished with the directive
#define HEADCOM
then the text that follows will not be included in the source file. As they say in the movies, it ends up on the cutting-room floor. Because HEADCOM is not defined when this text is first encountered, but is defined immediately after the #if !defined() directive, the text between there and the closing #endif will be included the first time it’s encountered, but never again. Here’s the arrangement:
#if !defined( HEADCOM ) |
//if HEADCOM not defined, |
#define HEADCOM |
//define it |
int globalVar; |
//define this variable |
Multifile Programs
647
int func(int a, int b) |
//define this function |
{ return a+b; } |
|
#endif |
//end condition |
You should use this approach whenever there’s any possibility a header file will be included in a source file more than once.
An older directive, #ifndef, was used the same way as #if !defined(), and will be seen in many of the header files supplied with your compiler. However, its use is now discouraged.
Note that the #if !defined() approach works in the situation where the definition of globalVar (or some other variable or function) may end up being included multiple times in the same source file. It does not work when globalVar is defined in file H, and file H is included in different source files A and B. The preprocessor is powerless to detect multiple statements in separate files, so the linker will complain that globalVar is multiply defined.
Namespaces
We’ve seen how to restrict the visibility of program elements by declaring them within a file or class, or by making global elements static or const. Sometimes, however, a more versatile approach is required.
For example, when writing a class library, programmers would prefer to use short and common names for non-member functions and classes, like add() and book. However, short and common names may turn out to be the same names selected by the creators of another library or by an application that uses the library. This can lead to “name clashes” and generate multipledefinition errors from your compiler. Before the advent of namespaces, programmers were forced to use long names to avoid this problem:
Henry’s_Simplified_Statistics_Library_add();
However, long names are difficult to read and write and take up excessive space in a listing. Namespaces can solve this problem. (Note that member functions don’t cause name clashes because their scope is limited to the class.)
Defining a Namespace
A namespace is a section of a file that is given a name. The following code defines a namespace geo with some declarations inside it:
namespace geo
{
const double PI = 3.14159; double circumf(double radius)
{ return 2 * PI * radius; }
}//end namespace geo
Chapter 13
648
Braces delimit the namespace. Variables and other program elements declared within the braces are called namespace members. Notice that there is no semicolon following the closing brace, as there is with classes.
Accessing Namespace Members
Code outside a namespace cannot access the elements within it, at least not in the normal way. The namespace makes them invisible:
namespace geo
{
const double PI = 3.14159; double circumf(double radius)
{ return 2 * PI * radius; }
}//end namespace geo
double c = circumf(10); //won’t work here
To make the elements visible outside the namespace you must invoke the namespace name when referring to them. There are two ways to do this. First, you can precede each element’s name with the namespace name and the scope resolution operator:
double c = geo::circumf(10); //OK
Or you can use the using directive:
using namespace geo;
double c = circumf(10); //OK
The using directive ordinarily causes the namespace to be visible from that point onward. However, you can restrict the region where the using directive is in effect to a particular block, such as a function:
void seriousCalcs() |
|
{ |
|
using namespace geo; |
|
//other code here |
|
double c = circumf(r); |
//OK |
} |
|
double c = circumf(r); |
//not OK |
Here the members of the namespace are visible only within the function body.
Namespaces in Header Files
Namespaces are most commonly used in header files containing library classes or functions. Each such library can have its own namespace. By this time you are familiar with the namespace std, whose members constitute the Standard C++ Library.
Multifile Programs
649
Multiple Namespace Definitions
There can be several instances of the same namespace definition:
namespace geo
{
const double PI = 3.14159; } // end namespace geo
//(some other code here)
namespace geo
{
double circumf(double radius)
{ return 2 * PI * radius; }
}//end namespace geo
This looks like a redefinition, but it’s really just a continuation of the same definition. It allows a namespace to be used in several header files, which can then all be included in a source file. In the Standard C++ Library, dozens of header files use the namespace std.
//fileA.h namespace alpha
{
void funcA();
}
//fileB.h namespace alpha
{
void funcB();
}
fileMain.cpp #include “fileA.h” #include “fileB.h”
using namespace alpha; funcA();
funcB();
You can place declarations outside a namespace that behave as if they were inside it. All you need is the scope resolution operator and the namespace name:
namespace beta
{
int uno;
}
int beta::dos;
Here, both uno and dos are declared in the namespce beta.
Chapter 13
650
Unnamed Namespaces
You can create a namespace without a name. Doing so creates a namespace that is automatically visible throughout the file in which it’s defined, but not visible from other files. The compiler gives an unnamed namespace an internal name unique to the file. Elements declared in the unnamed namespace can be accessed from anywhere in the file. In the following listing, funcA() and funcB() can access the gloVar variable in their respective files.
//fileA.cpp
namespace //unnamed namespace unique to fileA.cpp
{
int gloVar = 111;
}
funcA()
{ cout << gloVar; } //displays 111
//fileB.cpp
namespace //unnamed namespace unique to fileB.cpp
{
int gloVar = 222;
}
funcB()
{ cout << gloVar; } //displays 222
In this example both files contain a variable named gloVar, but there’s no conflict because each variables is declared in an unnamed namespace unique to its file and is invisible everywhere else.
This approach provides an alternative to the use of static for restricting the scope of global variables to their own file. In fact, the namespace approach is now considered preferable to making elements static.
Renaming Types with typedef
You may find the typedef keyword useful in certain situations, and you will certainly run across it in other people’s listings. It allows you to create a new name for a data type. For example, the statement
typedef unsigned long unlong;
makes unlong a synonym for unsigned long. Now you can declare variables using the new name:
unlong var1, var2;
This may save you a little space or make your listing more readable. More usefully, you can make up new type names that reveal the purpose of any variables declared with that type:
class GeorgeSmith_Display_Utility
{
//members
};
typedef GeorgeSmith_Display_Utility GSdu;
GSdu anObj;
typedef |
int |
FLAG; |
//int |
variables |
used |
to |
hold |
flag values |
typedef |
int |
KILOGRAMS; |
//int |
variables |
used |
to |
hold |
values in kilograms |
If you don’t like the way pointers are specified in C++, you can change it:
int *p1, *p2, *p3; |
//normal declaration |
typedef int* ptrInt; |
//new name for pointer to int |
ptrInt p1, p2, p3; |
//simplified declaration |
This avoids all those pesky asterisks.
Because classes are types in C++, you can use typedef to create alternative names for them. Earlier we mentioned that developers sometimes create excessively long names. If you need to use these names, writing them can be an inconvenience and can make the listing hard to read. You can fix the problem, at least for class names, with typedef:
//class definition
//rename the class
//create object using new name
Type renaming with typedef is typically handled in header files, so that multiple source files can use the new names. Many software development organizations make extensive use of typedef, resulting in what looks almost like a different language.
Now that we’ve explored some of the general concepts involved in multifile programs, let’s look at some examples. These programs won’t demonstrate all the topics we’ve covered in the previous section, but they will show you some typical situations where an application programmer uses code provided by a library writer.
A Very Long Number Class
Sometimes even the basic data type unsigned long does not provide enough precision for certain integer arithmetic operations. unsigned long is the largest integer type in Standard C++, holding integers up to 4,294,967,295, or about ten digits. This is about the same number of digits a pocket calculator can handle. But if you need to work with integers containing more significant digits than this, you have a problem.
Our next example offers a solution. It provides a class that holds integers up to 1,000 digits long. If you want to make even longer numbers (or shorter ones), you can change a single constant in the program.
Chapter 13
652
Numbers as Strings
The verylong class stores numbers as strings of digits. These are old-fashioned char* C- strings, which are easier to work with in this context than the string class. The use of C- strings explains the large digit capacity: C++ can handle long C-strings, since they are simply arrays. By representing numbers as C-strings we can make them as long as we want. There are two data members in verylong: a char array to hold the string of digits, and an int to tell how long the string is. (This length of data isn’t strictly necessary, but it saves us from having to use strlen() repeatedly to find the string length.) The digits in the string are stored in reverse order, with the least significant digit stored first, at vlstr[0]. This simplifies various operations on the string. Figure 13.2 shows a number stored as a string.
FIGURE 13.2
A verylong number.
We’ve provided user-accessible routines for addition and multiplication of verylong numbers. (We leave it as an exercise for the reader to write subtraction and division routines.)
The Class Specifier
Here’s the header file for VERYLONG. It shows the specifiers for the verylong class.
//verylong.h
//class specifier for very long integer type #include <iostream>
#include |
<string.h> |
//for strlen(), etc. |
#include |
<stdlib.h> |
//for ltoa() |
using |
namespace std; |
|
const |
int SZ = 1000; |
|
|
|
//maximum digits in verylongs |
Chapter 13
654
The Member Functions
Here’s VERYLONG.CPP, the file that holds the member function definitions:
//verylong.cpp
//implements very long integer type
#include “verylong.h” |
//header file for verylong |
//-------------------------------------------------------------- |
|
void verylong::putvl() const |
//display verylong |
{ |
|
char temp[SZ]; |
|
strcpy(temp,vlstr); |
//make copy |
cout << strrev(temp); |
//reverse the copy |
} |
//and display it |
//-------------------------------------------------------------- |
|
void verylong::getvl() |
//get verylong from user |
{ |
|
cin >> vlstr; |
//get string from user |
vlen = strlen(vlstr); |
//find its length |
strrev(vlstr); |
//reverse it |
} |
|
//-------------------------------------------------------------- |
|
verylong verylong::operator + (const verylong v) //add verylongs
{
char temp[SZ]; int j;
//find longest number
int maxlen = (vlen > v.vlen) ? vlen |
: v.vlen; |
int carry = 0; |
//set to 1 if sum >= 10 |
for(j = 0; j<maxlen; j++) |
//for each position |
{ |
|
int d1 = (j > vlen-1) ? 0 : vlstr[j]-’0’; //get digit |
int d2 = (j > v.vlen-1) ? 0 : v.vlstr[j]-’0’; //get digit |
int digitsum = d1 + d2 + carry; |
//add digits |
if( digitsum >= 10 ) |
//if there’s a carry, |
{ digitsum -= 10; carry=1; } |
//decrease sum by 10, |
else |
//set carry to 1 |
carry = 0; |
//otherwise carry is 0 |
temp[j] = digitsum+’0’; |
//insert char in string |
} |
|
if(carry==1) |
//if carry at end, |
temp[j++] = ‘1’; |
//last digit is 1 |
temp[j] = ‘\0’; |
//terminate string |
return verylong(temp); |
//return temp verylong |
} |
|