node从头到尾实现简单编译器

news2024/11/24 15:50:09

介绍
本文用node实现了一个简单的编译器mccompiler,主要用于学习,笔者能力和精力有限,如有不当,还请指出
原文地址:原文地址
项目地址:项目地址
本文涉及:编译器的词法分析,抽象语义树生成,语法分析,代码生成
本文重点内容:

  1. 实现正则表达式分析器
  2. 实现简易版 Flex
  3. 实现 LR0和SLR 语法,并简单介绍其他语法(LL, LR1 LSPR)
  4. 实现生成汇编代码
  5. 实现简易编译器功能,提供编译时期类型检查和推断,支持加减乘除支持函数的递归调用

会包含的:

  1. 实现 nfa,以及联合 nfa => dfa,进行词法分析
  2. 实现 dfa,进行语法分析,并基于此构建抽象语法树(AST)
  3. 基于 ast 进行语言义分析(类型检查以及是否符合语言规范)
  4. 基于 ast 生成汇编代码,虽然本文没有显示的终中间代码生成过程,但是也有类似的思想

不会包含的:
本语言比较简单,不包含复杂数据结构的支持,如数组对象等其他功能;不会涉及复杂的编译器后端知识:如垃圾回收,寄存器染色,数据流分析等等

minic 语法:

  1. 运算符:支持±*/运算,不支持优先级(, )
  2. 类型:支持自然数(int 类型),字符串,布尔值以及 void
  3. 语句:支持函数调用,嵌套的 if-else 语句(if-else必须成对出现,如下的语法是不允许的)
  4. 和 C 一样,必须要有 main 函数
  5. 变量必须声明的时候同时赋值,如下的声明是不允许的
  6. 允许块级作用域
  7. 允许返回值是条件表达式,运算表达式
  8. 运算符左右两侧仅可以是变量或是数字
    int sum(int x, int y) {
    return x + y;
    }
int feb(x: int) {
 if (x == 0) {
 return x;
  }
 return x + feb(x-1);
}

接下来从以下几个方面介绍:

  1. parser:包含正则表达式生成和词法token生成
  2. semantic:文法推导式解析和抽象语义树生成
  3. check:语法和类型校验
  4. gen:汇编代码生成
    Parser
    在词法分析阶段,输入是字符串,输出是 token 流,一开始设计输出是枚举值的数组,类似这样: [TYPE, ID, BRACE, ...],但是这样会有问题,因为词素值没办法保留下来,所以后面改成了如下输出: [(line_num, TYPE, int), (line_num, ID, feb)],(行号,类型, 词素值)三元组
    学在构建自动机过程中,自动机把输入流转成 token流,比如当词法分析器读完 int 的时候,这时候就会返回一个 TYPE token,但是如果是 int1,就应该返回一个 ID token,所以这里涉及到贪婪读取,另外对于像 if 这种关键字,如果同时满足多种终结状态,应该涉及到优先级,这里的优先级比较简单,直接遍历终态节点数组endStates(可以理解为叶子节点),遇到第一个符合的即返回,所以正则的优先级和前后顺序有关;

那么如何构建自动机?
我们的目标是构建一系列单个正则表达式单元nfa,然后联合成一个大的nfa单元,这个nfa可以解析我们的之前正则单元,再得到联合nfa的邻接矩阵edges,最后根据edges转成dfa,具体步骤如下:

首先,需要名明确的是,我们的词法分析器支持以下几个单元:
+: a+,
: a,
连接: ab,
逻辑或: a|b,
字符集: [a-z]
支持少部分字符转义,如:\s,\ t, \n

如何把正则表达式构建为 nfa:对于每一个单元正则表达式,可以直接生成对应的节点,但是有些问题我们需要注意:
我们的输入是一个个正则表达式,正则表达式本身可以理解为是一个个单元,而这些单元又可能是字符或者其他单元和成的,如:
a|b 就是一个单元,但是其组成就是 2 个字符
[a-z]|a 就是一个元外加一个普通字符
另外对于中括号这种还需要特殊处理,思路如下:即使是单个字符也会抽象成节点的概念,另外在生成自动机的过程中,由于存在[],+,*等等这样的修饰符号,考虑使用 stack 进行存储,类比括号匹配算法。(lib => parser => nfa => flex函数)

构建基本正则单元:

export function connect(from: VertexNode, to: VertexNode): VertexNode {
  // from的尾和to的头相互连接,注意circle
  let cur = graph.getVertex(from.index); // 获取邻接表
  const memo: number[] = [];
  while (cur.firstEdge && !memo.includes(cur.index)) {
    memo.push(cur.index);
    cur = graph.getVertex(cur.firstEdge.index);
  }

  graph.getVertex(cur.index).firstEdge = new Node(
    to.index,
    graph.getVertex(cur.index).firstEdge
  );
  return from;
}
export function or(a: VertexNode, b: VertexNode): VertexNode {
  const nodeStart = new VertexNode(Graph.node_id, null);
  graph.addVertexNode(nodeStart, nodeStart.index);
  nodeStart.firstEdge = new Node(a.index, null, a.edgeVal || null);
  nodeStart.firstEdge.next = new Node(b.index, null, b.edgeVal || null);
  const nodeEnd = new VertexNode(Graph.node_id, null);
  graph.addVertexNode(nodeEnd, nodeEnd.index);
  connect(a, nodeEnd);
  connect(b, nodeEnd);
  return nodeStart;
}
  1. 字符集
export function characters(chars: string[]) {
  const nodeStart = new VertexNode(Graph.node_id, null);
  graph.addVertexNode(nodeStart, nodeStart.index);
  const nodeEnd = new Node(Graph.node_id, null, chars);
  const tmp = new VertexNode(nodeEnd.index, chars);
  graph.addVertexNode(tmp, tmp.index);

  const pre = nodeStart.firstEdge;
  nodeStart.firstEdge = nodeEnd;
  nodeEnd.next = pre;

  return nodeStart;
}
  1. *修饰符
export function mutipliy(wrapped: VertexNode): VertexNode {
  const nodeStart = new VertexNode(Graph.node_id, null);
  graph.addVertexNode(nodeStart, nodeStart.index);
  const tmp = new Node(wrapped.index, null, null);
  nodeStart.firstEdge = tmp;
  let cur = graph.getVertex(wrapped.index); // 获取邻接表
  while (cur.firstEdge) {
    cur = graph.getVertex(cur.firstEdge.index);
  }
  connect(cur, nodeStart);
  return nodeStart;
}
  1. +修饰符
export function plus(base: VertexNode) {
  // 基于old新建节点
  let nodeStart = new VertexNode(Graph.node_id, base.edgeVal);
  nodeStart.firstEdge = base.firstEdge;
  const res = nodeStart;
  graph.addVertexNode(nodeStart, nodeStart.index);
  let cur = base?.firstEdge;
  while (cur) {
    const vertexNode = graph.getVertex(cur?.index);
    const tmp = new VertexNode(Graph.node_id, vertexNode.edgeVal);
    nodeStart.firstEdge = new Node(tmp.index, null, vertexNode.edgeVal);
    nodeStart = tmp;
    tmp.firstEdge = base.firstEdge;
    graph.addVertexNode(tmp, tmp.index);
    cur = vertexNode.firstEdge;
  }
  return mutipliy(res);
}

不过比较困扰的是这些节点的数据结构如何存储是一件要考虑周到的事:
需要节点 id,由于自动机是有向图,并且可能带环,并且节点和节点之间可能存在不止一条边,考虑了下,还是用 邻接表存储(主要是第一版的代码是这样的,再加上如果感觉节点之间的连接可能在某些情况下比较少,临界矩阵比较浪费内存),firstEdge 指向其所有的临界边,edgeVal 是边上的值,对于该图的搜索,使用 bfs+dfs+检测环。
如下:
if对应的nfa:
​

编辑

切换为居中
if对应的nfa
[a-z][a-z0-9]* 的nfa为:
​

编辑

切换为居中
[a-z][a-z0-9]* 的nfa
联合后就变成了一个大的nfa,并在终态节点上放置一些动作:
​

编辑

切换为居中
联合nfa
构建邻接矩阵:
const edges = new Array(200).fill(0).map((_item) => {
    return new Array(200).fill(0);
});
edges[起始点][终止点] = [边集合],如果是epsilon,则是null
build_edges() dfs + bfs + 集合去重
[
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],   ======> 0
  [0, 0, i, 0, null, 0, 0, 0, 0, 0],======> 1
  [0, 0, 0, f, 0, 0, 0, 0, 0, 0],======> 2
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]======> 3
  [0, 0, 0, 0, 0,	======> 4
    [
       a,  b,  c, 100, 101, 102,
      103, 104, 105, 106, 107, 108,
      109, 110, 111, 112, 113, 114,
      115, 116, 117, 118, 119, 120,
      121, z
    ],
    0, 0, 0, 0
  ],
  [0, 0, 0, 0, 0, 0, 0, 0, null, 0],======> 5
  [0, 0, 0, 0, 0, 0, 0, 0,======> 6
    [
       a,  b,  c, d, 101, 102, 103, 104,
      105, 106, 107, 108, 109, 110, 111, 112,
      113, 114, 115, 116, 117, 118, 119, 120,
      121, z,  0,  1,  2,  3,  4,  5,
       6,  7,  8,  9
    ],
    0,0
  ],
  [0, 0, 0, 0, 0, 0, 0, 0, null, 0],======> 7
  [0, 0, 0, 0, 0, 0, null, 0, 0, 0],======> 8
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]======> 9
]
可以验证下上面的邻接矩阵就是如下节点边值对(行索引对应source节点,列索引对应target节点,矩阵值就是边集合)1 => 2: i

1 => 4: null

2 => 3: f

4=>5: [a-z]

5=>8: null

6=>7: [a-z] [0-9]

7=>8: null

8=>6: null

根据邻接矩阵构建dfa

有了Closure和DFAedge算法单元,这样从NFA的起点出发,不断的更新DFAedge(S, c),每次新生成的DFAedge(S, c),即得到DFA里的状态节点,据此得到dfa状态转移表

states[0] <- [] 
states[1] <- Closure([S])
p <- 1, j <- 0 
while j <= p 
	for c in 字母集 
		e <- DFAedge(states[j], c) 
		if e == states[i] for some i <= p 
			then trans[j][c] <- i
		else 
			p <- p + 1
			states[p] <- e
			trans[j][c] <- p
	j <- j + 1

构建完正则表达式之后就可以对我们的输入处理成token流了。(lib => scan函数)

构建抽象语法树

这里我用的是简单的 LR(0)上下无关文法,关于什么是上下无关文什么是有关文,请戳->,

理论上上下无关文法所代表的文法范围: LR(1) > LAPR > SLR > LR(0)
LR(0): 没有提前预测的符号,容易出现 shift-reduce 冲突以及 reduce-reduce 冲突,所以需要设计适合的文法;
SLR: 有简单的预测,可以用follow集解决部分shift-reduce 冲突,但是在有些情况下还是 shift-reduce冲突
LR(1): 可以解决 shift-reduce 冲突,也解决 reduce-reduce 冲突
LAPR: 由于 LR(1)的表特别大,在此基础上做了优化

看如下文法1的 LR(0)生成过程:
E-> Program $
Program -> Assign == Assign
Assign -> Assign + Token
Assign -> Token
Token -> id

在这里插入图片描述

在这里插入图片描述

可以看到在状态 9 是存在移位-规约冲突的,这是因为 LR(0)默认在所有的终结符号处做规约。
slr 语法是比 LR(0),更为广泛的一种语法,它只在特定的地方放置规约动作。具体来说,在上面的例子里,在状态 9 处,只有规约式 1 的移位指针到达里末尾,所以可以看下规约式 1 后面可能会紧接着什么符号,也就是 followSet(Program) = {$},所以只在$处放置规约动作。

下面是 slr 分析表:
在这里插入图片描述

AST生成
生成好分析表之后,就可以根据分析表进行语法分析了,如下所示,在提前定义好的文法产生式做对应的规约动作,如下case 0就是Formals -> int, TOKEN.ID,在这里利用栈内的元素生成Formal_Class;就这样每次在对应的文法产生式上对对应的做规约动作,从而完成自底向上的ast的构建
[Formals -> int, TOKEN.ID]: 0,
[Formals -> Formals, ‘,’, int, TOKEN.ID]: 1

case 0:
  res = new Formal_Class(yyvalsp[0][2], yyvalsp[1][2]);
  break;
case 1:
  res = new Formal_Class(yyvalsp[0][2], yyvalsp[1][2], yyvalsp[3]);
  break;

下图简单模拟了int ID (int ID)的token流处理过程:
在这里插入图片描述

在没有规约动作的时候token一直push进栈,直到有对应的规约动作,这个时候按照指定的规约动作,生成非终结符,再把该非终结符放入栈内,重复进行,直到栈内为空或者遇到了$,当然,如果在这过程中遇到了不合法的字符,直接抛出异常。
以及过程中生成的简单ast如下:

Program_Class {
  expr: Function_Class {
    formal_list: [ 'x' ],
    name: 'total',
    expressions: Branch_Class {
      ifCond: Cond_Class {
        lExpr: Indentifier_Class { token: 'x' },
        rExpr: Int_Contant_Class { token: '0' },
        op: '=='
      },
      statementTrue: Return_Class { expr: Indentifier_Class { token: 'x' } },
      statementFalse: Assign_Class {
        name: 'm',
        ltype: 'int',
        r: Caller_Class {
          params_list: [ undefined ],
          id: 'total',
          params: Sub_Class {
            lvalue: Indentifier_Class { token: 'x' },
            rvalue: Int_Contant_Class { token: '1' }
          },
          next: undefined
        },
        next: Return_Class {
          expr: Add_Class {
            lvalue: Indentifier_Class { token: 'x' },
            rvalue: Indentifier_Class { token: 'm' }
          }
        }
      }
    },
    formals: Formal_Class { name: 'x', type: 'int', next: undefined },
    next: Function_Class {
      formal_list: [ 'x', 'y' ],
      name: 'sum',
      expressions: Return_Class {
        expr: Add_Class {
          lvalue: Indentifier_Class { token: 'x' },
          rvalue: Indentifier_Class { token: 'y' }
        }
      },
      formals: Formal_Class {
        name: 'y',
        type: 'int',
        next: Formal_Class { name: 'x', type: 'int', next: undefined }
      },
      next: Function_Class {
        formal_list: [],
        name: 'main',
        expressions: Assign_Class {
          name: 'x',
          ltype: 'int',
          r: Caller_Class {
            params_list: [ '10' ],
            id: 'total',
            params: Int_Contant_Class { token: '10' },
            next: undefined
          },
          next: Caller_Class {
            params_list: [ 'x' ],
            id: 'print',
            params: Indentifier_Class { token: 'x' },
            next: undefined
          }
        },
        formals: undefined,
        next: undefined,
        return_type: 'int'
      },
      return_type: 'int'
    },
    return_type: 'int'
  }
}

汇编代码生成
思路:遍历ast自上向下进行利用堆栈机代码生成,由于本语言比较简单,仅使用了3个寄存器,a0,v0,t0,其中v0是辅助寄存器帮助函数返回值存储以及系统调用的退出和打印;

cgenForSub(e1, e2) {
	cgen(e1)
	sw $a0, 0($29)
	addiu $29, $29, -4
	cgen(e2)
	add $a0, $t0, $a0
}

这里最重要的点是对声明变量的内存分配以及取变量的时候,要知道对应的作用域链,该从哪个作用域获取变量,只要我们对基本的一些单元表达式做好了代码生成的工作,后面就是搭积木的工作了;下面是该语言的函数栈示意图:

在这里插入图片描述

这里如何取参数?由于函数栈在扩增的时候,不太方便通过sp指针获取参数和变量的存储位置,所以这里使用fp指针去作为基地址,寻找参数和局部变量

关于作用域问题是采用的树结构存储(双向链表),每次从当前所在作用域内寻找变量,再继续依次向上寻找,直到找到函数级作用域;
八、编写代码高亮语法插件:

我这里是速成版,比较简单,只涉及简单的语法部分
需要安装:

$ npm install -g vsce
$ npm install -g yo

如果是需要编写全新的插件,则运行yo code 选择 new Language Support,提示一些问题,按需填写即可,插件名称尽可能唯一,不然在插件市场里不好搜,运行完命令之后会有一个生成目录,编写高亮语法的文件在 xxx.tmLanguage.json 文件里,如果你只是配置一个 VS Code 中已有语言的语法,记得删掉生成的 package.json 中的 languages 配置。

编写插件vscode

这里看下我的规则:

  "editor.tokenColorCustomizations":{
    "[Default Dark+]": { // 这里是自己所选择的主题颜色,我的是vscode默认的颜色
      "textMateRules": [
        {
          "scope": "identifier.name", // 自定义或者符合标准规范的命名,对应插件里的xxx.tmLanguage.json文件里的name选项
          "settings": {
              "foreground": "#33ba8f"
          }
      },
      {
        "scope": "id.name.mc",
        "settings": {
            "foreground": "#eb8328"
        }
      }
      ]
    }
  }

xxx.tmLanguage.json里的配置:

``json
{
	"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
	"name": "minic",
	"patterns": [
		{
			"include": "#keywords"
		},
    {
      "include": "#type"
    },
    {
      "include": "#number"
    },
    {
      "include": "#id"
    },
    {
      "include": "#comment"
    }
	],
	"repository": {
    "type": {
			"patterns": [{
				"name": "support.type.primitive.mc",
				"match": "\\b(void|int|bool)\\b" // 类型
			}]
		},
    "keywords": {
			"patterns": [{
				"name": "keyword.control.mc",
				"match": "\\b(if|while|for|return|else)\\b" // 关键字
			}]
		},
    "number": {
			"patterns": [{
				"name": "constant.numeric.mc",
				"match": "\\b[0-9]+\\b" 
			}]
		},
    "id": {
			"patterns": [{
				"name": "id.name.mc",
				"match": "\\b[a-z][a-z0-9]*\\b"
			}]
		},
    "comment": {
			"patterns": [{
				"name": "comment.line.double-dash",
				"match": "^//.*" //注释
			}]
		}
	},
	"scopeName": "source.mc"
}

参考文章

LL1文法、LR(0)文法、SLR文法、LR(1)文法、LALR文法
[栈和栈帧)
LL(1),LR(0),SLR(1),LALR(1),LR(1)对比与分析
语法分析——自底向上语法分析中的规范LR和LALR

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

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

相关文章

应用程序传递数据给驱动和驱动操作LED灯

目录 1. 应用程序将数据传递给驱动 1.1. 函数分析 1.2. 编写驱动.c文件 1.3. 编写编译驱动的makefile文件 1.4. 执行make命令&#xff0c;并安装驱动&#xff0c;生成设备文件 1.5. 写应用层.c文件 1.6. 执行可执行文件验证 2. 驱动操作LED灯 2.1. 函数分析 2.2. 手册…

【C语言】第一个C语言项目——“猜数字”游戏(内附源码)

君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 Hello米娜桑&#xff0c;这里是君兮_&#xff0c;今天又抽空为大家更新我们的主线0基础C语言啦&#xff01;鉴于最近讲解了非常多的选择语句与循环语句&#xff0c;咱们今天就来讲讲两者结合的一个简单的实战应用。 同时…

Python Locust全过程使用代码详解

下方查看历史精选文章 重磅发布 - 自动化框架基础指南pdfv1.1大数据测试过程、策略及挑战 测试框架原理&#xff0c;构建成功的基石 在自动化测试工作之前&#xff0c;你应该知道的10条建议 在自动化测试中&#xff0c;重要的不是工具 Python locust 是一个基于 Python 的开源负…

MKS SERVO4257D 闭环步进电机_系列9 上位机通讯示例

第1部分 产品介绍 MKS SERVO 28D/35D/42D/57D 系列闭环步进电机是创客基地为满足市场需求而自主研发的一款产品。具备脉冲接口和RS485/CAN串行接口&#xff0c;支持MODBUS-RTU通讯协议&#xff0c;内置高效FOC矢量算法&#xff0c;采用高精度编码器&#xff0c;通过位置反馈&a…

视觉SLAM十四讲——ch13实践(设计SLAM系统)

视觉SLAM十四讲——ch13的实践操作及避坑 1. 实践操作前的准备工作2. 实践过程2.1 运行测试程序2.2 运行00数据集2.3 更改代码画出运动轨迹 3. 遇到的问题及解决办法3.1 cmake ..时出现的问题3.2 make时出现的问题3.3 头文件下红色报错 1. 实践操作前的准备工作 下载Kitti数据…

使用dat.gui更改three.js中的物体变量

一、dat.gui介绍 gui是一种JavaScript库&#xff0c;用于创建可视化控件和调试工具。它是dat.gui的简称。dat.gui是一个用于在Web应用程序中创建可定制GUI的JavaScript库。它可以轻松创建滑块、复选框、颜色选择器等控件&#xff0c;用户可以直接在GUI上进行交互和调整。dat.g…

一起来看看 K-verse LAND 销售活动中的合作伙伴给大家的祝福吧~

K-verse 是 The Sandbox 中的韩国内容主题空间&#xff0c;自去年 12 月首次推出以来&#xff0c;已吸引多家合作伙伴加入。此外&#xff0c;现有的合作伙伴公司和品牌正在积极准备以新的形式展示元宇宙内容。 这里有着许多可能性&#xff0c;K-verse LAND 销售活动是不是让你们…

Tomcat及项目部署

一、Tomcat是什么&#xff1f; Tomcat 是基于 Java 实现的⼀个开源免费, 也是被⼴泛使⽤的 HTTP 服务器。 二、下载安装 官⽅⽹站&#xff1a;https://tomcat.apache.org/ 选择其中的 zip 压缩包, 下载后解压缩即可. 解压缩的⽬录最好不要带 "中⽂" 或者 特殊符号…

vue-cli 如何修改默认环境变量名称

比如想要修改开发环境 NODE_ENV 的默认值 &#xff1f; 1. 新建文件 .env.development 2. 在 packjson.json 的 script 中添加一行代码 --mode [文件 env 后面的环境名称] "dev": "vue-cli-service serve --mode development", 3. 然后 npm run dev 环境变…

JavaScript ES12新特性有哪些?

文章目录 导文Promise.any()WeakRef 和 FinalizationRegistry数字分隔符String.prototype.replaceAll()Logical Assignment Operators数字类型的新增方法私有字段和方法 导文 JavaScript ES12&#xff08;也称为ECMAScript 2022&#xff09;是JavaScript的最新版本&#xff0c;…

如何解决报错:nginx error!

目录 Nginx报错问题 nginx error! The page you are looking for is not found. Website Administrator 解决方法 Nginx报错问题 当访问搭建好的Nginx服务网站时 有以下报错 nginx error! The page you are looking for is not found. Website Administrator Someth…

猪齿鱼开源发布2.0版本:DevOps能力全面升级,研发效能显著提升,欢迎即刻体验!

近日&#xff0c;甄知科技猪齿鱼Choerodon数智化开发管理平台正式发布了开源2.0版本&#xff01; 开源发布会上&#xff0c;甄知产研团队、业内伙伴和社区开发者们齐聚一堂&#xff0c;共同见证猪齿鱼开源2.0的重磅发布&#xff01;发布会由上海甄知科技创始合伙人兼CTO张礼军先…

前端添加代理通过nginx进行转发解决跨域

记录在项目中遇到跨域并进行解决的方案 解决方案 记录在项目中遇到跨域并进行解决的方案前端代理部分nginx转发配置origin限制,修复CORS跨域漏洞 前端代理部分 代理后页面请求地址截图&#xff1a; 这里地址栏的地址是&#xff1a;http://127.0.0.1:13908 调用登录接口请求地…

OrCAD Capture 元件位号Part Reference有下划线

原因&#xff1a; 提示用户曾经修改过原理图封装。 现象&#xff1a; USB20_12 解决办法&#xff1a; 对着元器件右键>User Assigned Reference > Uset&#xff0c;即可消除下划线。 修改后&#xff1a;

通过域名的方式访问服务器里的资源

大家好&#xff0c;我是雄雄。欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 在平时的项目过程中&#xff0c;我们可能经常会遇到这样的场景。 上传资源&#xff0c;比如图片或者视频到服务器中&#xff0c;上传上去后&#xff0c;我们给数据库中存的是文件所在路径&…

SSMP整合案例(3) 创建数据层并在测试类中运行数据库增删查改操作

上文 SSMP整合案例(2) Spring Boot整合Lombok简化实体类开发我们已经开发完了实体类 我们就可以做数据层了 目前来讲 数据层技术 使用了最大的自然是 MyBatis 但其实MyBatis-Plus在国内很多中小企业还是使用的挺多的 这次 我们主要是通过MyBatis-Plus和Druid来做这件事情 这两…

5款界面简洁无广告的轻量级小软件

今天的主题是简洁&#xff0c;轻便&#xff0c;都是轻量级的小软件&#xff0c;界面都是非常简洁&#xff0c;而且无广告的。 文件同步——Syncthing Syncthing是一款用于同步和分享文件的工具。它可以让你在不同的设备上同步你的文件夹&#xff0c;并提供多种功能和选项来设…

鱼眼相机成像模型以及基于OpenCV标定鱼眼镜头(C++)

opencv系列 文章目录 opencv系列一、鱼眼镜头模型二、投影函数等距投影模型等立体角投影模型正交投影模型体视投影模型 三、OpenCV中的鱼眼相机模型四、标定&#xff08;C&#xff09;实现使用的函数采集标定图像标定代码标定结果 一、鱼眼镜头模型 鱼眼镜头一般是由十几个不同…

新能源充电桩4G无线物联网解决方案|4G路由器ZR2000

日常生活中新能源汽车已随处可见&#xff0c;新能源也逐渐普遍&#xff0c;绿色出行、低碳生活的环保概念也随着科普深入人心&#xff0c;新能源汽车必备的充电桩行业随之崛起&#xff0c;为保证用户体验及运营管理&#xff0c;充电桩需要通过网络实现数据传输、远程监控、位置…

19-递归的理解、场景

一、递归 &#x1f32d;&#x1f32d;&#x1f32d;在函数内部&#xff0c;可以调用其他函数。如果一个函数在内部调用自身本身&#xff0c;这个函数就是递归函数 核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解 一般来说&#xff0c;递归…