程序的翻译环境和执行环境
在ANSI C(标准C)的任何一种实现中,存在两个不同的环境。
计算机是能够执行二进制指令的,但我们写出的C语言代码是文本信息,计算机不能直接理解,翻译环境的存在,就是将C语言的代码转换为二进制的指令,二进制指令放在可执行程序中;执行环境则用来执行二进制的代码。
翻译环境
翻译环境,在这个环境中源代码被转换为可执行的机器指令。
每个源文件都会单独经过编译器的处理,生成自己对应的目标文件,这个过程就叫编译。
多个目标文件加上链接库经过链接器的处理,最后生成可执行程序,这个过程就叫链接。
链接库:写代码时我们会使用库函数,库函数可以直接使用是因为它们都编译放在了一些静态库中,例如我们在vs编译器上写代码,在代码中使用了scanf函数,那么vs编译器(集成开发环境)会自动把静态库链接到程序中去。
补充:如vs2022集成开发环境,集成了编译器(cl.exe)、链接器(link.exe)、调试器
编译和链接
在翻译环境中,整个源代码转换为可执行机器指令的过程可分为两个:
一、编译 编译又可分为:
(1)预编译(预处理)
该阶段会处理:注释的删除、#include等头文件的包含(展开)、#define等符号的替换,这些都是文本操作。
#include、#define这些被称为预处理指令,所有预处理指令都是在预处理阶段处理的
(2)编译
该阶段会检查语法错误,没有报错会把C语言代码翻译为汇编指令,生成汇编代码(是指令级代码),还会进行语法分析、词法分析、语义分析、符号汇总等操作。
《编译原理》:编译器的原理
(3)汇编
该阶段会把汇编代码翻译为二进制的指令,交给CPU去执行,生成目标文件,目标文件中放的都是二进制的指令。
该阶段会形成符号表
二、链接
该阶段会合并段表、合并和重定位符号表
该阶段会查看符号表
补充
Linux环境下,gcc编译产生的目标文件test.o、可执行程序test都是按照 ELF 这种文件的格式来存储的
readelf工具能够识别elf格式的文件
如test.o这样的文件,按照 ELF 这种文件的格式来存储时,会把文件分为一个个不同的段,每个段存放数据,如图所示
执行环境
执行环境,它用于实际执行代码。
运行环境
预处理
预定义符号
__FILE__ //进行编译的源文件名字
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
注意!!!:是左右各两个下划线_ _FILE_ _
int main()
{
printf("%s\n", __FILE__);//__FILE__是文件名,即字符串
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __STDC__);//如果显示未定义,则说明当前编译器不支持ANSI C
//预处理阶段就会被替换
return 0;
}
#define
#define定义标识符
#define name stuff
#define M 100
注意:
尽量不要写作 #define MAX 100 ; 这种后面加有 ; 的
#define M 100
int main()
{
int b = 0;
int c = 0;
if (b > 5)
b = M; // b=100;; 变成了两条语句 而if语句只能默认跟一条语句
else //这里就会出问题,else不知道和谁去匹配
b = -1;
return 0;
}
#define定义宏
#define name( parament-list ) stuff
![](https://i-blog.csdnimg.cn/direct/61fb1d4f54174fd1ae557b36c7e5ffec.png)
#define MAX(x,y) ((x)>(y)?(x):(y))
//x,y带括号是因为它们不是变量,而是参数,可能是一个表达式
int main()
{
printf("%d", MAX(5, 56));
//预处理阶段替换为((5)>(56)?(5):(56))
return 0;
}
注意:
#define MAX(x,y) ((x)>(y)?(x):(y))
// MAX后面没有空格才会认为MAX后面是它的参数
//例如:#define MAX (x,y) ((x)>(y)?(x):(y)), 会认为MAX替换为了(x,y) ((x)>(y)?(x):(y))
int main()
{
printf("%d", MAX(5, 56));//调用的时候可以加空格,比如写作 printf("%d", MAX (5, 56));
return 0;
}
2.使用#define定义宏不要去吝啬使用括号
#define SQUARE(x) x*x //比如想定义为平方,但很容易出问题
//所以建议最好写成((x)*(x))
int main()
{
int a = 3;
printf("%d\n", SQUARE(a));//9
printf("%d\n", SQUARE(a+2));//为什么是11而不是25?
//因为预处理时变为了a+2*a+2,所以变成了11
return 0;
}
#define替换规则
#define SQUARE(x) ((x)*(x))
#define M 3
int main()
{
int a = 3;
int r=SQUARE(M+2);
//这里有M,M首先被替换为3,
//2.SQUARE(3+2)
//3.预处理完后变为((3+2)*(3+2))
return 0;
}
注意:
#define M 10
int main()
{
printf("M=%d\n",M);//前面的M是字符串常量的内容,不被搜索。
//打印出来就是M=10
return 0;
}
#和##
可以理解为预定义操作符,只能在宏中使用
#
C语言支持这种写法,可以得出结论:字符串是有自动连接的特点的。
int main()
{
printf("hello world\n");
printf("hello ""world\n");
//这两段代码能打印出同样的hello world
return 0;
}
如何把参数插入到字符串中?
#define PRINT(x,format) printf("the value of "#x" is "format, x)
// 使用 # ,可以把一个宏参数变成对应的字符串,比如 #x 会被预处理为 "x"
// 只有当字符串作为宏参数的时候才可以把字符串放在字符串中,就如这里的format
int main()
{
int a = 10;
printf("the value of a is %d\n", a);
//能不能使用函数把它们封装起来统一打印?
//答案是不能的。
//但可以使用宏
PRINT(a, "%d\n");
int b = 20;
printf("the value of b is %d\n", b);
PRINT(b, "%d\n");
float c = 3.14f;
printf("the value of c is %f\n", c);
PRINT(c, "%f\n");
return 0;
}
##
#define CAT(x,y) x##y //注意这里就不能乱加括号了!!!
int main()
{
int Time_8_11 = 100;
printf("%d\n", Time_8_11);
printf("%d\n", CAT(Time, _8_11));//这样也可以实现打印Time_8_11的值
return 0;
}
带副作用的宏参数
何为副作用?
int main()
{
int a = 10;
//int b = a + 1;//b变成了11,a依旧是10
int b = ++a;//不但b变成了11,而且a也变成了11
//这个表达式就是带有副作用的表达式
return 0;
}
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 5;
int b = 6;
int c = MAX(a++, b++);
//int c =((a++)>(b++)?(a++):(b++))
//先会进行 a>b的判断
// 然后a++变成6,b++变成7
// 此时执行后面的 b++ ,会把7赋值给c,然后b再变为8
printf("%d\n", a);//6
printf("%d\n", b);//8
printf("%d\n", c);//7
return 0;
}
对比宏和函数
宏相比函数的优势
#define MAX(x, y) ((x)>(y)?(x):(y))
//宏
#define MAX(x, y) ((x)>(y)?(x):(y))
//函数
int Max(int x, int y)//只能比较两个 整型
{
return (x > y ? x : y);
}
int main()
{
int a = 5;
int b = 6;
//int c = MAX(a, b); //宏的参数会直接替换进去
int c = Max(a, b);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
return 0;
}
宏相比函数的劣势
补充
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
// int* p = (int*)malloc(126 * sizeof(int));
//malloc函数使用起来较为繁琐麻烦
//我们期望能够传类型 如 malloc(126, int);
//但实际上函数是不能接收类型参数的,函数的参数不能传类型
//这时我们可以使用宏
int*p = MALLOC(126, int);
//宏的参数会直接替换进去:int* p = (int*)malloc(126 * sizeof(int));
return 0;
}
总结
属性 |
#define
定义宏
| 函数 |
代码长度 |
每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长
|
函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
|
执行速度 | 更快 |
存在函数的调用和返回的额外开销,所以相对慢一些
|
操作符优先级 |
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。
|
函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
|
带有副作用的参数 |
参数可能被替换到宏体中的多个位置,所以带有副作用的参
数求值可能会产生不可预料的结果。
|
函数参数只在传参的时候求值一次,
结果更容易控制。
|
参数类型 |
宏的参数与类型无关,只要对参数的操作是合法的,它就可
以使用于任何参数类型。
|
函数的参数是与类型有关的,如果参
数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
|
调试 |
宏是不方便调试的
|
函数是可以逐语句调试的
|
递归 |
宏是不能递归的
|
函数是可以递归的
|
//宏
#define MAX(x, y) ((x)>(y)?(x):(y))
//函数
int Max(int x, int y)
{ //3 3
return (x > y ? x : y);
}
int main()
{
int c = MAX(1+2, 3);
//int c = ((1+2)>(3)?(1+2):(3)) 宏的参数会直接替换进去
c = Max(1+2, 3);//函数会先行计算,直接传 3和3
return 0;
}
内联函数——inline
既然宏和函数各有优缺,那么存不存在集合两者优点的东西?
是存在的,在最新的语法中,有一种称为内联函数的东西,集合了二者的优点。
命名约定
#include<stdio.h>
//宏
#define MAX(x,y) ((x)>(y)?(x):(y))
//函数
int Max(int x, int y)
{
return x > y ? x : y;
}
//有一个例外——offsetof是宏,但它是全小写
int main()
{
return 0;
}
#undef
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
printf("%d\n", MAX(3, 6));//会打印6
#undef MAX
printf("%d\n", MAX(3, 6)); //这里就会报错
//可以选择重新定义
#define MAX(x,y) 36
printf("%d\n", MAX(3, 6)); //这就又可以正常使用了
return 0;
}
命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。(VS环境无)
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是如果另外一个机器内存大些,我们又需要一个数组能够大写。)
![](https://i-blog.csdnimg.cn/direct/4add1b4a98934c92ab51b34390aec6c9.png)
代码中虽然没有给SZ一个值,但可以在编译时指定SZ的值是多少,使得程序正常执行
条件编译
满足某种条件,给代码进行编译;但不满足条件,就不进行编译。做到什么条件下编译什么样的代码(与 if else 语句类似)。条件编译也是在预处理阶段进行。
常见的条件编译指令
1.单个分支的条件编译
int main()
{
#if 9>6 //#if后面的表达式为真,所以对下面的代码进行编译
printf("hehe\n");//代码参与编译
printf("hehe\n");
#endif
#if 0 //#if后面的表达式为假,所以对下面的代码不进行编译
printf("hehe\n"); //预处理阶段直接删除
#endif
return 0;
}
2.多个分支的条件编译
#define M 1
int main()
{
#if M==0
printf("M=0\n");
#elif M==2
printf("M=2\n");
#else
printf("M=1\n");
#endif
return 0;
}
![](https://i-blog.csdnimg.cn/direct/b3cab541626f45fe8936aec6c1eb9527.png)
![](https://i-blog.csdnimg.cn/direct/5e2332e6fad34318ba44bc7d18f89855.png)
3.判断是否被定义
#define M 100
int main()
{
#if defined(M)
printf("M被定义了\n");
#endif
#ifdef M
printf("M被定义了\n");
#endif
#ifndef M
printf("M没有被定义\n");
#endif
#if !defined(M)
printf("M没有被定义\n");
#endif
return 0;
}
4.嵌套指令
#define B 50
#define N 100
int main()
{
#if defined(A)
#ifdef M
printf("AM被定义了\n");
#endif
#ifdef N
printf("AN被定义了\n");
#endif
#elif defined(B)
#ifdef N
printf("BN被定义了\n");
#endif
#endif
return 0;
}
补充:花式注释
#if 0
int main()
{
;
return 0;
}
#endif
//上述代码就相当于被注释掉了
int main()
{
printf("hehe\n");
return 0;
}
适用场景
文件包含
头文件被包含的方式
C语言中头文件的包含有两种形式:
1.本地文件包含
#inlcude " " 使用自己创建的头文件时
#include "filename"
/usr/include
C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include
2.库文件包含
#include < > 使用库里面的头文件时
#include <filename.h>
嵌套文件包含
#include指令可以使另外一个文件被编译,就好像它真的出现在了#include指令的地方一样,展开头文件的本质是拷贝。
这种替换的方式很简单:预处理器先删除这条指令,再用包含文件的内容替换,相当于把文件内容直接拷贝过来。假如一个头文件被包含10次,那实际上它就被编译了10次。
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
//test.h中声明了一个函数
int Add(int x,int y);
可以看到预处理阶段,该函数声明出现了四次
解决方法:条件编译
1.比如刚刚的test.h头文件,可以这样写:
#ifndef __TEST_H__ //随意定义一个符号
#define __TEST_H__
int Add(int x,int y);
#endif
这样预处理时就会只有一个函数的声明了。
2.或者这样写:
#pragma once
int Add(int x,int y);
与上面的写法效果相同。这样就可以避免头文件的重复引入了。
在VS的编译器上,创建头文件会自带 #pragma once