预处理命令

目录

一、预处理命令

二、#include

       1.尖括号/引号引入标准头文件区别

       2.#include 用法举例

三、C语言宏定义—-#define

四、带参数的宏定义

       对带参宏定义的说明

五、C语言带参宏定义和函数的区别

六、C语言宏参数的字符串化和宏参数的连接

                1.# 的用法

       2.##用法

七、C语言#if、##ifdef、#ifndef的用法

       1.#if 的用法

       2.#ifdef 的用法

       3.#ifndef 的用法

八、#error命令,阻止程序编译

九、C语言预处理命令总结

 

       在编译和链接之前,还需要对源文件进行一些文本方面的操作,比如文本替换、文件包含、删除部分代码等,这个过程叫做预处理,由预处理程序完成。

一、预处理命令

       使用C语言库函数之前,应该用#include引入对应的头文件。这种以#号开头的命令称为预处理命令

       C语言源文件要经过编译、链接才能生成可执行程序:

       (1) 编译(Compile)会将源文件.c文件)转换为目标文件。对于 VC/VS,目标文件后缀为.obj;对于GCC,目标文件后缀为.o。(编译是针对单个源文件的,一次编译操作只能编译一个源文件,如果程序中有多个源文件,就需要多次编译操作。)

       (2)链接(Link)是针对多个文件的,它会将编译生成的多个目标文件以及系统中的组件等合并成一个可执行程序

       在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。预处理主要是处理以#开头的命令,例如#include <stdio.h>等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。

       预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中。和.cpp一样,.i也是文本文件,可以用编辑器打开直接查看内容。

      C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

//开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行
/*
分析:这个程序的难点在于,不同平台下的暂停函数和头文件都不一样:
1.Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds)(注意 S 是大写的),参数的单位是“毫秒”,位于 <windows.h> 头文件。
2.Linux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds),参数的单位是“秒”,位于 <unistd.h> 头文件。
3.不同的平台下必须调用不同的函数,并引入不同的头文件,否则就会导致编译错误,因为 Windows 平台下没有 sleep() 函数,也没有 <unistd.h> 头文件,反之亦然。这就要求我们在编译之前,也就是预处理阶段来解决这个问题。
*/
#include <stdio.h>
//不同的平台下引入不同的头文件
#if _WIN32  //识别windows平台
#include <windows.h>
#elif __linux__  //识别linux平台
#include <unistd.h>
#endif
int main() {
    //不同的平台下调用不同的函数
    #if _WIN32  //识别windows平台
    Sleep(5000);
    #elif __linux__  //识别linux平台
    sleep(5);
    #endif
    puts("https://blog.csdn.net/xjtuse123/");
    return 0;
}

//对于 Windows 平台,预处理以后的代码变成:
#include <stdio.h>
#include <windows.h>
int main() {
    Sleep(5000);
    puts("https://blog.csdn.net/xjtuse123/");
    return 0;
}
//对于 Linux 平台,预处理以后的代码变成:
#include <stdio.h>
#include <unistd.h>
int main() {
    sleep(5);
    puts("https://blog.csdn.net/xjtuse123/");
    return 0;
}

二、#include

       1.尖括号/引号引入标准头文件区别

  #include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种,#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
      #include 的用法有两种,如下所示:

#include <stdHeader.h>
#include "myHeader.h"

       使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:

       1) 使用尖括号< >,编译器会到系统路径下查找头文件;

       2) 而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
       前面我们一直使用尖括号来引入标准头文件,现在我们也可以使用双引号了,如下所示:

#include "stdio.h"
#include "stdlib.h"

       stdio.h 和 stdlib.h 都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。当然,你也可以把当前项目所在的目录添加到系统路径,这样就可以使用尖括号了,但是一般没人这么做,纯粹多此一举,费力不讨好。

       在以后的编程中,大家既可以使用尖括号来引入标准头文件,也可以使用双引号来引入标准头文件;不过,我个人的习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。

       关于 #include 用法的注意事项:

       ①一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。

       ②同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。

       ③文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。

       2.#include 用法举例

       本例中需要创建三个文件,分别是 main.cpp、test.cpp 和 test.h,如下图所示(这里test.h和test.cpp文件名不需要都为test):

《预处理命令》

       test.cpp 所包含的代码:

//定义sum函数,计算从m到n之和
int sum(int m, int n){
	int i, sum = 0;
	for (i = m; i <= n; i++){
		sum += i;
	}
	return sum;
}

       test.h 所包含的代码:

//声明函数
int sum(int m, int n);

       main.cpp 所包含的代码:

#include <stdio.h>
#include "test.h"

int main(){
	printf("%d \n",sum(1,100));

	return 0;
}

       我们在test.cpp 中定义了 sum() 函数,在 test.h 中声明了 sum() 函数,这可能与很多初学者的认知发生了冲突:函数不是在头文件中定义的吗?为什么头文件中只有声明?「在头文件中定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误
       此外,细心的童鞋可以发现,main.c 只是引入了 test.h,却没有引入 test.cpp,然而当我们亲自去运行程序的时候,却发现运行结果是正确的,这个问题后续会进一步解释。

三、C语言宏定义—-#define

       #define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。

       宏定义的一般形式为:

#define  宏名  字符串
其中,#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。
注:这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。

       对 #define 用法的几点说明:

       1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。

       2) 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。

       3) 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。

//PI 只在 main() 函数中有效,在 func() 中无效。
#define PI 3.14159
int main(){
    // Code
    return 0;
}
#undef PI
void func(){
    // Code
}

       4) 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替

#include <stdio.h>
#define OK 100
int main(){
    printf("OK\n");
    return 0;
}
//运行结果:OK

       5) 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。

#define PI 3.1415926
#define S PI*y*y    /* PI是已定义的宏名*/

对语句:printf("%f", S);在宏代换后变为:printf("%f", 3.1415926*y*y);

       6) 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

       7) 可用宏定义表示数据类型,使书写方便。宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。

#define UINT unsigned int
在程序中可用 UINT 作变量说明:
UINT a, b;

       注:应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。

四、带参数的宏定义

       C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。

       带参宏定义的一般形式为:

#define 宏名(形参列表) 字符串

       带参宏调用的一般形式为:

宏名(实参列表);
#define M(y) y*y+3*y  //宏定义
// TODO:
k=M(5);  //宏调用
//解析:在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=5*5+3*5。
//输出两个数中较大的数
#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main(){
    int x , y, max;
    printf("input two numbers: ");
    scanf("%d %d", &x, &y);
    max = MAX(x, y);
    printf("max=%d\n", max);
    return 0;
}
//运行结果:
input two numbers: 10 30
max=30

       对带参宏定义的说明

       1) 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。

#define MAX(a,b) (a>b)?a:b 写为:#define MAX  (a,b)  (a>b)?a:b 将被认为是无参宏定义,宏名 MAX 代表字符串(a,b) (a>b)?a:b。

       2) 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
       这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。

//输入 n,输出 (n+1)^2 的值
#include <stdio.h>
#define SQ(y) (y)*(y)
int main(){
    int a, sq;
    printf("input a number: ");
    scanf("%d", &a);
    sq = SQ(a+1);
    printf("sq=%d\n", sq);
    return 0;
}
//运行结果:
input a number: 9
sq=100
//注:宏调用中实参为 a+1,是一个表达式,在宏展开时,用 a+1 代换 y,再用 (y)*(y) 代换 SQ,得到sq=(a+1)*(a+1);这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再传递给形参,而宏展开中对实参表达式不作计算,直接按照原样替换。

       3) 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。同时,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。

//参数两侧未加括号
#include <stdio.h>
#define SQ(y) y*y  //原式#define SQ(y) (y)*(y)
int main(){
    int a, sq;
    printf("input a number: ");
    scanf("%d", &a);
    sq = SQ(a+1);
    printf("sq=%d\n", sq);
    return 0;
}
//运行结果为:
input a number: 9
sq=19
//解析:宏替换后将得到sq=a+1*a+1;由于 a 为 9,故 sq 的值为 19。
//整个字符串外未加括号
#include <stdio.h>
#define SQ(y) (y)*(y)
int main(){
    int a,sq;
    printf("input a number: ");
    scanf("%d", &a);
    sq = 200 / SQ(a+1);
    printf("sq=%d\n", sq);
    return 0;
}
//运行结果
input a number: 9
sq=200
//解析:分析宏调用语句,在宏展开之后变为:sq=200/(a+1)*(a+1);a 为 9 时,由于“/”和“*”运算符优先级和结合性相同,所以先计算 200/(9+1),结果为 20,再计算 20*(9+1),最后得到 200。

五、C语言带参宏定义和函数的区别

       带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。

//①用函数计算平方值
#include <stdio.h>
int SQ(int y){
  return ((y)*(y));
}
int main(){
    int i=1;
    while(i<=5){
        printf("%d^2 = %d\n", (i-1), SQ(i++));//函数有多个参数的时候,会从后向前处理,注意此处i-1
    }
    return 0;
}
//运行结果:
1^2 = 1
2^2 = 4
3^2 = 9
4^2 = 16
5^2 = 25
//解析:
1.第一次循环时,i=1,满足循环条件进入循环。因为SQ(i++)参数是i++,所以先执行i*i返回1,再执行自增操作,此时i变为2。函数有多个参数时,从后向前处理,这里i值为2,故i-1。
2.第二次循环时,i=2,满足循环条件进入循环。因为SQ(i++)参数是i++,所以先执行i*i返回4,再执行自增操作,此时i变为3。函数有多个参数时,从后向前处理,这里i值为3,故i-1。
3.第三次循环时,i=3,满足循环条件进入循环。因为SQ(i++)参数是i++,所以先执行i*i返回9,再执行自增操作,此时i变为4。函数有多个参数时,从后向前处理,这里i值为4,故i-1。
4.第四次循环时,i=4,满足循环条件进入循环。因为SQ(i++)参数是i++,所以先执行i*i返回16,再执行自增操作,此时i变为5。函数有多个参数时,从后向前处理,这里i值为5,故i-1。
5.第五次循环时,i=5,满足循环条件进入循环。因为SQ(i++)参数是i++,所以先执行i*i返回25,再执行自增操作,此时i变为6。函数有多个参数时,从后向前处理,这里i值为6,故i-1。
6.第六次循环时,i=6,不满足循环条件退出循环。


//②用宏计算平方值
#include <stdio.h>
#define SQ(y) ((y)*(y))
int main(){
    int i=1;
    while(i<=5){
        printf("%d^2 = %d\n", i, SQ(i++));
    }
    return 0;
}
//运行结果:
3^2 = 1
5^2 = 9
7^2 = 25
//解析:
1.第一次循环时,i=1,满足循环条件进入循环。因为SQ(i++)宏展开为((i++)*(i++)),所以先执行i*i返回1,再执行两次自增操作,此时i变为3。函数有多个参数时,从后向前处理,这里i值为3。
2.第二次循环时,i=3,满足循环条件进入循环。因为SQ(i++)宏展开为((i++)*(i++)),所以先执行i*i返回9,再执行两次自增操作,此时i变为5。函数有多个参数时,从后向前处理,这里i值为5。
3.第三次循环时,i=5,满足循环条件进入循环。因为SQ(i++)宏展开为((i++)*(i++)),所以先执行i*i返回25,再执行两次自增操作,此时i变为7。函数有多个参数时,从后向前处理,这里i值为7。
4.第四次循环时,i=7,不满足循环条件退出循环。

六、C语言宏参数的字符串化和宏参数的连接

       在宏定义中,有时还会用到###两个符号,它们能够对宏参数进行操作。

       1.# 的用法

  #用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:

#define STR(s) #s
那么:
printf("%s", STR(www.baidu.com));
printf("%s", STR("www.baidu.com"));
分别被展开为:
printf("%s", "www.baidu.com");
printf("%s", "\"www.baidu.com\"");//可以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。
//#用法
#include <stdio.h>
#define STR(s) #s

int main(){
	printf("%s\n", STR(www.baidu.com));
	printf("%s\n", STR("www.baidu.com"));
	return 0;
}
//运行结果:
www.baidu.com
"www.baidu.com"

       2.##用法

  ##称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:

#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
那么:
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
将被展开为:
printf("%f\n", 8.5e2);
printf("%d\n", 123400);
//##用法
#include <stdio.h>
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
int main() {
	printf("%f\n", CON1(8.5, 2));
	printf("%d\n", CON2(12, 34));
	return 0;
}
//运行结果:
850.000000
123400

七、C语言#if、##ifdef、#ifndef的用法

       如第一部分示例代码——开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行。这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。

       1.#if 的用法

       #if 用法的一般格式为:

#if 整型常量表达式1
    程序段1
#elif 整型常量表达式2
    程序段2
#elif 整型常量表达式3
    程序段3
#else
    程序段4
#endif
//它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。

       需要注意的是,#if 命令要求判断条件为“整型常量表达式”,也就是说,表达式中不能包含变量,而且结果必须是整数;而 if 后面的表达式没有限制,只要符合语法就行。这是 #if 和 if 的一个重要区别。

       2.#ifdef 的用法

       #ifdef 用法的一般格式为:

#ifdef  宏名
    程序段1
#else
    程序段2
#endif
//它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
//也可以省略 #else:
#ifdef  宏名
    程序段
#endif

       VS/VC 有两种编译模式,DebugRelease。在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。

//当前程序的编译模式示例
#include <stdio.h>
#include <stdlib.h>

int main(){
	#ifdef _DEBUG
		printf("正在使用Debug模式编译程序...\n");
	#else
		printf("正在使用Release模式编译程序...\n");
	#endif

	system("pause");
	return 0;

}
//运行结果:正在使用Debug模式编译程序...

       3.#ifndef 的用法

       #ifndef 用法的一般格式为:

#ifndef 宏名
    程序段1 
#else 
    程序段2 
#endif
//与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。

       总结:需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef #ifndef 后面跟的只能是一个宏名,不能是其他的。

八、#error命令,阻止程序编译

       #error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:

#error error_message

       例如,我们的程序针对Linux编写,不保证兼容Windows,那么可以这样做:

#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif
//WIN32 是Windows下的预定义宏。当用户在Windows下编译该程序时,由于定义了WIN32这个宏,所以会执行#error命令,提示用户发生了编译错误,错误信息是:
//This programme cannot compile at Windows Platform
//注:报错信息不需要加引号" ",如果加上,引号会被一起输出。

九、C语言预处理命令总结

       预处理指令是以 号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。

       一些常用预处理指令:

指令说明
#空指令,无任何效果
#include包含一个源代码文件
#define定义宏
#undef取消已定义的宏
#if如果给定条件为真,则编译下面代码
#ifdef如果宏已经定义,则编译下面代码
#ifndef如果宏没有定义,则编译下面代码
#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif结束一个#if……#else条件编译块

       ①宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
       ②为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
       ③文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
       ④条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
       ⑤使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

 

 

 

参考文献:C语言中文网

    原文作者:Aamax
    原文地址: https://blog.csdn.net/xjtuse123/article/details/97534298
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞