C语言常量和变量详解

常量

常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量

常量的值在程序运行之前初始化的时候给定一次,以后都不会变了,以后一直是这个值。

C语言中,有三种实现常量的方式:
1、宏定义

 #define N 20    // 符号常量,注意宏定义末尾没有分号

2、const关键字

const int i = 14;

其实,const和类型符号可以互换位置。

int const i = 14;

二者是等效的。

#include <stdio.h>

int main()
{
	int const a = 5;
	a = 6;
    printf("Hello, World! %d\n", a);
   
    return 0;
}

运行结果如下:

const修饰的变量真的不能改吗?
其实,在gcc环境下,const修饰的变量其实是可以改的。
在某些单片机环境下,const修饰的变量是不可以改的。

const修饰的变量到底能不能真的被修改,取决于具体的环境,C语言本身并没有完全严格一致的要求。
在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。
更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。

由于常量一旦被创建后其值就不能再改变,所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误。

在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。

在C语言标准库中,有很多函数的形参都被 const 限制了,下面是部分函数的原型:

size_t strlen ( const char * str );
int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );
char * strcpy ( char * destination, const char * source );
int system (const char* command);
int puts ( const char * str );
int printf ( const char * format, ... );

用 const 加以限制,不但可以防止由于程序员误操作引起的字符串修改,还可以给用户一个提示,函数不会修改你提供的字符串,请你放心。


const是在编译器中实现的,编译时检查,并非不能骗过。所以在C语言中使用const,就好象是一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器、也告诉读程序的人,这个变量是不应该也不必被修改的。

3、枚举常量
枚举常量是宏定义的一种替代品,在某些情况下会比宏定义好用。
enum

后续会专门讲枚举,此处不赘述。

关于const修饰的变量

关键字const并不能把变量变成常量。在一个符号前加上const限定符只是表示这个符号不能被赋值,它的值对于这个符号是只读的,但他不能防止通过程序的内部(甚至是外部)的方法来修改这个值。

比如可以通过操作内存来修改。

虽然会报警告,但是不会报错

而直接赋值时是会报错的。

如果再间接点来修改a的值,连警告都不会有

为什么const修饰的变量能被修改呢?

这是因为const 只是编译器层面做的语法检查,在运行时只不过是栈里一个普通的变量。

以上说的是const修饰局部变量。

对于全局变量,就不一样了,const修饰的全局变量,是不允许被修改的,即使是通过指针。

为什么?

这是因为const修饰的全局变量,会在编译时,被放在内存中只读的区域。

可见,编译器对const修饰的局部变量和全局变量的处理是不一样的。

不过虽然const修饰的局部变量有办法被修改,但是不推荐使用。

所以有人说,const 在c中防君子不防小人。

变量

变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。

变量有声明、定义和初始化几个基础概念。

说明如下:

type variable_list;

//几个有效声明
int    i, j, k;
char   c, ch;
float  f, salary;
double d;

这种最普遍的情况,既是声明,也是定义。此时,就已经为变量创建好了内存空间。

声明/定义的同时可以初始化,一旦初始化,该内存空间就会赋予对应的值。

这种情况只能声明/定义一次。

变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义

还有一种:

extern type variable_list;

这种情况一定是声明,不会开辟内存空间。可以声明很多次。

如何区分全局变量的定义和声明?一般规律如下:如果定义的同时有初始化则一定会被认为是定义;如果只是定义而没有初始化则有可能被编译器认为是定义,也可能被认为是声明,要具体分析;如果使用extern则肯定会被认为是声明(实际上使用extern也可以有定义,实际上加extern就是明确声明这个变量为外部链接属性)。

总的来说,不必过于纠结,正常使用即可。

全局变量和局部变量

判断一个变量能不能使用,必须注意两点:

1、变量必须先定义后使用;

2、注意变量定义的作用域是否在当前位置有效。

从作用域上来看,变量分为全局变量和局部变量。


定义在函数外面的变量,就叫全局变量。

局部变量就是函数里用到的变量。

局部变量和全局变量对比:

1、定义时没有初始化,则局部变量的值是随机的,而全局变量的值默认为0。
2、使用范围上:全局变量具有文件作用域,而局部变量只有代码块作用域(所谓代码块,就是用{}括起来的一段代码)
3、生命周期上:全局变量是在程序开始运行之前的初始化阶段(main函数之前)就诞生,到整个程序结束退出的时候才死亡;而局部变量在进入局部变量所在的代码块时诞生,在该代码块退出的时候死亡。
4、变量分配位置:全局变量分配在数据段或者bss段上,而局部变量分配在栈上。

auto

修饰普通局部变量(auto)
普通的局部变量定义时直接定义,或者在定义前加auto关键字

void func1(void)
{
    int i = 1;
    i++;
    printf("i = %d.\n", i);
}

局部变量i的解析:
在连续三次调用func1中,每次调用时,在进入函数func1后都会创造一个新的变量i,并且给它赋初值1,然后i++时加到2,然后printf输出时输出2,然后func1本次调用结束,结束时同时杀死本次创造的这个i。这就是局部变量i的整个生命周期。
下次再调用该函数func1时,又会重新创造一个i,经历整个程序运算,最终在函数运行完退出时再次被杀死。

auto关键字在C语言中只有一个作用,那就是修饰局部变量。
auto修饰局部变量,表示这个局部变量是自动局部变量,自动局部变量分配在栈上。(既然在栈上,说明它如果不初始化那么值就是随机的······)
平时定义局部变量时就是auto的,只是省略了auto关键字而已。可见,auto的局部变量其实就是默认定义的普通的局部变量。

static

static关键字在C语言中有2种用法,而且这两种用法彼此没有任何关联、完全是独立的。其实当年本应该多发明一个关键字,但是C语言的作者觉得关键字太多不好,于是给static增加了一种用法,导致static一个关键字竟然有两种截然不同的含义。


static的第一种用法是

用来修饰局部变量,形成静态局部变量。要搞清楚静态局部变量和非静态局部变量的区别:非静态局部变量分配在栈上,而静态局部变量分配在数据段/bss段上。

静态局部变量:静态局部变量定义时前面加static关键字。

1、静态局部变量在第一次函数被调用时创造并初始化,但在函数退出时它不死亡,而是保持其值等待函数下一次被调用。下次调用时不再重新创造和初始化该变量,而是直接用上一次留下的值为基础来进行操作。
2、静态局部变量的这种特性,和全局变量非常类似。它们的相同点是都创造和初始化一次,以后调用时值保持上次的不变。不同点在于作用域不同。

static的第二种用法是

用来修饰全局变量,形成静态全局变量。要搞清楚静态全局变量和非静态全局变量的区别。区别是在链接属性上不同,并且有如下作用:

普通全局变量
普通全局变量就是平时使用的,定义前不加任何修饰词。普通全局变量可以在各个文件中使
用,可以在项目内别的.c文件中被看到,所以要确保不能重名。

静态全局变量
静态全局变量就是用来解决重名问题的。静态全局变量定义时在定义前加static关键字,告诉编译器这个变量只在当前文件内使用,在别的文件中绝对不会使用。

分析:
1、静态局部变量在存储类方面和全局变量一样。
2、静态局部变量在生命周期方面和全局变量一样。
3、静态局部变量和全局变量的区别是:作用域、链接属性。静态局部变量作用域是代码块作用域(和普通局部变量是一样的)、链接属性是无连接;全局变量作用域是文件作用域(和函数是一样的)、链接属性方面是外连接。

extern

extern主要用来声明全局变量,声明的目的主要是在a.c中定义全局变量而在b.c中使用该变量。

C语言中程序的编译时以单个.c源文件为单位的,因此编译a.c时只考虑a.c中的内容(不会考了b.c的内容),这就导致a.c中使用了b.c中定义的变量时在编译时报错。解决方案是声明。


应该在a.c中使用g_b之前先声明g_b,声明就是告诉a.c我在别的文件中定义了g_b,并且它的原型和声明的一样,将来在链接的时候链接器会在别的.o文件中找到这个同名变量。

虽然需要在a.c中去声明是外部全局变量,但在实际使用中,如果a.c文件将会包含b.c的头文件,则可以直接就在b.c的头文件b.h中就声明了,包含之后也就相当于已经在a.c中声明了。这样更利于代码的封装。

其实,extern也是可以用来修饰函数的,只是函数的声明extern关键词是可有可无的,因为函数本身不加修饰的话就是extern。

全局变量在外部使用声明时,extern关键字是必须的,如果变量没有extern修饰且没有显式的初始化,同样成为变量的定义,因此此时必须加extern。

volatile

volatile的字面意思:可变的、易变的。

C语言中volatile用来修饰一个变量,表示这个变量可以被编译器之外的东西改变。编译器之内的意思是变量的值的改变是代码的作用,编译器之外的改变就是这个改变不是代码造成的,或者不是当前代码造成的,编译器在编译当前代码时无法预知。譬如在中断处理程序isr中更改了这个变量的值,譬如多线程中在别的线程更改了这个变量的值,譬如硬件自动更改了这个变量的值(一般这个变量是一个寄存器的值)


以上说的三种情况(中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量)都是编译器在编译时无法预知的更改,此时应用使用volatile告诉编译器这个变量属于这种(可变的、易变的)情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。


编译器的优化在一般情况下非常好,可以帮助提升程序效率。但是在特殊情况下,变量会被编译器想象之外的力量所改变,此时如果编译器没有意识到而去优化则就会造成优化错误,优化错误就会带来执行时错误。而且这种错误很难被发现。


volatile是程序员意识到需要volatile然后在定义变量时加上volatile,如果你遇到了应该加volatile的情况而没有加程序可能会被错误的优化。如果在不应该加volatile而加了的情况程序不会出错只是会降低效率。所以我们对于volatile的态度应该是:正确区分,该加的时候加,不该加的时候不加,如果不能确定该不该加为了保险起见就加上。

补充说明:

能改变变量值的两种方式:

1、人为在程序中改变变量的值;

2、某变量的值是来自于某个寄存器,因为寄存器的值不断改变,从而导致变量的值不断改变,比如给某变量所赋予的温度传感器的寄存器的值,那么,这时的变量的值就会不断发生改变。

针对这种连续改变的变量,C编译器会对其进行优化。

本来是:

第一次改变变量的值

第二次改变变量的值

第三次改变变量的值

……

第n次改变变量的值

编译器会将其优化为:第一次改变变量的值

编译器会认为,这样过于频繁的非人为的“连续”改变是无意义的,是“陷入死循环的前兆”,因此必须对其进行优化。

但是,有时候,我们这种连续的变化是有意义的。

所以,就有了一个关键字volatile

volatile关键字主要是声明那些“频繁被改变的变量”,表示虽然频繁被改变但每次改变都有意义,提示编译器不要进行优化。

举个例子:

int square(volatile int *ptr)    
{     
    int a,b;     
    a = *ptr;    
    b = *ptr;    
    return a * b;     
// 中间*ptr的值可能会被寄存器所改变进而导致a,b两个变量不同,得不到a^2这样的结果
}    

这里指示ptr是个易变的不稳定的指针变量,所以编译器不会对其进行优化。

中间*ptr的值可能会被寄存器所改变进而导致a,b两个变量不同,得不到a^2这样的结果

如果想要得到a^2这样的结果,就有如下代码:

int square(int *ptr)    
{     
    int a,b;     
    a = *ptr;    
    b = *ptr;    
    return a * b;     
// 这样即使*ptr在函数体内发生相应改变编译器也会进行优化
// 使得*ptr在函数内不会发生改变,最终a,b值相同,这也就是我们所期望的a^2
}   

此时,就算ptr被连续改变,也会因为优化,而只有第一次改变的那个值,得到a^2.

试想一下,如果第一段代码中的改变是有意义的呢?

我们没有加volatile反而就因为优化得到了不对的结果。

所以,通常针对那些易改变的变量(且这种改变是有意义的),我们就要加上volatile,提示编译器不必优化,省得因为优化导致结果错误。

通常,以下几种情况需要添加volatile

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同的意义。

总结来说就是:volatile表示某变量允许被系统连续改变而不必进行优化。

为什么一个变量既可以是volatile类型也可以是const类型,这两个不冲突吗?
const主要是限定“不可以在程序中人为的改变”,而volatile则是限定“变量可以被寄存器频繁的改变”,针对对象不同,含义也就不矛盾了。

例如:volatile表明的是该对象可能会有意想不到的改变,例如从一个寄存器读取温度值,它就会不断的改变,而const表示的是该对象不能被程序改动也就是不能在程序中再对它赋值,这两者是不矛盾的。

Volatile

C语言中volatile关键字的作用_c语言volatile关键字使用场景-CSDN博客

下面是使用volatile变量的几个场景:

1>中断服务程序中修改的供其它程序检测的变量需要加volatile;

2>多任务环境下各任务间共享的标志应该加volatile

3>存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。

register 

寄存器变量:

register类型的局部变量表现上和auto是一样的,这东西基本没用,知道就可以了。

register被称为:C语言中最快的变量。C语言的运行时环境承诺,会尽量将register类型的变量放到寄存器中去运行(普通的变量是在内存中),所以register类型的变量访问速度会快很多。但是它是有限制的:首先寄存器数目是有限的,所以register类型的变量不能太多;其次register类型变量在数据类型上有限制,譬如你就不能定义double类型的register变量。一般只在内核或者启动代码中,需要反复使用同一个变量这种情况下才会使用register类型变量。

uboot中用到了一个register类型的变量,gd这个变量是用来存uboot的全局变量(gd就是global data)。因为这个全局变量在整个uboot中到处都被访问,所以定义成register的。

平时写代码要被定义成register这种情况很少,一般慎用。

register编译器只能承诺尽量将register修饰的变量放在寄存器中,但是不保证一定放在寄存器中。主要原因是因为寄存器数量有限,不一定有空用。

restrict

(1)c99中才支持的,所以很多延续c89的编译器是不支持restrict关键字,gcc支持的。
(2)restrict也是和编译器行为特征有关的。
(3)restrict只用来修饰指针,不能修饰普通变量。
(4)http://blog.chinaunix.net/uid-22197900-id-359209.html
(5)memcpy和memmove的区别

临时匿名变量

C语言和汇编的区别(汇编完全对应机器操作,C对应逻辑操作)
1、C语言叫高级语言,汇编语言叫低级语言。低级语言的意思是汇编语言和机器操作相对应,汇编语言只是CPU的机器码的助记符,用汇编语言写程序必须拥有机器的思维。因为不同的CPU设计时指令集差异很大,因此用汇编编程的差异很大。
2、高级语言(C语言)它对低级语言进行了封装(C语言的编译器来完成),给程序员提供了一个靠近人类思维的一些语法特征,人类不用过于考虑机器原理,而可以按照自己的逻辑原理来编程。譬如数组、结构体、指针····

3、更高级的语言如java、C#等只是进一步强化了C语言提供的人性化的操作界面语法,在易用性上、安全性上进行了提升。

C语言的一些“小动作”
高级语言中有一些元素是机器中没有的。高级语言在运算中允许我们大跨度的运算。意思就是低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。譬如一个变量i要加1,在C中只需要i++即可,看起来只有一句代码。但实际上翻译到汇编阶段需要3步才能完成:第1步从内存中读取i到寄存器,第2步对寄存器中的i进行加1,第3步将加1后的i写回内存中的i。

在这个过程中,会产生一些临时的中间变量,用于临时存储中间结果,称之为临时匿名变量。

我们可以使用临时变量来理解强制类型转换,理解不同数据类型之间的运算。

左值与右值

放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值 = 右值;

当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间;当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。

存储类

存储类就是存储类型,也就是描述C语言变量在何种地方存储。
内存有多种管理方法:栈、堆、数据段、bss段、.text段······一个变量的存储类属性就是描述这个变量存储在何种内存段中。
譬如:局部变量分配在栈上,所以它的存储类就是栈;显式初始化为非0的全局变量分配在数据段,显式初始化为0和没有显示初始化(默认为0)的全局变量分配在bss段。

作用域

作用域是描述这个变量起作用的代码范围。
以局部变量为例,其作用域规则是代码块作用域。意思就是这个变量起作用的范围是当前的代码块。代码块就是一对大括号{}括起来的范围,所以一个局部变量的作用域是:这个变量定义所在的{}范围内从这个变量定义开始往后的部分。(这就解释了为什么变量定义总是在一个函数的最前面)。

不管是局部变量、全局变量、函数,都要先定义才能使用。不过更严格来说:全局变量/函数的作用域都是自己所在的文件,但是缺少声明就没法用,解决方案是:

1、把它定义到前面去;

2、定义到后面但是在前面加声明;局部变量因为没法声明,所以只能定义在前面去。
在c89标准的编译器中(现在很多编译器还延续使用c89标准),所有的局部变量必须先定义在最前面,在变量定义完成之前不能有一句执行代码。在c99标准的编译器中(gcc兼容c99标准)允许在代码块内任意地方定义变量。但是允许定义的变量还是只能使用在定义了之后,定义之前还是不能用的。

同名变量的掩蔽规则
问题:编程时,不可避免会出现同名变量。变量同名后不一定会出错。
首先,如果两个同名变量作用域不同且没有交叠,这种情况下同名没有任何影响。
其次,如果两个同名变量作用域有交叠,C语言规定在作用域交叠范围内,作用域小的一个变量会掩蔽掉作用域大的那个。

生命周期

生命周期是描述这个变量什么时候诞生(运行时分配内存空间给这个变量)及什么时候死亡(运行时收回这个内存空间,此后再不能访问这个内存地址,或者访问这个内存地址已经和这个变量无关了)的。研究变量生命周期,有助于理解变量的行为特征。

栈变量的生命周期
局部变量(栈变量)存储在栈上,生命周期是临时的。临时的意思就是说:代码执行过程中按照需要去创建、使用、消亡的。
譬如一个函数内定义的局部变量,在这个函数每一次被调用时都会创建一次,然后使用,最后在函数返回的时候消亡。

堆变量的生命周期
首先要明白:堆内存空间是客观存在的,是由操作系统维护的。我们程序只是去申请然后使用然后释放。
我们只关心我们程序使用堆内存的这一段时间,因此堆变量也有了自己的生命周期,就是:从malloc申请时诞生,然后使用,直到free时消亡。
所以堆内存在malloc之前和free之后不能再去访问,因此堆内存在实践编程时都是被反复的malloc和free的。

数据段、bss段变量的生命周期
全局变量的生命周期是在程序被执行时诞生,在程序终止时消亡。
全局变量所占用的内存是不能被程序自己释放的,所以程序如果申请了过多的全局变量会导致这个程序一直占用大量内存。
如果说堆内存是图书馆借的书,那么全局变量就是自己买的书。

代码段、只读段的生命周期
其实就是程序执行的代码,其实就是函数,它的生命周期是永久的。不过一般代码的生命周期我们并不关注。
有时候放在代码段的不只是代码,还有const类型的常量,还有字符串常量。(const类型的常量、字符串常量有时候放在rodata段,有时候放在代码段,取决于平台)

链接属性

C语言程序的组织架构:多个C文件+多个h文件
庞大、完整的一个C语言程序(譬如linux内核、uboot)由多个c文件和多个h文件组成的。


程序的生成过程就是:编译+链接。

编译阶段就是把源代码变成.o目标文件(二进制的机器码格式),目标文件里面有很多符号和代码段、数据段、bss段等分段。符号就是编程中的变量名、函数名等。

运行时,变量名、函数名通过链接能够和相应的内存对应起来。

.o的目标文件链接生成最终可执行程序的时候,其实就是把符号和相对应的段给链接起来,将各个独立分开的二进制的函数链接起来形成一个整体的二进制可执行程序。

链接的作用,就是根据既定的逻辑组织成最终的成品。

编译以文件为单位、链接以工程为单位
编译器工作时是将所有源文件依次读进来,单个为单位进行编译的。
链接的时候实际上是把第一步编译生成个单个的.o文件整体的输入,然后处理链接成一个可执行程序。

C语言中的符号有三种链接属性:外连接、内链接、无链接

外连接的意思就是外部链接属性,也就是说这家伙可以在整个程序范围内(言下之意就是可以跨文件)进行链接,譬如普通的函数和全局变量属于外连接。


内链接的意思就是(c文件内部)内部链接属性,也就是说这家伙可以在当前c文件内部范围内进行链接(言下之意就是不能在当前c文件外面的其他c文件中进行访问、链接)。static修饰的函数/全局变量属于内链接。


无连接的意思就是这个符号本身不参与链接,它跟链接没关系。所有的局部变量(auto的、static的)都是无连接的。

函数和全局变量的同名冲突
因为函数和全局变量是外部链接属性,就是说每一个函数和全局变量将来在整个程序中所有的c文件都能被访问,因此在一个程序中的所有c文件中不能出现同名的函数/同名的全局变量。
最简单的解决方案就是起名字不要重复,但是很难做到。主要原因是一个很大的工程中函数和全局变量名字太多了,而且一个大工程不是一个人完成的,是很多人协作完成,所以很难保证不会重名。解决方案呢?
现代高级语言中完美解决这个问题的方法是命名空间namespace(其实就是给一个变量带上各个级别的前缀)。

但是C语言不是这么解决的。
C语言比较早碰到这个问题,当时还没发明namespace概念,当时C语言就发明了一种不是很完美但是凑活能用的解决方案,思路是这样的:我们将明显不会在其他c文件中引用(只在当前c文件中引用)的函数/全局变量,使用static修饰使其成为内链接属性,这样在将来连接时即使2个c文件中有重名的函数/全局变量,只要其中一个或2个为内链接属性就没事。
这种解决方案在一定程度上解决了问题。但是没有从根本上解决问题,留下了很多麻烦。所以这个就导致了C语言写大型的项目难度很大。


做个简单的总结和补充:
(1)普通(自动)局部变量定义时如果未显式初始化则其值随机,变量地址由运行时在栈上分配得到,多次执行时地址不一定相同,函数不能返回该类变量的地址(指针)作为返回值。


(2)写程序尽量避免使用全局变量,尤其是非static类型的全局变量。能确定不会被其他文件引用的全局变量一定要static修饰。


(3)全局变量应该定义在c文件中并且在头文件中声明,而不要定义在头文件中(因为如果定义在头文件中,则该头文件被多个c文件包含时该全局变量会重复定义)


(4)在b.c中引用a.c中定义的全局变量/函数有2种方法:一是在a.h中声明该函数/全局变量,然后在b.c中#include <a.h>;二是在b.c中使用extern显式声明要引用的函数/全局变量。其中第一种方法比较正式。


(5)存储类决定生命周期,作用域决定链接属性。


(6)宏和inline函数的链接属性为无连接。

补充 

变量使用的优先顺序是:普通局部变量、静态局部变量、静态全局变量、普通全局变量。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值