test.c(C代码,文本文件)                                   test.exe(二进制的信息)

源文件(源程序)_ ​_​ _   编译(编译器)   ______链接(链接器)______运行_____

                              翻译环境                                               运行环境

程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境,它用于实际执行代码。

详解编译+链接

C语言预处理_头文件

目标文件:test.obj;


组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。

每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

编译本身也分为几个阶段:预编译,编译,汇编

预编译:#include头文件的包含,注释删除,#define

编译:把C语言代码翻译成汇编代码,包含:语法分析,词法分析,语义分析,符号汇总;

汇编:形成符号表,汇编代码转换成二进制指令(.o文件);

链接:合并段表,符号表的合并和符号表的重定位;

预处理详解

预定义符号

__FILE__      //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
int main()
{

printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}

C语言预处理_头文件_02

int main()
{
//写日志文件
int i = 0;
int arr[10] = { 0 };
FILE* pf = fopen("log.txt", "w");
for (i = 0; i < 10; i++)
{
arr[i] = i;
fprintf(pf,"file:%s line:%s date:%s time:%s i=%d\n",
__FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}

#define定义标识符

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现

提问:

在define定义标识符的时候,要不要在最后加上;?

建议不要加上;,这样容易导致问题。 比如下面的场景:

#define MAX 100;
int main()
{
int a = MAX;
printf("%d\n", MAX);

return 0;
}

C语言预处理_头文件_03

#define定义宏

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

下面是宏的申明方式:#define name( parament-list ) stuff 其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。

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

#define square(X) X*X
int main()
{
int ret = square(5);
printf("%d\n", ret);
return 0;
}

C语言预处理_#endif_04

注意:宏实现的参数的替换,不是传参

例:

#define square(X) X*X
int main()
{

int ret = square(5 + 1);
printf("%d\n", ret);
return 0;
}

C语言预处理_#define_05

改进:

#define square(X) (X)*(X)

注意:括号的使用

#define double(X) X+X
int main()
{
int ret = 10 * double(5);
printf("%d\n", ret);
return 0;
}

C语言预处理_#endif_06

改进:

#define double(X) (X)+(X)

提示:

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

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

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

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

注意:

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

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

#和##

如何把参数插入到字符串中?

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

#define PRINT(X) printf("the value of "#X" is %d\n",X)
int main()
{
int a = 10;
int b = 20;
PRINT(a);
//printf("the value of ""a"" is %d\n",a);
PRINT(b);
//printf("the value of ""b"" is %d\n",b);
return 0;
}

C语言预处理_#endif_07

##的作用

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

#define CAT(X,Y) X##Y
int main()
{

int Class2022 = 2019;
printf("%d\n", CAT(Class, 2022));
return 0;
}

C语言预处理_头文件_08

带副作用的宏参数

带副作用的参数

int main()
{
int a = 10;
int b = ++a;

return 0;
}

++a可能是带副作用的参数,因为改变了a的初始值

带副作用的宏参数

#define MAX(X,Y) (X)>(Y)?(X):(Y)
int main()
{
int a = 10;
int b = 20;
int max = MAX(a++,b++);

printf("%d\n", max);
printf("%d\n", a);
printf("%d\n", b);

return 0;
}

C语言预处理_#endif_09

宏和函数的对比

//函数
int Max(int x, int y)
{
return(x > y ? x : y);
}
//宏
#define MAX(X,Y) (X)>(Y)? X:Y
int main()
{
int a = 10;
int b = 20;
int max = Max(a, b);
printf("max=%d\n", max);
max = MAX(a, b);
printf("max=%d\n", max);
return 0;
}

C语言预处理_#define_10

宏通常被应用于执行简单的运算。

比如在两个数中找出较大的一个。#define MAX(a, b) ((a)>(b)?(a):(b))

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

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

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

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

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2. 宏是没法调试的。

3. 宏由于类型无关,也就不够严谨。

4. 宏可能会带来运算符优先级的问题,导致程序容易出现错。

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

#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int* p = MALLOC(10, int);//类型

return 0;
}

详解宏和函数的对比

C语言预处理_头文件_11

命名约定

一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:

把宏名全部大写 函数名不要全部大写

#undef

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

#define MAX 100
int main()
{
printf("MAX=%d\n", MAX);
#undef MAX
printf("MAX=%d\n", MAX);

return 0;
}

C语言预处理_#define_12

命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

#include <stdio.h> 
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}

编译指令

gcc -D ARRAY_SIZE=10 programe.c

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

#define DEBUG
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#ifdef DEBUG //如果定义了编译,否则不编译
printf("%d ", arr[i]);
#endif
}
return 0;
}

常见的条件编译指令

C语言预处理_#define_13

C语言预处理_#endif_14

1.单分支

C语言预处理_#define_15

C语言预处理_#endif_16

2.多分支

int main()
{
#if 1==1
printf("hehe\n");
#elif 2==1
printf("haha\n");
#else
printf("呵呵\n");
#endif

return 0;
}

C语言预处理_#define_17

3.判断是否被定义

#define DEBUG 0  //定义了则会打印
int main()
{
#if defined(DEBUG)
printf("hehe\n");
#endif
return 0;
}

C语言预处理_头文件_18

C语言预处理_#endif_19

相同的写法

#define DEBUG 0
int main()
{
#ifdef DEBUG
printf("hehe\n");
#endif
return 0;
}

C语言预处理_#endif_20

文件包含

头文件被包含的方式:

本地文件包含

#include"stdio.h"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。

​查找速度慢​

​​库文件包含

#include<stdio.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用“”的形式包含?

​ 答案是肯定的,可以。但是这样做查找的​效率就低些,当然这样也不容易区分是库文件还是本地文件了。

解决重复包含头文件的办法

每个头文件开头写

1.

#ifndef __TEST__H_
#define __TEST__H_

int ADD(int x,int y)

#endif

2.

#pragma once
int ADD(int x, int y);

百度关于宏的一道面试题

题目:请编写宏,计算结构体中某变量相对于首地址的偏移,并给出说明

offsetof

​库函数的使用

#include<stddef.h>//注意头文件
struct S
{
char c1;
int a;
char c2;
};
int main()
{

printf("%d\n", offsetof(struct S, c1));
printf("%d\n", offsetof(struct S, a));
printf("%d\n", offsetof(struct S, c2));

}

C语言预处理_头文件_21

模拟实现offsetof

​实现思路:通过0偏移出找到成员,然后减去0,就得到了偏移量​

struct S
{
char c1;
int a;
char c2;
};
#define OFFSETOF(struct_name,member_name) (int) &(((struct_name*)0)->member_name)
//解释:首先将0强转化结构体指针,找到地址,然后从0地址出找到成员,然后&,
//因为OFFSETOF返回值是整数,所以将地址强转化为(int);
int main()
{

printf("%d\n", OFFSETOF(struct S, c1));
printf("%d\n", OFFSETOF(struct S, a));
printf("%d\n", OFFSETOF(struct S, c2));

}

C语言预处理_头文件_22