程序的翻译环境和执行环境
在ANSIC的任何一种实现中,存在两个不同的环境
1.一个是翻译环境,在这和环境中,会将源代码翻译成可执行的机器指令(二进制指令)
(在VS充当翻译环境时,这个可执行指令就是.exe文件
2.执行环境,用于实际执行代码
编译+链接
翻译环境
在VS2019的翻译环境下
源文件 test.c经过翻译环境,得到可执行的文件test.exe。
编译的三个过程:
编译的三个阶段
1.预编译
也叫预处理,生成test.i的文件
主要内容:
1.#include头文件包含
2.#define定义符号的替换
3.注释的删除
等等各种预处理指令的执行
这个阶段的所有操作都是文本操作
2.编译
生成test.s 的文件
主要内容:
1.把C语言代码翻译成汇编代码(.s文件里就是翻译后的汇编代码)
2.语法分析、词法分析、语义分析、符号汇总
3.汇编
生成test.o文件
主要内容:
1.把汇编语言翻译成计算机能读懂的二进制语言,(存放在test.o文件内)
2.形成符号表(全局变量、函数名.....)
链接
主要作用
1.合并段表
2.符号表的合并和重定位:
假设我们在源文件test.c中正常写下了主代码,使用了Add函数,但是没有在test.c中定义函数,而只是声明了函数(extern)
源文件test.c
函数的定义在另一个.c文件,Add.c里
Add.c
可以用于检查错误,比如说引用了函数,但是没有声明过,就会在这个步骤检测出来。\
运行环境
对于已经生成的可执行文件.exe,如何运行
1.把程序载入到内存中:该操作由操作系统完成
2.程序开始执行,调用main函数
3.开始执行程序代码。这个时候程序将使用运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4.终止程序。main结束;也有可能意外终止。
预处理详解
预处理符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSIC,其值为1,否则未定义
因为VS编译器不是ANSIC,所以__STDC__会未定义,也就是报错无法运行。
预处理指令
#define
#define name stuff
1.定义一个标识符
因为本质上是替换,理论上什么都能定义标识,定义的符号内容可以五花八门
#define MAX 100
#define do_forever for(;;)
#define CAES break;case
int main()
{
int a=MAX;//把MAX替换成100,给a赋值100
do_forever;
//相当于:
// for(;;)
// {;}
int i;
switch(i)
{
case 1:
CASE 2:
CASE 3:
}
//试着把CASE替换进去看看
}
注意:#define后面空一格,放标识,再空一格,放标识的内容。不能有多的空格,否则内容会出错。最后也不要有";",否则";"也会作为标识的内容一起替换过去的,会出bug
2.定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro),或定义宏(define macro)
#define name ( parament _list ) stuff
注意,参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
#define ADD(x,y) x+y
int main()
{
int a=ADD(2,3);
return 0;
}
这里就会把2+3替换到ADD(2,3)那。
!!定义宏最最最重要的一点:就是不要吝啬括号()
因为本质是替换,ADD(2,3)并不会像数学中那样被视为一个整体,只是替换:
#define ADD(x,y) x+y
int main()
{
int a=5*ADD(2,3);
//实际上是 5*2+3,结果是13
return 0;
}
包括:
#define SQUARE(x) x*x
int main()
{
int a=SQUARE(1+2);
//结果是1+2*1+2
return 0;
}
所以一定不要吝啬括号,好好把数括起来
#define ADD(x,y) ((x)+(y))
#define SQUARE(x) ((x)*(x))
对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
#define的替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
如:int x=ADD(SQUARE(4),5) ===>int x = ADD(((4)*(4)),5)
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
ADD(((4)*(4)),5)==>( ((4)*(4))+(5))
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
也就是说宏的参数不能是自己
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
即字符串"asdasdADDdasd"中的ADD是不会被检测为标识的。
#和##
--->定义宏是,如何把参数插到字符串中
只能在宏中使用,这里的"#"不是#define,或者#include的符号。
讲功能前,我们要先知道一个小知识:
int main()
{
printf("hello world\n");
printf("hello " "world\n");
return 0
}
这两行代码的输出结果是一样的
得出结论:虽然hello_,和world是两个单独的字符串,但是会自动合并
#define PRINT(x) printf("the value of x is %d\n",x)
这个宏,x的值只能传到后一个x上,没法传到字符串里的x。
#
#:把一个宏参数变成对应字符串
#define PRINT(x) printf("the value of "#x" is %d\n",x)
把字符串中的x从字符串中脱离出来,加上#,#x也就等价于"x",再加上字符串的自动合并,就实现了把参数插入到字符串中。
当然,我们传过去的数可能类型不一定是整型,所以也要把打印类型也传过去
#define PRINT(FORMAT,x) printf("the value of "#x" is "FORMAT"\n",x)
int main()
{
PRINT("%d",2);
return 0;
}
这里注意,"%d"传过去就是个字符串,所以FORMAT就不用加#,因为替换完就是"%d",就是个字符串。
##
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
#define CAT(x,y) x##y
int main()
{
int Class001=2024;
printf("%d\n",CAT(Class,001));
return 0;
}
这里就是把Class和001两个符号合并成一个,拼成了Class001,就成了有效符号
或是
#define ADD_TO_SUM(num, value) sum##num += value
int main()
{
int sum1=0;
int sum2=0;
ADD_TO_SUM(1,2);
//作用是给sum1加2.
return 0;
}
带有副作用的宏参数
副作用:
int a=10;
int b=a+1;//没有副作用,b是a加上1的数
int a=10;
int b=++a;//得到b的时候,a也会变,这就是副作用
因为宏不是函数,函数是传值过去,但是宏只是替换,会出问题:
#define MAX(x,y) ((a)>(b)?(a):(b))//求最大值
int main()
{
int a=2;
int b=3;
int m=MAX(a,b);
int n=MAX(a++,b++);
return 0;
}
会严重影响结果,且难以判断变化走向
结论:带有副作用的参数的宏是危险的!!!!!,不要这么定义宏
宏和函数的对比
宏通常用于解决一些小问题
--->为什么不用函数来解决这些问题?
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。
宏是类型无关的
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
因为每次替换时,就相当于把定义的那么大一坨式子给替换过去,会增加程序长度。
2. 宏是没法调试的。因为是在预处理阶段进行替换的
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。 所以要多写括号
——>>宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int *p=MALLOC(10,int);//类型作为参数
int *p=(int*)malloc(10*sizeof(int));//替换后
return 0;
}
命名约定
习惯性:
宏的名字全部大写
函数的名字不要全大写(首字母大写)。看企业习惯
#undef
这条指令用于移除一个宏定义
#define MAX 100
.
int main(){
printf("%d\n",MAX);//可以正常执行
#undef MAX
printf("%d\n,MAX);//无效
return 0;
}
条件编译
当我们再调试程序时,会加入一些测试用的代码,调试好后又不舍得删,但留着又会干扰正常的运行。这个时候就可以用条件编译。相当于是一个开关,决定这条代码是否要运行。
#ifdef 标识
#endif
#define PRINT 1
int main()
{
#ifdef PRINT//只有定义了PRINT这个标识,才会运行这条语句
// 只要有定义这个行为就行,对定义内容无关
printf("hello");
#endif//限制控制范围
return 0;
}
#if 常量表达式 --->为真,运行,为假,不运行
#endif
#if define(MAX)
#endif
等价于==>
#ifdef MAX
#endif
注意一定要是常量表达式,不能是变量,因为这个是预处理阶段,这个时候还没有变量.
多分枝:
#if ...
//
#elif ...
//
#else
//
#endif
如果没有定义MAX才实行
#ifndef
#if !define(MAX)
#endif
===>
#ifndef MAX
#endif
文件包含
#include
1.#include" "(test.h)
本地文件的包含(自己创建的头文件.h)
查找策略:再当前源文件的目录下找,如果没则去存放库函数的标准位置寻找
2.#include < >
c语言自带的头文件内容,库函数
查找策略:直接区标准位置找库,找不到直接报错
理论上引用库函数也可以用" "但是因为没直接去存放库函数的地方寻找,会浪费很多时间
如何防止头文件被多次重复引入
在头文件.h中,加入:
//开头加:
#pragma once
//或者写
#ifdef _TEST_H_
#define _TEST_H_
//头文件内容
#endif