Using Objective-C Preprocessor Directives

Revision as of 20:31, 12 November 2009 by Neil (Talk | contribs) (The #define Statement)

Revision as of 20:31, 12 November 2009 by Neil (Talk | contribs) (The #define Statement)

In general terms the compilation of Objective-C programs from source code to executable binary is a three phase process. In the first phase, a tool called the preprocessor scans the human written source code and converts it to compiler friendly content and format. In the second phase the compiler generates object code (usually in the form of file with a .o filename extension) from the preprocessed source code. Finally, the linker brings all the object code modules and libraries together, resolves symbol references and creates the executable binary.

The preprocessor phase also searches for special directives written by the programmer and converts them to code that can be handled by the compiler. Each directive begins with a hash (#) character, and serves to make the Objective-C programming task easier and code easier to read and manage. In this chapter of Objective-C 2.0 Essentials we will look at some of these preprocessor directives.

The #define Statement

The #define statement can be used for a variety of purposes and is probably the most flexible of preprocessor directives. Perhaps one of the most common uses is to give frequently used constant values in a program a programmer friendly name and single point of definition. For the sake of an example, lets say you need to frequently use the boiling temperature of water (a point of contention but for the purposes of this example we will assume it to be 99.61 degrees Celsius) throughout your Objective-C program. One option might be to simply enter the constant value wherever it is needed. For example:

double liquidTemp = 99.61 - ambientTemp;

The above code works but doesn't do much to explain why the number 99.61 is used. Another problem with this approach is that if one day the program needs to be modified to use a different temperature for the boiling point of water (like I said, this is a point of contention) you will have to change every instance of 99.61 in your code, while making sure you don't also change any instances of 99.61 that aren't related to the boiling point of water.

A much better approach is to assign the value a human friendly name using the #define directive:

#define BOILTEMP 99.61

Now, whenever the boiling point is needed in the code it can be referenced by the defined name instead of the constant value:

double liquidTemp = BOILTEMP - ambientTemp;

Now when we look at the code we can ascertain what is happening simply because the constant now has a meaningful name. In addition, if we ever need to specify a different temperature we just change the #define value and the change will be picked up by all references to the definition.

A #define directive can also contain an expression. A somewhat contrived example being:

#define TWOBYTWO 2 * 2

This can then be referenced in code. Continuing with our example, the following code will output "The result is 4":

NSLog (@"Result is %i", TWOBYTWO);

Creating Macros with the #define Statement

The #define statement may also be used in a more advanced fashion to create small fragments of code called macros. These can be thought of as small functions that can accept arguments, perform operations and return results. The following definition declares a macro designed to multiply two numbers together:

#define CalcInterest(x,y) ( x * y )

Having declared the macro, we can now call it from the code:

int earnings = CalcInterest(10,5));


Changing the Objective-C Language with #define

My first ever job involved writing communications software using the C programming language (on which Objective-C is based). I inherited some code written by a former employee who loathed the C language and preferred to use another programming language (the name of which escapes me). When I looked his C code it looked nothing like any C code I had ever seen before in my life. After about an hour of trying to understand how this could be possible (surely the compiler should have refused to compile this) I realized the other programmer had using the #define compiler directive to "modify" the syntax of the C programming language to make it look more like his preferred language. Whilst I am not suggesting that you too go to these lengths it is worth knowing that such adaptability is provided by the #define preprocessor statement.

Lets begin with a simple example and write a definition that assigns the word MINUS to the minus sign (-):

#define MINUS -

Having done this, we can now perform subtractions by using the the word MINUS:

int result;

result = 20 MINUS 10;

NSLog (@"Result = %i", result);

Now suppose that you, rather like the programmer whose code I inherited, dislike the curly braces ({}) used to encapsulate code blocks on Objective-C. You could, if you wish declare the words Begin and End to represent the open and close braces as follows:

#define Begin {
#define End }

Having done this, you would then be able to write code that looks like this (and still have it compile):

#import <Foundation/Foundation.h>

#define Begin {
#define End }

int main (int argc, const char * argv[])
Begin
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

        int i;
        int j = 10;

        for (i=0; i<10; i++)
        Begin
                j += i;
                NSLog (@"j = %i", j);
        End

        [pool drain];

        return 0;
End

Undefining a Definition with #undef

To undefine a definition made previously in a source file, use the #undef statement:

#define INTEL_X86

// .... Objective-C code

#undef INTEL_X86

// ... more code

Conditional Compilation

The preprocessor supports a range of statements designed for the purposes of defining areas of code that should or should not be compiled depending on a particular setting. This is of particular importance when you have a range of code statements that are to be run when you compile you code in a debug mode.

Suppose, for example, that in debug mode you have written a number of calls to NSLog to output diagnostic information. These would be encapsulated in #ifdef / #endif statements as follows:

#ifdef DEBUG
   NSLog (@"File search complete. Found %i files", filecount");
#endif

Under normal conditions this #ifdef construct will prevent the NSLog line from being compiled. In order to enable debugging code DEBUG must be defined. This can either be achieved in the code by adding the following line:

#define DEBUG

or at the command-line when compiling the program:

gcc -framework Foundation -D DEBUG myapp.m -o myapp

Alternatively, the definition can be defined in Xcode by selecting the Project -> Edit Project Settings menu option. Within the settings dialog, select the Build option and change the Show menu to User Defined Settings. This will display any currently defined user settings. Click on the gear button in the bottom left hand corner of the window and select Add User Defined Setting.

Conditional compilation is also invaluable when writing code that needs to compile on more than one platform. In this situation it is likely that the #ifdef / #else / #endif construct will be needed:

#ifdef INTEL_X86

// ... 32-bit specific code here

#else

// ... 64-bit specific code here

#endif

Similarly, the #ifndef statement can be used to define code to be compiled when something is not defined:

#ifndef INTEL_X86

// .. 64-bit code here ...

#endif

The #import Directive

The final directive we will look at is one you will probably have seen many times already in previous chapters. The #import directive allows you to import include files into your source files. We have doen this many times when we have included the Foundation Framework headers in our example programs:

#import <Foundation/Foundation.h>

This particular import statement has the path encapsulated by < and > characters. This means that the specified header file is to be found relative to system includes directory (or any other include directories defined for the compilation). To import a header file relative to the directory containing the source file, the file name must enclosed in quotes:

#import "includes/myincludes.h<//t>

The above example imports a header file named myincludes.h located in a sub-directory of the current directory called includes. This technique may also be used to specify an absolute path (i.e. one that is not relative to the current directory):

<tt>#import "/Users/demo/objc/includes/myincludes.h"

As you develop larger and more complex applications you will, of course, be writing and importing many of your own header files. Many of these will contain the preprocessor directives we have covered in this chapter.