【编译原理|实验】预测分析程序

一、实验目的

  1. 根据某一文法编制调试LL(1)分析程序,以便对任意输入的符号串进行分析。加深对预测分析分析法(LL(1))的理解。
  2. 通过设计、编写和调试构造LL(1)分析表的程序,了解构造LL(1)分析表的步骤以及对文法的要求,并能够从文法出发自动生成对应的LL(1)分析表。

二、实验要求

  1. 对下列文法,用LL(1)分析法对任意输入的符号串进行分析:
(1) E -> TG
(2) G -> +TG | -TG
(3) G -> ε
(4) T -> FS
(5) S -> *FS | /FS
(6) S -> ε
(7) F -> (E)
(8) F -> i
  1. 要求LL(1)分析表由程序自动生成。
  2. 输入/输出的格式要求:

(1)输入任意一个以#结束的符号串(包括+—*/()i#):。

(2)输出格式:

  1. LL(1)分析表如下:
表头表头表头
内容内容内容
  1. 分析栈
步骤分析栈剩余输入串所用产生式
1#Ei+i*i#E -> TG

在“所用产生式”一列中如果对应有推导则写出所用产生式;如果为匹配终结符则写明匹配的终结符;如分析异常出错则写为“分析出错”;若成功结束则写为“分析成功”。

三、实验步骤

1.明确语法分析程序的思想;
2.对指定文法进行必要的改造:消除左递归,提取左因子;
3.将程序划分成独立的模块;
4.给出主要模块的算法流程;
5.给出各模块完整可执行的程序代码(程序代码中应有关键变量及关键语句的注释)
6.给出程序使用说明、测试用例及程序运行示例,提交程序代码;
7.撰写实验报告。

四、实验分析

4.1 LL(1)分析法分析过程

在这里插入图片描述
上图所示的LL(1)分析器说明如下:

  1. 输入串是待分析的符号串,以界符“#”作为结束标志(注:#∈VT但不是文法符号,是由分析程序自动添加的);

  2. 分析栈中存放分析过程中的文法符号。分析开始时栈底先放一个“#”,然后再压入文法的开始符号。当分析栈中仅剩“#”,输入串指针也指向串尾的“#”时,分析成功;

  3. 分析表用一个二维数组M表示,它概括了相应文法的全部信息。数组的每一行与文法的一个非终结符相关联,而每一列与文法的一个终结符或界符“#”相关联。对于M[A,a],A为非终结符,a为终结符或“#”。分析表元素M[A,a]中的内容为一条关于A的产生式,表明当A面临输入符号a时当前应采用的候选式。当元素内容为空白(空白表示“出错标志”)时,则表明A不应该面临这个输入符号a,即输入串含有语法错误;

  4. 控制程序根据分析栈顶符号x和当前输入符号a来决定分析器的动作:

  • 若x=a="#”,则分析成功,分析器停止工作;
  • 若x=a≠“#”,即栈顶符号x与当前扫描的输入符号a匹配,则将x从栈顶弹出,输入指针指向下一个输入符号,继续对下一个字符进行分析;
  • 若x为一个非终结字符A,则查M[A,a];
    • 若M[A,a]中为一个A的产生式,则将A自栈顶弹出,并将M[A,a]中的产生式右部符号串按逆序逐一压入栈中。如果M[A,a]中的产生式为A->ε,则只将A自栈顶弹出;
    • 若M[A,a]中为空,则发现语法错误,调用出错处理程序进行处理。

4.2 消除左递归和回溯理解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3 FIRST集和FOLLOW集理解

FIRST(α)表示α的所有可能推导的开头终结符或可能的空字ϵ。其构造方法为反复运用下述规则,直到每个集合的FIRST集合不再变化为止:

  1. 若存在产生式X → a 且a为终结符,则将a加入FIRST(X)中。若存在X → ϵ,则将ϵ也加入到FIRST(X)中。

  2. 若存在X → Y…,且Y为非终结符,则将FIRST(Y)中所有非ϵ元素加入到FIRST(X)中。若存在产生式X → Y1Y2…Yk且Y1Yk都是非终结符,且产生式中存在ϵ,则将FIRST(Yj)中所有非ϵ元素加入到FIRST(X)中,其中j=1,2,…,i。当Y1Yk都含有ϵ产生式时,将ϵ加入到FIRST(X)中。

对于每个非终结符,构造FOLLOW集合需要连续使用以下规则:

  1. 对文法开始符号S,将#加入FOLLOW(S)中。

  2. 若存在产生式A → …Bβ,则将FIRST(β)中除ϵ的元素加入到FOLLOW(B)中。

  3. 若存在产生式A → …B或A → …Bβ且ϵ∈FIRST(B),则将所有元素加到FOLLOW(B)中。

五、实验流程

5.1 整体流程

首先从文件中读入文法,识别出文法的终结符与非终结符,对文法进行消除左递归、消除回溯、消除或。再次识别出文法的终结符与非终结符,对每个符号与短语求出其FIRST集合与FOLLOW集合,自动分析生成建立分析表。输入待分析的文法,对其进行分析。整体流程如下图所示。
在这里插入图片描述

5.2 求FIRST集

对字符或短语求FIRST集合时,往往需要用到递归。若在递归时发现c 字符的FIRST集合已经求解完毕,则直接返回。若当前字符c 是终结符,则FIRST集合为自己,返回。若不是终结符,则对其产生式进行遍历。若产生式中就是空字,则将空字加入FIRST集合。否则,遍历产生式的字符。递归的求解当前字符的FIRST集合,将该字符FIRST集合除了空字以外的元素加入。若该字符的FIRST集合存在空字,则遍历下一字符,如此循环。求解短语的FIRST集合的过程与求解某一非终结符的产生式的FIRST集合的流程是类似的。求FIRST集合的流程如下图。

在这里插入图片描述

5.3 求FOLLOW集

对字符或短语求FOLLOW集合时,也需要用到递归。对于文法开始符号,先将#加入其FOLLOW集合。对于每个非终结符,判断每条产生式,若存在A → . . . B b,则将b加B 入的FOLLOW集合。对每个产生式从右往左遍历,对于当前标识符之后的字符,若是非终结符,则将这个非终结符的FIRST集加入当前字符的FOLLOW集,这一过程需要递归地进行。若A → Bβ且ϵ∈FIRST(β),将FOLLOW(A)加入FOLLOW(B)。每次都要反复进行此过程。
在这里插入图片描述

5.4 SELECT集算法

求一个文法符号的FIRST集合,可以按以下步骤进行:

  1. 得到该符号的产生式左部(left)和右部(right)。

  2. 遍历右部产生式,首先分析右部第一个字符 right[0]。

    • 如果是终结符,如果为空符,则把FOLLOW(left)加入 results,否则直接把该符号加入到 results 中,然后退出遍历(break)。

    • 如果是非终结符,将 FIRST(right[0])-ϵ 加入到 results 中。如果 FIRST(right[0]) 中存在ϵ,则要继续往后看(continue)。如果 right[0] 是最后一个字符,则把 FOLLOW(left) 加入到 results。

注意:对于产生式右部中的每个符号,都要重复上述步骤,直到不再能够推导出ϵ为止。

在这里插入图片描述

5.5 求分析表并分析输入串

构造 LL(1) 分析表的流程如下:

  1. 对于每个非终结符 A,依次取它的每个产生式 A → α,对每个 a 属于 FIRST(α),将 A → α 添加到 M[A, a] 中。

  2. 如果存在 A → α,且 ϵ 属于 FIRST(α),则对 FOLLOW(A) 中的每个终结符 b,将 A → α 添加到 M[A, b] 中。

  3. 如果存在 A → ϵ,则对 FOLLOW(A) 中的每个终结符 b,将 A → ϵ 添加到 M[A, b] 中。

  4. 如果 M[A, a] 中已经有产生式了,且想要添加的产生式与已有的产生式不同,则说明文法不满足 LL(1) 文法,出现冲突,需要进行错误处理。

分析输入串的流程与上文一致,这里不再赘述。

六、程序运行截图

输入程序:
按实验要求输入该文法:
(1) E -> TG
(2) G -> +TG | -TG
(3) G -> ε
(4) T -> FS
(5) S -> *FS | /FS
(6) S -> ε
(7) F -> (E)
(8) F -> i
程序输出截图:
在这里插入图片描述
在这里插入图片描述

七、完整代码

1.	#include <iostream>  
2.	#include <iomanip>  
3.	#include <stdio.h>  
4.	#include <fstream> // 文件流  
5.	#include <string> // 字符串  
6.	#include <algorithm> // 字符串处理  
7.	#include <unordered_map>  // 哈希表  
8.	#include <map>   // 图  
9.	#include <stack> // 栈  
10.	#include <set>   // 集  
11.	#include <vector> //向量  
12.	using namespace std;  
13.	  
14.	// 主要的全局变量定义  
15.	unordered_map<string, string> grammar;// 文法集合哈希表  
16.	string S; // 开始符  
17.	set<string> Vn; // 非终结符集  
18.	set<string> Vt; // 终结符集  
19.	set<string> formulas; // 产生式集。求解select时方便遍历  
20.	unordered_map<string, set<char>> FIRST;// FIRST集合  
21.	unordered_map<string, set<char>> FOLLOW;// FOLLOW集合  
22.	unordered_map<string, set<char>> SELECT;// Select集合  
23.	map<pair<char, char>, string> TABLE;// 预测分析表  
24.	  
25.	//  函数预定义 功能函数  
26.	void readFile();     // 读取文件  
27.	void pretreatment(); // 预处理,简化、符号分类等  
28.	set<char> findFirstBV(string vn); // 某个非终结符的First集合(递归求解)  
29.	void findFirst();                 // 使用哈希表存储  
30.	set<char> findFollowBV(string vn); // 某个非终结符的Follow集合(递归求解)  
31.	void findFollow();                 // 使用哈希表存储  
32.	set<char> findSelectBF(string formula);  // 某个产生式的Select集合(递归求解)  
33.	void findSelect();                       // 使用哈希表存储  
34.	void isLL1();      // 判断是否为LL(1)分析  
35.	void makeTable();  // 构造预测分析表  
36.	void LL1Analyse(); // 分析字符串  
37.	  
38.	// 工具函数   
39.	// 根据左部获取产生式的右部(集合)  
40.	vector<string> getRights(string left);  
41.	// 判断是终结符还是非终结符  
42.	bool isVn(char v);  
43.	bool isVt(char v);  
44.	// 判断某个非终结符能否推出空  
45.	bool canToEmpty(char v);  
46.	//判断两个字符set的交集是否为空  
47.	bool isIntersect(set<char> a, set<char> b);  
48.	// 输出分析表  
49.	void printTable();  
50.	// 得到逆序字符串  
51.	string getStackRemain(stack<char> stack_remain);  
52.	// 显示输出一个char集  
53.	void printSet(set<char> sets);  
54.	// 求FOLLOW集合中的元素个数(用于判断:直到follow集合不再增大)  
55.	int getFS();  
56.	// =====================================主函数===================================  
57.	int main() {  
58.	    cout << "====================LL(1)分析器====================" << endl;  
59.	    // =====================================进入核心代码:LL(1)分析==================================  
60.	    // =====================================1、读取文法并且简单处理==================================  
61.	    readFile();  
62.	    // =====================================2、找First集=============================================  
63.	    findFirst();  
64.	    // =====================================3、找Follow集============================================  
65.	    findFollow();  
66.	    // =====================================4、找Select集============================================  
67.	    findSelect();  
68.	    // =====================================5、判断是否是LL1文法=====================================  
69.	    isLL1();  
70.	    // =====================================6、构建分析表============================================  
71.	    makeTable();  
72.	    // =====================================7、分析字符串============================================  
73.	    LL1Analyse();  
74.	    return 0;  
75.	}  
76.	// =====================================功能函数===================================  
77.	// 读取文件  
78.	void readFile() {  
79.	    // 输入文件名  
80.	    cout << "请输入文件名:";  
81.	    char file[100];  
82.	    cin >> file;  
83.	    // 首先展示文件所有内容  
84.	    cout << "====================文法读取====================" << endl;  
85.	    // ifstream文件流打开文件  
86.	    ifstream fin(file);  
87.	    if (!fin.is_open())  
88.	    {  
89.	        cout << "打开文件失败";  
90.	        exit(-1); // 直接退出  
91.	    }  
92.	    string line;  
93.	    bool isGet = false;  
94.	    while (getline(fin, line)) // 逐行读取  
95.	    {  
96.	        if (!isGet)  
97.	        {  
98.	            // 得到开始符  
99.	            S = line[0];  
100.	            isGet = true;  
101.	        }  
102.	        formulas.insert(line); // 得到所有表达式  
103.	        cout << line << endl;  // 输出  
104.	  
105.	        // 如果哈希表中已经存在该键,加在后面  
106.	        for (auto iter = grammar.begin(); iter != grammar.end(); ++iter) {  
107.	            if (iter->first == string(1, line[0]))  
108.	            {  
109.	                iter->second = iter->second + "|" + line.substr(3);  
110.	                break;  
111.	            }  
112.	        }  
113.	        // 往存储文法的哈希表中插入键值对  
114.	        grammar.insert(pair<string, string>(string(1, line[0]), line.substr(3)));  
115.	    }  
116.	  
117.	    cout << "请注意~表示空" << endl;  
118.	    fin.close(); // 关闭文件流  
119.	  
120.	    pretreatment();  
121.	}  
122.	// 简单处理:符号分类、输出显示  
123.	void pretreatment() {  
124.	    cout  << "====================文法简化====================" << endl;  
125.	    // 遍历文法哈希表  
126.	    for (auto iter = grammar.begin(); iter != grammar.end(); ++iter) {  
127.	        // 输出  
128.	        cout << iter->first << "→" << iter->second << endl;  
129.	        // ========================================符号分类==================================  
130.	        Vn.insert(iter->first); // 非终结符集  
131.	        // 终结符集合  
132.	        string str = iter->second;  
133.	        for (size_t i = 0; i < str.length(); i++)  
134.	        {  
135.	            if (str[i] != '|' && (str[i] < 'A' || str[i] > 'Z'))  
136.	            {  
137.	                Vt.insert(string(1, str[i]));  
138.	            }  
139.	        }  
140.	    }  
141.	    cout << "====================符号分类====================" << endl;  
142.	    // 输出终结符和非终结符集合  
143.	    cout << "开始符号:" << S << endl;  
144.	    cout << "非终结符集Vn = " << "{";  
145.	    for (auto iter = Vn.begin(); iter != Vn.end(); ) {  
146.	        cout << *iter;  
147.	        if ((++iter) != Vn.end())  
148.	        {  
149.	            cout << ",";  
150.	        }  
151.	    }  
152.	    cout << "}" << endl;  
153.	  
154.	    cout << "终结符集Vt = " << "{";  
155.	    for (auto iter = Vt.begin(); iter != Vt.end(); ) {  
156.	        cout << *iter;  
157.	        if ((++iter) != Vt.end())  
158.	        {  
159.	            cout << ",";  
160.	        }  
161.	    }  
162.	    cout << "}" << endl;  
163.	}  
164.	//  求某一个非终结符的First集合  
165.	set<char> findFirstBV(string vn) {  
166.	    // 求解思路  
167.	        //1、获取vn的右部,进行分析  
168.	        //2、遍历右部,一个个右部分析,各自求解first集加入到results  
169.	            //2.1 遍历当前右部(一个个字符分析)  
170.	                //如果第一个字符是终结符,加入first集合并且跳出;(这里会添加多余的空符)  
171.	                //如果是非终结符,则递归处理;  
172.	                //如果非终结符可以推空还需要循环处理该右部的下一字符(如果有)  
173.	        //3、遍历结束,最后如果该字符不能推空,就要删除results中的空符;返回结果  
174.	    set<char> results; // first集存储  
175.	    vector<string> rights = getRights(vn); // 获取右部  
176.	    if (!rights.empty()) // 如果右部不为空  
177.	    {  
178.	        // 遍历右部集合(每一个右部分别求解first,加入到该非终结符的first集合中)  
179.	        for (auto iter = rights.begin(); iter != rights.end(); ++iter) {  
180.	            string right = *iter;  
181.	            // 遍历当前右部: //如果第一个字符是终结符,加入first集合并且跳出循环;  
182.	                              //如果是非终结符,则递归处理;  
183.	                                //如果非终结符可以推空还需要循环处理该右部的下一字符(如果有)  
184.	            for (auto ch = right.begin(); ch != right.end(); ++ch) {  
185.	                if (isVn(*ch)) // 如果是非终结符,就要递归处理  
186.	                {  
187.	                    //先查first集合。如果已经有了就不需要重复求解  
188.	                    if (FIRST.find(string(1, *ch)) == FIRST.end()) // fisrt集合中不存在  
189.	                    {  
190.	                        // 递归调用自身!!!  
191.	                        set<char> chars = findFirstBV(string(1, *ch));  
192.	                        // 将结果存入结果  
193.	                        results.insert(chars.begin(), chars.end());  
194.	                        FIRST.insert(pair<string, set<char>>(string(1, *ch), chars));  
195.	                    }  
196.	                    else { // 存在就把该集合全部加到firsts(提高效率)  
197.	                        set<char> chars = FIRST[string(1, *ch)];  
198.	                        results.insert(chars.begin(), chars.end());  
199.	                    }  
200.	  
201.	                    // 如果这个字符可以推空,且后面还有字符,那么还需要处理下一个字符  
202.	                    if (canToEmpty(*ch) && (iter + 1) != rights.end())  
203.	                    {  
204.	                        continue;  
205.	                    }  
206.	                    else  
207.	                        break; // 否则直接退出遍历当前右部的循环  
208.	  
209.	                }  
210.	                else { // 如果不是非终结符,直接把这个字符加入first集合,并且跳出  
211.	                    // 这一步会把前面的空也加进去(后面会删除)  
212.	                    results.insert(*ch);  
213.	                    break;  
214.	                }  
215.	            }  
216.	        }  
217.	    }  
218.	    // 最后,如果该终结符不能推空,就删除空  
219.	    if (!canToEmpty(vn[0]))  
220.	    {  
221.	        results.erase('~');  
222.	    }  
223.	    return results;  
224.	}  
225.	// 求解First集,使用哈希表存储  
226.	void findFirst() {  
227.	    // 遍历非终结符集合,构建哈希表,便于后续查询  
228.	    for (auto iter = Vn.begin(); iter != Vn.end(); ++iter) {  
229.	        string vn = *iter; // 获取非终结符  
230.	        set<char> firsts = findFirstBV(vn); // 存放一个Vn的first集  
231.	        FIRST.insert(pair<string, set<char>>(vn, firsts));  
232.	    }  
233.	    // 显示输出  
234.	    cout << "==================FISRT集分析==================" << endl;  
235.	    for (auto iter = FIRST.begin(); iter != FIRST.end(); ++iter) {  
236.	        cout << "FIRST(" << iter->first << ")" << "= ";  
237.	        set<char> sets = iter->second;  
238.	        printSet(sets);  
239.	    }  
240.	}  
241.	// 单个非终结符符求解其Follow集合  
242.	set<char> findFollowBV(string vn) {  
243.	    //求解思路:  
244.	        //1、对于开始符号,把#加到results  
245.	        //2、遍历当前文法所有的右侧表达式,  
246.	            //2.1 遍历当前右部进行分析,如果发现了vn,则可进行下一步骤以获取results元素  
247.	                //如果当前字符vn是最后一个字符,说明位于句尾,则把#加入  
248.	                //否则遍历vn后的字符  
249.	                    // 如果再次遇到vn,回退并退出循环进入外部循环  
250.	                    // 如果遇到终结符,直接加入到results,并break退出循环  
251.	                    // 否则就是非终结符,那么求其first集合,去掉空后加入到results  
252.	                        // 此时还要考虑是继续循环还是跳出循环:  
253.	                            //如果当前字符可以推空,而且不是最后一个字符,说明还要继续分析下一个字符  
254.	                            //如果可以推空但是是最后一个字符,那么把#加入results  
255.	                            //如果不可以推空,直接跳出循环即可(可以推空,后面字符的first集合才有可能作为vn的follow集合)  
256.	        //3、遍历完成,返回results;具体代码如下:  
257.	    set<char> results; // 存储求解结果  
258.	    if (vn == S) // 如果是开始符号  
259.	    {  
260.	        results.insert('#'); // 把结束符加进去,因为有语句#S#  
261.	    }  
262.	  
263.	    // 遍历文法所有的右部集合  
264.	    for (auto iter = formulas.begin(); iter != formulas.end(); ++iter)  
265.	    {  
266.	        string right = (*iter).substr(3); // 获取当前右部  
267.	        // 遍历当前右部,看是否含有当前符号  
268.	        for (auto i_ch = right.begin(); i_ch != right.end();)  
269.	        {  
270.	            if (*i_ch == vn[0]) { // 如果vn出现在了当前右部  
271.	                if ((i_ch + 1) == right.end()) // vn是当前右部最后一个字符  
272.	                {  
273.	                    results.insert('#'); // 加入结束符  
274.	                    break;  
275.	                }  
276.	                else { // vn后面还有字符,遍历他们(除非又遇到vn:i_ch回退一个并且进入跳出循环)  
277.	                    while (i_ch != right.end())  
278.	                    {  
279.	                        ++i_ch;// 指针后移  
280.	                        if (*i_ch == vn[0])  
281.	                        {  
282.	                            --i_ch;  
283.	                            break;  
284.	                        }  
285.	                        if (isVn(*i_ch)) // 如果该字符是非终结符,把first集中的非空元素加进去  
286.	                        {  
287.	                            set<char> tmp_f = FIRST[string(1, *i_ch)];  
288.	                            tmp_f.erase('~'); // 除去空  
289.	                            results.insert(tmp_f.begin(), tmp_f.end());  
290.	  
291.	  
292.	                            // 还要该字符可否推空,需要考虑是否继续循环  
293.	                            if (canToEmpty(*i_ch))  
294.	                            {  
295.	                                if ((i_ch + 1) == right.end()) // 如果是最后一个字符,加入#  
296.	                                {  
297.	                                    results.insert('#');  
298.	                                    break;// 跳出循环  
299.	                                }  
300.	                                // 继续循环  
301.	                            }  
302.	                            else // 否则跳出循环  
303.	                                break;  
304.	                        }  
305.	                        else {  // 如果该字符是终结符  
306.	                            results.insert(*(i_ch));  // 加入该字符  
307.	                            break;  // 跳出循环  
308.	                        }  
309.	                    }  
310.	                }  
311.	            }  
312.	            else {  
313.	                ++i_ch;  
314.	            }  
315.	        }  
316.	    }  
317.	    return results;  
318.	}  
319.	// 完善Follow集合  
320.	void completeFollow(string vn) {  
321.	    // 遍历文法所有的右部集合  
322.	    for (auto iter = formulas.begin(); iter != formulas.end(); ++iter)  
323.	    {  
324.	  
325.	        string right = (*iter).substr(3); // 获取当前右部  
326.	        // 遍历当前右部,看是否含有当前符号  
327.	        for (auto i_ch = right.begin(); i_ch != right.end();)  
328.	        {  
329.	            char vn_tmp = *i_ch;  
330.	            if (vn_tmp == vn[0]) { // 如果vn出现在了当前右部  
331.	                if ((i_ch + 1) == right.end()) // vn是当前右部最后一个字符  
332.	                {  
333.	                    char left = (*iter)[0];  
334.	                    set<char> tmp_fo = FOLLOW[string(1, left)]; // 获取左部的follow集合  
335.	                    set<char> follows = FOLLOW[string(1, vn_tmp)]; // 获取自己的原来的follow集合  
336.	                    follows.insert(tmp_fo.begin(), tmp_fo.end());  
337.	                    FOLLOW[vn] = follows; // 修改  
338.	                    break;  
339.	                }  
340.	                else { // 不是最后一个字符,就要遍历之后的字符看是否可以推空  
341.	                    while (i_ch != right.end())  
342.	                    {  
343.	                        ++i_ch; // 注意指针后移了!!!  
344.	                        if (canToEmpty(*i_ch))  
345.	                        {  
346.	                            if ((i_ch + 1) != right.end()) // 不是最后一个元素,就要继续看后面有没有可以推空的  
347.	                            {  
348.	                                continue;  
349.	                            }  
350.	                            else { // 最后一个也能推空,则把左部加进去  
351.	                                char left = (*iter)[0];  
352.	                                set<char> tmp_fo = FOLLOW[string(1, left)]; // 左部的follow集合  
353.	                                set<char> follows = FOLLOW[string(1, vn_tmp)]; // 当前符号的follow集合  
354.	                                follows.insert(tmp_fo.begin(), tmp_fo.end());  
355.	                                FOLLOW[vn] = follows; // 修改原值  
356.	                                break;  
357.	                            }  
358.	                        }  
359.	                        else  // 如果不能推空,就退出循环  
360.	                            break;  
361.	                    }  
362.	                }  
363.	            }  
364.	            ++i_ch; // 遍历寻找vn是否出现  
365.	        }  
366.	    }  
367.	}  
368.	// 求解Follow集,使用哈希表存储  
369.	void findFollow() {  
370.	    // 遍历所有非终结符,依次求解follow集合  
371.	    for (auto iter = Vn.begin(); iter != Vn.end(); ++iter) {  
372.	        string vn = *iter; // 获取非终结符  
373.	        set<char> follows = findFollowBV(vn); // 求解一个Vn的follow集  
374.	        FOLLOW.insert(pair<string, set<char>>(vn, follows)); // 存储到哈希表,提高查询效率  
375.	  
376.	    }  
377.	    // 完善follow集合直到follow不再增大  
378.	    int old_count = getFS();  
379.	    int new_count = -1;  
380.	    while (old_count != new_count) // 终结符在变化,反复这个过程直到follow集合不再增大  
381.	    {  
382.	        old_count = getFS();  
383.	        // 再次遍历非终结符,如果出现在右部最末端的,把左部的follow集加进来  
384.	        for (auto iter = Vn.begin(); iter != Vn.end(); ++iter) {  
385.	            string vn = *iter; // 获取非终结符  
386.	            completeFollow(vn);  
387.	        }  
388.	        new_count = getFS();  
389.	    }  
390.	    // 显示输出  
391.	    cout << "==================FOLLOW集分析==================" << endl;  
392.	    for (auto iter = FOLLOW.begin(); iter != FOLLOW.end(); ++iter) {  
393.	        cout << "FOLLOW(" << iter->first << ")" << "= ";  
394.	        set<char> sets = iter->second;  
395.	        printSet(sets);  
396.	    }  
397.	}  
398.	// 单个表达式求解Select集合  
399.	set<char> findSelectBF(string formula) {  
400.	    // 求解思路  
401.	        // 1、得到产生式的left和right  
402.	        // 2、遍历右部产生式,首先分析右部第一个字符:right[0]  
403.	            // 如果是终结符:(如果为空符,则把follow(left)加入results,否则直接把该符号加入到results),然后break  
404.	            // 如果是非终结符:把first(right[0])-'~'加入到results;如果还可以推空,则要继续往后看(continue)  
405.	    set<char> results; // 存储结果  
406.	    // 1、得到产生式的left和right  
407.	    char left = formula[0]; // 左部  
408.	    string right = formula.substr(3); // 右部  
409.	    //cout << "Select集合分析" << left << "->" << right << endl;// 调试用  
410.	        // 2、遍历右部产生式,首先分析右部第一个字符:right[0]  
411.	    for (auto iter = right.begin(); iter != right.end(); ++iter)  
412.	    {  
413.	        //cout << "遍历右部" << *iter << endl; // 调试用  
414.	        // 如果是非终结符:把first(right[0])-'~'加入到results;如果还可以推空,则要继续往后看(continue)  
415.	        if (isVn(*iter))  
416.	        {  
417.	            set<char> chs = FIRST.find(string(1, *iter))->second; // 得到该符号的first、  
418.	            chs.erase('~'); // 去除空符  
419.	            results.insert(chs.begin(), chs.end()); // 加入select  
420.	            if (canToEmpty(*iter)) // 如果可以推空,继续处理下一个字符加入到select集合  
421.	            {  
422.	                if ((iter + 1) == right.end()) // 当前是最后一个字符,则把follow(left)加入results,然后break  
423.	                {  
424.	                    set<char> chs = FOLLOW.find(string(1, left))->second; // 得到左部的follow  
425.	                    results.insert(chs.begin(), chs.end()); // 加入select  
426.	                }  
427.	                else { // 继续处理下一字符  
428.	                    continue;  
429.	                }  
430.	            }  
431.	            else  
432.	                break; // 该字符不可以推空,退出循环  
433.	        }  
434.	        else {// 如果是终结符:(如果为空符,则把follow(left)加入results,否则直接把该符号加入到results),然后break  
435.	            if (*iter == '~') // 如果是空  
436.	            {  
437.	                set<char> chs = FOLLOW.find(string(1, left))->second; // 得到左部的follow  
438.	                results.insert(chs.begin(), chs.end()); // 加入select  
439.	            }  
440.	            else  
441.	                results.insert(*iter); // 直接加入select  
442.	            break; // 退出循环  
443.	        }  
444.	    }  
445.	  
446.	    return results;  
447.	}  
448.	// 求解Select集,使用哈希表存储  
449.	void findSelect() {  
450.	    // 遍历表达式集合  
451.	    for (auto iter = formulas.begin(); iter != formulas.end(); ++iter) {  
452.	        string formula = *iter; // 获取表达式  
453.	        set<char> selects = findSelectBF(formula); // 存放一个Vn的first集  
454.	        SELECT.insert(pair<string, set<char>>(formula, selects));  // 插入到哈希表,提高查询效率  
455.	    }  
456.	    // 显示输出  
457.	    cout << "==================SELECT集分析==================" << endl;  
458.	    for (auto iter = SELECT.begin(); iter != SELECT.end(); ++iter) {  
459.	        cout << "SELECT(" << iter->first << ")" << "= ";  
460.	        set<char> sets = iter->second;  
461.	        printSet(sets);  
462.	    }  
463.	}  
464.	// 判断是否为LL(1)分析  
465.	void isLL1() {  
466.	    // 求解思路:通过嵌套循环SELECT集合,判断不同的表达式但左部相同时的SELECT集合之间相交是否有交集  
467.	        // 如果有交集说明不是LL1,否则是LL1分析  
468.	    for (auto i1 = SELECT.begin(); i1 != SELECT.end(); ++i1)  
469.	    {  
470.	        for (auto i2 = SELECT.begin(); i2 != SELECT.end(); ++i2)  
471.	        {  
472.	            char left1 = (i1->first)[0]; // 获取左部2  
473.	            char left2 = (i2->first)[0]; // 获取左部2  
474.	            if (left1 == left2) // 左部相等  
475.	            {  
476.	                if (i1->first != i2->first) //表达式不一样  
477.	                {  
478.	                    if (isIntersect(i1->second, i2->second)) { // 如果select集合有交集  
479.	                        // 不是LL1文法  
480.	                        cout << "经过分析,您输入的文法不符合LL(1)文法,请修改后重试" << endl;  
481.	                        exit(0); // 直接退出  
482.	                    }  
483.	                }  
484.	            }  
485.	        }  
486.	    }  
487.	    // 是LL(1)文法  
488.	    cout << "====================进入分析器====================" << endl << endl;  
489.	    cout << "经过分析,您输入的文法符合LL(1)文法..." << endl;  
490.	}  
491.	// 构造预测分析表  
492.	void makeTable() {  
493.	    cout << "正在为您构造分析表..." << endl;  
494.	    // 求解思路:  
495.	        // 1、遍历select集合,对于键:分为left和->right;对于值,遍历后单个字符ch:  
496.	                // 把left和ch配对作为TABLE的键,而->right作为值  
497.	    // map键值对的形式,空间更多,查询效率高点  
498.	    char left_ch;  
499.	    string right;  
500.	    set<char> chars;  
501.	    for (auto iter = SELECT.begin(); iter != SELECT.end(); ++iter) // 遍历select集合  
502.	    {  
503.	        left_ch = iter->first[0]; // 获取左部  
504.	        right = iter->first.substr(1); // 获取->右部  
505.	        chars = iter->second;  
506.	        // 遍历chars.一个个放入  
507.	        for (char ch : chars) { // 遍历终结符  
508.	            TABLE.insert(pair<pair<char, char>, string>(pair<char, char>(left_ch, ch), right));  
509.	        }  
510.	    }  
511.	    /*cout << "分析表调试:" << TABLE.find(pair<char, char>('E', 'i'))->second;*/  
512.	    // 输出分析表  
513.	    printTable();  
514.	}  
515.	// 输出预测分析表  
516.	void printTable() {  
517.	    // 输出分析表  
518.	    cout << "====================预测分析表====================" << endl;  
519.	    cout << setw(9) << left << setfill(' ') << "VN/VT";  
520.	    set<string> vts = Vt;  
521.	    vts.erase("~");  
522.	    vts.insert("#");  
523.	    for (string str : vts) // 遍历终结符  
524.	    {  
525.	        cout << setw(12) << left << setfill(' ') << str;  
526.	    }  
527.	    cout << endl << endl;  
528.	    for (string vn : Vn)  
529.	    {  
530.	        cout << setw(7) << left << setfill(' ') << vn;  
531.	        for (string vt : vts) // 遍历终结符  
532.	        {  
533.	            if (TABLE.find(pair<char, char>(vn[0], vt[0])) == TABLE.end()) //如果找不到  
534.	            {  
535.	                cout << setw(12) << left << " " << " ";  
536.	            }  
537.	            else {  
538.	                cout << setw(12) << left << TABLE.find(pair<char, char>(vn[0], vt[0]))->second << " ";  
539.	            }  
540.	        }  
541.	        cout << endl;  
542.	    }  
543.	  
544.	    cout << setw(75) << setfill('=') << " " << endl;  
545.	}  
546.	// 分析字符串  
547.	void LL1Analyse() {  
548.	    // 求解思路:  
549.	        //1、构建先进后出栈,将#、S进栈  
550.	        //2、遍历句子,一个个符号送a;和栈顶送X,进入分析  
551.	            // 2.1 如果X是终结符  
552.	                // 如果和a相等,说明匹配成功:X出栈,并读取下一个字符  
553.	                // 否则是无法匹配:失败退出  
554.	            // 2.2 如果X是末尾符  
555.	                // a也是末尾符,接受分析字符串:成功退出  
556.	                // a不是末尾符,不接受分析字符串,失败退出  
557.	            // 2.3 否则X就是非终结符  
558.	                // 查找预测分析表,看是否有表达式  
559.	                    // 如果没有,分析出错,失败退出  
560.	                    // 如果有,X元素出栈,表达式逆序进栈,继续循环句子且要重复分析a  
561.	        //3、遍历完成,程序结束  
562.	    cout << "构造完成,请输入您要分析的字符串..." << endl;  
563.	    string str; // 输入串  
564.	    cin >> str;  
565.	    str.push_back('#'); // 末尾加入结束符  
566.	    cout << "正在分析..." << endl;  
567.	    cout << endl << setw(75) << right << setfill('=') << "分析过程====================" << endl;  
568.	  
569.	    cout << setw(16) << left << setfill(' ') << "步骤";  
570.	    cout << setw(16) << left << setfill(' ') << "分析栈";  
571.	    cout << setw(16) << left << setfill(' ') << "剩余输入串";  
572.	    cout << setw(16) << left << setfill(' ') << "分析情况" << endl;  
573.	  
574.	    stack<char> stack_a; // 分析栈  
575.	    stack_a.push('#'); // 末尾符进栈  
576.	    stack_a.push(S[0]); // 开始符号进栈  
577.	  
578.	    // 初始化显示数据  
579.	    int step = 1; // 步骤数  
580.	    stack<char> stack_remain = stack_a; // 剩余分析栈  
581.	    string str_remain = str; // 剩余分析串  
582.	    string str_situation = "待分析"; // 分析情况  
583.	  
584.	    // 初始数据显示  
585.	    cout << setw(16) << left << setfill(' ') << step;  
586.	    cout << setw(16) << left << setfill(' ') << getStackRemain(stack_remain);  
587.	    cout << setw(16) << left << setfill(' ') << str_remain << endl;  
588.	  
589.	    // 遍历所输入的句子,一个个字符分析  
590.	    for (auto iter = str.begin(); iter != str.end();) {  
591.	        char a = *iter; // 当前终结符送a  
592.	        char X = stack_a.top(); // 栈顶元素送X  
593.	  
594.	        if (isVt(X)) // 如果X是Vt终结符,栈顶元素出栈,然后读取下一个字符  
595.	        {  
596.	            if (X == a) // 和输入字符匹配  
597.	            {  
598.	                stack_a.pop(); // 移除栈顶元素  
599.	                // 从剩余分析串中移除本元素  
600.	                for (auto i_r = str_remain.begin(); i_r != str_remain.end(); i_r++)  
601.	                {  
602.	                    if (*i_r == a) {  
603.	                        str_remain.erase(i_r);  
604.	                        break; // 只删除第一个,  
605.	                    }  
606.	                }  
607.	                // 重新组装提示字符串  
608.	                string msg = "“" + string(1, a) + "”匹配";  
609.	                str_situation = msg;  
610.	                // 读取下一个字符  
611.	                ++iter;  
612.	            }  
613.	            else { // 无法匹配,分析出错  
614.	                cout << "分析出错:" << X << "和" << a << "不匹配" << endl;  
615.	                exit(-1); // 出错退出  
616.	            }  
617.	        }  
618.	        else if (X == '#') // 文法分析结束  
619.	        {  
620.	            if (a == '#') // 当前符号也是最后一个符号 , 接受分析结果  
621.	            {  
622.	                cout << "分析结束,当前文法接受您输入的字符串" << endl;  
623.	                exit(0); // 成功退出  
624.	            }  
625.	            else {  
626.	                cout << "分析出错,文法结束输入串未结束" << endl;  
627.	                exit(-1);  
628.	            }  
629.	        }  
630.	        else { // X 就是非终结符了  
631.	            // 查看TABLE(X,a)是否有结果  
632.	            if (TABLE.find(pair<char, char>(X, a)) == TABLE.end()) //如果找不到  
633.	            {  
634.	                if (!canToEmpty(X)) // 也不能推空  
635.	                {  
636.	                    cout << "分析出错,找不到表达式" << endl;  
637.	                    exit(-1); // 失败退出  
638.	                }  
639.	                else {  // 可以推空,  
640.	                    stack_a.pop(); // 移除栈顶元素    // 重新组装字符串  
641.	                    str_situation.clear();  
642.	                    str_situation.push_back(X);  
643.	                    str_situation = str_situation + "->";  
644.	                    str_situation = str_situation + "~";  
645.	                }  
646.	            }  
647.	            else {  
648.	                stack_a.pop();// 先将当前符号出栈  
649.	                string str = TABLE.find(pair<char, char>(X, a))->second.substr(2); // 获取表达式并且逆序进栈(除去->)  
650.	                // 重新组装字符串  
651.	                str_situation.clear();  
652.	                str_situation.push_back(X);  
653.	                str_situation = str_situation + "->";  
654.	                str_situation = str_situation + str;  
655.	  
656.	                reverse(str.begin(), str.end());  
657.	                for (auto iiter = str.begin(); iiter != str.end(); ++iiter)  
658.	                {  
659.	                    if (*iiter != '~')  
660.	                    {  
661.	                        stack_a.push(*iiter);  
662.	                    }  
663.	                }  
664.	                // (要继续识别该字符)  
665.	            }  
666.	        }  
667.	        // 重置显示数据  
668.	        ++step; // 步骤数加1  
669.	        stack_remain = stack_a; // 置剩余栈为当前栈  
670.	        // 每次循环显示一次  
671.	        cout << setw(16) << left << setfill(' ') << step;  
672.	        cout << setw(16) << left << setfill(' ') << getStackRemain(stack_remain);  
673.	        cout << setw(16) << left << setfill(' ') << str_remain;  
674.	        cout << setw(16) << left << setfill(' ') << str_situation << endl;  
675.	    }  
676.	}  
677.	// =====================================工具函数===================================  
678.	// 根据左部返回某一产生式的右部集合  
679.	vector<string> getRights(string left)  
680.	{  
681.	    vector<string> rights;  
682.	    if (grammar.find(left) == grammar.end()) // 语法中没有这一项,直接返回空  
683.	    {  
684.	        return rights;  
685.	    }  
686.	    else {  
687.	        string str = grammar.find(left)->second;  
688.	  
689.	        str = str + '|';   // 末尾再加一个分隔符以便截取最后一段数据  
690.	        size_t pos = str.find('|');//find函数的返回值,若找到分隔符返回分隔符第一次出现的位置,  
691.	        //否则返回npos  
692.	        //此处用size_t类型是为了返回位置  
693.	        while (pos != string::npos)  
694.	        {  
695.	            string x = str.substr(0, pos);//substr函数,获得子字符串  
696.	            rights.push_back(x);          // 存入right容器  
697.	            str = str.substr(pos + 1);     // 更新字符串  
698.	            pos = str.find('|');         // 更新分隔符位置  
699.	        }  
700.	        return rights;  
701.	    }  
702.	}  
703.	// 判断是终结符还是非终结符  
704.	bool isVn(char v) {  
705.	    if (v >= 'A' && v <= 'Z') { // 大写字母就是非终结符  
706.	        return true;  
707.	    }  
708.	    else {  
709.	        return false;  
710.	    }  
711.	}  
712.	bool isVt(char v) {  
713.	    if (isVn(v) || v == '#' || v == '|') // 如果是非终结符、末尾符号、分隔符,都不是终结符  
714.	    {  
715.	        return false;  
716.	    }  
717.	    return true;  
718.	}  
719.	// 判断某个非终结符能否推出空  
720.	bool canToEmpty(char vn) {  
721.	    vector<string> rights = getRights(string(1, vn)); // vn可能推出的右部集  
722.	    for (auto i = rights.begin(); i != rights.end(); ++i) // 遍历右部集合(如果前面的右部可以推空可提前跳出,不然就要看到最后)  
723.	    {  
724.	        string right = *i; // 此为一个右部  
725.	        // 遍历这个右部  
726.	        for (auto ch = right.begin(); ch != right.end(); ++ch) {  
727.	            if ((*ch) == '~')// 如果ch为空,说明可以推空(因为不可能存在右部是"εb"这样的情况,不需要看是否是最后一个字符)  
728.	            {  
729.	                return true;  
730.	            }  
731.	            else if (isVn(*ch)) { // 如果是vn则需要递归  
732.	                if (canToEmpty(*ch))// 如果可以推空  
733.	                {  
734.	                    // 而且是最后一个字符,则返回true  
735.	                                    //这里可能存在"AD"A->εD不能推空的情况,所以需要看是否最后一个字符  
736.	                    if ((ch + 1) == right.end())  
737.	                    {  
738.	                        return true;  
739.	                    }  
740.	                    continue; // 当前字符可以推空,但不是最后一个字符,无法确定能否推空,还需要看右部的下一个字符  
741.	                }  
742.	                else  // 如果不可以推空,说明当前右部不可以推空,需要看下一个右部  
743.	                    break;  
744.	            }  
745.	            else // 如果是非空vt说明目前右部不能推空,需要看下一个右部  
746.	                break;  
747.	        }  
748.	    }  
749.	    return false;  
750.	}  
751.	// 判断两个字符set的交集是否为空  
752.	bool isIntersect(set<char> as, set<char> bs) {  
753.	    for (char a : as) {  
754.	        for (char b : bs) {  
755.	            if (a == b)  
756.	            {  
757.	                return true;  
758.	            }  
759.	        }  
760.	    }  
761.	    return false;  
762.	}  
763.	// 得到逆序字符串  
764.	string getStackRemain(stack<char> stack_remain) {  
765.	    string str;// 剩余分析栈串  
766.	    while (!stack_remain.empty())  
767.	    {  
768.	        str.push_back(stack_remain.top());  
769.	        stack_remain.pop();// 出栈  
770.	    }  
771.	    reverse(str.begin(), str.end());  
772.	    return str;  
773.	}  
774.	// 显示输出一个char集  
775.	void printSet(set<char> sets) {  
776.	    cout << "{ ";  
777.	    for (auto i = sets.begin(); i != sets.end();) {  
778.	        cout << *i;  
779.	        if (++i != sets.end())  
780.	        {  
781.	            cout << " ,";  
782.	        }  
783.	    }  
784.	    cout << " }" << endl;  
785.	}  
786.	// 求FOLLOW集合中的元素个数  
787.	int getFS() {  
788.	    int count = 0;  
789.	    for (auto iter = FOLLOW.begin(); iter != FOLLOW.end(); ++iter) {  
790.	        count = count + iter->second.size();  
791.	    }  
792.	    return count;  
793.	}  


  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 合肥工业大学编译原理实验1是一个词法分析程序实验,使用Python语言进行编写。 词法分析是编译原理中的一个重要内容,主要负责将源代码文件中的字符序列分割成有意义的词法单元,如标识符、关键字、运算符、分隔符等,为后续的语法分析和语义分析做好准备。 Python语言由于其简洁易学和强大的功能,成为许多编译原理实验的首选语言之一。在这个实验中,我们将使用Python编写一个词法分析程序,实现对源代码的分析。 首先,我们需要读取源代码文件,将其转换为字符流,传递给词法分析程序。程序会逐个读取字符,并根据事先定义好的正则表达式进行匹配,识别出相应的词法单元。 实验中可能会用到的一些正则表达式包括:匹配标识符的正则表达式、匹配关键字的正则表达式、匹配运算符的正则表达式、匹配数值常量的正则表达式等。 在识别出词法单元后,程序会生成一个词法单元表,记录下每个词法单元的类型和对应的值。该词法单元表将作为语法分析的输入。 在编写这个词法分析程序时,需要注意处理多种可能的错误情况,如不合法的字符、不符合规范的标识符等。可以通过添加捕获异常的机制来处理这些错误情况,并及时进行提示。 综上所述,通过本次实验,我们可以学习到编译原理中词法分析的基本概念和原理,并通过实践来深入理解。通过使用Python语言编写词法分析程序,我们能够更好地掌握Python语言的特性和应用。 ### 回答2: 编译原理实验一是词法分析程序实验,要求使用Python语言编写程序。本实验的主要目的是通过实现词法分析器,能够将输入的源代码分解成一个个的词法单元。在合肥工业大学编译原理实验一的词法分析程序实验中,我们需要实现以下功能: 1. 识别并分类各种类型的词法单元,比如标识符、数字、关键字、运算符、界符等。 2. 跳过空格、换行符和注释等不影响程序执行的字符。 3. 输出每个词法单元的类型和值,方便后续程序分析和处理。 为了完成这个实验,我们可以使用Python语言提供的字符串处理函数和正则表达式库来帮助我们实现上述功能。下面是一个简单的实现示例: ```python import re def lexer(code): # 定义正则表达式,用于识别各种类型的词法单元 keywords = ['if', 'else', 'while', 'for', 'int', 'float', 'char'] # 关键字 operators = ['+', '-', '*', '/', '=', '==', '!=', '<', '>', '<=', '>='] # 运算符 delimiters = [';', '(', ')', '{', '}'] # 界符 pattern_keywords = '|'.join(keywords) pattern_operators = '|'.join(re.escape(op) for op in operators) pattern_delimiters = '|'.join(re.escape(dl) for dl in delimiters) pattern = f'({pattern_keywords})|({pattern_operators})|({pattern_delimiters})|\w+|\d+' # 开始词法分析 tokens = re.findall(pattern, code) for token in tokens: if token[0]: print(f'关键字:{token[0]}') elif token[1]: print(f'运算符:{token[1]}') elif token[2]: print(f'界符:{token[2]}') elif token[3]: print(f'标识符:{token[3]}') elif token[4]: print(f'数字:{token[4]}') # 测代码 code = ''' int main() { int a = 10; if (a > 0) { a = a - 1; } return 0; } ''' lexer(code) ``` 以上是一个简单的词法分析程序实验的实现示例,通过使用正则表达式来识别各种词法单元,并打印出每个词法单元的类型和值。实验中可以根据具体需求扩展代码,添加更多的词法单元类型和识别规则。 ### 回答3: 合肥工业大学编译原理实验1是关于词法分析程序实验。词法分析是编译过程中的第一个步骤,主要任务是将源代码分解为一个个的词法单元。在这个实验中,使用Python编写词法分析程序。 在开始编写程序之前,首先需要明确程序的功能和输入输出要求。根据实验要求,我们需要编写一个可以识别并输出源代码中的各个词法单元的程序。 编写词法分析程序的基本思路如下: 1. 读取源代码文件,将其按照字符进行分解; 2. 针对每一个字符,判断其所属的词法单元类型; 3. 将每个词法单元及其类型输出。 在Python中,可以利用正则表达式来匹配词法单元的模式。通过定义适当的正则表达式,可以方便地判断当前字符所属的词法单元类型。可以考虑使用re模块来处理正则表达式。 实验的输入是一个源代码文件,首先需要使用Python的文件操作来读取源文件的内容。之后,可以利用re模块的正则表达式相关函数,对每个字符进行匹配和识别。最后,将每个词法单元及其类型输出到一个文件中。 编写完程序后,可以使用一些示例的源代码文件进行测,验证程序的正确性。如果发现有问题,可以通过调和修改代码来改进程序的逻辑和功能。 总之,合肥工业大学编译原理实验1词法分析程序实验使用Python编写,通过正则表达式对源代码进行分析和识别,并将每个词法单元及其类型输出到一个文件中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱喝冰红茶的方舟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值