GCC/G++用法汇总

一 g++和gcc的区别

  GCC:GNU Compiler Collection(GUN 编译器集合),它可以编译C、C++、JAV、Fortran、Pascal、Object-C、Ada等语言;
  gcc:是GCC中的GUN C Compiler(C 编译器);
  g++:是GCC中的GUN C++ Compiler(C++编译器);
  一个有趣的事实就是,就本质而言,gcc和g++并不是编译器,也不是编译器的集合,它们只是一种驱动器,根据参数中要编译的文件的类型,调用对应的GUN编译器而已,比如,用gcc编译一个c文件的话,会有以下几个步骤:
  Step1:Call a preprocessor, like cpp.
  Step2:Call an actual compiler, like cc or cc1.
  Step3:Call an assembler, like as.
  Step4:Call a linker, like ld.
  由于编译器是可以更换的,所以gcc不仅仅可以编译C文件,所以,更准确的说法是:gcc调用了C compiler,而g++调用了C++ compiler。
  c和g++的主要区别:
  1、对于*.c和*.cpp文件,gcc分别当做c和cpp文件编译(c和cpp的语法强度是不一样的);
  2、对于 *.c和*.cpp文件,g++则统一当做cpp文件编译;
  3、使用g++编译文件时,g++会自动链接标准库STL,而gcc不会自动链接STL;
  4、gcc在编译C文件时,可使用的预定义宏是比较少的;
  5、gcc在编译cpp文件时/g++在编译c文件和cpp文件时(这时候gcc和g++调用的都是cpp文件的编译器),会加入一些额外的宏,这些宏如下:

#define __GXX_WEAK__ 1
#define __cplusplus 1
#define __DEPRECATED 1
#define __GNUG__ 4
#define __EXCEPTIONS 1
#define __private_extern__ extern

  6、在用gcc编译c++文件时,为了能够使用STL,需要加参数 –lstdc++ ,但这并不代表 gcc –lstdc++ 和 g++等价,它们的区别不仅仅是这个。
  误区:
  误区一:.c文件只能用gcc进行编译,.cpp文件只能用g++进行编译。
  (1)gcc和g++均可以编译c代码和c++代码,后缀为.c的,gcc把它当作C程序,而g++当作c++程序;后缀为.cpp的,两者均当作c++程序进行编译。虽然c++是c的超集,但两者对语法的要求是有区别的。C++的语法规则更加严谨一些。
  (2)编译阶段,g++会调用gcc,对于c++代码两者是等价的,但因为gcc命令不能自动和c++程序使用的库链接,所以通常用g++来完成链接,为了统一起见,干脆编译、链接统统用g++了,这就给人一种错觉好像.cpp文件只能用g++。
  误区二:gcc不会定义__cplusplus__宏,而g++会。
  (1)这个宏只是标志着编译器会把代码按照C/C++语法规则进行解释、编译。故此,如果后缀为.c,并且采用gcc编译器,则该宏就是未定义的,否则,就是已定义。
  误区三:编译只能用gcc,链接只能用g++.
  (1)混淆概念。应说为:编译可以用gcc/g++,而链接可以用g++或者gcc –lstdc++.因为gcc命令不能自动和c++程序使用的库链接,所以通常使用g++来完成链接。但在编译阶段,g++会自动调用gcc,二者等价。
  误区四:extern “C”和gcc/g++有关系。
  (1)实际上并无关系,无论是gcc还是g++,用extern “C”时,都是以C的命名方式来为symbol命名,否则,都是以c++方式命名。

二 gcc的参数

1 库和头文件

  1、-l:用来指定程序要链接的库,-l参数紧接着就是库名。那么库名跟真正的库文件名有什么关系呢?就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了。
  2、-L:跟着的是库文件所在的目录名。比如我们把libtest.so在/aaa/bbb/ccc目录下,那链接参数就是-L/aaa/bbb/ccc -ltest。
  3、-I(大写的i):编译程序按照-I指定的路进去搜索头文件。-I/home/include/表示将-I/home/include/目录作为第一个寻找头文件的目录,寻找的顺序是: /home/include/ -->/usr/include–>/usr/local/include。

2 警告

  -Wall开启所有警告;-w关闭警告。

3 操作大文件

gcc -o ser ser.c pub.h -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE

三 优化选项

  gcc默认提供了5级优化选项的集合:
  -O0:无优化(默认)。
  -O和-O1:使用能减少目标文件大小以及执行时间并且不会使编译时间明显增加的优化。在编译大型程序的时候会显著增加编译时内存的使用和时间。
   o1是最基本的优化,主要对代码的分支,表达式,常量来进行优化,编译器会在较短的时间下将代码变得更加短小,这样体积就会变得更小,会减少内存的占用率,在操作系统进行内存调度时就会更快。但是事情没有绝对的优点,当一个庞大的程序被拆碎细分的话,内存占用会大大增加,由于当今系统大多数都是多线程,就会出现卡顿和反应延迟。
  当在机子上,运行无法干涉到调试时,-O选项也会添加-fomit-frame-pointer选项。在ADA编译器上,-O不会添加-ftree-sra选项,想在ADA编译器下使用该选项,必须明确的在命令行指出。
  -O2: 此选项将增加编译时间和目标文件的执行性能。
包含-O1的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化。
编译器不执行循环展开以及函数内联。
  -Os:专门优化目标文件大小,执行所有的不增加目标文件大小的-O2优化选项,并且执行专门减小目标文件大小的优化选项。
  -O3:打开所有-O2的优化选项并且增加 -finline-functions, -funswitch-loops,-fpredictive-commoning,-fgcse-after-reload and -ftree-vectorize优化选项。
  链接:
  gcc优化选项:https://blog.csdn.net/zhang626zhen/article/details/52988242
  gCC优化选项的各种含义以及潜藏风险:https://blog.csdn.net/findaway123/article/details/41752857

四 gcc / g++ Debug 模式

1 Debug/release模式

  Debug模式下的代码比release模式具有更多的检测,比如在windows平台上assert宏只在release模式下有效。Linux上则没有这个限制,无论Debug还是release模式assert都生效,但是如果你想让assert失效,编译的时候 加上 -DNDEBUG 即可。

2 Debug检测

  我们都说了Debug模式可以帮我们检测一些东西,但是可能会有额外的性能开销(虽然微不足道)。默认下Linux的动态库文件是集于Debug 和 release于一身的。看看这个列子:

#include<vector>
using namespace std;
int main()
{
    vector<int> v;
    v.reserve(10); //容量为10  但是size 为0
    v[3] = 1;
    return 0;
}

  下面这份代码在windows 下的Debug模式下会崩溃,release模式下不会崩溃,这是因为windows下的vector对operator[] 做了assert的检测。那么我们跑这份代码在Linux下,它并不会报错,所以那我们如何开启真真的Debug 模式呢?

3 _GLIBCXX_DEBUG

  如果我们想开启Debug模式必须定义上面的宏。

4 debug模式下的容器

  gcc是有专门的debug容器的,这些容器使用起来和标准容器一模一样,所以放心使用就好,但是有个准则就是俩个不同的.o文件内不能一个使用了debug模式的容器,一个没使用debug模式下的容器,这俩个.o文件是不能链接到一起的。

5 非c++11的

在这里插入图片描述

6 c++11的Debug容器

在这里插入图片描述

7 demo

#include<debug/vector>
#define _GLIBCXX_DEBUG  

int main()
{
      __gnu_debug::vector<int> v;
      v.reserve(10);
      v[3]=1;
      return 0;
}
$ ./a.out 
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/debug/vector:265:
    error: attempt to subscript container with out-of-bounds index 3, but     
    container only holds 0 elements.
Objects involved in the operation:
sequence "this" @ 0x0x7ffeae481db0 {
  type = NSt7__debug6vectorIiSaIiEEE;
}
Aborted (core dumped)

五 静态库的创建

  动态库:windows:.dll后缀,Linux:.so后缀;静态库:windows:.lib后缀,Linux:.a后缀。

1 静态库的创建

  有源文件test.ctest.h
  1、编译test.c生成目标文件:gcc -o test.o -c test.c
  2、将目标文件加入到静态库中:ar rcv libtest.a test.o
  3、连接:gcc -o test test.c -lmytest(如果库不在默认路径,需要用-L表明路径)。

2 ar参数

  参数r:在库中插入模块(替换)。当插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在,ar显示一个错误消息,并不替换其他同名模块。默认的情况下,新的成员增加在库的结尾处,可以使用其他任选项来改变增加的位置。
  参数c:创建一个库。不管库是否存在,都将创建。
  参数s:创建目标文件索引,这在创建较大的库时能加快时间。(补充:如果不需要创建索引,可改成大写S参数;如果.a文件缺少索引,可以使用ranlib命令添加)。
  参数q:在文件包尾加入文件。
  参数v:将文件插入库文件中。

六 动态库的创建

  动态库:windows:.dll后缀,Linux:.so后缀;静态库:windows:.lib后缀,Linux:.a后缀。

1 动态库的编译

  下面通过一个例子来介绍如何生成一个动态库。这里有一个头文件:so_test.h,三个.c文件:test_a.c、test_b.c、test_c.c,我们将这几个文件编译成一个动态库:libtest.so。

//so_test.h: 

#ifndef __SO_TEST_H__
#define __SO_TEST_H__

void test_a(); 
void test_b(); 
void test_c(); 

#endif
//test_a.c: 
#include "so_test.h" 

void test_a() 
{ 
    printf("this is in test_a...\n"); 
} 
//test_b.c: 
#include "so_test.h" 

void test_b() 
{ 
    printf("this is in test_b...\n"); 
} 
//test_c.c:
#include "so_test.h" 

void test_c() 
{ 
    printf("this is in test_c...\n"); 
} 

  将这几个文件编译成一个动态库:libtest.so

$ gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so 

2 动态库的链接

//test.c: 
#include "so_test.h" 

int main() 
{ 
    test_a(); 
    test_b(); 
    test_c(); 
    return 0; 
} 

  将test.c与动态库libtest.so链接生成执行文件test:

$ gcc test.c -L. -ltest -o test 

  测试是否动态连接,如果列出libtest.so,那么应该是连接正常了:

$ ldd test

3 编译参数解析

  最主要的是GCC命令行的一个选项:
  -shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件。
  -fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
  -L.:表示要连接的库在当前目录中
  -ltest:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称
  LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
  当然如果有root权限的话,可以修改/etc/ld.so.conf文件,然后调用 /sbin/ldconfig来达到同样的目的,不过如果没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了。

4 注意

  调用动态库的时候有几个问题会经常碰到,有时,明明已经将库的头文件所在目录 通过 “-I” include进来了,库所在文件通过 “-L”参数引导,并指定了“-l”的库名,但通过ldd命令察看时,就是死活找不到你指定链接的so文件,这时你要作的就是通过修改LD_LIBRARY_PATH或者/etc/ld.so.conf文件来指定动态库的目录。通常这样做就可以解决库无法链接的问题了。
链接:
  用gcc编译生成动态链接库*.so文件的方法:http://www.linuxdiyf.com/viewarticle.php?id=3354

六 gdb

1 参数

  -tui启动UI选项;-q(quit)代表不打印那一大串版本版权信息之类的刷屏字幕;切换调试文件:(gdb)file test2或者(gdb)exec(-file) test2

2 list

  list(l) 查看最近10行源码;
  list fun 查看fun函数源码;
  list file:fun 查看file文件中的fun函数源码;
  list num1 num2 查看num1~num2行的源码;

3 break

  break 行号
  break fun 在函数处设置断点;
  break file:行号
  break file:fun
  break if <condition> 条件成立时程序停住;
  info break (i b) 查看断点;
  watch expr 当expr的值发生改变时,程序停住;
  delete n 删除断点;

4 step & stepi

  (1)step [count] 如果没有指定count, 则继续执行程序,直到到达与当前源文件不同的源文件中时停止;如果指定了count, 则重复行上面的过程count次。
  (2)stepi [count] 如果没有指定count, 继续执行下一条机器指令,然后停止;如果指定了count,则重复上面的过程count次。
  (3)step比较常见的应用场景:在函数func被调用的某行代码处设置断点,等程序在断点处停下来后,可以用step命令进入该函数的实现中,但前提是该函数编译的时候把调试信息也编译进去了,负责step会跳过该函数。

5 next & nexti

  (1)next [count] 如果没有指定count, 单步执行下一行程序;如果指定了count,单步执行接下来的count行程序。
  (2)nexti [count] 如果没有指定count, 单步执行下一条指令;如果指定了count, 音频执行接下来的count条执行。
  (3)stepi和nexti的区别:nexti在执行某机器指令时,如果该指令是函数调用,那么程序执行直到该函数调用结束时才停止。

6 disassemble

  用 gdb 查看汇编代码,可以采用 disassemble 和x命令。/m源码和汇编一起排列,/r还可以看到16进制代码。
  调试示例:

(gdb) disassemble /m main
Dump of assembler code for function main:
4    {
   0x00000000004004c4 <+0>:    push   %rbp
   0x00000000004004c5 <+1>:    mov    %rsp,%rbp
   0x00000000004004c8 <+4>:    sub    $0x20,%rsp
   0x00000000004004cc <+8>:    mov    %edi,-0x14(%rbp)
   0x00000000004004cf <+11>:    mov    %rsi,-0x20(%rbp)
5        int size=sizeof("hjj");
=> 0x00000000004004d3 <+15>:    movl   $0x4,-0x4(%rbp)
6        printf("size is %d\n",size);
   0x00000000004004da <+22>:    mov    $0x4005f8,%eax
   0x00000000004004df <+27>:    mov    -0x4(%rbp),%edx
   0x00000000004004e2 <+30>:    mov    %edx,%esi
   0x00000000004004e4 <+32>:    mov    %rax,%rdi
   0x00000000004004e7 <+35>:    mov    $0x0,%eax
   0x00000000004004ec <+40>:    callq  0x4003b8 <printf@plt>
7        return 0;
   0x00000000004004f1 <+45>:    mov    $0x0,%eax
8    }
   0x00000000004004f6 <+50>:    leaveq 
   0x00000000004004f7 <+51>:    retq   
End of assembler dump.

  用 x/i 可以查看指令:

(gdb) x/15i main
   0x4004c4 <main>:    push   %rbp
   0x4004c5 <main+1>:    mov    %rsp,%rbp
   0x4004c8 <main+4>:    sub    $0x20,%rsp
   0x4004cc <main+8>:    mov    %edi,-0x14(%rbp)
   0x4004cf <main+11>:    mov    %rsi,-0x20(%rbp)
=> 0x4004d3 <main+15>:    movl   $0x4,-0x4(%rbp)
   0x4004da <main+22>:    mov    $0x4005f8,%eax
   0x4004df <main+27>:    mov    -0x4(%rbp),%edx
   0x4004e2 <main+30>:    mov    %edx,%esi
   0x4004e4 <main+32>:    mov    %rax,%rdi
   0x4004e7 <main+35>:    mov    $0x0,%eax
   0x4004ec <main+40>:    callq  0x4003b8 <printf@plt>
   0x4004f1 <main+45>:    mov    $0x0,%eax
   0x4004f6 <main+50>:    leaveq 
   0x4004f7 <main+51>:    retq   

  $pc 指向当前程序运行地址:

(gdb) x/5i $pc
=> 0x4004d3 <main+15>:    movl   $0x4,-0x4(%rbp)
   0x4004da <main+22>:    mov    $0x4005f8,%eax
   0x4004df <main+27>:    mov    -0x4(%rbp),%edx
   0x4004e2 <main+30>:    mov    %edx,%esi
   0x4004e4 <main+32>:    mov    %rax,%rdi
(gdb) 

  p $edi 等可以查看寄存器。

7 layout

  layout 用于分割窗口,可以一边查看代码,一边测试。主要有以下几种用法:
  layout src 显示源代码窗口
  layout asm 显示汇编窗口
  layout regs 显示源代码/汇编和寄存器窗口
  layout split 显示源代码和汇编窗口
  layout next 显示下一个layout
  layout prev 显示上一个layout
  Ctrl + L:刷新窗口
  Ctrl + x,再按1:单窗口模式,显示一个窗口
  Ctrl + x,再按2:双窗口模式,显示两个窗口
  Ctrl + x,再按a:回到传统模式,即退出layout,回到执行layout之前的调试窗口。

8 函数调用命令

  Backtrace(bt) 查看栈帧信息;
  frame i调出第i层栈信息,可以结合bt使用;
  调出后可以通过print <var>来打印想要的变量;
  select-frame args 移动到指定的帧中去,不打印信息。

9 其他

  run/r 启动程序;
  continue(c) 运行至下一个断点;
  finish 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数信息;
  until 运行程序直到程序退出循环体;
  print i (p i) 查看变量的值;
  ptype 查看变量类型;
  print array 查看数组;
  print *array@len 查看动态内存;
  print x=5 改变运行时的数据;
  print &array 查看数组的地址;
  回车代表上一个命令;

七 core文件

1 概念

  当程序运行的过程中异常终止或崩溃(1、内存访问越界;2、非法指针;3、堆栈溢出),操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump(中文有的翻译成“核心转储”)。我们可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core文件记录的是程序崩溃时的内存映像,并加入调试信息。

2 开启core dump

  1. 在终端输入:ulimit -c,输出结果为0,则系统默认关闭core dump,即使程序异常终止,也不会生成core dump文件。
  2. ulimit -c unlimited ,开启core dump功能,且不限制core文件大小。
  3. ulimit -c filesize,开启core dump功能,且限制core文件大小,filesize的单位为kbyte。如果生成的信息超过此大小,将会被裁剪,最终生成一个不完整的core文件。在调试此 core文件的时候,gdb会提示错误。
  4. 上面命令开启,只会在此终端生效,如果想让修改永久生效,则需要修改配置文件,如 .bash_profile、/etc/profile或/etc/security/limits.conf。增加一行:
* soft core unlimited

3 修改core文件生成路径和名称

  1. 默认生成的core文件,在可执行文件的目录下,默认文件名为core。新的core文件生成将覆盖原来的core文件。
  2. /proc/sys/kernel/core_uses_pid可以控制core文件的文件名中是否添加pid作为扩展。文件内容为1,表示添加pid作为扩展名,生成的core文件格式为core.pid;为0则表示生成的core文件同一命名为core。可通过以下命令修改此文件:
$: echo "1" > /proc/sys/kernel/core_uses_pid
  1. /proc/sys/kernel/core_pattern可以控制core文件保存位置和文件名格式。可通过以下命令修改此文件:
$: echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

  可以将core文件统一生成到/corefile目录下,产生的文件名为core-命令名-pid-时间戳。
  以下是参数列表:

%p - insert pid into filename 添加pid
%u - insert current uid into filename 添加当前uid
%g - insert current gid into filename 添加当前gid
%s - insert signal that caused the coredump into the filename 添加导致产生core的信号
%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix

  时间

%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名

4 程序中开启core dump

  通过如下API可以查看和设置RLIMIT_CORE:

#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#define CORE_SIZE   1024 * 1024 * 500
int main()
{
    struct rlimit rlmt;
    if (getrlimit(RLIMIT_CORE, &rlmt) == -1) {
        return -1; 
    }   
    printf("Before set rlimit CORE dump current is:%d, max is:%d\n", (int)rlmt.rlim_cur, (int)rlmt.rlim_max);

    rlmt.rlim_cur = (rlim_t)CORE_SIZE;
    rlmt.rlim_max  = (rlim_t)CORE_SIZE;

    if (setrlimit(RLIMIT_CORE, &rlmt) == -1) {
        return -1; 
    }   

    if (getrlimit(RLIMIT_CORE, &rlmt) == -1) {
        return -1; 
    }   
    printf("After set rlimit CORE dump current is:%d, max is:%d\n", (int)rlmt.rlim_cur, (int)rlmt.rlim_max);

    /*测试非法内存,产生core文件*/
    int *ptr = NULL;
    *ptr = 10; 

    return 0;
}

5 gdb调试core文件

  gdb test test.core,进入test可执行文件的core文件。
  Backtrace(bt) 查看栈帧信息;

6 where命令

  where命令,可以显示导致段错误的执行函数处。

7 问题

  要保证存放coredump的目录存在且进程对该目录有写权限。存放coredump的目录即进程的当前目录,一般就是当初发出命令启动该进程时所在的目录。但如果是通过脚本启动,则脚本可能会修改当前目录,这时进程真正的当前目录就会与当初执行脚本所在目录不同。这时可以查看”/proc/<进程pid>/cwd“符号链接的目标来确定进程真正的当前目录地址。通过系统服务启动的进程也可通过这一方法查看。
  若程序调用了seteuid()/setegid()改变了进程的有效用户或组,则在默认情况下系统不会为这些进程生成Coredump。很多服务程序都会调用seteuid(),如MySQL,不论你用什么用户运行mysqld_safe启动MySQL,mysqld进行的有效用户始终是msyql用户。如果你当初是以用户A运行了某个程序,但在ps里看到的 这个程序的用户却是B的话,那么这些进程就是调用了seteuid了。为了能够让这些进程生成core dump,需要将/proc/sys/fs /suid_dumpable文件的内容改为1(一般默认是0)。
  设置足够大的Core文件大小限制 了。程序崩溃时生成的Core文件大小即为程序运行时占用的内存大小。但程序崩溃时的行为不可按平常时的行为来估计,比如缓冲区溢出等错误可能导致堆栈被 破坏,因此经常会出现某个变量的值被修改成乱七八糟的,然后程序用这个大小去申请内存就可能导致程序比平常时多占用很多内存。因此无论程序正常运行时占用 的内存多么少,要保证生成Core文件还是将大小限制设为unlimited为好。

八 多线程调试

  info threads 显示当前可调试的所有线程;
  thread ID 切换当前调试的线程为指定ID的线程;
  attach process-id 在gdb状态下,开始调试一个正在运行的进程;
  thread apply all command 所有线程执行command;
  thread apply all bt 查看所有线程堆栈信息。

九 多进程调试

1 单独调试子进程

2 启动选项follow-fork-mode

  follow-fork-mode允许选择程序在执行fork系统调用后是继续调试父进程还是调试子进程。

十 跨平台编译器

1 MinGw

安装:
https://zhuanlan.zhihu.com/p/76613134
https://www.mingw-w64.org/downloads/#mingw-builds

2 Cygwin

安装:
https://blog.csdn.net/xiaojin21cen/article/details/125146944

3 msys2

4 MinGW-w64 + CMake

  因为 windows 版的 CMake 支持很多的编译器,而且其默认的一般是最新的 vs 工具,所以,需要指定 Makefile 的格式:

cmake –G”MinGW Makefiles” .. 

  make:

mingw32-make

5 差异

https://www.zhihu.com/question/22137175
https://blog.csdn.net/u010168781/article/details/121764725

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值