预处理指令typedef条件编译多文件代码

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了预处理指令typedef条件编译多文件代码相关的知识,希望对你有一定的参考价值。


预处理指令

源代码中,以井号​​#​​开头的并不是C语言中的语句。它们属于预处理指令。

在代码被编译前,预处理器会先处理预处理指令,并根据预处理指令的意义修改C语言源码。

修改后的代码将另存为中间文件或直接输入到编译器。并不会保存到源文件中。所以,预处理器不会改动源文件。

预处理指令、typedef、条件编译、多文件代码_开发语言

预处理指令#include

预处理指令​​#include​​​,会将文件​​stdio.h​​​中的代码复制到该预处理指令出现处,并删除该预处理指令。
修改后的代码将另存为中间文件或直接输入到编译器。并不会保存到源文件中。所以,,预处理器不会改动源文件。

#include的两种形式

​#include <文件名>​​​​#include "文件名"​

  1. 文件名在尖括号内:将会在编译器的包含目录中搜索文件。
  2. 文件名在双引号内:先在当前目录中搜索文件,再到编译器的包含目录中搜索文件。

对于​​stdio.h​​文件来说,它是编译器自带的文件,在编译器的包含目录中。所以使用尖括号,即可找到该文件。

#define预处指令

#define 宏 替换体

一旦预处理在程序中找到宏后,就会用替换体替换该宏。

宏的命名规则遵循C语言标识符的命名规则:只能使用字母、数字、下划线,且首字符不能是数字。

替换体不仅仅限于值,它的形式非常丰富,唯一的要求就是替换到代码后,代码还能正常通过编译。

预处理指令、typedef、条件编译、多文件代码_c++_02


宏的替换是无差别的,它仅仅把代码当做文本来处理,遇到宏就替换为宏对应的替换体。

带参数的#define

在​​#define​​​中使用参数可以创建外形和作用与函数类似的宏函数。
​​​#define 宏(参数1, 参数2,...,参数n) 替换体​​​ 虽然由带参数的​​#define​​定义的宏函数,在使用方法上很像函数。但是,它的本质依然是将宏替换为对应的替换体。由此,如果简单地将其当做函数使用,会出现一些问题。

保证宏函数按照预期运行

由于宏函数仅仅是完成替换操作,将参数替换并拼接到替换体的表达式中。而不是先让参数运算得到结果后,再进行运算。因此,为了保证参数不被其他运算符优先级影响,请在参数两边加上括号
此外,宏函数展开后的表达式,如果作为一个更大表达式的子表达式,那么它有可能受到左右两边运算符优先级的影响。因此,为了保证宏函数展开后的表达式能够优先计算,请在替换体两边加上括号
最后,为了保证不要在一个表达式中对同一个变量多次进行自增、自减操作。若宏函数的替换体内在一个表达式中多次使用同一个参数,那么请不要在宏函数的参数内填自增、自减表达式

宏函数内两个有用运算符

井号#

一般情况下,宏函数的参数会替换替换体内的对应参数。但是,若在替换体内参数前加上井号​​#​​。替换后,会用双引号包括这个参数。

双井号##

双井号可以将替换体中的两个记号组合成一个记号。
例如,有两组变量。变量由前缀和变量名组成。

// 第一组变量,group1
int group1Apple = 1, group1Orange = 2;
// 第二组变量,group2
int group2Apple = 100, group2Orange = 200;

前缀:group1或group2
变量名:Apple或Orange
使用宏函数来组合前缀与变量名,让它们成为一个完整的变量。

#define VARNAME(group, name) group ## name
VARNAME(group1, Apple) 展开为 group1Apple 。
VARNAME(group1, Orange) 展开为 group1Orange 。
VARNAME(group2, Apple) 展开为 group2Apple 。
VARNAME(group2, Orange) 展开为 group2Orange 。

如果不使用双井号​​##​​:

#define VARNAME(group, name) group name
VARNAME(group1, Apple) 展开为 group1 Apple 。
VARNAME(group1, Orange) 展开为 group1 Orange 。
VARNAME(group2, Apple) 展开为 group2 Apple 。
VARNAME(group2, Orange) 展开为 group2 Orange 。

不使用双井号,展开后的两个参数之间留有空格,无法正常使用。

如果去掉替换体中的空格:

#define VARNAME(group, name) groupname

现在,宏函数出现了问题,它具有两个参数:group和name。但是,替换体中没有与参数对应的记号。
因此,双井号 ## 的存在是有意义的。

取消宏定义

#include <stdio.h>
#define NUM 100
#define NUM 101
int main()

printf("%d\\n", NUM);
return 0;

在Visual Stduio 2019中,覆盖定义宏并不会导致编译报错而停止
不过,更妥当的做法是:使用预处理指令​​​#undef​​,取消这个宏的定义,再重新定义它。

#include <stdio.h>
#define NUM 100
// 取消宏定义NUM
#undef NUM
// 重新定义宏NUM为101
#define NUM 101
int main()

printf("%d\\n", NUM);
return 0;

typedef关键词

给整型类型取个别名

给类型起一个别名有什么意义

C语言标准并未规定这些数据类型的大小范围,具体的实现交由了编译器和平台决定。
也就是说,​​​int​​​在​​Visual Studio 2019​​​中占用4字节大小,数据范围为-2147483648到2147483647。它也
有可能在另一个平台上,仅占用2字节大小,数据范围为-32768到32767。
如果我们要求程序需要满足在不同的平台上均能正确的运行,不会因为整型数据范围不同而产生数据溢出。那么,我们可以为整型取一些别名。

作用范围

别名如果定义在代码块中,那么它就具有块作用域。别名的作用域从别名声明开始,直到包含声明的代码块结束。
如果定义在块外,那么它具有文件作用域。别名的作用域从声明开始,直到该源文件结束。

函数 add 中无法使用别名​​int32_t​

预处理指令、typedef、条件编译、多文件代码_#define_03

作用域内均可使用别名 int32_t 。

预处理指令、typedef、条件编译、多文件代码_#include_04

typedef 用于结构

typedef 并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的别名。

#include <stdio.h>
typedef struct
char name[20];
int gender;
double height;
double weight;
person;
int main()

person p = "timmy", 1, 170.00, 60.00 ; // 无需关键词struct
printf("name:%s\\n", p.name);
printf("gender:%d\\n", p.gender);
printf("height:%.2f\\n", p.height);
printf("weight:%.2f\\n", p.weight);
return 0;

输出结果

name:timmy
gender:1
height:170.00
weight:60.00

typedef与#define的区别

  1. typedef 只能用于给类型取别名,不能用于值。
  2. typedef 由编译器解释,而不是预处理器。
  3. typedef 在某些情况下,比 #define 更合适。

提高整型可移植性

包含头文件 stdint.h ,即可使用别名。

打开头文件 stdint.h ,可以看到这些别名的定义。

预处理指令、typedef、条件编译、多文件代码_c语言_05


为保证函数​​printf​​​转换规范的可移植性,需要编译器提供的另外一个头文件​​inttype.h​​。

以Visual Studio 2019中为例,打开头文件 inttype.h ,可以找到如下定义。

// 有符号
#define PRId8 "hhd"
#define PRId16 "hd"
#define PRId32 "d"
#define PRId64 "lld"
// 无符号
#define PRIu8 "hhu"
#define PRIu16 "hu"
#define PRIu32 "u"
#define PRIu64 "llu"

在其他平台下,头文件​​inttype.h​​​将根据本平台中整型的别名,定义对应的转换规范。若​​int32_t​​​是整型​​long​​​的别名,则打印32位有符号整型的宏​​PRId32​​的定义为"ld"。

#include <stdio.h>
#include <inttypes.h>
int main()

int32_t n = 123;
printf("n = %" PRId32 "\\n", n);
return 0;

在Visual Studio 2019中,​​"n = %" PRId32 "\\n"​​​会被替换为​​"n = %" "d" "\\n"​​​,而相邻的字符串将会被拼接为一个字符串,即​​"n = %d\\n"​​​。
在​​​int32_t​​​是整型​​long​​​的别名的平台下,​​"n = %" PRId32 "\\n"​​​会被替换为​​"n = %" "ld" "\\n"​​​,而相邻的字符串将会被拼接为一个字符串,即​​"n = %ld\\n"​​。

条件编译

#if、#elif、#else

​#if​​​后无需括号,直接填写条件表达式,并用空格隔开。
不同于​​​if​​​,​​#if​​​要求条件表达式为一个常量表达式。常量表达式中不允许出现变量。
由于预处理指令中不使用花括号,无法将多条语句组成一条复合语句,所以需要用​​​#endif​​​指令标记指令块结束。就算​​#if​​​下仅有一条语句,也需要使用​​#endif​​标记指令块结束。

区别

预处理中的​​#if​​​:
预处理指令将在编译前,由预处理器处理。预处理器根据预处理指令的意图,修改代码。类似于​​​#define​​​指令,替换代码中出现的宏。​​#if​​​指令会根据分支的走向,保留需要走向分支的代码,删除被跳过分支的代码。
关键词​​​if​​​:
编译后,程序运行时,计算条件表达式的结果。根据表达式结果,让程序走向不同的分支。

预处理指令、typedef、条件编译、多文件代码_#define_06

由于在预处理时就需要计算出条件表达式​​N == 1​​​的结果。此时,程序还未编译并运行,不能使用任何变量。所以,条件表达式必须为一个常量表达式。
而​​​N​​​是由​​#define​​​定义的符号常量,值为0,表达式结果为假。那么,​​#if​​​到​​#endif​​组成的指令块中的代码将被删除。

#include <stdio.h>
#define N 0
int main()

#if N == 1
printf("111111\\n");
printf("222222\\n");
printf("333333\\n");
#elif N == 2
printf("AAAAAA\\n");
printf("BBBBBB\\n");
printf("CCCCCC\\n");
#else
printf("******\\n");
#endif
return 0;

#ifdef、#ifndef

​#ifdef​​​指令是​​if​​​和​​defined​​的缩写,意为是否定义了某某宏。

若定义了该宏,则保留指令块内的代码。否则,则删除代码块内的代码。

预处理指令、typedef、条件编译、多文件代码_c语言_07


与之相反,​​#ifndef​​​指令是​​if​​​和​​not defined​​的缩写,意为是否未定义了某某宏。

若定义了该宏,则删除指令块内的代码。否则,则保留代码块内的代码。

预处理指令、typedef、条件编译、多文件代码_c++_08

多文件代码

  1. 预处理:执行预处理指令,修改源代码。
  2. 编译:将预处理后的源代码转换为二进制目标文件。
  3. 链接:将需要用到的目标文件合并成可执行文件。

对于源文件来说,编译器是单个独立编译的,并生成对应的目标文件。
例如:
main.c 经过编译后,生成目标文件 main.obj 。
print.c 经过编译后,生成目标文件 print.obj 。
编译完成后,将会启动链接器。将所有的目标文件中,需要用到的代码链接为一个可执行文件

以“模仿printf”为例

print.c

#include <stdio.h>
void print(const char* str)

while (*str != \\0)

putchar(*str);
str++;

main.c

#include "print.c"  //需要用双引号,而非尖括号
int main()

print("Hello World\\n");
return 0;

预处理指令、typedef、条件编译、多文件代码_开发语言_09


为了正确编译​​main.c​​​,我们需要包含​​print.c​​​,让函数​​print​​​先定义后使用。目标文件​​main.obj​​​文件中有一份​​print​​​函数。而​​print.obj​​​文件,也有一份​​print​​函数。链接时,出现了同名函数的现象。因此,将链接失败。

问题的关键在于编译器是单个独立编译的,编译​​main.c​​​时,编译器不知道标识符​​print​​具体是什么

函数声明替换include

除了函数定义可以让编译器正确识别​​print​​​标识符,此外,函数声明也可以。
将文件​​​main.c​​​中的​​#include​​​指令先暂时去掉,换成函数​​print​​​的函数声明。
文件 main.c

void print(const char* str);
int main()

print("Hello World\\n");
return 0;

这样,在编译​​main.c​​​时,虽然不知道​​print​​​这个函数里面具体做了什么。但是,编译器知道这是一个函数,并且可以传什么参数给它,编译依然可以继续。编译生成的目标文件​​main.obj​​​中,指明需要一份​​print​​函数的实现。

预处理指令、typedef、条件编译、多文件代码_#define_10


链接时,目标文件​​main.obj​​​表示需要​​print​​​函数的具体实现。而正好​​print.obj​​中有该函数的具体实现。这样,它们可以被链接为一个可执行文件

将文件print.c中的代码删除,看看会发生什么

预处理指令、typedef、条件编译、多文件代码_c++_11


函数​​main.obj​​​文件中的函数​​main​​​需要​​print​​​函数的具体实现,而现在无法提供​​print​​函数的具体实现。因此,出现链接错误。

现在恢复代码

目前,文件 print.c 里面只定义了一个函数。若 print.c 里面定义的函数较多,在其他文件里面需要使
用这些函数时,那么还需要重复声明这些函数。
例如:文件 print.c 内定义了N个函数。若文件 main.c 中需要使用这些函数,则需要在文件 main.c 中
声明这些函数。

文件 main.c

void print1(const char* str);
void print2(const char* str);
void print3(const char* str);
void print4(const char* str);
void print5(const char* str);
...
void printN(const char* str);
int main()

print("Hello World\\n");
return 0;

那么,不如把这些声明单独写在一个文件里面,谁需要使用这些函数,就包含这个文件就好。并且,这种文件不需要经过编译器编译,仅供被其他文件包含。具有这种性质的文件被称作头文件。区别于需要被编译器编译的文件,其后缀名用​​.h​​。

将函数print的声明写入文件print.h。

文件 print.h

void print(const char* str);
将文件 main.c 中的函数声明改为包含头文件。
#include "print.h"
int main()

print("Hello World\\n");
return 0;

这种文件不需要经过编译器编译,仅供被其他文件包含。具有这种性质的文件被称作头文件。

预处理指令、typedef、条件编译、多文件代码_#include_12


一般情况下,系统自带函数的源文件被预先编译为了库,而编译器默认链接了该库。所以,我们无需做其他配置,也看不到这些函数实现的源文件。

更复杂的多文件代码

#include <stdio.h>
typedef struct
char name[20 + 1];
int gender;
double height;
double weight;
Person;
Person newPerson()

Person p;
printf("intput name (No more than %d letters):", 20);
scanf("%s", p.name);
printf("input gender (1.male 2.female):");
scanf("%d", &p.gender);
printf("intput height:");
scanf("%lf", &p.height);
printf("intput weight:");
scanf("%lf", &p.weight);
return p;

void printPerson(const Person* p)

printf("\\nname\\tgender\\theight\\tweight\\n");
//使用了成员间接运算符->
printf("%s\\t%d\\t%.2f\\t%.2f\\n", p->name, p->gender, p->height, p->weight);

int main()

Person p;
p = newPerson();
printPerson(&p);
return 0;

我们定义一个人员类型,类型名为​​Person​​​。它由名称、性别、身高、体重几个成员组成。性别用整型表示,1代表男生、2代表女生。
姓名的长度限制为20个字符。别忘了,结尾标记​​​\\0​​​也要占用一个字节的空间。因此,​​name​​​数组的长度为21。
接着我们定义一个人员信息输入函数。这个函数提示用户输入对应的信息,最后返回一个​​​Person​​​类型的结构。
在调用函数​​​printPerson​​​时,函数实参将被传递给函数形参。若传递的数据为​​Person​​​,则需要将整个结构传递进入函数,传递的数据量为​​sizeof(Person)​​​字节。
为了减少数据在函数之间传递的开销,我们将传递结构​​​Person​​​改为,传递指针​​Person *p​​​进入​​printPerson​​​函数。改为传递指针后,函数间传递的数据量仅需要​​sizeof(Person *)​​​字节。指针的大小在32位程序下为4,64位程序下为8。比起传递整个结构,还是小多了。
此外,函数​​​printPerson​​​仅仅是读取各成员数据用于显示,并不会修改任何信息。因此,我们在指针上使用​​const​​​关键词,限定为只读。保证不会因为误操作而修改了数据。同时,使用这个函数的人看到后,也知道这个函数不会修改​​Person​​​结构的数据。
最后,函数​​​main​​​中,声明一个​​Person​​结构变量。调用上述两个函数录入、显示成员信息

将代码进行模块化

代码中,出现了两个​​20​​,均指代人员名称的最大字符长度。若以后需要增加人员名称长度,那么我们需要同时修改两个数值。如果不小心,还会漏改。不如将人员名称长度定义为一个符号常量,以后仅需修改符号常量的数值,即同步修改所有用到该符号常量的地方。

文件 person.h

#define NAME_LENGTH 20
typedef struct
char name[NAME_LENGTH + 1];
int gender;
double height;
double weight;
Person;
Person newPerson();
void printPerson(const Person* p);

文件 person.c

#include <stdio.h>
#include "person.h" \\\\ 定义或声明来自于person.h
Person newPerson()

Person p;
printf("sizeof person in person.c %d", sizeof(Person));
printf("intput name (No more than %d letters):", NAME_LENGTH);
scanf("%s", p.name);
printf("input gender (1.male 2.female):");
scanf("%d", &p.gender);
printf("intput height:");
scanf("%lf", &p.height);
printf("intput weight:");
scanf("%lf", &p.weight);
return p;

void printPerson(const Person* p)

printf("\\nname\\tgender\\theight\\tweight\\n");
printf("%s\\t%d\\t%.2f\\t%.2f\\n", p->name, p->gender, p->height, p->weight);

文件 main.c

#include "person.h" \\\\ 定义或声明来自于person.h
int main()

Person p;
p = newPerson();
printPerson(&p);
return 0;

由于​​main.c​​​中没有标识符​​Person​​​的声明或定义。编译​​main.c​​​时,将无法识别标识符​​Person​​​。
所以,我们将​​​Person​​​结构类型的定义与符号常量​​NAME_LENGTH​​​,在文件​​person.h​​中也写了一遍。

为什么没有重定义报错

代码中出现了重复的声明或定义,构建时为什么不会出现重定义报错呢?
这是因为,重复的代码出现在不同文件中。
我们知道作用域分为两种:

  1. 块作用域:定义或声明在代码块内。
  2. 文件作用域:定义或声明在代码块外。

这里的定义或声明均在函数外。那么它们的作用域都是文件作用域。而重复代码在不同的文件中,作用域并未重叠。因此,能够构建成功。
但是,若以后需要调整代码,必须保证它们同时调整。例如,文件​​​person.c​​​中的​​Person​​​结构类型增加了一个成员。那么,文件​​person.h​​​中的​​Person​​​结构类型也需要相应的调整。否则,两边的 Person 不一致,虽然可以通过编译,但是运行时将有可能发生崩溃。
如果能让它们使用同一份代码就比较完美了。
我们将文件​​​person.c​​​中的重复代码删除,使用​​#include "person.h"​​​指令,包含文件​​person.h​​​。这样,就能保证定义是唯一的。
预处理后,文件​​​main.c​​​以及文件​​person.c​​​的关于人员的声明或定义均来自于文件​​person.h​​​。这样,就
能保证它们用的是同一份代码了。虽然,文件​​​person.h​​​中的函数声明没有必要出现在文件​​person.c​​中,但是,这样做并不碍事。

多文件代码小结

  1. 源文件​​person.c​​: 函数定义。
  2. 头文件​​person.h​​: 符号常量、函数宏、函数声明、结构声明、类型定义。
  3. 源文件​​person.c​​​需要头文件​​person.h​​​中的声明或定义。因此,需要在源文件中​​#include "person.h"​​。
  4. 使用者,例如文件​​main.c​​​。包含头文件​​person.h​​后,即可使用头文件中的声明或定义以及调用头文件中声明过的函数。

头文件守卫

重复包含

文件 main.c

#include "person.h" // 对person.h包含一次
#include "person.h" // 对person.h包含两次
int main()

Person p;
p = newPerson();
printPerson(&p);
return 0;

这种情况会导致文件​​main.c​​因为标识符重定义而编译失败

更隐蔽的重复包含

文件 main.c

#include "person.h"
#include "students.h"
int main()

Student s;
s = newStudent();
printStudent(&s);
return 0;

假设,头文件​​students.h​​​内包含了​​person.h​​​。这样依然会导致头文件​​person.h​​被重复包含的问题。并且,若嵌套层次更加复杂,会比较难排查。

头文件守卫

借助条件编译,使同一个头文件,只允许被包含一次。
添加的位置是在头文件内。
​​​#define PERSON_H​​​戳,用于记录是否定义
预处理指令​​​#ifndef​​用于测试其后跟着的宏是否没有被定义。

  1. 若没有被定义,则保留从​​#ifndef​​​到​​#endif​​之间的代码。
  2. 若被定义,则删除从​​#ifndef​​​到​​#endif​​之间的代码。

main.c

// -----------第一次包含-----------
#ifndef PERSON_H
#define PERSON_H
person.h头文件代码
#endif
// -----------第二次包含-----------
#ifndef PERSON_H
#define PERSON_H
person.h头文件代码
#endif

int main()

...

第一次包含时,预处理指令​​#ifndef​​​测试到宏​​PERSON_H​​​未定义。因此,将保留从​​#ifndef PERSON_H​​​开始,直到​​#endif​​的代码。而这段代码内,定义了宏​**PERSON_H**​​​。
第二次包含时,预处理指令​​​#ifndef​​​测试到宏​​PERSON_H​​​已经定义。因此,将删除从​​#ifndef PERSON_H​​​开始,直到​​#endif​​的代码。

嵌套重复包含

文件​​main.c​​​包含了头文件​​person.h​​​和​​students.h​​​。假设,头文件​​students.h​​​内又包含了​​person.h​​。

main.c

// -----------person.h-----------
#ifndef PERSON_H
#define PERSON_H
person.h头文件代码
#endif
// -----------student.h-----------
#ifndef STUDENT_H
#define STUDENT_H
// -----------person.h-----------
#ifndef PERSON_H
#define PERSON_H
person.h头文件代码
#endif
student.h头文件代码
#endif

int main()

...

直接包含​​person.h​​​时,预处理指令​​#ifndef​​​测试到宏​​PERSON_H​​​未定义。因此,将保留从​​#ifndef PERSON_H​​​开始,直到​​#endif​​​的代码。而这段代码内,定义了宏​​PERSON_H​​​。
嵌套包含​​​person.h​​​时,预处理指令​​#ifndef​​​测试到宏​​PERSON_H​​​已经定义。因此,将删除从​​#ifndef PERSON_H​​​开始,直到​​#endif​​的代码。

补充说明

头文件守卫中测试和定义的宏名称可以随意设置。例如,​​PERSON_H​​​,只要不和其他头文件一样即可。
这样,除了第一次包含的代码外,其他包含的代码被删除。保证了,在一个文件内,同一个头文件仅被包含一次。
注意,这并不是意味着这个头文件不能再被其他文件包含了。由于,宏定义的作用域是文件作用域。头文件守卫仅保证在该文件内,一个头文件只能被包含一次。

​#pragma once​​指令

如果编译器支持​​#pragma once​​​指令。在头文件首部使用​​#pragma once​​​。也可以达到头文件守卫的效果。
两种形式的防止多重包含的示例如下:

使用条件编译指令

#ifndef PERSON_H
#define PERSON_H
#define NAME_LENGTH 20
typedef struct
char name[NAME_LENGTH + 1];
int gender;
double height;
double weight;
Person;
Person newPerson();
void printPerson(const Person* p);
#endif

使用​​#pragma once​​指令

#pragma once
#define NAME_LENGTH 20
typedef struct
char name[NAME_LENGTH + 1];
int gender;
double height;
double weight;
Person;
Person newPerson();
void printPerson(const Person* p);


以上是关于预处理指令typedef条件编译多文件代码的主要内容,如果未能解决你的问题,请参考以下文章

使用方括号而不是引号定义的供应商的预处理指令

C-进制,多文件开发,预处理,地址

预处理指令

C#-#define条件编译

C++笔试常见问题

C#-C#-#define条件编译