GCC-3.4.6源代码学习笔记(74)

5.3.1.2.    宏调用

对于我们的案例,这下一个符号是“FUN”。对于这个节点,它的类型无疑是NT_MACRO。只要它不是失能的宏,并且不是在禁止宏展开的环境下(即,在处理断言、pragma#if系列指示,解析宏参数、函数式的宏调用),enter_macro_context将被调用。

 

709  static int

710  enter_macro_context (cpp_reader *pfile, cpp_hashnode *node)                      in cppmacro.c

711   {

712    /* The presence of a macro invalidates a file's controlling macro.  */

713    pfile->mi_valid = false;

714 

715    pfile->state.angled_headers = false;

716 

717   /* Handle standard macros.  */

718    if (! (node->flags & NODE_BUILTIN))

719    {

720      cpp_macro *macro = node->value.macro;

721 

722      if (macro->fun_like)

723      {

724        _cpp_buff *buff;

725 

726        pfile->state.prevent_expansion++;

727        pfile->keep_tokens++;

728        pfile->state.parsing_args = 1;

729        buff = funlike_invocation_p (pfile, node);

730        pfile->state.parsing_args = 0;

731        pfile->keep_tokens--;

732        pfile->state.prevent_expansion--;

733 

734        if (buff == NULL)

735        {

736          if (CPP_WTRADITIONAL (pfile) && ! node->value.macro->syshdr)

737            cpp_error (pfile, CPP_DL_WARNING,

738               "function-like macro /"%s/" must be used with arguments in traditional C",

739                   NODE_NAME (node));

740 

741          return 0;

742        }

743 

744        if (macro->paramc > 0)

745          replace_args (pfile, node, macro, (macro_arg *) buff->base);

746        _cpp_release_buff (pfile, buff);

747      }

748 

749      /* Disable the macro within its expansion.  */

750      node->flags |= NODE_DISABLED;

751 

752      macro->used = 1;

753 

754      if (macro->paramc == 0)

755        push_token_context (pfile, node, macro->exp.tokens, macro->count);

756 

757      return 1;

758    }

759 

760   /* Handle built-in macros and the _Pragma operator.  */

761    return builtin_macro (pfile, node);

762  }

 

注意在729行对funlike_invocation_p调用前后,对pfile的设置。

5.3.1.2.1.            函数式宏的扩展—实参收集

对于非内建宏,它需要设立上下文来进行宏的扩展。在720行,回忆整个宏的定义保存在宏的标识符节点的macro域中(参考create_iso_definition)。对于非函数形式的宏,把这部分符号放入一个新的上下文就可以了,如上面的755行。但对于还是形式的宏,它还需要进行宏的实参替换,这个上下文在实参替换中给出。注意在728行,域parsing_args在调用funlik_invocation_p前,设为1,之后重置为0

 

669  static _cpp_buff *

670  funlike_invocation_p (cpp_reader *pfile, cpp_hashnode *node)              in cppmacro.c

671  {

672    const cpp_token *token, *padding = NULL;

673 

674    for (;;)

675    {

676      token = cpp_get_token (pfile);

677      if (token->type != CPP_PADDING)

678        break;

679      if (padding == NULL

680          || (!(padding->flags & PREV_WHITE) && token->val.source == NULL))

681        padding = token;

682    }

683 

684    if (token->type == CPP_OPEN_PAREN)

685    {

686      pfile->state.parsing_args = 2;

687      return collect_args (pfile, node);

688    }

689 

690    /* CPP_EOF can be the end of macro arguments, or the end of the

691      file. We mustn't back up over the latter. Ugh.  */

692    if (token->type != CPP_EOF || token == &pfile->eof)

693    {

694      /* Back up. We may have skipped padding, in which case backing

695        up more than one token when expanding macros is in general

696        too difficult. We re-insert it in its own context.  */

697      _cpp_backup_tokens (pfile, 1);

698      if (padding)

699        push_token_context (pfile, NULL, padding, 1);

700    }

701 

702    return NULL;

703  }

 

例程collect_args收集宏调用时的实参。这些实参被保存,随后用于实参替换。注意到在进入该函数前,域parsing_args被设为2——已找到左括号。

 

535  static _cpp_buff *

536  collect_args (cpp_reader *pfile, const cpp_hashnode *node)                   in cppmacro.c

537  {

538    _cpp_buff *buff, *base_buff;

539    cpp_macro *macro;

540    macro_arg *args, *arg;

541    const cpp_token *token;

542    unsigned int argc;

543 

544    macro = node->value.macro;

545    if (macro->paramc)

546      argc = macro->paramc;

547    else

548      argc = 1;

549    buff = _cpp_get_buff (pfile, argc * (50 * sizeof (cpp_token *)

550                              + sizeof (macro_arg)));

551    base_buff = buff;

552    args = (macro_arg *) buff->base;

553    memset (args, 0, argc * sizeof (macro_arg));

554    buff->cur = (unsigned char *) &args[argc];

555    arg = args, argc = 0;

 

cpp_macroparamc域记录了宏参数的个数。类型macro_arg被设计用来保存用于实参替换的必要信息。

 

32    struct macro_arg                                                                                 in cppmacro.c

33    {

34      const cpp_token **first;    /* First token in unexpanded argument.  */

35      const cpp_token **expanded;    /* Macro-expanded argument.  */

36      const cpp_token *stringified;    /* Stringified argument.  */

37      unsigned int count;           /* # of tokens in argument.  */

38      unsigned int expanded_count;   /* # of tokens in expanded argument.  */

39    };

 

下面,在578行,看到cpp_get_token被调用提取实参的符号。对于我们的案例,指示#ifdef将被作为第三个符号读入,根据[12]这样的指示的行为标准没有定义,不过GCC决定让这些指示的行为如常——也就是调用_cpp_handle_directive。注意,在解析参数的过程中, 如果实参是一个宏,标准要求它被禁止展开(但在实参替换时展开)。

 

collect_args (continue)

 

557    /* Collect the tokens making up each argument. We don't yet know

558      how many arguments have been supplied, whether too many or too

559      few. Hence the slightly bizarre usage of "argc" and "arg".  */

560    do

561    {

562      unsigned int paren_depth = 0;

563      unsigned int ntokens = 0;

564 

565      argc++;

566      arg->first = (const cpp_token **) buff->cur;

567 

568      for (;;)

569      {

570        /* Require space for 2 new tokens (including a CPP_EOF).  */

571        if ((unsigned char *) &arg->first[ntokens + 2] > buff->limit)

572        {

573          buff = _cpp_append_extend_buff (pfile, buff,

574                                      1000 * sizeof (cpp_token *));

575          arg->first = (const cpp_token **) buff->cur;

576        }

577 

578        token = cpp_get_token (pfile);

579 

580        if (token->type == CPP_PADDING)

581        {

582          /* Drop leading padding.  */

583          if (ntokens == 0)

584            continue;

585        }

586        else if (token->type == CPP_OPEN_PAREN)

587          paren_depth++;

588        else if (token->type == CPP_CLOSE_PAREN)

589        {

590          if (paren_depth-- == 0)

591            break;

592        }

593        else if (token->type == CPP_COMMA)

594        {

595          /* A comma does not terminate an argument within

596            parentheses or as part of a variable argument.  */

597          if (paren_depth == 0

598             && ! (macro->variadic && argc == macro->paramc))

599            break;

600        }

601        else if (token->type == CPP_EOF

602             || (token->type == CPP_HASH && token->flags & BOL))

603          break;

604 

605        arg->first[ntokens++] = token;

606      }

607 

608      /* Drop trailing padding.  */

609      while (ntokens > 0 && arg->first[ntokens - 1]->type == CPP_PADDING)

610        ntokens--;

611  

612      arg->count = ntokens;

613      arg->first[ntokens] = &pfile->eof;

614 

615      /* Terminate the argument. Excess arguments loop back and

616        overwrite the final legitimate argument, before failing.  */

617      if (argc <= macro->paramc)

618      {

619        buff->cur = (unsigned char *) &arg->first[ntokens + 1];

620        if (argc != macro->paramc)

621          arg++;

622      }

623    }

624    while (token->type != CPP_CLOSE_PAREN && token->type != CPP_EOF);

 

注意到虽然实参由逗号分隔,但ISO标准允许在逗号两边插入任意的空白符,因而使用嵌套的循环来处理这个情形。内层的FOR循环取出可能带有拖尾空白符的参数(注意空白符也是符号),然后609行的while循环剥除这些空白符,只留下参数符号。

 

collect_args (continue)

 

626    if (token->type == CPP_EOF)

627    {

628      /* We still need the CPP_EOF to end directives, and to end

629        pre-expansion of a macro argument. Step back is not

630        unconditional, since we don't want to return a CPP_EOF to our

631        callers at the end of an -include-d file.  */

632      if (pfile->context->prev || pfile->state.in_directive)

633        _cpp_backup_tokens (pfile, 1);

634      cpp_error (pfile, CPP_DL_ERROR,

635               "unterminated argument list invoking macro /"%s/"",

636               NODE_NAME (node));

637    }

638    else

639    {

640      /* A single empty argument is counted as no argument.  */

641      if (argc == 1 && macro->paramc == 0 && args[0].count == 0)

642        argc = 0;

643      if (_cpp_arguments_ok (pfile, macro, node, argc))

644      {

645        /* GCC has special semantics for , ## b where b is a varargs

646          parameter: we remove the comma if b was omitted entirely.

647          If b was merely an empty argument, the comma is retained.

648          If the macro takes just one (varargs) parameter, then we

649          retain the comma only if we are standards conforming.

650 

651          If FIRST is NULL replace_args () swallows the comma.  */

652        if (macro->variadic && (argc < macro->paramc

653                            || (argc == 1 && args[0].count == 0

654                              && !CPP_OPTION (pfile, std))))

655          args[macro->paramc - 1].first = NULL;

656        return base_buff;

657      }

658    }

659 

660   /* An error occurred.  */

661    _cpp_release_buff (pfile, base_buff);

662    return NULL;

663  }

 

在找到右括号前,碰到文件结尾是一种异常的情况。否则右括号将结束符号的提取,实参接下来需要验证有效性。

 

494  bool

495  _cpp_arguments_ok(cpp_reader *pfile, cpp_macro *macro, const cpp_hashnode *node, unsigned int argc)

496  {

497    if (argc == macro->paramc)

498      return true;

499 

500    if (argc < macro->paramc)

501    {

502      /* As an extension, a rest argument is allowed to not appear in

503        the invocation at all.

504        e.g. #define debug(format, args...) something

505          debug("string");

506 

507        This is exactly the same as if there had been an empty rest

508        argument - debug("string", ).  */

509 

510      if (argc + 1 == macro->paramc && macro->variadic)

511       {

512        if (CPP_PEDANTIC (pfile) && ! macro->syshdr)

513          cpp_error (pfile, CPP_DL_PEDWARN,

514                   "ISO C99 requires rest arguments to be used");

515        return true;

516      }

517 

518      cpp_error (pfile, CPP_DL_ERROR,

519               "macro /"%s/" requires %u arguments, but only %u given",

520                NODE_NAME (node), macro->paramc, argc);

521    }

522    else

523      cpp_error (pfile, CPP_DL_ERROR,

524               "macro /"%s/" passed %u arguments, but takes just %u",

525               NODE_NAME (node), argc, macro->paramc);

526 

527    return false;

528  }

 

这个有效性验证仅考虑实参数目的准确性。

5.3.1.2.2.            函数式宏的扩展—实参替换

在收集了实参之后,在enter_macro_context745行,这些实参将由replace_args来替换定义中的形参。

 

768  static void

769  replace_args(cpp_reader *pfile, cpp_hashnode *node, cpp_macro *macro, macro_arg *args)

770  {

771    unsigned int i, total;

772    const cpp_token *src, *limit;

773    const cpp_token **dest, **first;

774    macro_arg *arg;

775    _cpp_buff *buff;

776 

777    /* First, fully macro-expand arguments, calculating the number of

778      tokens in the final expansion as we go. The ordering of the if

779      statements below is subtle; we must handle stringification before

780      pasting.  */

781    total = macro->count;

782    limit = macro->exp.tokens + macro->count;

783 

784    for (src = macro->exp.tokens; src < limit; src++)

785      if (src->type == CPP_MACRO_ARG)

786      {

787        /* Leading and trailing padding tokens.  */

788        total += 2;

789 

790        /* We have an argument. If it is not being stringified or

791          pasted it is macro-replaced before insertion.  */

792        arg = &args[src->val.arg_no - 1];

793 

794        if (src->flags & STRINGIFY_ARG)

795        {

796          if (!arg->stringified)

797           arg->stringified = stringify_arg (pfile, arg);

798        }

799        else if ((src->flags & PASTE_LEFT)

800               || (src > macro->exp.tokens && (src[-1].flags & PASTE_LEFT)))

801          total += arg->count - 1;

802        else

803        {

804          if (!arg->expanded)

805            expand_arg (pfile, arg);

806          total += arg->expanded_count - 1;

807        }

808      }

 

781行,域count记录了定义中的符号数,但实参替换后,这个数字可能就不准确了。在784行开始的FOR循环更新这个数字。对于定义体中所出现的形参,它们具有类型CPP_MACRO_ARG

根据ISO C/C++标准,跟在#(字符串化操作符)后的符号,及##(粘贴操作符)两边的符号,就算是宏,也不能展开。对于字符串化的情况,#操作符只能后跟一个符号,因此符号数没有变化。对于符号粘贴的情况,事实上,我们可以把一个表达式作为一个实参传入,例如:

 

#define fA(A, B)  c = A##B;

fA (a+b, B)     // output a+bB;

 

显然增加的符号数是构成实参符号的数目减去1。而对于剩下的情况,作为宏的实参应该被正常地展开。

 

979  static void

980  expand_arg (cpp_reader *pfile, macro_arg *arg)                                   in cppmacro.c

981  {

982    unsigned int capacity;

983    bool saved_warn_trad;

984 

985    if (arg->count == 0)

986      return;

987 

988    /* Don't warn about funlike macros when pre-expanding.  */

989    saved_warn_trad = CPP_WTRADITIONAL (pfile);

990    CPP_WTRADITIONAL (pfile) = 0;

991 

992    /* Loop, reading in the arguments.  */

993    capacity = 256;

994    arg->expanded = xmalloc (capacity * sizeof (cpp_token *));

995 

996    push_ptoken_context (pfile, NULL, NULL, arg->first, arg->count + 1);

997    for (;;)

998    {

999      const cpp_token *token;

1000

1001     if (arg->expanded_count + 1 >= capacity)

1002     {

1003       capacity *= 2;

1004       arg->expanded = xrealloc (arg->expanded,

1005                             capacity * sizeof (cpp_token *));

1006     }

1007

1008     token = cpp_get_token (pfile);

1009

1010     if (token->type == CPP_EOF)

1011       break;

1012

1013     arg->expanded[arg->expanded_count++] = token;

1014   }

1015

1016   _cpp_pop_context (pfile);

1017

1018   CPP_WTRADITIONAL (pfile) = saved_warn_trad;

1019 }

 

为了正确解析任意的嵌套的宏,需要由push_ptoken_context设立新的上下文。

 

931  static void

932  push_ptoken_context (cpp_reader *pfile, cpp_hashnode *macro,             in cppmacro.c

933              _cpp_buff *buff, const cpp_token **first, unsigned int count)

934  {

935    cpp_context *context = next_context (pfile);

936 

937    context->direct_p = false;

938    context->macro = macro;

939    context->buff = buff;

940    FIRST (context).ptoken = first;

941    LAST (context).ptoken = first + count;

942  }

 

设立了这个上下文后,在expand_arg1008行,cpp_get_token 将从这个上下文提取符号。如果实参也是宏,在1099行,enter_macro_context被再一次调用。从调用的次序,可以看到,嵌套的宏的处理是深度优先的,因此当在1011行退出FOR循环时,所有的嵌套其中的宏都应该得到处理。注意对于这个展开后的实参,所产生的符号保存在对应的macro_arg对象的expanded域。

 

replace_args (continue)

 

810    /* Now allocate space for the expansion, copy the tokens and replace

811       the arguments.  */

812    buff = _cpp_get_buff (pfile, total * sizeof (cpp_token *));

813    first = (const cpp_token **) buff->base;

814    dest = first;

815 

816    for (src = macro->exp.tokens; src < limit; src++)

817    {

818      unsigned int count;

819      const cpp_token **from, **paste_flag;

820 

821      if (src->type != CPP_MACRO_ARG)

822      {

823        *dest++ = src;

824        continue;

825      }

826 

827      paste_flag = 0;

828      arg = &args[src->val.arg_no - 1];

829      if (src->flags & STRINGIFY_ARG)

830        count = 1, from = &arg->stringified;

831      else if (src->flags & PASTE_LEFT)

832        count = arg->count, from = arg->first;

833      else if (src != macro->exp.tokens && (src[-1].flags & PASTE_LEFT))

834      {

835        count = arg->count, from = arg->first;

836        if (dest != first)

837        {

838          if (dest[-1]->type == CPP_COMMA

839             && macro->variadic

840             && src->val.arg_no == macro->paramc)

841          {

842            /* Swallow a pasted comma if from == NULL, otherwise

843              drop the paste flag.  */

844            if (from == NULL)

845              dest--;

846            else

847              paste_flag = dest - 1;

848          }

849          /* Remove the paste flag if the RHS is a placemarker.  */

850          else if (count == 0)

851            paste_flag = dest - 1;

852        }

853      }

854      else

855        count = arg->expanded_count, from = arg->expanded;

856 

857      /* Padding on the left of an argument (unless RHS of ##).  */

858      if ((!pfile->state.in_directive || pfile->state.directive_wants_padding)

859          && src != macro->exp.tokens && !(src[-1].flags & PASTE_LEFT))

860        *dest++ = padding_token (pfile, src);

861 

862      if (count)

863      {

864        memcpy (dest, from, count * sizeof (cpp_token *));

865        dest += count;

866 

867        /* With a non-empty argument on the LHS of ##, the last

868          token should be flagged PASTE_LEFT.  */

869        if (src->flags & PASTE_LEFT)

870          paste_flag = dest - 1;

871      }

872 

873      /* Avoid paste on RHS (even case count == 0).  */

874      if (!pfile->state.in_directive && !(src->flags & PASTE_LEFT))

875        *dest++ = &pfile->avoid_paste;

876 

877      /* Add a new paste flag, or remove an unwanted one.  */

878      if (paste_flag)

879      {

880        cpp_token *token = _cpp_temp_token (pfile);

881        token->type = (*paste_flag)->type;

882        token->val.str = (*paste_flag)->val.str;

883        if (src->flags & PASTE_LEFT)

884          token->flags = (*paste_flag)->flags | PASTE_LEFT;

885        else

886          token->flags = (*paste_flag)->flags & ~PASTE_LEFT;

887        *paste_flag = token;

888      }

889    }

890 

891    /* Free the expanded arguments.  */

892    for (i = 0; i < macro->paramc; i++)

893      if (args[i].expanded)

894        free (args[i].expanded);

895 

896    push_ptoken_context (pfile, node, buff, first, dest - first);

897  }

 

因为用于展开宏的空间已经被计算了,那么创建这个尺寸的缓存用于保存展开宏的符号。因为有可能将由多个符号组成的符号序列作为实参(如上例的表达式),对于符号粘贴操作,需要将序列中第一个符号(在##右边),或最后的符号(在##左边)标记为PASTE_LEFT。在896行,又一个新的上下文被创建,使得这些符号成为下一个提前的候选。

对于符号粘贴的情况,在cpp_get_token1071行,将调用paste_all_tokens

 

448  static void

449  paste_all_tokens (cpp_reader *pfile, const cpp_token *lhs)                     in cppmacro.c

450  {

451    const cpp_token *rhs;

452    cpp_context *context = pfile->context;

453 

454    do

455    {

456      /* Take the token directly from the current context. We can do

457        this, because we are in the replacement list of either an

458        object-like macro, or a function-like macro with arguments

459        inserted. In either case, the constraints to #define

460        guarantee we have at least one more token.  */

461      if (context->direct_p)

462        rhs = FIRST (context).token++;

463      else

464        rhs = *FIRST (context).ptoken++;

465 

466      if (rhs->type == CPP_PADDING)

467        abort ();

468 

469      if (!paste_tokens (pfile, &lhs, rhs))

470      {

471        _cpp_backup_tokens (pfile, 1);

472 

473        /* Mandatory error for all apart from assembler.  */

474        if (CPP_OPTION (pfile, lang) != CLK_ASM)

475          cpp_error (pfile, CPP_DL_ERROR,

476                   "pasting /"%s/" and /"%s/" does not give a valid preprocessing token",

477                   cpp_token_as_text (pfile, lhs),

478                   cpp_token_as_text (pfile, rhs));

479        break;

480      }

481    }

482    while (rhs->flags & PASTE_LEFT);

483 

484    /* Put the resulting token in its own context.  */

485    push_token_context (pfile, NULL, lhs, 1);

486  }

 

注意在424行,只有‘/’‘=’可以被粘贴起来产生‘/=’操作符,其他‘/’及非‘=’对需要在中间插入空白符。而下面433行,_cpp_temp_token创建了一个新的可用符号,其行号、列号从filecur_token拷贝(即被粘贴的符号)。这样确保诊断系统能给出准确的诊断信息。

 

407  static bool

408  paste_tokens (cpp_reader *pfile, const cpp_token **plhs, const cpp_token *rhs)

409  {

410    unsigned char *buf, *end;

411     const cpp_token *lhs;

412    unsigned int len;

413    bool valid;

414 

415    lhs = *plhs;

416    len = cpp_token_len (lhs) + cpp_token_len (rhs) + 1;

417    buf = alloca (len);

418    end = cpp_spell_token (pfile, lhs, buf);

419 

420    /* Avoid comment headers, since they are still processed in stage 3.

421      It is simpler to insert a space here, rather than modifying the

422      lexer to ignore comments in some circumstances. Simply returning

423      false doesn't work, since we want to clear the PASTE_LEFT flag.  */

424    if (lhs->type == CPP_DIV && rhs->type != CPP_EQ)

425      *end++ = ' ';

426    end = cpp_spell_token (pfile, rhs, end);

427    *end = '/n';

428 

429    cpp_push_buffer (pfile, buf, end - buf, /* from_stage3 */ true);

430    _cpp_clean_line (pfile);

431 

432    /* Set pfile->cur_token as required by _cpp_lex_direct.  */

433    pfile->cur_token = _cpp_temp_token (pfile);

434    *plhs = _cpp_lex_direct (pfile);

435    valid = pfile->buffer->cur == pfile->buffer->rlimit;

436    _cpp_pop_buffer (pfile);

437 

438    return valid;

439  }

 

在上面的429行,串接起来的名字被加入filebuffer域, 然后_cpp_lex_direct将这个串接名字作为符号提前出来,并赋给plhs。同样看到当返回到paste_all_tokens后,在485行,这个符号被2放入一个新的上下文,它在下一次调用cpp_get_token 时被提取出来。

在离开这一节前,我们看一下_cpp_lex_token里,lookahead域中的符号是从那里来的。它们来自在基本上下文中回退一个或多个符号时。

 

1153 void

1154 _cpp_backup_tokens (cpp_reader *pfile, unsigned int count)                  in cppmacro.c

1155 {

1156   if (pfile->context->prev == NULL)

1157   {

1158     pfile->lookaheads += count;

1159     while (count--)

1160     {

1161       pfile->cur_token--;

1162       if (pfile->cur_token == pfile->cur_run->base

1163          /* Possible with -fpreprocessed and no leading #line.  */

1164          && pfile->cur_run->prev != NULL)

1165       {

1166         pfile->cur_run = pfile->cur_run->prev;

1167         pfile->cur_token = pfile->cur_run->limit;

1168       }

1169     }

1170   }

1171   else

1172   {

1173     if (count != 1)

1174       abort ();

1175     if (pfile->context->direct_p)

1176       FIRST (pfile->context).token--;

1177     else

1178       FIRST (pfile->context).ptoken--;

1179   }

1180 }

 

enter_macro_context的末尾,注意到在750行,现在宏的标识符节点被标记为禁止扩展。这意味着在随后的读入中,如果在扩展体内发现了宏,它将被按普通标识符来处理。它避免了宏调用自己而导致无限递归,从而弄垮编译器。

而当这个宏展开后的上下文被读取完成后,在移除时,这个宏将重新被激活。

 

1024 void

1025 _cpp_pop_context (cpp_reader *pfile)                                                         in cppmacro.c

1026 {

1027   cpp_context *context = pfile->context;

1028

1029   if (context->macro)

1030     context->macro->flags &= ~NODE_DISABLED;

1031

1032   if (context->buff)

1033     _cpp_release_buff (pfile, context->buff);

1034

1035   pfile->context = context->prev;

1036 }

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值