
执行过程本小节主要讲解一个C程序从源代码到最终执行的过程这个过程又可以细分为两部分1、源代码到可执行文件的过程2、可执行文件在内存中执行是C程序员非常重要的基本功本小节是C语言基础中的重点。我们首先研究一下C语言程序从源码到可执行文件的过程。这个过程总的来说可以分为两大步骤1、编译2、链接编译这一过程就是广义上的编译预处理#预处理就是将头文件或者define的指令翻译到对应调用的位置include具有包含的意思。预处理指令”#include用于包含头文件。头文件即.h为后缀的文件头文件一般用于声明函数、结构体类型、变量等。关于头文件的详细使用我们后面再讲简单来说你可以认为该预处理指令的作用就是找到该头文件把其中的内容处理后复制到指令所在的位置。visual stdio这样设置就可以在文件夹里面看到预处理文件在项目属性-》C/C-》预处理器-》预处理到文件-》选择【是(/P)】预处理之后得到.i文件#include stdio.h #define PI 3.14 #define DIFF(x) x*x int main() { //还可以用define定义函数宏 printf(%d\n,DIFF(5)); //预处理做了替换DIFF5 --》 5*5 //定义宏函数坑很多可读性很差。最好是写函数的时候框框加括号。 printf(%d\n, DIFF(13)); //DIFF13 --》 13*13 这就是要加括号的重要性 return 0; }小编译然后小编译狭义上的编译将.i文件变为.s文件进行词法分析、语法分析、类型检查等操作。#编译器还在此阶段小编译对代码进行各种优化代码位置的挪动等价替换以提高效率减少最终生成代码的大小。最终将预处理后的源代码转换成汇编语言。也就是将.i文件转换成.s文件。汇编.s-.o#这时候代码会变成二进制的机器指令也就是二进制程序员已经无法看懂这块代码了汇编做完之后才算是把广义的编译做完了然后再由链接将.o文件变成exewindow可执行程序链接#在链接阶段链接器Linker会把项目中经过汇编过程生成的多个至少有1个文件和程序所需要的其它附加代码整合在一起生成最终的可执行程序。如你在代码中调用了标准库函数那么链接器会把标准库的机器代码完整复制打包进可执行文件里。解释为什么是机器指令而不是从头就预处理复制过来头文件.h只有函数声明没有函数实现代码。源文件.c才有函数实现。预处理只会把头文件的声明文本粘贴进来并不会把库里面的函数实现代码拉进来。编译 汇编阶段编译器看到 printf 只是一个外部符号只检查语法不会去找 printf 的实现。最后生成目标文件.obj/.o里面只留下一个标记此处引用了外部符号 printf。真正找函数实现是链接器的工作C 语言标准库已经提前编译好了- 库文件libc.a静态库、shturl.动态库里面全是编译好的机器指令。编译器不管库只有链接器才会去标准库里搜寻 printf 对应的机器代码1. 静态链接把 printf 的二进制代码复制到你的 exe 里2. 动态链接只记下 “运行时去 shturl. 里找 printf”不复制代码。面试题一个c代码是怎么跑出来的编译器会将你的mian.c代码变成一个.exe的可执行程序然后点击exe让它跑起来注意exe是window才有其它环境不一样所以一般不说exe这个过程就是.i-.s-.o编译链接可执行程序到运行起来我们主要研究的是虚拟内存对于内存是什么管理的。管理内存在真实的物理内存中我们假设要1个G的内存但是物理中这段内存是断断续续的不好连续使用用起来非常麻烦这时候就需要虚拟内存#直接用物理内存的坏处1、进程间隔离太过困难。在方格中往前一点就是别个的进程 2、碎片太多1个G可能要拆分很多内存利用率低下管理困难3、对于程序员管理内存非常麻烦为了解决这些问题系统给出了一个解决方案#虚拟内存我们假设我们需要1G的内存有了虚拟内存他就不是断断续续的而且一段连续的内存直接从0开始。每一个进程都在独立的虚拟内存中虚拟内存在现代操作系统中虚拟内存是一种重要的内存管理功能它使得进程能够使用一个独立于物理内存容量的、看似连续的地址空间。这个地址空间被称为”虚拟内存空间”。“虚拟”这一术语用于描述这种机制因为每个进程获得的内存空间并不是实际物理内存中连续的一段。实际上这些虚拟地址都是由操作系统与硬件主要是内存管理单元MMU共同管理以映射到物理内存地址。当一个进程启动时它所“看到”的是操作系统为其分配的这种连续的虚拟内存空间而这些虚拟地址并不直接等同于实际的物理内存地址。此时对于每一个进程来说它都认为它完全拥有了这台计算机它使用了整个计算机的资源这样的设计就使得内存管理的复杂性大大降低对程序员来说降低所以C/C程序员管理的内存只是进程的虚拟内存所谓内存地址也只是虚拟地址。虚拟内存的5大区域系统会自动将虚拟内存中连续的内存映射到物理内存中那那些不连续的块中系统给进程分配的虚拟空间就会像下面这样进程/运行中的程序拿到的内存都是虚拟内存。在拿到这样一块内存的时候程序使用内存的方式是分门别类的根据数据的特点来存放数据的。最下面的Code区域里面是放代码段的放的是机器指令。Data区域数据段程序运行时的全局数据存储在这里Heap区域堆区、堆空间。和手动分配内存相关。Stack区域栈区、栈空间。和函数调用相关。Kernel内核区。栈区和堆区的大小关系比如给程序的虚拟内存分配4G。栈区可能只能占1Mvscode,8Mlinux环境StackOverflow栈溢出报错堆区的内存可能能占到3.5G3.8G注意的是现在看到这个虚拟内存的分区只是逻辑是这样现实还是打乱的免得被被人随意抓取数据虚拟内存地址问题我们认为我们拿到的内存都是虚拟内存。内存地址标示的就是内存上某一单元的地址标识的最小单位是字节。也就是说一个地址只能标识一个字节大小空间指针和内存大小指针存放的是内存地址。地址有多大指针就占多少字节。不是代表内存所占的哪个区域不管是32位系统和64位系统1个内存地址只能对应 1 个字节8bit的存储空间。在x86系统中一个指针/地址的大小就是4字节在x64系统中一个指针/地址的大小就是8字节#include stdio.h int main() { //指针就是c中的地址 int num 10; int* p num; //就是把num的地址取出来放到一个变量P里面去 /* 那么这个int *占多大呢 和应用程序的位数相关。如果是32位应用程序32位0和14字节显示的时候是16进制显示32位也就是8个16进制数显示则指针占用4个字节 如果是64位应用程序64位0和18字节显示的时候是16进制显示64位也就是16个16进制数显示则指针占用8个字节 x64就是64位应用程序x86就是32位应用程序 指针大小也叫做地址大小受应用程序的位数相关 后续还是用x64 */ printf(%d\n, sizeof(int)); printf(%d\n, sizeof(int *)); }大小端存储#include stdio.h int main() { /* 内存地址永远只标记 1 个字节。这是硬件规定改不了。 编译器知道每个变量的类型所以解引用可以全部取出来 char ch → 编译器知道 **占 1 字节** int num → 编译器知道 **占 4 字节** */ char ch a; printf(ch%c\n, *(ch)); int num 10; printf(ch%d\n, *(num)); /* 数据的存储顺序 数据高位 数据低位正常我们日常写数字就是从左往右边写 0000 0000 0000 0000 0000 0000 0000 1010 [0,max]0就是低地址max就是高地址 把num存进去就是 [0000 00000000 00000000 00000000 1010] 而我打开内存一看是[0a,00,00,00]这个就是小端存储法 小端存储法数据的低位是存储到内存的低地址中 大端存储法数据的低位存储在内存的高位 不说的话有可能看不懂内存。 举例 int num123456; 数据高位 数据的低位 二进制0000 0000 0000 0001 1110 0010 0100 0000 十六进制 00 01 d2 40 内存40 d2 01 00 */ return 0; }