|

In C++, the preprocessor
processes code after the # sign at compile time. When the compiler
sees the preprocessor
sign, it performs those operations first before
compiling the code. This can be useful for creating a program that
will
compile into different versions based upon altering the code that
is preprocessed. It looks like it's time for a few more
keywords:
| "You
must be the change you wish to see in the world."
- Mahatma Gandhi |
#define - defines a string substitution Example: #define BIG 512 will substitute string 512
everywhere it sees string BIG.
token - string of characters than can be used wherever a string, constant or other set of letters might be used. BIG is a token.
#define BIG 512
//BIG becomes a token for the literal constant, 512. int MyArray[BIG];
//is the same as writing int MyArray[512];
Using
#define for constants substitutes for them, but this is not always a good idea,
since #define makes only a string substitution and never does
any type checking. Let's look at some other keywords:
#ifdef - tests whether a string has been defined.
#ifndef - tests whether a string has NOT been defined.
#endif - used to end an "ifdef" or "ifndef" statement.
#else - used with define, operates like if/else statement.
#define DEBUG
#ifdef DEBUG
cout << "Debug defined.";
#endif |
This would add the
cout << "Debug defined." to the source code at compile time since it was defined.
If we commented it out, it would not be added. Another example:
#define DemoVersion
#define DOS_VERSION 6
#include <iostream.h>
int main()
{
cout << "Checking on the definitions of DemoVersion, DOS_VERSION";
cout << "and WINDOWS_VERSION...\n";
#ifdef DemoVersion
cout << "DemoVersion defined.\n";
#else
cout << "DemoVersion not defined.\n";
#endif
#ifndef DOS_VERSION
cout << "DOS_VERSION not defined!\n";
#else
cout << "DOS_VERSION defined as: " << DOS_VERSION << endl;
#endif
#ifdef WINDOWS_VERSION
cout << "WINDOWS_VERSION defined!\n";
#else
cout << "WINDOWS_VERSION was not defined.\n";
#endif
cout << "Done.\n";
return 0;
} |
Output:
Checking on the definitions of DemoVersion, DOS_VERSION and WINDOWS_VERSION...
DemoVersion defined...
DOS_VERSION defined as: 5
WINDOWS_VERSION was not defined.
Done.
In derivation and polymorphism, there is the danger of including a declaration of the same class more than once.
Improper inclusion will generate a compiler error. This
becomes a serious problem when your project consists of many separate
source and header files. If the Animal class is declared in "ANIMAL.HPP" then the Dog class, which derives from Animal, must include it in "DOG.HPP". The Cat class also includes "ANIMAL.HPP" for the same reason. If you create a method that uses Dog and Cat, you
might include Animal twice. We can solve this problem with inclusion guards.
The use of inclusion guards may save you hours of debugging. Cause compiler to test
a condition before including a file. If the Animal has not been included statement evaluates
to be true, the Animal code is added. If the Animal has been included statement evaluates
to be false, the Animal code is not included a 2nd erroneous time. At top of
the base class, in "ANIMAL.HPP" in this case, we would write:
#ifndef ANIMAL_HPP
#define ANIMAL_HPP
***REST OF ENTIRE FILE GOES HERE***
#endif |
Defining on the command line involves placing special debugging code between "#ifdef DEBUG" and "#endif". This allows special code to be turned on or off for debugging by whether
or not "DEBUG" is defined. Using #define commands, you
may compile for different operating systems using the same source code.
This is called CONDITIONAL compilation. Let's look at one more
keyword:
#undef - This is the opposite of and antidote for define. It undefines
a previously defined token.
Macro functions - A symbol created using #define and taking an argument, like a function. The preprocessor will substitute the substitution string for whatever argument it is given.
Common macro functions that return the greater or lesser of 2 values are:
#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) #define MIN(x,y) ( (x) < (y) ? (x) : (y) )
Below is an examples that will double
what is passed in:
#define TWICE(x) ( (x) * 2) TWICE(4)
In the final intermediate code, the entire string "TWICE(4)" will be removed and the value "8" will be substituted in its place. The parameters must come DIRECTLY after the substitution string, the preprocessor is not as forgiving of white space. Example:
#define MAX (x,y) ( (x) > (y) ? (x) : (y) )
//wrong; white space between MAX and (x,y) int x = 5, y = 7, z; z = MAX(x.y);
The expression below creates intermediate code that is a simple text substitution rather than invoking the macro:
int x = 5, y = 7, z; z = (x,y) ( (x) > (y) ? (x) : (y) ) (x,y)
When white space is removed, the intermediate code becomes:
int x = 5, y = 7, z; z = 7;
Parentheses help avoid unwanted side effects when passing complicated values to a macro. Example:
//Macro Expansion
#include <iostream.h>
#define CUBE(a) ( (a) * (a) * (a) )
#define THREE(a) a * a * a
int main()
{
long x = 5;
long y = CUBE(x);
long z = THREE(x);
cout << "y: " << y << endl;
cout << "z: " << z << endl;
long a = 5, b = 7;
y = CUBE(a+b);
z = THREE(a+b);
cout << "y: " << y << endl;
cout << "z: " << z << endl;
return 0;
}
|
Output:
y: 125
z: 125
y: 1728
z: 82
The reason for the different outcomes to what should essentially be the
same answer lies in whether or not we use parentheses. The macro, CUBE,
is defined using parentheses. The macro, THREE, is defined without using parentheses. In first instance,
the value 5 is given to both macros as a parameter, and they both work out to 125.
However, in the next instance, the parameter becomes "5 + 7". In the case of
the macro, CUBE, it evaluates to ( (5+7) * (5+7) * (5+7) ), which evaluates to
( (12) * (12) * (12) ) which evaluates to
1,728. In the case of the macro,
THREE, however, there are no parentheses. Since multiplication has a higher precedence (binds tighter) than addition, the expression evaluates to
5 + 7 * 5 + 7 * 5 + 7 which evaluates to
5 + (7*5) + (7*5) + 7 which evaluates to
5 + (35) + (35) + 7 which evaluates to
82.
There are
4 general problems with macros:
1 - Macros are confusing if large, they have to be defined on one line.
You can extend the line using "\", but they can still be confusing.
2 - Macros are expanded inline each time they are used. Though
this runs quicker, it increases program size.
3 - Macros do not appear in the intermediate source code, and so are unavailable to most debuggers.
4 - Macros are not type-safe. Any argument can be used, and this undermines
the strong typing of C++. Templates overcome this.
Stringizing - The stringizing operator is "#". It puts quotes around any characters following the operator, until the next white space.
For example, if you write
#define WRITESTRING(x) cout << #x and then call
WRITESTRING(This is a string); the precompiler will turn it into
cout << "This is a string";. The string, "This is a string", is put into quotes as required by cout.
Concatenation - Concatenation allows you to bond together more than
one term into a new word. A new word is usually a token that can be used as a class name,
a variable name, an offset to an array, or anywhere else a series of letters might appear.
To do this we'd write ## between the items
we want to concatenate. What if we write:
#define fPRINT(x) f
## x ## Print
fPRINT(Two) fPRINT(Three)
In this case,the precompiler would generate "fTwoPrint" and "fThreePrint". If
we wrote:
#define Listof(Type)
class Type##List \
{ \
public: \
Type##List() {} \
private: \
int intsLength; \
};
When we were ready to create the class AnimalList,
we'd write "Listof(Animal)", and this would be turned into the declaration of the class AnimalList
at compile time. There are some problems with this method that are solved by using Templates.
Predefined macros - Many compilers predefine macros such as
_LINE_ , _DATE_ ,
_TIME_ , and _FILE_ . Each are surrounded by
two underscore characters to make sure they don't conflict with other file names. Remember the substitution is made when the source is compiled, not when the program is run.
Therefore, _DATE_ will render the date the program was compiled on, not the current date.
assert() - The assert() function is offered by many compilers, or
we can create our own. This function returns true if its parameter evaluates
to be true, and takes an action if it evaluates to be false. The precompiler collapses it into no code
at all if DEBUG is not defined. Compilers abort the program or throw exceptions when
assert() fails. Remember that white
space really matters with #, the preprocessor, defines and macros.
If you are going to go to the next line, you must use the "
\ " character and go to the line
immediately after it, with NO lines in between:
//ASSERTS
- Remember to use the " \ " when skipping to the
next line wit no lines in between.
#define DEBUG
#include <iostream>
using namespace std;
#ifndef DEBUG
#define ASSERT(x)
#else
#define ASSERT(x) \
if(! (x)) \
{ \
cout << "ERROR!! The Assert() argument " << #x << " failed\n";
\
cout << " on line " << __LINE__ << "\n";
\
cout << " in file " << __FILE__ << "\n";
\
}
#endif
int main()
{
int x = 5;
cout << "First assert: \n";
//Evaluates true - since x has the value of 5.
ASSERT(x==5);
cout << "\nSecond assert: \n";
//Evaluates false - since x does have the value of
5.
ASSERT(x != 5);
cout << "\nDone.\n";
return 0;
} |
Output:
First assert:
Second assert:
ERROR!! Assert x!=5 failed
on line 24
in file test2103.cpp
In the example above, DEBUG is defined and the ASSERT() macro is defined.
This is typically done in a header file and included in main(). As
we proceed, the term DEBUG is tested. If it is not defined, ASSERT() is defined to create no code
and do nothing at all. If DEBUG is defined, and ASSERT() returns
false, an error message will be displayed showing the line number and
file name. In this example, ASSERT() is 1 long statement split across 7 lines using " \
". The value passed in to ASSERT() as a parameter is tested. If
it evaluates to be false, the error message is invoked, printing a message. If
the value passed in is true, no action is taken. Any time code depends on a particular value being in a
particular variable, you may want to use ASSERT() to evaluate that it is true.
Are there side effects? Yes, ASSERT() can have side effects. If you write ASSERT(x = 5) using the assignment operator, when you
really mean to test whether or not x==5
using the evaluates to operator, you will create a nasty bug. If x was previously 0 and you want to check to see if it is equal to 5, you
may not realize that you are actually setting x to be equal to 5, and
then returning the value 5. The test will always return true, since x is 5 (true) and not 0 (false). When you turn debugging off, x no longer gets set to 5, and
so the program breaks. When debugging is turned back on, it works again because x is set to 5,
and not just tested. Keep an eye out for side effects that show up when debugging is turned off
and on. These bugs can be difficult to find and detect.
Class invariants are conditions that should always be true whenever you are finished with a class member function.
For example, Circles should NEVER have a radius of 0, or Animals should ALWAYS have an age > 0 but <
200. To test for the conditions, you would declare in
Invariant() method that only returns true if each of these conditions are still true. Then insert
Assert(Invariants()) at
the start and completion of each class method. The exception is that your
Invariants() is not expected to return true before your constructor runs, or after your destructor ends. Example:
#define DEBUG
#define SHOW_INVARIANTS
#include <iostream.h>
#include <string.h>
#ifndef DEBUG
#define ASSERT(x)
#else
#define ASSERT(x) \
if(! (x))
\
{ \
cout << "ERROR!! Assert " << #x << " failed\n";
\
cout << " on line " << __LINE__ << "\n";
\
cout << " in file " << __FILE__ << "\n";
\
}
#endif
//-------------------------------------------------------------------------------------
class String
{
public:
//Overloaded constructors
String();
String(const char * const);
//Copy constructor
String(const String
&);
~String();
char &
operator[](int offset);
char
operator[](int offset)
const;
String & operator= (const String &);
int GetLen() const {
return itsLen; }
const char * GetString()
const {
return itsString; }
bool Invariants()
const;
private:
String (int);
//private constructor
char * itsString;
unsigned short itsLen;
};
//The
default constructor creates string of 0 bytes
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen=0;
ASSERT(Invariants());
}
//A private (helper) constructor, used only by class methods for creating a new string of
//the required size. It is null filled.
String::String(int len)
{
itsString = new char[len+1];
for(int i = 0; i<=len; i++)
itsString[i] = '\0';
itsLen=len;
ASSERT(Invariants());
}
// Converts a character array to a String
String::String(const char *
const cString)
{
itsLen = strlen(cString);
itsString = new char[itsLen+1];
for(int i = 0; i<itsLen; i++)
itsString[i] = cString[i];
itsString[itsLen]='\0';
ASSERT(Invariants());
}
//The copy constructor
String::String (const String & rhs)
{
itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
for(int i = 0; i<itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
ASSERT(Invariants());
}
//The destructor, frees allocated memory
String::~String ()
{
ASSERT(Invariants());
delete [] itsString;
itsLen = 0;
}
//Overloaded operator equals, frees existing memory then copies
the string and size
String& String::operator=(const String &
rhs)
{
ASSERT(Invariants());
if(this ==
&rhs)
return
*this;
delete [] itsString;
itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
for(int i = 0; i<itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
ASSERT(Invariants());
return *this;
}
//Non-constant offset operator, returns
a reference to a character so it can be changed.
char
& String::operator[](int offset)
{
ASSERT(Invariants());
if(offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
ASSERT(Invariants());
}
//Constant offset operator for use on const objects (see copy constructor)
char String::operator[](int offset)
const
{
ASSERT(Invariants());
if(offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
ASSERT(Invariants());
}
bool String::Invariants()
const
{
#ifdef SHOW_INVARIANTS
cout << " String OK. ";
#endif
return ( (itsLen && itsString) || (!itsLen && !itsString) );
}
//-------------------------------------------------------------------------------------
class Animal
{
public:
Animal():itsAge(1),itsName("Such an animal!")
{ASSERT(Invariants());}
Animal(int, const String
&);
~Animal(){}
int GetAge() { ASSERT(Invariants());
return itsAge;}
void SetAge(int Age)
{
ASSERT(Invariants());
itsAge = Age;
ASSERT(Invariants());
}
String & GetName() { ASSERT(Invariants());
return itsName; }
void SetName(const String
& name)
{
ASSERT(Invariants());
itsName = name;
ASSERT(Invariants());
}
bool Invariants();
private:
int itsAge;
String itsName;
};
Animal::Animal(int age, const String& name):
itsAge(age),
itsName(name)
{
ASSERT(Invariants());
}
bool Animal::Invariants()
{
#ifdef SHOW_INVARIANTS
cout << " Animal OK. ";
#endif
return (itsAge > 0 && itsName.GetLen());
}
//-------------------------------------------------------------------------------------
int main()
{
Animal Rufus(2,"Rufus");
cout << "\n" << Rufus.GetName().GetString() << " is ";
cout << Rufus.GetAge() << " years old.";
Rufus.SetAge(5);
cout << "\n" << Rufus.GetName().GetString() << " is ";
cout << Rufus.GetAge() << " years old.";
return 0;
}
|
Output:
String OK. String OK. String OK. String OK. String
OK. String OK. String
OK. Animal OK. String OK. Animal OK.
Rufus is Animal OK. 2 years old. Animal OK. Animal OK. Animal OK.
Rufus is Animal OK. 5 years old. String OK.
In this example, the ASSERT() macro is defined. If DEBUG is defined,
it writes out an error message when the ASSERT() macro evaluates to be false.
Later, the String class member function, "Invariants()", is declared. After
the object is fully constructed, the
Invariants()
method is called to confirm proper construction. This process is repeated for all the other constructors
as they are called, one by one. The destructor then calls
Invariants() before it sets out to destroy each object. The remaining class functions call
the Invariant()
method first before taking any action, and then again before returning
any values. This validates a fundamental C++ principal: member functions other than constructors and destructors should work on valid objects, and
they should leave them in a valid state. As the program
progresses, the Animal class declares and defines its own
Invariants() method.
Printing interim values - As another way of assisting the
debugging process, you can print the current values of pointers, variables and strings for checking assumptions and locating off-by-one loops. Example:
//Printing values in DEBUG mode
#include <iostream.h>
#define DEBUG
#ifndef DEBUG
#define PRINT(x)
#else
#define PRINT(x) \
cout << #x << ":\t" << x << endl;
#endif
enum BOOL { FALSE, TRUE } ;
int main()
{
int x = 5;
long y = 2153978;
PRINT(x);
for(int i = 0; i < x; i++)
{
PRINT(i);
}
PRINT(y);
PRINT("Bye-bye!");
int *px =
&x;
PRINT(px);
PRINT(*px);
return 0;
} |
Output:
x: 3
i: 0
i: 1
i: 2
i: 3 |
i: 4
y: 2153978
"Bye-bye!": Bye-bye!
px: 0x2100
*px: 5 |
In this example, the macro prints the current value of
the supplied parameter. The first thing fed to cout is the stringized version of the parameter. If
we pass in x, cout receives "x". Next, cout receives a quoted string ":t" which prints a colon and then a TAB. Finally, cout receives
the value of a parameter, (x), and then adds "endl" which writes a new line and flushes
out the buffer.
Debugging levels - Setting different levels of debugging offers more control than simply turning DEBUG on and off. To define a
debugging level, follow the
#define DEBUG statement with a number. You can have any
number of levels, though it is common to have four:
high, medium, low and none.
Below is an example of setting up different levels of debugging on the
String and Animal classes.
#include
<iostream.h>
#include <string.h>
//So, int value of 0
1 2
3
enum LEVEL { NONE, LOW, MEDIUM, HIGH };
const int FALSE = 0;
const int TRUE = 1;
typedef int BOOL;
//-------------------------------------------------------------------------------------
#define DEBUGLEVEL
HIGH
#if DEBUGLEVEL <
LOW
//No
debugging. Must be low, medium or high.
#define ASSERT(x)
#else
#define ASSERT(x) \
if(! (x)) \
{ \
cout << "ERROR!! Assert " << #x << " failed\n"; \
cout << " on line " << __LINE__ << "\n";
\
cout << " in file " << __FILE__ << "\n";
\
}
#endif
#if DEBUGLEVEL <
MEDIUM
#define EVAL(x)
#else
#define EVAL(x) \
cout << #x << ":\t" << x << endl;
#endif
#if DEBUGLEVEL <
HIGH
#define PRINT(x)
#else
#define PRINT(x) \
cout << x << endl;
#endif
//-------------------------------------------------------------------------------------
class String
{
public:
//Constructors
String();
String(const char * const);
String(const String &);
~String();
char &
operator[](int offset);
char
operator[](int offset) const;
String & operator= (const String &);
int GetLen()const { return itsLen; }
const char * GetString()
const
{ return itsString; }
BOOL Invariants() const;
private:
String (int);
//Private constructor
char * itsString;
unsigned short itsLen;
};
//Default constructor creates string of 0 bytes
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen=0;
ASSERT(Invariants());
}
//Private (helper) constructor, used only by class methods for creating a
//new string of required size. Null filled.
String::String(int len)
{
itsString = new char[len+1];
for(int i = 0; i<=len; i++)
itsString[i] = '\0';
itsLen=len;
ASSERT(Invariants());
}
//Converts a character array to a String
String::String(const char *
const cString)
{
itsLen = strlen(cString);
itsString = new char[itsLen+1];
for(int i = 0; i<itsLen; i++)
itsString[i] = cString[i];
itsString[itsLen]='\0';
ASSERT(Invariants());
}
//Copy constructor
String::String (const String
& rhs)
{
itsLen = rhs.GetLen();
itsString = new char[itsLen+1];
for(int i = 0; i<itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
ASSERT(Invariants());
}
//Destructor, frees allocated memory
String::~String ()
{
ASSERT(Invariants());
delete [] itsString;
itsLen = 0;
}
//Overloaded operator equals, frees existing memory then copies string and size.
String & String::operator=(const String & rhs)
{
ASSERT(Invariants());
if(this == &rhs)
return *this;
delete [] itsString;
itsLen = rhs.GetLen();
itsString = new char[itsLen+1];
for(int i = 0; i<itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
ASSERT(Invariants());
return *this;
}
//Non-constant offset operator, returns reference to character so it can be
changed.
char & String::operator[](int
offset)
{
ASSERT(Invariants());
if(offset > itsLen)
return
itsString[itsLen-1];
else
return itsString[offset];
ASSERT(Invariants());
}
//Constant offset operator for use on const objects (see copy constructor)
char String::operator[](int offset) const
{
ASSERT(Invariants());
if(offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
ASSERT(Invariants());
}
BOOL String::Invariants() const
{
PRINT("(String Invariants Checked)");
return ( (BOOL) (itsLen && itsString) || (!itsLen && !itsString) );
}
//-------------------------------------------------------------------------------------
class Animal
{
public:
Animal():itsAge(1),itsName("John Q. Animal")
{ ASSERT(Invariants()); }
Animal(int, const String &);
~Animal(){}
int GetAge()
{
ASSERT(Invariants());
return itsAge;
}
void SetAge(int Age)
{
ASSERT(Invariants());
itsAge = Age;
ASSERT(Invariants());
}
String & GetName()
{
ASSERT(Invariants());
return itsName;
}
void SetName(const String & name)
{
ASSERT(Invariants());
itsName = name;
ASSERT(Invariants());
}
BOOL Invariants();
private:
int itsAge;
String itsName;
};
Animal::Animal(int age, const String& name):
itsAge(age),
itsName(name)
{
ASSERT(Invariants());
}
BOOL Animal::Invariants()
{
PRINT("(Animal Invariants Checked)");
return (itsAge > 0 && itsName.GetLen());
}
//-------------------------------------------------------------------------------------
int main()
{
const
int AGE = 3;
EVAL(AGE);
Animal Ruffus(AGE,"Ruffus");
cout << "\n" << Ruffus.GetName().GetString();
cout << " is ";
cout << Ruffus.GetAge() << " years old.";
Ruffus.SetAge(7);
cout << "\n" << Ruffus.GetName().GetString();
cout << " is ";
cout << Ruffus.GetAge() << " years old.";
return 0;
}
|
Output 1
(DEBUG set to HIGH):
AGE: 3
(String Invariants Checked)
(String Invariants Checked)
(String Invariants Checked)
(String Invariants Checked)
(String Invariants Checked)
(String Invariants Checked)
(String Invariants Checked)
(String Invariants Checked)
(Animal Invariants Checked) |
(String Invariants Checked)
(Animal Invariants Checked)
Ruffus is (Animal Invariants Checked)
3 years old.(Animal Invariants Checked)
(Animal Invariants Checked)
(Animal Invariants Checked)
Ruffus is (Animal Invariants Checked)
7 years old.(String Invariants Checked) |
In this example, the ASSERT() macro is defined to be stripped
out if the DEBUG level is less than LOW (that is, NONE). If it is any other level, DEBUG is enabled and
ASSERT() works accordingly. EVAL() is declared to be stripped out if DEBUG is less than MEDIUM or if DEBUG
is LOW or NONE. The PRINT() macro is declared to be stripped out if DEBUGLEVEL is less than HIGH. It is used only when
the debug level is set to its maximum. You can set level to to MEDIUM to disable this macro, yet still use EVAL() and
ASSERT().
PRINT() is used within the Invariants() methods to print an informative message. EVAL() is used
to evaluate the current value of the constant integer AGE. When
using macros and preprocessor commands, remember:
- Use uppercase letters for macro names.
- Don't allow macros to have side effects.
- Surround all arguments with parentheses.
©2004 C. Germany
|