当前位置: 首页 > news >正文

简易计算器(c++ 实现)

前言

本文将用 c++ 实现一个终端计算器:

  • 能进行加减乘除、取余乘方运算
  • 读取命令行输入,输出计算结果
  • 当输入表达式存在语法错误时,报告错误,但程序应能继续运行
  • 当输出 ‘q’ 时,退出计算器

【简单演示】
在这里插入图片描述
【源码位置】 Calculator 的 src_old 目录下

如果读者学过编译原理,那么实现简易计算器对你来说将轻而易举;倘若你没学过也没关系,本文将从初学者的角度带领你做出满足上述要求的计算器。


一、词法分析

假设有如下的表达式:

> 10 + 1

那么计算器将输出:= 11。
对于计算器而言,“10 + 1” 仅仅是一串字符,它是怎么”看懂“的,又是怎么计算的呢?

举一个大家都懂的例子:翻译英语。
给定一英语 “hello world.”,你为什么能翻译出它的意思?因为你学过 “hello”、“world” 两个单词,知道他们的意思,所以你会翻译这句话。也就是说,学过单词的你,这句话在你的眼中相当于两个连续的单词:“hello”、“world”;但如果你没学过英语,可能你看到的就是一个一个的字母而已 ‘h’、‘e’、‘l’ … 这就是词法分析,简单说就是识别出字符串中基本单元——单词 (token)。

再来分析上述例子:“10 + 1”
从人的角度来看,接收这串字符,你将识别出这些 token: “10”、“+”、“1”;接着,你明白 “10” 是一个数字,“+” 是一个加法运算符,“1” 是一个数字,此时你明白这是一个加法式子,你便能计算出结果为 “11”。因此,计算器第一步应该像学英语一样先学习单词,让它能像人一样识别字符串中的 token,这就是第一步词法分析

1. TokenKind

学习英语单词时,不仅要背翻译,还要记忆词性(动词、形容词等),这样的目的是为了之后的语法分析,比如:主语后往往接一个动词。
对于计算器也是类似的,识别出一个 token 后,计算器将关心这个 token 的

  • kind(相当于词性):它是一个数字,还是一个运算符?
  • value(相当于翻译):token("10”).value = 10, token(‘+’).value = +

根据上述描述,你可能想到的方案如下:

class Token
{
public:std::string value;	// 字符串存储值TokenKind	kind;	// 枚举类 TokenKind 标识类型字段
};

这个方案可行,但比较复杂:

  • 对于数字来说:只需要先判断 kind,即可得到 value
  • 对于运算符来说:也需要先判断 kind,再取 value

看下面的方案:

class Token
{
public:std::string value;char		kind;
};

该方案用 char 替代了 TokenKind,为什么呢?对于运算符 ‘+’、‘-’、‘*’、‘/’、‘%’、‘^’,你会发现他们都是单字符,同时各不相同,而且都是非字母数字字符,那么这么做就有一个好处:对于运算符来说,kind 就是 value,这能简化后面的编码(语法分析部分可体会到)。

因此,对于 Token 的设计采用第二种方案。

2. Token

上一部分说到,Token 有两个字段:value、kind。对于运算符来说,仅 kind 字段有效;对于数字来说,两字段均有效。也就是说 value 仅当 kind 表示数字时有效,因此,我采用自己设计的 Number 类(代码位于 Number 文件夹下) 来存储 value:

class Token
{
public:static const char kd_null   = '`';      // nullstatic const char kd_number = 'n';      // numberstatic const char kd_quit   = 'q';      // 结束程序public:Token() :val{ }, kind{ kd_null } { }~Token() = default;public:bool is_null() const { return kind == kd_null; }public:		/* 方便后续编码 */Number val;char   kind;
};

3. TokenStream

c++ 的 cin 关联到控制台输入,将输入字符视为字符流,可以使用 cin.get() 获取流中的第一个字符。这是不是跟词法分析的任务很像:从表达式中识别出 token,并一个个地返回

class TokenStream
{
public:TokenStream(std::istream& is);~TokenStream() = default;public:bool eof() const;Token get();	// 返回流中的第一个 tokenToken peek();  	// 查看流中的第一个 tokenprivate:std::istream& _is;			// 与输入流相关联,从其中读取 tokenbool          _eof;			// 是否到流的结尾 bool          _full;		// _buffer 是否满了Token         _buffer;		// 缓冲区
};

在语法分析中,常常需要提前读取下一个 token:

简单举个例子:运算符后应该跟一个数字或者左括号(“1 + 1”、“1 + (2 - 1)”)。那么如果当前处理的 token.kind == ‘+’,那么下一个 token 要么是数字,要么是左括号,两种情况处理结果不一样。为此,应该需要提前读取下一个 token,即 peek( ) 函数:与 get( ) 不同的是,peek( ) 仅仅是查看流中的下一个 token(假设是 X),调用后此时流中的第一个 token 仍然是 X;但是调用 get( ) 将返回流中的第一个 token,即 X 被读取了,此后流中的第一个 token 不再是 X。

为了实现 peek( ) 函数,TokenStream 引入缓冲区:_full 标记缓冲区是否已经满了,_buffer 保存当前流中的第一个 token:
在这里插入图片描述

Token TokenStream::peek() 
{if (!_full) {_buffer = get();_full = true;}return _buffer;
}

此类的难点在 get( ) 的实现:如何从流中识别出下一个 token。
在之前的分析,流中的 token 有如下几种:

number、‘q’、‘+’、‘-’、‘*’、‘/’、‘%’、'^‘、null (用来表示流已经没有 token 了,即到了流的末尾)

对于除了 number 的其他都是单字符,非常容易识别出;难点在于如何识别 number。下面来介绍如何识别 number:

number 包括 整数、浮点数

引入 “文法” 的概念,你可以简单理解为语法,它描述一种语言生成的方式。
先以最简单的整数(int)为例:+1、1、-1都是整数,即除去第一位的符号位,其余都是数字(digit),因此 int 对应文法如下:

  1. int ⇒ ‘+’ digit digits | ‘-’ digit digits | digit digits
  2. digits ⇒ digit digits | ‘’
  3. digit ⇒ ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’

【解释】

  1. 整数可以是
  • ‘+’ 后必须先接单个数字 (digit) 再接数字串 (digits)
  • ’-‘ 后必须先接单个数字再接数字串
  • 单个数字后接数字串
  1. 数字串可以是
  • 单个数字后接数字串
  • 空串 ( ‘’ )
  1. 单个数字为 ‘0’ ~ ‘9’

对此即可写对应程序识别出 int。
【示例程序】

std::string get_digits(std::istream& is)	/* digits ⇒ digit digits | '' */
{std::string digits;while (isdigit(is.peek())) digits.push_back(is.get());return digits;
}std::string get_int(std::istream& is)
{std::string num;char c = is.get();switch (c) {case '+': case '-':		/*  '+' digit digits | '-' digit digits */{num.push_back(c);if (isdigit(is.peek())) {num.push_back(is.get());num += get_digits(is);}else throw std::string{"bad number"};break;}case '0': case '1': case '2': case '3': case '4':case '5': case '6': case '7': case '8': case '9':  /* digit digits  */{num.push_back(c);num += get_digits(is);break;}default: throw std::string{"bad number"};}return num;
}

类似的,可以对应写出 number 识别程序:

【注意】“.123” 也视为 number

  • 文法
  1. number ⇒ int | int float | float
  2. float ⇒ ‘.’ int
  • 程序
std::string get_float(std::istream& is)
{if (is.peek() != '.') throw std::string{"bad float"};std::string res;res.push_back(is.get());res += get_int(is);return res;
}std::string get_number(std::istream& is)
{char c = is.peek();std::string num;switch (c) {case '.': {num = get_float(is);break;}case '0': case '1': case '2': case '3': case '4':case '5': case '6': case '7': case '8': case '9':{num = get_int(is);if (is.peek() == '.') num += get_float(is);break; }default: throw std::string{"bad number"};}return num;
}

上述为示例程序,在计算器程序中,我将使用自己设计的 Number 类,此类重载了输入运算符,即你可如下使用 Number:

Number num;
cin >> num;	// 如果读取 num 失败,将设置流状态 (cin.fail() == true)

因此 get( ) 函数如下:

Token TokenStream::get() 
{if (_full) {	// 先读取缓冲区中的 token_full = false;return _buffer;}Token token;if (_eof) return token;while (true) {char c = _is.peek();if (_is.eof()) {_eof = true;return Token{};}switch (c) {case ' ': 	/* 跳过空格 */{_is.get();continue;}case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.':{Number num;_is >> num;if (_is.fail()) throw CalExcep{"illegal number"};token.kind = Token::kd_number;token.val  = num;return token;}case globe::ADD: case globe::SUB: case globe::MUL: case globe::DIV: case globe::MOD: case globe::POW: case globe::LBRA: case globe::RBRA: case globe::QUIT:{token.kind = _is.get();return token;}default: throw CalExcep{ "illegal terminal character: '" + std::string{c} + "'" };}}return token;
}

阅读上面代码,你可能注意到一个问题:以 ‘+’、‘-’ 开头的为什么不当作数字去读取?看下面这个输入

1+1

‘+’ 之后紧接着是 ‘1’,倘若将它识别为 ‘+1’,这个输入将被识别为 ‘1’、‘+1’ 两个数字,那么语法分析处将报错:这不是一个合法表达式;但显然我们知道它是合法的。说到这里你可能会选择将 ‘+’、‘-’ 开头的不再当作数字,而是当作运算符,这种做法是正确的。倘若有如下输入

-1+1

你会发现上述输入将被识别为 “-”、“1”、“+”、“1”,这样的 token 流在语法分析处可通过计算器文法设计解决。


二、语法分析

从网上搜索计算器文法如下:

  1. expr ⇒ term | expr ‘+’ term | expr ‘-’ term
  2. term ⇒ factor | term ‘*’ factor | term ‘/’ factor | term ‘%’ factor
  3. factor ⇒ primary | primary ‘^’ factor
  4. primary ⇒ ‘(’ expr ‘)’ | number | ‘-’ number | ‘+’ number

[上面的每个式子称为 “产生式”]

此文法能正确描述出表达式,倘如你要问这文法是怎么来的,额,反正我不知道-_-。举个例子来看此文法是怎么描述表达式的:
先举一个简单的例子:

expr ⇒ term ⇒ factor ⇒ primay ⇒ number

因此单独一个数字也是表达式:“100” 是一个合法表达式。

expr
⇒ expr ‘+’ term ⇒ term ‘+’ term ⇒ primay ‘+’ term ‘*’ factor
⇒ ‘-’ number ‘+’ factor ‘*’ number ⇒ ‘-’ number + number ‘*’ number

也就是说 “-1 + 1 * 1” 是一个合法表达式。
上边的两个例子展示了如何通过文法产生合法表达式,下面来看它的逆过程:已知一表达式,检查是否满足文法要求,这就是语法分析。

先看简单例子:

1 + 1

假如它是合法表达式,那么一定存在如下推导:

expr ⇒ … ⇒ number ‘+’ number

现在就要寻找是否存在这样的推导。观察文法,有 ‘+’ 的是 “expr ⇒ expr ‘+’ term”,因此第一步推导如下:

expr ⇒ expr ‘+’ term ⇒ … ⇒ number ‘+’ number

那么接下来就是找出是否存在 “expr ⇒ … ⇒ number”,显然有,因为

expr ⇒ term ⇒ factor ⇒ primay ⇒ number

同时也找到了 “term ⇒ … ⇒ number”,所以,“1 + 1” 满足文法,它是合法表达式。这个过程可用树形结构来表示:
在这里插入图片描述
看一个较为复杂的例子:

在这里插入图片描述
上面是人的角度来选取文法产生式的,但我们的目标是用程序实现。先看第一个文法产生式:

expr ⇒ term | expr ‘+’ term | expr ‘-’ term

那么对应伪代码应该为

expr()
{case 1: term();case 2: expr(); get('+'); term();case 3: expr(); get('-'); term();other : throw "bad expression";
}

将此伪代码转为 c++ 代码,需要解决一个问题:怎么判断什么情况下选用 case 1、case 2、case 3。你可能想到只需要判断在 term( )、expr( )、expr( ) 之后的下一个 token 是什么即可。那来看下面的输入:

2 * 3 + 1

从 expr 出发,第一次调用 case 1: term( ),即用 term( ) 去分析 “2 * 3 + 1”,那么一定会分析出这是一个非法表达式(因为 term 一定不能产生出 ‘+’),那么就无法判断下一个 token 了,显然这样做是不对的;应该回溯到 expr( ),再次判断 case 2、case 3,当都不满足时,才抛出异常 “bad expression”。
这样做的确可以,但由于用到回溯算法,效率自然不会高。有没有什么方法提高效率呢?改写文法,构造预测分析表

此处涉及到编译原理 LL文法 知识,读者如果有兴趣可自行了解,不了解也无所谓,在下面只需要会根据 LL(1) 预测分析表 编写代码即可。

在这里对文法先进行消除左递归,合并左公因子 得到满足 LL 文法要求的新文法:

  1. expr ⇒ term E
  2. E ⇒ ‘+’ term E | - term E | ‘’
  3. term ⇒ factor T
  4. T ⇒ ‘*’ factor T | ‘/’ factor T | ‘’
  5. factor ⇒ primary F
  6. F ⇒ ^ factor | ‘’
  7. primary ⇒ ‘(’ expr ‘)’ | number | ‘+’ number | ‘-’ number

利用 FIRST集、FOLLOW集 构造出 LL(1) 预测分析表 (想了解的可点击此处):

(+-*/%^)numbereof
exprterm Eterm Eterm Eterm E
E‘+’ term E‘-’ term E‘’‘’
termfactor Tfactor Tfactor Tfactor T
T‘’‘’‘*’ factor T‘/’ factor T‘%’ factor T‘’‘’
factorprimary Fprimary Fprimary Fprimary F
F‘’‘’‘’‘’‘’‘^’ factor‘’‘’
primary‘(’ expr ‘)’‘+’ number‘-’ numbernumber

【说明】

  • 第一行表示终结符,表达式的基本单元,即 token
  • 第一列为产生式左部
  • eof 表示当前为流的末尾(无 token 可读取)
  • ‘’ 表示空串
  • 为空 (不是空串) 的地方表示出现语法错误
  • 第二行第一列:表示在 expr( ) 分析时,如果下一个 token 为 ‘(’,则应使用产生式 “expr ⇒ term E”;由于产生式右部第一个单元为 term,不是终结符,进入 term 分析
  • 第三行第三列:表示在 E( ) 分析时,如果下一个 token 为 ‘+’,则应使用产生式 “E ⇒ ‘+’ term E”;由于产生式右部第一个单元为 ‘+’,是终结符并且等于当前 token,则读取此 token,此时产生式转为 “E ⇒ term E”,进入 term 分析。

【举例如何使用预测分析表】

  • 表达式为:“1 + 1”:

    • 此时正在分析 expr,下一个 token = number(‘1’),使用 “expr ⇒ term E”,进入 term 分析
    • 此时正在分析 term,下一个 token = number(‘1’),使用 “term ⇒ factor T”,则新的产生式为 “expr ⇒ factor T E”,进入 factor 分析
    • 此时正在分析 factor,下一个 token = number(‘1’),使用 “factor ⇒ primary F”,则新的产生式为 “expr ⇒ primary F T E”,进入 primary 分析
    • 此时正在分析 primary,下一个 token = number(‘1’),使用 “primary ⇒ number”,新的产生式为 “expr ⇒ number F T E”,由于 number 为终结符,恰好与 token 相同,故读取此 token,新的产生式为 “expr ⇒ F T E”,进入 F 分析
    • 此时正在分析 F,下一个 token = ‘+’,使用 " F ⇒ ‘’ "(F 转为空串),新的产生式为 “expr ⇒ T E”,进入 T 分析
    • 此时正在分析 T,下一个 token = ‘+’,使用 " T ⇒ ‘’ ",新的产生式为 “expr ⇒ E”,进入 E 分析
    • … …(以此类推)
    • 最终的产生式为 " expr ⇒ ‘’ "(expr 转为空串),因此语法分析结束,并且语法正确。 (如果最终产生式不为空串,语法分析错误)
  • 表达式为:" / 1":

    • 此时正在分析 expr,下一个 token = ‘/’,但是对应的预测分析表第二行第六列为空,因此语法错误
  • 表达式为:“+”

    • 此时正在分析 expr,下一个 token = ‘+’,使用 “expr ⇒ term E”,进入 term 分析
    • 此时正在分析 term,下一个 token = ‘+’,使用 “term ⇒ factor T”,新的产生式为 “expr ⇒ factor T E”,进入 factor 分析
    • 此时正在分析 factor,下一个 token = ‘+’,使用 “factor ⇒ primary F”,新的产生式为 “expr ⇒ primary F T E”,进入 primary 分析
    • 此时正在分析 primary,下一个 token = ‘+’,使用 “primary ⇒ ‘+’ number”,新的产生式为 “expr ⇒ ‘+’ number F T E”,产生式右部第一个单元 ‘+’ 与当前 token 相同,故读取当前 token,新的产生式为 “expr ⇒ number F T E”,下一个 token = eof,但是产生式第一个单元为终结符 number,与 eof 不相等,因此语法分析错误。

根据上述描述,设计出 Calculator 类:

class Calculator
{
public:Calculator() = default;~Calculator() = default;public:/* 计算 expression 结果并返回 */Number calculate(const std::string& expression);private:void expr	(TokenStream& ts);void E		(TokenStream& ts);void term	(TokenStream& ts);    void T		(TokenStream& ts);void factor	(TokenStream& ts);void F		(TokenStream& ts);void primary(TokenStream& ts);private:Number _val; /* 接受结果 */
};

_val 用于接收计算结果,在语义分析部分会使用到。

以 T 分析为例:(其他都类似)

void Calculator::T(TokenStream& ts) 
{auto peek = ts.peek();switch (peek.kind) {case globe::MUL:	// T ⇒ '*' factor T{ts.get();factor(ts);T(ts);break;}case globe::DIV:	// T ⇒ '/' factor T{ts.get();factor(ts);T(ts);break;}case globe::MOD:	// T ⇒ '%' factor T{ts.get();factor(ts);T(ts);break;}case globe::RBRA: case globe::ADD: 		// T ⇒ ''case globe::SUB: case Token::tk_null: break;default: throw CalExcep{"lack of operator"};	// throw}
}

三、语义分析

前面我们分析了词法、语法,都是在分析表达式的合法性,并没有去计算表达式的结果,在此部分变便来完成这个任务。
还是以英语翻译为例:

一个简单英语文法:

  1. 英语句子’⇒ 主语 动词
  2. 主语 ⇒ “I” | “You”
  3. 动词 ⇒ “see” | “say”

那么对于输入:“I see”,对应语法树为:
在这里插入图片描述
在语法树中,每个节点是文法的一部分,在这里给节点引入属性值这一字段,在原文法中引入语义动作:需要执行的程序片段,用 { } 包围

引入语义动作的英语文法:

  1. 英语句子’⇒ 主语 动词 {英语句子.val = 主语.val + 动词.val}
  2. 主语 ⇒ “I” { 主语.val = "I" 的翻译 }
  3. 主语 ⇒ “You” { 主语.val = "You" 的翻译 }
  4. 动词 ⇒ “see” { 动词.val = "see" 的翻译 }
  5. 动词 ⇒ “say” { 动词.val = "say" 的翻译 }

为此,含有语义动作的语法树变为:
在这里插入图片描述
所以,当我们引入语义动作后,完成了英语翻译这一任务。类似的,计算器语义分析也是如此:只需要在计算器表达式文法加入合适的语义动作,便能完成计算任务。

如何添加合适的语义动作呢?可以通过语法树来分析:
以 “1 + 1” 为例:
在这里插入图片描述
在上图中:“primary --> 1” 对应 “primary ⇒ number”,primary.val 应等于 number.value,因此有

primary ⇒ number {primary.val = number.value}

在树的根部处对应 “expr ⇒ expr ‘+’ term”,expr.val 应等于 expr.val + term.val,故有

expr ⇒ expr ‘+’ term {expr.val = expr.val + term.val}

其他的也是如此类推,当然上图使用的文法是原文法

  1. expr ⇒ term | expr ‘+’ term | expr ‘-’ term
  2. term ⇒ factor | term ‘*’ factor | term ‘/’ factor | term ‘%’ factor
  3. factor ⇒ primary | primary ‘^’ factor
  4. primary ⇒ ‘(’ expr ‘)’ | number | ‘-’ number | ‘+’ number

不是 LL 文法:

  1. expr ⇒ term E
  2. E ⇒ ‘+’ term E | - term E | ‘’
  3. term ⇒ factor T
  4. T ⇒ ‘*’ factor T | ‘/’ factor T | ‘’
  5. factor ⇒ primary F
  6. F ⇒ ^ factor | ‘’
  7. primary ⇒ ‘(’ expr ‘)’ | number | ‘+’ number | ‘-’ number

本文实现的计算器使用的是 LL 文法,如何给它加入语义动作呢?方法也是先画语法分析树,在根据自己的理解,自底向上地进行节点属性值的赋值,由此推导出语义动作。
读者可自行推导,下面是我自己采用的另外一种方法:不用节点属性值赋值,而是用一个变量保存结果(即之前说的 _val 成员变量),这样做的好处是减少了许多不必要的赋值。

  1. expr ⇒ term E
  2. E ⇒ {left = _val} ‘+’ term {_val = left + _val} E
  3. E ⇒ {left = _val} ‘-’ term {_val = left - _val} E
  4. E ⇒ ‘’
  5. term ⇒ factor T
  6. T ⇒ {left = _val} ‘*’ factor {_val = left * _val} T
  7. T ⇒ {left = _val} ‘/’ factor {_val = left / _val} T
  8. T ⇒ ‘’
  9. factor ⇒ primary F
  10. F ⇒ {left = _val} ^ factor {_val = left ^ _val}
  11. F ⇒ ‘’
  12. primary ⇒ ‘(’ expr ‘)’
  13. primary ⇒ number {_val = number}
  14. primary ⇒ ‘+’ number {_val = number}
  15. primary ⇒ ‘-’ number { _val = -1 * number}

则在 expr 分析完毕后,_val 就是计算器计算的结果。

【说明】E ⇒ {left = _val} ‘+’ term {_val = left + _val} E
在分析 E 时,先需要执行语义动作 {left = _val},保存左值(+、-、*、/、%、^ 都是二元运算符,因此在 ‘+’ 之前需要保存左值 left),之后读取 token(‘+’),然后进行 term 分析,此时 _val 保存的是右值,故执行 {_val = left + _val},结果等于 左值(left) + 右值(_val),之后再进行 E 分析。

当然可能你看到上述文法会感觉很疑惑,无法理解,最好理解的方式是自己画语法树,然后 debug 代码一步一步地理解,如果你有耐心的话。

【源码位置】 Calculator 的 src_old 目录下

相关文章:

简易计算器(c++ 实现)

前言 本文将用 c 实现一个终端计算器: 能进行加减乘除、取余乘方运算读取命令行输入,输出计算结果当输入表达式存在语法错误时,报告错误,但程序应能继续运行当输出 ‘q’ 时,退出计算器 【简单演示】 【源码位置】…...

AI大模型开发原理篇-4:神经概率语言模型NPLM

神经概率语言模型(NPLM)概述 神经概率语言模型(Neural Probabilistic Language Model, NPLM) 是一种基于神经网络的语言建模方法,它将传统的语言模型和神经网络结合在一起,能够更好地捕捉语言中的复杂规律…...

SpringBoot 基础特性

SpringBoot 基础特性 SpringApplication 相关特性 自定义 banner 在主配置文件写 banner.txt 的地址 #也可以不写默认路径就是 banner.txt #从类路径下找 banner #类路径就是 编译的target 目录 还有导入的第三方类路径。 spring.banner.locationclasspath:banner.txt#控制…...

网站快速收录:提高页面加载速度的重要性

本文转自:百万收录网 原文链接:https://www.baiwanshoulu.com/32.html 网站快速收录中,提高页面加载速度具有极其重要的意义。以下从多个方面详细阐述其重要性: 一、提升用户体验 减少用户等待时间:页面加载速度直接…...

如何使用formlinker,重构微软表单创建的数字生产力法则?

仅需三步:上传文件-下载文件-导入文件到微软表单 凌晨两点的格式炼狱:被浪费的300万小时人类创造力 剑桥大学的实验室曾捕捉到一组震撼数据:全球教育工作者每年花在调整试题格式上的时间,足够建造3座迪拜哈利法塔。当北京某高校的…...

从零搭建一个Vue3 + Typescript的脚手架——day3

3.项目拓展配置 (1).配置Pinia Pinia简介 Pinia 是 Vue.js 3 的状态管理库,它是一个轻量级、灵活、易于使用的状态管理库。Pinia 是 Vue.js 3 的官方状态管理库,它可以帮助开发者更好地管理应用的状态。Pinia 是一个开源项目,它有丰富的文档…...

Three.js实战项目02:vue3+three.js实现汽车展厅项目

文章目录 实战项目02项目预览项目创建初始化项目模型加载与展厅灯光加载汽车模型设置灯光材质设置完整项目下载实战项目02 项目预览 完整项目效果: 项目创建 创建项目: pnpm create vue安装包: pnpm add three@0.153.0 pnpm add gsap初始化项目 修改App.js代码&#x…...

Linux——网络(tcp)

文章目录 目录 文章目录 前言 一、TCP逻辑 1. 面向连接 三次握手(建立连接) 四次挥手(关闭连接) 2. 可靠性 3. 流量控制 4. 拥塞控制 5. 基于字节流 6. 全双工通信 7. 状态机 8. TCP头部结构 9. TCP的应用场景 二、编写tcp代码函数…...

Ubuntu Server 安装 XFCE4桌面

Ubuntu Server没有桌面环境,一些软件有桌面环境使用起来才更加方便,所以我尝试安装桌面环境。常用的桌面环境有:GNOME、KDE Plasma、XFCE4等。这里我选择安装XFCE4桌面环境,主要因为它是一个极轻量级的桌面环境,适合内…...

分享|通过Self-Instruct框架将语言模型与自生成指令对齐

结论 在大型 “指令调整” 语言模型依赖的人类编写指令数据存在数量、多样性和创造性局限, 从而阻碍模型通用性的背景下, Self - Instruct 框架, 通过 自动生成 并 筛选指令数据 微调预训练语言模型, 有效提升了其指令遵循能…...

指针空值——nullptr(C++11)——提升指针安全性的利器

C11引入的nullptr是对指针空值的正式支持,它提供了比传统NULL指针更加安全和明确的指针空值表示方式。在C语言中,指针操作是非常基础且常见的,而如何安全地处理指针空值,一直是开发者关注的重要问题。本文将详细讲解nullptr的引入…...

C++游戏开发

C 是游戏开发中广泛使用的编程语言,因其高性能、灵活性和对硬件的直接控制能力而备受青睐。以下是 C 游戏开发的一些关键点: 1. 游戏引擎 Unreal Engine:使用 C 作为主要编程语言,适合开发高质量 3D 游戏。Unity:虽然…...

【Docker】ubuntu中 Docker的使用

之前记录了 docker的安装 【环境配置】ubuntu中 Docker的安装; 本篇博客记录Dockerfile的示例,docker 的使用,包括镜像的构建、容器的启动、docker compose的使用等。   当安装好后,可查看docker的基本信息 docker info ## 查…...

Linux C openssl aes-128-cbc demo

openssl 各版本下载 https://openssl-library.org/source/old/index.html#include <stdio.h> #include <string.h> #include <openssl/aes.h> #include <openssl/rand.h> #include <openssl/evp.h>#define AES_KEY_BITS 128 #define GCM_IV_SIZ…...

【卫星通信】链路预算方法

本文介绍卫星通信中的链路预算方法&#xff0c;应该也适用于地面通信场景。 更多内容请关注gzh【通信Online】 文章目录 下行链路预算卫星侧参数信道参数用户侧参数 上行链路预算链路预算计算示例 下行链路预算 卫星侧参数 令卫星侧天线数为 M t M_t Mt​&#xff0c;每根天线…...

【Elasticsearch】 索引模板 ignore_missing_component_templates

解释 ignore_missing_component_templates 配置 在Elasticsearch中&#xff0c;ignore_missing_component_templates 是一个配置选项&#xff0c;用于处理索引模板中引用的组件模板可能不存在的情况。当您创建一个索引模板时&#xff0c;可以指定一个或多个组件模板&#xff0…...

Github 2025-01-30 Go开源项目日报 Top10

根据Github Trendings的统计,今日(2025-01-30统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Go项目10Ollama: 本地大型语言模型设置与运行 创建周期:248 天开发语言:Go协议类型:MIT LicenseStar数量:42421 个Fork数量:2724 次关注人…...

Linux 下注册分析(4)

系列文章目录 Linux 设备树 Linux 下注册分析&#xff08;1&#xff09; Linux 下注册分析&#xff08;2&#xff09; Linux 下注册分析&#xff08;3&#xff09; Linux 下注册分析&#xff08;4&#xff09; 文章目录 系列文章目录1、device_create简介device_createdevice_c…...

PhotoShop中JSX编辑器安装

1.使用ExtendScript Tookit CC编辑 1.安装 打开CEP Resource链接&#xff1a; CEP-Resources/ExtendScript-Toolkit at master Adobe-CEP/CEP-Resources (github.com) 将文件clone到本地或者下载到本地 点击AdobeExtendScriptToolKit_4_Ls22.exe安装&#xff0c;根据弹出的…...

目前市场主流的AI PC对于大模型本地部署的支持情况分析-Deepseek

以下是目前市场主流AI PC对**大模型本地部署支持情况**的综合分析&#xff0c;结合硬件能力、软件生态及厂商动态进行总结&#xff1a; --- ### **一、硬件配置与算力支持** 1. **核心处理器架构** - **异构计算方案&#xff08;CPUGPUNPU&#xff09;**&#xff1a;主流…...

51单片机开发:独立键盘实验

实验目的&#xff1a;按下键盘1时&#xff0c;点亮LED灯1。 键盘原理图如下图所示&#xff0c;可见&#xff0c;由于接GND&#xff0c;当键盘按下时&#xff0c;P3相应的端口为低电平。 键盘按下时会出现抖动&#xff0c;时间通常为5-10ms&#xff0c;代码中通过延时函数delay…...

微服务网关鉴权之sa-token

目录 前言 项目描述 使用技术 项目结构 要点 实现 前期准备 依赖准备 统一依赖版本 模块依赖 配置文件准备 登录准备 网关配置token解析拦截器 网关集成sa-token 配置sa-token接口鉴权 配置satoken权限、角色获取 通用模块配置用户拦截器 api模块配置feign…...

STM32 TIM输入捕获 测量频率

输入捕获简介&#xff1a; IC&#xff08;Input Capture&#xff09;输入捕获 输入捕获模式下&#xff0c;当通道输入引脚出现指定电平跳变时&#xff0c;当前CNT的值将被锁存到CCR中&#xff0c;可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数 每个高级定时器…...

python | OpenCV小记(一):cv2.imread(f) 读取图像操作(待更新)

python | OpenCV小记&#xff08;一&#xff09;&#xff1a;cv2.imread&#xff08;f&#xff09;读取图像操作 1. 为什么 [:, :, 0] 提取的是第一个通道&#xff08;B 通道&#xff09;&#xff1f;OpenCV 的通道存储格式索引操作 [:, :, 0] 的解释常见误解 1. 为什么 [:, :,…...

解析静态链接

文章目录 静态链接空间与地址分配相似段合并虚拟地址分配符号地址确定符号解析与重定位链接器优化重复代码消除函数链接级别静态库静态链接优缺点静态链接 一组目标文件经过链接器链接后形成的文件即可执行文件,如果没有动态库的加入,那么这个可执行文件被加载后无需再进行重…...

Ollama 运行从 ModelScope 下载的 GGUF 格式的模型

本文系统环境 Windows 10 Ollama 0.5.7 Ollama 是什么&#xff1f; Ollama 可以让你快速集成和部署本地 AI 模型。它支持各种不同的 AI 模型&#xff0c;并允许用户通过简单的 API 进行调用 Ollama 的安装 Ollama 官网 有其下载及安装方法&#xff0c;非常简便 但如果希…...

【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)

文章目录 【2025年最新版】Java JDK安装、环境配置教程 &#xff08;图文非常详细&#xff09;1. JDK介绍2. 下载 JDK3. 安装 JDK4. 配置环境变量5. 验证安装6. 创建并测试简单的 Java 程序6.1 创建 Java 程序&#xff1a;6.2 编译和运行程序&#xff1a;6.3 在显示或更改文件的…...

探索性测试与自动化测试的结合

随着软件开发周期的不断缩短和质量要求的不断提高&#xff0c;测试行业正在经历一场深刻的变革。自动化测试因其高效性和可重复性成为测试团队必不可少的工具&#xff0c;而探索性测试&#xff08;Exploratory Testing, ET&#xff09;则因其灵活性和创意性在面对复杂、动态变化…...

我是如何写作的?

以前是如何写作的 从小学三年级开始学写作文&#xff0c;看的作文书&#xff0c;老师布置作文题目&#xff0c;内容我都是自己写的。那时会积累一些好词&#xff0c;听到什么好词就记住了。并没有去观察什么&#xff0c;也没有好好花心思在写作上。总觉得我写的作文与真正好的…...

智慧园区管理系统为企业提供高效运作与风险控制的智能化解决方案

内容概要 快鲸智慧园区管理系统&#xff0c;作为一款备受欢迎的智能化管理解决方案&#xff0c;致力于为企业提供高效的运作效率与风险控制优化。具体来说&#xff0c;这套系统非常适用于工业园、产业园、物流园、写字楼及公寓等多种园区和商办场所。它通过数字化与智能化的手…...

INCOSE需求编写指南-附录 B: 首字母缩略词和缩写

附录 Appendix B: 首字母缩略词和缩写ACRONYMS AND ABBREVIATIONS AD 难易程度的进阶 Advancement Degree of Difficulty AI 人工智能 Artificial Intelligence CM 配置管理 Configuration Management ConOps 运作理念 Concept of Operations COTS 商业现货 Comme…...

VS2008 - debug版 - 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。

文章目录 VS2008 - debug版 - 由于应用程序配置不正确&#xff0c;应用程序未能启动。重新安装应用程序可能会纠正这个问题。概述笔记VS2008安装环境VS2008测试程序设置默认报错的情况措施1措施2备注 - exe清单文件的问题是否使用静态库?_BIND_TO_CURRENT_VCLIBS_VERSION的出处…...

Docker容器数据恢复

Docker容器数据恢复 1 创建mongo数据库时未挂载数据到宿主机2 查找数据卷位置3 将容器在宿主机上的数据复制到指定目录下4 修改docker-compose并挂载数据&#xff08;注意端口&#xff09;5 重新运行新容器 以mongodb8.0.3为例。 1 创建mongo数据库时未挂载数据到宿主机 versi…...

翼星求生服务器搭建【Icarus Dedicated Server For Linux】

一、前言 本次搭建的服务器为Steam平台一款名为Icarus的沙盒、生存、建造游戏,由于官方只提供了Windows版本服务器导致很多热爱Linux的小伙伴无法释怀,众所周知Linux才是专业服务器的唯一准则。虽然Github上已经有大佬制作了容器版本但是容终究不够完美,毕竟容器无法与原生L…...

如何在data.table中处理缺失值

&#x1f4ca;&#x1f4bb;【R语言进阶】轻松搞定缺失值&#xff0c;让数据清洗更高效&#xff01; &#x1f44b; 大家好呀&#xff01;今天我要和大家分享一个超实用的R语言技巧——如何在data.table中处理缺失值&#xff0c;并且提供了一个自定义函数calculate_missing_va…...

react中如何获取dom元素

实现代码 const inputRef useRef(null) inputRef.current.focus()...

引入@Inject的依赖包

maven引入Inject的依赖包 在 Maven 项目中引入 Inject 注解所需的依赖包同样取决于你打算使用的依赖注入框架。以下是一些常见框架及其 Maven 依赖配置的示例&#xff1a; 1. Google Guice 如果你打算使用 Google Guice&#xff0c;你需要在 pom.xml 文件中添加 Guice 的依赖…...

Deep Seek R1本地化部署

目录 说明 一、下载ollama 二、在ollama官网下载模型 三、使用 后记 说明 操作系统&#xff1a;win10 使用工具&#xff1a;ollama 一、下载ollama 从官网下载ollama&#xff1a; ollama默认安装在C盘&#xff0c;具体位置为C:\Users\用户名\AppData\Local\Programs\O…...

RDMA 工作原理 | 支持 RDMA 的网络协议

注&#xff1a;本文为 “RDMA” 相关文章合辑。 英文引文机翻未校。 图片清晰度受引文所限。 Introduction to Remote Direct Memory Access (RDMA) Written by: Dotan Barak on March 31, 2014.on February 13, 2015. What is RDMA? 什么是 RDMA&#xff1f; Direct me…...

再见了流氓软件~~

聊一聊 最近一直在测试软件&#xff0c;需要装各种软件和工具配合测试&#xff0c;导致现在电脑都快装满了&#xff0c;需要把不用的软件卸载。电脑自带的卸载只能一个一个卸载&#xff0c;不但麻烦还卸载不干净。 相信很多人也有这方面的需要&#xff0c;电脑装了很多软件&a…...

165. 比较版本号

两个注意的点&#xff1a; 分割字符串的时候&#xff0c;要用split("\\.")而不能用split(".")&#xff0c;因为前者表示“对.使用斜杠转义&#xff0c;\\表示一个斜杠”&#xff0c;而后者表示匹配任意单个字符&#xff0c;例如version2 "1.2.3&quo…...

一文大白话讲清楚webpack进阶——9——ModuleFederation实战

文章目录 一文大白话讲清楚webpack进阶——9——ModuleFederation实战1. 啥是ModuleFederation2. 创建容器应用3. 创建远程应用4. 启动远程应用5. 使用远程应用的组件 一文大白话讲清楚webpack进阶——9——ModuleFederation实战 1. 啥是ModuleFederation 先看这篇文章&#…...

【llm对话系统】LLM 大模型Prompt 怎么写?

如果说 LLM 是一个强大的工具&#xff0c;那么 Prompt 就是使用这个工具的“说明书”。一份好的 Prompt 可以引导 LLM 生成更准确、更相关、更符合你期望的输出。 今天&#xff0c;我们就来聊聊 LLM Prompt 的编写技巧&#xff0c;掌握这把解锁 LLM 潜能的钥匙&#xff01; 一…...

INCOSE需求编写指南-附录 C: 需求模式

附录 Appendix C: 需求模式 Requirement Patterns C.1 需求模式简介 Introduction to Requirement Patterns 需求模式&#xff08;样板或模板&#xff09;的概念最初于 1998 年在英国的未来水面战斗人员 (FSC) 国防项目中应用&#xff08;Dick 和 Llorens&#xff0c;2012 年…...

WGCLOUD使用介绍 - 如何监控ActiveMQ和RabbitMQ

根据WGCLOUD官网的信息&#xff0c;目前没有针对ActiveMQ和RabbitMQ这两个组件专门做适配 不过可以使用WGCLOUD已经具备的通用监测模块&#xff1a;进程监测、端口监测或者日志监测、接口监测 来对这两个组件进行监控...

【VASP】AIMD计算总结

【VASP】AIMD计算总结 vasp 计算文件INCAR 参数介绍后处理 二维材料与异质结的构造除了筛选优势还应该判断是否稳定&#xff0c;所以我在这分享一篇基于vasp6.2计算的AIMD 示例&#xff1a; https://www.vasp.at/wiki/index.php/Liquid_Si_-_Standard_MD vasp 计算文件 POSCA…...

春节旅游高峰,人力资源如何巧妙应对?‌

‌春节旅游高峰&#xff0c;人力资源如何巧妙应对&#xff1f;‌ 春节等假期一到&#xff0c;各大旅游景区便人潮汹涌&#xff0c;游客如织。面对这种旅游高峰&#xff0c;工作人员往往要连续超负荷运转&#xff0c;身心俱疲。特别是在那些热门景区和网红打卡地&#xff0c;人…...

zsh安装插件

0 zsh不仅在外观上比较美观&#xff0c;而且其具有强大的插件&#xff0c;如果不使用那就亏大了。 官方插件库 https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins 官方插件库并不一定有所有的插件&#xff0c;比如zsh-autosuggestions插件就不再列表里&#xff0c;下面演示zs…...

continuous batching、chunked-prefill相关概念

batching VS. continuous batching batching是所有requests的output都生成完毕之后&#xff0c;才能开始处理下一个batch。一般要做input padding&#xff0c;要等待凑够batch才运行&#xff08;也有超时bar&#xff09;。 continuous batching是每完成1个request&#xff0c;就…...

python算法和数据结构刷题[2]:链表、队列、栈

链表 链表的节点定义&#xff1a; class Node():def __init__(self,item,nextNone):self.itemitemself.nextNone 删除节点&#xff1a; 删除节点前的节点的next指针指向删除节点的后一个节点 添加节点&#xff1a; 单链表 class Node():"""单链表的结点&quo…...