C语言进阶(八) - 指针进阶

前言

指针是一个变量,用来存放地址。而地址是内存单元的编号。
指针的大小是4字节或8字节。
指针具有多种类型,不同类型的指针看待内存单元的视角不同,管理的内存单元的大小也有差异。
指针可以进行加减运算、比较大小等运算。

接下来是更加详细的的指针专题!

1. 字符指针

1.1 用法

是指向字符的指针,用来存放字符的地址。
重点在后头。

用法1:存放单个字符的地址并解引用。

#include <stdio.h>

int main(){
    char ch = 'a';
    char* p = &ch;
    *p = 'b';
    printf("%c", ch);
    return 0;
}

用法2:存放字符串的首元素的地址,可以找到该字符串。

#include <stdio.h>
#include <string.h>

int main(){
    const char *p = "abcdef";//这是字符串常量,不能被改变,所以用const修饰人为防止修改。
    int len = strlen(p);
    printf("%s", p);
    return 0;
}

注意:一个字符指针只能存放一个地址,存不下整个字符串。

1.2 一个例子

#include <stdio.h>

int main(){
    char* p1 = "abcdef";
    char* p2 = "abcdef";
    if(p1 == p2){
        printf("p1 == p2\n");   
    }
    else{
        printf("p1 != p2\n");
    }
    
    char str1[] = "abcdef";
    char str2[] = "abcdef";
    if(str1 == str2){
        printf("str1 == str2\n");
    }
    else{
        printf("str1 != str2\n");
    }
    return 0;
}

运行结果:
P715YVL9X{F9{7GBEP5D4HE.png
结果分析:

"abcdef"是字符串常量,创建之后就不能够改变,其在内存中是唯一的一份,字符指针p1与p2均指向了字符串常量的首元素,二者相等。
字符数组则有些不同,str1[[]与str2[]是两个不同的数组,二者在内存空间中占据着不同的位置,数组名是数组首元素的地址,str1与str2是不同的数组名,二者不同。

字符指针1.png字符指针2.png


2. 指针数组

是存放指针的数组。
重点在后头。

2.1 用法

#include <stdio.h>

int main(){
    //(一级)整型指针数组
    int* arr1[10] = {0};
    //二级整型指针数组
    int** arr2[10] = {0};
    //一级字符指针数组
    char* ch1[10] = {0};
    //二级字符指针数组
    char** ch2[10] = {0};
    
    return 0;
}

2.2 一个例子 - 关联几个一维数组

#include <stdio.h>

int main() {
    int arr1[5] = { 1,2,3,4,5 };
    int arr2[5] = { 2,3,4,5,6 };
    int arr3[5] = { 3,4,5,6,7 };
    int* parr[3] = { arr1, arr2, arr3 };
    int i = 0;
    for (i = 0; i < 3; i++) {
        int j = 0;
        for (j = 0; j < 5; j++) {
            printf("%d ", parr[i][j]);//*(parr[i]+j)
        }
        printf("\n");
    }
    return 0;
}

运行结果:
7S8O4FWRW%RPLX1QB6DYB.png

arr1、arr2、arr3这三个数组并不一定连续,与二维数组是连续的有着本质不同。

指针数组_1.png


3. 数组指针

是指向数组的指针,用来存放数组的地址。
重点在后头。

3.1 形式

例如:int (*p)[5]

解释:看见小括号,p1小于*结合,故p1首先是一个指针,指向一个数组,该数组有5个元素,每个元素的类型都是int。
p的类型是去掉p之后的部分 ,即int (*)[5]

注意:[]的优先级高于*

3.2 再探数组名

数组名一般情况下指的是数组首元素的地址
除了两种特殊情况下指的是整个数组。

  1. sizeof(数组名),此时数组名单独出现在sizeof运算符内部,表示整个数组,计算的是整个数组的大小。
  2. &数组名,此时数组名表示整个数组,取出的是整个数组的地址,只是数组上等于数组首元素的地址。

举例说明:

#include <stdio.h>

int main(){
    int arr[10] = {0};
    printf("sizeof(arr[0]) == %d\n", sizeof(arr[0]));
    printf("sizeof(arr) == %d\n", sizeof(arr));
    //-------------------------------------------------
    printf("arr == %p\n", arr);
    printf("arr + 1 %p\n", arr + 1);
    printf("arr[0] == %p\n", arr[0]);
    printf("arr[0] + 1%p\n", arr[0] + 1);
    printf("&arr == %p\n", &arr);
    printf("&arr + 1%p\n", &arr + 1);
    
    return 0;
}

运行结果:
GE8M7D47TS6ZCGZ@{N_)$VC.png
数组名特殊情况画图.png

3.3 使用数组指针

  1. 指向一维数组:
#include <stdio.h>

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int (*p)[10] = &arr;
    int i = 0;
    for(i=0; i<10; i++){
        printf("%d ", *(*p + i));//
    }
    return 0;
}

运行结果:
.png

p是整个数组的地址,*p就是整个数组,相当于数组名arr,也就是数组首元素的地址,**p就是数组首元素arr[0]。

  1. 指向二维数组

正常二维数组使用:

#include <stdio.h>

void prints(int arr[3][4], int row, int col) {
    int i = 0;
    for (i = 0; i < row; i++) {
        int j = 0;
        for (j = 0; j < col; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main() {
    int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    prints(arr, 3, 4);

    return 0;
}

使用数组指针:

#include <stdio.h>

void prints(int (*p)[4], int row, int col) {
    int i = 0;
    for (i = 0; i < row; i++) {
        int j = 0;
        for (j = 0; j < col; j++) {
            printf("%d ", *(*(p+i)+j));//p[i][j]
        }
        printf("\n");
    }
}
int main() {
    int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
    prints(arr, 3, 4);

    return 0;
}

运行结果:
.png

3.4 二维数组的首元素探讨

二维数组的首元素是一维数组。
二维数组在内存中实际上是以一维数组的方式进行储存的。

二维数组真实情况.png

3.5 数组指针数组

是存放数组指针的数组。

#include <stdio.h>

int main(){
    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};
    int (*arr[3])[5] = {&arr1, &arr2, &arr3};
    
    return 0;
}

4. 数组参数与指针参数

4.1 一维数组传参

#include <stdio.h>
void test_1(int arr[10]){
//正确
}
void test_2(int arr[]){
//正确
}
void test_3(int* arr){
//正确
}
int main(){
    int arr[10] = {0};
    test_1(arr);
    test_2(arr);
    test_3(arr);
    return 0;
}
#include <stdio.h>
void test_1(int* arr[10]){
//正确
}
void test_2(int** arr){
//正确
}

int main(){
    int* arr[10] = {0};
    test_1(arr);
    test_2(arr);
    return 0;
}

4.2 二维数组传参

多维数组作为参数时,只能够省略第一维的数,也就是以一个方框的数。

#include <stdio.h>
void test_1(int arr[3][4]){
//正确
}
void test_2(int arr[][4]){
//正确
}
void test_3(int arr[][]){
//错误!
}
void test_4(int* arr){
//错误!
}
void test_5(int* arr[4]){
//错误!
}
void test_6(int (*arr)[4]){
//正确
}
void test_7(int** arr){
//错误!
}
int main(){
    int arr[3][4];
    test_1(arr);
    test_2(arr);
    test_3(arr);
    test_4(arr);
    test_5(arr);
    test_6(arr);
    test_7(arr);
    return 0;
}

4.3 一级指针传参

#include <stdio.h>

void test(int *p){
   
}

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int* p = arr;
    test(p);
    return 0;
}

当函数参数是一级指针时,传递的数据可以有哪些情况?

void test(int *p){}
可以是一级指针、一维数组的数组名、&变量名、二级指针解引用。

4.4 二级指针传参

#include <stdio.h>
void test(int** p){

}
int main(){
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    test(ppa);
    test(&pa)
    return 0;
}

当函数参数是二级指针时,传递的数据可以有那些情况?

void test(int** p){}
可以是二级指针int **p
一级指针的地址、指针数组的数组名int* arr[5]


5. 函数指针

是指向函数的指针,用来存放函数的地址。
一个函数在编译期间就分配了地址。

5.1 形式

int (*p)(int,int);
首先是一个指针p,指向一个函数,这个函数的参数有两个,均是int类型,这个函数的返回类型是int类型。

5.2 类比数组指针

函数名就是函数的地址,&函数名也是函数的地址,故二者等价。

#include <stdio.h>

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

int main(){
    printf("%p", &Add);
    printf("%p", Add);
    
    return 0;
}

运行结果:
.png

&Arr - 取出了数组的地址
&Add - 取出了函数的地址

#include <stdio.h>

int Add(int x, int y){
    return x + y;
}
int main(){
    int arr[10] = {0};
    int (*p1)[10] = &arr;
    //
    int (*p)(int,int) = &Add;//int (*p)(int, int) = Add
    return 0;
}

5.3 函数指针的使用方式分析


#include <stdio.h>

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

int main() {
    int (*p)(int, int) = &Add;//int (*P)(int, int) = Add
    int x = 3;
    int y = 4;

    //直接函数调用
    int ret1 = Add(x, y);
    //通过函数指针的函数调用
    int ret2 = (*p)(x, y);
    int ret3 = p(x, y);//由函数直接调用知,其直接使用的是函数名-也就是函数的地址,而p中存放的就是函数的地址,故p与函数名等价
    //由此,*在此处函数调用下不起作用,可以没有,也可以有多个。
    int ret4 = (*****p)(x, y);

    int ret5 = (*Add)(x, y);

    printf("ret1 == %d\n", ret1);
    printf("ret2 == %d\n", ret2);
    printf("ret3 == %d\n", ret3);
    printf("ret4 == %d\n", ret4);
    printf("ret5 == %d\n", ret5);
    return 0;
}

运行结果:
在这里插入图片描述

5.4 函数指针拓展分析

(* ( void (*)() ) 0)();
这是一次函数调用,调用了0地址处的函数。
整数0被一个函数指针void (*)()强制类型转换为该函数指针类型,之后解引用间接对0地址处的函数进行调用。
其中这个函数的没有形参,返回类型为void。

void (* signal( int , void (*)(int) ) )(int);
这是一个函数声明。
函数名是signal,形参一个是int;另一个是函数指针void (*)(int),该函数指针指向一个形参为int,返回类型为void的函数。
返回类型又是一个函数指针void (*)(int),该函数指针指向一个形参为int,返回类型为void的函数。

使用typedef对函数指针void (*)(int)类型进行重命名,简化上面的函数声明:

//对void (*)(int)类型重命名为pf
typedef void (* pf)(int);
//对void (* signal( int , void (*)(int) ) )(int)进行化简
pf signal(int, pf);

5.5 函数指针的用途举例:

实现一个简单的计算器

  1. 未使用函数指针版本
#include <stdio.h>
void menu() {
    printf("****************************\n");
    printf("****   1.Add    2.Sub   ****\n");
    printf("****   3.Mul    4.Div   ****\n");
    printf("*******   0.Exit   *********\n");
    printf("****************************\n");
}
int Add(int x, int y) {
    return x + y;//加法
}

int Sub(int x, int y) {
    return x - y;//减法
}

int Mul(int x, int y) {
    return x * y;//乘法
}

int Div(int x, int y) {
    return x / y;//除法
}

int main() {
    int input = 0;
    int x, y;
    int ret = 0;
    do {
        menu();
        printf("请输入你的选择>");
        scanf("%d", &input);
        switch (input) {
        case 1:
            printf("请输入两个操作数>");
            scanf("%d %d", &x, &y);
            ret = Add(x, y);
            printf("%d\n", ret);
            break;
        case 2:
            printf("请输入两个操作数>");
            scanf("%d %d", &x, &y);
            ret = Sub(x, y);
            printf("%d\n", ret);
            break;
        case 3:
            printf("请输入两个操作数>");
            scanf("%d %d", &x, &y);
            ret = Mul(x, y);
            printf("%d\n", ret);
            break;
        case 4:
            printf("请输入两个操作数>");
            scanf("%d %d", &x, &y);
            ret = Div(x, y);
            printf("%d\n", ret);
            break;
        case 0:
            printf("程序退出\n");
            break;
        default:
            printf("输入错误,请重新输入!\n");
                break;
        }
    } while (input);

    return 0;
}

注意到switch语句内部case1到case4的代码太相似,显得代码有些臃肿。

把这些相似的代码封装成一个函数,用函数指针来接收,达到简化代码的目的。

#include <stdio.h>
void menu() {
    printf("****************************\n");
    printf("****   1.Add    2.Sub   ****\n");
    printf("****   3.Mul    4.Div   ****\n");
    printf("*******   0.Exit   *********\n");
    printf("****************************\n");
}
int Add(int x, int y) {
    return x + y;//加法
}

int Sub(int x, int y) {
    return x - y;//减法
}

int Mul(int x, int y) {
    return x * y;//乘法
}

int Div(int x, int y) {
    return x / y;//除法
}
//封装的一个函数
void calculate(int (*p)(int, int)){
    printf("请输入两个操作数>");
    int x,y;
    scanf("%d %d", &x, &y);
    int ret = p(x, y);
    printf("%d\n", ret);
}
int main() {
    int input = 0;
    do {
        menu();
        printf("请输入你的选择>");
        scanf("%d", &input);
        switch (input) {
        case 1:
            calculate(Add);
            break;
        case 2:
            calculate(Sub);
            break;
        case 3:
            calculate(Mul);
            break;
        case 4:
            calculate(Div);
            break;
        case 0:
            printf("程序退出\n");
            break;
        default:
            printf("输入错误,请重新输入!\n");
                break;
        }
    } while (input);

    return 0;
}

6. 函数指针数组

是存放函数指针的数组。

6.1 形式

int (* arr[10])(int, int);其中arr是数组名。
解释:arr先与[10]结合,是一个数组,这个数组有10个元素,每一个元素都是一个函数指针。
这个函数指针类型是int (*)(int, int),指向一个有两个int类型形参,返回类型是int类型的函数。

6.2 用途 - 再次改进计算器

使用函数指针数组存放Add( )、Sub( )、Mul( )、Div( )函数,不再使用switch语句。

#include <stdio.h>
void menu() {
    printf("****************************\n");
    printf("****   1.Add    2.Sub   ****\n");
    printf("****   3.Mul    4.Div   ****\n");
    printf("*******   0.Exit   *********\n");
    printf("****************************\n");
}
int Add(int x, int y) {
    return x + y;//加法
}

int Sub(int x, int y) {
    return x - y;//减法
}

int Mul(int x, int y) {
    return x * y;//乘法
}

int Div(int x, int y) {
    return x / y;//除法
}

int main() {
    int input = 0;
    //函数指针数组
    int (* arr[10])(int, int) = {0, Add, Sub, Mul, Div};
    int x, y;
    int ret = 0;
    do {
        menu();
        printf("请输入你的选择>");
        scanf("%d", &input);
        
        if(input > 0 && input < 5){
            printf("请输入两个操作数>");
            scanf("%d %d", &x, &y);
            ret = (arr[input])(x, y);
            printf("%d\n", ret);
        }
        else if(input == 0){
            printf("程序退出!\n");
        }
        else{
            printf("输入错误,请重新输入!\n");
        }
    } while (input);

    return 0;
}

7. 指向函数指针数组的指针

是一个指针。
指向一个数组。
数组的元素都是函数指针,也就是函数的地址

7.1 形式

函数指针数组:
int (*arr[10])(int, int) = {Add, Sub};其中Add、Sub是函数名。
指向函数指针数组的指针:
int (* (*parr)[10])(int, int) = &arr;


8. 回调函数

8.1 是什么

回调函数是一个通过函数指针调用的函数。
当你把函数的指针(或地址)作为参数传递给另一个函数,这个指针被用来调用其所指向的函数时,就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,对该事件或条件进行反馈。

8.2 qsort函数初识

8.2.1 函数原型

void qsort(void* base, //待排序的数据的起始地址 
           size_t num, //待排序的数据元素个数
           size_t size, //待排序的单个数据元素的大小(以字节为单位)
           int (*comper)(const void* e1, const void* e2));
           //函数指针,指向一个比较函数,该函数的形参是const void* e1和const void* e1,返回 			  //类型是int。

qsort函数可以用来比较任意相同类型的数据,具体比较哪一种类型的数据由使用者自己决定。
决定的方式便通过比较函数实现,这个比较函数需要由使用者写出。
这个比较函数可以接收两个地址,并返回一个整数。

void*类型的指针作为形参可以接收任意类型的指针,是一种泛型指针,也可以说是垃圾箱-来者不拒。
比较函数内部,对e1与e1所指向的数据进行比较之前需要先进行强制类型转换,否则出错。而强制类型转换为哪一种类型由待比较的数据的类型决定。

qsort函数排序默认结果是升序排列
默认返回情况如下:
e1所指向数据大于e2所指向数据返回1;
e1所指向数据等于e2所指向数据返回0;
e1所指向数据小于e2所指向数据返回-1。

8.2.2 void* 类型的指针

  • void*是无具体类型的指针,可以接收任意类型的地址。
  • 不能对void*类型的指针直接进行解引用操作、等指针运算操作。

8.2.3 具体使用qsort函数

qsort是基于快速排序思想写成的函数,头文件是stdlib.h

对整型数据进行排列:

#include <stdio.h>
int cmp_int(const void* e1, const void* e2) {
    return *(int*)e1 - *(int*)e2;
}
void test_int() {
    int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
    
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main() {
    test_int();
    return 0;
}

运行结果:
%{​{LNMR7%DO_@RV@J0GHIMS.png
对结构体类型数据进行排列:

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

typedef struct student {
    char name[20];
    int num;
}STU;
//比较结构体中的字符串
int cmp_name(const void* e1, const void* e2) {
    return strcmp(((STU*)e1)->name, ((STU*)e2)->name);
}
void test_struct_name() {
    STU s[] = { {"sunwukong", 2}, {"shasheng", 4}, {"tangsheng",1},{"zhubajie",3} };
    int sz = sizeof(s) / sizeof(s[0]);
    int i = 0;
    printf("前\n");
    for (i = 0; i < sz; i++) {
        printf("%s %d\n", s[i].name, s[i].num);
    }

    qsort(s, sz, sizeof(s[0]), cmp_name);

    printf("后\n");
    for (i = 0; i < sz; i++) {
        printf("%s %d\n", s[i].name, s[i].num);
    }
    return 0;
}

//比较结构体中的整型
int cmp_num(const void* e1, const void* e2) {
    return ((STU*)e1)->num - ((STU*)e2)->num;
}
void test_struct_num() {
    STU s[] = { {"sunwukong", 2}, {"shasheng", 4}, {"tangsheng",1},{"zhubajie",3} };
    int sz = sizeof(s) / sizeof(s[0]);
    int i = 0;
    printf("前\n");
    for (i = 0; i < sz; i++) {
        printf("%s %d\n", s[i].name, s[i].num);
    }

    qsort(s, sz, sizeof(s[0]), cmp_num);

    printf("后\n");
    for (i = 0; i < sz; i++) {
        printf("%s %d\n", s[i].name, s[i].num);
    }
    return 0;
}

int main() {
    test_struct_name();
    printf("----------------\n");
    test_struct_num();
    return 0;
}

运行结果:
.png

8.2.4 采用冒泡排序思想模拟实现qsort函数的功能

冒泡排序思想:
快速图解:
bubble_sort_0.1_快速图解.gif

慢速图解:
bubble_sort_0.5_慢速图解.gif
一个例子:

#include <stdio.h>
void bubble_sort(int arr[], int sz) {
    int i = 0;
    int flag = 0;//默认数据已经升序排列,不需要交换
    //趟数
    for (i = 0; i < sz - 1; i++) {
        int j = 0;
        //比较次数
        for (j = 0; j < sz - 1 - i; j++) {
            //比较
            if (arr[j] > arr[j + 1]) {
                //交换
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                //交换过,flag要改变
                flag = 1;
            }
        }
        //如果第一趟为交换,那么之后的几趟也不会交换,直接退出!
        if(flag == 0){
            break;
        }
    }
}
int main() {
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    bubble_sort(arr, sz);
    
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

模拟实现:

void Swap(char* buf1, char* buf2, int size) {
    int i = 0;
    for (i = 0; i < size; i++) {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}
//
void bubble_sort(void* base,
                 size_t num, 
                 size_t size, 
                 int (*cmp)(const void* e1, const void* e2)) {
    int i = 0;
    //趟数
    for (i = 0; i < num - 1; i++) {
        int j = 0;
        int flag = 0;//默认数据已经升序排列,不需要交换
        //比较次数
        for (j = 0; j < num - 1 - i; j++) {
            //比较,
            if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
                //交换
                Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
                //交换过,flag要改变
                flag = 1;
            }
        }
        //如果第一趟为交换,那么之后的几趟也不会交换,直接退出!
        if(flag == 0){
            break;
        }
    }
}

注意比较部分
cmp((char*)base + j * size, (char*)base + (j + 1) * size)

作为设计者,应该想到作为一个通用的排序函数,可以比较事先未知的数据类型。既然是未知的数据类型那么就不能仅仅使用某一种特定的数据类型直接对指针解引用找到某一个元素,而是把指针base强制类型转换为字符指针,从而借助单个数据元素大小size找到相应的元素。

注意交换部分:
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);

void Swap(char* buf1, char* buf2, int size) {
    int i = 0;
    for (i = 0; i < size; i++) {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

作为设计者,既然实现不能事先知道排序的是哪一种类型,那要实现交换两个元素通过两个字符指针分别指向两个元素,以字节为单位交换这两个元素对应的内容,这样就不再需要考虑是哪一种数据类型,直接完成交换。


9. 结语

本节主要涉及了指针的一些拓展知识,从一般的一级指针开始、逐渐到指针数组、数组指针、函数指针、函数指针数组。另外还了解了回调函数,模拟实现了qsort函数并很好地完成了功能。


END

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

re怠惰的未禾

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值