==============================================java
copyright: KIRA-lznios
==============================================c++
转载请注明出处,这篇是我原创,翻版必究!!!!!!!!!!!!!!!!!!!程序员
==============================================正则表达式
若是以为写个好,请留个言,点个赞。redis
最喜欢吴军博士的一句话,和我本人的学习理念比较接近,因此对他的书也很是着迷:技术分为术 和 道,术 是具体作事的算法,道是其背后隐藏的根本机理。算法
就像吴军博士说的那样,sql
1.高大上的天然语言处理背后模型机理尽然如此简单(固然细节不简单)数据库
2.怎么像你奶奶解释搜索引擎?其实搜索引擎的背后机理其实简单的不能再简单了,就是布尔运算!!!三句话就能讲明白,一是下载尽量多的网页,二是创建索引,三是根据相关性给网页排序!没了,这就是搜索引擎,任何智能的搜素引擎都逃不出布尔运算的框架。express
3…..
以我我的愚见,首先得深入理解道,而后再去发扬术会比较好。由于只有深入理解道,而后才能举一反十!!!而后在你接触新东西的时候,能对之前学的知识加以联系,发现其中的隐含机理的类似性。并能把一个领域的经典研究方法带到另外一个研究领域。
先交代一下:
1.这是我第一篇,忽然想写点有质量的文章,来和你们分享知识,写的很差的地方欢迎拍砖。
2.本人写过编译器,编译器根本不是什么高大上的东西,本质就是一种数据(信息/语言)处理的方法而已,和处理其余数据同样,并和处理天然语言进行对比
3.下一篇是关于学完编译器以后,应该掌握的技能,即进阶信息安全的基础:
关于一段c/c++代码,编译以后,生成怎么样的x86,calling convention,prolog/epilog,caller-saved/callee-saved register,堆栈平衡,全部变量的内存分布,函数符号修饰成什么样,静态连接,动态连接,地址修正,连接指示对编译过程的影响,如dllimport,dllexport,#pragma,函数声明顺便提一下连接器,以及windows下病毒的运行机理,我不会重点写什么是动态连接,而是解释为何动态连接,及其背后隐藏的缘由
4.下一篇关于OO object model,本人对OO有必定了解,封装,继承(单一,多重,怎么解决菱形多重继承数据二义性问题,微软怎么解决?gcc怎么办?分析咱们用的 prefix算法 实现对象模型的继承 ,并给出拓展),多态,这篇以c++为基本,讲述c++ object model,并给出c++为何转换指针会变化(Base* b = new Derived();编译器怎么理解对象模型的,怎么就能多态??对象模型长成什么样,怎么样会形成覆盖,遮蔽?和多态在对象模型上有什么区别?遮蔽,覆盖为毛就不能多态了?),并分析一下c++对象模型优缺点,容易受到什么攻击(堆溢出,堆喷射),虽然hook 函数指针本质不是c++语言自己形成的。。可是c++对象模型若是对于你们都是好人的状况下,是很优秀的对象模型,but。。。
5.下一篇准备写关于高级(多核)操做系统内核的理解,固然是基于MIT的 xv6 和 Yale的pios ,关于 Vitrual Memory:逻辑地址->线性地址->物理地址, fork/join/ret ,copy-on-write…..
6.再下一篇多是关于 内存数据库 新存储方式的新实现(本人拍脑壳想的),并和 sqlite3,nosql,redis 等内存数据库进行 性能,实现方式 的比较
本文参考了数学之美,编译器(虎书 和 龙书),和在USTC老师教的,加上我本身写编译器过程的理解,
最后加上我本身设计的final project:code generation(minijava->x86,AT&T,IA32)
本人花了一个学期的时间,认真的写了一个编译器,差很少由如下部分组成:
miniJava compiler ->
implement: lexer,parser,AST,elabrator,garbage collector(Gimple algorithm),
code generation(minijava->C), code generation(minijava->java bytecode),
code generation(minijava->x86,AT&T,IA32),
object model(encapsulation,single inherit,polymorphism)
theory: exception,closure,SSA(static single assignment),
register allocation(graph coloring)
optimiztion:CFG(liveness analysis,Reaching Definition analysis),DFG,SSA, Lattice, register allocation(graph coloring)
写本文的目的:
写完编译器,发现编译器更多的是一种数据处理的方法,而不是什么高大上的东西,我写这篇文章的目的,是想任何读完我文章的人,知道编译器到底在干嘛,编译器到底能干些什么?学了编译器以后有神马好处?学完编译器应该掌握什么技能???
我会不断提出问题,引起读者的思考,我喜欢有逻辑的思考问题,但愿这样能让文章更有逻辑性。
并且我写东西,不喜欢记流水帐,好比这个应该怎么怎么样,而是写为何要这样,我喜欢搞清楚其背后的缘由。
本文可能会很长,我会从背后隐含的原理去写,而不去探讨高大上的技术。
好了,废话很少说,正文开始。
1. 先说说天然语言处理吧(本人不是很懂),一些基本概念,懂行的人直接跳过,谢谢。
a.首先古老的文明为何会出现文字?
由于文字仅仅是信息的一种载体,意图仍是想把信息记录下来,本质仍是信息,古代没有文字的时候,人们好比到了冬天冷,会发出一些 ,"嗖嗖"的声音,肚子饿了,会发出一些什么什么声音,而后因为声音太多,信息太多,人们没法记住,也没法统一,如此才出现了文字,没有为何,就是由于没有人能记住全部的声音,
这样就须要一种文字,去记录那些信息。
b.有了文字,就必定会有语句,N个文字用不一样的语法规则去拼凑生成的语句,不一样的语法规则,生成不一样的语言,这个很好理解。
c.随着文明的发展,信息愈来愈多,可是文字的数量不能成倍的增加,不然也不便于记忆,这样就出现了聚类,把一些相同概念的意思,概括用一个字(词)去表示,好比一次多义,"日"表示太阳,表示太阳早上从东边升起,从西边落下,因此又能够表示一天,等等。
第c条就是所谓的一词多义,绝对是困扰古今中外语言学者,包括计算机科学家的一个大问题,也就是理解这个词的意思,须要参照上下文(context)
d.常识。
The pen is in the box.
The box is in the pen.
第一句正常人都懂,第二句有点坑了,不过外国人很容易理解,因为外国人的常识,经验,因此外国人立马就明白,第二句的pen的意思是围栏。
天然语言处理,想分析语句的语义就又多了一坑。
其实我就是想说 c 和 d 是基于 编译器技术的 lexer+parser分析 天然语言的语义 上的一个大坑, 这个就是困扰计算机科学家,语言学家多年,以及阻碍处理天然语言的缘由之一。
e.为何要分词?
像英语这种基本不须要,由于空格就是活生生的分隔符(可是对于手写识别英文,空格不明显,仍是须要分词的),可是对于 中,日,韩,泰 等语言,好比 今天我学会了开汽车,中间没有分隔符,因此须要分词。
分词其实也是一坑,好比:
此地\安能\居住,其人\好不\悲伤
此地安\能居住,其人好\不悲伤
2.为何要扯天然语言处理,这个和编译器到底有什么关系?
听我慢慢道来。
天然语言处理,其实就是处理好比,今天\我\学会了\开\汽车。 you \ are \ so \ cool.
而基于编译器技术的 lexer + parser ,则也是同样, 今天\我\学会了\开\汽车,不过一般是处理计算机语言,相似,static void main(string[] args)等等。
so:
a.天然语言处理,处理的是天然语言,好比上面举得例子,The box is in the pen. 定义的上下文相关文法,即其中词语的意思不能肯定(一次多义),须要结合相应的语境才能知道pen的意思,和你们作的英文完形填空是差很少的。
b.编译器的如java语法,static void main()定义的是上下文无关文法,注意,上下文无关文法的好处就是,只要你定义的好,不会发生歧义,由于不存在一次多义,稍稍举个小例子。
exp -> NUM
-> ID
-> exp + exp
-> exp * exp
碰见exp就能够无条件分解为后面这四种状况,而后再不断的递归降低(recursive decendent parser/ top-down parsing/predicative parsing )迭代,解析语句。
为何说只要定义的好呢?由于咱们lab用的是ll(k),也就是说,只支持从左到右parser,若是出现左递归就会出现永远循环下去,由于是无条件分解。
定义左递归上下文无关文法坑:
a.左递归->右递归
b.歧义->提取公因式
一些其余编译器应该支持lr(k)
到这里看不懂不要紧,这里只是随便提提。
我只是想说,像编译器编译的 c/c++/java...,包括sql语句,都是上下文无关文法,均可以用基于编译器的技术,lexer + parser 去解决问题
ok,有的人就要问了,那为何基于编译器的技术,lexer + parser 把天然语言,先分解为一系列的token,以后生成语法树,而后用llk or lrk 去遍历这棵树,而后进行 语义分析, 为何不能很好的处理天然语言?
误区:本来科学家觉得,随着语言学家对天然语言语法的归纳愈来愈完备,计算机计算能力又在逐渐提升,基于编译器的技术应该可以很好的解决天然语言处理。
but:一条很简单的上下文相关的语句,却能分析出很庞大复杂的 AST(parser 返回结果是 语法树), 若是再复杂一点,基于语法树的分析根本行不通。
考虑一句很长的文言文,此处省略100字。
结论:因此说,基于编译器技术的lexer + parser 只适合解决上下文无关文法 定义出的语言。
那上下文无关文法 就不能定义 天然语言了??要不试试看?
反正我不试。。缘由以下:
a.想要经过上下文无关文法定义汉语的50%语句,语言学家不只会累死,并且因为一词多义,须要结合语境,因此还要在文法里定义各类语境,能够想象那个工做量 吗
b.定义的上下文无关文法越多,越容易出现歧义(提取公因式),并且会出现左递归(改为右递归),如此,如此,会疯掉的。因此 没法涵盖全部语言语法不说,还有歧义,这个是要作成实际应用的,这样能忍吗?
如此说来,20世纪50年代到70年代,用 基于编译器技术 lexer + parser 分析天然语言的语义,绝对是科学家们走的弯路。
直到20世纪70年代,才有先驱从新认识这个问题,基于数学模型和统计,天然语言处理进入第二个阶段。
再总结一下结论: 基于编译器技术 lexer + parser 分析语言的语义, 只适合 上下文无关文法, 而上下文无关文法 没法(不容易)定义 天然语言,so,不能用lexer + parser 去分析天然语言的语义。
3. 那到底怎么处理天然语言呢?(本文不会详细写怎么处理,只写基本原理),懂行的请自觉跳过,谢谢。
从规则到统计,用数学的方法去描述语言规律。
注意,统计语言模型的产生初衷是为了解决语音识别问题,也就是说 一句话,让你分析,这句话究竟是不是具备正确意义的天然语言。
用统计的思想思考:一个句子,由特定的单词串组成,s = w1,w2,...,wn ,一个句子有意义的几率是 P(s) ,
由条件几率很容易获得 P(s) = P(w1) * P(w2 | w1) * ..... * P(wn | w1,w2,...,wn-1)
只要算出这个语句有意义的几率,不就能判断到底这句话有木有意义了呢
可是越到后面这个条件几率越难算了,怎么破?
不要紧,马尔可夫为咱们想了一个偷懒并且颇为有效的方法就是,假设 一个词 wi 出现的 几率 只和它前面的那个词 wi-1 有关系,
因此公式就简化为 P(S) = P(w1) * P(w2 | w1) * P(w3 | w2) * ..... * P(wn | wn-1)
固然,这个模型,不少人第一次见到,确定会问,就这东东,能分析这么难文法的天然语言。。。。吗?
答案是确定的,Google 的 罗塞塔 系统,仅仅开发2年,就是基于相似这种数学统计模型,就一举成名的得到了 NIST 评测的第一。
固然,对于高阶一点的语言模型,其余模型,模型的训练,零几率问题,我在本文不想深刻讨论,讨论的重点,主要仍是想放在编译器上面。
一点点思考:
说到这里,说一点题外话。本人还写过内存数据库,因此,须要支持sql语句,为sql语句也写过 lexer 和 parser,用的也是上下文无关文法。
考虑若是sql语言,若是发展足够强大,就像天然语言同样,语法愈来愈多,会不会出现 聚类(一词多义) ?若是出现聚类,那根据个人结论,
lexer + paser这种方法不work了,那是否是得用到 天然语言处理的 某些方法,或者其余方法???
因为目前的语言c/c++/java/sql 仍是处于上下文无关文法就能够定义的语言,有个度(界限)的问题,若是跨越到天然语言,则之前的方法根本不能用了,是否是得考虑新的技术。
啧啧,随便说说。
4.关于天然语言处理 和 编译器相关技术处理 的浅薄关系 在上面已经说过了,接下来就是我要讲清楚,编译器到底在干什么?
我以前说过,编译器也是对一种语言的处理过程,因此上文和天然语言处理进行了对比,而后引起了一点点小思考。
ok,书上说编译器就是把高级语言翻译成低级语言,忘了,书上好像是这么写的。
不过我理解的编译器应该是这样,
a. 编译器会通过 lexer + parser + elabraor + code generation : IR(N种) for optimization + 可能还连接一个garbage collector
->而后生成object file(目标文件),注意目标文件仍是不能运行,可是就差那么一点点,这一点点是什么(对于外部符号,编译器不知道,只能进入符号表,等待连接器来修正)?
好比你 cl /c main.c 这样只编译不连接,若是出现编译器不认识的符号,不要紧,反正生成目标文件,那些符号就进入了符号表,等连接器下一步工做。
可是你 cl main.c ,这样既编译又连接,若是有不认识的符号,直接报错(假设你其余目标文件也木有这个符号)
总结:等连接器,把其余的目标文件link到一块儿(主要是地址修正),而后生成可执行文件(静态连接/动态连接/动态连接静态加载/动态连接动态加载,不同), 这样就生成了可执行文件 .exe / a.out ... 芯片上跑去吧
详细细节留给下一篇吧,要写就停不下来。。。
b. 编译器确实是把高级语言翻译成低级语言,可是其中会通过不少种IR(中间代码),大部分缘由是由于优化,像gcc就通过N种优化,而后生成一个最简的x86机器码,而后跑在intel的芯片上,固然ARM,MIPS均可以。。。固然你翻译成java的bytecode ,在虚拟机上跑,都是能够的。
IR嘛,举个例子,好比
第一步我就要对AST进行优化,优化一般有 常量折叠,代数简化,标量代替聚量, 常量传播,拷贝传播,死代码删除,公共子表达式删除等等
class TreeVisitor{
public static void main(String[] a){
System.out.println(new TV().Start());
}
}
lexer的输出,很明显是,a stream of lexical tokens :class | TreeVisitor | { | public | static | void | main | ( | String | [ | ] | a | ) | { | System | . | out | . | println | ( | new | TV | ( | ) | . | Start | ( | ) | ) | ; | } | }
看一下 Token结构体长成神马样子?
class Token{
public Kind kind; // kind of the token
public String lexeme; // extra lexeme for this token, if any
public Integer lineNum; // on which line of the source file this token appears 目前能够忽略,只是为了输出
......}
看下输出,你们就明白了:
TOKEN_CLASS: class : at line 5
TOKEN_ID: TreeVisitor : at line 5
TOKEN_LBRACE: <NONE> : at line 5
TOKEN_PUBLIC: public : at line 6
TOKEN_STATIC: static : at line 6
TOKEN_VOID: void : at line 6
TOKEN_MAIN: main : at line 6
TOKEN_LPAREN: <NONE> : at line 6
TOKEN_STRING: String : at line 6
TOKEN_LBRACK: <NONE> : at line 6
TOKEN_RBRACK: <NONE> : at line 6
TOKEN_ID: a : at line 6
TOKEN_RPAREN: <NONE> : at line 6
TOKEN_LBRACE: <NONE> : at line 6
..................
ok,分解为了 a stream of lexical tokens ,很明显用一个 队列 去存储它们。
note:
很是建议用队列去存储,为何?
1.咱们lab用的是直接在parser里面一个一个直接读取lexer分解出来的Token,即不能回滚,即上一个Token还得用一个value记录下来,固然你能够
定义回滚几个,而后记录 rollbackToken1,rollbackToken2,rollbackToken3....等
2.用队列虽然浪费了存储空间,可是能够任意回滚任意个数的Token
so,建议看具体须要。
神马状况下会遇到回滚Token?
好比,
MyVisitor v ;
root = new Tree();
因为是递归降低分析(在paser中详细讨论,看完paser再回来理解),只能像微软的编译器同样,写c语言的时候,定义放在语句前面,若是你在中间某个地方写了,int a = fun(1,2);则微软编译器会报错,可是一个这样的小错误,微软的优化器会爆出各类错。。。让你根本就不知道哪错了
回到正题:因为和c语言同样,本编译器算法是,前面是定义,后面是语句。
so,检测到root 的时候 Token是个ID,没问题,可是后面发现Token 是 = 号,也就是你进入 定义和语句的 临界区域了,so,你的代码还在分析定义的代码里,怎么破?你得回滚,而后跳出整个 分析 定义的代码,进入分析语句的代码,而后 current Token 得回滚到 root (原来在=)。
note:
可是gcc支持语句中有定义,不是由于 ANSI c 支持,而是gcc进行的拓展。
gcc怎么实现的?其实很容易,和c++/java 同样,加减符号表运算便可
gcc的c还支持bool呢,呼呼。
note:
吐槽微软编译器:
void fun(){}
这样的空函数,微软还不优化,
fun:
push ebp
mov ebp,esp
push ebx
push esi
push edi
这三个是 callee-saved 寄存器,微软还要入栈保存,是否是有点懒了,别说寄存器分配了,若是 寄存器分配(好比图着色) 只用到一个寄存器,这样入栈保存一个不就好了吗?
note:算了,仍是表扬下微软的编译器吧,好比你看到,
push ebp
mov ebp,esp
push ecx // 而不是 sub esp,4
为何不用sub esp,4 ? 。。。。。。。这个缘由很深入,由于,一样是往下开辟4个byte, push ecx 用的(指x86,ARM不知道)机器码更少哦
其实我是想解释,为何 lexer 要 translates the source program into a stream of lexical tokens ?而不分解为其余结构 ?
想一想中文为何要分词? eg,今天我学会了开汽车,你用指针去扫源代码的时候,扫到 unicode "今" ,你能把它做为一个Token吗?明显不行,由于"今天"才是一个Token。。。那怎么样断句呢?即,怎么分词呢?最简单的方法就是查字典,这种方法最先是由北航的梁南元教授提出的。即,字典里有的词就表示出来,遇到复合词就最长匹配。
可是最长匹配也有问题。
好比, 上海大学城书店,你怎么分?
最长匹配是: 上海大学/城/书店?
显然不对,应该是 上海/大学城/书店
这里不进一步讨论。
好了,以前说过,像英文这样 I am so cool. 语句之间有标点符号,语句之中有空格,因此,不须要分词,Token很容易找到!!!!!!
代码也是这样,大部分是有分隔符(以空格分开)的,可是也有例外,好比,
/
//
/*
遇到一个/,你能武断说这个Token是 / 吗?嗯,得看看后面跟的是啥。
回到正题,为何要分解为a stream of lexical tokens?
由于好比天然语言是由一个一个单词组成的,单词组成的顺序,则是语法。
你只有先把一个一个单词分解出来,而后去分析每一个单词之间为何这样排列(这就是分析这句话是神马语法 -> 找出它的语法规则 ),而后生成一棵语法树,存储起来。
分词就是lexer干的事情,它的输出就是给 parser 的输入,parser 则负责生成 AST(抽象语法树),并传给 elabrator。
note:
说道分词,编译器技术已经完美解决了这个问题(仅仅针对上下文无关文法),即用 正则表达式。 NFA -> DFA
我不想延伸,由于内容太多,之后有机会再写。
固然lexer有不少,好比 flex, sml-lex, Ocaml-lex, JLex, C#lex ......
说道这里,lexer我是否已经讲清楚了呢??我以为差很少了,之后有机会补充。
b. parser -> 根据 递归降低 分析算法,生成语法树
class TreeVisitor{
public static void main(String[] a){
System.out.println(new Visitor().Start());
}
}
class Visitor {
Tree l ;
Tree r ;
public int Strat(Tree n){
int nti ;
int a;
while(n < 10)
a = 1;
if (n.GetHas_Right())
a = 3;
else
a = 12 ;
return a;
}
}
递归降低,能够用一个词来来归纳,其实就是 while循环。
若是说要返回一个AST,这样固然须要先定义全部抽象语句的类,而后生成其对象,而后reference相互连起来,造成一棵树。
parser 输出返回一棵AST -> theAst = parser.parse();
ast.program.T prog = parseProgram();
.......
ast.mainClass.MainClass mainclass = parseMainClass();
java.util.LinkedList<ast.classs.T> classes = parseClassDecls();
......
java.util.LinkedList<ast.classs.T> classes = new java.util.LinkedList<ast.classs.T>();
ast.classs.T oneclass = null;
while (current.kind == Kind.TOKEN_CLASS) {
oneclass = parseClassDecl();
classes.add(oneclass);
}
注意,我为何说,递归降低就是while循环,上面漂绿的字体很明显了,当你分析某一种语法的时候,不断用while探测,若是进入下一个语法,则跳出while循环。
再说细一点:
int nti ;
int a;
while(n < 10)
a = 1;
if (n.GetHas_Right())
a = 3;
else
a = 12 ;
函数开始的时候,先分析 "定义" ,分析到 int nti; 没问题,是 "定义" ,而后到 int a; 也没问题,是 "定义"。
可是到了 while 语句,则 编译器代码跳出 分析 “定义” 的代码,进入 分析 "语句" 的代码。
注意一点便可,我上面举得例子。
MyVisitor v ;
root = new Tree();
OK,返回了AST,好办了,能够直接 pretty print 出来了,由于你已经有了AST,即一棵树,全部这段程序的语义都存储起来了,你想怎么打印,
不就怎么打印了?
好比:
@Override
public void visit(ast.stm.If s)
{
this.sayln("");//if语句前换个行先
this.printSpaces();
this.say("if (");
s.condition.accept(this);
this.sayln(")");
this.indent();
s.thenn.accept(this);
this.unIndent();
this.printSpaces();
this.sayln("else");
this.indent();
s.elsee.accept(this);
this.unIndent();
return;
}
理论联系下实际:
假设定义语义 : int + int -> int
@Override
public void visit(ast.exp.Add e)
{
e.left.accept(this);
if (!this.type.toString().equals("@int"))
error("operator '+' left expression must be int type",e.addleftexplineNum);
ast.type.T leftty = this.type;
e.right.accept(this);
if (!this.type.toString().equals("@int"))
error("operator '+' right expression must be int type" ,e.addrightexplineNum);
this.type = new ast.type.Int(); //表示当前操做 add,完成以后,“返回”一个操做数类型为 int
return;
}
note:
这里不讨论关于继承(多态),function call等再难一点的语义分析,不是本文重点。
OK, elabrator 的工做,总结下,就是先扫一遍AST,而后生成相应的符号表(多态涉及prefix算法计算继承后的对象模型中虚函数表的函数指针排列顺序,这里不讨论),而后进行类型系统的判断,报出一些语句出错的信息,或者警告信息。
elabrator我是否已经讲清楚了呢?
d. code generation -> 生成 IR
本人作了 minijava -> java bytecode / c / x86
minijava 直接 -> x86,我几回推翻重写,不过最后完成了,仍是很 happy (minijava没有很高深的java语法,仅仅是封装,继承,多态,我用x86模拟了而已)
仍是有必定难度的,用汇编这种低级语言去模拟封装,继承,多态,仍是有必定难度的,放在之后讨论吧,写不完了。
e. 讨论 exception,closure,SSA(static single assignment) 是怎么样实现的
exception:其实编译器一般有2种方法,
1.基于异常栈
2.基于异常表:pay as you go
细节,不想在本文讨论了。
closure: 我会讨论在java非要支持nested function以后,一步一步逃逸变量是怎么样不可以存储,而后引出closure的解决方法的,还会给出closure 和 object model 有什么区别?
SSA(static single assignment) 真心是一种牛逼的IR,让不少优化变得很是简单。可是内容太多,写不完了。自从有的这个SSA,gcc版本从某一个版本,忘了,开始所有把基于 CFG , DFG 的优化,变成SSA了
5. 用编译器知识理解语言小细节
1.好比到底应该写成 char* p; 还应该写成 char *p; 这种问题其实很好理解,为何,编译器怎么处理指针? 即,碰到类型后面碰到*,就把后面的变量当作指针,好理解了吗,这就是为神马 char* p1,p2; p2不是指针的缘由
我我的喜爱,就把 char* 当作一个类型,只须要注意 char* p1,p2; p2 这种状况便可,不少人不是喜欢这样写typedef char* pchar吗,这不就是赤裸裸的认为char*就是一个类型吗?没错,我就喜欢把它当作一个类型。
2.好比 const 修饰的 变量,老是分不清 ,
const char* p;
char const* p;
char* const p;
const char* const p;
我说一句话,你就能永远分清,信不信?固然这个是我从effective c++里面学的,
const 出如今*左边修饰的是指针指向的value,而出如今右边则是修饰的是指针,
没错,你已经会了。
前面2个一个意思,都是修饰value是const,第三个是修饰指针是const,第四个是2个都修饰。
3.好比神马 前加加,后加加 ,搞不清楚 ++a;a++;....
int a = 1;
printf("%d",a++); 为毛答案仍是1 ?
int a = 1;
printf("%d",++a); 为毛答案就是2了 ?
你若是学过编译器,你就懂了,你能够这样理解:
printf("%d",a++); 其实会被编译成2句话
printf("%d",a);
a++;(a = a + 1;)
这样,答案是神马,不用我说了吧。这个就叫作后加,懂了吧
printf("%d",++a);实际上也是会被编译成2句话
a++;(a = a + 1;)
printf("%d",a);
为何是2?一目了然,之后还分不清前加后加吗?嘻嘻。
4. 其实 循环语句,其实对于x86来讲就1种->跳转,固然跳转有2种,
结语:
note:
其实编译器技术,还有不少不少,我只是讨论了其中的九牛一毛,并且因为篇幅限制,我写不下太多。做为第一篇文章,暂时先这样吧,之后再更新。
本人对信息安全也略懂,因此对底层的一些东西有一些本身的理解,其实这些都是基础,作安全最最重要的基础,是在课本上根本学不到的东西,最最精华的东西,在之后的文章中我会陆续提到:
学完编译器,对语言的理解又更深了一步,好比你看到以下东西,
int c = 4;
int d;
void fun(int a,int b)
{ int n = 4;
int i;
for(i=0;i<n;i++)
printf("a+b=%d\n",a+b);
}
int main()
{
fun(1,2);
return 0;
}
要思考,编译以后,生成怎么样的x86,calling convention,prolog/epilog,caller-saved/callee-saved register,堆栈平衡,全部变量的内存分布,函数符号修饰成什么样,静态连接,动态连接,地址修正,连接指示对编译过程的影响,如dllimport,dllexport,#pragma,函数声明等等
之后的文章我会陆续解释。
其实学完编译器的真正效果,就是你看到上面的c,能想到,其实它就是神马。。。