自己动手写basic解释器(七)

 

自己动手写basic解释器

刺猬@http://blog.csdn.net/littlehedgehog

 





注: 文章basic解释源码摘自梁肇新先生的《编程高手箴言》(据他所说这个代码也是网上摘录的),源码解读参考《java编程艺术》。《java编程艺术》里面自然是java版了(可能旭哥更加适合点儿),我这里还是解读的C版basic解释器代码。





终于把这个basic解释器主干源码解述完了。其实说来这个解释器实际意义并不大,但是通过阅读源代码我们可以深一步领悟程序语言执行内部机理。我觉得特别值得提的三点:
1、通过prog指针模拟CPU中的eip寄存器,巧妙地借鉴了世界最顶尖级的硬件工程师在处理程序运行问题上的思路。
2、模拟函数调用栈,这个在go_sub函数中得到了淋漓尽致地体现。
3、p_buf就相当于计算机内存,或者说是程序运行空间的text段,26个变量就相当于data段,而我们的模拟栈恰好就是程序中的栈空间。


最后我把主程序的代码贴出来,方便兄弟伙们:


  1. #include <stdio.h>
  2. #include <setjmp.h>
  3. #include <math.h>
  4. #include <ctype.h>
  5. #include <stdlib.h>

  6. #define NUM_LAB 100
  7. #define LAB_LEN 10
  8. #define FOR_NEST 25
  9. #define SUB_NEST 25
  10. #define PROG_SIZE 10000
  11. #define DELIMITER 1
  12. #define VARIABLE 2
  13. #define NUMBER 3
  14. #define COMMAND 4
  15. #define STRING 5
  16. #define QUOTE 6

  17. #define PRINT 1
  18. #define INPUT 2
  19. #define IF 3
  20. #define THEN 4
  21. #define FOR 5
  22. #define NEXT 6
  23. #define TO 7
  24. #define GOTO 8
  25. #define EOL 9
  26. #define FINISHED 10
  27. #define GOSUB 11
  28. #define RETURN 12
  29. #define END 13

  30. char *prog;      /* holds expression to be analyzed  */
  31. jmp_buf e_buf;   /* hold environment for longjmp() */

  32. int variables[26]= {  /* 26 user variables,A-Z  */
  33.     0,0,0,0,0,0,0,0,0,0,
  34.     0,0,0,0,0,0,0,0,0,0,
  35.     0,0,0,0,0,0
  36. };

  37. struct commands { /* keyword lookup table  */
  38.     char command[20];
  39.     char tok;
  40. } table[] = {  /* command must be entered lowercase  */
  41.     "print",PRINT,   /* in this table  */
  42.     "input",INPUT,
  43.     "if",IF,
  44.     "then",THEN,
  45.     "goto",GOTO,
  46.     "for",FOR,
  47.     "next",NEXT,
  48.     "to",TO,
  49.     "gosub",GOSUB,
  50.     "return",RETURN,
  51.     "end",END,
  52.     NULL,END
  53. };

  54. char token[80];     //注意token是数组类型
  55. char token_type,tok;

  56. struct label {
  57.     char name [LAB_LEN];
  58.     char *p;    /* point to place to go in source */
  59. };

  60. struct label label_table[NUM_LAB];
  61. char *find_label(),*gpop();

  62. struct for_stack {
  63.     int var;   /* counter variable  */
  64.     int target;  /* target value  */
  65.     char *loc;
  66. } fstack[FOR_NEST];  /* stack for FOR/NEXT loop  */
  67. struct for_stack fpop();

  68. char *gstack[SUB_NEST];  /* stack for gosub  */
  69. int ftos;  /* index to top of FOR stack  */
  70. int gtos;  /* index to top of GOSUB  */

  71. void print(),scan_labels(),find_eol(),exec_goto();
  72. void gosub(),greturn(),gpush(),label_init(),fpush();

  73. /* Load a program */
  74. load_program (char *p,char *fname)
  75. {
  76.     FILE *fp;
  77.     int i=0;
  78.    
  79.     if (!(fp=fopen(fname,"rb")))  return 0;

  80.     i=0;
  81.     do  {
  82.         *p = getc(fp);
  83.         p++;i++;
  84.     } while (!feof(fp)&&i<PROG_SIZE);
  85.     *(p-2) = '/0';   /* null terminate the program  */
  86.     fclose (fp);
  87.     return 1;
  88. }


  89. /* 给变量赋值  比如 a=3  
  90.  * 注意这里为了简化起见,我们的变量就设置为26个字母
  91.  */
  92. assignment()
  93. {
  94.     int var,value;

  95.     /* getthe variable name */
  96.     get_token();
  97.     if (!isalpha(*token))  //因为变量我们用字母代替 所以必定是字母类型
  98.     {
  99.         serror(4);
  100.         return;
  101.     }

  102.     var = toupper(*token)-'A';  //转化为大写字母  然后减去'A' 这样让变量在hash表中有了座次 比如A减去A为0 这样A字符变量在变量hash表中第一个位置

  103.     /* get the equals sign 
  104.      * 这里我们取a=3 中间的等号*/
  105.     get_token();
  106.     if (*token!='=')    //既然赋值么 肯定有等号了
  107.     {
  108.         serror(3);
  109.         return;
  110.     }

  111.     /* a=3  等号取走了 我们来取数值  */
  112.     get_exp(&value);
  113.    
  114.     /* 把我们取到的变量 比如a 值为3 存放在hash表中 */
  115.     variables[var] = value;
  116. }


  117. /* execute a simple version of the BASIC PRINT statement 
  118.  * 执行打印  这里我们还是举例说明*/
  119. void print()
  120. {
  121.     int answer;
  122.     int len=0,spaces;
  123.     char last_delim;
  124.    
  125.     do  {
  126.         get_token();  /* get next list item */
  127.         if (tok==EOL||tok==FINISHED)  break;  //如果取到的符号是一行结束或者文件结束  自然的打印结束

  128.                  
  129.         //BASIC 中print一般有两种用法  第二种就是print "hello world"  打印字符串  
  130.         if (token_type==QUOTE)  
  131.         {  
  132.             printf ("%s",token);
  133.             len+=strlen(token);
  134.             get_token();    //注意我们打印了后又取了一次符号
  135.         }
  136.         else   //打印变量的
  137.         { 
  138.             putback();
  139.             get_exp(&answer);
  140.             get_token();    //注意我们打印了后又取了一次符号
  141.             len += printf ("%d",answer);
  142.         }
  143.         last_delim = *token;    
  144.         
  145.         
  146.         /* Basic 有两种打印间隔标识 
  147.          * 比如 print a,b 表示按标准格式打印
  148.          * 而print a;b 表示按照紧凑格式打印  
  149.          * 所谓标准格式简单来讲就是间隔大点儿  紧凑自然间隔小点儿  
  150.          */
  151.         if (*token==',')  
  152.         {
  153.             /* compute number of move to next tab */
  154.             spaces = 8-(len%8);
  155.             len += spaces;  /* add in the tabbing position */
  156.             while (spaces)  {
  157.                 printf (" ");
  158.                 spaces--;
  159.             }
  160.         }
  161.         else if (*token==';')  
  162.             printf ("  ");
  163.         else if (tok != EOL && tok != FINISHED) serror (0);     //print a,b 打完一次后 要么是逗号、分号 要么就是行结束或者文件结束  如果四者不居其一  必然错了
  164.     } while (*token==';'||*token==',');     //例如 print a,b,c 如果token是逗号、分号 那么表示后面还有打印  继续来

  165.     /* 当处于行结束或者文件结束  那么前一次分界符不能是;或者,  
  166.      * 示例 如果 "print a," 这个明显是语法错误 a后面不应该要逗号
  167.      * 那么打印完a取出token是逗号  我们赋值给last_delim 继续循环
  168.      *  下一个是行结束  跳出打印但是检验出last_delim是逗号  出错 */
  169.     if (tok==EOL||tok==FINISHED)    
  170.     {
  171.         if (last_delim != ';' && last_delim != ',') printf ("/n");
  172.     }
  173.     else serror(0);  /* error is not, or ; */
  174. }


  175. /* 搜索所有标签 
  176.  * 这个函数可以说是basic里面的预处理  
  177.  * 我们搜索源代码 找出里面的标签  将其存入标签表
  178.  * 所谓标签label 其实C语言也有 不过一般不常用 因为label多半和goto一起出现的  而在结构化程序设计中 goto出现被人认为是绝对不能的
  179.  * 不过内核中goto却是常常出现
  180.  * 下面这个函数最大的困惑判断标签的特征类型 我们设置为数字  要知道这里标签我们都是设置为数字的
  181.  * 但是如何把标签与普通数值分开呢?
  182.  */
  183. void scan_labels()
  184. {
  185.     int addr;
  186.     char *temp;

  187.     label_init();  /* zero all labels */
  188.     temp = prog;  /* save poiter to top of program */

  189.     /* 如果源代码中第一个是个数字的话  存入标签表中  不过说实话   我没理解这个有什么意义*/
  190.     get_token();
  191.     if (token_type==NUMBER)  
  192.     {
  193.         strcpy (label_table[0].name,token);
  194.         label_table[0].p=prog;
  195.     }
  196.    
  197.     find_eol();     //提行
  198.     do  {
  199.         get_token();
  200.         if (token_type==NUMBER)     //如果是数字   这里是一行开头  开头的数字不可能是一个数值  
  201.         {
  202.             addr = get_next_label(token);
  203.             if (addr==-1||addr==-2)  
  204.             {
  205.                 (addr==-1) ? serror(5):serror(6);
  206.             }
  207.             strcpy (label_table[addr].name,token);
  208.             label_table[addr].p = prog;  /* current point in program */
  209.         }
  210.         /* if not on a blank line , find next line */
  211.         if (tok!=EOL) find_eol();
  212.     } while (tok!=FINISHED);
  213.     prog = temp;  /* restore to original */
  214. }


  215. /* find the start of next line */
  216. void find_eol()
  217. {
  218.     while (*prog!='/n'&&*prog!='/0')  ++prog;
  219.     if (*prog)  prog++;
  220. }


  221. /* return index of next free posion in the label array
  222.       -1 is returned if the array is full.
  223.       -2 is returned when duplicate label is found.
  224. */
  225. get_next_label(char *s)
  226. {
  227.     register int t;

  228.     for (t=0;t<NUM_LAB;++t) {
  229.         if (label_table[t].name[0]==0)  return t;
  230.         if (!strcmp(label_table[t].name,s)) return -2;  /* dup */
  231.     }
  232.     return -1;
  233. }

  234. /* find location of given label. A null is returned if
  235.    label is not found; ohtherwise a pointer to the position
  236.    of the label is returned.
  237. */
  238. char *find_label(char *s)
  239. {
  240.     register int t;

  241.     for (t=0;t<NUM_LAB;++t)
  242.         if (!strcmp(label_table[t].name,s))  return label_table[t].p;
  243.     return '/0';  /* error condition */
  244. }


  245. /* execute a GOTO statement. 
  246.  * goto一般形式即是 goto label 
  247.  */
  248. void exec_goto()
  249. {
  250.     char *loc;

  251.     get_token();  /* 这里获取标号,即是标签内容 */
  252.     
  253.     loc = find_label (token);  //标签是为跳转所用,所以获取标签后我们马上要想办法得到标签所代表地址
  254.     if (loc=='/0')
  255.         serror(7);  /* 出错 */
  256.     else prog=loc;  /* 重新 设置prog指针  指出了下一个我们运行的地址  我们得完全听他的*/
  257. }


  258. /* initialize the array that holds the labels.
  259.    by convention , a null label name indicates that
  260.    array posiiton is unused.
  261. */
  262. void label_init()
  263. {
  264.     register int t;

  265.     for (t=0;t<NUM_LAB;++t)  label_table[t].name[0]='/0';
  266. }


  267. /* execute an IF statement 
  268.  * 执行if语句
  269.  */
  270. void exec_if()
  271. {
  272.     int x,y,cond;
  273.     char op;
  274.     /* 这里我们只是处理一个简单的if  就是if (x operator y) */
  275.     get_exp(&x);  /* 获取操作符左边数值 */

  276.     get_token();  /* 获取操作符  "比较符" */
  277.     if (!strcmp("<>",*token))   //这里有点儿问题  一个字符串不可能跟一个字符比较吧
  278.     {
  279.         serror(0);  /* not a leagal oprator */
  280.         return;
  281.     }
  282.     op = *token;
  283.     get_exp(&y);  /* 操作符右边  */

  284.     /* determine the outcome */
  285.     cond = 0;
  286.     switch(op)  {
  287.         case '<':
  288.             if (x<y) cond=1;
  289.             break;
  290.         case '>':
  291.             if (x>y) cond=1;
  292.             break;
  293.         case '==':      //这里也是有点儿问题,op是字符类型 怎么会可能会是'==',而且好笑的是basic没有这个符号
  294.             if (x==y) cond=1;
  295.             break;
  296.     }
  297.     if (cond)  {  /* is true so process target of IF */
  298.         get_token();
  299.         if (tok != THEN)  {     //if 后面会连上then 所以有if没then是错误的
  300.             serror(8);
  301.             return;
  302.         }  /* else program execution starts on next line */
  303.     }
  304.     else find_eol();  /* find start of next line */
  305. }


  306. /* execute a FOR loop
  307.  * for 循环  其主要格式 文章第一篇已经给出  
  308.  * for i=1 to 10
  309.  * next i
  310.  * 下面就引用此例了
  311.  */
  312. void exec_for()
  313. {
  314.     struct for_stack i;     //申请一个栈元素  到时候加入
  315.     int value;

  316.     get_token();  /*  获取标号  这里获取到变量i */
  317.     if (!isalpha(*token))  //变量必定是字符型
  318.     {
  319.         serror(4);
  320.         return;
  321.     }

  322.     i.var = toupper(*token) - 'A';  /* 我们是把变量放在hash表中的  所以这里来计算变量在hash表中位置   */

  323.     get_token();  /* 这里得到了等号 */
  324.     if (*token!='=')  
  325.     {
  326.         serror(3);
  327.         return;
  328.     }
  329.     get_exp(&value);  /* 初始值  比如这里是1 */

  330.     variables[i.var]=value;     //这里把初始值放在变量数组中

  331.     get_token();

  332.     if (tok != TO) serror(9);  /* 读取to单词 */
  333.     get_exp(&i.target);  /* 取得最终要达到的数值  比如这里是10 */

  334.     /* if loop can execute at least once, push into on stack */
  335.     if (value<=i.target)  {
  336.         i.loc = prog;       //记录要执行的语句  这里是for循环的里面要执行的语句
  337.         fpush(i);       //压栈
  338.     }
  339.     else  /* otherwise, skip loop code altogether */
  340.         while (tok!=NEXT)  get_token();     //每到next之前  都输入for循环要执行的语句   所以一直执行
  341. }


  342. /* execute a NEXT statement */
  343. void next()
  344. {
  345.     struct for_stack i;

  346.     i = fpop();  /*read the loop info */

  347.     variables[i.var]++;  /* increment control variable */
  348.     if (variables[i.var]>i.target)  return;  /* all done */
  349.     fpush(i);   /* otherwise,return the info */
  350.     prog = i.loc;  /* loop */
  351. }


  352. /* push function for the FOR stack */
  353. void fpush(struct for_stack i)
  354. {
  355.     if (ftos>FOR_NEST)
  356.     serror(10);
  357.     fstack[ftos]=i;
  358.     ftos++;
  359. }


  360. struct for_stack fpop()
  361. {
  362.     ftos--;
  363.     if (ftos<0)  serror(11);
  364.     return (fstack[ftos]);
  365. }


  366. /* exec a simple form of BASIC INPUT command */
  367. void input()
  368. {
  369.     char str[80],var;
  370.     int i;

  371.     get_token();  /* see if prompt string id=s present */
  372.     if (token_type == QUOTE)  {
  373.         printf (token);  /* if so , print it and check for command */
  374.         get_token();
  375.         if (*token != ',')  serror(1);
  376.         get_token();
  377.     }
  378.     else printf ("? ");  /* otherwise, prompt with / */
  379.     var = toupper(*token) - 'A';  /* get the input var */

  380.     scanf ("%d",&i);  /* read input */
  381.     variables[var] = i;  /* store it */
  382. }


  383. /* execute a GOSUB command 
  384.  * 这个类似c语言中的函数调用 */
  385. void gosub()
  386. {
  387.     char *loc;

  388.     get_token();
  389.     /* find the label to call */
  390.     loc = find_label(token);
  391.     if (loc=='/0')
  392.         serror(7);  /* label not defined */
  393.     else  
  394.     {
  395.         gpush(prog);  /* 当前执行的地址压栈 */
  396.         prog = loc;  /* 重新把要执行的地址赋值给prog */
  397.     }
  398. }


  399. /* return from GOSUB */
  400. void greturn()
  401. {
  402.     prog = gpop();
  403. }


  404. /* GOSUB stack push function */
  405. void gpush(char *s)
  406. {
  407.     gtos++;

  408.     if (gtos==SUB_NEST)  
  409.     {
  410.         serror(12);
  411.         return;
  412.     }

  413.     gstack[gtos] = s;
  414. }


  415. /* GOSUB stack pop function */
  416. char *gpop()
  417. {
  418.     if (gtos==0)  {
  419.         serror(13);
  420.         return 0;
  421.     }
  422.     return gstack[gtos--];
  423. }

  424. main (int argc,char *argv[])
  425. {
  426.     char in[80];
  427.     int answer;
  428.     char *p_buf;
  429.     char *t;

  430.     if (argc!=2)  {
  431.         printf ("usage: run <filename>/n");
  432.         exit (1);
  433.     }

  434.     /* allocate memory for the program */
  435.     if (!(p_buf=(char *)malloc(PROG_SIZE)))  {
  436.         printf ("allocation failure");
  437.         exit (1);
  438.     }

  439.     /* load the program to execute */
  440.     if (!load_program(p_buf,argv[1]))  exit(1);

  441.     if (setjmp(e_buf))  exit(1); /* initialize the long jump */

  442.     prog = p_buf;
  443.     scan_labels();  /*  搜索所有的标签  */
  444.     ftos = 0;  /* 初始化栈  这个是为for循环作准备的  */
  445.     gtos = 0;  /* 初始化栈  这个是为gosub作准备的 */
  446.     do  {
  447.         token_type = get_token();
  448.         /* 如果当前是变量 */
  449.         if (token_type==VARIABLE)  {
  450.             putback();  /* 回退prog指针到变量前 */
  451.             assignment();  /* 赋值  */
  452.         }
  453.         else  /* 除了变量那就是关键字了  可能有同学会问  呃  那个比如一个数字怎么没考虑  请想想一个数字怎么会单独出现 */
  454.             switch (tok)  {
  455.                 case PRINT:
  456.                     print();
  457.                     break;
  458.                 case GOTO:
  459.                     exec_goto();
  460.                     break;
  461.                 case IF:
  462.                     exec_if();
  463.                     break;
  464.                 case FOR:
  465.                     exec_for();
  466.                     break;
  467.                 case NEXT:
  468.                     next();
  469.                     break;
  470.                 case INPUT:
  471.                     input();
  472.                     break;
  473.                 case GOSUB:
  474.                     gosub();
  475.                     break;
  476.                 case RETURN:
  477.                     greturn();
  478.                     break;
  479.                 case END:
  480.                     exit(0);
  481.             }
  482.     }while (tok != FINISHED);
  483. }






刺猬@littlehedgehog
### 回答1: 自己动手一个Basic解释器(也就是将Basic语言的代码转化为计算机可以理解的指令集合)需要一定的编程知识和技能。以下是一个简要的步骤介绍,以帮助您编一个基本的Basic解释器。 第一步是理解Basic语言的语法和特性。Basic是一种面向过程的编程语言,其语法包括变量的声明、赋值和操作,循环结构,条件语句,函数和子程序等。 第二步是设计解释器的整体架构。你需要决定如何表示和存储变量、函数和代码的执行顺序。这可以通过数据结构(如符号表、堆栈和解析树)来实现。 第三步是编一个词法解析器,将Basic代码分解为一个个的词法单元(如关键字、操作符和标识符)。词法解析器可以使用正则表达式或手动编的代码实现。 第四步是编一个语法解析器,将词法单元转化为可执行的指令。这可以通过使用解析器生成器(如Yacc/Bison)或手动编的代码来实现。 第五步是实现变量和表达式的存储和计算。你需要设计适当的数据结构来存储变量的值,并在执行代码时对表达式进行求值。 第六步是实现控制结构,如循环和条件语句。你需要考虑如何跟踪和修改代码的执行路径,以实现控制流程的正确运行。 第步是实现函数和子程序的定义和调用。你需要设计一种数据结构来存储函数的参数和局部变量,并在调用函数时正确地传递参数和保存返回值。 第八步是测试解释器的功能和性能。编一些基本的Basic代码,并验证解释器能够正确地解释并执行这些代码。 总结而言,自己动手一个Basic解释器需要深入理解Basic语言的语法和特性,并具备编程基础知识和技能。通过合理设计解释器的架构,编词法解析器、语法解析器和执行引擎,最后进行测试和调试,就可以实现一个基本的Basic解释器。 ### 回答2: 编一个 BASIC 解释器是一项相对复杂的任务,需要对计算机系统和编程语言规范有一定的了解和技能。在简要介绍这个解释器的实现过程之前,可以先了解一下 BASIC 编程语言的基本特性和工作原理。 BASIC(Beginner's All-purpose Symbolic Instruction Code)是一种旨在教授编程基础的高级编程语言,具有易学易用的特点。一个 BASIC 解释器的主要任务是读取并解释 BASIC 代码,将其转化为计算机能够理解和执行的指令。 为了编一个 BASIC 解释器的 zip 版本,我们可以按以下步骤进行操作: 1. 了解 BASIC 语言规范:熟悉 BASIC 语言的基本语法和关键字,例如变量、循环、条件语句等。这样才能准确解析和执行 BASIC 代码。 2. 设计词法分析器:编一个词法分析器,用于将输入的 BASIC 代码分解成标记(Tokens),例如关键字、变量名和数值等。这个过程称为词法分析。 3. 设计语法分析器:编一个语法分析器,根据 BASIC 语法规则将标记转化为语法树。语法树是一个树形结构,用于描述程序的结构和执行顺序。 4. 设计执行引擎:在基于语法树的基础上,编一个执行引擎,按照语法树的指令顺序逐条执行 BASIC 代码。这个过程涉及到变量管理、循环控制、条件判断等。 5. 实现基本功能和特性:根据 BASIC 语言规范,逐步实现基本的功能和特性,例如变量赋值、循环结构、条件判断、输入输出等。确保解释器能够正确解析和执行这些功能。 6. 进行测试和调试:编一些简单的 BASIC 代码进行测试,并且调试解释器中出现的错误和问题。持续进行测试和修复,直到解释器能够正确处理各种情况下的 BASIC 代码。 总结起来,编一个 BASIC 解释器需要一定的计算机科学基础和编程技能。通过设计词法分析器、语法分析器和执行引擎等模块,实现解释器的基本功能和特性。最后,通过测试和调试确保解释器能够正确解析和执行 BASIC 代码。 ### 回答3: 编 BASIC 解释器是一个非常有趣和有挑战性的任务。BASIC(Beginner's All-purpose Symbolic Instruction Code)是一种广泛使用的高级程序设计语言,在学习编程的初级阶段非常流行。基本的解释器应该能够解析和执行 BASIC 代码。 首先,需要设计和实现一个词法分析器(lexer),它能够将 BASIC 代码分解成一个个的词法单元,例如关键字、标识符、运算符和常量。然后,需要编一个语法分析器(parser),使用上一步的词法单元构建抽象语法树(AST),以表示 BASIC 代码的结构和语义。 接下来,解释器需要按照 BASIC 语法规则,逐行解析和执行 AST。这包括执行赋值语句、条件语句、循环语句等。对于变量的维护,需要实现一个符号表(symbol table),用于存储和管理变量的值和类型。 在执行过程中,解释器还需要实现算术和逻辑运算、函数调用和参数传递等功能。同时,解释器还需要处理用户输入和输出,以便与用户交互。 最后,为了提高解释器的性能和效率,可以添加优化和解释器技巧,例如解释器解释器、即时编译等。 总之,编 BASIC 解释器需要深入理解 BASIC 语言的语法和语义,熟悉编译原理和解释器设计。通过实现一个自己的 BASIC 解释器,可以更好地理解程序执行的原理和底层工作机制。这对于提高编程技能和理解高级语言的内部运行方式非常有帮助。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值