小项目-词法分析器

小项目-词法分析器

1.理论

一个完整的编译器,大致会经历如下几个阶段

各个阶段的职责,简单描述如下:

  1. 词法分析:对源文件进行扫描,将源文件的字符划分为一个一个的记号(token) (注:类似中文中的分词)。

  2. 语法分析:根据语法规则将 Token 序列构造为语法树。

  3. 对语法树的各个结点之间的关系进行检查,检查语义规则是否有被违背,同时对语法树进行必要的优化,此为语义分析。

  4. 遍历语法树的结点,将各结点转化为中间代码,并按特定的顺序拼装起来,此为中间代码生成。

  5. 对中间代码进行优化

  6. 将中间代码转化为目标代码

  7. 对目标代码进行优化,生成最终的目标程序

以上阶段的划分仅仅是逻辑上的划分。实际的编译器中,常常会将几个阶段组合在一起,甚至还可以能省略其中某些阶段。

1.1词法分析

编译器扫描源文件的字符流,过滤掉字符流中的空格、注释等,并将其划分为一个个的 token,生成 token 序列。

例如下面的语句:

a = value + sum(5, 123); 

将被拆分为11个 token :

a           标识符
=           赋值运算符
value       标识符
+           加号
sum         标识符
(           左括号
5           整数
,           逗号
123         整数
)           右括号
;           分号

这个步骤和中文中分词非常相似:

我/喜欢/美丽动人的/茜茜/。  

本质上,词法分析阶段所做的事情就是模式匹配。判断哪些字符属于标识符,哪些字符属于关键字,哪些字符属于整数…

1.2 有限状态机

那么该如何做模式匹配呢?这就要用到有限状态机了 (注:术语都是纸老虎,有限状态机一般都是用 switch + while + if 语句实现的,按我自己的理解为,还未扫描元素的时候,那个目标串字符串(token)符合所有的字符条件,扫描完第一个元素,判断剩余还满足什么字符串的条件,再扫描,直到可以确定出目标字符串是什么类型的字符串为止)。

  • 单字符 Token,可以直接识别: ; ) ( { } 等
  • 双字符 Token,需要用 if 语句进行判断:+=, -=, *=, ==, !=
  • 多字符 Token,需要用 while 语句一直读取到结束标志符: 标识符,字符串,数字,字符等。

接下来我们重点看一下:如何判断一个Token到底是标识符还是关键字。这里我们采用Trie树的方式进行判断,因为不管是从空间上还是时间上,Trie树的方式都优于哈希表的方式。在逻辑上,我们可以将 C 语言的关键字组织成下面这样的形式:

具体实现的示例:

switch (first) {
case 'b': return checkKeyword(1, 4, "reak", TOKEN_BREAK);
case 'c': {char second = scanner.start[1];switch (second) {case 'a': return checkKeyword(2, 2, "se", TOKEN_CASE);case 'h': return checkKeyword(2, 2, "ar", TOKEN_CHAR);case 'o': {if (scanner.start[3] == 's') {return checkKeyword(3, 2, "st", TOKEN_CONST);}else {return checkKeyword(3, 5, "tinue", TOKEN_CONTINUE);}}}
}
case 'd': {char second = scanner.start[1];if (second == 'e') {return checkKeyword(2, 6, "efault", TOKEN_DEFAULT);}else {if (scanner.start[2] == 'u') {return checkKeyword(3, 4, "uble", TOKEN_DOUBLE);}else {return checkKeyword(0, 2, "do", TOKEN_DO);}}
}default:break;
} 

2.效果

该词法分析器既能交互式地运行,也能够处理‘.c’文件

交互方式效果为:

交互式运行结果

对‘.c’文件进行词法分析

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string.h>int test(void) {fprintf(stderr, "error: xxx.\n");char dest[1024];int a = 10;double b = 0.2;char *str = "hello!";sprintf(dest, "a = %d, b = %.2f, str = %s", a, b, str);puts(dest);FILE *fp = fopen("1.txt", "w");if (fp == NULL) {fprintf(stderr, "failed to open file.\n");exit(-1);}fprintf(fp, "str = %s, a = %d, b = %.2f", str, a, b);fclose(fp);FILE *fp2 = fopen("1.txt", "r");if (fp2 == NULL) {fprintf(stderr, "failed to open file.\n");exit(-1);}int var1;double var2;char var3[1024];fscanf(fp2, "str = %[^,], a = %d, b = %lf", var3, &var1, &var2);// 格式的读字符串destint num1;double num2;char str2[1024];sscanf(dest, "a = %d, b = %lf, str = %s", &num1, &num2, str2);return 0;
}

效果为:

处理文件

3.代码框架

main.c的代码框架:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>#include "scanner.h"
#include "tools.h"// main.c中的核心逻辑
static void run(const char *source) {initScanner(source);	// 初始化词法分析器int line = -1;  // 用于记录当前处理的行号,-1表示还未开始解析for (;;) {  Token token = scanToken();  // 获取下一个TOKENif (token.line != line) {		// 如果Token中记录行和现在的lin不同就执行换行打印的效果		printf("%4d ", token.line); line = token.line; }else {		// 没有换行的打印效果printf("   | ");}char *str = convert_to_str(token);printf("%s '%.*s'\n", str, token.length, token.start);if (token.type == TOKEN_EOF) {break;	// 读到TOKEN_EOF结束循环}}
}
// repl是"read evaluate print loop"的缩写
// repl 函数定义了一个交互式的读取-求值-打印循环(REPL)逻辑
// 它允许用户输入源代码行,逐行进行词法分析,并打印分析结果
// 也就是说启动时没有主动给出一个命令行参数表示文件路径的话,那么就进行交互式界面进行词法分析
static void repl() {// TODO// 这里应该存在一个死循环,而且要逐行的读取键盘输入fgets
}// 用户输入文件名,将整个文件的内容读入内存,并在末尾添加'\0'
// 注意: 这里应该使用动态内存分配,因此应该事先确定文件的大小。
static char *readFile(const char *path) {// TODO
}// 该函数表示对传入的文件路径名的字符串进行处理
static void runFile(const char *path) {// 处理'.c'文件:用户输入文件名,分析整个文件,并将结果输出// 这个代码非常简单,我帮你直接写好// 会调用上面的readFile函数,根据文件路径名生成一个包含文件全部字符信息的字符串char *source = readFile(path);// 调用run函数处理源文件生成的字符串run(source);// 及时释放资源free(source);
}
/*
* 主函数支持操作系统传递命令行参数
* 然后通过判断参数的个数:
* 1.如果没有主动传入参数(argc=1),因为第一个参数总会传入一个当前可执行文件的目录作为命令行参数
* 此时执行repl函数
* 2.如果传递了一个参数(argc=2),说明传递了一个参数,将传递的参数视为某个源代码的路径
* 然后调用runFile函数,传入该源代码文件的路径,处理源文件
*/
int main(int argc, const char *argv[]) {if (argc == 1) {repl();}else if (argc == 2) {runFile(argv[1]);}else {// 如果主动传入超过一个命令行参数.即参数传递有误,错误处理// 告诉用户正确的使用函数的方式fprintf(stderr, "Usage: scanner [path]\n");exit(1);}return 0;
}

scanner.h的代码框架,这里面主要是一些结构体的定义

#ifndef SCANNER_H
#define SCANNER_H// 定义一个TokenType枚举,用于标记不同种类的Token
typedef enum {/* 单字符 Token */TOKEN_LEFT_PAREN,        // '(' 左小括号TOKEN_RIGHT_PAREN,       // ')' 右小括号TOKEN_LEFT_BRACKET,      // '[' 左中括号TOKEN_RIGHT_BRACKET,     // ']' 右中括号TOKEN_LEFT_BRACE,        // '{' 左大括号TOKEN_RIGHT_BRACE,       // '}' 右大括号TOKEN_COMMA,             // ',' 逗号TOKEN_DOT,               // '.' 点TOKEN_SEMICOLON,         // ';' 分号TOKEN_TILDE,             // '~' 波浪号/* 可能是单字符或双字符的Token */TOKEN_PLUS,                  // '+' 加号TOKEN_PLUS_PLUS,             // '++' 自增运算符TOKEN_PLUS_EQUAL,            // '+=' 加赋运算符TOKEN_MINUS,                 // '-' 减号或负号TOKEN_MINUS_MINUS,           // '--' 自减运算符TOKEN_MINUS_EQUAL,           // '-=' 减赋运算符TOKEN_MINUS_GREATER,         // '->' 结构体指针访问TOKEN_STAR,                  // '*' 乘号TOKEN_STAR_EQUAL,            // '*=' 乘赋运算符TOKEN_SLASH,                 // '/' 除号TOKEN_SLASH_EQUAL,           // '/=' 除赋运算符TOKEN_PERCENT,               // '%' 取模运算符TOKEN_PERCENT_EQUAL,         // '%=' 取模赋运算符TOKEN_AMPER,                 // '&' 按位与运算符TOKEN_AMPER_EQUAL,           // '&=' 按位与赋运算符TOKEN_AMPER_AMPER,           // '&&' 逻辑与运算符TOKEN_PIPE,                  // '|' 按位或运算符TOKEN_PIPE_EQUAL,            // '|=' 按位或赋运算符TOKEN_PIPE_PIPE,             // '||' 逻辑或运算符TOKEN_HAT,                   // '^' 按位异或运算符TOKEN_HAT_EQUAL,             // '^=' 按位异或赋运算符TOKEN_EQUAL,                 // '=' 赋值运算符TOKEN_EQUAL_EQUAL,           // '==' 等于比较运算符TOKEN_BANG,                  // '!' 逻辑非运算符TOKEN_BANG_EQUAL,            // '!=' 不等于比较运算符TOKEN_LESS,                  // '<' 小于比较运算符TOKEN_LESS_EQUAL,            // '<=' 小于等于比较运算符TOKEN_LESS_LESS,             // '<<' 左移运算符TOKEN_GREATER,               // '>' 大于比较运算符TOKEN_GREATER_EQUAL,         // '>=' 大于等于比较运算符TOKEN_GREATER_GREATER,       // '>>' 右移运算符// 所有的三字符Token都去掉了 >>= <<= 实现它们也没什么特殊的,但会很无聊繁琐,所以就都去掉了/* 多字节Token: 标识符、字符、字符串、数字 */TOKEN_IDENTIFIER,            // 标识符TOKEN_CHARACTER,             // 字符TOKEN_STRING,                // 字符串TOKEN_NUMBER,                // 数字,包含整数和浮点数/* 关键字Token 涉及C99所有关键字 */TOKEN_SIGNED, TOKEN_UNSIGNED,TOKEN_CHAR, TOKEN_SHORT, TOKEN_INT, TOKEN_LONG,TOKEN_FLOAT, TOKEN_DOUBLE,TOKEN_STRUCT, TOKEN_UNION, TOKEN_ENUM, TOKEN_VOID,TOKEN_IF, TOKEN_ELSE, TOKEN_SWITCH, TOKEN_CASE, TOKEN_DEFAULT,TOKEN_WHILE, TOKEN_DO, TOKEN_FOR,TOKEN_BREAK, TOKEN_CONTINUE, TOKEN_RETURN, TOKEN_GOTO,TOKEN_CONST, TOKEN_SIZEOF, TOKEN_TYPEDEF,// 注意:#define #include这样的预处理指令 不是关键字// 辅助Token// 词法分析阶段也是可以检测出一些错误的 比如$只能在字符和字符串中 比如字符串"acb 缺少右边双引号// 词法分析阶段不进行错误处理,只是将错误的Token信息抛出,以待后续统一进行处理// 流水线架构每个阶段都可能出错,如果每个阶段都进行错误处理,那代码的可维护性就太差了TOKEN_ERROR,                 // 错误Token 词法分析时遇到无法识别的文本TOKEN_EOF                    // 文件结束Token 表示源代码已经分析完毕
} TokenType;// 词法分析器的目的就是生产一个一个的Token对象 
typedef struct {TokenType type;		// Token的类型, 取任一枚举值// Token的起始字符指针const char *start;	// start指向source中的字符,source为读入的源代码。int length;		    // length表示这个Token的长度int line;		    // line表示这个Token在源代码的哪一行, 方便后面的报错和描述Token
} Token;	// 这个Token只涉及一个字符指针指向源代码的字符信息,没有在内部保存字符数据// 对 词法分析器Scanner 进行初始化 
void initScanner(const char *source);	// 源代码字符串(这里涉及一个将源码转换成字符串的函数)
// 核心API, 调用scanToken(), 就生产一个Token, 也就是源代码中下一段字符数据的Token
Token scanToken();	// 当Token返回的是TOKEN_EOF时,源文件被消耗完毕,词法分析结束#endif  // !SCANNER_H

scanner.c的代码框架,词法分析器的实现细节

#define _CRT_SECURE_NO_WARNINGS#include <stdbool.h>
#include <string.h>
#include <stdio.h>#include "scanner.h"typedef struct {const char *start;		// 指向当前正在扫描的Token的起始字符const char *current;	// 词法分析器当前正在处理的Token的字符,一开始它就是start开始,直到遍历完整个Token,指向该Token的下一个字符int line;		// 记录当前Token所处的行
} Scanner;// 全局变量结构体对象
static Scanner scanner;void initScanner(const char *source) {// 初始化全局变量Scannerscanner.start = source;scanner.current = source;scanner.line = 1;
}// 下面我给大家提供了很多会用到的辅助函数,建议使用
// 判断当前字符是不是字母或者下划线
static bool isAlpha(char c) {return (c >= 'a' && c <= 'z') ||(c >= 'A' && c <= 'Z') ||c == '_';
}
// 判断当前字符是不是数字
static bool isDigit(char c) {return c >= '0' && c <= '9';
}
// 判断Scanner当前正在处理的字符是不是空字符,判断是不是处理完了
static bool isAtEnd() {return *scanner.current == '\0';
}// curr指针前进一个字符,并返回之前curr指针指向的元素
static char advance() {return *scanner.current++;
}
// 查看当前正在处理的字符是什么,curr不动
static char peek() {return *scanner.current;
}
// 如果当前正在处理的字符不是空字符,那就瞥一眼下一个字符是什么,curr不动
static char peekNext() {if (isAtEnd()) {return '\0';}return *(scanner.current + 1);
}// 判断当前正在处理的字符是不是符合预期,如果符合curr前进一位
static bool match(char expected) {if (isAtEnd()) {return false;	// 如果正在处理的是空字符,那就返回false}if (peek() != expected) {return false;	// 如果不符合预期,也返回false}scanner.current++;return true;	// 只有符合预期才会返回true 而且curr会前进一位
}// 根据传入的TokenType类型来制造返回一个Token
static Token makeToken(TokenType type) {Token token;token.type = type;token.start = scanner.start;token.length = (int)(scanner.current - scanner.start);	// 计算Token字符串的长度token.line = scanner.line;return token;
}// 遇到不能解析的情况时,我们创建一个ERROR Token. 比如:遇到@,$等符号时,比如字符串,字符没有对应的右引号时。
static Token errorToken(const char *message) {Token token;token.type = TOKEN_ERROR;token.start = message;token.length = (int)strlen(message);token.line = scanner.line;return token;
}static void skipWhitespace() {// 跳过空白字符: ' ', '\r', '\t', '\n'和注释// 注释以'//'开头, 一直到行尾// 注意更新scanner.line!// 提示: 整个过程需要跳过中间的很多字符,所以需要死循环
}// 用于检查当前扫描的Token的类型是不是type 如果是就返回type
static TokenType checkKeyword(int start, int length, const char *rest, TokenType type) {/*start: 待检查序列的起始字符下标比如要检查关键字break,那么在case b的前提下,需要传入reak来进行检查这里start就等于1,scanner.start[1]length: 待检查序列的长度,如果检查的是break,就是检查剩余的reak需要传入4rest指针,待检查的剩余序列字符串,这里直接传入一个字面值字符串就行了比如检查break,传入"reak"就好了type:你要检查的关键字Token的类型,比如检查break,那就传入Token_BREAK*/if (scanner.current - scanner.start == start + length &&/*int memcmp(const void *s1, const void *s2, size_t n);这里的参数分别是:s1:指向第一块内存区域的指针。s2:指向第二块内存区域的指针。n:要比较的字节数。memcmp 函数会逐字节比较 s1 和 s2 指向的内存区域,直到有不相等的字节或比较了 n 个字节为止。如果两个内存区域完全相同,则 memcmp 返回 0;如果第一个不同的字节在 s1 中的值小于 s2 中对应的值,返回负数;反之,返回正数。*/memcmp(scanner.start + start, rest, length) == 0) {return type;}return TOKEN_IDENTIFIER;
}static TokenType identifierType() {// 确定identifier类型主要有两种方式:// 1. 将所有的关键字放入哈希表中,然后查表确认 // Key-Value 就是"关键字-TokenType" 可以做 但存在额外内存占用且效率不如下一个方式好// 2. 将所有的关键字放入Trie树(字典查找树)中,然后查表确认// Trie树的方式不管是空间上还是时间上都优于哈希表的方式// 用switch...switch...if组合构建逻辑上的trie树char first = scanner.start[0];	// 该Token的第一个字符switch (first) {case 'b': return checkKeyword(1, 4, "reak", TOKEN_BREAK);case 'c':}// 没有进switch一定是标识符return TOKEN_IDENTIFIER;
}// 当前Token的开头是下划线或字母判断它是不是标识符Token
static Token identifier() {// 判断curr指针当前正在处理的字符是不是 字母 下划线 数字while (isAlpha(peek()) || isDigit(peek())) {advance();  // 继续前进看下一个字符 直到碰到下一个字符不是字母 下划线 以及数字 结束Token}// 当while循环结束时,curr指针指向的是该Token字符串的下一个字符// 这个函数的意思是: 只要读到字母或下划线开头的Token我们就进入标识符模式// 然后一直找到此Token的末尾// 但代码运行到这里还不确定Token是标识符还是关键字, 因为它可能是break, var, goto, max_val...// 于是执行identifierType()函数,它是用来确定Token类型的return makeToken(identifierType());
}static Token number() {// 简单起见,我们将NUMBER的规则定义如下:// 1. NUMBER可以包含数字和最多一个'.'号// 2. '.'号前面要有数字// 3. '.'号后面也要有数字// 这些都是合法的NUMBER: 123, 3.14// 这些都是不合法的NUMBER: 123., .14(虽然在C语言中合法)// 提示: 这个过程需要不断的跳过中间的数字包括小数点,所以也需要循环
}static Token string() {// 字符串以"开头,以"结尾,而且不能跨行// 为了简化工作量,简化了字符串// 如果下一个字符不是末尾也不是双引号,全部跳过(curr可以记录长度,不用担心)
}static Token character() {// 字符'开头,以'结尾,而且不能跨行// 如果下一个字符不是末尾也不是单引号,全部跳过(curr可以记录长度,不用担心)// 这两个函数不能说一模一样,也是几乎一样
}// 处理无法识别的字符
static Token errorTokenWithChar(char character) {char message[50];// 将无法识别的字符是什么输出sprintf(message, "Unexpected character: %c", character);return errorToken(message);
}// Scanner核心逻辑,用于返回一个制作好的Token
Token scanToken() {// 跳过前置空白字符和注释skipWhitespace();// 记录下一个Token的起始位置scanner.start = scanner.current;// 如果已经处理完毕了 直接返回TOKEN_EOFif (isAtEnd()) {return makeToken(TOKEN_EOF);}// curr指针现在指向Token的第二个字符,但这个字符c仍然是第一个字符的值char c = advance();// 如果Token的第一个字符是字母和下划线就进入标识符的处理模式if (isAlpha(c)) {return identifier();}// 如果Token的第一个字符是数字,那就进入数字的处理模式if (isDigit(c)) {return number();}// 如果Token的第一个字符既不是数字也不是字母和下划线,那么就switch处理它switch (c) {// 第一部分: 处理单字符Tokencase '(': return makeToken(TOKEN_LEFT_PAREN);case ')': return makeToken(TOKEN_RIGHT_PAREN);// ...TODO// 可单可双字符的Token处理会稍微复杂一点,但不多// 如果Token的第一个字符是+号case '+':// 如果Token的第二个字符也是+,那就生产++双字符Token返回if (match('+')) {return makeToken(TOKEN_PLUS_PLUS);}else if (match('=')) {return makeToken(TOKEN_PLUS_EQUAL);}// ...TODOcase '"': return string(); // 如果Token的第一个字符是双引号,那就解析出字符串Token返回case '\'': return character();	// 如果Token的第一个字符是单引号,那就解析出字符Token返回}// 如果上述处理都没有处理成功,都没有生成合适的Token,说明该字符无法识别return errorTokenWithChar(c);
}

tool.h 与 tool.c 将枚举的int类型转换为char*类型的辅助函数

//tool.h
#include "scanner.h"
char *convert_to_str(Token token);
//tool.c
#include "tools.h"char *convert_to_str(Token token) {switch (token.type) {case TOKEN_LEFT_PAREN: return "LEFT_PAREN";case TOKEN_RIGHT_PAREN: return "RIGHT_PAREN";case TOKEN_LEFT_BRACKET: return "LEFT_BRACKET";case TOKEN_RIGHT_BRACKET: return "RIGHT_BRACKET";case TOKEN_LEFT_BRACE: return "LEFT_BRACE";case TOKEN_RIGHT_BRACE: return "RIGHT_BRACE";case TOKEN_COMMA: return "COMMA";case TOKEN_DOT: return "DOT";case TOKEN_SEMICOLON: return "SEMICOLON";case TOKEN_TILDE: return "TILDE";case TOKEN_PLUS: return "PLUS";case TOKEN_PLUS_PLUS: return "PLUS_PLUS";case TOKEN_PLUS_EQUAL: return "PLUS_EQUAL";case TOKEN_MINUS: return "MINUS";case TOKEN_MINUS_MINUS: return "MINUS_MINUS";case TOKEN_MINUS_EQUAL: return "MINUS_EQUAL";case TOKEN_MINUS_GREATER: return "MINUS_GREATER";case TOKEN_STAR: return "STAR";case TOKEN_STAR_EQUAL: return "STAR_EQUAL";case TOKEN_SLASH: return "SLASH";case TOKEN_SLASH_EQUAL: return "SLASH_EQUAL";case TOKEN_PERCENT: return "PERCENT";case TOKEN_PERCENT_EQUAL: return "PERCENT_EQUAL";case TOKEN_AMPER: return "AMPER";case TOKEN_AMPER_EQUAL: return "AMPER_EQUAL";case TOKEN_AMPER_AMPER: return "AMPER_AMPER";case TOKEN_PIPE: return "PIPE";case TOKEN_PIPE_EQUAL: return "PIPE_EQUAL";case TOKEN_PIPE_PIPE: return "PIPE_PIPE";case TOKEN_HAT: return "HAT";case TOKEN_HAT_EQUAL: return "HAT_EQUAL";case TOKEN_EQUAL: return "EQUAL";case TOKEN_EQUAL_EQUAL: return "EQUAL_EQUAL";case TOKEN_BANG: return "BANG";case TOKEN_BANG_EQUAL: return "BANG_EQUAL";case TOKEN_LESS: return "LESS";case TOKEN_LESS_EQUAL: return "LESS_EQUAL";case TOKEN_LESS_LESS: return "LESS_LESS";case TOKEN_GREATER: return "GREATER";case TOKEN_GREATER_EQUAL: return "GREATER_EQUAL";case TOKEN_GREATER_GREATER: return "GREATER_GREATER";case TOKEN_IDENTIFIER: return "IDENTIFIER";case TOKEN_CHARACTER: return "CHARACTER";case TOKEN_STRING: return "STRING";case TOKEN_NUMBER: return "NUMBER";case TOKEN_SIGNED: return "SIGNED";case TOKEN_UNSIGNED: return "UNSIGNED";case TOKEN_CHAR: return "CHAR";case TOKEN_SHORT: return "SHORT";case TOKEN_INT: return "INT";case TOKEN_LONG: return "LONG";case TOKEN_FLOAT: return "FLOAT";case TOKEN_DOUBLE: return "DOUBLE";case TOKEN_STRUCT: return "STRUCT";case TOKEN_UNION: return "UNION";case TOKEN_ENUM: return "ENUM";case TOKEN_VOID: return "VOID";case TOKEN_IF: return "IF";case TOKEN_ELSE: return "ELSE";case TOKEN_SWITCH: return "SWITCH";case TOKEN_CASE: return "CASE";case TOKEN_DEFAULT: return "DEFAULT";case TOKEN_WHILE: return "WHILE";case TOKEN_DO: return "DO";case TOKEN_FOR: return "FOR";case TOKEN_BREAK: return "BREAK";case TOKEN_CONTINUE: return "CONTINUE";case TOKEN_RETURN: return "RETURN";case TOKEN_GOTO: return "GOTO";case TOKEN_CONST: return "CONST";case TOKEN_SIZEOF: return "SIZEOF";case TOKEN_TYPEDEF: return "TYPEDEF";case TOKEN_ERROR: return "ERROR";case TOKEN_EOF: return "EOF";default: return "UNKNOWN";}
}

4.实现词法分析器

main.c实现

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include "scanner.h"
#include "tools.h"// main.c中的核心逻辑
static void run(const char *source) {initScanner(source);	// 初始化词法分析器int line = -1;  // 用于记录当前处理的行号,-1表示还未开始解析for (;;) {  Token token = scanToken();  // 获取下一个TOKENif (token.line != line) {		// 如果Token中记录行和现在的lin不同就执行换行打印的效果		printf("%4d ", token.line); line = token.line; }else {		// 没有换行的打印效果printf("   | ");}char *str = convert_to_str(token);printf("%s '%.*s'\n", str, token.length, token.start);if (token.type == TOKEN_EOF) {break;	// 读到TOKEN_EOF结束循环}}
}
// repl是"read evaluate print loop"的缩写
// repl 函数定义了一个交互式的读取-求值-打印循环(REPL)逻辑
// 它允许用户输入源代码行,逐行进行词法分析,并打印分析结果
// 也就是说启动时没有主动给出一个命令行参数表示文件路径的话,那么就进行交互式界面进行词法分析
static void repl() {// TODO// 这里应该存在一个死循环,而且要逐行的读取键盘输入fgetsprintf("> ");char get_char[1024] = { 0 };while (fgets(get_char, 1024, stdin)) {run(get_char);printf("> ");}
}// 用户输入文件名,将整个文件的内容读入内存,并在末尾添加'\0'
// 注意: 这里应该使用动态内存分配,因此应该事先确定文件的大小。
static char *readFile(const char *path) {FILE* fp = fopen(path, "rb");if (fp == NULL) {fprintf(stderr, "无法打开文件%s", path);exit(-1);}fseek(fp, 0, SEEK_END);int len = ftell(fp);rewind(fp);//计算出文件长度,并且将指针回退到文件开头char* file = calloc(len+1, sizeof(char));if (file == NULL) {exit(-1);}fread(file, sizeof(char), len, fp);fclose(fp);return file;
}// 该函数表示对传入的文件路径名的字符串进行处理
static void runFile(const char *path) {// 处理'.c'文件:用户输入文件名,分析整个文件,并将结果输出// 这个代码非常简单,我帮你直接写好// 会调用上面的readFile函数,根据文件路径名生成一个包含文件全部字符信息的字符串char *source = readFile(path);// 调用run函数处理源文件生成的字符串run(source);// 及时释放资源free(source);
}
/*
* 主函数支持操作系统传递命令行参数
* 然后通过判断参数的个数:
* 1.如果没有主动传入参数(argc=1),因为第一个参数总会传入一个当前可执行文件的目录作为命令行参数
* 此时执行repl函数
* 2.如果传递了一个参数(argc=2),说明传递了一个参数,将传递的参数视为某个源代码的路径
* 然后调用runFile函数,传入该源代码文件的路径,处理源文件
*/
int main(int argc, const char *argv[]) {if (argc == 1) {repl();}else if (argc == 2) {runFile(argv[1]);}else {// 如果主动传入超过一个命令行参数.即参数传递有误,错误处理// 告诉用户正确的使用函数的方式fprintf(stderr, "Usage: scanner [path]\n");exit(1);}return 0;
}

scanner.c实现

#define _CRT_SECURE_NO_WARNINGS#include <stdbool.h>
#include <string.h>
#include <stdio.h>#include "scanner.h"typedef struct {const char* start;		// 指向当前正在扫描的Token的起始字符const char* current;	// 词法分析器当前正在处理的Token的字符,一开始它就是start开始,直到遍历完整个Token,指向该Token的下一个字符int line;		// 记录当前Token所处的行
} Scanner;// 全局变量结构体对象
static Scanner scanner;void initScanner(const char* source) {// 初始化全局变量Scannerscanner.start = source;scanner.current = source;scanner.line = 1;
}// 下面我给大家提供了很多会用到的辅助函数,建议使用
// 判断当前字符是不是字母或者下划线
static bool isAlpha(char c) {return (c >= 'a' && c <= 'z') ||(c >= 'A' && c <= 'Z') ||c == '_';
}
// 判断当前字符是不是数字
static bool isDigit(char c) {return c >= '0' && c <= '9';
}
// 判断Scanner当前正在处理的字符是不是空字符,判断是不是处理完了
static bool isAtEnd() {return *scanner.current == '\0';
}// curr指针前进一个字符,并返回之前curr指针指向的元素
static char advance() {return *scanner.current++;
}
// 查看当前正在处理的字符是什么,curr不动
static char peek() {return *scanner.current;
}
// 如果当前正在处理的字符不是空字符,那就瞥一眼下一个字符是什么,curr不动
static char peekNext() {if (isAtEnd()) {return '\0';}return *(scanner.current + 1);
}// 判断当前正在处理的字符是不是符合预期,如果符合curr前进一位
static bool match(char expected) {if (isAtEnd()) {return false;	// 如果正在处理的是空字符,那就返回false}if (peek() != expected) {return false;	// 如果不符合预期,也返回false}scanner.current++;return true;	// 只有符合预期才会返回true 而且curr会前进一位
}// 根据传入的TokenType类型来制造返回一个Token
static Token makeToken(TokenType type) {Token token;token.type = type;token.start = scanner.start;token.length = (int)(scanner.current - scanner.start);	// 计算Token字符串的长度token.line = scanner.line;return token;
}// 遇到不能解析的情况时,我们创建一个ERROR Token. 比如:遇到@,$等符号时,比如字符串,字符没有对应的右引号时。
static Token errorToken(const char* message) {Token token;token.type = TOKEN_ERROR;token.start = message;token.length = (int)strlen(message);token.line = scanner.line;printf("");//不写这句打印的第一行会出现问题,如果有大佬可以解决,也希望可以告诉一下我如何解决//fflush(stdout);//int a;return token;
}static void skipWhitespace() {// 跳过空白字符: ' ', '\r', '\t', '\n'和注释// 注释以'//'开头, 一直到行尾// 注意更新scanner.line!// 提示: 整个过程需要跳过中间的很多字符,所以需要死循环while (1) {char ch = peek();switch (ch) {case '/':if(peekNext()=='/') { while (advance() != '\n'); scanner.line++;}break;case ' ': advance(); break;case '\r':advance(); break;case '\t':advance(); break;case '\n':scanner.line++; advance(); break;default: scanner.start = scanner.current; return;}}}// 用于检查当前扫描的Token的类型是不是type 如果是就返回type
static TokenType checkKeyword(int start, int length, const char* rest, TokenType type) {/*start: 待检查序列的起始字符下标比如要检查关键字break,那么在case b的前提下,需要传入reak来进行检查这里start就等于1,scanner.start[1]length: 待检查序列的长度,如果检查的是break,就是检查剩余的reak需要传入4rest指针,待检查的剩余序列字符串,这里直接传入一个字面值字符串就行了比如检查break,传入"reak"就好了type:你要检查的关键字Token的类型,比如检查break,那就传入Token_BREAK*/if (scanner.current - scanner.start == start + length &&/*int memcmp(const void *s1, const void *s2, size_t n);这里的参数分别是:s1:指向第一块内存区域的指针。s2:指向第二块内存区域的指针。n:要比较的字节数。memcmp 函数会逐字节比较 s1 和 s2 指向的内存区域,直到有不相等的字节或比较了 n 个字节为止。如果两个内存区域完全相同,则 memcmp 返回 0;如果第一个不同的字节在 s1 中的值小于 s2 中对应的值,返回负数;反之,返回正数。*/memcmp(scanner.start + start, rest, length) == 0) {return type;}return TOKEN_IDENTIFIER;
}static TokenType identifierType() {// 确定identifier类型主要有两种方式:// 1. 将所有的关键字放入哈希表中,然后查表确认 // Key-Value 就是"关键字-TokenType" 可以做 但存在额外内存占用且效率不如下一个方式好// 2. 将所有的关键字放入Trie树(字典查找树)中,然后查表确认// Trie树的方式不管是空间上还是时间上都优于哈希表的方式// 用switch...switch...if组合构建逻辑上的trie树char first = scanner.start[0];	// 该Token的第一个字符switch (first) {case 'b': return checkKeyword(1, 4, "reak", TOKEN_BREAK);case 'c': {char second = scanner.start[1];switch (second) {case 'a': return checkKeyword(2, 2, "se", TOKEN_CASE);case 'h': return checkKeyword(2, 2, "ar", TOKEN_CHAR);case 'o': {if (scanner.start[3] == 's') {return checkKeyword(3, 2, "st", TOKEN_CONST);}else {return checkKeyword(3, 5, "tinue", TOKEN_CONTINUE);}}}}case 'd': {char second = scanner.start[1];if (second == 'e') {return checkKeyword(2, 6, "efault", TOKEN_DEFAULT);}else {if (scanner.start[2] == 'u') {return checkKeyword(3, 4, "uble", TOKEN_DOUBLE);}else {return checkKeyword(0, 2, "do", TOKEN_DO);}}}case 'e': {char second = scanner.start[1];switch (second){case 'l': return checkKeyword(1, 3, "lse", TOKEN_ELSE);case 'n':return checkKeyword(1, 3, "num", TOKEN_ENUM);default:break;}}case 'f': {char second = scanner.start[1];if (second == 'l') {return checkKeyword(1, 4, "loat", TOKEN_FLOAT);}else if (second == 'o') {return checkKeyword(1, 2, "or", TOKEN_FOR);}}case 'g': return checkKeyword(1, 3, "oto", TOKEN_GOTO);case 'i': {char second = scanner.start[1];if (second == 'f') {return checkKeyword(0, 2, "if", TOKEN_IF);}else if (second = 'n') {return checkKeyword(1, 2, "nt", TOKEN_INT);}}case 'l':return checkKeyword(1, 3, 'ONG', TOKEN_LONG);case 'r':return checkKeyword(1, 5, "eturn", TOKEN_RETURN);case 's': {char second = scanner.start[1];switch (second){case 'h':return checkKeyword(1, 4, "hort", TOKEN_SHORT);case 'i': {char third = scanner.start[2];if (third == 'g') return checkKeyword(2, 4, 'gned', TOKEN_SIGNED);else if (third == 'z') return checkKeyword(2, 4, 'ZEOF', TOKEN_SIZEOF);}//case 't':return checkKeyword(1,5,"tatic",TOKEN_STATIC )case 't': return checkKeyword(1, 5, "truct", TOKEN_STRUCT);case 'w':return checkKeyword(1, 5, "witch", TOKEN_SWITCH);default:break;}}case 't': return checkKeyword(1, 6, "ypedef", TOKEN_TYPEDEF);case 'u': {char third = scanner.start[2];if (third == 'i') return checkKeyword(2, 3, "ion", TOKEN_UNION);else if (third == 's')return checkKeyword(2, 6, "signed", TOKEN_UNSIGNED);}case 'v': return checkKeyword(1, 3, "oid", TOKEN_VOID);case 'w':return checkKeyword(1, 4, "hile", TOKEN_WHILE);}// 没有进switch一定是标识符return TOKEN_IDENTIFIER;
}// 当前Token的开头是下划线或字母判断它是不是标识符Token
static Token identifier() {// 判断curr指针当前正在处理的字符是不是 字母 下划线 数字while (isAlpha(peek()) || isDigit(peek())) {advance();  // 继续前进看下一个字符 直到碰到下一个字符不是字母 下划线 以及数字 结束Token}// 当while循环结束时,curr指针指向的是该Token字符串的下一个字符// 这个函数的意思是: 只要读到字母或下划线开头的Token我们就进入标识符模式// 然后一直找到此Token的末尾// 但代码运行到这里还不确定Token是标识符还是关键字, 因为它可能是break, var, goto, max_val...// 于是执行identifierType()函数,它是用来确定Token类型的return makeToken(identifierType());
}static Token number() {// 简单起见,我们将NUMBER的规则定义如下:// 1. NUMBER可以包含数字和最多一个'.'号// 2. '.'号前面要有数字// 3. '.'号后面也要有数字// 这些都是合法的NUMBER: 123, 3.14// 这些都是不合法的NUMBER: 123., .14(虽然在C语言中合法)// 提示: 这个过程需要不断的跳过中间的数字包括小数点,所以也需要循环int point_count = 0;while (isDigit(peek()) || peek() == '.') {char ch = peek();if (ch == '.') {if (point_count > 1) {return errorToken("error number!");//小数点大于一个 不合法的number}elsepoint_count++;}//if (index == 0 && ch == '.') {//.14这种类型的不合法number//	return errorToken("error number!");//}if (ch == '.' && !isDigit(peekNext())) {//14. 这种类型的不合法numberreturn errorToken("error number!");}advance();}return makeToken(TOKEN_NUMBER);
}static Token string() {// 字符串以"开头,以"结尾,而且不能跨行// 为了简化工作量,简化了字符串// 如果下一个字符不是末尾也不是双引号,全部跳过(curr可以记录长度,不用担心)while (peek != '\n' && peek != EOF && peek() != '"') {advance();}if (peek() == '"') advance();return makeToken(TOKEN_STRING);}static Token character() {// 字符'开头,以'结尾,而且不能跨行// 如果下一个字符不是末尾也不是单引号,全部跳过(curr可以记录长度,不用担心)// 这两个函数不能说一模一样,也是几乎一样while (peek != '\n' && peek != EOF && peek() != '\'') {advance();}if (peek() == '\'') advance();return makeToken(TOKEN_STRING);
}// 处理无法识别的字符
static Token errorTokenWithChar(char character) {char message[50];// 将无法识别的字符是什么输出sprintf(message, "Unexpected character: %c", character);return errorToken(message);
}// Scanner核心逻辑,用于返回一个制作好的Token
Token scanToken() {// 跳过前置空白字符和注释skipWhitespace();// 记录下一个Token的起始位置scanner.start = scanner.current;// 如果已经处理完毕了 直接返回TOKEN_EOFif (isAtEnd()) {return makeToken(TOKEN_EOF);}// curr指针现在指向Token的第二个字符,但这个字符c仍然是第一个字符的值char c = advance();// 如果Token的第一个字符是字母和下划线就进入标识符的处理模式if (isAlpha(c)) {return identifier();}// 如果Token的第一个字符是数字,那就进入数字的处理模式if (isDigit(c)) {return number();}// 如果Token的第一个字符既不是数字也不是字母和下划线,那么就switch处理它switch (c) {// 第一部分: 处理单字符Tokencase '(': return makeToken(TOKEN_LEFT_PAREN);case ')': return makeToken(TOKEN_RIGHT_PAREN);case '[': return makeToken(TOKEN_LEFT_BRACKET);case ']':return makeToken(TOKEN_RIGHT_BRACKET);case '{': return makeToken(TOKEN_LEFT_BRACE);case '}':return makeToken(TOKEN_RIGHT_BRACE);case ',':return makeToken(TOKEN_COMMA);case '.':return makeToken(TOKEN_DOT);case ';':return makeToken(TOKEN_SEMICOLON);case '~':return makeToken(TOKEN_TILDE);// 可单可双字符的Token处理会稍微复杂一点,但不多// 如果Token的第一个字符是+号case '+':// 如果Token的第二个字符也是+,那就生产++双字符Token返回if (match('+')) {return makeToken(TOKEN_PLUS_PLUS);}else if (match('=')) {return makeToken(TOKEN_PLUS_EQUAL);}else return makeToken(TOKEN_PLUS);case '-':if (match('-')) {return makeToken(TOKEN_MINUS_MINUS);}else if (match('=')) {return makeToken(TOKEN_MINUS_GREATER);}else if (match('>')) {return makeToken(TOKEN_MINUS_GREATER);}else return makeToken(TOKEN_MINUS);// ...TODOcase '*':if (match('=')) {return makeToken(TOKEN_STAR_EQUAL);}else {return makeToken(TOKEN_STAR);}case '/':if (match('=')) {return makeToken(TOKEN_SLASH_EQUAL);}else if (match('/')) skipWhitespace();else return makeToken(TOKEN_SLASH);case '%':if (match('=')) {return makeToken(TOKEN_PERCENT_EQUAL);}else return makeToken(TOKEN_PERCENT);case '&':if (match('=')) {return makeToken(TOKEN_AMPER_EQUAL);}else if (match('&')) {return makeToken(TOKEN_AMPER_AMPER);}else return makeToken(TOKEN_AMPER);case '|':if (match('=')) {return makeToken(TOKEN_PIPE_EQUAL);}else if (match('|')) {return makeToken(TOKEN_PIPE_PIPE);}else return makeToken(TOKEN_PIPE);case '^':if (match('=')) {return makeToken(TOKEN_HAT_EQUAL);}else return makeToken(TOKEN_HAT);case '=':if (match('=')) {return makeToken(TOKEN_EQUAL_EQUAL);}else return makeToken(TOKEN_EQUAL);case '!':if (match('=')) {return makeToken(TOKEN_BANG_EQUAL);}else return makeToken(TOKEN_BANG);case '<':if (match('=')) {return makeToken(TOKEN_LESS_EQUAL);}else if (match('<')) {return makeToken(TOKEN_LESS_LESS);}else return makeToken(TOKEN_LESS);case '>':if (match('=')) {return makeToken(TOKEN_GREATER_EQUAL);}else if (match('>')) {return makeToken(TOKEN_GREATER_GREATER);}else return makeToken(TOKEN_GREATER);case '"': return string(); // 如果Token的第一个字符是双引号,那就解析出字符串Token返回case '\'': return character();	// 如果Token的第一个字符是单引号,那就解析出字符Token返回}// 如果上述处理都没有处理成功,都没有生成合适的Token,说明该字符无法识别return errorTokenWithChar(c);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/4006.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Eagle for Mac:强大的图片管理工具

Eagle for Mac是一款专为Mac用户设计的图片管理工具&#xff0c;旨在帮助用户更高效、有序地管理和查找图片资源。 Eagle for Mac v1.9.2中文版下载 Eagle支持多种图片格式&#xff0c;包括JPG、PNG、GIF、SVG、PSD、AI等&#xff0c;无论是矢量图还是位图&#xff0c;都能以清…

EasyRecovery数据恢复软件2025激活码及下载使用步骤教程

EasyRecovery数据恢复软件是一款功能强大且用户友好的数据恢复工具&#xff0c;专为帮助用户找回因各种原因丢失的数据而设计。该软件由全球知名的数据恢复技术公司开发&#xff0c;经过多年的技术积累和更新迭代&#xff0c;已经成为行业内备受推崇的数据恢复解决方案。 EasyR…

【定制化体验:使用Spring Boot自动配置,打造个性化Starter】

项目结构 Pom <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4…

SpringBoot---------整合Redis

目录 第一步&#xff1a;引入依赖 第二步&#xff1a;配置Redis信息 第三步&#xff1a;选择Spring Data Redis进行操作Redis数据库 ①操作String类型数据&#xff08;用的少&#xff09; ②操作Object类型数据&#xff08;重要&#xff01;&#xff01;&#xff01;&#x…

[iOS]使用CocoaPods发布私有库

1.创建私有 Spec 仓库 首先&#xff0c;需要一个私有的 Git 仓库来存放你的 Podspec 文件&#xff0c;这个仓库用于索引你所有的私有 Pods。 在 GitHub 或其他 Git 服务上创建一个新的私有仓库&#xff0c;例如&#xff0c;名为 PrivatePodSpecs。克隆这个仓库到本地&#xf…

AI大模型探索之路-训练篇2:大语言模型预训练基础认知

文章目录 前言一、预训练流程分析二、预训练两大挑战三、预训练网络通信四、预训练数据并行五、预训练模型并行六、预训练3D并行七、预训练代码示例总结 前言 在人工智能的宏伟蓝图中&#xff0c;大语言模型&#xff08;LLM&#xff09;的预训练是构筑智慧之塔的基石。预训练过…

Docker基本操作 容器相关命令

docker run:运行镜像; docker pause:暂停容器&#xff0c;会让该容器暂时挂起&#xff1b; docker unpauser:从暂停到运行; docker stop:停止容器&#xff0c;杀死进程; docker start:重新创建进程。 docker ps&#xff1a;查看所有运行的容器及其状态&#xff0c;默认只展…

JavaScript创建和填充数组的更多方法

空数组fill()方法创建并填充数组 ● 我们之前创建数组的方式都是手动去创建去一个数据&#xff0c;例如 console.log([1, 2, 3, 4, 5, 6, 7]);● 当然我们也可以使用Array对象来构造数组 console.log([1, 2, 3, 4, 5, 6, 7]); console.log(new Array(1, 2, 3, 4, 5, 6, 7));…

python生成二维码及进度条源代码

一、进度条 1、利用time模块实现 import time for i in range(0, 101, 2):time.sleep(0.3)num i // 2if i 100:process "\r[%3s%% ]: |%-50s|\n" % (i, # * num)else:process "\r[%3s%% ]: |%-50s|" % (i, # * num)print(process, end, flushTrue)2、使…

tcp服务器端与多个客户端连接

如果希望Tcp服务器端可以与多个客户端连接&#xff0c;可以这样写&#xff1a; tcpServernew QTcpServer(this);connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection())); void MainWindow::onNewConnection() {QTcpSocket *tcpSocket;//TCP通讯的Sockettcp…

陪丨玩丨系丨统前后端开发流程,APP小程序H5前后端源码交付支持二开!多人语音,开黑,线上线下两套操作可在一个系统完成!

100%全部源码出售 官网源码APP源码 管理系统源码 终身免费售后 产品免费更新 产品更新频率高 让您时刻立足于行业前沿 软件开发流程步骤及其作用&#xff1a; 软件开发是一个复杂而系统的过程&#xff0c;涉及多个环节&#xff0c;以下是软件开发的主要流程步骤及其作用…

leetCode60. 排列序列

leetCode60. 排列序列 方法一:语法版&#xff0c;面试官不认可的方法&#xff1a;next_permutation函数 // 方法一&#xff1a;使用next_permutation函数&#xff0c;将某容器设置为当前按照字典序 // 的下一个全排列的内容 class Solution { public:string getPermutation(in…

SystemUI KeyButtonView setDarkIntensity 解析

继承自 ImageView KeyButtonDrawable intensity为0时按键颜色为白色。 intensity为1时黑色为的调用堆栈&#xff1a; java.lang.NullPointerException: Attempt to invoke virtual method int java.lang.String.length() on a null object referenceat com.android.systemui.…

LLaMA-Factory参数的解答(命令,单卡,预训练)

前面这个写过&#xff0c;但觉得写的不是很好&#xff0c;这次是参考命令运行脚本&#xff0c;讲解各个参数含义。后续尽可能会更新&#xff0c;可以关注一下专栏&#xff01;&#xff01; *这是个人写的参数解读&#xff0c;我并非该领域的人如果那个大佬看到有参数解读不对或…

CARLA (I)--Ubuntu20.04 服务器安装 CARLA_0.9.13服务端和客户端详细步骤

目录 0. 说明0.1 应用场景&#xff1a;0.2 本文动机&#xff1a; 1. 准备工作2. 安装 CARLA 服务端软件【远程服务器】3. 安装 CARLA 客户端【远程服务器】3.1 .egg 文件安装&#xff1a;3.2 .whl 文件安装&#xff1a;3.3 从Pypi下载Python package 4. 运行服务端程序5. 运行客…

Unity入门实践小项目

必备知识点 必备知识点——场景切换和游戏退出 必备知识点——鼠标隐藏锁定相关 必备知识点——随机数和Unity自带委托 必备知识点——模型资源的导入 实践项目 需求分析 UML类图 代码和资源导入 开始场景 场景装饰 拖入模型和添加脚本让场景动起来 开始界面 先用自己写的GUI…

Feign负载均衡

Feign负载均衡 概念总结 工程构建Feign通过接口的方法调用Rest服务&#xff08;之前是Ribbon——RestTemplate&#xff09; 概念 官网解释: http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign Feign是一个声明式WebService客户端。使用Feign能让…

2726641 - Failed to resolve Object Based Navigation target

服务和支持/知识库文章和注释/人事管理/人员发展/目标设置和评估 (PA-PD-PM) 2726641 - 未能解析基于对象的导航目标 SAP Knowledge Base Article, Version: 1, 审批日期: 30.11.2018 组件PA-PD-PM对象状态 优先级正常对象状态 类别问题对象状态 审批状态已发布至客户对象…

Java设计模式 _创建型模式_原型模式(Cloneable)

一、原型模式 1、原型模式&#xff08;Prototype Pattern&#xff09;是用于创建重复的对象&#xff0c;同时又能保证性能比较好。一般对付出较大代价获取到的实体对象进行克隆操作&#xff0c;可以提升性能。 2、实现思路&#xff1a; &#xff08;1&#xff09;、需要克隆的…

STM32、GD32等驱动AMG8833热成像传感器源码分享

一、AMG8833介绍 1简介 AMG8833是一种红外热像传感器&#xff0c;也被称为热感传感器。它可以用来检测和测量物体的热辐射&#xff0c;并将其转换为数字图像。AMG8833传感器可以感知的热源范围为-20C到100C&#xff0c;并能提供8x8的像素分辨率。它通过I2C接口与微控制器或单…