目录
3.#define定义宏(可以理解成特殊的比较"死板"的函数)
1.翻译环境和运行环境
翻译环境:代码源在这里被转换成可执行的机器指令(二进制)。
运行环境:用于实际执行代码

2.翻译环境由两大过程组成:编译和链接
如下:

- 多个**.c⽂件** 单独经过编译器,编译处理⽣成对应的**⽬标⽂件**。
- 在Windows环境下的⽬标⽂件的后缀是 .obj ,Linux环境下⽬标⽂件的后缀是 .o
- 多个⽬标⽂件和链接库⼀起经过链接器处理⽣成最终的可执⾏程序。
- 链接库是指运⾏时库 (它是⽀持程序运⾏的基本函数集合)或者第三⽅库。
Windows系统是高度集成的,很多细节观察不到,一般用Linux环境观察。(需要自行配置)
编译过程大致如下:

2.1预处理(预编译)(.c->.i)(详解在下文)
gcc编译环境下,x想观察.i文件,使用预编译指令:
gcc -E test.c -o test.i
- 将所有的 #define 删除,并展开所有的宏定义
- 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif
- 处理#include 预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进⾏的,也就是说被包含的头⽂件也可能包含其他⽂件
- 删除所有的注释
- 添加**⾏号和⽂件名标识**,⽅便后续编译器⽣成调试信息等
- 保留所有的**#pragma** 的编译器指令,编译器后续会使⽤
2.2编译(.i->.s->.o)
过程包括,词法分析,语法分析,语义分析
编译指令:
gcc -S test.i -o test.s
2.2.1词法分析
此过程代码被输入扫描器,进行词法分析,将代码中的字符转换成一系列有意义的记号,方便进行语法分析。
2.2.2语法分析
语法分析器,对记号进行语法分析,根据语法规则产生语法树(以表达式为节点,理顺表达式方便语义分析)
2.2.3语义分析
检查代码是否有意义,比如变量是否声明,类型是否兼容等
2.3汇编(将汇编代码转换成机器可执行的二进制指令)
指令
gcc -c test.s -o test.o
2.4链接(解决一个项目中,多文件,多模块相互调用的问题)
链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才**⽣成可执⾏程序**。
链接过程主要包括:地址和空间分配 ,**符号决议和重定位(汇总各文件全局符号的符号表,进行决议和重定位,生成新的符号表,起统一的作用)**等这些步骤。
3.运行环境
程序必须载⼊内存 中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成(单片机)。 程序的执⾏便开始。
接着便调⽤main函数。 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程 ⼀直保留他们的值。
. 终⽌程序。正常终⽌main函数;也有可能是意外终⽌。
下面的内容是对预编译的详细补充
1.预定义符号
c中有一些可以直接使用的预定义符号(它们是在预编译阶段被处理的)
FILE // 进⾏编译的源⽂件
LINE // ⽂件当前的⾏号
DATE // ⽂件被编译的⽇期
TIME // ⽂件被编译的时间
STDC // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
例
cpp
printf("file:%s line:%d\n", __FILE__, __LINE__);
2.#define定义常量
define name stuff
例
cpp
#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字
//register的作用是把指定变量放到寄存器中,增加运行速率,但是register只起建议性作用,要不要把变量放进去由系统决定
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
#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定义标识符的低吼,不要加;
在进行文本替换的时候容易重复导致出错
例
cpp
#define MAX 1000;
if(condition)
max = MAX;
else
max = 0;
这边if和else之间有了两条语句,其中一个是由于重复导致的空语句,而if在没有{ }的情况下默认只能跟一个语句,语法错误
3.#define定义宏(可以理解成特殊的比较"死板"的函数)
#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏 (define macro)。
宏的声明
define name( parament-list ) stuff
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。
例
cpp
#define SQUARE( x ) x * x
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
上述代码等价于
cpp
printf ("%d\n",a + 1 * a + 1 );
由于运算符的优先级发生了逻辑错误,所以在使用宏的时候尽量给参数加上括号
cpp
#define SQUARE(x) (x) * (x)
这样就不会发生上述问题。
4.带有副作用的宏
x+ 1 ; // 不带副作⽤
x++; // 带有副作⽤
cpp
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
上述代码就体现出有副作用的宏对代码的影响,后置++先用再加,运行完之后,y和x的值已经变了,得不到原来的值
-
z = 9
-
x = 6(只在比较时自增一次)
-
y = 10(比较和结果各自增一次)
5.宏的替换规则
1.在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2.替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
宏参数和#define 定义中可以出现其他#define定义的符号 。但是对于宏,不能出现递归(自己定自己)。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。如pringf(" ")
" "中的宏无法被替换。
6.宏和函数的对比
7.#和##
7.1#运算符
#运算符将宏的⼀个参数 转换为字符串字⾯量
它仅允许出现在带参数的宏的替换列表中
#运算符所执⾏的操作可以理解为**"字符串化**"
例:
cpp
#define PRINT(n) printf("the value of "#n " is %d", n);
int a=10;
PRINT(a);//printf("the value of ""a" " is %d", a);
打印:the value of a is 10
7.2##运算符(记号粘合)
可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符
cpp
int int_max(int x, int y)
{
return x > y ? x : y;
}
float float_max(float x, float y)
{
return x > y ? x : y;
}
cpp
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}
cpp
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{
//调⽤函数
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}
体会这种奇妙的用法,生成一个函数模板,只需要提供相应的类型就能生成相应的函数。
8.#undef
这条指令⽤于移除⼀个宏定义。
undef NAME
// 如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
9.条件编译
用来决定是否将⼀条语句(⼀组语句)编译或者放弃。
对于调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。
cpp
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
常用的条件编译指令
cpp
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分⽀的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#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
12.头文件被包含
12.1头文件被包含方式
12.1.1本地文件
include "filename"
查找策略:
先在源⽂件所在⽬录 下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。
如果找不到就提⽰编译错误。
12.1.2库文件包含
#include <filename.h>
查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误
对于库⽂件也可以使⽤ " " 的形式包含但是这样做查找的效率就低些 (因为要找两次),当然这样也不容易区分是库⽂件还是本地⽂件了。
12.2嵌套⽂件包含
已知,#include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样
下列文件
test.c
cpp
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
test.h
cpp
void test();
struct Stu
{
int id;
char name[20];
};
如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。这样工程量很大。
我们可以用条件编译,在头文件中写下下列代码。
cpp
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__
或者写
cpp
#pragma once
这样头文件只会被编译一次了