一、typedef关键词
1.给整型类型取别名
原因:我们在整型数据类型中讨论过的,C语言标准并未规定这些数据类型的大小范围,具体的实现交由了编译器和平台决定。
也就是说,int在Visual Studio 2019中占用4字节大小,数据范围为-2147483648到2147483647。它也有可能在另一个平台上,仅占用2字节大小,数据范围为-32768到32767。
如果我们要求程序需要满足在不同的平台上均能正确的运行,不会因为整型数据范围不同而产生数据溢出。那么,我们可以为整型取一些别名。
![]()
 
我们将整型类型使用别名替代,在不同平台下编译时,仅需要更改别名对应的实际类型,即可避免不同平台下,整型数据范围不同而产生数据溢出。
 
 
 2. typedef关键词
除了使用#define,C语言还提供了typedef关键词用于定义类型别名。
typedef int int32_t;
原来的变量名,就变成了这种类型的别名。这样,我们就成功定义int的别名int32_t。
#include<stdio.h>
int main()
{typedef int int32_t;
int32_t n=123;
printf("n=%d\n",n);
return 0;}
结果为n=123
 
3. typedef与#define的区别
1.typedef只能用于给类型取别名,不能用于值。
2.typedef由编译器解释,而不是预处理器。
3.typedef在某些情况下,比#define更合适。
4.#define可以为值设置一个别名,而typedef不行。
某些情况下,使用typedef更合适。例如:我们定义char*的别名为STR。声明两个这种类型的变量。
#define STR char*STR name1,name2;
若使用#define定义的别名,宏STR展开后,代码如下。
char*name1,name2;
这样,仅有name1的类型为char*,而name2的类型为char。
若需要两个char*的变量,应当这样声明。
char *name1,*name2;
若使用typedef定义别名,两个变量均为char*类型。
typedef char *STR;
STR name1 name2;
 
4.提高整型可移植性
整型类型的别名无需我们自己定义,编译器会根据本平台的整型范围大小,设置对应的别名。我们仅需包含头文件stdint.h,即可使用这些别名。
![]()
#include<stdint.h>
int main()
{int32_t n=123;
printf("n=%d\n",n);
return 0;}
 
 
 
 最后,还有一个问题。函数printf的转换规范如何保证可移植性呢?
例如:在上面的代码中,使用了"%d"打印int32_t。若int32_t是整型int的别名,那么代码没有问题。
若int32_t是整型long的别名,那么应当使用%ld打印
我们需要编译器提供的另外一个头文件inttype.h。以Visual Studio 2019中为例,打开头文件inttype.h,可以找到如下定义。
![]()
 
#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"。
 
二、条件编译
在程序中运用分支结构这个知识点我们已经学过了。但是,下面我们需要将分支结构运用在预处理中。
 1.#if
在if关键词前加一个#,即#if。
现在,它从一个C语言关键词,变成了一个预处理指令了。
#if后无需括号,直接填写条件表达式,并用空格隔开。不同于if,#if要求条件表达式为一个常量表达式。常量表达式中不允许出现变量。
由于预处理指令中不使用花括号,无法将多条语句组成一条复合语句,所以需要用#endif指令标记指令块结束。并且,就算#if下仅有一条语句,也需要使用#endif标记指令块结束。
 
#include<stdio.h>
#define N 0
int main()
{#if N==1//常量表达式,无需括号
  printf("111111\n");
  printf("222222\n");
  printf("333333\n");
#endif//必须使用#endif标记指令块结束
  printf("AAAAAA\n");
  printf("BBBBBB\n");
  printf("CCCCCC\n");
  return 0;}
 
结果为AAAAAA
BBBBBB
CCCCCC
 
为什么有了if关键词,还需要使用预处理指令#if呢?
预处理中的#if:
预处理指令将在编译前,由预处理器处理。预处理器根据预处理指令的意图,修改代码。类似于#define指令,替换代码中出现的宏。
#if指令会根据分支的走向,保留需要走向分支的代码,删除被跳过分支的代码。
关键词if:
编译后,程序运行时,计算条件表达式的结果。根据表达式结果,让程序走向不同的分支。
会有一个删除的区别
 
2.#else
参照#if和分支语句的用法,#else类似
 
3.#elif
嵌套if的写法elseif,在预处理命令里面对应的不是组合两个命令,而是一个新的命令#elif。
#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;}
保留打印星号的printf函数调用。其他printf函数调用均删除。
#include<stdio.h>
int main()
{printf("******\n");}
 
 
4.#ifdef、#ifndef
#ifdef指令是if和defined的缩写,意为是否定义了某某宏。若定义了该宏,则保留指令块内的代码。否则,则删除代码块内的代码。
#include<stdio.h>
#define printNumber
int main()
{#ifdef printNumber
printf("111111\n");
printf("222222\n");
printf("333333\n");
#endif
#ifdef printLetter
printf("AAAAAA\n");
printf("BBBBBB\n");
printf("CCCCCC\n");
#endif
return 0;}
结果为111111
222222
333333
 
与之相反,#ifndef指令是if和notdefined的缩写,意为是否未定义了某某宏。
 
三、多文件代码
 
 #include的两种形式预处理指令
#include有两种形式:
#include<文件名> 
#include"文件名"
1.文件名在尖括号内:将会在编译器的包含目录中搜索文件
。2.文件名在双引号内:先在当前目录中搜索文件,再到编译器的包含目录中搜索文件。
对于stdio.h文件来说,它是编译器自带的文件,在编译器的包含目录中。所以使用尖括号,即可找到该文件。
 
仿照printf的形式,写一个简化版的print函数。并使其可以通过#include来使用
 
Visual Studio 2019中,源文件上点右键,选择【添加】,在选择【新建项】。新建一个C++文件,并将文件名改为print.c。并写入代码
#include<stdio.h>
void print(const char*str)
{
while(*str!='\0')
{putchar(*str);
str++;}
}
 
接下来,我们在文件main.c中,包含文件print.c并使用print函数。文件main.c
#include<print.c>
int main()
{print("HelloWorld\n");
return 0;}
出现了一个编译错误:无法打开包括文件"print.c"。
这是因为中,编译器的包含目录中没有文件"print.c"。我们需要扩大搜索范围,使用双引号形式的#include命令,会在当前目录搜索文件,如果没有则会继续搜索编译器的包含目录中是否有该文件。
而我们刚刚是在当前目录下新建的print.c文件。
 
现在,编译通过了,但是出现了一个链接错误:print函数重定义。注意其错误代码为LNK开头。LNK为链接Link的缩写。
我们深入地了解一下从代码到可执行文件的构建过程。
 1. 预处理:执行预处理指令,修改源代码。
 2. 编译:将预处理后的源代码转换为二进制目标文件。 
3. 链接:将需要用到的目标文件合并成可执行文件。
![]()
 
 为了正确编译 main.c ,我们需要包含 print.c ,让函数 print 先定义后使用。目标文件 main.obj 文件 中有一份 print 函数。而 print.obj 文件,也有一份 print 函数。
链接时,出现了同名函数的现象。因 此,将链接失败。
 
 除了函数定义可以让编译器正确识别 print 标识符,此外,函数声明也可以。 我们将文件 main.c 中的 #include 指令先暂时去掉,换成函数 print 的函数声明。
 
 链接时,目标文件 main.obj 表示需要 print 函数的具体实现。而正好 print.obj 中有该函数的具体实 现。这样,它们可以被链接为一个可执行文件
目前,文件 print.c 里面只定义了一个函数。若 print.c 里面定义的函数较多,在其他文件里面需要使 用这些函数时,那么还需要重复声明这些函数。
不如把这些声明单独写在一个文件里面,谁需要使用这些函数,就包含这个文件就好。并且,这种文件不需要经过编译器编译,仅供被其他文件包含。
 
具有这种性质的文件被称作头文件。区别于需要 被编译器编译的文件,其后缀名用 .h 。