C language essay 01 --- how to understand conditional compilation

Architecture diagram

preface

Due to the regional translation relationship, some books translate macro into "macro" and some into "macro". In order to avoid confusion (myself), the English name macro will replace the Chinese name

What is conditional compilation

Conditional compilation is a statement that makes selective judgment according to the defined macro. It will be completed before the compiler, which is mainly responsible by the preprocessor

The preprocessor will tell the compiler the result of the conditional compilation instruction and ask him to compile the code of the specified section. Conditional compilation instructions may appear anywhere in the program. Depending on the use method, for example, the following simple program example contains conditional compilation:

#include <stdio.h>

/*If a is not defined, define it*/
#ifndef a
#define a 1
#endif

int main(){
	#if (a == 1)
		printf("a == 1\n");
	#else
		printf("a != 1\n");
	#endif
	
	return 0;
}

Unlike general conditional statements, conditional compilation is determined before compile. On the contrary, normal conditional statements (if, else, if, else...) can only be judged at run time

In other words, conditional compilation statements can let the compiler know which code sections need to be compiled and which can be directly discarded; The normal conditional statement needs to judge the execution block according to the variable value during execution, so the whole logical block will be compiled anyway

In the figure below, we see one c files are compiled into executable files. The green block is the main part of conditional compilation. Conditional compilation is a bit of pre deployment. It will determine who will be included, compiled and ignored. It is not compiled by the compiler and certainly does not belong to the category of C/C + +

Conditional compilation type

#if, #elif, #defined

#if, #elif use the following constant expression to determine whether the code section needs to be included

For example, the following simple code fragment will compile and execute the section between #if and #else because test is defined as 1 and this condition coincides with the first section

#include <stdio.h>

#define test 1

int main(){
   #if (test == 1)
       printf("Macro test exist...");
   #else
		printf("Macro test is not defined...");
   #endif
}

Output results:

Macro test exist...

#The constant expression after if can be judged by unary operator or logical operator combined with multiple judgment expressions. When the judgment conditions exceed two groups, #elif, #else can be used, which is no different from the general if else statement

#include <stdio.h>

#define test1 10
#define test2 1

int main(){
   #if (test1 > 8) && (test1 < 15) && (test2 > 0)
       printf("Macro test meet the requirement");
   #elif(test1 > 15)
	   printf("Macro test meet the requirement, but way too big");
   #else
	   printf("Macro test doesn't meet the requirement");
   #endif
}

Output results:

Macro test meet the requirement

Remember #if to add parentheses ()

#if can also add the conditional compilation statement defined(), which is used to determine whether a macro is defined. For example, let's rewrite the above code a little:

#include <stdio.h>

#define test1 10
// #define test2 1

int main(){
   #if (test1 > 8) && (test1 < 15) && defined(test2)
       printf("Macro test meet the requirement");
   #elif(test1 > 15)
	   printf("Macro test meet the requirement, but way too big");
   #else
	   printf("Macro test doesn't meet the requirement");
   #endif
}

Output results:

Macro test doesn't meet the requirement

Since test2 is annotated by us, it is not actually defined, so the final output result fails to meet #if and #elif conditions

Some frequently asked questions
The timing of using #if and #defined is actually a little different. The former must be used alone with an expression to judge the value of macro; The latter is only used to determine whether the macro is defined

Suppose we want to use #if instead of #defined to judge whether a test2 is defined:

#include <stdio.h>

#define test1 1
#define test2 2

int main(){
    #if defined(test1) && (test2)
        printf("success\n");
    #else
        printf("fail\n");
    #endif
    
    return 0;
}

The output result is:

success

Test1 and test2 are judged to be successful. However, if we modify the definition value of test2, the results will be very different:

#include <stdio.h>

#define test1 1
#define test2 0

int main(){
    #if defined(test1) && test2
        printf("success\n");
    #else
        printf("fail\n");
    #endif
    
    return 0;
}

At this time, the output result becomes:

fail

It's quite different from the judgment function we expect, but at least it can be printed out. Modify the definition value of test2 again:

#include <stdio.h>

#define test1 1
#define test2

int main(){
    #if defined(test1) && (test2)
        printf("success\n");
    #else
        printf("fail\n");
    #endif
    
    return 0;
}

The error missing expression between '(' and ')' of the compiler is obtained during execution. If test2 does not fill in parameters, it will be interpreted as an empty string, which cannot be judged by expression. Therefore, an error will occur even if judgment is added to the above code (test2 > 10). Error: operator '> has no left operate

If you simply do not define a macro, 0 will be passed in the #if judgment, which is a little different

#include <stdio.h>

// #define test 1

int main(){
    #if (test == 0)
        printf("test equal to 0...");         
    #elif (test > 10)
        printf("test greater than 10...");
	#elif (test <= 10)
		printf("test lesser than or equal to 10...");
	#else
	    printf("test is not defined");	
    #endif
    
    return 0;
}

Output results:

test equal to 0...

We clearly do not define test, and the output result determines that it is equal to 0, because the preprocessor replaces the undefined macro with 0

From the above series of cases, it can be found that if you want to judge whether a macro is defined, you must add the #defined() instruction after the #if. In addition, you should judge whether the macro exists before using the expression

The macro used for conditional compilation avoids being defined as a decimal point

#ifdef, #ifndef

In fact, #ifdef is #if defined()# ifndef is #if! Of course, the purpose of defined() is to judge whether the macro is defined. Its logic is as follows:

  • If macro is defined:
    • #ifdef() will judge as true
    • #ifdef() will judge as false
  • If macro is not defined
    • #ifdef() will judge as false
    • #ifndef() will judge as true

For example:

#include <stdio.h>

#define test1 1
#define test2 0
int main(){
    #ifndef test1 // #if !defined(test1)
        printf("test1 is not defined...\n");         
    #else
        printf("test1 is defined...\n");   
    #endif
    
    #ifdef test2 // #if defined(test2)
        printf("test2 is defined...\n");         
    #else
        printf("test2 is not defined...\n");   
    #endif
    
    return 0;
}

Output results:

test1 is defined...
test2 is defined...

#else

#Else statement is an extension of conditional compilation judgment. When #if and #elif judge no, the code section between #else and #endif will be executed:

#include <stdio.h>

#define test 100

#if (test > 500)
	#define MAX 75
#elif (test > 300)
	#define MAX 50
#elif (test > 150)
	#define MAX 35
#else
	#define MAX 10
#endif

When used in #ifndef, #ifdef is relatively simple, because they have only two states: existence and nonexistence:

#include <stdio.h>

#ifdef test
	#define MAX 75
#else
	#define MAX 50
#endif

#endif

#ENDIF is used to end the conditional compilation section. Each time a conditional judgment structure is completed, an #endif statement needs to be used. The following is a pseudo code example. Each complete conditional compilation statement needs #endif to end:

/*Conditional compilation*/
#if (...)
	#if (...)
		// do-something
	#else
		// do-something
	#endif
#endif

nested structure

Conditional compilation can be nested like general conditional statements.

We assume that the code will determine by definition which operating system platform to run thread initialization. The use of nested structure helps us to subdivide our goals. You can see its structure. In fact, it is the same as ordinary conditional statements:

#if defined(Linux)
	#ifdef ubuntu
		ubuntu_thread_init();
	#endif /*ubuntu*/
	#ifdef centos
		centos_thread_init();
	#endif /*centos*/
#elif defined(MS)
	#ifdef WIN10
		windows_10_thread_init();
	#endif /*WIN10*/
	#ifdef WIN7
		windows_7_thread_init();
	#endif /*WIN7*/
#endif

Empty definition

As the name suggests, an empty definition does not define any value for macro:

#define test 

An empty definition is a macro with nothing. The preprocessor will not replace any parameters with the code using it. It represents an empty string:

#include <stdio.h>
#define test

int main(){
    test test test test test test test
    test printf("empry macro!\n"); test
    test test test test test test test
    
    return 0;
}

Output results

empry macro!

But do you think it's useless? Although an empty definition does not represent any value, it can be captured by #if defined(), #ifdef and other conditional compilation

In other words, in some scenarios where there is no need to replace the defined value at all, it is very useful to use null definition, such as the header guard function to be introduced next

Header guard

First, learn about the function of the #include statement. The preprocessor will copy all the contents of the included header file, and then delete the #include statement

However, a problem arises. What happens if the main program repeats #include the same header file?, For example, the following program:

/*test1.h*/
#define SerialName      "my_test_0001\n"
#define SW_version          "V.1.3.0\n"
#define FW_version          "V.1.3.0\n"

typedef enum
{
    socket_init = 0,
    socket_connecting,
    socket_connected,
	socket_close
}socket_process;

// ...

/*test2.h*/
#include "test1.h"
#include <stdint.h>

#define MAX_SOCKET_NUMBER 4

typedef struct{
	uint8_t family;
	uint8_t port;
	uint8_t* addr;
	socket_process socket_information;
}socket_info[MAX_SOCKET_NUMBER];

// ...

/**
 * main.c
 */
#include "test1.h"
#include "test2.h"

int main(){
	// do-something
	return 0;
}

The problem with the above program is test1 H in main C and test2 H is nested and included. If the same header file is repeated twice, its contents will be compiled twice, which not only wastes resources, but also may cause errors

For example, the compiler will remind you that "xxx" has already been declared in the xxx file or similar messages, that is, repeated compilation occurs

This is the time for header guards to stand up. Its purpose is to prevent header file contents from being compiled repeatedly, such as various types of data, structure data, static variables, etc

Back to the original program example, let's rewrite it:

/*test1.h*/
#ifndef __TEST1_H
#define __TEST1_H

#define SerialName      "my_test_0001\n"
#define SW_version          "V.1.3.0\n"
#define FW_version          "V.1.3.0\n"

typedef enum
{
    socket_init = 0,
    socket_connecting,
    socket_connected,
	socket_close
}socket_process;

// ...

#endif /*__TEST1_H*/
/*test2.h*/
#ifndef __TEST2_H
#define __TEST2_H

#include "test1.h"
#include <stdint.h>

#define MAX_SOCKET_NUMBER 4

typedef struct{
	uint8_t family;
	uint8_t port;
	uint8_t* addr;
	socket_process socket_information;
}socket_info[MAX_SOCKET_NUMBER];

// ...

#endif /*__TEST2_H*/
/**
 * main.c
 */
#include "test1.h"
#include "test2.h"

int main(){
	// do-something
	return 0;
}

__ TEST1_H is called a preprocessing variable, usually in__ As the beginning, all English letters are capitalized. This special writing method is to avoid users
A macro with the same name is also defined, resulting in an error

The whole process is shown in the figure below, including test1.0 for the first time H is not defined__ TEST1_H. It will successfully enter the ifndef conditional compilation section and copy the content

The second repetition contains test1 H occurs with test2 H, due to test1 H medium__ TEST1_H has been defined last time, so ifndef conditional compilation blocks will be ignored to successfully prevent repeated inclusion

Because the code written in the middle of the first header may be very long, and there are many other conditional compiled code, it is best to add comments after #endif__ TEST1_H to remind developers that this #endif belongs to the header guard section

Cutting characteristics

The cutting feature is simply to run a specific section of code without directly deleting the code. Usually, the cut feature of conditional compilation is used to debug tests or run a specified version of code. For example, the following pseudo code is an example:

debug test

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define THREAD1_TEST_MODE

int main(void){
    thread1_init();
	
    #ifndef THREAD1_TEST_MODE
    thread2_init();
    #endif

    while(1){
        thread1();

        #ifndef THREAD1_TEST_MODE
    	thread2();
        #endif
    }
}

Thread1 is defined when we need to test the thread1 function_ TEST_ Mode. In this way, the thread2 code is automatically ignored. Because the thread2 part will not be compiled by the compiler, to some extent, the cutting feature can save code size. This feature is more obvious in the next case

Specifies the version code
For example, there is a software with four different schemes. We only need to compile software according to the requirements of conditional compilation_ When version is defined as the specified parameter value, you can explicitly compile and execute the code of the specified version:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define SOFTWARE_VERSION 0

int main(void){
	while(1){
		#if (SOFTWARE_VERSION == 0)
			personal_version_thread(); // Personal version
		#elif (SOFTWARE_VERSION == 1)
			family_version_thread(); // Home version
		#elif(SOFTWARE_VERSION == 2)
			enterprise_version_thread(); // Enterprise version
		#else
			pro_version_thread(); // Professional version
		#endif
    }
}

Portability issues

Select the preprocessing section using macro
Using conditional compilation is also conducive to the portability of the program. I am used to creating a header file responsible for setting parameters and multiple functional header files cut according to parameter definitions

For the following simple program, I use header 1 H defines the macro required by the program and the condition macro used to select the function section

In other words, I can select the preprocessing section through SPECIALTY (see header2.h). Since the definition names of macro are the same, from the perspective of program logic, I only need to change header1.h each time I migrate the program The defined value of H can be compatible with the same program as code base

Of course, this program has no logic at all. It is just an example, but the core concept is inconvenient. It still uses conditional compilation to improve portability

/*header1.h*/
#ifndef __HEADER1_H
#define __HEADER1_H

#define NAME        "HAU-WEI"
#define GENDER      "male"
#define AGE         25

#define PROGRAMMER  0
#define MANAGER     1
#define ATHLETE     2

#define SPECIALTY PROGRAMMER
#include "header2.h"

#endif /*__HEADER1_H*/
/*header2.h*/
#ifndef __HEADER2_H
#define __HEADER2_H

#if (SPECIALTY == PROGRAMMER)
    #define Intro(x) printf("Hi, my name is %s, I'm a programmer\n[Gender][%s]\n[Age][%d]\n", NAME, GENDER, AGE)
    #define SKILL1      "JAVA"
    #define SKILL2      "C++"
    #define SKILL3      "SQL"
    #define SKILL4      "linux"
#elif (SPECIALTY == MANAGER)
    #define Intro(x) printf("Hi, my name is %s, I'm a manager\n[Gender][%s]\n[Age][%d]\n", NAME, GENDER, AGE)
    #define SKILL1      "Communication"
    #define SKILL2      "Management"
    #define SKILL3      "Negotiation"
    #define SKILL4      "English"
#elif (SPECIALTY == ATHLETE)
    #define Intro(x) printf("Hi, my name is %s, I'm a athlete\n[Gender][%s]\n[Age][%d]\n", NAME, GENDER, AGE)
    #define SKILL1      "Basketball"
    #define SKILL2      "Soccer"
    #define SKILL3      "Swimming"
    #define SKILL4      "Tenis"    

#endif /*SPECIALTY*/

#endif /*__HEADER2_H*/
#include <stdio.h>
#include "header1.h"
/**
 * main.c
 */
int main(){
    Intro(NAME);

    if(SKILL1 == "JAVA"){
        printf("I'm capable for JAVA!\n");
    }
    else{
        printf("I'm not capable for JAVA!\n");
    }

    
    return 0;
}

Output results:

Hi, my name is HAU CHEN, I'm a programmer
[Gender][male]
[Age][25]
I'm capable for JAVA!

We tried to put header1 Change SPECIALTY in H to MANAGER to see what happens to the output:

Hi, my name is HAU-WEI, I'm a manager
[Gender][male]
[Age][25]
I'm not capable for JAVA!

The output result does change according to the defined parameter type!

Use different header files
If you want to use the migration feature for development on large programs, you can use conditional compilation to determine #include which header file. These header files contain the same names of functions, macro, etc

We only need to change the value defined by macro to switch functions according to requirements. This method is applicable to products with similar but small differences when the main program architecture logic remains unchanged and we want to rewrite some special functions

For example, the following program example will create different header files according to the definition value #include of FUNC. And because each header file has a name called print_result function, so if you want to migrate the file in the future, just include print_ The file of result function can be transplanted

The source and header files are usually transplanted in pairs. For convenience, the example writes the code in the header, but the logic remains the same

/*header1*/
#ifndef __HEADER1_H
#define __HEADER1_H

int operation(int a, int b){
   return a + b; 
}


int print_result(int a, int b){
    printf("%d + %d = %d\n", a, b, operation(a, b));
}

#endif /*__HEADER1_H*/
/*header2*/
#ifndef __HEADER2_H
#define __HEADER2_H

int operation(int a, int b){
   return a - b; 
}


int print_result(int a, int b){
    printf("%d - %d = %d\n", a, b, operation(a, b));
}

#endif /*__HEADER2_H*/
/*header3*/
#ifndef __HEADER3_H
#define __HEADER3_H

int operation(int a, int b){
   return a * b; 
}


int print_result(int a, int b){
    printf("%d * %d = %d\n", a, b, operation(a, b));
}

#endif /*__HEADER3_H*/
/**
 * main.c
 */
#include <stdio.h>

#define FUNC 3 

#if (FUNC == 1)
    #include "header1.h"
#elif (FUNC == 2)
    #include "header2.h"
#elif (FUNC == 3)
    #include "header3.h"
#endif

int main(){
    #if defined(FUNC)
    int a=4, b=3;
    print_result(a, b);
    #endif
    
    return 0;
}

Output results:

4 * 3 = 12

Try to change FUNC to 2 and check the output results:

4 - 3 = 1

Well, that's all for conditional compilation. I hope it will be helpful to you in large-scale program development in the future 😎

Keywords: C Back-end

Added by webpals on Sat, 18 Dec 2021 03:29:24 +0200