C语言知识点梳理

前言

本文侧重于编程技巧和细节归纳,故不会过多讲解相关的基础知识。作者能力有限,文章难免有不足和勘误。请读者多多包涵。

一. 编译器和操作系统的影响

因为C语言和操作系统联系密切,故在不同编译器和操作系统中差异很大。如不同数据类型的字节大小,对错误的检测机制等。(本文中用的操作系统为 Windows 10,编译器为Visual Studio 2019)

二. 输入输出函数

2.1 scanf

2.1.1 scanf的结束输入方法

① 遇空格,回车,跳格键时结束。
② 遇宽度结束,即字符串的最大长度。
③ 遇非法输入。

2.1.2 scanf的正则表达式

// 1.限制读取字符的范围
scanf("%[a-z]", s);        // 只能读入a-z的字符,读到其他字符就停止
scanf("%[a-z0-9A-Z]", s);  // 只能读入0-9a-zA-Z的字符,读到其他字符就停止
scanf("%[key]", s);        // 只能读入key这几个字符,读到其他字符就停止 

// 2.设置以什么字符结束
scanf("%[^\n]", s);  // 只能读入非'\n'的字符,读到'\n'就停止 (常用于读取带空格的字符串)
    
// 3.丢弃读到的数据
scanf("%*c");    // %*c 表示丢弃读到的字符,不写入
scanf("%*[:]");  // %*[:]表示丢弃':'字符,不写入

// 4.综合应用
scanf("%*[^\n]%*c");  // 清空输入缓冲区。%*[^\n] 表示丢弃所有非'\n'的字符,%*c表示丢弃最后的'\n'字符

2.2 printf

// 1.定义的输出格式
int a = 10; float b = 123456.34; char* s = "abcdefg";
printf("%.9s\n", s);    // 输出9个字符,不够时左补空格
printf("%-9s\n", s);   	// 左对齐
printf("%4.3s\n", s);  	// 先输出(4减3)1个空格,再输出3个字符
printf("%0.4f%\n", b); 	// 输出保留4位小数
printf("%12.4f\n",b);  	// 输出(12减4)8个整数,整数不够时先左补空格,再输出4位小数
	
// 2.输出分段字符串
int n = 1;
printf("%s\n", "abc\0def\0" + 4 * n);

三. 预处理命令及其高级用法

3.1 #,#@ 和 ##的使用规则

// 1.#@ 加单引号,数字直接转成字符(不能是字符串)
#define ToChar(x) #@x 
printf("%c", ToChar(1));

// 2.# 加双引号,转成字符串
#define ToString(x) #x
printf("%s", ToString(1234));

// 3.## 连接多个标签
#define Conn(x, y) x##y 
// 3.1 当x,y为字符串时,其作用为连接多个字符串
printf("%s", Conn("x", "y"));
// 3.2 当x,y为整数时,其作用为连接多个整数(返回int类型)
printf("%d", Conn(12,34));
// 3.3 当x,y为标签时,其作用为连接多个标签组成变量名(可以是任何类型的变量名)
int xyter = 100;
printf("%d", Conn(xy,ter));

3.2 预处理的综合应用

// 1.声明函数
#define TYPE(type1,type2) type1##_##type2
TYPE(image,load)(int a, int b) 
{ 
    return a + b; 
}
printf("%d", image_load(3, 4));

// 2.初始化结构体(类似于构造函数)
typedef struct tagBOOK
{
	char s[100];
	int i;
}BOOK;
#define InitBOOK(s,i) {#s, i}
BOOK b = InitBOOK(qwe2312, 1234);

// 3.重定义各种运算符(不建议使用)
#define y1 (
#define y2 )
#define and ||
#define or &&
printf y1 "34543" y2;

// 4.宏函数
#define SUMu(a,b)\   // do while(0)型,特点是无返回值,但最后没有逗号
do\
{\
   a+b;\
\
}while (0)

// 5.加载静态数据库
#pragma comment(lib,"msimg32.lib")

// 6.防止文件重包含
#ifndef _IMAGE_H_
#define _IMAGE_H_
// 文件代码块
#endif // _IMAGE_H_

// 7.加载巨大的数组
int array[10] ={
	#include "num.txt"   // #include"X" 的作用是,将X文件中的所有内容展开放入#include所在的一行中
};
num.txt ==> 1,2,3,4,5,6,7,8,9,0

四. 各运算符的使用

4.1 前加和后加运算符

注意:不同编译器对前加和后加运算符的处理是有区别的,但性质是通用的。

// 1.前加和后加运算符的核心性质	
a.前加和后加运算,类似于函数调用,他们都有返回值。 
b.前加的返回值是值得地址。 
c.后加的返回值是值本身, 且后加会产生临时变量。 
d.前加的优先级大于后加,故在同一式子中先计算完所有的前加,在计算后加。
// 2.例子 
int a[5] = {1,2,3,4,5}; int k = 0;
a[++k] = a[++k]; // 12345     // 在vs中结果不变
a[++k] = a[k++]; // 12245     // 在vs中结果不变
a[k++] = a[k++]; // 22345     // 在vs中结果不变
a[k++] = a[++k]; // 32345     // 在vs中结果不变
++k + ++k = 4    // 定理c
(++k + ++k + k++) = (k++ + ++k + ++k);  // 定理d
printf("%d%d%d",++k,++k,++k); // 333    // 与函数参数的堆栈操作有关
printf("%d%d%d",k++,k++,k++); // 210    // 与函数参数的堆栈操作有关
// 注意:以上一连串的连加,是未定义行为,无法正确翻译成汇编代码,所以在vs中对该行为进行了处理,在实际开发中慎用

4.2 位移和逻辑运算符

// 1.交换两个数
a ^= b;
b ^= a;
a ^= b; \\注意:判断交换的条件中不能包含等于
    
// 2.取相反数
a == ~a + 1
    
// 3.判奇偶数
num & 1 == 1 \\为奇数
num & 1 == 0 \\为偶数

4.3 三元运算符

int a = 2 > 1 ? 10 : 20; // 单个三元运算符与单个if else的处理速度差不多。
int a = 2 > 1 ? 3 : 10 > 8 ? 10 : 100; // 长式子,在后端连接

五. 编程结构和语句

5.1 switch case 语句的本质

1.选择结构
(1).switch case语句
	int a = 0;
	switch (a)
	{
	case 1:
		a = 10;
	case 2:
		a = 20;
	default:
		a = 40;
		break;
	}
// 反汇编的前半部(后半部是赋值给a赋值)
 mov         eax,dword ptr [a]  
 mov         dword ptr [ebp-0D0h],eax  
 cmp         dword ptr [ebp-0D0h],1  
 je          __$EncStackInitStart+3Dh (0A825B9h)  
 cmp         dword ptr [ebp-0D0h],2  
 je          __$EncStackInitStart+44h (0A825C0h)  
 jmp         __$EncStackInitStart+4Bh (0A825C7h)  
// 由反汇编代码可知,switch case 语句的本质是,先逐个的去判断条件,一满足条件直接case跳转,并一直运行运行到底,或遇到break跳出switch语句。
// 注意:switch case 语句,条件和结果的汇编代码是分开的。if  else语句,条件和结果的汇编代码的分开是一一对应,混在一起的。

5.2 for 和 while 语句的区别

// for循环
1.for循环,开头可以声明局部变量,一般用于嵌套循环,且循环次数已知
  
// while循环
1.while循环,开头不能可以声明局部变量,一般用于循环次数未知的情况,且后续会用到局部变量(循环次数)

5.3 跳转语句

// 注意:一般跳转语句一句占一整行
break      // 用于跳出switch,for,while结构,且一次只能跳一层
continue   // 用于循环结构中,不再运行continue以下的语句,跳过当次循环,进入下次循环
return     // 用于停止所在函数的运行,不再运行return以下的语句,并返回指定的值
goto       // 用于跳转到指定的标签位置,多用于多层嵌套结构的跳出问题中
exit();    // 用于结束所运行的exe程序

5.4 C99复合文字

注意:复合文字的用法类似于强制类型转换

typedef struct 
{ 
    int a; 
    char b; 
}NUM;

void snn(NUM* n) { printf("%d %c", n->a, n->b); }

NUM* num1 = &(NUM) { 10, '#' };    // 声明结构体指针
snn(&(NUM) { 20, '@' });           // 调用函数
int *a = (int[]){ 1, 23, 45, 6 };  // 声明数组指针

六. 数组和字符串

6.1 数组

// 1.指定数组下标位置进行赋值
int a[10] = { [7] = 10, [2] = 3 };
printf("%d %d\n", a[2], *(a + 7));   // 一个用下标读取,一个用指针读取

// 2.数组的取值,倒取值
int a[5] = { 1,2,3,4,5 };
int* end_pa = &a + 1;    // 取a[4 + 1]的位置,即最后的位置,加1加的是整个数组的长度
int* top_pa = a;         // 取a[0]的位置,即第一的位置
printf("%d %d\n", end_pa[-1], top_pa[1]);

// 3.扩展数组的含义(在解决实际问题时可极大的简化代码)
// 3.1 以ascll字符码为下标的数组
int a[256(ascll)] = {.....};
printf("%d", a['g']);
// 3.2 同时保存字符串和整形的数组
char* s[2][2] = { {12,"qwe"},{24,"asd"} };
printf("%d %s %c", (int)s[0][0], s[1][1], s[1][1][2]);

6.2 字符串

6.2.1字符串变量和字符串常量

char s1[5] = "1234";  // 字符串数组属于变量,在堆上分配空间,且只能读入length-1个字符,最后一个是'\0'
char* s2 = "qwer";    // 直接定义的字符串属于常量,也在栈上分配空间,但只能进行只读操作

6.2.2 常用的字符串个处理函数

// 1.strlen 计算指定的字符串的长度
size_t strlen(const char *s);
strlen计算的长度是不包括结束字符'\0',当s是字符串数组时其值为length-1(因为最后的'\0'未算在内)

// 2.strcat 合并两个字符串
char* strcat(char* s1 ,const char* s2);
a.合并s1和s2保存到s1中,并返回s1的指针
b.s1必须为字符串数组
c.strcap最后会补'\0',所以s1要足够大

// 3.strcap 复制字符串(多用于给结构体中字符串赋值或进行替换)
char *strcpy(char *s1, const char *s2)
将s2复制到s1中,并返回一个指向s1的新指针
    
// 4.strncpy 截取字符串
char *strncpy(char *s1, const char *s2, size_t n)
a.截取s2中的n个字符并保存到s1中,并返回s1的指针
b.s1必须为字符串数组,且必须初始化,strncap不会在最后补'\0'
c.默认位置截取是从s2[0]开始到s2[n-1],可通过strncpy(s1, s2 + m, n);改变截取的起始位置

// 5.strcmp 比较两个字符串
int strcmp(const char* s1, const char* s2);
根据ASCII码表比较s1和s2字符串的大小,s1 == s2 返回0,s1 > s2 返回1,s1 < s2 返回-1

// 6.strchr 和 strrchr 查找字符串中指定的字符,返回该字符在内存中的地址,无则返回NULL
char* strchr(const char* str, char c);  // 从前向后查找
char* strrchr(const char* str, char c); // 从后向前查找
// 注意:(char*)查 — (char*)原 == 开头到查找位置的字符串长度

// 7.sprintf 将各种数据连接成字符串并保存,返回字符串的长度
int sprintf(char *str, const char *format, ...)

// 8.sscanf 从指定字符串中,格式化获取各种数据,并保存在各变量中
int sscanf(const char *str, const char *format, ...)

七. 函数

7.1 值传递和指针传递的区别

// 值传递:形参是实参的备份,改变形参不能改变实参本身,且值传递参数数据较大,但可返回局部变量的值
// 指针传递:可通过解引用改变原值,可定义任何类型的参数传递(数组,函数等),且参数数据较小,但不能返回局部变量的指针

7.2 可变参数函数

// 1.使用头文件实现可变参函数
#include <stdarg.h>  // 实现可变参数必须包含该头文件
int sum(int a, ...)
{
	int reNum = 0;
	va_list list;  // 声明参数列表
	va_start(list, a); // 创建参数列表
	
	for (int i = 0; i < a; i++)
		reNum += va_arg(list, int);  // 使用参数列表

	va_end(list); // 删除参数列表
	return reNum;
}
printf("%d\n", sum(4, 1, 2, 3, 4));

// 2.使用__VA_ARGS__宏实现可变参宏函数
#define PRINTF(format, ...) printf(#format, __VA_ARGS__) 
PRINTF(%d\n, sum(4, 1, 2, 3, 4));

八. 结构体

// 结构体的对齐
typedef struct          		typedef struct
{                       		{
	char a;             			int a;
	int b;              			char b;
	char c;							char c;
}BOOK;							}BOOK;
sizeof(BOOK) == 12				sizeof(BOOK) == 8
// 结构体的大小为结构体中最大元素的整数倍,同时也要满足4的倍数,不足时在末尾补全。
// 综上:定义结构体时一般将最大元素放在开头,或结尾。

九. 指针

// 1.指针数组(数组中保存的元素是指针)
char* s[2] = { "qwewq", "weqw" };  // 字符串数组
int** a = (int**)malloc(sizeof(int*)*10); // 动态申请int*数族,注意区分不是二维指针和数组指针

// 2.数组指针(指针指向的是数组)
int(*p)[4] = (int(*)[4])malloc(sizeof(int) * 7 * 4); // 动态申请二维数组,注意行数定义为4 
int a[4] = {1, 3, 5, 6}
int(*p)[4] = &a;  // 如果数组指指向一维数组则要加&号

// 3.指针函数(函数的返回值是指针)
int* ReturnAarray(int a);  // 返回值是整形指针
int (*ReturnAarray())[5];  // 返回值是数组指针
void (*ReturnFunction(int a, int b))(char, int);  // 返回值是 void fun(char, int) 型的函数指针

// 4.函数指针(指针指向的是函数)
// 函数指针数组
typedef void (*EVENT)();  // 定义EVENT函数指针数组
enum FUNCTION { PRINTF = 1, SCANF = 0 }; // 用枚举便于管理
int main()
{
	EVENT Event[2];  // 在声明时定义函数指针数组的大小
	Event[SCANF] = printf;
	Event[PRINTF] = scanf;
	
	int a = 0;
	Event[PRINTF]("%d", &a); // 使用数组中的函数
	Event[SCANF]("%d", a);
	
	return 0;
}

// 5.二维指针
int a1 = 123, a2 = 456, a3 = 789;
int *num1 = &a1, *num2 = &a2;
void UseNum(int **n1, int** n2)
{
	*n1 = &a3;    // 一层解引用,改变原指针的指向
	**n2 = 12345; // 两层解引用,改变原数值
}
UseNum(&num1, &num2);
printf("%d %d", *num1, *num2);
// 注意:NULL指针无法进行两层解引用	

十. 动态内存分配

// 1.malloc 申请指定大小的内存空间(不会对内存空间进行初始化)
void* malloc(size_t size);
struct* book = (struct* book)malloc(sizeof(struct book) * 10); \\申请结构体数组

// 2.calloc 申请指定大小的内存空间并初始化为0
void *calloc(size_t nmemb,size_t size);
struct* book = (struct* book)calloc(sizeof(struct book) , 10); \\申请结构体数组

// 3.memset初始化内存空间(该函数在string.h文件中,多用于初始化字符串,数组,结构体)
void* memset(void* s, int c, size_t n);
memset(array,-1,sizeof(array)); // 用-1初始化数组
// 注意:memset是以一个字节为单位初始化指定的内存空间,所以只能初始化赋值为-1或0, 且'\0'== 0

// 4.free 释放指定内存空间(只能释放动态申请的内存空间)
void free(void *ptr);

十一. 文件的读取

11.1 文件路径的处理

// 1.获取运行程序的文件路径
TCHAR exeFilePath[260] = { 0 };  // 注意是TCHAR类型
GetModuleFileName(NULL, exeFilePath, 260);
// 转化成char类型便于后续的处理
char exeFilePath_char[260] = { 0 };
for (int i = 0; i < 260; i++) exeFilePath_char[i] = (char)exeFilePath[i];
printf("%s", exeFilePath_char);

// 2.获取文件后缀名
char* lastName = strrchr(filePath, '.'); 

// 3.截取文件路径(一般截取开头到最后一个'\')
char* usefile1 = strrchr(filePath, '\\');
char* usefile2 = strncpy((char[260]) { 0 }, filePath, usefile1 - filePath + 1);
printf("%s\n%s\n%s\n", filePath, usefile1, usefile2);

// 4.连接或修改文件路径
char filePath[MAX_PATH];  // MAX_PAT==260 
sprintf(filePath, "%s_%s", exePath, ".txt");

11.2 读取文本文件

注意:读取文本文件的核心是,系统会自动以空格和回车作为分隔符

// 1.标准代码框架
FILE* fp; // 创建文件结构体
if ((fp = fopen("map.txt", "r""w")) == NULL)  // 打开指定文件
	printf("file\n");     
// 读写操作的代码段.....
fclose(fp);  // 释放文件结构体

// 2.读取操作
// 2.1读取数组
int num[10] = { 0 };
for (int i = 0; !feof(fp); i++)   // feof 用于检测是否读到整个文本的末尾,是则返回0,否则返回非0
	fscanf(fp, "%d", &num[i]);    // fscanf 通过指定格式读取数据
// 2.2用正则表达式读取
fscanf(fp, "%d %*[^:]%*c%[^\n]", &num, str);  // %*[^:]%*c%[^\n] 表示读取':'后面的内容

// 3.写入操作("w"为重写全文,"r+"为覆盖指定位置的内容,"a"为文本末尾追加)
fprintf(fp, "%s", write);

// 4.重要函数
// 4.1 ftell 获取光标的当前位置
long ftell(FILE *_Stream);

// 4.2 fseek 移动光标到指定位置,成功返回0,否则返回非0
int fseek(FILE* stream, long int offset, int whence) 
//whence: SEEK_CUR 当前位置,SEEK_END 末尾位置,SEEK_SET 开头位置

//后补:文件的读和写操作一般是分开处理的,不会混写在一起

11.3 读取二进制文件

// 1.标准代码框架
FILE* fp;  // 创建文件结构体
if ((fp = fopen("map.txt", "rb""wb")) == NULL)  // 打开指定文件
	printf("file\n");     
// 读写操作的代码段.....
fclose(fp);  // 释放文件结构体

// 2.写入操作(以结构体的读写为例)
typedef struct tagBOOK
{
	char name[100];
	int ID;
	float price;
}BOOK;
BOOK book = { "qwer",123,34.56 };
fwrite(&book, sizeof(BOOK), 1, fp);  // 元素指针,元素大小,元素数量,文件结构体指针

// 3.读取操作
fread(&book, sizeof(BOOK), 1, fp); // 元素指针,元素大小,元素数量,文件结构体指针

// 补.在保存结构体数组时改变的是元素的大小,而不是元素的数量,即直接保存结构体数组

十二. C相关的其它常用知识

12.1 生成随机数

#include<stdlib.h>
srand((unsigned int)time(NULL));  // 加在每个rand函数前面
int num = rand() % n;  // 随机生成0~n-1的整数 

12.2 获取系统时间

#include<time.h>
time_t timep;
struct tm* tp;
time(&timep);  // 获取1970-01-01至今的小时数
tp = localtime(&timep);  // 将time_t的值分解为tm结构,并用本地时区表示
printf("%d/%d/%d %02d:%02d:%02d\n", 1900 + tp->tm_year, 1 + tp->tm_mon, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec);

12.3 调用DOS命令

#include<stdlib.h>
system("cls");

12.4 内联汇编

// 调用printf函数
char* s = "%d";
int num = 100;
_asm  // 内联汇编的关键字
{
	mov num, 300
	push num
	mov eax, s
	push eax
	call printf
	add esp, 8
}

十三. 常用的WIN32API

WIN32API 编程是属于操作系统相关的另一体系的知识,且限于文章篇幅和主题,因此在下文中只是给出相应的代码,不做过多的讲解。

13.1 控制台

SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &(CONSOLE_CURSOR_INFO) { 1, 0 });  // 隐藏光标,防止打印时闪屏
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), (COORD) { 0, 0 });  // 改变光标位置
system("color F0 "/*F背景色,0前景色*/);  \\改变整个控制台的背景色和前景色
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), BACKGROUND_BLUE);  // 改变输入时字符的背景色和前景色。 BACKGROUND_BLUE(背景色) FOREGROUND_BLUE(前景色) 

// 例子	
int color[2] = { BACKGROUND_BLUE ,BACKGROUND_RED }, a = 0;
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &(CONSOLE_CURSOR_INFO) { 1, 0 });
system("color F0 "/*F背景色,0前景色*/);  
while(1)
{
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), (COORD) { 0, 0 }); \\改变光标位置
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color[a]); 
    printf("%s", "⬛⬛⬛⬛⬛⬛⬛\0⬛⬛⬛⬛\0" + a * 11);
	a ^= 1;
}.1. printf("⬛");一个完整正方形要两个空格 
2. 0=黑色,1=蓝色,2=绿色,3=湖蓝色,4=红色,5=紫色,6=黄色,7=白色,8=灰色,9=淡蓝色,A=淡绿色,B=淡浅绿色,C=淡红色,D=淡紫色,E=淡黄色,F=亮白色 

13.2 控制鼠标键盘

13.2 .1 C原生的非拥塞控制

#include<conio.h>
if (_kbhit())   // 有按键按下则返回非0,否则返回0
	KeyBoard(); // 自定义读取处理函数

13.2 .2 WIN32API对鼠标键盘的处理

// 1.设置鼠标位置 (比例坐标)
SetCursorPos(x, y);   // 用的是屏幕坐标系,与窗口坐标系无关

// 2.获得鼠标坐标(比例坐标)
POINT p;
GetCursorPos(&p); // 注意显示设置,中的缩放与布局的百分比,对坐标的影响,100%==屏幕分辨率坐标
// SetPixel 函数用的是比例坐标,所以要进行转换

// 3.控制鼠标按键
mouse_event (MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 )  // 后面四个参数是保留值,写0即可
mouse_event (MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0 )mouse_event (MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 )

// 4.控制键盘按键
keybd_event(49, 0, 0, 0);    // 49==A键,中间两位是保留值,最后一位0表示,按键按下
keybd_event(49, 0, 0, KEYEVENTF_KEYUP);

// 5.检测鼠标和键盘的状态
SHORT GetAsyncKeyState(int vKey);  // 该函数会改变的返回值的最后一位(第16位)和第一位的值,所以该函数有两种使用方法,用于判断不同的情况
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) // 按住是1,不按是0,会一下发出多消息,可模拟长按!!!!
		printf("ok");
if (GetAsyncKeyState(VK_LBUTTON) & 0x01)   // 按下是1,抬起是0,一次发一条消息)
		printf("ok");

// 补.各按键的编号:数字 0 - 9==0x30 - 0x39   字母 A(a) - Z(z)==0x41 - 0x5A (注意要写十六进制)
// 补.检测鼠标时的参数为:VK_RBUTTON(右键),VK_MBUTTON(中键),VK_LBUTTON(左键)                
// 注意:以上代码有一定的危险性请谨慎使用

// 补充:GetSystemMetrics() 获取设备相关的各种数据(与窗口无关),如鼠标在屏幕上的位置

13.3 多线程编程

void FirstThread()
{
	while(1) printf("FirstThread\n");
}

HANDLE hThread = (HANDLE)_beginthreadex((void*)NULL, (unsigned)0, (void*)FirstThread, (void*)0, (unsigned)0, (unsigned*)NULL);

十四. 附件

14.1 ASCII码表

在这里插入图片描述
在这里插入图片描述

14.2 文件读取模式

在这里插入图片描述

十五. 结语

1.本文对C语言知识整理并不全面和详细,有些知识点也是选了其重要的常用的部分经行介绍。后续作者可能会对其进行补充。
2.学习汇编可以更深入的了解C语言乃至其他语言的运行过程。

作者:墨尘_MO
时间:2022年3月9日

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值