一:程序的翻译环境和执行环境
- 第一种是 翻译环境, 在这个环境中源代码被转换为可执行的机器指令
-
第2种是 执行环境,它用于实际执行代码
1. 翻译环境详解
翻译环境可以分为编译和链接两部分
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中
- 在windows环境下目标文件为.obj形式,在Linux环境下目标文件为.o形式
编译又可以分为预编译,编译,汇编三部分
下面将从这三个方面来解释翻译环境是如何把代码翻译成可执行的机器指令的
1.1 编译
(1) 预编译
在预编译阶段,会进行以下操作:
1.注释的替换和删除
2.头文件的包含
3.#define 符号的替换
上代码看实例:
本次示例用的是VS2019的环境
#include<stdio.h>
#define M 10
int main()
{
int a = 3;
int b = M;
printf("%d", a + b);
return 0;
}
为了方便看预编译后产生了什么影响,在项目--属性--c/c++中把预处理到文件打开,这时我们运行代码,会产生一个名为test.i的文件,我们打开这个文件:
我们发现,这个文件竟然有一万多行代码,翻到文件最后,发现我们写的代码也在这里,但是和我们写的代码不同的是,注释被删掉了,并且我们预定义的M被替换成了10,而在test.i文件中这一万行代码,就是stdio库中所有信息的声明
(2) 编译
将c语言代码翻译成汇编代码,在此过程也会进行复杂的处理,会进行
词法分析 语法分析 语义分析 符号汇总 等过程
(3) 汇编
把汇编代码翻译成二进制指令,生成.o文件(目标文件)
在此阶段会进行生成符号表的操作
1.2 链接
链接目标文件和链接库生成可执行程序
1.合并段表
2.符号表的合并和重定位
我们在一个文件中定义了一个函数,想在另一个文件中使用,我们需要在这个文件中声明一下这个函数,但是为什么声明了就可以用了呢?我们来探讨一下这个问题:
在编译阶段会进行符号汇总(一般汇总的符号都是全局的,因为只有全局的在不同文件中才有价值),在add.c的文件中就汇总出了 add 的符号, add函数在定义会有一个地址我们假设为0x1010,在汇编阶段会进行生成符号表,两者会被一起管理起来在add.o文件中形成符号表
同理在test.c的文件中汇总出了 add main 等符号,由于add在test.c文件中只是声明了一下,并不知道add函数的具体地址是多少,我们先付给他一个无意义地址0x0000,假设main的地址为0x1111,在test.o文件中生成符号表
而像 add.o,test.o文件都是由格式的
在gcc编译器生成的目标和二进制可执行文件都是按照elf这种文件的形式组织的,在链接阶段会把多个源文件链接到一起,其实就是把两个文件中相同段的信息进行合并,这个过程就叫做合并段表
同时符号表也会合并和重定位
由于在符号表中有两个add,同时也有两个地址,因为在test.o文件中add的地址是虚假的,符号表合并我们会保存add.o文件中add函数的地址,这样当我们想调用add函数时,就可以通过这个地址来找到它
了解了这些以后,如果说我们在写add函数时把名字写成了Add,例如以下代码:
通过编译链接,最终形成的符号表就为
真正的add函数地址为0x1010,由于两文件中add函数的名字不同,最终add真正的地址并没有把虚假的Add地址给覆盖掉,所以通过Add的虚假地址就找不到add函数
2. 运行环境
3.总结
二:预处理详解
1.预定义符号
__FILE__ //进行编译的源文件__LINE__ //文件当前的行号__DATE__ //文件被编译的日期__TIME__ //文件被编译的时间__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
2. #define
2.1 #define定义标识符
#include<stdio.h>
#define CASE break; case
int main()
{
int n = 0;
scanf("%d", &n);
switch (n)
{
case 1:
CASE 2:
//相当于break;case 2:
CASE 3:
//相当于break;case 3:
}
return 0;
}
这里还有一个问题:
#define后边要不要加分号呢?
答案是:建议不要加,因为有些情况加的分号会导致错误发生
例如:
要注意#define会把MAX替换为1000;再加上原本语句还有一个分号,这里的两个分号就相当于两条语句,因为if后面没有加大括号,那后边的else就检测不到if了,所以就报错了
2.2 #define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的定义方式:
注意:参数列表的左括号必须与 name 紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分例如:
#include<stdio.h>
#define mul(x,y) x*y
int main()
{
int a = 10;
int b = 0;
printf("%d", mul(a, b));
//相当于printf("%d", a*b);
return 0;
}
int a = 5;
printf("%d\n" ,mul(a+1,a+1) );
这里我们是想求(a+1)+(a+1)的值,但是预处理器会将其替换为: a+1*a+1 运算顺序会改变,结果会发生错误,想要解决这个问题就要尽可能的多加有效的括号: mul(a,b) (a)*(b)
这样替换完后,就变成(a+1)*(a+1)
再例如:
#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
这里我们是想求10*(a+a)的值,预期答案应该为100,但是替换完后:10*(a)+(a),答案为55,要解决这个问题只要在外边加一个大括号就可以了: #define DOUBLE(x) ( (x) + (x) )
所以用于对数值表达式进行求值的宏定义都应该尽可能多加有效的括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
2.3 #define替换规则
1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
2.4 #和##
引例:
我们发现字符串是有自动连接的特点的, 打印字符串不论在一个引号中,或者在多个引号中,只要内容一样是不会影响输出结果的
那我们是不是可以这样写:
#define PRINT(FORMAT, VALUE)\
printf("the value is " FORMAT "\n", VALUE);
PRINT("%d", 10);
这样替换完后:
printf("the value is " %d" "\n", 10); 就可以正常输出了
还有一个技巧是可以使用#把一个宏参数变成其对应字符串
例如:
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
#include<stdio.h>
int main()
{
int i = 10;
PRINT("%d", i + 3);
return 0;
}
#define ADD_TO_SUM(num, value) \sum##num += value;...ADD_TO_SUM ( 5 , 10 ); // 作用是:给 sum5 增加 10
2.5 带副作用的宏参数
x+1; //不带副作用x++; //带有副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++); //本来是想求x++和y++谁最大
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
预处理后MAX宏将会被替换为:
(x++)>(y++)为假,x和y都自增一次,x变为6,y变为9,在执行y++,把y++的执行结果赋值给z,y又会自增一次,即x=6,y=10,z=9
int MAX(int a,int b)
{
int z= ( (a) > (b) ? (a) : (b) );
return z;
}
2.6 宏和函数的对比
属
性
|
#define
定义宏
|
函数
|
代
码
长
度
|
每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长
|
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
|
执
行
速
度
|
更快
|
存在函数的调用和返回的额外开
销,所以相对慢一些
|
操
作
符
优
先
级
|
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生
不可预料的后果,所以建议宏在书写的时候多些括 号。
|
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。
|
带
有
副
作
用
的
参
数
|
参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果
|
函数参数只在传参的时候求值一
次,结果更容易控制
|
参
数
类
型
|
宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型
|
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
相同的。
|
调
试
|
宏是不方便调试的
|
函数是可以逐语句调试的
|
递
归
|
宏是不能递归的
|
总结: 宏通常被应用于执行简单的运算
2.7 命名规则
- 把宏名全部大写
- 函数名不要全部大写
2.8 #undef
#undef NAME//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
3.条件编译
#if 常量表达式//代码#endif
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif
当 tset_2.h 调用 test_1.h 时就把 __TEST_H__ 定义上了,第二次调用stdio.h时if语句为假就不会编译该头文件的内容
现在大多数编译器在创建头文件时就会自动在第一行补充:
#pragma once
这个语句也是防止头文件重复引用的