【C++编译器前端深度解析】:词法与语法分析的2大原理及实战技巧
发布时间: 2024-09-30 22:50:58 阅读量: 38 订阅数: 22
编译原理词法分析与语法分析实验报告
5星 · 资源好评率100%
![【C++编译器前端深度解析】:词法与语法分析的2大原理及实战技巧](https://img-blog.csdnimg.cn/img_convert/666f6b4352e6c58b3b1b13a367136648.png)
# 1. C++编译器前端概述
## 1.1 编译器前端的角色与任务
在软件开发中,编译器扮演着至关重要的角色。编译器前端作为编译器的第一部分,主要负责将源代码转换为中间表示(IR),为后续的优化和代码生成打下基础。它处理源代码,进行词法、语法分析,并生成抽象语法树(AST),这一步骤是构建高效、可靠编译器的基石。
## 1.2 编译过程中的位置和作用
编译器前端位于整个编译流程的初期阶段,在预处理之后,它接收预处理后的源代码并执行以下关键任务:
- **词法分析(Lexical Analysis)**:将源代码文本分解成一系列的词法单元(Token),例如关键字、标识符、操作符等。
- **语法分析(Syntax Analysis)**:基于词法单元构建抽象语法树,确保代码遵循语言的语法规则。
- **语义分析(Semantic Analysis)**:对AST进行类型检查和其他语义检查,确保代码在逻辑上是正确的。
## 1.3 从源代码到中间表示
C++编译器前端的主要目标是将程序员编写的源代码转换成一种更为通用、平台无关的中间表示。这个过程中,前端保证了从源代码到中间代码的映射保持了源代码的语义不变性,为编译器后端的优化和目标代码的生成提供了坚实的基础。在随后的章节中,我们将深入了解前端如何处理和分析源代码,并探讨实现这些任务的具体技术。
# 2. 词法分析的基本原理与实现
## 2.1 词法分析的角色与任务
### 2.1.1 编译过程中的位置和作用
词法分析是编译过程的第一个阶段,紧随编译器的源代码读取阶段之后。它在编译流程中的定位可以形象地比作自然语言处理中的分词过程。词法分析器(Lexer)负责将源代码文本分割成一系列的词法单元(Token),为后续的语法分析阶段做准备。
编译器前端的其它部分,如语法分析器(Parser)和语义分析器(Semantic Analyzer),依赖于这些Token来理解源代码的结构和含义。词法分析器的作用是将字符序列转化为计算机能够理解的离散元素,这一步骤至关重要,因为它奠定了编译过程中后续步骤的基础。
### 2.1.2 词法单元(Token)的识别
词法单元(Token)是编译器处理源代码的基本单元。每个Token都是一个具有特定意义的字符序列,例如关键字、标识符、运算符、字面量等。词法分析器的工作就是识别这些Token,并为它们分类。
识别过程依赖于预定义的词法规则,这些规则定义了什么字符序列构成什么样的Token。例如,一个连续的数字序列可能被识别为整数字面量Token,而“if”这样的字符序列则可能被识别为关键字Token。
## 2.2 正则表达式与词法分析器生成器
### 2.2.1 正则表达式在词法分析中的应用
正则表达式(Regular Expressions)是一种强大的文本处理工具,能够定义复杂的字符序列匹配模式。在词法分析中,正则表达式用于定义每个Token的匹配规则。
例如,一个整数字面量可能通过如下正则表达式进行匹配:
```regex
[0-9]+
```
这里,表达式`[0-9]+`匹配一个或多个数字,对应于一个整数字面量Token。正则表达式为编写词法分析器提供了一种简洁、直观和强有力的方式,极大地简化了Token的定义和识别过程。
### 2.2.2 利用词法分析器生成器自动构建词法分析器
词法分析器生成器如Flex(Fast Lexical Analyzer Generator)可以自动根据正则表达式规则生成词法分析器代码。开发者只需提供正则表达式描述的Token模式,词法分析器生成器便可自动生成相应的C/C++代码,极大地减少了手动编码的复杂性和错误率。
Flex读取定义Token的正则表达式文件,然后生成一个C源文件,其中包含了用于执行实际词法分析的函数。当编译时,这些函数能够将输入的源代码字符串转换为Token流,准备给语法分析器使用。
例如,下面是Flex的一个简单词法规则的片段:
```yaml
%{
#include <stdio.h>
%}
[0-9]+ { printf("NUMBER: %s\n", yytext); }
[ \t\n]+ ; // 忽略空白字符
. { printf("UNKNOWN: %s\n", yytext); }
int main(int argc, char **argv) {
yylex();
return 0;
}
```
在这个例子中,Flex定义了整数字面量(NUMBER),空白字符和未知字符(UNKNOWN)三种Token,并输出它们的类型和内容。
## 2.3 词法分析的实践技巧
### 2.3.1 手动编写词法分析器的方法和技巧
尽管词法分析器生成器如Flex能大大简化开发过程,但手动编写词法分析器在某些情况下仍不可少。手动编写词法分析器通常涉及有限状态自动机(Finite State Machine, FSM)的构建。这种方法提供了对生成Token过程的精细控制,允许开发者处理更复杂的词法规则。
例如,对于一些自定义的或非常规的词法规则,可能需要手动编写代码来精确控制状态转换和Token的生成。手动编写词法分析器的一个典型步骤包括:
1. 确定所有需要识别的Token类型和相应的正则表达式。
2. 构建一个状态转移图来表示从一个状态到另一个状态的转换规则。
3. 实现一个循环,不断读取源代码字符,并根据状态转移图进行状态转换。
4. 识别特定模式并输出相应的Token。
### 2.3.2 词法分析常见问题及解决方案
在编写词法分析器时,开发者可能会遇到一些常见问题:
- 正则表达式冲突:当定义的多个正则表达式模式具有重叠时,某些Token可能会被错误识别。解决这类问题通常需要调整正则表达式的优先级或使用特定的正则表达式技巧来明确区分不同的Token。
- 复杂的词法规则:例如,多行注释或字符串字面量等。对于这些复杂的规则,开发者可能需要编写更复杂的有限状态机或使用特定的编程技术,如递归下降,来处理嵌套或跨行的Token。
- 性能问题:大型源文件可能需要高效的词法分析器。在这种情况下,可以考虑优化状态转移逻辑,减少不必要的回溯,或使用更加高效的编程语言实现词法分析器。
通过理解和解决这些问题,开发者可以构建出更加健壮和高效的词法分析器,进而为编译器前端打下坚实的基础。
# 3. 语法分析的基本原理与实现
## 3.1 语法分析的重要性
### 3.1.1 从词法单元到语法结构
语法分析是编译器前端的核心环节,它将词法分析得到的序列(Token流)转换为更高级的结构——语法结构。在C++等编程语言中,语法结构通常以抽象语法树(AST)的形式来表示。AST是一个树状的数据结构,它准确地捕获了源代码中程序元素之间的层次和关联关系。
例如,在C++中,一条赋值语句 `int a = 3;` 被词法分析后分解为标识符(`int`、`a`)、字面量(`3`)和操作符(`=`)等Token。在语法分析阶段,这些Token将被组织成一个AST节点,节点可能代表声明、赋值或表达式等语法概念。
### 3.1.2 上下文无关文法(CFG)和语法树
为了定义语法结构,编译器使用了上下文无关文法(Context-Free Grammar, CFG)。CFG由一组产生式规则组成,规定了如何从更小的构造组合成更大的构造。在语法树中,每个内部节点通常对应一个产生式规则,而叶子节点则是词法单元。
比如,一个简单的CFG定义可能包含如下规则,用于描述C++中的表达式语法:
```
<expression> ::= <number>
| <expression> + <expression>
| <expression> - <expression>
| <identifier>
```
基于上述规则,我们可以构建出表达式 `1 + 2 - x` 的语法树:
```mermaid
graph TD
A[<expression>] --> B[<expression>]
A --> C[-]
B --> D[1]
B --> E[<expression>]
E --> F[2]
C --> G[<identifier>]
G --> H[x]
```
这个语法树表示了一个表达式由两个子表达式和一个标识符相减组成。
## 3.2 语法分析算法
### 3.2.1 自顶向下分析和LL(k)解析
自顶向下分析是一种语法分析方法,它从根节点开始,逐步向下构建AST。LL(k)解析器是一种常见的自顶向下解析器,其中LL代表从左至右扫描输入,并且使用左递归(Leftmost derivation)进行解析。
LL(k)解析器在解析过程中使用一个k个符号的向前看(lookahead)缓冲区来决定文法的扩展规则。一个LL(1)解析器只向前看一个符号,这通常要求文法是无歧义并且是左递归的。例如,考虑下面的文法:
```
<if-statement> ::= "if" "(" <condition> ")" <statement>
| "if" "(" <condition> ")" <statement> "else" <statement>
```
该文法不是LL(1),因为当解析到"if"时,解析器无法决定使用哪一个产生式规则,除非它知道下一个符号是什么。
### 3.2.2 自底向上分析和LR解析
自底向上分析与自顶向下相反,它从Token开始,逐步向上合并形成更复杂的结构直至根节点。LR解析器是一种自底向上解析器,其中L代表从左至右扫描输入,而R代表右递归(Rightmost derivation in reverse)。
LR解析器使用一个堆栈来存储符号,并根据解析表来决定何时和如何将Token或堆栈中的符号组合成更高层次的语法结构。常见的LR解析器变体包括SLR、LR(1)、LALR等。LR解析器能处理复杂的文法,包括那些LL解析器无法处理的文法。
## 3.3 语法分析的实践技巧
### 3.3.1 工具辅助的语法分析实现
现代编译器前端开发中,手动编写语法分析器的情况相对较少。大多数情况下,开发者会使用如Bison、ANTLR等工具,这些工具可以根据CFG自动生成解析代码。例如,Bison能够根据用户提供的文法定义文件(通常是`.y`或`.yy`扩展名)生成C或C++代码的解析器。
使用工具辅助的另一个好处是它们可以生成调试信息和跟踪信息,这对于测试和维护编译器前端代码非常有帮助。工具还会提供辅助命令和库函数,简化了错误处理和恢复的过程。
### 3.3.2 解析错误的检测与修复
解析错误是语法分析过程中不可避免的,然而优雅地处理这些错误对提升用户体验至关重要。一个好的编译器应该能够尽可能准确地报告错误位置,并提供修复建议。例如,一些工具支持产生错误恢复策略,以便解析器在检测到错误后能够尝试跳过一些Token,寻找一个安全的恢复点。
错误检测通常依赖于解析器的状态以及输入流中的下一个Token。当解析器检测到一个无法与任何产生式匹配的Token时,错误就被报告了。而错误修复策略可能包括插入、删除或替换某些Token,以尝试恢复到一个已知的合法状态。
以Bison为例,错误恢复机制可能看起来像这样:
```c
%error-verbose
%parse-error
%token错误处理的Token标识
%token 正常处理的Token标识
// 文法规则定义部分
```
这里,`%error-verbose`指令让Bison生成更详细的错误消息,而`%parse-error`指令则指示Bison在发现错误时抛出一个异常。
在本章节中,我们详细探讨了语法分析的基础原理和实现方法,包括自顶向下和自底向上分析策略,以及如何使用现代工具来实现语法分析器。同时,我们也提到了实际开发中遇到错误时的处理技巧。接下来,我们将深入编译器前端的实战应用,包括构建一个简单的自定义语言前端,以及如何使用现代编译器前端工具链。
# 4. 编译器前端实战应用
在现代软件开发中,理解并能够操作编译器前端是至关重要的。本章节通过实战的角度探讨编译器前端的应用,重点在于如何利用现代工具开发自定义语言的前端,以及如何从理论过渡到实践来构建一个小型编译器。
## 4.1 现代C++编译器前端工具链
编译器前端负责从源代码到中间表示的转换过程。这一节将介绍工具链中各组件的作用与交互,特别关注Clang和LLVM框架。
### 4.1.1 工具链中各组件的作用与交互
编译器前端工具链通常包括词法分析器、语法分析器、语义分析器、中间代码生成器等组件。它们按照顺序处理源代码,并将源代码转换为中间表示。
- **词法分析器(Lexer)**:将源代码文本分解成一系列的标记(Token)。
- **语法分析器(Parser)**:根据语言的语法规则,将标记组织成抽象语法树(AST)。
- **语义分析器(Semantic Analyzer)**:检查AST中的元素是否符合语言的语义规则,如变量声明和类型检查。
- **中间代码生成器(IR Generator)**:将AST转换为中间表示,如LLVM IR。
这些组件通过定义良好的接口进行交互,以确保源代码逐步转换为编译器能够进行优化和代码生成的形式。
### 4.1.2 Clang和LLVM框架简介
Clang是一个编译器前端,专为C、C++和Objective-C设计,其目的是提供快速、模块化且与语言无关的编译器基础设施。LLVM则是一个更为底层的框架,提供了丰富的中间表示(IR)和优化工具。
- **Clang**:
- 专注于提供高质量的错误信息和快速的编译速度。
- 支持模块化、插件化的设计,允许轻松扩展。
- **LLVM**:
- 包含了广泛的IR工具、优化器和代码生成器。
- 具有高度优化的后端,可以生成高效的目标代码。
Clang和LLVM一起构建了现代C++编译器前端工具链的基础。Clang负责解析C++代码并产生LLVM IR,后者则进行优化和目标代码生成。
## 4.2 实战技巧:自定义语言的前端开发
开发一个自定义语言的编译器前端是深入理解编译器工作原理的好方法。本节将讨论如何设计自定义语言语法和构建相应的词法及语法分析器。
### 4.2.1 设计简单的自定义语言语法
设计一个简单的自定义语言语法,可以基于BNF(巴科斯范式)或EBNF(扩展巴科斯范式)。这里我们以一个简单计算器语言为例:
```
<expr> ::= <expr> + <term>
| <expr> - <term>
| <term>
<term> ::= <term> * <factor>
| <term> / <factor>
| <factor>
<factor> ::= (<expr>)
| <number>
<number> ::= <digit> +
```
这个简单的语法可以处理基本的算术表达式。
### 4.2.2 构建自定义语言的词法和语法分析器
构建自定义语言的词法分析器和语法分析器可以使用词法分析器生成器(如flex)和语法分析器生成器(如bison)。同时,也可以手动编写这些组件来增加灵活性。
**词法分析器(使用flex)**:
```flex
%{
#include "parser.tab.h" // bison生成的头文件,包含了yylex()函数声明
%}
digit [0-9]
number {digit}+
{number} { yylval = atoi(yytext); return NUMBER; }
. { /* 忽略其他字符 */ }
int main(int argc, char **argv) {
yylex();
return 0;
}
```
**语法分析器(使用bison)**:
```bison
%{
#include <stdio.h>
extern int yylex();
void yyerror(const char *s) { fprintf(stderr, "错误: %s\n", s); }
%}
%token NUMBER
lines : lines expr '\n' { printf("结果: %d\n", $2); }
| /* 空 */
;
expr : expr '+' term { $$ = $1 + $3; }
| expr '-' term { $$ = $1 - $3; }
| term { $$ = $1; }
;
term : term '*' factor { $$ = $1 * $3; }
| term '/' factor { $$ = $1 / $3; }
| factor { $$ = $1; }
;
factor : '(' expr ')' { $$ = $2; }
| NUMBER { $$ = $1; }
;
```
这是构建一个简单的自定义语言编译器前端的基本步骤,通过灵活使用工具生成器可以减少工作量并提升开发效率。
## 4.3 从理论到实践:构建小型编译器
实践是检验理论的最佳方式。本节将介绍构建一个小型编译器的环境搭建、工具准备及关键步骤的实现。
### 4.3.1 环境搭建与工具准备
构建小型编译器的环境搭建非常关键。需要安装以下工具:
- 编译器(如GCC或Clang)
- 构建工具(如make)
- 词法分析器生成器(如flex)
- 语法分析器生成器(如bison)
- 文本编辑器或IDE(如VSCode, CLion)
安装完成后,需要准备构建脚本或项目文件,用于自动化编译和链接各部分。
### 4.3.2 编译器前端的关键步骤实现
编译器前端的关键步骤包括解析源代码,构建AST和生成中间代码。这通常涉及以下操作:
1. **预处理**:处理源文件中的预处理指令。
2. **词法分析**:将源代码文本分解为标记。
3. **语法分析**:将标记组织成AST。
4. **语义分析**:在AST上进行类型检查和其他语义分析。
5. **生成中间代码**:将AST转换为中间代码。
例如,使用Clang作为前端,可以调用其库函数来完成这些步骤:
```c++
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
int main(int argc, const char **argv) {
auto ExpectedParser = CommonOptionsParser::create(argc, argv, MyToolCategory);
if (!ExpectedParser) {
llvm::errs() << ExpectedParser.takeError();
return 1;
}
CommonOptionsParser &OptionsParser = ExpectedParser.get();
ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());
return Tool.run(createFrontendActionFactory().get());
}
```
以上代码片段展示了如何使用Clang的库来创建一个简单的编译器前端。
通过理论与实践结合的方式,可以构建并理解一个完整的编译器前端,这是理解编译器工作的基础。下一章将深入探讨编译器前端的优化与挑战。
# 5. 编译器前端的优化与挑战
## 5.1 代码优化与分析技术
### 5.1.1 优化技术在前端的应用场景
在编译器前端中,代码优化技术是一种提高目标代码效率和性能的重要手段。优化技术主要应用于以下场景:
1. **消除冗余代码**:通过识别并移除不必要的代码段,减少目标程序的大小和运行时的资源消耗。
2. **常量折叠**:对程序中的常量表达式进行计算,直接在编译时得出结果,避免运行时计算。
3. **死代码消除**:移除那些永远不会被执行到的代码,通常称为不可达代码。
4. **循环优化**:通过变换算法,减少循环的开销,例如循环展开和循环不变代码外提。
5. **寄存器分配优化**:在目标代码生成时,尽可能高效地利用处理器的寄存器资源。
### 5.1.2 静态分析工具和方法
静态分析是在不执行程序的情况下对源代码进行分析的过程。它有助于识别潜在的代码错误和性能瓶颈。以下是常见的静态分析工具和方法:
- **代码度量**:通过计算代码复杂度和可维护性指标来评估代码质量。
- **数据流分析**:追踪程序中数据的流动,以检测变量的定义和使用。
- **控制流分析**:分析程序的执行流程,以识别循环、条件判断和可能的程序异常。
- **抽象语法树(AST)检查**:利用AST进行深入代码结构的检查,以便于发现逻辑错误。
- **代码风格和规范检查**:确保代码遵循既定的编程风格和规范。
### 5.1.3 代码度量与分析实例
假设我们有一个小型的C++函数,我们想要使用静态分析工具对它的代码复杂度进行评估。首先,我们可以使用一些现成的静态分析工具,如SonarQube或CLOC(Count Lines of Code)。
以下是一个简单的C++函数示例:
```cpp
int add(int a, int b) {
int result = a + b;
return result;
}
```
使用CLOC进行代码行数的统计,我们可能会得到如下结果:
```
***/AlDanial/cloc v 1.86 T=0.0 s (12.0 files/s, 188.0 lines/s)
Language files blank comment code
C++ 1 0 0 3
SUM: 1 0 0 3
```
我们还可以使用更高级的静态分析工具来检测潜在的bug或代码质量问题。
## 5.2 处理复杂性:大型项目的编译前端挑战
### 5.2.1 处理大规模代码库的策略
大型项目往往伴随着大规模的代码库,这会给编译器前端处理带来挑战:
1. **增量编译**:仅重新编译改动过的代码,提高编译效率。
2. **并行编译**:利用多核处理器并行处理多个编译任务,缩短编译时间。
3. **模块化编译**:将代码库划分成独立的模块,每个模块单独编译,并在最后链接。
4. **预编译头文件**:预先编译常用的头文件,避免重复编译,加快编译过程。
### 5.2.2 多语言和模块化编译前端的考量
多语言编译前端需要考虑的是如何统一不同语言的编译和优化策略,而模块化编译前端则需要解决模块间依赖和接口一致性的问题。
- **多语言支持**:编译器前端需要支持多种编程语言,每种语言都有其特定的语法和语义规则。
- **模块接口管理**:为了模块化编译,编译器前端需要有机制管理不同模块之间的依赖关系。
- **跨模块优化**:为了保持优化的连续性,编译器前端可能需要执行跨模块的优化策略。
## 5.3 未来趋势:编译器前端的发展方向
### 5.3.1 新编译技术的探索与应用
编译器前端技术的发展方向主要集中在以下几个方面:
1. **JIT编译技术**:即时编译(Just-In-Time)技术,以提高程序的运行效率。
2. **LLVM优化**:利用LLVM框架进行更高级别的优化,包括跨函数和跨模块的优化。
3. **自动并行化**:自动检测代码中的并行性并生成并行执行的代码。
### 5.3.2 编译器前端在软件开发中的作用演变
随着软件开发实践的变化,编译器前端的角色也在逐渐演变:
- **持续集成/持续部署(CI/CD)**:编译器前端的快速迭代和高兼容性变得尤为重要。
- **自动生成代码**:编译器前端有可能集成更多的代码自动生成工具。
- **集成开发环境(IDE)的交互**:编译器前端与IDE的集成会更加紧密,提供更加智能化的代码提示和错误检查功能。
这些变化都指向了编译器前端向更加强大、更加智能化、更加符合现代软件开发流程的方向发展。随着编程语言和编译技术的不断进步,我们可以预见,编译器前端将继续是软件开发领域的关键组件。
# 6. C++编译器前端的高级特性
## 6.1 C++语言的模板机制与前端处理
### 6.1.1 模板编译原理
模板是C++语言中最为强大也是最复杂的特性之一。模板机制允许程序员编写与数据类型无关的代码,这些代码在编译时根据实际使用情况实例化为具体的代码。模板编译原理涉及到模板的定义、特化以及实例化过程。
在编译器前端处理模板时,主要任务是解析模板定义并进行模板参数的检查。在模板实例化阶段,编译器需要根据模板使用处的具体类型参数,生成相应的代码。在模板编译过程中,编译器需要解决诸多问题,如依赖查找、成员函数的实例化、模板特化选择等。
### 6.1.2 实践中的模板特化和实例化
在实践中,模板的特化和实例化是编译器前端面临的挑战之一。特化是模板功能扩展的一种方式,允许程序员为特定类型或条件提供定制的模板实现。实例化则是将模板应用于特定类型时生成具体函数或类的过程。
编译器前端处理模板实例化时,首先需要检查模板的可用性,然后解析模板定义,并替换所有模板参数,最后生成一个具体的函数或类实例。此过程需要保证类型正确性和语义一致性,确保最终生成的代码是正确的。
## 6.2 C++11/C++14/C++17等新标准的影响
### 6.2.1 新标准对前端的影响
从C++11开始,C++语言引入了大量新特性,这些新特性不仅提高了语言表达力,也给编译器前端带来了新的挑战。新标准中引入了如lambda表达式、auto关键字、移动语义等特性,这些都需要编译器前端进行准确的词法和语法分析。
新标准中的特性往往也意味着在模板处理、类型推导、表达式解析等方面要进行更多的工作。比如,C++11引入的可变参数模板和类型推导规则,要求编译器前端能够处理更加复杂的模板实例化过程。
### 6.2.2 语言特性的前端实现细节
对于新加入的语言特性,编译器前端不仅要能够解析和理解这些特性,还要在内部表示它们,以便后续的语义分析和代码生成阶段可以正确地处理它们。例如,C++17中的结构化绑定特性,编译器前端需要识别这种特定的语法结构,并将其转换为内部的数据结构,以便后端可以生成对应的机器码。
实现这些细节通常需要对编译器前端的各个组件进行调整或增强,确保它们能够处理各种新特性的语法规则,并做出正确的处理。
## 6.3 构建现代C++前端的架构考量
### 6.3.1 代码兼容性与维护性
构建现代C++编译器前端时,代码的兼容性和维护性是两个重要的考量点。随着C++标准的不断演进,新的语言特性和库的加入要求编译器前端必须能够处理越来越多的语法和语义规则。
为了保持良好的兼容性,编译器前端架构设计时,需要考虑到与旧标准的兼容问题。同时,为了维护性考虑,编译器的各个模块应该具有良好的模块化和抽象层次,便于后期的维护和升级。
### 6.3.2 性能与资源消耗的平衡
另一个重要的架构考量是性能与资源消耗之间的平衡。编译器前端在处理复杂的语言特性和大型代码库时,会消耗大量的CPU和内存资源。因此,在设计架构时,需要在性能优化和资源使用之间寻找一个平衡点。
例如,可以采用延迟分析、并行编译等方式来提高编译效率,同时使用内存池等技术来减少内存使用。所有这些考量都需要在设计编译器前端架构时仔细权衡。
0
0