手写一个PrattParser基本运算解析器3: 基于Swift的PrattParser的项目概述

点击查看 基于Swift的PrattParser项目


PrattParser项目概述

前段时间一直想着手恶补 编译原理 的相关知识, 一开始打算直接读大学的 编译原理, 虽然内容丰富, 但是着实抽象难懂. 无意间看到B站的熊爷关于普拉特解析器相关内容, 感觉是一个非常好的切入点.所以就写了基于Swift版本的 PrattParser.

下面是我整理的项目中各个类以及其中函数的作用.

更加具体的请查看 PrattParser解释器项目类与函数


接下来, 我把整个项目UML图发出来, 大家可以借鉴查看.

更加具体的请查看 PrattParser的Swift项目UML图


接下来, 我就以 词法分析语法分析中间代码生成 三部分逐步来说明一下这个 基于Swift的PrattParser项目


词法分析

词法分析的核心类是 Lexer, 输入的原始代码字符串 code, 输出的是一组词法单元 Token.

在词法分析器 Lexer 中, 核心函数就是 nextToken, nextToken函数职责一共有两个职责.

  • 去除代码格式化的逻辑, 例如, 去除 空格 换行 等等. 这一步主要是通过调用 skipWhitespace() 函数实现的.

    public func skipWhitespace() {while (hasNext()) {if (word == " " || word == "\t" || word == "\n" || word == "\r")  {readCodeAction();} else {break}}
    }
    
  • 读取数学符号与数字并且生成 词法单元Token

    switch(word) {
    case "+" :token = PrattParser.Token(TokenType.PLUS, "+")break
    case "-" :token = PrattParser.Token(TokenType.MINUS, "-")break
    case "*" :token = PrattParser.Token(TokenType.ASTERISK, "*")break
    case "/" :token = PrattParser.Token(TokenType.SLASH, "/")break
    case "(" :token = PrattParser.Token(TokenType.LPAREN, "(")break
    case ")" :token = PrattParser.Token(TokenType.RPAREN, ")")break
    case "^" :token = PrattParser.Token(TokenType.HAT, "^")break
    case nil :token = PrattParser.Token(TokenType.EOF, "")break
    default:if (isDigit(word)) {let num: String = readNum();token = PrattParser.Token(TokenType.NUM, num);return token;} else {throw LexerError.lexerError(message: "Lexer error")}
    }
    

生成词法单元函数 nextToken 的整体逻辑流程图如下所示. 基本涉及了词法分析器 Lexer 的所有函数.

这里要补充的一点的就是由于数学符号大部分是单个字符, 例如 + - * / ( ), 这个读取直接生成即可. 但是数字可能是有多位的, 所以生成的过程需要通过循环一直查找. 在该项目中的代码实现中读取数字字符的逻辑代码主要存在于 readNum 函数中.

public func readNum() -> String {var num: String = ""while (isDigit(word)) {num += word ?? ""readCodeAction()}return num;
}

生成数字函数 readNum 的整体逻辑流程图如下所示.

在该项目中, 词法分析器Lexer 的外部驱动力是 语法分析器Parser, 也就是说语法分析器Parser一直在调用 LexernextToken 函数从而不断地生成词法单元 Token.


语法分析

词法分析 模块, 我们了解到了 词法分析器Lexer 会为 语法分析器Parser 提供源源不断生成的词法单元 Token.

语法分析器Parser 则会这些词法单元 Token根据 符号的优先级 生成一颗 AST语法树.

语法分析器Parser 生成 AST语法树 的过程中, 其入口函数是 parseMain(), 核心函数是 parseExpression(), 具体代码如下所示.

func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 生成前置节点, 获取左节点var leftExpression: Expression? = getPrefixExpression(funcName);// 能递归的原因 判断下一个词法单元是否是EOF, 判断下一个词法单元的优先级是否大于当前的优先级while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}//读取下一个词法单元nextToken();// 生成中置节点, 更新AST语法树leftExpression = parseInfixExpression(leftExpression);}return leftExpression
}

由于递归过程比较复杂, 我整理了一下整体的逻辑流程图.

当我们看到上一个图的时候, 我们会诧异, 说好的递归过程在哪呢? 其实递归过程主要隐藏在生成中置节点函数 parseInfixExpression() 中, 由于 parseExpression()parseInfixExpression()parseExpression().... 的调用关系会最终产生递归效果.

在中置节点生成函数parseInfixExpression中, 右节点的生成依然会依赖 parseExpression(), 这也就递归产生的驱动力.

// 中置节点生成函数
func parseInfixExpression(_ left: Expression?) -> Expression? {let infixExpression = InfixExpression();infixExpression.left = left;infixExpression.operatorValue = cur?.value;let precedence: Precedence = Precedence.getPrecedence(cur?.type);nextToken();// 右节点的生成是递归产生的驱动力infixExpression.right = parseExpression(precedence);return infixExpression
}

中置节点生成函数parseInfixExpression的逻辑流程图如下所示.


粗略的说了大致的流程, 接下来, 我们就详情的说一下具体的执行流程.

具体的以 1 + 4 - 31 + 2 * 3 两个数学运算为示例.


1 + 4 - 3 的AST语法树构建过程

强烈建议大家一边项目断点, 一边对照该模块的流程!!!

  • 整体还是以 parseMain() 为入口, 初始过程中会传入一个最低的优先级(Precedence.LOWEST)用于驱动整个AST语法树的构建. 当然了, 这时候词法单元读取模块也已经准备就绪了.

    // 构建AST树主入口
    public func parseMain() -> Expression? {return parseExpression(Precedence.LOWEST);
    }
    

  • 通过 parseMain 函数进入的 parseExpression() 函数中, 首先找的就是前置节点, 通过 词法单元读取模块 获取到第一个词法单元 1. 并且生成根据 前置节点的类型 生成 数字类型的AST前置节点. getPrefixExpression 就不过多叙述了, 比较简单.

    let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];
    if (funcName == nil) {print("未找到AST节点构建函数名称")return nil
    }
    // 获取左节点
    var leftExpression: Expression? = getPrefixExpression(funcName);
    
    func getPrefixExpression(_ funcName: String?) -> Expression? {switch(funcName) {case "parseInteger" :return parseInteger()case "parsePrefixExpression":return parsePrefixExpression()case "parseGroupExpression":return parseGroupExpression()default:return nil}
    }
    
    func parseInteger() -> Expression? {let number = Double(cur?.value ?? "0")let integerExpression = IntegerExpression(value: number)return integerExpression
    }
    

  • 紧接着就是去找到中置节点, 这时候通过peekPrecedence() 知道下一个词法单元为 +, 优先级较高, 满足优先级条件. 进入递归循环. 然后nextToken()读取下一个词法单元 +, 然后通过调用 parseInfixExpression() 尝试生成AST中的中置节点.

    while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}nextToken();leftExpression = parseInfixExpression(leftExpression);
    }
    

  • 在中置节点生成函数 parseInfixExpression() 中, 由于当前的词法单元为 + , 左节点为前置节点 1, 我们可以直接构建出这一部分的AST语法树.

    let infixExpression = InfixExpression();
    infixExpression.left = left;
    infixExpression.operatorValue = cur?.value;
    

  • 构建了中置节点的值和左节点, 我们尝试用 parseExpression() 递归的形式找到 +的中置节点 的右节点, 我们需要先读取当前 + 的优先级(Precedence.SUM), 然后读取下一个节点.

    let precedence: Precedence = Precedence.getPrecedence(cur?.type);
    nextToken();
    infixExpression.right = parseExpression(precedence);
    

  • parseExpression() 寻找 +的中置节点 的右节点, 首先, 就是获取 数字词法单元4 生成前置节点, 然后往后读取, 发现是 符号词法单元- 优先级与 当前 符号词法单元+ 的优先级相同, 所以就不进入while循环, 故+的中置节点 的右节点是 前置节点4.

    // 参数优先级为 Precedence.SUM
    func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 获取左节点, 生成 数字前置节点 4var leftExpression: Expression? = getPrefixExpression(funcName);//  - 与 + 的优先级相同不进入while循环while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}nextToken();leftExpression = parseInfixExpression(leftExpression);}// 返回 数字前置节点 4return leftExpression
    }
    

  • 这时候, 对于 中置节点+号 的AST语法树就构建完成了, 如图所示.

  • 然后外部又一次进行while循环, 这次找到的是 ➖ 号, 然后把 中置节点+号 的AST语法树 整体作为➖中置节点的左节点传入.

    // 这时候再次进入 减号➖ 的循环中
    while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}// 读取词法单元减号➖nextToken();// 这里的leftExpression是 加号➕ 的AST语法树//   +//  ╱ ╲// 1   4leftExpression = parseInfixExpression(leftExpression);
    }
    

  • ➖ 号的中置节点 构建过程中, 中置节点+号 的AST语法树 作为其左节点, - 作为其值, 右节点继续通过parseExpression()寻找.

    let infixExpression = InfixExpression();
    // 这里的left是 加号➕ 的AST语法树
    //   +
    //  ╱ ╲
    // 1   4
    infixExpression.left = left;
    infixExpression.operatorValue = cur?.value;

  • 中置节点+号 寻找右节点的逻辑是一样. 我们继续尝试用 parseExpression() 递归的形式找到 -的中置节点 的右节点, 我们需要先读取当前 - 的优先级(Precedence.SUM), 然后读取下一个节点.

    let precedence: Precedence = Precedence.getPrecedence(cur?.type);
    nextToken();
    infixExpression.right = parseExpression(precedence);
    

  • 这次在 parseExpression() 就很简单了, 我们先构建了前置节点3 然后往后查找过程发现是结束词法单元EOF, 我们直接返回 前置节点3 即可.

    func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 构建 前置节点3var leftExpression: Expression? = getPrefixExpression(funcName);// peekToken == EOF 不满足while循环条件while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}nextToken();leftExpression = parseInfixExpression(leftExpression);}// 返回 前置节点3return leftExpression
    }
    

  • 返回了右节点之后, 我们就直接构建 ➖减号的AST语法树, 这里看一下整体的构建过程.

  • ➖减号的AST语法树 然后再次返回整体的循环, 发现当前的词法节点以及全部循环完成了, 所以就跳出了while循环, 返回最终的AST语法树. 这里就把整理的流程贴图如下所示.

  • 所以1 + 4 - 3 形成的AST语法树是这样的. 如下图所示.


1 + 2 * 3 的AST语法树构建过程

相比于 1 + 4 - 3 的最终结果来说, 1 + 2 * 3 其中 乘法* 一定要比 加法+ 优先级高. 最终应该是这样的 1 + (2 * 3) . 也就是我们预想的AST语法树应该如下所示.

接下来, 我们就一起看一下 1 + 2 * 3 的AST语法树构建逻辑.

  • 对于 1 + 2 * 3 一直到加法的中置节点寻找右节点之前的逻辑都是与先前一样的. 这里直接贴图了, 就不过多叙述代码了.

  • 接下来, 对于 中置节点加号+ 需要通过 parseExpression() 去寻找它自身的右节点. 这时候准备工作也要做好, 读取下一个词法单元2, 获取当前加号的优先级(Precedence.SUM).

    // 当前加号的优先级为 Precedence.SUM
    let precedence: Precedence = Precedence.getPrecedence(cur?.type);
    // 下一个词法单元为 词法单元2
    nextToken();
    // 寻找 中置节点加号+ 的 右节点
    infixExpression.right = parseExpression(precedence);
    

  • 然后, 在 parseExpression() 就是先构建 前置节点2, 然后查看后一个词法单元, 发现是 乘法符号*, 乘法符号的优先级(Precedence.PRODUCT) 要比 加法符号的优先级(Precedence.SUM) 要高, 所以进入while循环中. 继续构建关于 中置节点乘法* 的相关AST语法树.

    // 当前优先级是 Precedence.SUM, 当前Token是 2
    func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 构建左节点 前置节点2var leftExpression: Expression? = getPrefixExpression(funcName);// 乘法符号的优先级比当前加号优先级高, 正常进入while循环while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}// 读取下一个词法单元 乘法符号*nextToken();// 生成乘法符号的中置节点并且更新leftExpressionleftExpression = parseInfixExpression(leftExpression);}return leftExpression
    }
    
    let infixExpression = InfixExpression();
    infixExpression.left = left;
    infixExpression.operatorValue = cur?.value;
    // 当前的乘法符号 的AST语法树
    //   *
    //  ╱ ╲
    // 2   ?
    

  • 紧接着, 就是寻找乘法AST语法树的右节点, 仍然是通过 parseExpression() 函数, 传入的Token则是 词法单元3, 乘法符号的优先级为 Precedence.PRODUCT,

    // 乘法符号的优先级为 Precedence.PRODUCT
    let precedence: Precedence = Precedence.getPrecedence(cur?.type);
    // 读取词法单元3
    nextToken();
    infixExpression.right = parseExpression(precedence);
    

  • 在这次乘法符号寻找右节点的 parseExpression() 中, 首先构建了 前置节点3, 由于看到下一个节点是 结束词法单元EOF, 所以不进入循环, 直接返回 前置节点3.

    func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 构建 前置节点3var leftExpression: Expression? = getPrefixExpression(funcName);// peekToken == EOF 不满足while循环条件while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}nextToken();leftExpression = parseInfixExpression(leftExpression);}// 返回 前置节点3return leftExpression
    }
    

  • 这时候也就构建完成了乘法的AST语法树部分了. 我们一起看一下整体的乘法符号的AST语法树构建过程.

  • 由于已经遍历到了最后(遇到了EOF), 紧接着就跳出了 加法符号寻找右节点的parseExpression()过程中的while循环. 并把 乘法符号的AST语法树作为 加法符号的右节点进行了添加.

    // 这里是加法符号寻找右节点的递归方法
    // 当前优先级是 Precedence.SUM, 当前Token是 2
    func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 构建左节点 前置节点2var leftExpression: Expression? = getPrefixExpression(funcName);// 乘法符号的优先级比当前加号优先级高, 正常进入while循环while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}// 读取下一个词法单元 乘法符号*nextToken();// 生成乘法符号的中置节点并且更新leftExpressionleftExpression = parseInfixExpression(leftExpression);}// 最终 leftExpression 是乘法符号的AST语法树//   *//  ╱ ╲// 2   3return leftExpression
    }
    

    上述代码就是下图中 红色的parseExpression()的内部过程.

  • 最后返回初始那一层 parseMain() 进入的 parseExpression() 过程, 也是已经遍历到了最后(遇到了EOF), 跳出循环, 返回最终的AST语法树.

    // 这里是由 `parseMain()` 进入的 `parseExpression()`
    func parseExpression(_ precedence: Precedence) -> Expression? {let funcName: String?  = prefixParseFnHashMap[cur?.type ?? TokenType.None];if (funcName == nil) {print("未找到AST节点构建函数名称")return nil}// 构建左节点 前置节点1var leftExpression: Expression? = getPrefixExpression(funcName);// 构建了加法的AST语法树之后, 就退出了循环while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {let infixParseFnName: String?  = infixParseFnHashMap[peek?.type ?? TokenType.None];if (infixParseFnName == nil) {print("未找到AST节点构建函数名称")return leftExpression;}nextToken();leftExpression = parseInfixExpression(leftExpression);}// 最终 leftExpression 是加法符号的AST语法树//   +//  ╱ ╲// 1   *//    ╱ ╲//   2   3return leftExpression
    }
    

  • 这样我们对于数学表达式的 1 + 2 * 3 的 AST语法树构建过程就有整体的了解,最终输出的AST语法树如下所示.


中间代码生成与验证

对于上面的 1 + 4 - 31 + 2 * 3 的两个示例, 我们对PrattParser构建AST语法树的过程有了大体的了解.

接下来就是中间代码生成过程(其实不太准确, 大体模拟吧~), 我们会直接输入一个结果对象(MInt), 模拟中间代码的生成.

中间代码生成是由 Evaluator 来实现的, 其主要作用就是解析AST语法树, 生成中间代码结果对象(MInt). 这部分也是很简单, 主要是通过 eval() 函数来递归解析AST语法树, 然后通过 op() 函数进行各种数学计算. 整体的计算也是由递归完成的.

  • eval() 函数中 主要有三个逻辑分支, 一个是数字的前置节点 一个是符号的前置节点, 最后一个是中置节点. 数字的前置节点中置节点 没有什么好说的, 符号的前置节点 主要应对于第一个前置节点带符号的情况例如 -1+349 等等.

    public static func eval(_ node: Node?) -> MObj? {if let nodeValue = node as? IntegerExpression {// 纯数字节点逻辑return MInt(nodeValue.value)} else if let nodeValue = node as? PrefixExpression {// 带符号的数字节点逻辑if (nodeValue.operatorValue == "-") {return minus(node);} else if (nodeValue.operatorValue == "+") {return eval(nodeValue.right);}} else if let nodeValue = node as? InfixExpression {// 中置节点逻辑let left = eval(nodeValue.left);let right = eval(nodeValue.right);return op(left, right, nodeValue.operatorValue);}return nil;
    }
    
  • op() 就是根据符号进行不同的数学运算, 整体逻辑比较简单, 这里就不过多叙述了.

    public static func op(_ left: MObj?, _ right: MObj?, _ operatorValue: String?) -> MObj? {if let leftValue = left as? MInt,let rightValue = right as? MInt {switch(operatorValue) {case "+" :return MInt(leftValue.number + rightValue.number)case "-" :return MInt(leftValue.number - rightValue.number)case "*" :return MInt(leftValue.number * rightValue.number)case "/" :return MInt(leftValue.number / rightValue.number)case "^" :return MInt(pow(leftValue.number, rightValue.number))default:return nil;}}return nil;
    }
    
  • minus() 函数主要是用来处理带符号的前置节点情况. 整体逻辑也比较简单, 这里就不过多叙述了.

    public static func minus(_ node: Node?) -> MObj? {if let nodeValue = node as? PrefixExpression {let m : MObj? = eval(nodeValue.right);if let mValue = m as? MInt {if (nodeValue.operatorValue == "-") {mValue.number = -mValue.number}return mValue;}}return nil;
    }
    

最后, 我们就能看到最终的输出结果.

var code = "1+2*3"var lexer: Lexer! = Lexer(code: code)var parser: Parser! = Parser(lexer)var expression: Expression? = parser.parseMain()if let intObj = Evaluator.eval(expression) as? MInt {print(intObj.toString())
}


总结

通过这篇博客详细大家对 PrattParser解析器的前端工作有个大体的了解了. 希望看这篇博客是可以一边断点项目, 一边查看, 主要是递归过程比较绕, 希望有耐心看完.


点击查看 基于Swift的PrattParser项目


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

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

相关文章

【网络安全 --- win10系统安装】win10 系统详细安装过程(提供资源)

一&#xff0c;资源下载 百度网盘镜像下载地址链接&#xff1a; 百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固&#xff0c;支持教育网加速&#xff0c;支持手机端。注册使用百度网盘即可享受免费存储空间https://pan.ba…

自定义Docker镜像--Jupyterlab

概述 自定义Jupyterlab镜像&#xff0c;为deployment做准备 步骤 下载基础镜像&#xff1a;centos:7.9.2009 docker search centos:7.9.2009 docker pull centos:7.9.2009 启动容器 部署应用 # 启动容器 docker run -it --name test centos:7.9.2009 bash# 在容器内部署…

4K壁纸小程序源码 全内容自动采集

全内容自动采集 4K壁纸小程序源码&#xff0c;带流量主。用的都是一个接口&#xff0c;不过这个不知是谁改的&#xff0c;成了LSP版&#xff0c;是真色啊&#xff0c;专搜小姐姐。 4K壁纸&#xff0c;静态壁纸&#xff0c;头像等都有保留&#xff0c;界面广告位很多&#xff0c…

手机通过WiFi连接调试UR机器人

1.测试物料 1.1ur机器人 https://item.taobao.com/item.htm?spma1z10.1-c.w4004-25069442759.18.2ff56d6bmuxX0Z&id740002623764 1.2 路由器&#xff08;TPLINK&#xff09; https://detail.tmall.com/item.htm?abbucket7&id548610924784&ns1&spma21n57.1.…

英国人工智能公司【TitanML】完成280万美元融资

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;总部位于英国伦敦的人工智能公司【TitanML】近期宣布已完成280万美元种子轮融资&#xff0c;该公司的产品允许机器学习团队部署大型语言模型(llm)。 本轮融资由Octopus Ventures领投&#xff0c;还…

Linux中的主要系统调用

Linux 操作系统中就是创建进程。创建进程的系统调用叫fork。在 Linux 里&#xff0c;要创建一个新的进程&#xff0c;需要一个老的进程调用 fork 来实现&#xff0c;其中老的进程叫作父进程&#xff08;Parent Process&#xff09;&#xff0c;新的进程叫作子进程&#xff08;C…

Redux详解(二)

1. 认识Redux Toolkit Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。 通过传统的redux编写逻辑方式&#xff0c;会造成文件分离过多&#xff0c;逻辑抽离过于繁琐&#xff08;具体可看上篇文章 Redux详解一&#xff09;&#xff0c;React官方为解决这一问题&#xff0c;推…

小样本学习--(1)概论

目录 一、概述 二、小样本学习的数据集 1、Omniglot 2、MiniimageNet 三、孪生网络 四、三元组损失函数 一、概述 小样本学习用于处理训练数据集中样本数量少的情况&#xff0c;一般来说&#xff0c;小样本学习流程是这样的&#xff0c;从一个多种类少量样本的巨大数据集…

微信小程序开发之会议oa(首页搭建)

前言&#xff1a; 上一篇我们掌握了关于小程序的框架&#xff0c;这篇博客带你完成小程序版的会议OA首页。效果如下&#xff1a; 一&#xff0c; 1.1先创建OA首页页面&#xff1a; 首先我们先建一个新项目&#xff0c;在app.json中编写代码 {"pages": ["pages/…

Tang Capital宣布收购纳斯达克上市公司Rain Oncology100%股权

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;纳斯达克上市公司Rain Oncology(Rain)宣布近期已收到Tang Capital Partners旗下的子公司Concentra Biosciences以每股1.25美元的现金收购要约。 这家临床阶段微型市值癌症治疗药物开发商的股价在消…

初识Java 14-1 测试

目录 测试 单元测试 JUnit 测试覆盖率 前置条件 断言 Java提供的断言语法 Guava提供的更方便的断言 契约式设计中的断言 DbC 单元测试 Guava中的前置条件 本笔记参考自&#xff1a; 《On Java 中文版》 测试 ||| 如果没有经过测试&#xff0c;代码就不可能正常工作…

C++数据结构X篇_16_二叉树的拷贝和释放(采用递归的方法)

在上篇的基础上&#xff0c;本篇介绍如何进行二叉树的拷贝和释放。从代码中可以看到采用递归方式进行的拷贝、释放操作&#xff0c;基本套路都是一样的。 文章目录 1. 二叉树的拷贝2. 二叉树的释放 1. 二叉树的拷贝 #include <iostream> using namespace std;//定义二叉…