This chapter focuses on issues related to preprocessing procedures. Run the Preprocessing Program before compiling the program. Maybe you have never seen this program before, because it usually runs behind the scenes, and programmers cannot see it. However, this program is very useful.
The Preprocessing Program will modify your program according to the preprocessing commands in the source code. Preprocessing commands (such as # define) provide specific commands for the Preprocessing Program and tell it how to modify your source code. The Preprocessing Program reads all contained files and source code to be compiled, and processes the generated source code pre-processing version. In this version, the macro and constant identifier are replaced by the corresponding code and value. If the source code contains conditional preprocessing commands (such as # If), The Preprocessing Program first judges the conditions and then modifies the source code accordingly.
Preprocessing programs have many very useful functions, such as macro definition, Conditional compilation, inserting predefined environment variables in the source code, enabling or disabling a compilation option, and so on. For professional programmers, a deep understanding of the features of preprocessing programs is one of the keys to creating fast and efficient programs.
When reading this chapter, remember the technologies used in this chapter (and some common traps mentioned) to better utilize the functions of preprocessing programs.
5.1 What is macro )? How to Use Macros?
Macros are pre-processing commands that provide a mechanism to replace strings in source code. macros are defined using the # define statement. The following is an example of macro definition:
# Define version-Stamp "1.02"
The macro defined in the preceding example is often called an identifier. In the above example, the identifier version_stamp represents the string "1.02" -- during compilation and preprocessing, each version_stamp identifier in the source code will be replaced by the string "1.02.
The following is an example of another macro definition:
# Define cube (x), (x) * (x ))
In the preceding example, a macro named cube is defined. It has a parameter X. Cube macro has its own macro, that is, (x) * (x)-during compilation and preprocessing, each cube (x) macro in the source code will be (X) * (x) is replaced.
Using macros has the following advantages;
(1) You can skip many typing operations when entering source code.
(2) Because macros only need to be defined once, but can be used multiple times, the use of macros can enhance the program's accessibility and reliability.
(3) using macros does not require additional overhead, because the Code represented by macros is expanded only in the place where macros appear, so it does not cause jump in the program.
(4) macro parameters are not sensitive to types, so you do not have to consider which data types are passed to macros.
Note that there must be no space between the macro name and the brackets that enclose the parameter. In addition, to avoid ambiguity during macro translation, the macro should also be enclosed in parentheses. For example, it is incorrect to define a cube macro like in the following example:
Denne cube (x) x * x
Take care of the parameters passed to the macro. For example, a common error is to pass the auto-Increment Variable to the macro. See the following example:
# Include <stdio. h>
# Include cube (x) (x * X)
Void main (void );
Void main (void)
{
Int X, Y;
X = 5;
Y = cube (++ X );
Printfc 'y is % d/N ". y );
}
In the preceding example, what is the value of Y? In fact, Y is equal to 125 (5 cubes) or 336 (6*7*8. Because the auto-increment operation is performed when variable X is passed to the macro as a parameter, the cube macro in the preceding example is actually expanded in the following form:
Y = (++ X) * (++ X ));
In this way, X is auto-incrementing every time X is referenced, so the result you get is far different from what you expected. In the preceding example, X is referenced three times, in addition, the auto-increment operator is used. Therefore, when the macro code is expanded, X is actually 8, and you will get 8 cubes instead of 5 cubes.
The above errors are common. I have personally seen people with many years of C programming experience make such mistakes. It is very difficult to check such errors in the program, so you should pay full attention to them. You 'd better try the above example and see with your own eyes the surprising result value (512 ).
Macros can also use some special operators, such as the string operators "#" And. The connection operator "#". The "#" operator can convert macro parameters to strings with double quotation marks. See the following example:
Define debug_value (v) printf (# V "is equal to % d./N", V)
You can use the debug_value macro in the program to check the value of the variable. See the following example:
Int x = 20;
Debug_value (X );
The preceding statement prints "X is equal to 20" on the screen ". This example shows that the "#" operator used by macros is a very convenient debugging tool.
The "#" operator is used to connect two independent strings into one. For details, see section 5.16.
See:
5.10 is it better to use macros or functions?
5.16 What is the function of the connection operator?
5.17 how to create a non-sensitive macro?
5.18 what is a standard predefined macro?
5.31 how to cancel a Defined Macro?
5.2 what is the role of Preprocessor?
C language Preprocessing Program is used to modify your source code according to the preprocessing instructions in the source code. A pre-processing command is a command statement (such as # define) that instructs the pre-processing program to modify the source code. Before compiling a program, the program will automatically run the Preprocessing Program and compile the program for preprocessing. This part of the work is invisible to the programmer.
The Preprocessing Program reads all contained files and the source code to be compiled, and then generates the source code pre-processing version. In the pre-processing version, the macro and constant identifiers have all been replaced by the corresponding code and value. If the source code contains conditional preprocessing commands (for example, # If), The Preprocessing Program first judges the conditions and then modifies the source code accordingly.
The following example uses multiple preprocessing commands:
# Include <stdio. h>
# Define true 1
# Define false (! True)
# Define greater (A, B) (a)> (B )? (True): (false ))
# Define pig-Latin false
Void main (void );
Void main (void)
{
Int X, Y;
# If pig_latin
Printf ("easeplay enternay ethay aluevay orfay xnay :");
Scanf ("% d", & X );
Printf ("easeplay enternay ethay aluevay orfay ynay :");
Scanf ("% d", & Y );
# Else
Printf ("Please enter the value for X :");
Scanf ("% d", & X );
Printf ("Please enter the value for Y :");
Scanf ("% d", & Y );
# Endif
If (greater (x, y) = true)
{
# If pig-Latin
Printf ("xnay Islay eatergray anthay ynay! /N ");
# Else
Printf {"X is greater than y! /N ");
# Endif
}
Else
{
# If pig_latin
Printf ("xnay Islay otnay eatergray anthay ynay! /N ");
# Else
Printf ("X is not greater than y! /N ");
# Endif
}
}
In the preceding example, three identifier constants (true, false, and pig_latin) and a macro (greater (A, B) are defined through preprocessing commands, and a set of Conditional compilation commands are used. When the Preprocessing Program processes the source code in the previous example, it first reads stdio. h header file, interpret the pre-processing commands, replace all the identifier constants and macros with the corresponding values and code, and finally judge whether pig_latin is true, and then decide whether to use Latin or
English.
If pig_latin is false, the preprocessing version of the previous example is as follows:
/* Here is where all the include files
Wocould be expanded .*/
Void main (void)
{
Int X, Y;
Printf ("Please enter the value for X :");
Scanf ("% d", & X );
Printf ("Please enter the value for Y :");
Scanf ("% d", & Y ),
If (x)> (y )? (1 ):(! 1) = 1)
{
Printf ("X is greater than y! /N ");
}
Else
{
Printf {"X is not greater than y! /N ");
}
}
Most compilers provide a command line option or an independent pre-processing program that allows you to start only the pre-processing program and save the pre-processing version of the source code to a file. You can use this method to view the preprocessing version of the source code, which is useful for debugging errors related to macros or other preprocessing commands.
See:
5.3 how to avoid multiple times containing the same head file?
5.4 can I use the # include command to include files whose type name is not ". H?
What are the differences between 5.12 # include <File> and # include "file?
5.22 preprocessing command # What is the role of Pragma?
5.23 # What is the role of line?
5.3 how to avoid multiple times containing the same head file?
Using the # ifndef and # define commands, you can avoid multiple times containing the same head file. When creating a header file, you can use the # define command to define a unique identifier name for it. You can run the # ifndef command to check whether the identifier name has been defined. If it has been defined, it indicates that the header file has been included, so do not include the header file again. Otherwise, this identifier name is defined to avoid further inclusion of this header file. The following header files use this technology:
# Ifndef _ filename_h
# DEFINE _ filename_h
# Define ver_num "1. 00. 00"
# Define rel_date "08/01/94"
# If _ WINDOWS _
# Define OS _ver "Windows"
# Else
# Define OS _ver "dos"
# Endif
# Endif
When the Preprocessing Program processes the preceding header file, it first checks whether the identifier name _ filename_h has been defined-if not, The Preprocessing Program processes subsequent statements, until the last # endif statement; otherwise, the Preprocessing Program no longer processes subsequent statements.
See:
5.4 can I use the # include command to include files whose type name is not ". H?
5, 12 # What are the differences between include <File> and # include "file?
5.14 can contain files be nested?
5.15 how many layers of files can be nested?
5. 4 can I use the # include command to include a file whose type name is not ". H?
The Preprocessing Program will contain any file specified by the # include command. For example, if the program contains the following statement, the Preprocessing Program will contain the macros. inc file.
# Include <macros. inc>
However, it is best not to use the # include command to include files whose type name is not ". H", because it is difficult to distinguish which files are used for compilation and preprocessing. For example, the person Who modifies or debugs your program may not be able to view macros. the macro definition in the inc file, and the type name is ". H "file, but he cannot find it in macros. macro defined in the inc file. If you change the macros. inc file to macros. H, this problem can be avoided.
See:
5.3 how to avoid multiple times containing the same head file?
5.12 # What are the differences between include <File> and # include "file?
5, 14 can contain files embedded?
5.15 how many layers of files can be nested?
5.5 what are the advantages of using the # define command to describe constants?
If the # define command is used to describe constants, a constant can be used in the program multiple times. When maintaining the program, you only need to modify the # define statement, instead of modifying all the instances of constants one by one. For example, if you want to use PI (about 3.14159) multiple times in a program, you can describe a constant as follows:
# Define PI 3.14159
To improve the PI precision, you only need to modify the PI value defined in The # define statement, so you do not have to modify it everywhere in the program. Generally, it is better to place the # define statement in a header file so that multiple modules can use the same constant.
Another benefit of using the # define command is that constants consume the least amount of memory, because constants defined in this way will directly enter the source code without allocating variable space in the memory.
However, this method also has a disadvantage, that is, most debugging programs cannot check constants described in # define.
The # under command can be used to cancel constants described by the # define command. This means that if the originally defined identifier (such as null) does not meet your requirements, you can first cancel the original definition and then define a new identifier as required. For details, see section 5.31.
See:
5.6 what are the advantages of using the enum keyword to describe constants?
5.7 what are the advantages of using the enum keyword to describe constants compared to using the # define command to describe constants?
5.31 how to cancel a Defined Macro?
5.6 what are the advantages of using the enum keyword to describe constants?
Using the enum keyword to describe constants (that is, enumeration constants) has three benefits:
(1) constants described with the enum keyword are automatically generated by the compiler. programmers do not need to manually assign values to constants one by one.
(2) using the enum keyword to describe constants makes the program clearer and easier to read, because an Enum constant is also defined as an enumeration type identifier.
(3) It is very useful to check enumeration constants during program debugging, especially when you have to manually check the constant values in the header file.
However, the enum keyword indicates that constants occupy more memory than the # define command, because the former requires memory allocation to store constants.
The following is an example of an enumerated constant used to detect program errors:
Enum error_code
{
Out_of_memory,
Insufficient_disk_space,
Logic_error,
File_not_found
};
Compared to the description of constants with # define, it is also advantageous to use Enum to describe constants. This will be described in more detail in 5.7.
See:
5.5 what are the advantages of using the # define command to describe constants?
5.7 what are the advantages of using the enum keyword to describe constants compared to using the # denne command to describe constants?
5.7 what are the advantages of using the enum keyword to describe constants compared to using the # define command to describe constants?
Compared with the description of constants (that is, identifier constants) using the # define command, the use of the enum keyword to describe constants (that is, enumeration constants) has the following benefits:
(1) make the program easier to maintain, because enumeration constants are automatically generated by the Compilation Program, and the identifier constants must be manually assigned by the programmer. For example, you can define a group of enumerated constants as possible error numbers in the program. See the following example:
Enum error_code
{
Out_of_memory,
Insufficient_disk_space,
Logic_error,
File + not_found
};
In the preceding example, enumerated constants such as out_of_memory are automatically assigned to 0, 1, 2, and 3 by the compiled program.
Similarly, you can use the # define command to describe a similar set of constants. See the following example:
# Define out_of_memory 0
# Define insufficient_disk_space 1
# Define logic_error 2
# Define file_not_found 3
The results of the above two examples are the same.
Suppose you want to add two new constants, such as drive_not_ready and corrupt_file. If the constant is described by the enum keyword, you can insert the two constants at any position of the original constant, because the compiler automatically assigns a unique value to each enumerated constant; if the constant is described using the # define command, you have to manually assign values to the new constant. In the above example, you do not care about the actual value of a constant, but only about whether the value of a constant is unique. Therefore, use the enum keyword to describe a constant to make the program easier to maintain, it also prevents different constants from being assigned the same value.
(2) make the program easier to read, so that it is easier for others to modify your program. See the following example:
Void copy_file (char * source_file_name, char * dest_file_name)
{
......
Error_code, err;
......
If (drive_ready ()! = True)
Err = drive_not_ready;
......
}
In the above example, we can see from the definition of the variable err; the value assigned to err can only be a value in the enumerated type error_code. Therefore, when another programmer wants to modify or add the function of the previous example, he only needs to check the definition of error_code to know the valid values assigned to err.
Note: After defining a variable as an enumeration type, the value assigned to the variable cannot be the valid value in the enumeration type.
In the above example, the compiled program does not require that the err value be only a valid value in the error-code type. Therefore, the programmer must ensure that the program can implement this.
On the contrary, if the constant was originally described using the # define command, the above example may be as follows:
Void copy_file (char * Source * file, char * dest_file)
{
......
Int err;
......
If (drive_ready ()! = True)
Err = drive_not_ready;
......
}
When another programmer wants to modify or add the function of the previous example, he cannot immediately know the valid values of the variable err. He must first check the # definedrive_not_ready statement in the header file, in addition, it is expected that all related constants are defined in the same header file.
(3) Facilitate program debugging because some identifiers can print the values of enumerated constants. This is not commonly used in program debugging, because if your program stops in a row of statements using enumeration constants, you can immediately check the value of this constant; otherwise, most debugging programs cannot print the value of the identifier constant, so you have to manually check the value of this constant in the header file.
See:
5.5 what are the advantages of using the # Dehne command to describe constants?
5.6 what are the advantages of using the enum keyword to describe constants?
5.8 how to invalidate some programs in the demo version?
If you are creating a demo version for your program, you can use preprocessing commands to make some of your program take effect or expire. The following is an example of using the # If and # endif commands to implement the above functions:
Int save_document (char * doc_name)
{
# If demo_version
Printf ("Sorry! You can't save your ents using the demo version
-> This program! /N ");
Return (0 );
# Endif
}
When writing the source code of the demo version program, if the # define demo_version statement is inserted, the Preprocessing Program will include the code that meets the compilation conditions in the above save_document () function. In this way, users who use the demo version cannot save their files. A better way is to define demo_version in the compilation options so that you do not have to modify the source code of the program.
These skills are useful in many different situations. For example, if your program may be used in multiple operating systems or operating environments, you can define macros such as windows_ver, unix_ver, and dos_ver, they instruct the Preprocessing Program how to include the corresponding code into your program according to specific conditions.
See:
5.32 how to check whether a symbol has been defined?
5.9 when should I replace functions with Macros?
See 5.1-0.
See:
5.1 What is macro )? How to Use Macros?
5.1-0 is better to use macros, Or is it better to use functions?
5.17 how to create a non-sensitive macro?
5.10 is it better to use macros or functions?
It depends on the situation in which your code is written. A macro has an obvious advantage over a function, that is, it is more efficient (and faster) than a function, because the macro can be expanded directly in the source code, and calling a function requires additional overhead. However, macros are relatively small and cannot handle large and complex code structures, while functions can. In addition, macros need to be expanded row by row, so every time a macro appears, the macro code needs to be copied once, so that your program will become larger, and using functions will not increase the program.
Generally, you should use macros to replace small and reusable code segments so that the program can run faster. When the task is complex and requires multiple lines of code, or the smaller the program, the better, the function should be used.
See:
5.1 What is macro )? How to Use Macros?
5.17 how to create a non-sensitive macro?
5.11 what is the best way to add comments to a program?
Most C compilations provide the following two methods for adding annotations to a program:
(1) mark the start and end of the comment with the symbols "/*" and "*/" respectively, any content between the symbols "/*" and "*/" will be processed by the compiled program as comments. This method is the best way to add comments to the program.
For example, you can add the following annotations to the program:
/*
This portion of the program contains
A comment that is several lines long
And is not supported ded In the compiled
Version of the program.
*/
(2) Use the symbol "//" to mark the comment line. Any content between the symbol "//" and the end of the current line will be processed by the Compilation Program as a comment. To add an independent comment, it is most convenient to use the symbol. However, for the above example, because there are four lines of content in an independent comment, it is inappropriate to use the symbol "//". See the following example:
// This portion of the program contains
// A comment that is several lines long
// And is not supported ded In the compiled
// Version ofthe program.
Note that the method for adding comments with the symbol "//" is incompatible with the ANSI standard, which is not supported by many earlier compilation programs.
See:
5.8 how to invalidate some programs in the demo version?
5.12 # What are the differences between include <File> and # include "file?
There are two methods to include files in a C program:
(1) use the symbols "<" and ">" to enclose the file names. This method instructs the Preprocessing Program to search for files in the predefined default path. The predefined default path is usually specified in the include environment variable. See the following example:
Include = C:/Compiler/include; s:/source/headers;
For the preceding include environment variables, if the # include <File> statement is used to include the file, the compiler first searches for the file in the C:/Compiler/include directory. If not, then to S:/source/Headers
Directory to continue searching; if not, continue searching under the current directory.
(2) enclose the file names to be included in double quotes. This method indicates that the Preprocessing Program first searches for files in the current directory, and then searches for files in the predefined default path.
For the include environment variable in the above example, if the # include "file" statement is used to include the file, the Compilation Program will first find the file in the current directory; if not found, it will go to C: continue searching under the/Compiler/include directory; if not, continue searching under the S:/source/headers directory.
# Include <File> statements are generally used to contain standard header files (such as stdio. h or stdlib. h), because these header files are rarely modified, and they are always stored in the standard inclusion file directory of the Compilation Program. # The include "file" statement is generally used to contain non-standard header files, because these header files are generally stored in the current directory, you can modify them frequently, and require that the compiler always use the latest version of these header files.
See:
5.3 how to avoid multiple times containing the same head file?
5. 4 can I use the # include command to include a file whose type name is not ". H?
5.14 can contain files be nested?
5.15 how many layers of files can be nested?
5.13 can you specify which header file is included during compilation?
You can implement this through the # If, # else and # endif commands. For example, the header files alloc. h and malloc. h have the same functions and content, but the former is used by Borlandc ++ compiler and the latter is used by Microsoft softc ++ compiler. If you are writing a program that supports both BorlandC ++ and microsoftc ++, you should specify that the program contains alloc during compilation. the H header file still contains malloc. h header file. See the following example:
# Ifdef _ BorlandC __
# Include <alloc. h>
# Else
# Include <malloc. h>
# Endif
When BorlandC ++ is used to compile a program to process the preceding example, the compiler automatically defines the _ BorlandC _ identifier name. Therefore, alloc. the H header file will be included. When the microsoftc ++ compilation program is used to process the above example, the Compilation Program checks that the _ BorlandC _ identifier name is not defined, so malloc. the H header file will be included.
See:
5.21 how can I determine whether a program is compiled in C or C ++?
5.32 how to check whether a symbol has been defined?
5.14 can contain files be nested?
Including files can be nested, but you should avoid multiple inclusion of the same file (see section 5.3 ).
In the past, people thought that header file Nesting is an undesirable programming method, because it increases the workload of dependencytrackingfunction in the make program, thus reducing the compilation speed. Now, by introducing a pre-compiled header file (precompiledheaders, that is, all header files and their dependent files are stored in a pre-compiled state) technology, many popular compilation programs have solved the above problems.
Many programmers like to create their own header files to include each header file required by each module. This is a header file.
Including files can be nested, but you should avoid multiple inclusion of the same file (see section 5.3 ).
In the past, people thought that header file Nesting is an undesirable programming method, because it increases the workload of dependencytrackingfunction in the make program, thus reducing the compilation speed. Now, by introducing a pre-compiled header file (precompiledheaders, that is, all header files and their dependent files are stored in a pre-compiled state) technology, many popular compilation programs have solved the above problems.
Many programmers like to create their own header files to include each header file required by each module. This is a header file.
See:
5.3 how to avoid multiple times containing the same head file?
5.4 can I use the # include command to include files whose type name is not ". H?
5.12 # include <file ~ What is the difference between # include "file" and # include "file?
5.15 how many layers of files can be nested?
5.15 how many layers of files can be nested?
Although theoretically containing files can be nested at any layer, if there are too many layers of nesting, the Compilation Program will use up its stack space. Therefore, the actual number of nested layers is limited. It generally depends on your hardware settings and the version of the compilation program.
When writing a program, you should avoid excessive nesting. In general, only contained files are nested when necessary. For example, a header file is created to include each header file required by each module.
See:
5.3 how to avoid multiple times containing the same head file?
5.4 can I use the # include command to include files whose type name is not ". H?
5.12 # What are the differences between include <File> and # include "file?
5.14 can contain files be nested?
5.16 What is the function of the connection operator?
The concatenation operator "#" Can concatenate two independent strings into one. In the macro of C, the "#" operator is often used. See the following example:
# Include <stdio. h>
# Define sort (x) sort_function # x
Void main (void );
Void main (void)
{
Char * array;
Int elements, element_size ;.
Sort (3) (array, elements, element_size );
}
In the preceding example, macro sort uses the "#" operator to connect the string sort_function with the string passed by parameter X, which means the statement
Sort (3) (array, elemets, element_size );
Convert the preprocessed program into a statement
Sort_function3 (array, elements, element_size );
You can see from the usage of macro sort that if you can determine which function to call at runtime, you can use the "#" operator to dynamically construct the name of the function to be called.
See:
5.1 What is a macro? How to Use Macros?
5.17 how to create a type-sensitive macro?