一个程序从源代码到可执行程序的过程】【转载链接:https://blog.csdn.net/qq_39755395/article/details/78293733
一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接。
其中,编译是主要部分,其中又分为六个部分:词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化。
链接中,分为静态链接和动态链接,本文主要是静态链接。
一、预编译:主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下
1.删除所有的#define,展开所有的宏定义。
2.处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3.处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4.删除所有的注释,“//”和“/**/”。
5.保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
6.添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
C语言的宏替换和文件包含的工作,不归入编译器的范围,而是交给独立的预处理器。
C语言中源代码文件的文件扩展名为.c,头文件的文件扩展名为.h,经预编译之后,生成xxx.i文件。
在C++,源代码文件的扩展名是.cpp或.cxx,头文件的文件扩展名为.hpp,经预编译之后,生成xxx.ii文件。
————————————————
1.词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
以上的一行C语言程序,一共有16个空字符,经扫描机扫描之后,产生了16个记号。lex可以实现词法分析。见下表:
见上图:
词法分析产生的记号分类有:关键字、标识符、字面量(数字、字符串)、特殊符号(加号、等号等)
在语法分析的同时,就把运算符的优先级确定了下来,如果出现表达式不合法,——各种括号不匹配、表达式中缺少操作,编译器就会报错。
3.语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
其中,静态语义通常包括:声明和类型的匹配,类型的转换,那么语义分析就会对这些方面进行检查,例如将一个int型赋值给int*型时,语义分析程序会发现这个类型不匹配,编译器就会报错。
经过语义分析阶段之后,所有的符号都被标识了类型(如果有些类型需要做隐式转化,语义分析程序会在语法树中插入相应的转换节点),见下图:
————————————————
4.优化:源代码级别的一个优化过程,例如该语句中的(3+8)的值可以在编译期确定,源代码优化器会将整个语法树转换成中间代码——语法树的顺序表示,十分接近目标代码。
中间代码有很多种类型,最常见的是“三地址码”和“P-代码”,其中三地址码的基本形式为:x = y op z,表示将变量y和z进行op操作后,赋值给x,op操作可以是加减乘除等。
经优化之后的语法树为:
————————————————
另一个关于中间代码的要点:中间代码使得编译器可以被分成前端和后端,编译器前端负责产生与机器无关的中间代码,编译器后端将中间代码转换为机器代码。
源代码优化去产生中间代码标志着下面的过程都属于编译器后端,后端主要包括:代码生成器和目标代码优化器。
5.目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
6.目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
上述的六个步骤完毕之后,编译过程也就告一段落了。最终产生了由汇编语言编写的目标代码。
gcc把预编译和编译两个步骤合并成一个步骤。对于C语言的代码,是用“cc1”这个程序来完成这两步,对于C++代码,对应的程序为“cc1plus”。gcc这个命令只是后台程序的包装,根据不同的参数去调用:预编译编译程序——cc1,汇编器——as,连接器——ld。
C语言的代码,经编译后产生的文件名为xxx.s。
————————————————
版权声明:本文为CSDN博主「帅气的羊羊杨」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_39755395/article/details/78293733
答案就是:模块之间,符号的引用!
这就像是一张画有大树的拼图,叶子、枝干、根系都零散的分布在那些拼图碎片上,想要看到完整的大树,我们就会耐心地把那些碎片拼合在一起。
1.链接:“组装”模块的过程。
2.链接的内容:把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。(就像拼图,凸起和凹槽的位置一定一一对应,否则…)
3.链接的过程:地址和空间的分配、符号决议(也叫“符号绑定”,倾向于动态链接)和重定位
以gcc编译器为例,看基本的链接过程:
c文件经过编译器、汇编器之后得到目标文件.o,目标文件再与库进行链接得到可执行文件.out。
库其实就是一组目标文件的打包,这些目标文件中都是一些常用的代码。
我们在fun.c模块中定义了函数foo(),在main.c模块中引用了foo()函数,在编译过程当中,编译器并不知道main.c中foo()的地址,所以将调用foo()的指令的目标地址部分搁置,
等到了链接的阶段,链接器会去找到foo()定义的那个模块,在main.o中填入正确的函数地址,这个修改地址的过程被叫做“重定位”,每个被修正的地方叫“重定位入口”。
————————————————
版权声明:本文为CSDN博主「帅气的羊羊杨」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_39755395/article/details/78293733
接下来就是对shell有一个认识,以免以后难以记住
装载链接:https://blog.csdn.net/weixin_37490221/article/details/80869792
Shell是一个用C语言编写的程序,通过Shell用户可以访问操作系统内核服务,类似于DOS下的command和后来的cmd.exe。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量、参数、函数、流程控制等等。它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,shell是最重要的实用程序,深入了解和熟练掌握shell的特性极其使用方法,是用好Unix/Linux系统的关键。
————————————————
我经常说道shell脚本,其实是因为Shell是一种脚本语言,也就是解释性语言。程序设计语言可以分为两类:编译型语言和解释型语言。
语言 区别
编译型语言 需要预先将我们写好的源代码转换成目标代码,这个过程被称作“编译”。运行程序时,直接读取目标代码。由于编译后的目标代码非常接近计算机底层,因此执行效率很高,这是编译型语言的优点
解释型语言 也叫做脚本语言。执行这类程序时,解释器需要读取我们编写的源代码,并将其转换成目标代码,再由计算机运行。因为每次执行程序都多了编译的过程,因此效率有所下降
————————————————
#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种Shell,这里指定bash
echo 是Shell的一个内部指令,用于在屏幕上打印出指定的字符串
注意,一定要写成./helloworld.sh,而不是helloworld.sh,linux系统会去PATH里寻找有没有叫helloworld.sh的,而helloworld.sh不在PATH里,所以写成helloworld.sh是会找不到命令的,要用./helloworld.sh告诉系统说,就在当前目录找。
————————————————
变量类型
类型 解释
局部变量 局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量
环境变量 所有的程序,包括shell启动的程序,都能访问环境变量,有些程 序需要环境变量来保证其正常运行。可以用过set命令查看当前环境变量
shell变量 由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell 的正常运行
————————————————
C语言----enum枚举类型详解
https://blog.csdn.net/weixin_39918693/article/details/80454440
在实际的编程应用中,有的变量只有几种可能的取值,譬如说一个星期的七种可能,性别的两种可能等等。C语言为这种类型的变量的定义提供了enum关键字。
要使用枚举类型的变量,首先需要先定义一个枚举类型名,然后再声明该变量是枚举类型的
例1:
enum WEEKDAY{ \该语句定义了一个枚举类型
MONDAY = 1,
TUSEDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};
enum WEEKDAY day; \该语句声明了一个枚举类型的变量
上述例子中的MONDAY、TUSEDAY等称为枚举元素或枚举常量(本人更倾向于称其为枚举常量,因为这样可以反映其性质),它们是用户自定义的标识符而已。
在没有显式说明的情况下,枚举类型中的第一个枚举常量的值为0,第二个为1,以此类推。如果只指定了部分枚举常量的值,那么未指定值的枚举常量的值将依着最后一个指定值向后递增(步长为1)
不同枚举类型中的枚举常量的名字必须互不相同。同一枚举类型中的不同的枚举常量可以具有相同的值
枚举类型为建立常量值与标识符之间的关联提供了一种便利的方式。相对于#define语句来说,它的优势在于常量值可以自动生成
尽管可以声明枚举类型的变量,但编译器不检查这种类型变量中存储的值是否为该枚举类型中的有效值,不过,枚举类型的变量提供这种检查
注意:
不能对枚举常量进行赋值操作(定义枚举类型时除外)
枚举常量和枚举变量可以用于判断语句,实际用于判断的是其中实际包含的值
一个整数不能直接赋值给一个枚举变量,必须用该枚举变量所属的枚举类型进行类型强制转换才行
使用常规的手段输出无法输出枚举常量所对应的字符串,因为枚举常量为整型值
在使用枚举变量的时候,我们不关心其值的大小,而是其表示的状态
————————————————
版权声明:本文为CSDN博主「凌动处理器」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39918693/article/details/80454440
#ifdef与#ifdefine()之间的区别
装载链接:https://blog.csdn.net/stoic163/article/details/73527598
#if和#ifdef之间的区别
对于#if后面需要是一个表达式,如果表达式为1则调用#if下面的代码。
对于#ifdef后面需要的只是这个值有没有用#define定义,并不关心define的这个值是0还是1。
要使得判别条件#ifdef结果为假,也就是不执行判别条件后面所跟的语句,需要对#ifdef后面所跟的宏使用#undef语句进行声明。
另外:
经常在内核中看到这样的宏判断:
#ifdef CONFIG_**
…
#endif
比如,init/main.c中有这样的代码:
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
这里的意思我知道,就是根据CONFIG_PROC_FS是否定义来选择proc文件系统。
如果选择proc文件系统,在.config中就会有如下内容:
CONFIG_PROC_FS=y
如果要将上面的proc_root_init编译进内核,必须有#define CONFIG_PROC_FS或者编译时使用-DCONFIG_PROC_FS,但这两种方法我在代码中都找不到,难道是CONFIG_PROC_FS=y起作用了
我是这样理解的:
#define CONFIG_PROC_FS 1 是在 include/linux/autoconf.h 中定义的 (autoconf.h 又是通过make menuconfig生成的)
在 顶层的Makefile中 定义了 LINUXINCLUDE.
LINUXINCLUDE := -Iinclude
$(if ( K B U I L D S R C ) , − I i n c l u d e 2 − I (KBUILD_SRC),-Iinclude2 -I (KBUILDSRC),−Iinclude2−I(srctree)/include)
-include include/linux/autoconf.h
在编译 init/main.c时gcc会处理 -include include/linux/autoconf.h,相当于源文件中有 #include "linux/autoconfi.h"语句,此时 init/main.c就可以使用autoconf.h中的个个宏了。
为何不能在头文件里写定义?
转载链接:https://blog.csdn.net/trap94/article/details/50602090
我们都知道,在写外部函数的时候,应该在源文件中写具体定义,而在对应的头文件中写申明,最后在执行文件中包含这个头文件就行了。
但到底为什么不能在直接在头文件里写定义呢?
一句话解释下:因为会产生重复定义的问题!
但我们还知道,我们还有#ifndenf这个东西,如果要问起来这东西是干啥用的,估计大家都知道这东西是用来防止源文件重复包含相同的头文件的,但我们用了这东西是不是表示就允许在头文件里写定义了呢?
准确的说:依然不可以,因为#ifndenf只解决了部分环节的重复定义问题!
########给新人科普##########
头文件中#ifndef的用法如下:
#ifndenf __XXXXX_H
#define __XXXXX_H
函数声明
#endif
其中ifndef(if not define)用来判断这个宏有没有定义过,如果没有定义过,说明这个头文件是第一次引用,那么就继续往下执行;如果这个宏定义过了,说明这个头文件已经被包含过了,直接跳到endif,也就是什么都不执行,就可以防止重复包含头文件了。
########################
那么为什么说#ifndef不能解决所有重复定义的问题呢?因为重复定义这个可能发生编译的不同环节,下面我们一个环节一个环节来分析。
第一个环节:预处理
这个环节做两件事,一把include的头文件内容进行替换,二是处理宏定义。不过这个环节并没有对语法进行检查,所以无论怎么重复定义,这个环节都不会报错的,只不过这个环节以后就没有include了,头文件的内容都替换上去了。
第二个环节:编译(汇编+编译)
这个环节是将源文件变成二进制的目标文件,也就是把.c变成.o(windows下是.obj),并且这个环节是会进行语法检查的,所以如果在某个头文件中定义了一个函数,然后源文件中又连续多次包含这个头文件的话,那么在编译阶段就会报错,例子如下所示:
因此我们需要利用#ifndef来避免重复包含相同的头文件(表面上看好像不会有人傻到像我上面这样连着写很多一样的include,但是我们知道头文件是可以嵌套包含的,如果a.h中包含着b.h,而如果你的.c文件又同时包含了a.h和b.h,一旦项目复杂了,这种情况是否就有可能发生了呢)所以使用#ifndef还是很有必要的。
ok,到目前为止,#ifndef成功地帮我们解决了在编译阶段的重复定义问题,不过它能做的也仅限于此了。
第三个阶段:链接
这是我之前一直都没有想通的一个地方,先看下面这个例子:
上面这个例子的头文件中使用了ifndef,但是最后还是出现了重复定义的问题,为什么呢?原因是在编译阶段,每个源文件都是独立编译的,他们会生成独立的.o文件,这些文件单独出来看的话是每个问题的,每个源文件都只有一次定义,因而编译通过,生成了main.o和file.o这两个目标文件,但这两个文件中各有一次定义,所以在链接阶段,把这个两个目标文件链接在一起的时候就变成有两次定义了,也就出现重复定义的问题了。
总结一下,ifndef可以解决编译阶段发生的重复定义问题,但不能解决链接阶段发生的重复定义问题,所以不要在头文件中作具体定义!
当然以上所说都是在头文件中进行了定义的情况,如果没有在头文件中作定义,而只是声明的话,那么,要是不讲就的话,哪怕不写ifndef都是可以的,因为只有重复定义会报错,重复声明是不会报错的。
————————————————
版权声明:本文为CSDN博主「trap94」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/trap94/article/details/50602090
C语言的头文件和宏定义详解
装载链接:https://blog.csdn.net/abc_12366/article/details/79155540
之前对C语言的头文件和宏定义抱着一知半解的态度,现理清思路并以文字的形式整理出来,以供不时之需
头文件
头文件,顾名思义就是定义在C语言文件头部的那一坨东西
#include
1
这就是一个标准输入输出的头文件声明,头文件声明写在定义文件(后缀名.c的文件)的头部,并在定义文件中具体实现
#include
#include “mylib.h”
1
2
这是两种声明头文件的方法,其中尖括号表示“到环境指定的目录去引用”,而双引号表示“首先在当前目录查找,然后在到环境指定的目录去引用”
在C表准库中每个库函数都在一个头文件中声明,可以通过第一种方式引用
头文件的格式
#ifndef MYLIB_H
#define MYLIB_H
…
#endif
1
2
3
4
5
第一句“ifndef”意思是“如果在导入头文件的文件中之前没有导入该头文件就编译下面的代码”,该句的作用是防止重复导入
第二句“define”是“宏定义”的意思,表示以下代码是的头文件主体部分
最后来一句“endif”和“ifdef”首尾呼应
其中“ifndef”和“define”后面跟的是相同的“标识”,通常和头文件名相同,所有字母均大写并把点号改为下划线即可
#include “mylib.h”
1
看到这句话后编译器会把该头文件“mylib.h”复制粘贴到导入的文件中,之后你就可以使用头文件中定义的常量和结构定义了
显然恰当地使用头文件有利于更好的组织文件和项目
提请注意
何时使用
小栗子
#ifndef NODE_H
#define NODE_H
typedef struct _node{
int value;
struct _node *next;
}Node;
#endif
1
2
3
4
5
6
7
8
9
#include “node.h”
int main(int argc, char const argv[])
{
Node p=(Node)malloc(sizeof(Node));
…
return 0;
}
1
2
3
4
5
6
7
常用头文件
stdio.h 标准输入输出
stdlib.h 标准常用库
string.h 字符串函数库
math.h 数学库
ctype.h 字符函数库
time.h 时间库
windows.h 微软视窗库
宏定义
宏定义是C语言提供的三种预处理功能的其中一种,这三种预处理包括:宏定义、文件包含、条件编译。宏定义和操作符的区别是:宏定义是替换,不做计算,也不做表达式求解。
“宏定义”也称“宏替换”,“宏”
define PI 3.1415926
1
这就是一个简单的宏,在程序的预处理阶段宏名会被替换为后面的字符串
传入参数的宏
#define func(para) #para
…
char str[]=func(hello); //被展开为:char str[]=“hello”
1
2
3
说明:如果传入的参数之前有空格则忽略之,如果参数之间有多个空格则在连成字符串时只算一个
#define fun(pa) #@pa
char a=fun(a); //被展开为char a=‘a’;
1
2
#define COMMAND(a,b) a##b
…
COMMAND(1,2); //相当于12
CMOOAND(ac,b); //相当于acb
1
2
3
4
#define LOOP(FROM, TO, CONTENT)
for(int i=FROM;i
}
1
2
3
4
#define eprintf(…) fprintf (stderr, VA_ARGS)
eprintf ("%s:%d: ", input_file, lineno)
//==> fprintf (stderr, "%s:%d: ", input_file, lineno)
1
2
3
为什么要使用宏定义
简而言之,使用宏定义可以提高代码的可读性
具体的说,可以减少magic number的使用,并且可以做一些方便的替换,如下面的代码:
#define MALLOC(n, type) (type*)malloc((n)*sizeof(type))
1
使用时,int *p=MALLOC(10, int); 即可
宏的规范写法
另外宏定义需要注意的
#define sqrt(x) xx
…
int y=sqrt(1+2); //y = 1+21+2 = 5 ≠9
1
2
3
这时候加上括号就好了:
#define sqrt(x) (x)(x)
…
int y=sqrt(1+2); //y = (1+2)(1+2) = 9
1
2
3
https://www.cnblogs.com/clover-toeic/p/3728026.html