C++开发:源码编译过程
C++源码的编译过程可以分为几个主要步骤。让我们一步一步地详细解释这个过程:
预处理是编译过程的第一个阶段。这个阶段主要处理预处理指令(如 #include、#define 等)。预处理器会执行以下操作:
- 文件包含:将 #include 指令中的头文件内容插入到源文件中。
- 宏替换:将 #define 定义的宏替换为其对应的值。
- 条件编译:根据 #ifdef、#ifndef、#if 等预处理指令,决定哪些代码片段应该被编译。
- 注释删除:删除源代码中的注释。
预处理的输出是一个纯文本文件,通常带有 .i 扩展名。
编译阶段将预处理后的代码转换为汇编代码。这个阶段包括以下步骤:
- 词法分析:将源代码分解成一系列的记号(tokens)。
- 语法分析:将记号序列转换为语法树(Syntax Tree)。
- 语义分析:检查语法树的语义是否正确,例如类型检查、作用域检查等。
- 中间代码生成:将语法树转换为中间表示(IR)。
- 优化:对中间表示进行优化,以提高代码运行效率。
- 目标代码生成:将优化后的中间表示转换为汇编代码。
编译的输出通常是一个汇编文件,带有 .s 扩展名。
汇编阶段将汇编代码转换为机器码(也称为目标代码)。汇编器会:
- 将汇编指令转换为机器指令。
- 分配内存地址。
- 生成目标文件,通常带有 .o 或 .obj 扩展名。
链接阶段将一个或多个目标文件和库文件组合成一个可执行文件。链接器会:
- 解析符号:将目标文件中的符号(如函数和变量)解析为具体的内存地址。
- 处理外部引用:解决目标文件之间的函数调用和变量引用。
- 合并代码段和数据段:将不同目标文件中的代码段和数据段合并。
- 生成可执行文件:输出最终的可执行文件,通常没有扩展名或带有 .exe 扩展名(在Windows上)。
C++源码的编译过程可以总结为以下四个主要步骤:
- 预处理:处理预处理指令,生成预处理后的源代码。
- 编译:将预处理后的代码转换为汇编代码。
- 汇编:将汇编代码转换为目标代码(机器码)。
- 链接:将目标代码和库文件链接成一个可执行文件。
每个步骤都有其特定的工具和作用,协同工作完成整个编译过程。希望这个详细的分步解释对你有所帮助!
如何编写优雅的 Java 代码
不知道各位在项目开发过程中有没有过这种体会,接手上一任的代码,看到代码的那一刻,有一种想要砸电脑的冲动,一个方法体内写了无数行代码,到处皆可看到复制粘贴的代码,变量命名也让人看不懂。
各位在编码时,是否有想过,我如何才能写出高质量的代码,写出优雅的代码,写出高度可扩展的代码。我相信大家都希望能写出那样的代码,让人佩服的五体投地,可不知道如何写,那么本文就是为帮助大家提高编码质量而生的。
我相信,大家在看完本文后,一定会有所领悟。下面,我们就进入主题。
在介绍 lombok 之前,我们先来看一段代码:
这段代码大家应该都很熟悉,我们在开发 JavaWeb 项目时,定义一个 Bean,会先写好属性,然后设置 getter/setter 方法,这段代码本身没有任何问题,也必须这样写。
但是每个 bean 都需要些 getter/setter,这样写的话就不够优雅了,这段代码我们如何优雅的写呢?接下来就轮到强大的 lombok 出场了。
lombok 是一个可以通过注解的形式来简化我们的代码的一个插件,要使用它,我们应该先安装插件,安装步骤如下:
1.
打开 IDEA 的 Plugins,点击 Browse repositories。
2.
搜索 lombok,并安装它,安装好后重启 IDEA。
3.
打开 Settings,找到 Annotaion Processors,将 Enable annotaion processing 勾选上。
4.单纯这样还不够,我们要用到 lombok 的注解还需要添加其依赖:
我们可以看到,在类上加入了 Getter 和 Setter 两个注解,将之前写的 getter/setter 方法干掉了,这种代码看着清爽多了,写个 main 方法来测试下:
我们并没有写任何 setter/getter 方法,只是加了两个注解就可以调用了,这是为什么呢?这是因为 lombok 提供的 Getter 和 Setter 注解是编译时注解,也就是在编译时,lombok 会自动为我们添加 getter/setter 方法,因此我们不需要显式地去写 getter/setter 方法而可以直接调用,这样的代码是不是看着非常优雅。
当然,lombok 的功能不止于此,它提供了很多注解以简化我们的代码,下面,我将分别介绍它的其他常用注解:
该注解的作用是是否开启链式调用,比如我们开启链式调用:
构建者模式,我们在使用第三方框架时经常能看到 Builder 模式,比如 HttpClient 的:
那么,通过 Builder 注解可以很方便的实现它:
编译时添加 getter、setter、toString、equals 和 hashCode 方法,如:
添加输入输出流 close 方法,如:
我们通过断点调试发现,它会调用 close(),说明 lombok 帮我们生成了 close():
通过 lombok 的注解,可以极大的减少我们的代码量,并且更加清爽,更加优雅。
lombok 还有很多注解,如:
- @ToString: 生成 toString() 方法。
- @EqualsAndHashCode: 生成 equals 和 hashCode 方法。
日志相关注解(当然需要添加相关日志依赖):
- Slf4j、Log4j 等。
- NoArgsConstructor、AllArgsConstructor: 生成无参和全参构造函数。
- Value: 生成 getter、toString、equals、hashCode 和全参构造函数。
- NonNull: 标注在字段和方法上,表示非空,会在第一次使用时判断。
一说到设计模式,我相信大家都能说出那 23 种设计模式,并且还能说出每种设计模式的用法,但是大多数同学应该都没有在实际应用中真正运用过设计模式,还只是停留在理论阶段。
不知道各位是否有过这个感觉,整个应用被相同的代码充斥着,自己也知道这种代码不好,但是不知道怎么做优化,虽然知道有 23 种设计模式,却不知道怎么运用。
本节我就将以实际的例子教大家如何在实际应用中灵活运用所学的设计模式。
(实际应用中,一个场景可能不只包含一个设计模式,很有可能需要多种设计模式配合使用才能写出优雅的高质量的代码。)
我们在做后台管理系统时,会有这样一个需求,根据后台的数据统计导出报表,需要支持 Excel、Word、PPT、PDF 等格式。
对于以上需求,一般做法是:为每一个导出报表的方法提供一个方法,然后在 Service 里判断,如果为 excel,则调用 excel 的方法,如果为 Word 则调用 word 的方法,
如:
这样写本身没有问题,也能实现需求,但是它有以下缺点:
- 1.代码不够优雅,业务方法内存在太多 if – else。
- 2.扩展性不强,每增加一个报表格式,就需要修改业务方法,增加一个 if – else。
我们在开发时需要遵循有一个原则:一个方法做你该做的事。也就是无论增加什么样的报表格式,业务方法 exportReport 的作用依然是导出功能,除非业务需求发生改变,否则不能修改业务方法。
那么,我们该怎么改造呢?
我们发现,导出报表可以导出不同的格式,这些格式我们可以理解为产品,需要由一个地方产出,因此马上就能想到可以利用工厂模式对其进行改造,下面是改造后的代码:
这样就完成了工厂模式对报表导出的改造,在业务方法内,通过 TemplateFactory 创建 template,然后调用 template 的 read 或 write 方法,以后我们每增加一个格式,只需要实现 Template 的相应方法,在 factory 实例化它即可,无需修改业务方法。
此场景用到的设计模式有:简单工厂模式。
这种场景也比较多见,比如:
- 1.我们实现一个注册功能,注册的字段比较多,可能就会分步骤进行,第一步,填写手机号验证码,第二步,填写头像昵称。
- 2.我们发布一篇文章,第一步,填写标题和内容,第二步,设置定时任务,第三步,设置文章打赏规则。
针对这些情况,一般做法也是在业务方法内,做个 if – else 判断,如果是第一步,则执行第一步的业务,如果是第二步,则执行第二步的业务,这种方式同场景 1 一样,代码也比较难看。
对于这样的场景,我们同样可以使用设计模式来实现,因为每一步都是有关联的,执行完第一步,才能执行第二步,执行完第二步才能执行第三步,它很像一条链子将它们联系起来,所以很容易想到可以采用责任链模式。
下面,请看具体的实现:
业务类传入一个 step,通过 HandlerFactory 实例化 handler,通过 handler 就可以执行指定的步骤,同样地,增加一个步骤,业务类无需任何变动。
有些时候,我们会使用多重循环,直接带业务方法里写,看着很不优雅,就像这样:
我们可以将其进行封装改造,将循环细节封装起来,只将一些方法暴露给业务方调用:
上面的 main 方法就是我们业务调用时需要调用的方法,可以看出,我们将循环细节封装到 Lists 里面,使调用方的代码更加优雅。
此场景用到的设计模式有:构建者模式、观察者模式。
在实际应用中,我们看到最多的代码便是 if – else,这样的代码在业务场景中出现太多的话,看着就不太优雅了,前面的场景其实已经多次将 if – else 用设计模式替换,本场景,我将会用新的设计模式来替换讨厌的 if – else,那就是策略模式。
策略模式,通俗点讲,就是根据不同的情况,采取不同的策略,我们把它转化成 if – else,即:
这样,我们就避免了在业务场景中大量地使用 if – else 了。
通过以上的学习,我们其实是可以写出很多优雅的代码,各位在实际中如果有什么问题,或者在实际应用中发现一段代码不知道如何优化也可以再本 chat 的读者圈随时向我提问。
接下来,我将告诉大家一些 Java 编程的小技巧,利用这些技巧,可以避免一些低级 bug,也可以写出一些优雅的代码。
我们在集成一个类时,可能会重写父类方法,大家务必加上 Override 注解,请看下面的代码:
我们的本意是要重写 method,但是参数类型写错了,变成了重载,编译器不会报错,如果我们加上 @Override,编译器会报错,我们就能马上发现代码的错误,而避免运行一段时间导致的 bug。
为什么这么说呢?错误我们马上就能发现,而且如果是编译时错误,都无法运行,但是警告并不影响编译和运行,举个例子:
我的本意是 for 循环用 i,但是却写成了 j,这时编译不会报错,但是 IDE 会给出警告:
它告诉我们i这个变量没有使用到,如果忽略警告,那么很可能运行一段时间出现致命性的 bug,但是如果我们重视警告,当编译器提出这个警告时,我们就会想,i为什么没有用到呢,检查代码,马上就能发现隐藏的 bug。
在开发数据库项目时,经常会有一些譬如状态、类型、性别等具有固定值的字段,一般我们会用数字表示,在业务中,也会经常判断,比如状态为 1 时,执行什么操作,如果直接这样写数字,必须要写注释,否则很难懂。类似这种字段,尽量封装成枚举类型,如:
我们在使用时直接调用枚举,可读性增加了,也利于扩展。
小王是公司的 Android 开发工程师,在开发应用时,封装了一些常量,用于提示语。
架构师在 code review 时发现,变量命名很成问题,如:
架构师要求小王更正,但小王给我的理由是这种编码是产品经理定的,我可以在每个调用者上面加上注释,而保留现状。
很明显,这样的代码是不可取的,如果换成一个可读变量名是不是更清晰呢?比如:
这个原则很好理解,即一个方法只做一件事,如果一个方法做了太多的事,请考虑重构此方法,合理运用类似上面提到的设计模式。
下面对两种代码进行比较:
如果变量放在前面,一旦变量为 null,则会出现空指针异常,但是常量放在前面,则不会出现空指针异常。
各位看到网上经常再说,位运算效率高怎么怎么样的,但事实真的如此吗,我们不妨做个测试:
以上代码,我分别测试了 1 万次,10 万次和 100 万次,得出的结论是 1 万次速度一样,10 万次和 100 万次都只相差 2 毫秒,如今计算机计算性能越来越好,利用位运算和四则运算效率相差太小,而位运算的可读性非常低,除非有详细的注释,否则一般人真看不懂。
因此,尽量少用位运算。当然有些场景是避免不了的,比如:密码生成、图像处理等,但实际应用中,我们很少自己写这类算法。
我们如果要精确计算浮点数,切记不要用 float 和 double,它们的计算结果往往不是你想要的,比如:
计算结果为:
我们要精确计算,需要用 BigDecimal 类,如:
这样就能得出精确的值:
java8 为我们带来了 lambda 表达式,也带来了集合的流式运算,java8 以前,我们循环集合是这样的:
java8 以后,我们可以这样做:
通过集合的流式操作,我们可以很方便的过滤元素、分组、排序等,如:
除了,集合的流式操作,通过 lambda 表示,我们还可以实例化匿名类,如:
可以看出,使用 lambda 表达式,让我们的代码更加简洁,也更加优雅,同学们,请拥抱 lambda 吧!
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。