In this chapter, you’ll delve deeper into the capabilities of the preprocessor, and I’ll explain how you can use it to help find bugs in your code. You’ll also explore some library functions that complement some of the standard capabilities of the preprocessor.
More about the preprocessor and its operation
How to write preprocessor macros
What standard preprocessor macros are available
What logical preprocessor directives are and how you can use them
What conditional compilation is and how you can apply it
More about the debugging methods that are available to you
How you get the current date and time at runtime
Preprocessing
As you are certainly aware by now, preprocessing of your source code occurs before it’s compiled to machine instructions. The preprocessing phase can execute a range of service operations specified by preprocessing directives, which are identified by the # symbol as the first character of each preprocessor directive. The preprocessing phase provides an opportunity for manipulating and modifying your C source code prior to compilation. Once the preprocessing phase is complete and all directives have been analyzed and executed, all such preprocessing directives will no longer appear in the source code. The compiler begins the compile phase proper, which generates the machine code equivalent of your program.
You’ve already used preprocessor directives in all the examples so far, and you’re familiar with both the #include and #define directives . There are other directives that add considerable flexibility to the way in which you write your programs. Keep in mind as you proceed that all these are preprocessing operations that occur before your program is compiled. They modify the set of statements that constitute your program. They aren’t involved in the execution of your program at all.
Including Header Files
Any standard library header file name can appear between the angled brackets. If you include a header file that you don’t use, the only effect, apart from slightly confusing anyone reading the program, is to extend the compilation time.
A file introduced into your program by an #include directive may also contain #include directives. If so, preprocessing will deal with these #include directives in the same way and continue replacing such directives with the contents of the corresponding file until there are no #include directives in the program.
Defining Your Own Header Files
You can define your own header files, usually with the extension .h. You can give the file whatever name you like within the constraints of the operating system. In theory you don’t have to use the extension .h for your header files, although it’s a convention commonly adhered to by most programmers in C, so I strongly recommend you stick to it too.
Header files should not include implementation, by which I mean executable code. You create header files to contain declarations, not function definitions or initialized global data. All your function definitions and initialized global variables are placed in source files with the extension .c. You can place function prototypes, struct type definitions, symbol definitions, extern statements, and typedefs in a header file. A very common practice is to create a header file containing the function prototypes and type declarations for a program. These can then be managed as a separate unit and included at the beginning of any source file for the program. You need to avoid duplicating information if you include more than one header file in a source file. Duplicate code will often cause compilation errors. You’ll see later in this chapter in the “Conditional Compilation” section how you can ensure that any given block of code will appear only once in your program, even if you inadvertently include it several times.
This statement will introduce the contents of the file named between double quotes into the program in place of the #include directive. The contents of any file can be included in your program by this means, not just header files. You simply specify the name of the file between quotes, as shown in the example.
The difference between enclosing the file name between double quotes and using angled brackets lies in the process used to find the file. The precise operation is compiler dependent and will be described in your compiler documentation, but usually the angled brackets form will search a default header file directory that is the repository for standard header files for the required file, whereas the double quotes form will search the current source directory first and then search the default header file directory if the file was not in the current directory.
Managing Multiple Source Files
A complex program is invariably comprised of multiple source files and header files. In theory you can use an #include directive to add the contents of another .c source file to the current .c file, but it’s not usually necessary or even desirable. You should only use #include directives in a .c file to include header files. Of course, header files can and often do contain #include directives to include other header files into them.
Each .c file in a complex program will typically contain a set of related functions. The preprocessor inserts the contents of each header identified in an #include directive before compilation starts. The compiler creates an object file from each .c source file. When all the .c files have been compiled, the object files are combined into an executable module by the linker.
If your C compiler has an interactive development environment with it, it will typically provide a project capability, where a project contains and manages all the source and header files that make up the program. This usually means you don’t have to worry too much about where files are stored for the stages involved in creating an executable. The development environment takes care of it. For larger applications though, it’s better still if you create a decent folder structure yourself instead of letting the IDE put all the files in the same folder.
External Variables
These statements don’t create these variables—they just identify to the compiler that these names are defined elsewhere, and this assumption about these names should apply to the rest of this source file. The variables you specify as extern must be declared and defined somewhere else in the program, usually in another source file. If you want to make these external variables accessible to all functions within the current file, you should declare them as external at the very beginning of the file, prior to any of the function definitions. With programs consisting of several files, you could place all initialized global variables at the beginning of one file and all the extern statements in a header file. The extern statements can then be incorporated into any program file that needs access to these variables by using an #include statement for the header file.
Only one definition of each global variable is allowed. Of course, global variables may be declared as external in as many files as necessary.
Static Functions
This function can only be called in the .c file in which this definition appears. Without the static keyword, the function could be called from any function in any of the source files that make up the program.
You can apply the static keyword in a function prototype, and the effect is the same.
Substitutions in Your Program Source Code
This is perfectly correct as a preprocessor directive but is sure to result in compiler errors here.
The dimensions of both arrays can be changed by modifying the single #define directive, and of course the array declarations that are affected can be anywhere in the program file. The advantages of this approach in a large program involving dozens or even hundreds of functions should be obvious. Not only is it easy to make a change but using this approach also ensures that the same value is being used throughout a program. This is especially important with large projects involving several programmers working together to produce the final product.
The difference between this approach and using the #define directive is that MAXLEN here is no longer a token but is a variable of a specific type with the name MAXLEN. The MAXLEN in the #define directive does not exist once the source file has been preprocessed because all occurrences of MAXLEN in the code will be replaced by 256. You will find that the preprocessor #define directive is often a better way of specifying array dimensions because an array with a dimension specified by a variable, even a const variable, is likely to be interpreted as a variable-length array by the compiler.
This will cause any occurrence of Black in your program to be replaced with White. The sequence of characters that is to replace the token identifier can be anything at all. The preprocessor will not make substitutions inside string literals though.
Macros
You could use this directive to specify a printf_s() function call to output the value of an integer at various points in your program. A common use for this kind of macro is to provide a simple representation of a complicated function call in order to enhance the readability of a program.
Macros That Look Like Functions
Now everything will work as it should. The inclusion of the outer parentheses may seem excessive, but because you don’t know the context in which the macro expansion will be placed, it’s better to include them. If you write a macro to sum its parameters, you will easily see that without the outer parentheses, there are many contexts in which you will get a result that’s different from what you expect. Even with parentheses, expanded expressions that repeat a parameter, such as the one you saw earlier that uses the conditional operator, will still not work properly when the argument involves the increment or decrement operator.
Strings As Macro Arguments
There will be no substitution for MYSTR in the printf_s() function argument in this case. Anything in quotes in your program is assumed to be a literal string, so it won’t be analyzed during preprocessing.
You may be wondering why this apparent complication has been introduced into preprocessing. Well, without this facility, you wouldn’t be able to include a variable string in a macro definition at all. If you were to put the double quotes around the macro parameter, it wouldn’t be interpreted as a variable; it would be merely a string with quotes around it. On the other hand, if you put the quotes in the macro expansion, the string between the quotes wouldn’t be interpreted as an identifier for a parameter; it would be just a string constant. So what might appear to be an unnecessary complication at first sight is actually an essential tool for creating macros that allows strings between quotes to be created.
This is possible because the preprocessing phase is clever enough to recognize the need to put " at each end to get a string that includes double quotes to be displayed correctly.
Joining Two Arguments in a Macro Expansion
This might be applied to synthesizing a variable name for some reason or when generating a format control string from two or more macro parameters.
Preprocessor Directives on Multiple Lines
A preprocessor directive must be a single logical line, but this doesn’t prevent you from using the statement continuation character, which is just a backslash, . In doing so, you can span a directive across multiple physical lines, using the continuation character to designate those physical lines as a single, logical line.
Here, the directive definition continues on the second physical line with the first nonblank character found, so you can position the text on the second line to wherever you feel looks like the nicest arrangement. Note that the must be the last character on the line, immediately before you press Enter. The result is seen by the compiler as a single, logical line.
Logical Preprocessor Directives
The previous example you looked at appears to be of limited value, because it’s hard to envision when you would want to simply join var to 123. After all, you could always use one parameter and write var123 as the argument. One aspect of preprocessing that adds considerably more potential to the previous example is the possibility for multiple macro substitutions where the arguments for one macro are derived from substitutions defined in another. In the previous example, both arguments to the join() macro could be generated by other #define substitutions or macros. Preprocessing also supports directives that provide a logical if capability, which vastly expands the scope of what you can do during the preprocessing phase.
Conditional Compilation
If the specified identifier is defined by a #define directive prior to this point, statements that follow the #if are included in the program code until the directive #endif is reached. If the identifier isn’t defined, the statements between the #if and the #endif will be skipped. This is the same logical process you use in C programming, except that here you’re applying it to the inclusion or exclusion of program statements in the source file.
Here the statements following the #if down to the #endif will be included if identifier hasn’t previously been defined. This provides you with a method of avoiding duplicating functions, or other blocks of code and directives, in a program consisting of several files or ensuring bits of code that may occur repeatedly in different libraries aren’t repeated when the #include statements in your program are processed.
If the identifier block1 hasn’t been defined, the sequence of statements following the #define directive for block1 will be included and processed, and the identifier block1 will be defined. Any subsequent occurrence of this directive to include the same group of statements won’t include the code because the identifier block1 now exists.
The #define directive for block1 doesn’t need to specify a substitution value in this case. For the conditional directives to operate, it’s sufficient for block1 to appear in a #define directive without a substitution string. You can now include this block of code anywhere you think you might need it, with the assurance that it will never be duplicated within a program. The preprocessing directives ensure this can’t happen.
With this arrangement, it is impossible for the contents of MyHeader.h to appear more than once in a source file.
You should always protect code in your own header files in this way.
Testing for Multiple Conditions
This will evaluate to true if both block1 and block2 have previously been defined, so the code that follows such a directive won’t be included unless this is the case. You can use the || and ! operators in combination with && if you really want to go to town.
Undefining Identifiers
If block1 was previously defined, it is no longer defined after this directive. The ways in which these directives can all be combined to useful effect are only limited by your own ingenuity.
There are alternative ways of writing these directives that are slightly more concise. You can use whichever of the following forms you prefer. The directive #ifdef block is the same as the #if defined block. And the directive #ifndef block is the same as the #if !defined block.
Testing for Specific Values for Identifiers
The printf_s() statement will be included in the program here only if the identifier CPU has been defined as Intel_i7 in a previous #define directive.
Multiple-Choice Selections
In this case, one or the other of the printf_s() statements will be included, depending on whether or not CPU has been defined as Intel_i7.
With this sequence of directives, the output of the printf_s() statement will depend on the value assigned to the identifier Country, in this case US.
We need to be careful about these evaluations that if the first expression evaluates to true, then the rest of expressions will not be evaluated at all; therefore, we may have invalid expressions in our source code, but it will compile successfully. This behavior was clarified in C17. We can see in the preceding example, the second expression is wrong on purpose, and currency will be printed as Dollar.
Standard Preprocessing Macros
There are usually a considerable number of standard preprocessing macros defined, which you’ll find described in your compiler documentation. I’ll mention those that are of general interest and that are available in a conforming implementation.
The __DATE__ macro generates a string representation of the date in the form Mmm dd yyyy when it’s invoked in your program. Here Mmm is the month in characters, such as Jan, Feb, and so on. The pair of characters dd is the day in the form of a pair of digits 1–31, where single-digit days are preceded by a space. Finally, yyyy is the year as four digits—2012, for example.
A similar macro, __TIME__ , provides a string containing the value of the time when it’s invoked, in the form hh:mm:ss, which is evidently a string containing pairs of digits for hours, minutes, and seconds, separated by colons. Note that the time is when the compiler is executed, not when the program is run.
Note that both __DATE__ and __TIME__ have two underscore characters at the beginning and the end. Once the program containing this statement is compiled, the values that will be output are fixed until you compile it again. On each execution of the program, the time and date that it was last compiled will be output. Don’t confuse these macros with the time() function, which I’ll discuss later in this chapter in the section “Date and Time Functions.”
The __FILE__ macro represents the name of the current source file as a string literal. This is typically a string literal comprising the entire file path, such as "C:\Projects\Test\MyFile.c".
If fopen_s() fails, there will be a message specifying the source file name and the line number within the source file where the failure occurred.
_Generic Macro
Since C11, _Generic was added; thus, it can have dynamic type macros at compilation time. Hence it is being introduced a more flexible typing (that is new in C) for macros. This new feature behaves like a function. Well, here there is another vision about macros vs. functions and the trade-off between both approaches).
As you can see, the macro can return a function from your code or from a standard library (math.h).
_Generic is not supported by Visual Studio 2019 yet; please use GCC or Pelles for this example.
Debugging Methods
Most of your programs will contain errors, or bugs , when you first complete them. Removing such bugs from a program can represent a substantial proportion of the time required to write the program. The larger and more complex the program, the more bugs it’s likely to contain and the more time it will take to get the program to run properly. Very large programs, such as those typified by operating systems, or complex applications, such as word processing systems or even C program development systems, can be so involved that all the bugs can never be eliminated. You may already have experience of this in practice with some of the systems on your own computer. Usually these kinds of residual bugs are relatively minor, with ways in the system to work around them.
Your approach to writing a program can significantly affect how difficult it will be to test. A well-structured program consisting of compact functions, each with a well-defined purpose, is much easier to test than one without these attributes. Finding bugs will also be easier in a program that has extensive comments documenting the operation and purpose of its component functions and has well-chosen variable and function names. Good use of indentation and statement layout also makes testing and fault finding simpler. It’s beyond the scope of this book to deal with debugging comprehensively, but in this section I’ll introduce the basic ideas that you need to be aware of.
Integrated Debuggers
Tracing program flow : This capability allows you to execute your program one source statement at a time. It operates by pausing execution after each statement and continuing with the next statement after you press a designated key. Other provisions of the debug environment will usually allow you to display information easily, pausing to show you what’s happening to the data in your program.
Setting breakpoints: Executing a large or complex program one statement at a time can be very tedious. It may even be impossible in a reasonable period of time. All you need is a loop that executes 10,000 times to make it an unrealistic proposition. Breakpoints provide an excellent alternative. With breakpoints, you define specific selected statements in your program at which a pause should occur to allow you to check what’s happening. Execution continues to the next breakpoint when you press a specified key.
Setting watches: This sort of facility allows you to identify variables that you want to track the value of as execution progresses. The values of the variables you select are displayed at each pause point in your program. If you step through your program statement by statement, you can see the exact point at which values are changed or perhaps not changed when you expect them to be.
Inspecting program elements: It may also be possible to examine a wide variety of program components. For example, at breakpoints the inspection can show details of a function such as its return type and its arguments. You can also see details of a pointer in terms of its address, the address it contains, and the data stored at the address contained in the pointer. Seeing the values of expressions and modifying variables may also be provided for. Modifying variables can help to bypass problem areas to allow other areas to be executed with correct data, even though an earlier part of the program may not be working properly .
The Preprocessor in Debugging
By using conditional preprocessor directives, you can arrange for blocks of code to be included in your program to assist in testing. In spite of the power of the debug facilities included with many C development systems, the addition of tracing code of your own can still be very useful. You have complete control of the formatting of data to be displayed for debugging purposes, and you can even arrange for the kind of output to vary according to conditions or relationships within the program.
How It Works
This defines the random() macro in terms of the rand() function that’s declared in stdlib.h. The rand() function generates random numbers in the range 0–RAND_MAX, which is a constant defined in stdlib.h. The macro maps values from this range to produce values from 0 to NumValues-1. You cast the value from rand() to double to ensure that computation will be carried out as type double, and you cast the overall result back to int because you want an integer in the program.
I defined random() as a macro to show you how, but it would be better defined as a function because this would eliminate any potential problems that might arise with argument values to the macro.
The first defines a symbol that specifies the number of iterations in the loop that executes one of three functions at random. The other three are symbols that control the selection of code to be included in the program. Defining the test symbol causes code to be included that will output the value of the index that selects a function. Defining testf causes code that traces function calls to be included in the function definitions. When the repeatable symbol is defined, the srand() function is called with a fixed seed value, so the rand() function will always generate the same pseudo-random sequence, and the same output will be produced on successive runs of the program. Having repeatable output during test runs of the program obviously makes the testing process somewhat easier. If you remove the directive that defines the repeatable symbol, srand() will be called with the current time value as the argument, so the seed will be different each time the program executes, and you will get a different output on each execution of the program.
This defines an array of pointers to functions that have two parameters of type int and a return value of type int. The array is initialized using the names of three functions, so the array will contain three elements.
If repeatable is defined, the statement that calls srand() with the argument value 1 will be included in the source for compilation. This will result in the same output each time you execute the program. Otherwise, the statement with the result of the time() function as the argument will be included, and you will get a different output each time you run the program.
This executes the random() macro with element_count as the argument. This is the number of elements in the pfun array and is calculated immediately before the loop. The preprocessor will substitute element_count in the macro expansion before the code is compiled. For safety, there is a check that we do indeed get a valid index value for the pfun array .
These include the printf_s() statement in the code when the test symbol is defined. If you remove the directive that defines test, the printf_s() call will not be included in the program that is compiled.
The function definition includes an output statement if the testf symbol is defined. You can therefore control whether the statements in the #ifdef block are included here independently from the output block in main() that is controlled by test. With the program as written with both test and testf defined, you’ll get trace output for the random index values generated and a message from each function as it’s called, so you can follow the sequence of calls in the program exactly.
You can have as many different symbolic constants defined as you wish. As you’ve seen previously in this chapter, you can combine them into logical expressions using the #ifdef form of the conditional directive.
Assertions
An assertion is an error message that is output when some condition is met. There are two kinds of assertions: compile-time assertions and runtime assertions. I’ll discuss the latter first because they are used more widely.
Runtime Assertions
The expression will be true (nonzero) if a is equal to b. If a and b are unequal, the argument to the macro will be false, and the program will be terminated with a message relating to the assertion that includes the text of the argument to the macro, the source file name, the line number, and the name of the function in which the assert() appears. Termination is achieved by calling abort(), so it’s an abnormal end to the program. When abort() is called, the program terminates immediately. Whether stream output buffers are flushed, open streams are closed, or temporary files are removed, it is implementation dependent, so consult your compiler documentation on this.
You can see that the function name and the line number of the code are identified as well as the condition that was not met.
Switching Off Assertions
This code snippet will cause all assert() macros in your code to be ignored.
By including the directive to undefine NDEBUG, you ensure that assertions are enabled for your source file. The #undef directive must appear before the #include directive for assert.h to be effective.
Compile-Time Assertions
When the constant expression evaluates to zero, compilation stops, and the error message is output.
CHAR_MIN is defined in limits.h and is the minimum value for type char. When char is an unsigned type, CHAR_MIN will be zero, and when it is a signed type, it will be negative. Thus, this will cause compilation to be halted and an error message that includes your string to be produced when type char is signed.
The static_assert is defined in assert.h as _Static_assert, which is a keyword. You can use _Static_assert instead of static_assert without including assert.h into your source file.
I can demonstrate runtime assertions in operation with a simple example.
How It Works
Apart from the assert() statement, the program doesn’t need much explanation because it simply displays the values of x and y in the for loop. The program is terminated by the assert() macro as soon as the condition x < y becomes false. As you can see from the output, this is when x reaches the value 5. The macro displays the output on stderr. Not only do you get the condition that failed displayed but you also get the file name and line number in the file where the failure occurred. This is particularly useful with multifile programs in which the source of the error is pinpointed exactly.
Assertions are often used for critical conditions in a program in which, if certain conditions aren’t met, disaster will surely ensue. You would want to be sure that the program wouldn’t continue if such errors arise.
This must be placed before the #include directive for assert.h to be effective. With this #define at the beginning of Program 13.2, you’ll see that you get output for all the values of x from 0 to 19 and no diagnostic message.
Date and Time Functions
The preprocessor macros for the date and the time produce values that are fixed at compile time. The time.h header declares functions that produce the time and date when you call them. They provide output in various forms from the hardware timer in your PC. You can use these functions to obtain the current time and date, to calculate the time elapsed between two events, and to measure how long the processor has been occupied performing a calculation.
Getting Time Values
This function returns the processor time (not the elapsed time) used by the program since some implementation-defined reference point, often since execution began. You typically call the clock() function at the start and end of some process in a program, and the difference is a measure of the processor time consumed by the process. The return value is of type clock_t, which is an integer type that is defined in time.h. Your computer will typically be executing multiple processes at any given moment. The processor time is the total time the processor has been executing on behalf of the process that called the clock() function. The value that is returned by the clock() function is measured in clock ticks . To convert this value to seconds, you divide it by the value that is produced by the macro CLOCKS_PER_SEC, which is also defined in time.h. The value produced by CLOCKS_PER_SEC is the number of clock ticks in 1 second. The clock() function returns –1 if an error occurs. In C17, it was clarified and added that if the value cannot be represented, the function returns an unspecified value; this may be due to overflow of the clock_t type.
This fragment stores the total processor time used by the process in cpu_time . The cast to type double is necessary in the last statement to get the correct result.
As you’ve seen, the time() function returns the calendar time as a value of type time_t. The calendar time is the current time usually measured in seconds since a fixed time on a particular date. The fixed time and date is often 00:00:00GMT on January 1, 1970, and this is typical of how time values are defined. However, the reference point is implementation defined, so check your compiler and library documentation to verify this.
If the argument isn’t NULL, the current calendar time is also stored in timer. The type time_t is defined in the header file and is often equivalent to type long.
The function will return the value of T2 - T1 expressed in seconds as a value of type double. This value is the time elapsed between the two time() function calls that produce the time_t values, T1 and T2.
How It Works
This program illustrates the use of the functions clock(), time(), and difftime(). The time() function usually returns the current time in seconds, and when this is the case, you may not get values less than 1 second. Depending on the speed of your machine, you may want to adjust the number of iterations in the loop to reduce or increase the time required to execute this program. Note that the clock() function may not be a very accurate way of determining the processor time used in the program. You also need to keep in mind that measuring elapsed time using the time() function can be a second out.
Casting the values of cpu_start and calendar_start to type long long obviates any formatting problems that might arise because of the types that clock_t and time_t are implemented as.
The inner loop calls the sqrt() function that is declared in the math.h header iterations times , so this is just to occupy some processor time. If you are leisurely in your entry of a response to the prompt for input, this should extend the elapsed time. Note the newline escape sequence in the beginning of the first argument to scanf_s(). If you leave this out, your program will loop indefinitely, because scanf_s() will not ignore whitespace characters in the input stream buffer.
Finally, you output the final values returned by clock() and time() and calculate the processor and calendar time intervals. The library that comes with your C compiler may well have additional nonstandard functions for obtaining processor time that are more accurate than the clock() function .
Note that the processor clock can wrap around, and the resolution with which processor time is measured can vary between different hardware platforms. For example, if the processor clock is a 32-bit value that has a microsecond resolution, the clock will wrap back to zero roughly every 72 minutes.
Getting the Date
The function accepts a pointer to a time_t variable as an argument that contains a calendar time value returned by the time() function. It returns a pointer to a 26-character string containing the day, the date, the time, and the year, which is terminated by a newline and ' '.
Both arguments must be non-NULL . The structure contains at least the members listed in Table 13-1.
Members of the tm Structure
Member | Description |
---|---|
tm_sec | Seconds (0–60) after the minute on 24-hour clock. This value goes up to 60 for positive leap-second support |
tm_min | Minutes after the hour on 24-hour clock (0–59) |
tm_hour | The hour on 24-hour clock (0–23) |
tm_mday | Day of the month (1–31) |
tm_mon | Month (0–11) |
tm_year | Year (current year minus 1900) |
tm_wday | Weekday (Sunday is 0; Saturday is 6) |
tm_yday | Day of year (0–365) |
tm_isdst | Daylight saving flag. Positive for daylight saving time, 0 for not daylight saving time, and negative for not known |
All the members are of type int. The localtime() function returns a pointer to the same structure each time you call it, and the structure members are overwritten on each call. If you want to keep any of the member values, you need to copy them elsewhere before the next call to localtime(), or you could create your own tm structure and save the whole lot if you really need to. You supply the structure as an argument to localtime_s() so you control whether you reuse a structure object. This makes operations simpler and less error prone.
The time that localtime() and localtime_s() produce is local to where you are. If you want to get the time in a tm structure that reflects UTC (Coordinated Universal Time), you can use the gmtime() function or, better, the optional gmtime_s() function . These expect the same arguments as the localtime() and localtime_s() functions and return a pointer to a tm structure .
TIME_UTC and timespec_get are new in C11; TIME_UTC can be as argument to function timespec_get(&ts, TIME_UTC).
The timespec_get function sets the interval pointed to by ts to hold the current calendar time based on the specified time base.
You’ve defined arrays of strings to hold the days of the week and the months. You use the appropriate member of the structure that has been set up by the call to the localtime_s() function . You use the day in the month and the year values from the structure directly. You can easily extend this to output the time.
The asctime_s() stores the string in str, which must be an array with at least 26 elements and smaller than RSIZE_MAX; size is the number of elements in str. The function returns 0 when everything works and a nonzero integer when it does not. The function works for tm structures where the year is from 1000 to 9999, so it should be okay for a while yet! The string that results is of the same form as that produced by ctime().
How It Works
You define arrays of strings in main() for the days of the week, the months in the year, and the suffix to be applied to a date value. Each statement defines an array of pointers to char. You could omit the array dimensions in the first two declarations, and the compiler would compute them for you, but in this case you’re reasonably confident about both these numbers, so this is an instance in which putting them in helps to avoid an error. The const qualifier specifies that the strings pointed to are constants and should not be altered in the code.
The enumeration constants, st, nd, rd, and th, will be assigned values 0–3 by default, so we can use the sufsel variable as an index to access elements in the suffix array. The names for the enumeration constants make the code a little more readable.
The values for the members of this structure will be set by the localtime_s() function .
The sole purpose of this is to select what to append to the date value. Based on the member tm_mday , the switch selects an index to the suffix array for use when outputting the date by setting the sufsel variable to the appropriate enumeration constant value.
The day, the date, and the time are displayed, with the day and month strings obtained by indexing the appropriate array with the corresponding structure member value. You add 1900 to the value of the tm_year member because this value is measured relative to the year 1900.
Getting the Day for a Date
You pass the address of a tm structure object to the function with the tm_mon , tm_mday , and tm_year members set to values corresponding to the date you are interested in. The values of the tm_wday and tm_yday members of the structure will be ignored, and if the operation is successful, the values will be replaced with the values that are correct for the date you have supplied. The function returns the calendar time as a value of type time_t if the operation is successful or –1 if the date cannot be represented as a time_t value, causing the operation to fail. Let’s see it working in an example.
How It Works
The date values are read directly into the members of the birthday structure. The month should be zero based and the year relative to 1900, so you adjust the values stored accordingly.
The if statement checks whether the function returns –1, indicating that the operation has failed. In this case, you simply output a message and terminate the program. Finally, you display the day corresponding to the birth date that was entered in the same way as in the previous example.
Summary
In this chapter, I discussed the preprocessor directives that you use to manipulate and transform the code in a source file before it is compiled. Because the chapter is primarily about preprocessing, there is no “Designing a Program” section. Your standard library header files are an excellent source of examples of coding preprocessing directives. You can view these examples with any text editor. Virtually all of the capabilities of the preprocessor are used in the libraries, and you’ll find a lot of other interesting code there too. It’s also useful to familiarize yourself with the contents of the libraries, as you can find many things not necessarily described in the library documentation. If you want to know what the type clock_t is, for example, just look in time.h.
The debugging capability that the preprocessor provides is useful, but you will find that the debugging tools provided with many C programming systems are much more powerful. For serious program development, the debugging tools are as important as the efficiency of the compiler. We will wrap up our discussion in the next chapter on advanced and specialized areas of programming.
The following exercises enable you to try out what you learned in this chapter. If you get stuck, look back over the chapter for help. If you’re still stuck, you can download the solutions from the Source Code/Download area of the Apress website (www.apress.com), but that really should be a last resort.
Exercise 13-1. Define a macro, COMPARE(x, y), that will result in the value –1 if x < y, 0 if x == y, and 1 if x > y. Write an example to demonstrate that your macro works as it should. Can you see any advantage that your macro has over a function that does the same thing?
Exercise 13-2. Define a function that will return a string containing the current time in 12-hour format (a.m./p.m.) if the argument is 0 and in 24-hour format if the argument is 1. Demonstrate that your function works with a suitable program.
Exercise 13-3. Define a macro, print_value(expr), that will output on a new line expr = result where result is the value that results from evaluating expr. Demonstrate the operation of your macro with a suitable program.