Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

lafore_robert_objectoriented_programming_in_c

.pdf
Скачиваний:
51
Добавлен:
27.03.2023
Размер:
8.94 Mб
Скачать

Multifile Programs

645

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”

13

PROGRAMS MULTIFILE

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

13

PROGRAMS MULTIFILE

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.

13

PROGRAMS MULTIFILE

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;

Multifile Programs

651

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.

13

PROGRAMS MULTIFILE

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

Multifile Programs

653

class

verylong

 

 

 

{

 

 

 

 

 

private:

 

 

 

 

 

char

vlstr[SZ];

//verylong

number, as a string

 

int vlen;

//length of verylong

string

 

verylong multdigit(const int) const; //prototypes for

 

verylong mult10(const verylong) const; //private functions

public:

 

 

 

 

 

verylong() : vlen(0)

 

//no-arg constructor

 

{

vlstr[0]=’\0’; }

 

 

 

 

verylong(const char s[SZ])

//one-arg

constructor

 

{

strcpy(vlstr, s); vlen=strlen(s); }

//for string

 

verylong(const unsigned long n)

//one-arg

constructor

 

{

 

 

 

//for long int

 

ltoa(n, vlstr, 10);

 

//convert

to string

 

strrev(vlstr);

 

//reverse

it

 

vlen=strlen(vlstr);

 

//find length

 

}

 

 

 

 

 

void

putvl() const;

 

//display

verylong

 

void

getvl();

 

//get verylong from user

 

verylong operator + (const verylong); //add verylongs

 

verylong operator * (const verylong); //multiply verylongs

};

 

 

 

 

 

In addition to the data members, there are two private-member functions in class verylong. One multiplies a verylong number by a single digit, and the other multiplies a verylong number by 10. These routines are used internally by the multiplication routine.

There are three constructors. One sets the verylong to 0 by inserting a terminating null at the beginning of the array and setting the length to 0. The second initializes it to a string (which is in reverse order), and the third initializes it to a long int value.

The putvl() member function displays a verylong, and getvl() gets a verylong value from the user. You can type as many digits as you like, up to 1,000. Note that there is no error checking in this routine; if you type a non-digit the results will be inaccurate.

Two overloaded operators, + and *, perform addition and multiplication. You can use expressions like

alpha = beta * gamma + delta;

to do verylong arithmetic.

13

PROGRAMS MULTIFILE

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

}