指针得解析

一.指针是什么

​ 指针是一个值为内存地址得变量,真如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。

内存被划分为一个个的单元,一个最小的单元就是一个字节。指针是内存中一个最小单元的编号,也就是地址。指针提供了一种以符号形式使用地址的方法。

​ 平时口语种说的指针,通常指的是指针变量,是用来存放内存地址的变量。

​ 我们可以通过&(取地址操作符)取出变量的内存,其实就是地址,把地址可以存放到一个变量中,这个变量就是指针变量。

#include <stdio.h>
int main()
{
    int a = 10;//在内存中开辟一块空间
    int *p = &a;//这里对变量a,取出它的地址,可以使用&操作符。a变量占用四个字节的空间(整型变量的大小为四字节),这里是将要a的四个字节的第一个字节的地址存放在p变量中,因为地址在内存中存储的连续的,所以可以通过指针变量p来找到a的地址,p就是一个指针变量。
    return 0;
}

总结:

​ 指针变量,用来存放地址的变量(存放在指针中的值,都会被当成地址处理)。

​ 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平和低电平就是1或者0(人为用1和0 来规定高电平和低电平的情况)。

00000000 00000000 00000000 00000000
   00000000 00000000 00000000 00000001
   00000000 00000000 00000000 00000010
   00000000 00000000 00000000 00000011
   ...
   11111111 11111111 11111111 11111111
   这里有232次方个地址,一共有232方bit的空间,所以我们可以给4G的空闲进行编址。同样的,64位机器,有64根地址线,那可以给16G空闲的进行编址,编址16G空间。

​ 在32位的机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

​ 在36位机器上,如果有64个地址线,那一个指针变量的大小是8字节,才能存放一个地址。

​ 指针是用来存放地址的,地址是唯一标示一块空间的方式,指针的大小在32位平台是4个字节,在64位平台是8个字节。

二.指针和指针类型

int main()
{
    char* pc = NULL;
    int* pa = NULL;
    float* pb = Null;
    printf("%d %d %d",sizeof(pc),sizeof(pb),sizeof(pa));
    return 0;
}

​ 从上面的代码中我们可以发现,指针的大小与指针的类型之间没有关系,指针的大小只和编译代码时所处的环境有关,在32位环境下,指针的大小永远是4个字节,在64位环境下,指针的大小永远是8个字节。

​ 指针和变量一样,有着不同的类型,例如下面的代码。

 int num = 10;
p = &num;

char* pc = NULL;
int* pi = NULL;
short*ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;

​ 指针的定义方式为 type + * 。 要将&num(num的地址)保存到p中,p是一个指针变量,它的类型为整型指针。

​ char*类型的指针是为了存放char 类型变量的地址。

​ short*类型的指针是为了存放short类型变量的地址。

​ int*类型的指针是为了存放int类型变量的地址。

2.1 指针和整数的运算

​ 首先看下面这段代码,n为一个整型变量10,将n的地址强制类型转换为char * 的形式,用pc这个 char* 类型的指针变量来存储。再创建一个 int* 类型的整型变量,将整型变量n的地址用整型指针来pi存储。

​ 所以第一行打印的将为整型变量n的地址。

​ 第二行打印的是字符指针pc,和整型变量n的地址相同。

​ 第三行打印的是字符指针 pc 向后跳过一个字符后的地址,也就是往后下一个字节的地址。

​ 第四行打印的是整型指针 pi,和整型变量n的地址相同。

​ 第五行打印的是整型指针pi向后跳过一个整型后的地址,也就是往后下四个字节的地址。

int main()
{
    int n = 10;
    char* pc =  (char*)&n;
    int* pi = &n;
    printf("%p\n",&n);
    printf("%p\n",pc);
    printf("%p\n",pc + 1);
    printf("%p\n",pi);
    printf("%p\n",pi + 1);
    return 0;
}

​ 指针的类型决定了一次能操作几个字节大小的地址,也决定的指针向前或者向后走一步多大(跳过几个字节)。

​ 例如一个 char * 类型的字符指针,每次只能对一个字节大小的地址进行操作,也就是决定在解引用的时候一次能访问几个字节。虽然 char* 类型的指针一次只能访问一个字节的内容,但存储的地址是不变的, char* 类型的指针和 int* 类型的指针的区别是,在32位操作系统中,都是4个字节的大小,地址均为32bit位。但 char*类型的指针仅能对第一个字节,也就是低八位进行操作,而 int * 类型的指针可以对整个32bit位 也就是4个字节的地址进行操作。

int main()
{
	int n = 256;
	char* p = &n;
	*p = 10;
	printf("%d\n", n);
	return 0;
}

​ 例如上面这段代码,n在内存中的存储形式为00000000 00000000 00000001 00000000 ,由于p为char*类型的指针变量,解引用的时候一次只可以访问一个字节,而我们的计算机一般是以小端存储的形式存储数据(一个数据的低位字节序内容存放阿紫低地址处,高位字节序的内容存放在高地址处),所以只可以操作n的低八位,所以最后打印的结果n变成了266。

​ 但假如最后打印的时候把整型变量n换位对指针p解引用*p,那么最后的结果仍然为10,因为p作为字符指针, *p解引用的时候只可以访问一个字节的内容,所以仅仅打印一个字节保存的内容,结果为10。

2.2 指针的解引用

```c
int main()
{
    int n = 0x11223344;
    char *pc = (char*)&n;
    int *pi = &n;
    *pc = 0;
    printf("%d\n", n);
    *pi = 0;
    printf("%d\n", n);
    return 0;
}
```

​ 上面这段代码,因为 pc 是一个字符指针,强制类型转化整型变量n的地址用字符指针 pc来存储,导致的是对字符指针pc解引用只可以操作低八位,所以整型变量 n的值会变成 0X11223300 ,又因为%d是以有符号十进制的整型形式来打印,所以打印出来的结果是十进制的287453952,而pi为整型指针,可以对四个字节的地址进行操作,所以第二次打印的结果就是0了。

三. 野指针

​ 野指针就是指针指向的位置是不可知的,指针变量中的值即存储的地址是非法的内存地址,但野指针不是空指针NULL,野指针指向的内存是不可能用,野指针往往会造成内存越界,段错误等问题。

3.1 野指针成因

3.1.1局部指针变量没有初始化

​ 局部变量不会像全局变量一样,不赋值自动初始化为0,所以局部指针变量不初始化的话,指向的是随机值,也就是程序员无法把控的内存。一般把局部变量初始化为0,在定义局部指针变量的时候初始化为NULL。

3.1.2指针所指向的变量在指针使用之前就被销毁

​ 最常见的是在函数调用结束后,返回指向局部变量的指针,所以绝对不要在函数中返回局部变量和局部数组的地址。因为函数是使用的时候开辟空间,使用结束后空间销毁,所以可能会指向已经被销毁的空间,造成野指针的产生。

3.1.3 使用过已释放过的指针

​ 比如malloc申请的堆空间通过free释放后又去调用该指针,一定要在释放过后将指针变量的值赋值为NULL。

3.1.4 指针运算错误

​ 有些情况下,指针的初始化或者申请堆空间的时候并未造成野指针,但操作指针的不当使指针指向一块已经被别的进程使用的内存也会造成野指针。

​ 为了避免这种情况,一定要确保字符数组要以’\0’结尾,自己编写的内存相关函数指定长度信息(防止内存越界)。

3.2 如何规避野指针

3.2.1 指针初始化

3.2.2 小心指针越界

3.2.3 指针指向空间释放后使其为NULL

3.2.4 避免返回局部变量的地址

3.2.5 指针使用之前检查有效性

#include <stdio.h>
int main()
{
    int *p = NULL;
    //....
    int a = 10;
    p = &a;
    if(p != NULL)
   {
        *p = 20;
   }
    return 0;
}

​ 在使用指针之前,要确保其不为空指针,再去使用它,检测指针的有效性,避免野指针的产生。假如指针为空指针,使用它会造成程序的崩溃,产生了野指针。

四.指针运算

4.1 指针±整数

#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
     *vp++ = 0;
}

​ 上面这段代码的含义为定义一个宏 N_VALUES 的值为5,然后创建一个浮点数数组values,创建了一个float * 类型的指针vp,然后将数组第一个元素的地址用 float * 类型的指针vp保存,而*vp++ = 0; 因为为后置++ 所以是先对变量vp进行解引用,赋值为0,然后再执行++,向后跳四个字节大小,指向数组第二个元素的位置。所以这段代码的作用为,将浮点数组values的所有元素初始化为0。

4.2 指针-指针

int my_strlen(char *s)
{
       char *p = s;
       while(*p != '\0' )
              p++;
       return p-s;
}

​ 可以通过指针的减法运算来计算字符串长度,如上面这段代码的含义是,将 char * 类型的指针变量s所储存的地址赋给指针变量 p,然后用while(*p != ‘\0’ ) 语句来检测有没有到字符串的最后一个字符,没有的话继续向后走一个字符的大小,即一个字节,最后通过指针之间的减法,来算出相差多少个变量的大小,即字符串的长度啦。

4.3指针的关系运算

for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}
代码简化,这将代码修改如下:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}

​ 在大多数的编译器上可行,然尽量避免这么写,因为标准并不保证它可行。

标准规定,允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。

五.指针和数组

int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    return 0;
}

​ 数组名除了在 sizeof(数组名) 中表示得是整个数组得大小,和在 &数组名 表示得是取出整个数组得地址,其它情况下,数组名表示得是数组首元素得地址,也就是对上面这段代码来说,arr和&arr[0]是一样得,都是数组首元素得地址。

​ 那么就可以用数组名作为数组首元素得地址额,存放于指针中,又因为数组元素在内存中得存储是连续得,所以就可以通过指针访问数组中任意得元素,如下面得代码所示,这样是可行得。

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放得是数组首元素得地址
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,0};
    int *p = arr; //指针存放数组首元素的地址
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(i=0; i<sz; i++)
   {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p+i);
   }
    return 0;
}

​ 从上面这段代码得运行结果可以看出,p + i 其实计算的是数组arr下标为i得地址,所以我们就可以直接通过指针来访问数组。

如下所示:

int main()
{
 int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
 int *p = arr; //指针存放数组首元素的地址
 int sz = sizeof(arr) / sizeof(arr[0]);
 int i = 0;
 for (i = 0; i<sz; i++)
 {
 printf("%d ", *(p + i));
 }
 return 0;
}

六.二级指针

​ 指针变量也是变量,是变量就有地址,而变量的地址也可以用指针变量去存储,这就是二级指针。

int a = 10;   //假设a的地址为 0X0018FF44
int *pa = &a; //指针pa的值为 0X0018FF44 假设pa的地址为0X0018FF40 
int **ppa = &pa; //指针ppa的值为 0X0018FF40 假设ppa的地址为 0X0018FF3C

​ a的地址存放在pa中,pa的地址存放在ppa中,pa 是一级指针,而 ppa 是二级指针。

​ 对于二级指针来说,解引用操作,*ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa。

int b = 20;
*ppa = &b;//等价于 pa = &b;

​ **ppa 先通过 *ppa 找到 pa, 然后对 pa进行解引用操作: *pa,那找到的就是变量a。

**ppa = 30;
//等价于 *pa = 30
//等价于 a = 30;

七.字符指针

​ 在指针的类型中,我们知道有一种指针类型为字符指针char*;

一般使用:

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

还有一种使用方式如下:

int main()
{
    const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\n", pstr);
    return 0;
}

​ 这是把,字符串"hello bit"中首字符的也就是h的地址放到了字符指针 pstr中,又因为字符串和数组一样,在内存中的存储是连续的,所以就可以通过第一个字符h的地址向后打印直到’\0’之前,来完成对这个字符串打印的操作。

int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char *str3 = "hello bit.";
    const char *str4 = "hello bit.";
    if(str1 ==str2)
 printf("str1 and str2 are same\n");
    else
 printf("str1 and str2 are not same\n");
       
    if(str3 ==str4)
 printf("str3 and str4 are same\n");
    else
 printf("str3 and str4 are not same\n");
       
    return 0;
}

​ 上面这段代码最后输出的结果是 str1 and str2 are not same

str3 and str4 are same。

​ 这里的str3和str4指向的是同一个常量字符串。c/cpp会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际上会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候会开辟出不同的内存块,所以str1和str2不同。

八.指针数组

​ 指针数组也是数组的一种,是存放指针的数组。

int main()
{
    int *arr[10];//指针数组 存放的元素是int*
    char* ch[5];//指针数组  存放的是char*类型的元素。
    return 0;
}

​ 如上面这代码,因为arr和[]先结合,所以arr是数组名,10代表数组中的元素个数。将arr[10],也是数组名和元素个数去掉后,剩下的int*就是元素类型,也就是说上面这个数组是指针数组,其中存放的元素类型是 int *。

九.数组指针

9.1 数组指针的定义

​ 上面说到指针数组是数组的一种,而数组指针则是指针的一种。

​ 例如整型指针:int * pint: 能够指向整形数据的指针;浮点型指针:float*pf:能够指向浮点型数据的指针。那么数组指针应该是:能够指向数组的指针。

int *pi[10];
int (*p2)[10];

    对于上面两个指针来说,int *pi[10]; pi先和[10]结合,为数组名,所以pi是一个有十个元素的数组,数组中元素的类型为int*int (*p2)[10];因为括号()的存在,p2先和*结合,代表p2是一个数组指针,数组指针的类型是 int (*) [10]

9.2&数组名VS数组名

​ 对于下面的数组

int arr[10];

​ arr是数组名,数组名在绝大多数情况下表示的是数组首元素的地址,而&arr表示的是取整个数组的地址。

int main()
{
    int arr[10] = {0};
    printf("%p\n",arr);
    printf("%p\n".&arr);
    return 0;
}

​ 上面这两行打印代码代码打印的虽然都是数组首元素的地址,即arr[0]的地址,但它们代表的含义是不同的,再看下面这段代码。

#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0;
}

​ &arr表示的是数组的地址,而不是数组首元素的地址,arr表示的是数组首元素的地址,所以arr+1是跳过数组中元素的大小,即跳过数组的首元素。

​ 而%arr的类型是:int (*) [10],是一种数组指针类型,( * )说明这是一个指针,指向的数组有10个元素,元素类型为 int 整型。数组的地址+1,即&arr + 1 ,跳过的是整个数组的大小,所以&arr + 1相对于&arr的差值是40个字节。

9.3数组指针的使用

​ 数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    return 0;
}

​ 这里的 p就是数组指针,存放了整个数组的地址,它的类型是int(*)[10], ( * )说明p是一个指针,它指向的数组是有十个元素的整型数组。

int main()
{
	char arr[5];
	char(*pa)[5] = &arr;
	int* parr[6];
	int* (*ppa)[6] = &parr;
	return 0;
}

​ 再看上面这段代码,假如利用数组指针来存储指针数组的地址,例如 int * parr[6]; int * (*ppa)[6] = &parr; parr是一个有六个元素,元素类型为 int * 的数组,而 (*paa)说明ppa是一个指针, int * [6] 说明paa这个数组指针,指向的是一个元素个数为6个,元素类型为整型指针 int * 的数组。

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;
}

​ 不过数组指针访问数组内容的时候会有一定的困扰,例如上面这段代码中,因为 p中保存着整个数组arr的地址,所以 *p 就是arr,也就是数组首元素的地址,对数组首元素的地址 + i 再进行解引用,才能找到数组中各个元素的值,这样显得非常繁琐,所以数组指针的应用场景也不是这样的。

void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }       
       printf("\n");
   }
}
void print_arr2(int (*arr)[5], int row, int col)
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);/*这里除了常规的arr[i][j],利用二维数组的形式去访问二位数组中的元素,还以使用指针的形式去访问。因为arr是个数组指针,对arr解引用,*arr就相当于得到了数组名,也就是二维数组第一行的地址。所以(*arr + i)得到的就是第i + 1行的地址,(*arr + i) + j 得到的就是第 i + 1行第j + 1个元素的地址,再对其解引用,就访问到了对应的元素了,最后写出来的形式便是*((*arr + i) + j)*/
       }
        printf("\n");
   }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
   print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

​ 上面这段代码中,值得注意的是,用来存储二维数组地址的指针int (*arr)[5]; 和用来存储一位数组的指针形式上是一样的,在这里的原因是,因为数组名是首元素的地址,而对二维数组还说,首元素的地址就是第一行元素的地址,所以用来存储二位数组地址的数组指针和一维数组的数组指针形式上表面是一样的,但其中的含义是不同的。

​ 存储二位数组地址的指针,后面的[]中的数字代表还有几行,存储一位数组地址的指针,后面[]中的数字代表还有几个元素。

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

​ 上面这四行代码中:

​ 第一行 int arr[5]; 表明的是有一个整型数组有五个元素,数组名是arr。

​ 第二行 int *parr1[10]; 数组名是parr1 w3,这个数组中有十个元素,元素类型是整型指针 int *。

​ 第三行 int (*parr2)[10]; 这是个数组指针,本质上是指针,指向的数组是有10个元素,元素类型是整型的数组。

​ 第四行 int (*parr3[10])[5]; 数组名是parr3,数组中有10个元素,每个元素是数组指针,指向有五个元素的整型数组。

十.数组参数、指针参数

10.1一维数组传参

void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
}

​ 上面这段代码中,有各式各样的函数的写法。在 void test(int arr[]) {;} 个函数中,数组传参数组接收,数组的大小是否正确或者是否写没有关系,因为函数中的参数是形式参数,不会真的去创建一个数组。因为数组在传参的时候,本质上是传的数组名,也就是传的数组首元素的地址。

​ 形参的部分除了数组名外,写成指针的形式也可以。例如int arr[10] = {0}; 在传递arr,也就是这个数组首元素的地址。在接收该地址的时候,形参也可以写成指针的形式,也就是 int *arr 的形式,因为数组的首元素的地址是整型变量的地址,所以要用整型指针变量,int * 去接收。

​ 在最后一个函数,也就是 void test2 (int ** arr){}中,int *arr2[20] = {0}; ,数组名arr2是首元素的地址,也就是int * 的地址,而一级指针的地址可以用二级指针来接收,所以 int ** arr 的形式也是完全可以的。

10.2二维数组传参

void test(int arr[3][5])
{}
void test(int arr[][])
{}
void test(int arr[][5])
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)
{}
void test(int* arr[5])
{}
void test(int (*arr)[5])
{}
void test(int **arr)
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

​ 对二维数组的传参来说,如果形参写成数组的形式的话,行可以省略,但是列也就是第二个[]的数字不可以省略,因为对一个二位数组来说,可以不知道有多少行,但是必须知道一行有多少个元素,不然这个二二维数组就有无数种组成形式了。

​ 对后面四个函数来说,是用指针的形式来写形参,但第一个第二个和第四个全是错误的写法。因为 int arr[ 3 ] [ 5 ] = {0}; 是一个二维数组,arr作为数组名,是数组的首元素的地址,对二维数组来说,它的首元素的地址就是第一行元素的地址。所以 一行元素用整型指针 int* arr 肯定是不可以的,int * arr[5] 本质上是一个有五个元素,元素类型是整型指针的数组,不是指针。 int ** arr 是一个二级指针,只有一级指针的地址才能用二级指针来接收。

​ 第三个函数, int (*arr)[5] 是一个数组指针,它的类型是 int( * )[5],所以是可以作为形式参数来接收二维数组arr的第一行五个元素的地址的。

10.3一级指针传参

void print(int *p, int sz)
{
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0;
}

​ 上面这段代码中,就是使用了一级指针p来传参,一级指针中p存放的是arr的地址,数组名的地址,也就是整型数组arr首元素的地址,所以形参用整型指针 int* p来接收是完全可以的。

void test1(int *p)
{
    ;
}

void test2(char* p)
{
    ;
}

int main()
{
    int a = 10;
    int * ptr = &a;
    int arr[10] = {0};
    test(&a);
    test(ptr);
    test(arr);
    char ch = 10;
    char * pr = &ch;
    char ch1[10] = {0};
    test2(&ch);
    test2(ch1);
    test2(pr);
    return 0;
}

​ 如上面这段代码,函数test1 的参数为 int* p 也就是一级整型指针,可以接收 &a,整型变量的地址。也可以接收一级整型指针本身,比如ptr,也可以接收整型数组的数组名也就是整型数组的首元素的地址。

​ test2的参数为 char* p 也就是一级字符指针,可以接收&ch ,字符变量的地址。也可以接收一级字符指针本身,比如 pr,或者接收字符数组的数组名,也就是字符数组的首元素的地址,例如pr。

10.4二级指针传参

void test(int** ptr)
{
 printf("num = %d\n", **ptr); 
}
int main()
{
 int n = 10;
 int*p = &n;
 int **pp = &p;
 test(pp);
 test(&p);
 return 0;
}

​ 上面这段代码中,pp是二级指针的指针变量名,传二级指针用二级指针 int ** ptr来接收完全可以。同时也可以传递一级指针的地址,也就是&p来使用二级指针来接收同样是可以的。

void test(char **p)
{
 
}
int main()
{
 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];
 test(&pc);
 test(ppc);
 test(arr);//Ok?
 return 0;
}

​ 对二级指针来说,重要的和一级指针一样,要去思考二级指针可以接收什么样的参数的传参。

​ 上面就是三种常见的二级指针可以接收的三种参数的形式。

​ 第一种是对一级字符指针pc取地址,一级字符指针的地址可以用二级字符指针来接收。

​ 第二种是可以直接传递二级字符指针ppc。

​ 第三种是可以对指针数组进行传参,char * arr[10];是一个数组指针,有十个元素,元素类型是char*。arr作为数组名是首元素的地址,也就是字符指针char * 的地址,而一级字符指针的地址刚好可以用二级字符指针 char ** p 来接收。

十一.函数指针

​ 函数指针本质上就是指向函数的指针。

void test()
{
 printf("hehe\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0;
}

​ 对于函数来说,函数名和取地址函数名拿到的都是函数的地址,二者的作用是相同的。函数的地址的含义就是函数存放的位置。

void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

​ 在上面的两行代码,void (*pfun1)();可以用来存放函数test的地址,( * pfun1)说明pfun1是一个指针,后面跟着圆括号说明指针的类型是函数指针,该函数的无参数,返回类型是void。而第二行代码是一种错误的写法。

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

int main()
{
    int (*pf)(int,int) = &Add;//这里加不加取地址符号&都是一样的,因为都是函数Add的地址。
    int sum = (*pf)(2,3);
    int sum = pf(2,3);
    printf("%d\n",sum);
    return 0;
}

​ 通过函数指针,可以调用函数指针所指向的函数。比如使用函数指针pf把函数Add的地址放在函数指针pf中。使用的时候通过解引用操作符,找到该地址中所存的函数,然后就可以传参使用这个函数了。

​ 在上面这段代码中,int sum = (*pf)(2,3); 和 int sum = Add(2,3);的作用是完全相同的。同时,int sum = pf(2,3);这种形式也是可以的。

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

​ 上面是两段非常有趣的代码。

​ 代码1的形式是 ((void( * )())0)(); ,这段代码的含义是,将0强制类型转换成 void()()这种的类型的函数指针,然后再对其解引用,调用0地址处这个无参数返回类型是void的函数。 整段代码的作用就是调用0地址处的函数。

​ 代码2的形式是 void (*signal(int,void( * )(int)))(int); 这段代码的含义是 signal是一个函数声名,这个函数的参数有两个。第一个参数是整型,第二个参数是一个函数指针,返回类型也是一个函数指针,该指针的函数参数类型是int,返回类型是void。

typedef void(*pfun_t)(int);++++

​ 上面的代码2也可以通过typedef的形式来将进行简化,例如void(*pfun_t)(int);的含义是,利用pfun_t来代替void( * )(int)这个函数指针类型,pfun_t是一个新的函数指针类型的名字。

​ void( * p)(int); //p是函数指针变量

​ typedef void(*pf_t)(int);//pf_t是类型名

十二.函数指针数组

​ 数组是一个存放相同类型的存储空间,例如下面的int * arr[10];就是一个指针数组,是一个存放有十个元素,元素类型为 int* 整型指针的指针数组。

int *arr[10];
//数组的每个元素是int*

​ 如果把函数的地址存到一个数组中,那这个数组就叫函数指针数组。

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

​ 上面这三行代码中,parr1是一个函数指针数组,也就是 int(*parr1[10])(); 是一个函数指针数组。parr1是数组名,数组中的元素类型是 int ( * )()类型的函数指针。

int add(int a, int b)
{
 return a + b;
}
int sub(int a, int b)
{
 return a - b;
}
int mul(int a, int b)
{
 return a*b;
}
int div(int a, int b)
{
 return a / b;
}
int main()
{
 int x, y;
 int input = 1;
    int ret = 0;
    do
   {
        printf( "*************************\n" );
        printf( " 1:add           2:sub \n" );
        printf( " 3:mul           4:div \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
        scanf( "%d", &input);
        switch (input)
       {
         case 1:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = add(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 2:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = sub(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 3:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = mul(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 4:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = div(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 0:
                printf("退出程序\n");
 break;
        default:
              printf( "选择错误\n" );
              break;
       }
 } while (input);
    
    return 0;
}

​ 上面这种计算器的写法是常规计算器的写法,但它也有不足的地方,就是重复的代码过多,四个case的本质区别只是调用函数的不同,这时候就可以用函数指针来进行代码的简化操作。

#include <stdio.h>
int add(int a, int b)
{
           return a + b;
}
int sub(int a, int b)
{
           return a - b;
}
int mul(int a, int b)
{
           return a*b;
}
int div(int a, int b)
{
           return a / b;
}
int main()
{
     int x, y;
     int input = 1;
     int ret = 0;
     int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
     while (input)
     {
          printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
      scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
         {
          printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);
         }
          else
               printf( "输入有误\n" );
          printf( "ret = %d\n", ret);
     }
      return 0;
}

​ 这段代码就是使用函数指针数组,经过简化后的效果。 int (*p[5])(int x, int y) = {0, add, sub, mul , div}; p后面跟着[],说明这是一个数组,p是数组名,数组中有五个元素,元素的类型是 int( * )(int x,int y) 有两个参数都是整型,返回类型是整型的函数指针。将函数指针放到数组中,所以是函数指针数组。

​ 函数指针数组中,可以存放函数参数类型一样,返回类型一样的数组来进行代码的简化。

十三.指向函数指针数组的指针

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

int main()
{
    int (*pa)(int ,int) = Add;//函数指针
    int (*pfA[4])(int,int);//函数指针数组
    int (*(*ppfA)[4])(int,int) = &pfA;//ppfA 是一个指针,该指针指向了一个存放函数指针的数组。
    return 0;
}

​ 指向函数指针数组的指针,就是把函数指针数组的地址放到一个指针中,这就是这向函数指针数组的指针。ppfA 是指针的名称,指针的类型为 int(* ( * )[4])(int,int) 最中间的*代表 ppfA 是一个指针,它指向的类型是有四个元素,元素类型为返回类型为整型,两个参数分别是整型的函数指针。

void test(const char* str)
{
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0;
}

十四.回调函数

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

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
    int i = 0;
    for (i = 0; i< size; i++)
   {
        char tmp = *((char *)p1 + i);
       *(( char *)p1 + i) = *((char *) p2 + i);
       *(( char *)p2 + i) = tmp;
   }
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
    for (i = 0; i< count - 1; i++)
    {
       for (j = 0; j<count-i-1; j++)
       {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
           {
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
    }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

十五.指针和数组笔试题解析

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
printf("%d\n",sizeof(a+0));//4或者8
printf("%d\n",sizeof(*a));//4
printf("%d\n",sizeof(a+1));//4或者8	
printf("%d\n",sizeof(a[1]));//4
printf("%d\n",sizeof(&a));//4或者8
printf("%d\n",sizeof(*&a));//16
printf("%d\n",sizeof(&a+1));//4或者8
printf("%d\n",sizeof(&a[0]));//4或者8
printf("%d\n",sizeof(&a[0]+1));//4或者8

​ 数组名单独在sizeof中表达是整个数组的大小,而不是数组首元素的地址,这是一个要注意的点。

​ 第一行:sizeof中单独有数组名,含义是计算整个数组的大小,即四个整型元素的大小,为16字节。

​ 第二行:sizeof中为数组名+0数字的形式,无论数字是几,数组名都不是单独存在在数组名中,所以代表的含义是数组首元素的地址,加0则跳过了0的元素,所以还是数组首元素的地址,地址存储空间的大小即是指针变量的大小,在32位操作环境下是4字节,在64位操作环境下是8字节。

​ 第三行:数组名不是单独放在sizeof内,所以是对数组首元素的地址进行解引用,得到的是数组的首元素,即计算一个int类型的大小为4字节。

​ 第四行:和第二行同理,数组名不单独位于sizeof内,所以是数组的首元素+1,即跳过数组第一个元素,为数组第二个元素的地址,所以大小为4字节或8字节。

​ 第五行:a[1]为数组中的第二个元素,大小为整型变量的大小,4字节。

​ 第六行:对数组名使用取地址操作符得到的是数组的地址,只要是地址,那么大小值随操作环境的变化而变化,所以大小为4字节或8字节。

​ 第七行: 第一种理解方式是*和&有抵消作用, *&a相当于a,sizeof( *&a) 最终还是计算的数组a的大小,为16字节。 第二种理解方式是,&a 的指针类型是 int( * )[4],如果解引用,访问的就是4个int的数组,大小是16个字节。

​ 第八行: &a 是取出整个数组的地址,&a + 1 跳过整个数组后的地址,是地址就是4个字节或8个字节。

​ 第九行:&a[0]是取出数组中第一个元素的地址,大小为4个字节或8个字节。

​ 第十行: &a[0] + 1 是数组中第二个元素的地址,是地址大小就是4个字节或8个字节。

//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));

​ 第一行: arr作为数组名单独放在sizeof内部,计算的整个数组的大小,单位是字节,6。

​ 第二行:arr就是首元素的地址,arr+0还是首元素的地址,地址大小就是4/8。

​ 第三行:arr就是首元素的地址,*arr就是首元素,是一个字符,大小是一个字节,1。

​ 第四行:arr[1]就是数组的第二个元素,是一个字符,大小是1个字节。

​ 第五行:&arr取出的是数组的地址,数组的地址也是地址,地址就是4/8个字节。

​ 第六行:&arr取出的是数组的地址,&arr+1,跳过了整个数组,&arr+1还是地址,地址就是4/8个字节。

​ 第七行:&arr[0]是第一个元素的地址,&arr[0]+1就是第二个元素的地址,地址就是4/8个字节。

​ 第八行:arr是首元素的地址,但是arr数组中没有\0,计算的时候就不知道什么时候停止,结果是:随机值。

​ 第九行:arr是首元素的地址,arr+0还是首元素的地址,结果是:随机值。

​ 第十行:错误的一种写法,strlen需要的是一个地址,从这个地址开始向后找字符,直到\0,统计字符的个数。但是*arr是数组的首元素,也就是’a’,这是传给strlen的就是’a’的ascii码值97,strlen函数会把97作为起始地址,统计字符串,会形成内存访问冲突。

​ 第十一行:&arr是arr数组的地址,虽然类型和strlen的参数类型有所差异,但是传参过去后,还是从第一个字符的位置向后数字符,结果还是随机值。

​ 第十二行:随机值。

​ 第十三行:随机值。

    char arr[] = "abcdef";
    printf("%d\n", sizeof(arr));
    printf("%d\n", sizeof(arr+0));
    printf("%d\n", sizeof(*arr));
    printf("%d\n", sizeof(arr[1]));
    printf("%d\n", sizeof(&arr));
    printf("%d\n", sizeof(&arr+1));
    printf("%d\n",sizeof(&arr[0]+1));
    printf("%d\n", strlen(arr));
    printf("%d\n", strlen(arr+0));
    printf("%d\n", strlen(*arr));
    printf("%d\n", strlen(arr[1]));
    printf("%d\n", strlen(&arr));
    printf("%d\n", strlen(&arr+1));
    printf("%d\n", strlen(&arr[0]+1));

​ 第一行:为字符串的大小,字符串在求大小的时候会将末尾的’\0’默认算在内,所以大小为7字节。

​ 第二行:为字符串首元素地址的大小,大小为4字节或8字节。

​ 第三行:对字符串第一个字符解引用,得到的是一个字符的大小,即1字节。

​ 第四行:为字符喘第二个字符的大小,大小1字节。

​ 第五行:为整个字符串的地址,大小为4字节或8字节。

​ 第六行:为跳过整个整个字符串的地址,本质上还是地址,大小为4字节或8字节。

​ 第七行:为第二个字符的地址,大小为4字节或8字节。

​ 第八行:为整个字符串的长度,大小为6字节。

​ 第九行:从字符串首元素开始来计算长度,大小为6字节。

​ 第十行:错误,相当于把字符串的首元素的ASCII值作为地址进行访问。

​ 第十一行: 错误,相当于把字符串的第二个元素的ASCII值作为地址进行访问。

​ 第十二行:整个数组的地址和数组首元素的地址相同,所以长度仍然为6字节。

​ 第十三行:相当于从字符串后的第一个地址开始往后计算,直到遇到’\0’,所以长度为随机值。

​ 第十四行:相当于从字符串第二个字符开始计算长度,长度为5字节。

char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));

printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));

​ 第一行:p是一个指针变量,sizeof§计算的就是指针变量的大小,就是4个或8个字节。

​ 第二行: p 是指针变量,p + 1 也是地址,地址大小就是4个或8个字节。

​ 第三行: *p访问的是一个字节,访问的是字符串的首元素。

​ 第四行: p[0] --> *(p + 0) --> *p 访问的是首元素,大小为1字节。

​ 第五行:&p也是地址,是地址就是大小是4字节或8字节,&p是二级指针。

​ 第六行:&p是地址,+1后还是地址,是地址就是4个字节或8个字节。&p+1,是p的地址的地址+1,在内存中跳过变量p后的地址。

​ 第七行:p[0]就是a,&p[0]就是a的底子,&p[0] + 1就是b的地址,是地址就是4字节或8字节。

​ 第八行:p中存放的是’a’的地址,strlen§就是从’a’的位置向后求字符串的长度,长度是6字节。

​ 第九行:p+1是’b’的地址,从b的位置开始求字符串长度是5个字节。

​ 第十行: 错误。

​ 第十一行: 错误。

​ 第十二行:随机值。

​ 第十三行:随机值。

​ 第十四行:p[0] -> *(p+0) -> *p ->‘a’ ,&p[0]就是首字符的地址,&p[0]+1就是第二个字符的地址,从第2 字符的位置向后数字符串,长度是5字节。

//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));

​ 第一行:数组名单独放在sizeof内部,计算的是整个数组的大小。

​ 第二行:就是二维数组第一个元素的大小,大小为4字节。

​ 第三行:a[0]表示第一行的数组名,a[0]作为数组单独放在sizeof内部,计算的是整个数组的大小,也就是那一行的大小,大小为16字节。

​ 第四行:a[0]在sizeof内,不是单独存在,所以代表第一行第一个元素的地址,所以a[0] + 1 是第一行第二个元素的地址,大小为4字节或8字节。

​ 第五行:a[0] + 1 是第一行第二个元素的地址,解引用访问的是第一行第二个元素,大小为4字节。
​ 第六行:数组名没有单独放在sizeof中,表示首元素地址,二维数组首元素的地址即二维数组第一行的地址,+1则为二位数组第二行的地址,大小为4字节或8字节。

​ 第七行:*(a + 1)相当于第二行的数组名,相当于a[1],大小为4字节或8字节。

​ 第八行:a[0]是第一行的地址,&a[0]是第1行的地址,&a[0]+1就是第二行的地址,大小为4字节或8字节。

​ 第九行:相当于第二行,也就是a[1],sizeof(a[1]),大小为16字节。

​ 第十行:a是二维数组的数组名,没有&,没有单独放在sizeof内部,*a是二维数组的首元素,也就是第一行,所以大小16字节。

​ 第十一行:表面上a[3]越界了,但是不会真的去访问它,和a[0]的作用是相同的,所以大小是16字节。

十六.指针笔试题

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}
//程序的结果是什么?

​ a是数组名,也是首元素地址,a + 1则是数组中第二个元素地址,解引用找到数组第二个元素2。 &a 是整个数组的地址,&a + 1跳过了整个数组,所以指针ptr中存着跳过整个数组的地址,-1则为数组最后一个元素的地址,解引用后最后的结果为5。

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
 int Num;
 char *pcName;
 short sDate;
 char cha[2];
 short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
 printf("%p\n", p + 0x1);
 printf("%p\n", (unsigned long)p + 0x1);
 printf("%p\n", (unsigned int*)p + 0x1);
 return 0;
}

​ 假如p的值是0x100000的话,0x1就是十六进制的1,和十进制的1是相同的。p + 0x1 相当于p + 1,指针变量 + 1是跳过一个其所指向对象的大小,又因为p是一个结构体指针,指向的结构体Test类型的变量大小是20字节,所以第一行打印的结果将会是0x100014。

​ 第二行是把指针强制转换成无符号长整型,无符号长整形 + 0x1就是单纯的+1,所以最后打印的结果是0x100001。

​ 第三行是把结构体指针p强制类型转换成整型指针,所以 + 1会跳过一个整型变量的大小,最后打印的结果是0x100004。

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0;		
}

​ 这道题要充分对强制类型转换和指针进行理解。

​ a作为数组名,&a取出的是整个数组的地址,&a + 1是从整型数组a后的地址开始访问。ptf1[-1] 相当于 *(ptr1 - 1),所以指向的是数组末尾,往前四个字节的位置,即打印出来的结果是4。

​ 计算机内是小端存储,小端存储就是 低位放在低地址处,高位放在高地址处。 一开始,数组a在内存中的存储形式是 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00,因为ptr2被强制转换成整型之后,+1就相当于加数字 1 ,即向后移动的一个字节,而%x会访问打印四个字节,所以最后访问的就是 00 00 00 02这四个字节,打印出来的结果便是20000000

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值