(一)程序的翻译环境
(二)程序的执行环境
(三)预定义符号的介绍
(四)预处理指令#define
(五)预处理操作符#和##的介绍
(六)条件编译
(一)翻译环境:源代码被转换为可执行的机器指令 (二进制)
编译 + 链接 运行
text.c——————翻译环境—-------——————>text.exe-——运行环境————>
文本文件 二进制文件
(源文件 源程序) (可执行程序)
源文件1(.c) 经过编译器生成 | 目标文件1(.obj) |
源文件2 | 目标文件2 |
源文件3 | 目标文件3 |
链接库 | 链接器(目标文件通过链接器生成可执行程序(.exe)) |
(1)组成一个程序的每个源文件通过编译过程分别转换为目标文件
(2)每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序
(3)链接器同时也会引入标准c函数库中任何被该程序所用到的函数,而且它可以搜索个人的程序库,将所需要的函数也链接到程序中去。
test.c text.exe
翻译环境 | 运行环境 |
编译 链接 |
目标文件
编译又可以分为三个阶段
预编译 | 编译 | 汇编 |
预编译:(文本操作)
(1)在linux环境下: 如果输入gcc.-E text.c 进行预编译///预处理
(2) #include头文件的包含在预编译系统就进去了
(3)预编译的时候删除注释(使用空格来替换注释)
(4) 预处理阶段也会完成#define
编译:把c代码翻译为汇编代码
语法分析 词法分析 语义分析 符号汇总(函数名,全局变量)
汇编:形成符号表
链接:
1.合并段表
2.符号表达的合并和重定位
合并段表:生成的目标文件(.o)有固定格式(elf),会生成好几个段,但放的内容不一样,需要将它们合并,exe的文件格式也是(elf)
符号表:
add | 0x000 |
main | 0x200 |
add ox100 |
合并之后
add | ox100 |
main | 0x200 |
(二)运行环境
1.程序必须要载入内存中,在有操作系统的环境中,这个一般由操作系统完成。在独立的环境中,程序的载入必须要手工安排,也可能是通过可执行代码置入只读内存中完成。
2. 程序的执行便开始,接着便调用main函数
3.开始执行程序代码,这个时候程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用static内存,存储于静态内存中的变量可以在程序的整个执行过程中一直保留他们的值。
4.终止程序,正常终止main函数,也可能是意外终止
(三)预定义符号
_FILE_:这个对应的文件名
_LINE_:对应的行号
_DATE_:当前日期
_TIME_:当前时间
_STDC_:如果编译器遵循ANSIC,其值为1,否则未定义
#include<stdio.h>
int main()
{
printf("%s",_FILE_);
printf("%d",_LINE_);
}
#include<stdio.h>//编写日志
int main()
{
int i;
int arr[10]={10};
FILE*pf=fopen("log.txt","w");
for(i=0;i<10;i++)
{
arr[i]=i;
fprintf(pf,"file:%s line:%d date:%s i=%d",
_FILE_,_LINE_,_DATE_,_TIME_,i);
}
fclose()
for(i=0;i<10;i++)
{
printf("%d",arr[i]);
}
}
(四)预处理指令
#define #include #pragma pack[4] #pragma #if #endif #itdef #line
#define:可以定义宏和标识符
#define reg register;//为register这个关键字创建一个简短的名字
#define MAX 1000;
#define CASE break;case//在case造句的时候自动把break写上
#define do_forever for(::) //用更形象的语言替换一种实现(死循环)
#define do_forever for(;;)
int main()
{
for(;;)
死循环的内容
;
return 0;
}
#define包含了一个规定,允许把参数替换到文本中。这种实现称为定义宏。宏不可以递归。字符串中内容并不会被替换。
#define square(x) x*x
int main()
{
int ret=square(5+1);
return 0;
}//红的参数是替换的而不是传参的 5+1*5+1=11
#define square(x) (x)*(x)
int main()
{
int ret=square(5+1);
return 0;
}//(5+1)*(5+1)=36//写宏的时候不要吝啬括号
(五)#和##的区别
把参数插入字符串。#x会把内容变成”a"
#define CAT(X,Y) X##Y
int main()
{
int class84=2020;
printf("%d",CAT(class,84));
//等价于printf("%d",class84);
//##可以把位于它俩边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
}
printf("woai");==printf("wo""ai");//俩个字符串会自动连在一起。
#define print(x) printf("the value of"#x"is %d\n",x)
int main()
{
int a=10;
int b=20;
print(a);
print(b);
}
//带有副作用的宏
#include<stdio.h>
#define MAX(X,Y) (X)>(Y)?(X):(Y)
int main()
{
int a=10;
int b=11;
int max=MAX(a++,b++);
//int max=(a++)>(b++)?(a++):(b++);
printf("%d%d%d",max,a,b);//12 11 13
}
函数调用的时候会有函数返回和调用的开销,宏在预处理阶段就完成了替换,没有函数的调用和返回的开销。 宏与类型无关,而函数必须声明类型。但是,每次使用宏的时候,一份宏的代码插入到程序中,会增加程序的长度。并且宏没办法调试,宏与类型无关则不够严谨。宏会带来运算的优先级问题容易出错。
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int *p=MALLOC(10,int);
}
#undef:移除一个宏定义
(六)条件编译
{#ifdef debug;//如果debug被定义过下面的句子参与编译 定义在上面用#define
//#ifdef defined(debug)
printf("%d",a);
#endif}
#if 常量表达式//表达式为真则编译
//...
#endif
#if 常量表达式
//
#elif 常量表达式
//
#else 常量表达式
//
#endif
#ifndef debug==#ifdef !debug
NOTE:
防止头文件被重复包含,在头文件前面写
#ifndef _test_h
#define _test_h
//头文件的内容
#endif
或者#pragma once