C语言 gcc编译和链接(linux)

目录

一、第一个hello world程序

1.1 创建.c文件

1.2 编译链接+运行可执行程序

二、编译链接过程

2.1 预编译阶段

2.2 编译阶段

2.3 汇编阶段

2.4 链接阶段

三、gcc 分步编译链接

3.0 常用命令 -E -S -c -o

1、 -E -S -c -o详情

2、 目录选项

① -I :

② -idirafter 

③ -L :

④ -l :

3.1 一步完成编译链接到可执行程序

3.2 分两步完成

3.3 多文件编译链接

四、 make工具和makefile文件

4.1 什么是make和makefile

4.2 利用make工具完成上一小节的自动化编译过程

4.2.1 编写makefile文件

4.2.2 利用make工具自动生成可执行程序

4.3 编译&外部变量与方法声明

方式1 test.c + main.c

方式2 test.h&.c + main.c

五、 静态库和动态库

5.1 前言

5.2 静态和动态库区别

5.3 静态和动态库制作样例

5.4 静态库制作

① 静态库生成命令

② ar命令详解

③ make 生成静态库

5.5 静态库的使用(三种方式)

方式① gcc编译

方式② 拷贝到系统默认的路径

方式③ 建立软连接

5.6 动态库制作

① 方式一:分两步

② 方式二:一步完成,不产生中间目标.o文件

③ 方式三:make 生成动态库

5.7 动态库的使用(同静态库)

① gcc编译

② 拷贝到系统默认的路径

③ 建立软连接

5.8 解决加载不到动/静态库的方法

5.9 头文件的引入


一、第一个hello world程序

1.1 创建.c文件

hello.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() 
{
    printf("welcome to guangxi\n");    
    exit(0);
}

1.2 编译链接+运行可执行程序

gcc -o hello hello.c         //hello => 可执行程序名 hello.c => 编译的c文件

上述过程可以分解成4个步骤,分别是预处理、编译、汇编和链接

1、C语言是一门编译型语言,编译型语言首先将源代码编译生成机器语言,再由机器运行机器码(二进制)。对于编译型语言,绕不过的就是编译器。GCC(GNU编译器套件):GNU Compiler Collection。可以编译C、C++、JAVA、Fortran、Pascal、Object-C、Ada等语言。

2、gcc是GCC中的GNU C Compiler(C 编译器),g++是GCC中的GNU C++ Compiler(C++编译器)

注意:对于Linux平台下,生成的可执行程序没有后缀.exe ,关于编译链接的过程,我们下面作详细分析

二、编译链接过程

2.1 预编译阶段

a) 删除所有的“#define”,并且展开所有的宏定义;

b) 处理所有的条件预编译指令,“#if”、“#ifdef”、“#endif”等;

c) 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置;

d) 删除所有的注释;

e) 添加行号和文件名标识,以便于编译器产生调试用的符号信息及编译时产生编译错误和警告时显示行号;

f) 保留所有的#pragma 编译器指令,因为编译器需要使用它们。

2.2 编译阶段

词法分析、语法分析、语义分析,代码优化,汇总符号。

2.3 汇编阶段

将汇编指令翻译成二进制格式,生成各个section,生成符号表。

2.4 链接阶段

a) 合并各个section,调整section的起始位移和段大小,合并符号表,进行符号解析,给符号分配虚拟地址 b) 符号重定位

链接是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件?链接过程到底包含了声明内容?为什么要链接?我们用第三章来分析静态链接。下面我们看看怎么样调用ld才可以产生一个能够正常运行的HelloWorld程序:

$ld -static /usr/lib/crtl.o /usr/lib/crti.o

/usr/lib/gcc/i486-linux-gnu/4.1.3/crtbeginT.o

-L/usr/lib/gcc/i486-linux-gnu/4.1.3 -L/usr/lib -L/lib hello.o --start-group

-lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/i486-linux-gnu/4.1.3/crtend.o

/usr/lib/crtn.o

如果把所有路径省略,那么上面的命令就是:

ld -static ctrl.o ctri.o  crtbeginT.o hello.o -start -group -lgcc -lgcc_en -lc --end-group crtend.o crtn.

我们需要将一大堆文件链接起来才可以得到“a.out”,即最终的可执行文件。

三、gcc 分步编译链接

GCC 常用参数

gcc 编译选项解释说明
-E预处理,主要是进行宏展开等步骤,生成 test.i
-S编译指定的源文件,但是不进行汇编,生成 test.s
-c编译、汇编源文件,但是不进行链接,生成 test.o
-o指定链接的文件名及路径
-g在编译的时候,生成调试信息,该程序可以被调试器调试
-D在程序编译的时候,指定一个宏
-std指定 C 方言,如 -std=c99。gcc 默认的方言是 GNU C
-l在程序编译的时候,指定使用的库(库的名字一定要掐头去尾,如 libtest.so 变为 test)
-L在程序编译的时候,指定使用的库的路径
-fpic生成与位置无关的代码
-shared生成共享目标文件,通常用在建立动态库时

3.0 常用命令 -E -S -c -o

1、 -E -S -c -o详情

-E

gcc -E hello.c -o hello.i        //正常预编译生成hello.i文件

gcc -E hello.c > pianoapan.txt        //只激活预处理,不生成文件,你需要把它重定向到一个输出文件里面

gcc -E hello.c | more        //查看更多,不生成文件一句 'hello word' 也要预处理成800行的代码,慢慢看吧

-S

gcc -S hello.c        //只激活预处理和编译,就是指把文件编译成为汇编代将生成hello.s的汇编代码

-c

gcc -c hello.c        //只激活预处理,编译,和汇编,将生成.o的目标文件(object file)

-o

gcc -o hello.out hello.c         //指定目标名称,缺省的时候,gcc/g++编译出来的文件是a.out

gcc hello.c                            //默认生成可执行程序,名为a.out

gcc -o hello.asm -S hello.c         //将.c文件编译为.asm的汇编文件

2、 目录选项

-I <headerDir>:

=> 添加头文件 .h 搜索路径 (注意是大写的 i 即 include 首字母大写),如果只有.c文件,效果相同

gcc -o main main.c -I headerDir

相当于“#include”,在你是用#include "file"的时候,gcc/g++会先在当前目录查找你所指定的头文件,如果没有找到,会到系统默认的头文件目录找。如果使用 -I 指定了目录,编译器会先在指定的目录查找,然后再去系统默认头文件目录查找。对于#include ,gcc/g++会到 -I 指定的目录查找,查找不到,然后再到系统默认的头文件目录查找。

例子:两个文件 fun.c 和main.c

fun.c

int fun()
{
    return 9;
}

main.c

#include <stdio.h>
#include "fun.c"

int main(void)
{
    int i = fun();
    printf("i = %d\n", i);
    return 0;
}

情况1:目录结构如下

正常编译,产生可执行文

gcc -o main main.c

情况2:目录结构

执行之前的命令则报错

修改之前的编译命令,加入 -I  <dir>(大写的i)

gcc -o main main.c -I xxx       //编译正常

也可以写成(-I和xxx直接连着写)

gcc -o main main.c -Ixxx 

-idirafter <dir>:

=> 如果在 -I 的目录里面查找失败,将到目录dir里面查找。

-L <library dir>

=> 添加库文件 .a .so 搜索路径(windows下静态库是.lib 动态库是.dll)

编译的时候,指定搜索库的路径。比如你自己的库,可以用它指定目录,不然编译器将只在标准库的目录找。这个dir就是库文件目录的名称。

-l <library>:

=> 链接指定的库文件(注意是L的小写,即 library 首字母)。

gcc -lcurses hello.c         // 使用curses库编译连接,生成程序

3.1 一步完成编译链接到可执行程序

gcc -o main main.c

3.2 分两步完成

gcc  -c  main.c                 //生成main.o文件

gcc  -o  main main.o        //生成main可执行程序

3.3 多文件编译链接

创建3个源文件如下:add.c max.c main.c

add.c

int add(int x, int y)
{
    return x + y;
}

max.c

int max(int x, int y)
{
    return x > y ? x : y;
}

main.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern int add(int x, int y);    //一般外部方法和变量使用extern声明
extern int max(int x, int y);

int main() 
{
    int a=4,b=7;
    printf("a = %d\n",a);
    printf("b = %d\n",b);
    printf("a + b = %d\n",add(a,b));
    printf("a和b的较大者是:%d\n",,max(a,b));
    exit(0);
}

方式1:一步直接完成编译链接生成可执行程序

gcc -o main main.c add.c max.c

方式2:分两步;

首先,先把每个文件生成对应的.o文件, gcc -c xxx.c ...

其次,将所有的.o文件链接生成可执行程序文件main,

gcc -o main main.c add.c max.c

四、 make工具和makefile文件

4.1 什么是make和makefile

        当源码文件比较多的时候就不适合通过直接输入gcc命令来编译,这时候就需要一个自动化的编译工具, 这就是make工具,make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。

1、make:一般说GNU Make,是一个软件,用于将源代码文件编译为可执行的二进制文件,make工具主要用于完成自动化编译。make工具编译的时候需要Makefile文件提供编译文件。

2、Makefile:make工具所使用的文件,Makefile指明了编译规则。makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

4.2 利用make工具完成上一小节的自动化编译过程

注意:执行make 命令,需要一个名为“makefile”或“Makefile”的文本文件

4.2.1 编写makefile文件

makefile:

all: main  #指定生成最终的可执行程序

main : add.o max.o main.o  #可执行程序test的生成依赖这几个.o文件,生成规则如下
    gcc -o main add.o max.o main.o

add.o : add.c  #.o文件按照此规则生成
    gcc -c add.c

max.o : max.c
    gcc -c max.c

main.o : main.c
    gcc -c main.c

clean:  #删除以.o结尾,以及test文件,并且如果没有这些文件,不报错
    rm -rf *.o

工程是需要被清理的,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行, 不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。 

4.2.2 利用make工具自动生成可执行程序

make

make clean

利用make工具完成自动化编译的大致步骤如下:

1. 提供好所有的源文件.c和makefile文件

2. 执行命令make, 自动生成编译链接四个阶段的所有文件 

3. 清理中间的目标文件,执行命令:make  clean

4.3 编译&外部变量与方法声明

方式1 test.c + main.c

test.c

#include <stdio.h>

int g_val = 2016;

void print(const char *str)
{
    printf("%s\n", str);
}

main.c         //注意此处没有 #include 引入 test.c 文件,而是在 main.c 中使用 extern

#include <stdio.h>

extern void print(char *str);
extern int g_val;

int main()
{
    printf("%d\n", g_val);
    print("hello bit.\n");
    return 0;
}

编译:

gcc -o test test.c sum.c         //生成可执行程序 test

方式2 test.h&.c + main.c

test.h

#ifndef _TEST_H
#define _TEST_H

extern int g_val;

void print(char *str);

#endif // _TEST_H

test.c

#include <stdio.h>

int g_val = 2016;

void print(const char *str)
{
    printf("%s\n", str);
}

main.c       //此处引入了test.h 头文件

#include <stdio.h>
#include "test.h"

int main()
{
    printf("%d\n", g_val);
    print("hello bit.");
    return 0;
}

编译:

gcc -o main main.c test.c         //生成可执行程序 main

五、 静态库和动态库

5.1 前言

库文件的命名: 必须使用lib作为前缀: 比如 libDeployPkg.so.0 / libhgfs.so.0 .....;静态库一般以 .a 为后缀 ;动态库一般以.so为后缀 库文件会有不同的版本, 一般写在后缀后面, 比如 lib.a.so.0.1.2

linux下库的命名格式一般为:

静态库: lib+库的名字+.a         eg:c标准库为 libc.a

动态库: lib+库的名字+.so

5.2 静态和动态库区别

        静态库程序编译时会被完整地拷贝到最终的可执行文件中,这意味着使用静态库的程序在部署时不需要依赖额外的哭文件,因为所有需要的代码都已经包含在内。这也导致了可执行文件体积较大,静态库通常以“.a”或“.lib”为扩展名

        动态库程序编译时不包含在可执行文件中,而是在程序运行时由操作系统加载。这意味着可执行文件体积较小,且当动态库更新时,只需替换库文件即可,无需重新编译程序。动态库的使用提高了代码的复用性,降低了程序之间的耦合度。动态库则以“.so”或“.dll”为扩展名

 注意:不管需要制作的是静态库还是动态库,原材料都是.o文件 (可重定位文件) 库文件内部不允许出现主函数main

编写 add.c sub.c max.c 和 main.c 函数

5.3 静态和动态库制作样例

add.c

int add(int x, int y)
{
    return x + y;
}

sub.c

int sub(int x, int y)
{
    return x - y;
}

max.c

int max(int x, int y)
{
    return x > y ? x : y;
}

main.c

#include <stdio.h>
#include "add.c"
#include "sub.c"
#include "max.c"

int main()
{
    int a = 3, b = 2;
    printf("a = %d,b = %d\n", a ,b);
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", sub(a, b));
    printf("a 和 b 中的最大值是 %d\n", max(a, b));
    return 0;
}

5.4 静态库制作

第一步、首先生成 add.o & sub.o

gcc -c add.c sub.c max.c

第二步、生成 libxxx.a文件

① 静态库生成命令

ar -rcs lib+库名.a  +所用目标文件(.o)

ar -rcs libmymath.a add.o sub.o max.o

r 将文件快速追加到归档文件中(替换归档文件中已有的文件或加入新文件)

c 不在必须创建库的时候给出警告

s 作为ranlib工作,ranlib则负责为这些静态库文件生成索引,显著提高了静态库的链接效率

' - '可写可不写,但为了与gcc编译器一致一般写上

1、其实创建使用最基本最基本 ar -r 即可

ar -r libmymath.a add.o sub.o max.o

当libmymath.a不存在时会打印ar:正在创建 xxx.a

当libmymath.a已经存在时无任何打印

2、使用 ar -rc

ar -rc libmymath.a add.o sub.o max.o

当libmymath.a不存在时,创建则不显示创建提示

当然存在时,更不会显示创建提示

3、通常使用 ar -rcs 创建,便于用 ranlin => 创建一个包含符号索引的归档文件

ar -rcs libmymath.a add.o sub.o max.o

② ar命令详解

可用ar -v 显示命令详解

其余常用命令

1、t 列出静态库中的所有成员文件

ar -t libmymath.a

2、x 提取静态库中的指定成员到当前目录

ar -x libmymath.a add.o sub.o max.o

3、 d 从静态库中删除指定的成员

ar -d libmymath.a add.o

③ make 生成静态库

Makefile有三个非常有用的变量。分别是$@,$^,$<

$@ -- 目标文件,$^ -- 所有的依赖文件,$< -- 第一个依赖文件。

makefile:

staticlib=libmymath.a   #定义名为staticlib的变量,变量值为libmymath.a

$(staticlib): add.o sub.o max.o   #如何使用.o构建staticlib
    ar -rcs $@ $^

add.o: add.c    #.o文件按照此规则生成
    gcc -fPIC -c $^

sub.o: sub.c
    gcc -fPIC -c $^
    
max.o: max.c
    gcc -fPIC -c $^

.PHONY: output
output:
    mkdir -p lib; \
    cp *.a ./lib

.PHONY: clean
clean:      #清除所有的.o  .so  和staticlib文件
    rm -rf *.o *.a staticlib

注意:如果output处写成

mkdir -p lib

cp *.a ./lib

易出现问题makefile:x: *** 缺失分隔符。 停止。>>我自己基本都出现这个情况,因此多行都是加 ;\ 

解决方法:确保.PHONY 后面的每个依赖都单独占一行。如果你需要在.PHONY 下执行多个命令,可以使用分号(;)或者回车(\)来合并多行命令。

make

make output

make clean

这样最终生成的静态库放在lib文件夹内

5.5 静态库的使用(三种方式)

方式① gcc编译

gcc -o 程序名 main.c -I ./头文件的路径 -L ./库文件的路径 -l 链接库的名称

注意: 上面的代码除了-o之外的内容缺一不可:

gcc -o bin/main src/*.c -I ./include -L ./lib -l mylib

gcc 编译器

-o bin/main定生成二进制文件为 bin/main

src/*.c 源文件

-I ./include 指定头文件路径,路径为./include,即当前路径下include文件夹

-L ./lib 指定库文件路径为 ./lib

-l mylib 链接库名为 mylib

-L . 库路径为当前路径,也可写成-L. 或者 -L ./ (之前使用makefile生成的话写成-L . /lib)

-l mymath 使用的库名为mymath => 即找libmymath.a,也可写成-lmymath

注意:就算没头文件也要加上 ' -L . ',否则编译器会去找系统下的默认路径,找不到会报错

  • 头文件的系统默认查找路径:/usr/include
  • 库文件的系统默认查找路径: /lib 或 /lib64等

引用5.3中编译生成的libmymath.a

1、当 main.c 和 libmymath.a 在同级路径下时

gcc -o main main.c -I . -L . -l mymath -static 

-static 指名使用库的是静态库(可写可不写)

2、当main.c 和 libmymath.a 不在同级路径下,.a 在 ./lib 路径下时

gcc -o main main.c -I . -L ./lib -l mymath -static

方式② 拷贝到系统默认的路径

拷贝文件到系统路径同样可以实现静态库的使用:

sudo cp ./include/mymath.h /usr/include/

sudo cp ./lib/libmymath.a /lib64/libmymath.a

没有头文件的情况直接

sudo cp libmymath.a /lib64/libmymath.a

gcc 无法直接编译我们的main.c文件,还是需要我们告诉编译器其中调用的静态库的名字才可以 => -l mymath

然后执行

gcc -o main main.c -l mymath

但是一般不推荐,这样会对我们系统的路径造成污染,删除:

sudo rm /lib64/libmymath.a

方式③ 建立软链接

软链接应用广泛,可以快速找到.h和.c文件。

使用时,main函数的头文件要修改为文件的路径。(根据自己的路径)

sudo ln -s /home/laber/workspace/c/5.4_a/include /usr/include/mymath.h

sudo ln -s /home/laber/workspace/c/5.4_a/lib/libmymath.a /lib64/libmymath.a

没有头文件的情况直接

sudo ln -s /home/laber/workspace/c/5.4_a/lib/libmymath.a /lib64/libmymath.a

注意:软链接要使用绝对路径

然后执行,效果一样

gcc -o main main.c -l mymath

解除链接:

sudo unlink /lib64/libmymath.a

有头文件的 =>

sudo unlink /usr/include/mymath.h

5.6 动态库制作

方式①:分两步

第一步、与静态库不同的是生成 add.o & sub.o时命令要加 -fPIC

gcc -fPIC -c add.c sub.c max.c

-fPIC:由于动态库在内存中是可加载的,它可能在内存中的任意位置,也可能被映射到进程地址空间的每个区域,所以为了保证库当中的代码执行不会出错,也就是要保证库中的代码是与位置无关的,因此生成.o文件时需要带上-fPIC选项表示生成与位置无关码。

第二步、生成 libxxx.so文件

gcc -shared -o lib+库名.so +所用目标文件(.o)

gcc -shared -o libmymath.so add.o sub.o max.o

方式②:一步完成,不产生中间目标.o文件

gcc -shared -fPIC -o lib+库名.so +所用源文件(.c)

gcc -shared -fPIC -o libmymath.so add.c sub.c

方式③:make 生成动态库

makefile:

sharedlib=libmymath.so   #定义名为sharedlib的变量,变量值为libmymath.so

$(sharedlib): add.o sub.o max.o  #如何使用.o构建sharedlib
    gcc -shared -fPIC -o $@ $^

add.o: add.c    #.o文件按照此规则生成
    gcc -fPIC -c $^

sub.o: sub.c
    gcc -fPIC -c $^

max.o: max.c
    gcc -fPIC -c $^

.PHONY: output
output:
    mkdir -p lib; \
    cp *.so ./lib

.PHONY: clean
clean:      #清除所有的.o  .so  和sharedlib文件
    rm -rf *.o *.so sharedlib

make

make output

make clean

最终生成的.so在./lib路径下

5.7 动态库的使用(同静态库)

① gcc编译

gcc -o 程序名 main.c -I ./头文件的路径 -L ./库文件的路径 -l 链接库的名称

gcc -o main main.c -I . -L . -l mymath        // 因为是动态链接 所以不用带-static了      

如果使用以上make生成的则动态库文件,动态库在 ./lib 路径下,则为

gcc -o main main.c -I . -L ./lib -l mymath

注意:gcc 编译链接动态库,生成可执行程序 main 的命令 不要加 -shared

(曾经踩过的坑,静态库 gcc 编译链接使用时到是可以加 -static )

② 拷贝到系统默认的路径

③ 建立软链接

5.8 解决加载不到动/静态库的方法

  • 1.拷贝到系统默认的库路径 /lib64 /usr/lib64/
  • 2.在系统默认的库路径 /ib64 /usr/lib64/下建立软连接
  • 3.将自己的库所在的路径,添加到系统的环境变量LD LIBRARY PATH中
  • 4. /etc/ld.so.conf.d 建立自己的动态库路径的配置文件,然后重新ldconfiq即可

实际情况,我们用的库都是别人的成熟的库,都采用直接安装到系统的方式。

5.9 头文件.h的引入

问题:假设我们现在只有生成的 libmymath.so 动态库,而没有 add.c 和 sub.c,main.c怎么编译调用这个动态库呢?如图,main.c #include 报错

这时候就要使用头文件告知 main.c 有什么方法来调用了,例如创建一个 head.h 头文件

head.h

#ifndef _HEAD_H_
#define _HEAD_H_

int add(int x, int y);
int sub(int x, int y);
int max(int x, int y);

#endif

main.c 修改为

#include <stdio.h>
#include "head.h"    //引入head.h头文件

int main()
{
    int a = 3, b = 2;
    printf("a = %d,b = %d\n", a ,b);
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", sub(a, b));
    printf("a 和 b 中的最大值是 %d\n", max(a, b));
    return 0;
}

gcc -o main main.c -I . -L ./lib -l mymath

./main

运行出错

除非将 libmymath.so 移动到跟可执行程序同级目录下

mv ./lib/libmymath.so ./

./main

这样运行才成功(系统找不到的情况,程序会找当前同级路径),但显然这不是我们想要的

如何解决呢?可参照5.8中介绍的办法,既然编译都声称可执行程序了,此时的可执行程序是没问题的,因此已经与编译过程无关了;

推荐的解决方法:

这属于运行问题,其实运行时系统也会去默认路径下找到我们所使用的动态库,但在默认路径下没有我们的库。这里推荐修改环境变量LD_LIBRARY_PATH,将动态库所在路径.lib添加到该环境变量中,这样程序在运行时系统就能够找到动态库,从而运行成功。

将动态库移动回 ./lib 路径,使用export LD_LIBRARY_PATH修改环境变量

mv libmymath.so ./lib

export LD_LIBRARY_PATH=:/home/laber/workspace/c/5.6_so/lib

./main

运行成功:

不过这只是临时的环境变量,关闭终端后失效,

想要永久生效我们可以将 LD_LIBRARY_PATH 写入到 ~/.bashrc 或 ~/.zshrc中(根据自己终端使用)

我是使用zsh的

vi ~/.zshrc

 在zshrc最后一行写入

export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/laber/workspace/c/5.6_so/lib

:wq保存,然后source(用 bash 的执行 source ~/.bashrc)

source ~/.zshrc

关闭,在 main 所在路径打开新终端后运行,成功

可知,头文件的作用是编译时提供 main() 方法打包成可执行程序需要链接库是告知库中拥有那些方法的,通常情况下每个.c 文件应该拥有自己的头文件,将方法暴露出来,头文件写法同head(仅包含自身源文件.c)的方法声明,如下图

此处仅展示add.h,其余 sub.h max.h 类似

#ifndef _Add_H_
#define _Add_H_

int add(int x, int y);

#endif

通常头文件 .h 放在include文件夹下,源文件 .c 放在src文件夹下,可执行文件放在 bin 文件夹下,库文件放在 lib 文件夹下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值