程序处理和预编译
- 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第一种是编译环境,
.c
文件到.exe
文件,在这个环境中源代码被转换为可执行的机器指令。好比:vs编辑器,gcc编译器。第二中,就是执行环境,用于执行代码。例如:windows\linux等。
每一个源文件对应一个编译器的处理结果。
使用Linux
- 预处理阶段
gcc test.c -E > test.i
- 完成头文件的包含#include,<就是在自己编写的代码前面,把头文件换成,一些库中的函数,printf()#include <stdio.h>>
- #infine 定义的符号和宏的替换,<就是把宏替换,但不做运算。>
- 注释删除,<自己写的注释,被删除了>
以上都是文本操作,能看的懂一部分,floadfunction,
- 编译阶段 --linux系统中的gcc编译器
gcc test.i -S
生成:test.s文件,
把C语言代码转化成汇编代码
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
《编译原理》,如何实现一个编译器,编译器的工作原理。
- 汇编
gcc test.s -c
把test.o (相当于win的obj文件,test.obj)
把汇编代码转换成了机器指令(二进制指令)
- 生成符号表
test.o 文件是一个elf格式的文件
可使用readelf - 工具,打开。
- 链接
把多个目标文件和链接库进行连接
- 合并段表
- 符号表的合并和重定位。
合并段表,即使elf格式文件,相同的进行合并,
符号表的合并和重定位,是将**全局的Add和mian合并,相同的合并
连接将test.o文件和add.o文件生成a.out文件 ---- (elf格式的文件)。
-
编译过程中的符号合并和符号表
-
段表的合并
- 一个编译的过程,
2.3运行环境
程序执行的过程:
- 程序必须存到内存中。嵌入式的话,叫烧板子。在有操作系统的环境中:一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始,接着便调用mian函数。
- 开始执行程序代码,程序将使用一个运行时栈堆(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数,也可能时意外终止。
下面的是换一种。
预处理
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s\n", __FILE__);
printf("line:%d\n", __LINE__);
printf("date:%s\n", __DATE__);
printf("time:%s\n", __TIME__);
上面的可以写一个日志文件夹:
int main()
{
FILE* pf = fopen("log.txt", "a+");//以追加的形式写
if (pf == NULL)
{
perror("log文件\n");
return 1;
}
for (int i = 0; i < 5; i++)
{
fprintf(pf, "%s %d %s %s %d\n", __FILE__, __LINE__, __DATE__, __TIME__,i);
}
fclose(pf);
pf = NULL;
return 0;
}
#define定义符号
#define 定义标识符
#define MAX 1000
#define reg register //为register这个关键字,创建一个简短的名字。
#define do_forever for(;;) //为了更形象的符号来替换一种实现
#define CSAE break;case //在写case语句的时候自动把break写上。//这一种的适用情况很少。
//如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续航符号)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \\
date: %s\t time:%s\n ,\\
__FILE__,__LINE__, \\
__DATE__,__TIME__")
- #define 定义的标识符,在最后加上;是可行的吗?
#define MAX 1000
#define MAX 1000;
//最好不要
//有可能报错
#define MAX 1000;
int main()
{
if ((100)> 101)
MAX
else
printf("101");
return 0;
}
//具体地例子忘了。
//具体替换把;也替换下来了。
宏不能出现递归
宏参数和#define定义中可以出现其他#define定义的变量。
字符串常量中的相同名字的宏,不会被替换,-----预处理
#和##
如何把参数插入到字符串中?
- “” “”“”
int main()
{
printf("hello world\n");
printf("hello" "world\n");//是要有空格的。
return 0;
}
//打印的完全一样。
#define PRINT(X) printf("the value of "#X" is %d\n",X)
//#define PRIN(X,TMP) printf("the value is"#X"is"#TMP"\n",X,TMP)
#define PRIN(X,TMP) printf("the value of "#X" is "TMP"\n",X)//正解
//还不能使用X,会报错,不知道是不是参数的问题。
int main()
{
int a = 10;
//the value of a is 10
PRINT(a);
int b = 20;
//the value of b is 20
int c = 30;
//the value of c is 30
float f = 5.5f;
PRIN(f,"%f");
return 0;
}
//the value of a is 10
//the value of f is 5.500000
- 两个##,,,就是拼接的
#define CAT(X,Y) X##Y
int main()
{
int class101 = 100;
printf("%d\n", CAT(class, 101));//100
//相当于
printf("%d\n",class101);//100
return 0;
}
5.调用宏和函数的对比
优势:1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
好比调用函数,需要5ms,函数返回需要3ms,执行需要的时间根据函数代码量的多少计算。
而宏在调用的时候,只有替换的过程。
若代码量过多,函数更合适。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。而宏可以适用于整型、长整型、浮点型等用于**>**来比较的类型。宏是类型无关的。
为什么?用**>**来比较。
//比如,函数无法传递,不同类型的值。
int Add(int x,int y)
{
return x+y;
}
int main()
{
int a = 10;
int b = 20;
double c = 5.5f;
Add(a,b);//ok
Add(c,b);
return 0;
}
//Add(),只接受int类型的值.
宏相较于函数的劣势之处:
- 每次适用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
就好比,使用宏打印log文件信息。
printf(“%s %d %s %s\n”,__FILE__,__LINE__,__DATE__,__TIME__);//额,不算过长,
- 宏是没法调试的。
宏是编译器在预处理阶段,做的事。
- 宏由于类型无关,也就不够严谨。
这一点,也是宏的优点吧!!!
- 宏可能会带来运算符优先级的问题,导致程序出现错误。
宏在预处理阶段的应用,就是把程序中的宏替换,不会计算
比如a++直接替换,不考虑后置++,问题;或运算符优先级问题。
宏可以做到函数做不到的事情,比如:宏的参数可以出现类型,但函数做不到。
-
宏的副作用
-
-
++a的副作用
-
int a = 1; int b = a + 1;//b = 2,a = 1 int b = ++a;//b = 2,a = 2 //++a是有副作用的。
-
-
宏和函数的比较表格——使用标准是,代码量少的使用宏,且加括号
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码。 |
执行速度 | ||
操作符优先级 | ||
带有副作用的参数 | ||
参数类型 | ||
调试 | ||
递归 | ||
- 使用宏开辟动态内存
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
//malloc(10*sizeof(int));
MALLOC(10,int);
}
- 宏的命名约定
一般来讲函数的宏的使用语法很相似。
宏名全部大写
函数名不要全部大写,驼峰式,_式。
预处理指令
-
#include 包含头文件。
-
#define 定义一个宏
-
#undef 删除已定义的宏
#include <stdio.h>//
#define M 100//
int mian()
{
printf("%d\n",M);//可以打印出来
#undef M
printf("%d\n",M);//打印不出来
return 0;
}
条件编译
#define PRINTF
int main()
{
#ifdef PRINTF //只有该条件存在才会执行下面的代码。
printf("hehe\n");
#endif
return 0;
}
- #if相关的条件编译
#int main()
{
#if 1==1
printf("hehe\n");
#elif
1 == 2
printf("haha\n");
#else
printf("heihei\n");
#endif
return 0;
}
//只能是常量表达式。
在Linux系统中,使用gcc编译器,
执行
gcc test.c -D M = 100
命令定义,或许相当于宏的创建。
文件包含
#include
可以在另一文件中被编译,
就好比,Add.c Add.h test.c这三个文件,Add.c函数定义||Add.h函数的声明|| test.c主函数中的调用
- 文件的包含
#include <stdio.h>
#include "Add.h"
//第一种是直接在库目录下,搜索库文件
//第二种是先在文件所在的目录下,搜索文件,再在库文件下搜索。
//两者不同是:查找的策略的不同。
Lniux环境下的标准头文件的路径:
/usr/include
vs环境的标准头文件的路径:
自己查,转到定义,–> 打开文件所在的目录。
-
若是多次声明头文件
-
//类似 #include <stdio.h> #include <stdio.h> //多次声明
阻止办法
在add.h这样的文件中,函数的声明。
#pragma once
int Add(int x,int y);
#ifndef __ADD_H__
#define __ADD_H__
int Add(int x,int y);
#endif
#include <stdio.h>
#include "Add.h"
//第一种是直接在库目录下,搜索库文件
//第二种是先在文件所在的目录下,搜索文件,再在库文件下搜索。
//两者不同是:查找的策略的不同。
Lniux环境下的标准头文件的路径:
/usr/include
vs环境的标准头文件的路径:
自己查,转到定义,–> 打开文件所在的目录。
-
若是多次声明头文件
-
//类似 #include <stdio.h> #include <stdio.h> //多次声明
阻止办法
在add.h这样的文件中,函数的声明。
#pragma once
int Add(int x,int y);
#ifndef __ADD_H__
#define __ADD_H__
int Add(int x,int y);
#endif