C语言----指针基础详解

前言:本文为一篇面向C语言初学者的分享性随笔,主要内容:指针、指针运算、指针与数组、多级指针、指针数组等。如有错误,请私信指正。

一、指针基础

在计算机的内存中,每一个字节单元(可存储8个bit的数据),都有一个独特的编号,即地址,内存单元使用这个地址来标识自身的位置,内存单元的地址就称作指针。而专门用来存放内存单元地址的变量,称为指针变量,在不影响理解的情况下,常对地址、指针和指针变量不作区分,统称为指针。

在32位的计算机中,地址的字宽是32位,因此指针也是32位的,即4个字节,由于计算机内存的地址都是一样的宽度,所以无论何种类型的指针都是32位宽度;同理,在64位计算机中,指针的字宽为64位,即8个字节;我们可以在随后使用sizeof指令验证这一点。

1.1、指针的定义

指针变量和其他变量一样,使用之前需要先进行定义,示例如下:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int *p1; //p1为一个指向整型变量的指针变量
	float *p2; //p2为一个指向浮点型变量的指针变量
	char *p3; //p3为一个指向字符型变量的指针变量
	return 0;
}

如上所示,一般使用类型说明符 *变量名的形式进行定义,其中,类型说明符表示本指针变量所指向的变量的数据类型类型说明符后的 * 表示这是一个指针变量,变量名即为自己定义的指针变量名。
需要注意的是,指针类型和指针指向的类型是两个不同的概念,如int *p;,指针的类型为int *,而指针所指向的类型为int。
指针的类型即为去掉指针变量名后剩余的部分,指针指向类型为去掉指针声明符“ * 和指针变量名”的部分,这点需要牢记。

同时,我们可以使用sizeof指令验证各类指针变量的大小,如下:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	printf("%d %d %d\n",sizeof(int *),sizeof(float *),sizeof(char *));
	return 0;
}

所输出的结果为:8 8 8(64位机环境),可见,所有类型的指针变量所占空间大小都相等,都为8个字节。

注意: ① 指针变量的变量名不包括*号,如int *p1; 此整型指针变量的变量名为p,而不是 *p
② 虽然所有指针变量占用的大小都相同,但仍需定义指针的类型说明符,因为在之后的指针运算中都涉及指针所指向变量的数据宽度;同时,需要注意,一个指针变量只能指向同类型的变量,如上例中的p1只能指向整型变量。

1.2、指针的赋值

指针变量在定义后需要进行赋值才能使用,未经赋值的指针变量随意使用会造成程序发生段错误。上面已经知道,指针变量的值实际上是其他变量的地址,因而对指针变量进行赋值时,同样需要使用地址。

在C语言中,我们可以使用运算符“&”来表示变量的地址,如 &a 即表示变量a的地址。我们可以使用如下方式对指针变量进行引用赋值:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int *p = &a;
	printf("a的值和地址为:%d %p\n p的值和地址为:%p %p\n",a,&a,p,&p);
	return 0;
}

所输出的结果为:
a的值和地址为:10 0x7ffc2696660c
p的值和地址为:0x7ffc2696660c 0x7ffc26966610

可见,整型指针变量p中所存放的值实际上为整型变量a的地址,我们通过上面的程序将整型变量a的地址赋值给了指针p。

这里有一个很容易进入的误区,如上例结果所示,p的值和地址为:0x7ffc2696660c 0x7ffc26966610 ,两个地址之间相差4个字节,但在64位机中,所有类型的指针变量所占空间都为8个字节,这是为什么?因为第一个地址0x7ffc2696660c实际上是整型变量a的地址,而不是指针变量的地址,整型变量占用4个字节的空间,即0x7ffc2696660c、0x7ffc2696660d、0x7ffc2696660e、0x7ffc2696660f;而指针变量占用的空间实际为
0x7ffc26966610 - 0x7ffc26966617,共8个字节。

1.3、指针的解引用

指针变量中存储的地址即为指针的指向,而指针指向的内存区域中的数据即为指针的目标。如果指针指向的区域是程序中某个变量的内存空间,则这个变量为指针的目标变量。而对指针目标的控制,则需要用到 &* 两个运算符。
& 即为取地址运算符,如 &a 的作用即为取变量a的地址,而 * 为指针运算符,也称作解引用,如 *p 的作用为取指针变量p所指向的存储单元里的内容
我们举一个例子来理解这两个运算符的功能:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	int *p = &a;
	printf("a = %d *p = %d\n",a,*p);
	*p = 15;
	printf("a = %d *p = %d\n",a,*p);
	p = &b;
	printf("b = %d *p = %d\n",*p);
	return 0;
}

所输出的结果为:
a = 10 *p = 10
a = 15 *p = 15
b = 20 *p = 20
上例中,首先使用&将指针p指向a的地址,此时p中存放的值为整型变量a的地址,随后对p进行解引用,*p,即取指针变量p中存储的地址里的内容,即a的地址中的内容,即为10;随后在解引用的同时对其进行赋值,即取a的地址中的内容,并对其赋值为15,所以此时a = 15,*p = 15;最后将指针变量p指向b的地址,再次进行解引用,得到的是b的值,即 *p = 20 。

在引用指针时,常会出现如下错误:

int *p;
*p = 10;

上例中虽然定义了指针变量p,但并没有对其进行初始化,此时指针p指向一个未知的对象,即变量p的值是不确定的,这样的指针被称为 野指针,这种代码在执行时通常会出现段错误,原因是使用了一个非法的地址。

二、指针的运算

2.1、指针算术运算

运算形式意义
p+n指针向地址大的的方向移动n个数据
p-n指针向地址小的的方向移动n个数据
p++、++p指针向地址大的的方向移动1个数据
p- -、- -p指针向地址小的的方向移动1个数据
p - q两个指针间相隔数据的个数

需要注意的是,这里的指针移动的单位不是1个字节,而是指针指向类型的数据长度,如p+n表示的实际内存单元地址为:p + sizeof(p指向的类型) * n;而p - q的运算结果是两个指针间相隔数据的个数,如int *p - int *q = (p - q)/sizeof(int),这个值可以是负值。

示例如下:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	double b = 20;
	int *p = &a;
	double *q = &b;
	printf("a = %d &a = %p\n",a,&a);
	printf("p = %p p+1 = %p\n",p,p+1);

	printf("b = %lf &b = %p\n",b,&b);
	printf("q = %p q+1 = %p\n",q,q+1);
	return 0;
}

输出的结果为:
a = 10 &a = 0x7ffc855523d0
p = 0x7ffc855523d0 p+1 = 0x7ffc855523d4
b = 20.000000 &b = 0x7ffc855523d4
q = 0x7ffc855523d4 q+1 = 0x7ffc855523dc
因为int的数据长度为4个字节,double的数据长度为8个字节,所以整型指针p+1地址增加了4,而双精度型指针q+1地址增加了8。

※ 不同数据类型的指针进行运算是无意义的。

2.2、指针关系运算

两指针间的关系运算,即表示他们指向的地址间的关系运算,指向地址大的指针大于指向地址小的指针。
我们举个例子来说明指针间的关系运算:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	char a[] = "Solahalo";
	char *p = a;
	char *q = a +strlen(a) - 1; //q指向字符串的最后一位
	printf("p = %p  q = %p\n",p,q);
	if(p < q)
		printf("p < q\n");
	else if(p > q)
		printf("p > q\n");
	else if(p == q)
		printf("p = q\n");
	return 0;
}

所输出的结果为:
p = 0x7ffce899b14f q = 0x7ffce899b156
p < q
可以看到,p是字符数组a的首地址,即‘s’的地址,q为字符‘o’的地址,q的指向地址较大,所以p < q。

2.3、空指针

空指针即是把零号地址存入指针变量的指针,可以使用int *p = 0;或int *p = NULL;定义空指针,它可以用来表明一个指针目前没有指向任何对象。
以下语句为示例:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int *p = NULL;
	printf("%d\n",*p);
	*p = 20;
	return 0;
}

上面的程序中,p就是一个空指针,但访问零号地址存储的值或修改它都是不允许的,运行该程序时,一定会出现段错误,除非另行对指针变量p进行赋值,我们可以用这种方法预先纠正野指针的错误。

*三、指针与数组

3.1、指针与一维数组

3.1.1、数组的指针

数组,就是具有一定顺序关系的若干变量的集合,它占用的存储空间是连续的,这个集合中的每个变量被称为数组的元素。
数组元素的地址是值数组元素在内存中的起始地址,由各个元素加上取地址符号 & 构成,如&a[0]就表示数组中第一个元素的地址,&a[1]表示第二个元素的地址,依此类推。
对于一个数组来说,数组名就代表了数组的起始地址。下面这两个表达式的值是相等的:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a[10];
	printf("a = %p  &a[0] = %p\n",a,&a[0]);
	return 0;
}

输出的结果为:
a = 0x7ffc1811f980 &a[0] = 0x7ffc1811f980

这里会引出一个数组指针的概念,数组指针就是指向数组起始地址的指针,它的本质仍是指针,一维数组的数组名即为一维数组的指针。

3.1.2、数组元素的表示

上文已经讲过,指针进行加减等运算的方法,配合解引用操作,就可以使用指针表示数组元素,如上例:a + 2 即为指针a后的第二个对象,计算方法为 a + sizeof(int) * 2,即将指针a向较大方向移动8个字节,在上例中,a+2的地址应为 0x7ffc1811f988;随后,使用解引用操作 *(a+2) ,此操作即为取指针a后面第二个对象内的值,它的结果和a[2]是一致的。

因此,对于数组而言,指针相加就相当于依次指向数组中的下一个元素。
下面举个例子来说明上述问题:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a[] = {1,3,5,7,9},i;
	int *p = a;
	int n = sizeof(a)/sizeof(int)for(i = 0;i < n;i++)
		printf("%d %d %d %d\n",a[i],*(a+i),p[i],*(p+i));
	return 0;
}

输出的结果为:
1 1 1 1
3 3 3 3
5 5 5 5
7 7 7 7
9 9 9 9
由此可见,上述四种表示方式a[i], *(a+i) ,p[ i ] , *(p+i)所得到的结果都是一致的。a[ i ]、p[ i ] 的表示方法称为下标法, *(a+1)的方法称作指针法。

3.2、指针与二维数组

3.2.1、一级指针(列指针)与二维数组

二维数组就是具有两个下标的数组。在C语言中没有多维数组的概念,实际上它就是两个一维数组的组合。

在C语言中,二维数组的元素也是连续存储的,按行优先存储,第一行存完后,存储第二行的,依此类推,所以仍然可以使用一级指针来访问二维数组,示例如下:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a[2][3] = {1,3,5,7,9,11},i,j;
	int *p = &a[0][0];
	int h = sizeof(a)/sizeof(a[0]); //行数
	int l = sizeof(a[0])/sizeof(int); //列数
	int n = sizeof(a)/sizeof(int); //元素个数

	for(i = 0;i < h;i++)
		for(j = 0;j < l;j++)
			printf("%d  %p\n",a[i][j],&a[i][j]); //下标法
	printf("\n");

	for(i = 0;i < n;i++)
		printf("%d  %p\n",*(p+i),p+i); //列指针
	return 0;
}

输出的结果为:
1 0x7ffd25894190
3 0x7ffd25894194
5 0x7ffd25894198
7 0x7ffd2589419c
9 0x7ffd258941a0
11 0x7ffd258941a4

1 0x7ffd25894190
3 0x7ffd25894194
5 0x7ffd25894198
7 0x7ffd2589419c
9 0x7ffd258941a0
11 0x7ffd258941a4
可以看出,一级指针p在进行加法时,例如p+1,相当于指向从第一行第一列移动到了第一行第二列,因此也称指针p为列指针。

3.2.2、数组指针(行指针)与二维数组

从另外一个角度来理解二维数组,将二维数组看作由两个一维数组组成,如上例的int a[2][3],可以理解成含有两个元素a[0]、a[1]的数组;元素a[0]是一个一维数组名,含有a[0][1]、a[0][1]、a[0][2]三个元素,即二维数组的第一行,元素a[1]也是一个一维数组名,含有a[1][0]、a[1][1]、a[1][2]三个元素,即二维数组的第二行。

二维数组名同样代表了数组的起始地址,二维数组名+1,是移动至下一行元素。a即代表第一的首地址,a+1即代表第二的首地址。以下标法表示,即a[0]或&a[0][0]代表第一行第一列元素地址,a[1]或&a[1][0]代表第二行第一列元素地址,依次类推。
我们举个例子来证明这一点:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a[2][3] = {1,3,5,7,9,11};

	printf("%p %p %p\n",a,a[0],&a[0][0]);
	printf("%p %p %p\n",a+1,a[1],&a[1][0]);
	return 0;
}

输出的结果为:
0x7ffea33b8440 0x7ffea33b8440 0x7ffea33b8440
0x7ffea33b844c 0x7ffea33b844c 0x7ffea33b844c
由此可见,以上三种表示方法得到的值都是相同的,都为某行首列元素地址,但是,a、a+1的表示方法和其他两种表示方法虽然得到的地址值相同,但它们的原理是不同的,a和a+1是行地址,a+1代表第二行的地址;而a[1]是一个列地址,它代表第二行第一列的地址。

需要注意的是,这里的a[0]和a[1]与一维数组中的a[0]和a[1]是不同的,一维数组中的a[1]代表的是数组中的第二个元素的值,而二维数组中的a[1]是一个数组的名字,它是一个列地址,用来表示二维数组第二行首列的地址。

那么,如何表示二维数组中其他的元素的地址呢?
第一种,使用下标法表示,如&a[1][1],即第二行第二列的元素地址。

第二种,首先表示行的地址,二维数组名就是行地址,如a+1即第二行的地址。然后表示第二行第一个元素的地址,很容易想到,a[1]即是第二行第一列的地址,对其进行加减即是对一维数组进行加减,即改变列;但是这里其实有个隐晦的问题,我们在上文中讲过,不同类型的指针是不能进行运算的,a+1是第二行的行地址,如何将它转换为列地址以控制列数呢?对其进行解引用,*(a+1)得到的就是列地址a[1]。此时对其进行加减,如 *(a+1)+1即表示第二行第二列元素的地址,这个表达式和a[1]+1与&a[1][0]+1是等价的。

那么,如何表示第二行第二列元素的值呢?
很显然,对列地址*(a+1)+1进行解引用,即得到该地址内的值,即 * (*(a+1)+1),这与表达式 *(a[1]+1)是等价的。

而存储行地址的指针变量,即为行指针变量,例如int a[2][3]; int (*p)[3];,方括号中的数字代表指针加1时,移动几个数据,当用行指针操作二维数组时,代表一行的元素个数,即列数。

下面举个例子来演示行指针:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a[2][3] = {1,3,5,7,9,11},i,j;	
	int (*p)[3] = a;
	int h = sizeof(a)/sizeof(a[0]); //行数
	int l = sizeof(a[0])/sizeof(int); //列数

	for(i = 0;i < h;i++)
	{
		for(j = 0;j < l;j++)
		{
			printf("%d  %d  %d\n",a[i][j],*(a[i]+j),*(*(a+i)+j));
			printf("%d  %d  %d\n",p[i][j],*(p[i]+j),*(*(p+i)+j));
		}
		printf("\n");
	}
	return 0;
}

输出的结果为:
1 1 1
1 1 1
3 3 3
3 3 3
5 5 5
5 5 5

7 7 7
7 7 7
9 9 9
9 9 9
11 11 11
11 11 11
可以看到,两种表示方法输出的结果是一样的,但是这不能说明二维数组名本身就是一个行指针,它们的本质区别是,数组名是一个地址常量,而数组指针是一个变量。

四、多级指针

4.1、多级指针的定义及解引用

多级指针就是一个指向指针变量的指针变量。对于指向处理数据的指针变量称为一级指针变量,简称为一级指针;而指向一级指针变量的指针变量称为二级指针变量,简称为二级指针。

通俗的说,一级指针内存储的是被处理数据的地址,对其进行解引用,得到处理数据(int a = 10; int *p = &a; *p 得到的结果即为10;),二级指针内存储的是一级指针的地址,对其进行解引用,得到的是一级指针本身,随后再进行一次解引用,才得到处理数据。

我们举个例子来说明它:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int *p = &a;
	int **q = &p;

	printf("a = %d  &a = %p  p = %p  &p = %p\n",a,&a,p,&p);
	printf("q = %p  &q = %p  *q = %p  **q = %d\n",q,&q,*q,**q);
	return 0;
}

输出的结果为:
a = 10 &a = 0x7fff7c1390d4 p = 0x7fff7c1390d4 &p = 0x7fff7c1390d8
q = 0x7fff7c1390d8 &q = 0x7fff7c1390e0 *q = 0x7fff7c1390d4 **q = 10

在本程序中,a是一个int型变量,存储&a的指针变量p的类型是int *,存储&p的指针变量q的类型是int **,而如果需要存储二级指针变量q的地址&q,则需要使用类型为int ***的三级指针变量。

需要特别注意的是,上文中说到的行指针,即数组指针也需要进行两次解引用才能得到目标数据的值,但是,它并不是一个二级指针,它的本质仍是一个指向一维数组的一级指针,他存储的值是一维数组名;单纯通过解引用次数来判断指针级别的方法是不可取的。

4.2、多级指针运算

上文已说明过指针的运算,指针变量+ - 1,实际上是向地址大或小的方向移动一个数据,数据指的是指针的目标变量,即移动一个指向类型的数据长度。多级指针同理,比如int **p;p+1就是移动一个int *型变量所占的内存空间。
示例程序如下:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int *p = &a;
	int **q = &p;

	printf("q = %p  &q = %p  *q = %p  **q = %d\n",q,&q,*q,**q);
	printf("q + 1 = %p  sizeof(int *) = %d\n",q+1,sizeof(int *));
	return 0;
}

输出的结果为:
q = 0x7fffd1f81d38 &q = 0x7fffd1f81d40 *q = 0x7fffd1f81d34 **q = 10
q + 1 = 0x7fffd1f81d40 sizeof(int *) = 8

可以看到,q+1与q之间相差了8个字节的地址,因为int *型是指针变量,所有类型的指针变量都占用8个字节(64位运行环境)。而&q的结果和q+1的结果相同,这是因为存储&q的空间首地址为0x7fffd1f81d40,q的地址存储在0x7fffd1f81d40 - 0x7fffd1f81d48间的内存区域,而0x7fffd1f81d38 -0x7fffd1f81d40间的8个字节存储的是一级指针p的地址。

5、指针数组

5.1、指针数组的定义和初始化

指针数组就是由若干个具有相同存储类型和数据类型的指针变量构成的集合。一般形式为:数据类型 * 指针变量数组名[大小],如int *p[2];就是定义了一个指向int类型的指针数组。
关于它的初始化,我们举个例子来说明:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10,b = 20;
	int *p[2];
	p[0] = &a;
	p[1] = &b;

	printf("sizeof(int [2]) = %d\n",sizeof(int [2]));
	printf("&a = %p  &b = %p\n",&a,&b);
	printf("p[0] = %p  p[1] = %p\n",p[0],p[1]);
	printf("p = %p  &p[0] = %p  &p[1] = %p\n",p,&p[0],&p[1]);
	printf("*p = %p  *(p+1) = %p\n",*p,*(p+1));

	printf("\na = %d  b = %d\n",a,b);
	printf("*p[0] = %d  *p[1] = %d\n",*p[0],*p[1]);
	printf("**p = %d  **(p+1) = %d\n",**p,**(p+1));

	return 0;
}

输出的结果为:
sizeof(int [2]) = 8
&a = 0x7fff2f07d3c8 &b = 0x7fff2f07d3cc
p[0] = 0x7fff2f07d3c8 p[1] = 0x7fff2f07d3cc
p = 0x7fff2f07d3d0 &p[0] = 0x7fff2f07d3d0 &p[1] = 0x7fff2f07d3d8
*p = 0x7fff2f07d3c8 *(p+1) = 0x7fff2f07d3cc

a = 10 b = 20
*p[0] = 10 *p[1] = 20
**p = 10 **(p+1) = 20

我们来说明一下上面的结果,以变量a为例:a的值为10,a的地址为0x7fff2f07d3c8;p[0]是指针数组p的第一个元素,它存储的内容是a的地址,所以p[0]也为0x7fff2f07d3c8;而p是一个指针数组名,它的类型是int *[2],它存储的内容是指针数组p第一个元素的地址,即为&p[0],0x7fff2f07d3d0;同理,对指针数组名p进行解引用,得到的是指针数组p的第一个元素。即 *p = p[0]。

而*p[0]即为对指针数组p第一个元素进行解引用,p[0]内的值为a的地址,对其解引用得到a的值,即10;上面已经说过,*p = p[0],所以对**p相当于对p[0]进行解引用,也为a的值 10。

5.2、指针数组名

上面已经说过,指针数组的数组名,代表数组的起始地址,由于数组的元素已经是指针了,而数组名是数组首元素的地址,所以数组名实际上是一个指针的地址,也就是本质上是一个多级指针。若用指针存储指针数组的起始地址p或&p[0],需要用到二级指针,如int **q = p;。
我们使用指针数组遍历二维数组的例子来理解一下:

#include<stdio.h>

int main(int argc, const char *argv[])
{
	int a[2][3] = {1,3,5,7,9,11},i,j;	
	int *p[2];
	int **q = p;
	p[0] = a[0];
	p[1] = a[1];
	int h = sizeof(a)/sizeof(a[0]);
	int l = sizeof(a[0])/sizeof(int);

	for(i = 0;i < h;i++)
	{
		for(j = 0;j < l;j++)
		{
			printf("%d  %d  %d  ",p[i][j],*(p[i]+j),*(*(p+i)+j));
			printf("%d  %d  %d  ",q[i][j],*(q[i]+j),*(*(q+i)+j));
		}
		printf("\n");
	}
	return 0;
}

输出的结果为:
1 1 1 1 1 1 3 3 3 3 3 3 5 5 5 5 5 5
7 7 7 7 7 7 9 9 9 9 9 9 11 11 11 11 11 11

在程序中,我们把列地址a[0]和a[1]赋值给了指针数组的两个元素p[0]和p[1],此时,数组名p的内容即为&p[0],要输出二维数组a中的值,有三种方法;
第一种:p[ i ][ j ],即为二维数组中的元素,这与a[ i ][ j ]是等价的;

第二种:*(p[ i ]+ j),p[ i ]为指针数组中的元素,由于已经把列地址a[0]赋值给了p[0],所以p[ i ]也为列地址,给列地址+j,为改变其列数的操作,再进行解引用,得到二维数组中的元素。

第三种:((p+i)+j),p中的内容为p[0]的地址,对其进行解引用,得到p[0],即列地址,随后对其进行加减改变列数,再次进行解引用,得到二维数组中的元素。
在本例中,p的使用方法和行地址类似,对指针数组p中的元素进行赋值,将列地址赋值给它们,此时p实际上用法就变成了一个行地址,因为p本身的作用实际上是一个行地址。
而使用二级指针变量q仍然会得到一样的结果,但q是一个二级指针变量,p是一个指针数组名,它是一个地址常量,它们并不一样。

码字不易,如果觉得有用,请点个收藏哈。
=v=

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_Ela

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

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

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

打赏作者

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

抵扣说明:

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

余额充值