03、指针强化

1、指针是一种数据类型

1.1、指针变量

  指针是一种数据类型,占用内存空间,用来保存内存地址。

#define _CRT_SECURE_NO_WARNINGS	//VS不建议使用传统的库函数,如果不使用这个宏,会出现一个错误,编号:C4996
#include<stdio.h>				//std 标准 i-input 输入 o-output 输出
#include<string.h>				//strcp strcmp strcat strstr
#include<stdlib.h>				//std 标准 lib 库 malloc free

void test01() {
	int *p1 = 0x1234;
	int ***p2 = 0x1111;

	printf("p1 size:%d\n", sizeof(p1));
	printf("p2 size:%d\n", sizeof(p2));

	//指针是变量,指针本身也占内存空间,指针也可以被赋值
	int a = 10;
	p1 = &a;

	printf("p1 address:%p\n", &p1);
	printf("p1 address:%p\n", p1);
	printf("a address:%p\n", &a);
}

//程序入口
int main() {
	test01();
	system("pause");			//按任意键暂停,阻塞功能
	return EXIT_SUCCESS;		//返回 正常退出值 0
}

在这里插入图片描述

1.2、野指针和空指针

1.2.1、空指针

  标准定义了 NULL 指针,它作为一个特殊的指针变量,标识不指向任何东西。要使一个指针为 NULL,可以给它赋值一个零值。为了测试一个指针是否为 NULL,你可以将它与零值进行比较。

  对指针解引用操作可以获得它所指向的值。但从定义上看,NULL 指针并为指向任何东西,因为对一个 NULL 指针解引用是一个非法的操作,在解引用之前,必须确保它不是一个 NULL 指针。

  如果对一个 NULL 指针间接访问会发生什么呢?结果因编译器而异。

  不允许向 NULL 和非法地址拷贝内存:

void test() {
	char *p = NULL;
	//给p指向的内存区域拷贝内容
	strcpy(p, "1111");	//err

	char *q = 0x1122;
	//给q指向的内存区域拷贝内容
	strcpy(q, "2222");//err
}

1.2.2、野指针

  在使用指针时,要避免野指针的出现:

  野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL 避免,而只能通过养成良好的编程习惯来尽力减少。

对野指针进行操作很容易造成程序错误。

  什么情况下会导致野指针?

  • 指针变量未初始化

    任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。

  • 指针释放后为置空

    有时指针在 free 或 delete 后未赋值 NULL,便会使人以为是合法的。别看 free 和 delete 的名字(尤其是 delete),它们只是把拔指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针设置为 NULL,防止产生“野指针”。

  • 指针操作超越变量作用域

    不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

void test() {
	//1、未初始化指针
	//int* p;
	//printf("%d\n", *p);

	//2、malloc后也free了,但是指针没有置空
	//int* p = malloc(sizeof(int));	
	//*p = 100;
	//free(p);	//野指针
	//p = NULL;	//将free后的指针要进行置空,防止野指针的出现
	//printf("%d\n", *p);

	//3、指针的操作超越了变量的作用域
	int *p2 = doWork();		//p2就是野指针
	printf("%d\n", *p2);
	printf("%d\n", *p2);
}

int * doWork()
{
	int a = 10;
	int *p = &a;
	return p;
}

空指针和野指针是否可以被释放

void test() {
	int * p = NULL;
	free(p);			//空指针是可以释放的
	free(p);

	int* p2 = malloc(4);
	free(p2);			//野指针是不可以被释放的
	free(p2);
}

操作野指针是非常危险的操作,应该规避野指针的出现:

  • 初始化时置 NULL

    指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

  • 释放时置 NULL

    当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。

1.3、间接访问操作符

  通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。

  注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。

int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]
&arr1
  • 在指针声明时,* 号表示所声明的变量为指针

  • 在指针使用时,* 号表示操作指针所指向的内存空间

    1)* 相当通过地址(指针变量的值)找到指针指向的内存,再操作内存

    2)* 放在等号的左边赋值(给内存赋值,写内存)

    3)* 放在等号的右边取值(从内存中取值,读内存)

//解引用
void test01(){
	//定义指针
	int* p = NULL;
	//指针指向谁,就把谁的地址赋给指针
	int a = 10;
	p = &a;
	*p = 20;//*在左边当左值,必须确保内存可写
	//*号放右面,从内存中读值
	int b = *p;
	//必须确保内存可写
	char* str = "hello world!";
	*str = 'm';
    printf("a:%d\n", a);
	printf("*p:%d\n", *p);
	printf("b:%d\n", b);
}

1.4、指针的步长

  指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

  思考如下问题:

int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;

//为什么*p1打印出来正确结果?
printf("%x\n", *p1);
//为什么*p2没有打印出来正确结果?
printf("%x\n", *p2);

//为什么p1指针+1加了4字节?
printf("p1  =%d\n", p1);
printf("p1+1=%d\n", p1 + 1);
//为什么p2指针+1加了1字节?
printf("p2  =%d\n", p2);
printf("p2+1=%d\n", p2 + 1);
#define _CRT_SECURE_NO_WARNINGS	//VS不建议使用传统的库函数,如果不使用这个宏,会出现一个错误,编号:C4996
#include<stdio.h>				//std 标准 i-input 输入 o-output 输出
#include<string.h>				//strcp strcmp strcat strstr
#include<stdlib.h>				//std 标准 lib 库 malloc free
#include<stddef.h>

//指针的步长意义
//1、指针变量+1后,跳跃的字节数量
void test01() {
	char * p = NULL;
	printf("%d\n", p);
	printf("%d\n", p + 1);

	double * p2 = NULL;
	printf("%d\n", p2);
	printf("%d\n", p2 + 1);
}

//2、解引用时候,取的字节数
void test02() {
	char buf[1024] = { 0 };
	
	int a = 1000;
	
	memcpy(buf, &a, sizeof(int));

	char * p = buf;		//通过p找到buf的首地址

	printf("buf中的a = %d\n", *(int*)p);
}

//3、自定义数据类型 练习
struct Person
{
	char a;		//0~3
	int b;		//4~7
	char buf[64];	//8~71
	int d;		//72~75
};

void test03() {
	struct Person p1 = { 'a',10,"Hello World",1000 };

	//计算结构体中属性的偏移,利用offsetof(结构体,属性)获取自定义数据类型中属性的偏移
	printf("p1.d的偏移量为:%d\n", offsetof(struct Person, d));
	//打印d的值
	printf("p1.d的值为:%d\n", *(int*)((char*)&p1 + offsetof(struct Person, d)));
}

//程序入口
int main() {
	//test01();
	//test02();
	test03();

	system("pause");			//按任意键暂停,阻塞功能
	return EXIT_SUCCESS;		//返回 正常退出值 0
}

2、指针的意义—间接赋值

2.1、间接赋值的三大条件

通过指针间接赋值成立的三大条件:

1)2个变量(一个普通变量,一个指针变量,或者一个实参一个形参)

2)建立关系

3)通过 * 操作指针指向的内存

void changeValue(int *p)	//a2实参,p形参
{
    *p = 1000;
}

void test(){
    int a = 100;	//两个变量
    int *p = NULL;
    //建立关系
    //指针指向谁,就把谁的地址赋值给指针
    p = &a;
    //通过*操作内存
    *p = 22;
    printf("a = %d\n",a2);
    
    int a2 = 10;
    changeValue(&a2);
    printf("a2 = %d\n",a2);
}

2.2、如何定义合适的指针变量

void test(){
    int b;
    int *q = &b;	//0级指针
    int **t = &q;	//1级指针
    int ***a = &t;	//2级指针
}

2.3、间接赋值:从 0 级指针到 1 级 指针

#define _CRT_SECURE_NO_WARNINGS	//VS不建议使用传统的库函数,如果不使用这个宏,会出现一个错误,编号:C4996
#include<stdio.h>				//std 标准 i-input 输入 o-output 输出
#include<string.h>				//strcp strcmp strcat strstr
#include<stdlib.h>				//std 标准 lib 库 malloc free

int func1() { return 10; }

void func2(int a) {
	a = 100;
}

//指针的意义—间接赋值
void test01() {
	int a = 0;
	a = func1();
	printf("a = %d\n", a);

	func2(a);
	printf("a = %d\n", a);	//此处的a还是10,因为func2中的a形参,a = 100是给形参赋值了
}

void func3(int* a){
    *a = 100;
}

void test02(){
    int a = 0;
    a = func1();
	printf("a = %d\n", a);
    
    func3(&a);
    printf("a = %d\n", a);
}

//程序入口
int main() {

	//test01();
    test02();
    
	system("pause");			//按任意键暂停,阻塞功能
	return EXIT_SUCCESS;		//返回 正常退出值 0
}

2.4、间接赋值:从 1 级指针到 2 级 指针

void AllocateSpace(char** p){
    *p = (char*)malloc(100);
    strcpy(*p,"hello world!");
}

void FreeSpace(char** p){
    if(p == NULL){
        return;
    }
    if(*p != NULL){
        free(*p);
        *p = NULL;
    }
}

void test(){
    char* p = NULL;
    AllocateSpace(&p);
    printf("%s\n", p);
    FreeSpace(&p);
    if(p == NULL){
        printf("内存释放!\n");
    }
}

3、指针做函数参数

指针做函数参数,具备输入输出特性:

  • 输入:主调函数分配内存

  • 输出:被调函数分配内存

3.1、输入特性

#define _CRT_SECURE_NO_WARNINGS	//VS不建议使用传统的库函数,如果不使用这个宏,会出现一个错误,编号:C4996
#include<stdio.h>				//std 标准 i-input 输入 o-output 输出
#include<string.h>				//strcp strcmp strcat strstr
#include<stdlib.h>				//std 标准 lib 库 malloc free

//1、输入特性:主调函数中分配内存,被调函数中使用内存
void func(char* p) {
	strcpy(p, "hello world");
}

void test01() {
	//分配到栈上
	char buf[1024] = { 0 };
	func(buf);

	printf("%s\n", buf);
}

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

void test02() {
	//分配到堆区
	char * p = malloc(sizeof(char) * 64);
	memset(p, 0, 64);

	strcpy(p, "hello world");

	printString(p);
}

//程序入口
int main() {
	//test01();
	test02();

	system("pause");			//按任意键暂停,阻塞功能
	return EXIT_SUCCESS;		//返回 正常退出值 0
}

3.2、输出特性

#define _CRT_SECURE_NO_WARNINGS	//VS不建议使用传统的库函数,如果不使用这个宏,会出现一个错误,编号:C4996
#include<stdio.h>				//std 标准 i-input 输入 o-output 输出
#include<string.h>				//strcp strcmp strcat strstr
#include<stdlib.h>				//std 标准 lib 库 malloc free

//2、输出特性:被调函数分配内存,主调函数使用内存
void allocateSpace(char ** pp) {
	char * temp = malloc(sizeof(char) * 64);
	memset(temp, 0, 64);
	strcpy(temp, "hello world");
	*pp = temp;
}

void test03() {
	char *p = NULL;
	allocateSpace(&p);

	printf("%s\n", p);
}

//程序入口
int main() {
	test03();

	system("pause");			//按任意键暂停,阻塞功能
	return EXIT_SUCCESS;		//返回 正常退出值 0
}

4、字符串指针强化

4.1、字符串指针做函数参数

4.1.1、字符串基本操作

//字符串基本操作
//字符串是以0或者'\0'结尾的字符数组,(数字0和字符'\0'等价)
void test01(){
    //字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
    char str1[] = {'h','e','l','l','o'};
    printf("%s\n",str1);
    
    //字符数组部分初始化,剩余填0
    char str2[100] = {'h','e','l','l','o'};
    printf("%s\n",str2);
    
    //如果以字符串初始化,那么编译器默认会在字符串尾部添加'\0'
    char str3[]="hello";
    printf("%s\n",str3);
    printf("sizeof str:%d\n",sizeof(str3));
    printf("strlen str:%d\n",strlen(str3));
    
    //sizeof 计算数组大小,数组包含'\0'字符
    //strlen 计算字符串的长度,到'\0'结束
    
    //那么如果我这么写,结果是多少呢?
    char str4[100]="hello";
    printf("sizeof str:%d\n",sizeof(str4));
    printf("strlen str:%d\n",strlen(str4));
    
    //请问下面输出结果是多少?sizeof 结果是多少?strlen结果是多少?
    char str5[]="hello\0wolrd";
    printf("%s\n",str5);
    printf("sizeof str:%d\n",sizeof(str5));
    printf("strlen str:%d\n",strlen(str5));
    
    //再请问下面输出结果是多少?sizeof 结果是多少?strlen结果是多少?
    char str6[]="hello\012wolrd";		// \012是宝进制下转十进制的10,在ASCII表中对应的是换行
    printf("%s\n",str6);
    printf("sizeof str:%d\n",sizeof(str6));
    printf("strlen str:%d\n",strlen(str6));
}

在这里插入图片描述
八进制和十六进制转义字符:

  在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的一般形式是’\ddd’,d是0-7的数字。十六进制字符的一般形式是’\xhh’,h是0-9或A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。

比如 :

  • ‘\063’表示的是字符’3’,因为’3’的ASCII码是30(十六进制),48(十进制),63(八进制)。

  • ‘\x41’表示的是字符’A’,因为’A’的ASCII码是41(十六进制),65(十进制),101(八进制)。

4.1.2、字符串拷贝功能实现

//拷贝方法1
void copy_string01(char* dest, char* source)
{
	int len = strlen(source);
	for (int i = 0; source[i] != '\0'; i++)
	{
		dest[i] = source[i];
	}
	dest[len] = '\0';
}

//拷贝方法2
void copy_string02(char* dest, char* source)
{
	while (*source != '\0' /**source!=0*/) {
		*dest = *source;
		source++;
		dest++;
	}
	*dest = '\0';
}

//拷贝方法3
void copy_string03(char* dest, char* source)
{
	//判断*dest是否为0,0则退出循环
	while (*dest++ = *source++) {}
}

void test02() {
	char * str = "Hello World";
	char buf[1024];
	//copy_string01(buf, str);
	//copy_string02(buf, str);
	copy_string03(buf, str);
	printf("%s\n", buf);
}

4.1.3、字符串反转模型

在这里插入图片描述

//第一种方式,利用[ ]进行反转
void reverseString01(char * str)
{
	int len = strlen(str);

	//起始位置下标
	int start = 0;

	//结束位置的下标
	int end = len - 1;

	while (start<end)
	{
		char temp = str[start];
		str[start] = str[end];
		str[end] = temp;
		start++;
		end--;
	}
}

//第二种方式 利用指针
void reverseString02(char * str)
{
	int len = strlen(str);

	//起始位置下标
	char * start = str;

	//结束位置的下标
	char * end = str + len - 1;

	while (start<end)
	{
		char temp = *start;
		*start = *end;
		*end = temp;
		start++;
		end--;
	}
}

void test03() {
	char str[] = "abcdefg";
	//reverseString01(str);
	reverseString02(str);
	printf("%s\n", str);
}

4.2、字符串的格式化

4.2.1、sprintf

int sprintf(char *str,const char *format,...);
功能:
    根据参数 format 字符串来转换并格式化数据,然后将结果输出到 str 指定的空间中,知道出现字符串结束符'\0'为止。
参数:
    str:字符串首地址
    format:字符串格式,用法和printf()一样
返回值:
    成功:实际格式化的字符个数
    失败:-1
void test04() {

	//1、格式化字符串
	char buf[1024] = { 0 };
	sprintf(buf, "你好,%s,欢迎加入我们!", "John");
	printf("buf:%s\n", buf);

	memset(buf, 0, 1024);
	sprintf(buf, "我今年%d岁了!", 20);
	printf("buf:%s\n", buf);

	//2、拼接字符串
	memset(buf, 0, 1024);
	char str1[] = "hello";
	char str2[] = "wolrd";
	int len = sprintf(buf, "%s%s", str1, str2);	//返回的是字符串的长度
	printf("buf:%s len:%d\n", buf, len);

	//3、数字转字符串
	memset(buf, 0, 1024);
	int num = 100;
	sprintf(buf, "%d", num);
	printf("buf:%s\n", buf);
	//设置宽度,右对齐
	memset(buf, 0, 1024);
	sprintf(buf, "%8d", num);
	printf("buf:%s\n", buf);
    //设置宽度,左对齐
    memset(buf, 0, 1024);
	sprintf(buf, "%-8d", num);
	printf("buf:%s\n", buf);
    //转成16进制字符串小写
    memset(buf, 0, 1024);
	sprintf(buf, "0x%x", num);
	printf("buf:%s\n", buf);
    //转成八进制字符串
    memset(buf, 0, 1024);
	sprintf(buf, "0%o", num);
	printf("buf:%s\n", buf);
}

4.2.2、sscanf

int sscanf(const char *str,const char *format,...);
功能:
    从str指定的字符串读取数据,并根据参数 format 字符串来转换并格式化数据。
参数:
    str:指定的字符串首地址
    format:字符串格式,用法和scanf()一样
返回值:
    成功:成功则返回参数数目
    失败:-1
格式作用
%*s 或 % *d跳过数据
%[width]s读指定宽度的数据
%[a-z]匹配 a 到 z 中任意字符(尽可能多的匹配)
%[aBc]匹配a、B、c中一员,贪婪性
%[ ^a ]匹配非a的任意字符,贪婪性
%[ ^a-z ]表示读取除a-z以外的所有字符
//1、%*s或%*d 跳过数据
void test05() {
	char * str = "12345abcde";
	char buf[1024] = { 0 };
	sscanf(str,"%*d%s",buf);
	printf("%s\n", buf);
}

void test06() {
	char * str = "12345abcde";	//在中间加空格或者\t都可实现效果
	char buf[1024] = { 0 };
	//sscanf(str, "%*s%s", buf);
	sscanf(str, "%*[a-z]%s", buf);
	printf("%s\n", buf);
}

//2、%[width]s 读取指定宽度的数据
void test07() {
	char * str = "abcde12345";	//在中间加空格或者\t都可实现效果
	char buf[1024] = { 0 };
	sscanf(str, "%6s", buf);
	printf("%s\n", buf);
}

//3、%[a-z]	匹配 a 到 z 中任意字符(尽可能多的匹配)
void test08() {
	char * str = "12345abcde";	//在中间加空格或者\t都可实现效果
	char buf[1024] = { 0 };
	sscanf(str, "%*d%[a-c]", buf);
	printf("%s\n", buf);	//abc
}
//4、%[aBc]	匹配a、B、c中一员,贪婪性
void test09() {
	char * str = "aabcde12345";	//在中间加空格或者\t都可实现效果
	char buf[1024] = { 0 };
	sscanf(str, "%[aBc]", buf);	//在匹配过程中,只要有一个匹配失败,后续就不在进行匹配了
	printf("%s\n", buf);	//aa
}

//5、%[ ^a ]	匹配非a的任意字符,贪婪性
void test10() {
	char * str = "aabcde12345";	//在中间加空格或者\t都可实现效果
	char buf[1024] = { 0 };
	sscanf(str, "%[^c]", buf);	//在匹配过程中,只要有一个匹配失败,后续就不在进行匹配了
	printf("%s\n", buf);	//aab
}

//6、%[ ^a-z ]	表示读取除a-z以外的所有字符
void test11() {
	char * str = "aabcde12345";	//在中间加空格或者\t都可实现效果
	char buf[1024] = { 0 };
	sscanf(str, "%[^0-9]", buf);	//在匹配过程中,只要有一个匹配失败,后续就不在进行匹配了
	printf("%s\n", buf);	//aabcde
}

5、一级指针易错点

5.1、越界

void test(){
	char buf[3] = "abc";
	printf("buf:%s\n",buf);
}

5.2、指针叠加会不断改变指针指向

void test(){
	char *p = (char *)malloc(50);
	char buf[] = "abcdef";
	int n = strlen(buf);
	int i = 0;

	for (i = 0; i < n; i++)
	{
		*p = buf[i];
		p++; //修改原指针指向
	}

	free(p);
}

5.3、返回局部变量地址

char *get_str()
{
	char str[] = "abcdedsgads"; //栈区,
	printf("[get_str]str = %s\n", str);
	return str;
}

5.4、同一款内存释放多次(不可以释放野指针)

void test(){	
	char *p = NULL;

	p = (char *)malloc(50);
	strcpy(p, "abcdef");

	if (p != NULL)
	{
		//free()函数的功能只是告诉系统 p 指向的内存可以回收了
		// 就是说,p 指向的内存使用权交还给系统
		//但是,p的值还是原来的值(野指针),p还是指向原来的内存
		free(p); 
	}

	if (p != NULL)
	{
		free(p);
	}
}

6、const使用

//const修饰变量
void test01(){
	//1. const基本概念
	const int i = 0;
	//i = 100; //错误,只读变量初始化之后不能修改

	//2. 定义const变量最好初始化
	const int j;
	//j = 100; //错误,不能再次赋值

	//3. c语言的const是一个只读变量,并不是一个常量,可通过指针间接修改
	const int k = 10;
	//k = 100; //错误,不可直接修改,我们可通过指针间接修改
	printf("k:%d\n", k);
	int* p = &k;
	*p = 100;
	printf("k:%d\n", k);
}

//const 修饰指针
void test02(){

	int a = 10;
	int b = 20;
	//const放在*号左侧 修饰p_a指针指向的内存空间不能修改,但可修改指针的指向
	const int* p_a = &a;
	//*p_a = 100; //不可修改指针指向的内存空间
	p_a = &b; //可修改指针的指向

	//const放在*号的右侧, 修饰指针的指向不能修改,但是可修改指针指向的内存空间
	int* const p_b = &a;
	//p_b = &b; //不可修改指针的指向
	*p_b = 100; //可修改指针指向的内存空间

	//指针的指向和指针指向的内存空间都不能修改
	const int* const p_c = &a;
}
//const指针用法
struct Person{
	char name[64];
	int id;
    int age;
	int score;
};

//每次都对对象进行拷贝,效率低,应该用指针
void printPersonByValue(struct Person person){
	printf("Name:%s\n", person.name);
	printf("Name:%d\n", person.id);
	printf("Name:%d\n", person.age);
	printf("Name:%d\n", person.score);
}

//但是用指针会有副作用,可能会不小心修改原数据
void printPersonByPointer(const struct Person *person){
	printf("Name:%s\n", person->name);
	printf("Name:%d\n", person->id);
	printf("Name:%d\n", person->age);
	printf("Name:%d\n", person->score);
}
void test03(){
	struct Person p = { "Obama", 1101, 23, 87 };
	//printPersonByValue(p);
	printPersonByPointer(&p);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Geek@Yang

码字不易,来点鼓励~~~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值