C预处理,宏,条件编译详解

        在使用GCC编译器编译C程序时,通常会经历预处理、编译、汇编和链接这四个主要阶段。以下是这些阶段的详细解释:

1. 预处理(Preprocessing):

  • 目的:预处理阶段主要处理以#开头的预处理指令,如#include#define等,对源文件进行文本替换和宏展开。
  • 输出:预处理后的文件通常以.i扩展名保存,包含了预处理指令处理后的完整源代码。
  • 命令:可以使用-E选项进行单独执行预处理阶段,如gcc -E source.c -o source.i

2. 编译(Compilation):

  • 目的:编译阶段将预处理后的源文件编译成汇编代码,进行语法分析、语义分析和优化。
  • 输出:编译生成的文件通常以.s扩展名保存,包含了汇编代码。
  • 命令:可以使用-S选项进行单独执行编译阶段,如gcc -S source.i -o source.s

3. 汇编(Assembly):

  • 目的:汇编阶段将汇编代码转换成机器可读的目标文件。
  • 输出:汇编生成的文件通常以.o扩展名保存,包含了机器代码的二进制表示。
  • 命令:可以使用-c选项进行单独执行汇编阶段,如gcc -c source.s -o source.o

4. 链接(Linking):

  • 目的:链接阶段将所有的目标文件和库文件链接在一起,解析符号引用,生成最终的可执行文件。
  • 输出:链接生成的文件通常没有特定的扩展名,可以是可执行文件或库文件。
  • 命令:普通编译过程会自动执行链接阶段,直接使用gcc source.c -o program即可进行整个编译链接过程。
# 预处理
gcc -E source.c -o source.i

# 编译
gcc -S source.i -o source.s

# 汇编
gcc -c source.s -o source.o

# 链接
gcc source.o -o program

        组成一个程序的每个源文件通过编译分别转换成目标代码(object code)。 每个目标文件由链接器(linker)链接在一起,形成一个单一而完整的可执行程序。 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

程序的执行:

1. 程序执行必须载入内存中

在计算机系统中,程序在执行之前必须首先被载入到内存中。这个过程通常包括将程序的可执行文件从存储介质(如硬盘)中加载到计算机的主内存(RAM)中。

一旦程序被加载到内存中,CPU可以访问程序的指令和数据,程序就可以开始执行。程序的执行过程涉及到CPU的指令执行,数据读写等操作,而这些操作都是在内存中进行的。

在有操作系统的环境中:一般这个由操作系统完成。

在独立的环境中:例如嵌入式系统或裸机环境(没有操作系统支持)程序的载入是用手动操作,也可以是通过可执行代码置入只读内存来完成。

在一些特殊的嵌入式系统中,程序可以被预先编译为二进制格式,然后通过特定的方式(如烧录)将可执行代码直接置入只读内存(ROM)中。这种方式可以确保程序在系统上电后能够立即执行,而无需进行加载操作。

2. 开始执行程序代码

程序开始执行调用main函数。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址等。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留值。

3. 终止程序

正常终止main函数或是意外终止。

预处理详解:

预定义符号:

        预定义符号(Predefined Symbols)通常指在编程语言或编程环境中预先定义好的符号或标识符,在程序中可以直接使用而无需额外定义。这些符号具有特定的含义和功能,通常用于简化编程过程、提供便利或实现特定的功能。

#include <stdio.h>

int main()
{

	printf("当前文件名:%s\n", __FILE__);//__FILE__:当前源文件的文件名,以字符串形式表示。

	printf("当前行号:%d\n", __LINE__);//__LINE__:当前源文件中的行号,以整数形式表示。

	printf("当前函数:%s\n", __func__);//__func__:当前函数的函数名,以字符串形式表示。

	printf("编译日期:%s\n", __DATE__);//__DATE__:当前编译日期,以字符串形式表示。

	printf("编译时间:%s\n", __TIME__);//__TIME__:当前编译时间,以字符串形式表示。


#ifdef __STDC__						//__STDC__:用于表示编译器是否符合 C 标准。
	printf("编译器符合 C 标准\n");
#else
	printf("编译器不符合 C 标准\n");
#endif

	return 0;
}

#define

#define 定义标识符

        #define 是用来定义标识符的预处理指令。通过 #define 可以将一个标识符与一个常量值或者带参数的宏关联起来。这种定义是在编译之前由预处理器处理的,它会在源代码中简单地进行文本替换。

#define name stuff
#include <stdio.h>

#define MAX 100
#define stu stu_a_b_c		   //创建一个简短的名字
#define CASE break;case        //在写case语句的时候自动写上break

// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。注意反斜杠后不要加其他内容 包括空格 因为/相当于转义回车 
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                         __FILE__,__LINE__ ,\
                         __DATE__,__TIME__ )  

#define print printf("我不需要;");

int main()
{
   
    printf("%d\n",MAX);
    
    switch (2)
    {
        case 1:
            printf("1\n");
        CASE 2 :
            printf("2\n");
        CASE 3 :
            printf("3\n");
        break;
        default:
            printf("?\n");
    }

    DEBUG_PRINT;

    print

	return 0;
}

#define 定义宏

        #define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

#include <stdio.h>

// 定义一个带参数的宏,计算两数之和
#define ADD(x, y) ((x) + (y)) //对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

int main() {
    int a = 5, b = 3;

    // 使用宏计算两个数的和的2倍
    int sum = ADD(a, b);

    printf("The double sum of %d and %d is %d\n", a, b, 2 * sum);

    return 0;
}

#define 替换规则

        #define定义符号和宏时,需涉及几个步骤。

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#include <stdio.h>

#define VALUE 10

int main() {
    const char *str = "The value of VALUE is: VALUE";

    printf("%s\n", str);

    return 0;
}

VALUE 是一个宏,它的值是 10。然而,在字符串常量 const char *str = "The value of VALUE is: VALUE"; 中,VALUE 并没有被展开为 10。相反,它被视为字符串常量中的文本内容,保留原样。因此,输出将会是 "The value of VALUE is: VALUE" 而不是 "The value of VALUE is: 10"。

#和##

字符串是有自动连接的特点的。

#include <stdio.h>

int main() {
    
    char* p = "hello" " world";
    printf("%s\n", p);
    printf("hell""o world");

    return 0;
}

使用 # ,把一个宏参数变成对应的字符串。

#include <stdio.h>

#define PRINT1(N) printf("the value of ""N"" is %d\n",N)//这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

#define PRINT2(N) printf("the value of "#N" is %d\n",N) //#N 就是#把N替换成字符串的形式  如N是a 就相当于 printf("the value of " " a " " is %d\n",N)

int main()
{
	int a = 10;
	PRINT1(a);			//the value of N is 10
	PRINT2(a + 1);		//the value of a+1 is 11
	return 0;
}

代码中的 #N 会预处理为: "N" 。

## 的作用:

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

注: 这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

#include <stdio.h>

#define ZH(h,w) w##h

int main()
{
	int num = 100;
	
	printf("%d\n",ZH(m,nu));//ZH(m,nu) 相当于 nu##m 等价 num 

	return 0;
}

带副作用的宏参数

        当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

#include <stdio.h>

#define MAX(a,b) ((a)>(b)?(a):(b))

int main()
{
	int a = 5;

	int b = 6;

	printf("%d\n", MAX(a, b));//6

	printf("%d\n", MAX(a++, b++));//((a++)>(b++)?(a++):(b++))  5 6 ? 6 7  --> 7 

	printf("%d %d\n", a, b);//6  8

	return 0;
}

宏和函数对比

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

原因有二:

1. 调用函数和函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。

2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型,宏是类型无关的。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#include <stdio.h>

#define MALLOC(num, type) (type *)malloc((num) * sizeof(type))

int main()
{
	
	int* p = MALLOC(10, int);//类型作为参数   

//预处理器替换之后:
//(int*)malloc((10) * sizeof(int));
    int* p1 = (int*)malloc((10) * sizeof(int));
	
	return 0;
}

宏的缺点:

当然和函数相比宏也有劣势的地方:

  1. 可读性:宏展开后可能会导致代码不易阅读和理解,特别是在宏嵌套和复杂逻辑的情况下且宏是没法调试的。

  2. 副作用:宏的展开可能会导致副作用,例如多次计算表达式、参数替换等,这可能会引入潜在的错误。

命名约定

一般来讲函数和宏的使用很相似。所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是: 把宏名全部大写 ,函数名不要全部大写。

但这又不是绝对的,如:offsetof 是一个宏,用于计算结构体中成员的偏移量。

#undef

这条指令用于移除一个宏定义。

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#include <stdio.h>

#define MAX 100

int main()
{

	printf("%d\n", MAX);

#undef MAX

	//printf("%d\n", MAX); err MAX未声明

	return 0;
}

命令行定义

 许多C编译器提供了一种能力,允许在命令行中定义符号。

如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用。

#include <stdio.h>

int main()
{
        int arr[sz];
        int i=0;

        for(i=0;i<sz;i++)
        {
                arr[i]=i;
        }

        for(i=0;i<sz;i++)
        {
                printf("%d\n",arr[i]);
        }
}

上面代码在用gcc编译时可在命令行中定义符号

 gcc test1.c -D sz=10

条件编译

在编译一个程序的时候如果要将一条语句(一组语句)编译或放弃可以选择性的编译,可用条件编译指令。

#include <stdio.h>

#define __DEBUG__
#define BU 6

int main()
{
#ifdef __DEBUG__
	printf("hello world\n");
#endif 

#if BU == 6//为真
	printf("条件编译\n");
#endif
//多个分支的条件编译
#if BU>3
	printf("BU>3\n");
#elif BU<3
	printf("BU<3\n");
#elif BU==3
	printf("BU==3\n");
#else
	printf("BU\n");
#endif

//判断是否被定义
#if defined(BU)
	printf("BU\n");
#endif

#ifdef BU
	printf("BU\n");
#endif

#if !defined(BU)
	printf("BU\n");
#endif

#ifndef BU
	printf("BU\n");
#endif

//嵌套指令
#if defined(OS_UNIX)
    #ifdef OPTION1
	    unix_version_option1();
    #endif

    #ifdef OPTION2
	    unix_version_option2();
    #endif

#elif defined(OS_MSDOS)
    #ifdef OPTION2
	    msdos_version_option2();
    #endif
#endif

	return 0;
}

这种条件编译结构可以用于实现跨平台兼容性,针对不同操作系统提供不同的实现,或者根据不同的选项开启或关闭特定功能。这种灵活性使得代码能够更容易地适应不同的环境和需求。

文件包含

#include 指令,预处理器先删除这条指令,并用包含文件的内容替换。

#include "filename"

包含方式

  • 使用尖括号 < > 包含系统头文件,例如 #include <stdio.h>
  • 使用双引号 " " 包含用户自定义头文件,例如 #include "myfunctions.h"

预处理器指令

  • #ifndef 和 #define 或#pragma once用于防止头文件被多次包含。
  • #ifdef 和 #endif 用于条件编译,根据符号是否定义来包含头文件。
  • 大多数现代编译器都支持 #pragma once,但在跨平台开发时,为了确保代码的可移植性,仍然使用传统的头文件防范式宏定义。

offsetof 宏的实现

#include <stdio.h>
#include <stddef.h>

#define OFFSETOF(type,name)   (size_t)&(((type*)0)->name)

struct S
{
	char a;
	int b;
	float c;
};

int main()
{

	struct S s = { 0 };
	printf("%d\n", offsetof(struct S, c));

	printf("%d\n", OFFSETOF(struct S, c));

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值