指针的主题,我们在C语言初阶博客已经接触过了,我们知道了指针的概念:
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小是固定的4/8个字节(32位平台/64位平台)。
3.指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
4.指针的运算。
下面,我们继续探讨指针的高级主题
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:
int main() { char ch = 'a'; char *pc = &ch; *pc = 'a'; return 0; }
还有一种使用方式如下:
int main() { const char* pstr = "hello C."; printf("%s\n", pstr); return 0; }
那么这里是把一个字符串放到pstr指针变量里了吗?
其实并不是哦,这里实际上是把字符串的首字符也就是h的地址存放到指针变量pstr中。
我们看看下面这道关于字符指针的面试题
#include <stdio.h> 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和str2分别存的是对应数组的首字符地址,他们并不相同,所以第一个输出结果是str1 and str2 are not same,这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
2. 指针数组
前面在初阶C语言时我们就讲过指针数组
如下:
int* arr1[10]; //整形指针的数组 char *arr2[4]; //一级字符指针的数组 char **arr3[5];//二级字符指针的数组
3. 数组指针
3.1 数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * p; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
那么下面代码哪个是数组指针?
int *p1[10]; int (*p2)[10];
答案是int (*p2)[10];
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
要注意的是:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
3.2 &数组名VS数组名
对于数组
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:
#include <stdio.h> int main() { int arr[10] = {0}; printf("%p\n", arr); printf("%p\n", &arr); return 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 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型,数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
3.3 数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; int(*p)[10] = &arr; return 0; }
把数组arr的地址赋值给数组指针变量p,但是我们一般很少这样写代码
如下:
#include <stdio.h> 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; int j = 0; for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { printf("%d ", 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); print_arr2(arr, 3, 5); return 0; }
数组名arr,表示首元素的地址
但是二维数组的首元素是二维数组的第一行
所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
可以数组指针来接收
我们再来看看下面代码分别代表什么意思
int arr[5];//数组 int *parr1[10];//指针数组 int (*parr2)[10];//数组指针 int (*parr3[10])[5];//数组指针数组
4. 数组参数、指针参数
4.1 一维数组传参
void test(int arr[])//ok? {} void test(int arr[10])//ok? {} void test(int* arr)//ok? {} void test2(int* arr[20])//ok? {} void test2(int** arr)//ok? {} int main() { int arr[10] = { 0 }; int* arr2[20] = { 0 }; test(arr); test2(arr2); }
这几种传递方式都是可以的
4.2 二维数组传参
void test(int arr[3][5])//ok {} void test(int arr[][])//不可以 {} void test(int arr[][5])//ok {} void test(int* arr)//不可以 {} void test(int* arr[5])//ok {} void test(int(*arr)[5])//ok {} void test(int** arr)//不可以 {} int main() { int arr[3][5] = { 0 }; test(arr); }
总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。
4.3 一级指针传参
#include <stdio.h> 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]); print(p, sz); return 0; }
4.4 二级指针传参
#include <stdio.h> 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; }
5. 函数指针
先看代码
#include <stdio.h> void test() { printf("hehe\n"); } int main() { printf("%p\n", test); printf("%p\n", &test); return 0; }
不难看出函数名即为函数地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存?
#include <stdio.h> void test() { printf("hehe\n"); } void (*pfun1)();
pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
6. 函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:
int *arr[10]; //数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。
函数指针数组的用途:转移表
示例:计算器常规写法
#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; 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;
使用函数指针数组的实现:
#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; }
这样代码就不显得冗余了
7. 指向函数指针数组的指针
指向函数指针数组的指针是一个 指针,指针指向一个数组 ,数组的元素都是函数指针 ;
那么该如何定义呢,看下面代码:
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; }
8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
我们以库函数种的qsort函数为例,我们先看此函数的定义
_ACRTIMP void __cdecl qsort( _Inout_updates_bytes_(_NumOfElements * _SizeOfElements) void* _Base, _In_ size_t _NumOfElements, _In_ size_t _SizeOfElements, _In_ _CoreCrtNonSecureSearchSortCompareFunction _CompareFunction );
第一个形参为void* 是因为其能够容纳任意类型的指针,
最后一个 _CoreCrtNonSecureSearchSortCompareFunction _CompareFunction实际上就是一个回调函数,我们在使用qsort函数时,自己还需要再写一个_CompareFunction函数对不同类型排序。
示例:
#include<stdlib.h> #include <stdio.h> 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; }
其中int_cmp函数即为qsort调用的_CoreCrtNonSecureSearchSortCompareFunction _CompareFunction
需要注意的是,这种默认的写法一般为升序,若想改为降序,可以将p1和p2互换,如下代码:
int int_cmp(const void* p1, const void* p2) { return (*(int*)p2 - *(int*)p1); }
我们再看对结构体数组的排序
#include<stdlib.h> #include <stdio.h> #include <string.h> struct Stu { char name[20]; int age; }; int cmp_stu_by_age(const void* p1, const void* p2) { return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age; } void test1() { struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} }; int sz = sizeof(arr) / sizeof(arr[0]); printf("初始序列:> "); for (int i = 0; i < sz; i++) { printf("%s,%d ", arr[i].name, arr[i].age); } printf("\n"); qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age); printf("按年龄排序:> "); for (int i = 0; i < sz; i++) { printf("%s,%d ", arr[i].name, arr[i].age); } printf("\n"); } int cmp_stu_by_name(const void* p1, const void* p2) { return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name); } void test2() { struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name); printf("按名字排序:> "); for (int i = 0; i < sz; i++) { printf("%s,%d ", arr[i].name, arr[i].age); } } int main() { test1(); test2(); return 0; }
根据同样的原理,我们还可以写出适应多类型的冒泡排序回调函数
示例:
#include<stdio.h> void Swap(void* p1, void* p2, int size) { for (int i = 0; i < size; i++) { char tmp = *((char*)p1 + i); *((char*)p1 + i) = *((char*)p2 + i); *((char*)p2 + i) = tmp; } } void Bubble_Sort(void* base, int num, int size, int(*cmp)(void*,void*)) { int i = 0; int j = 0; for (i = 0; i < num - 1; i++) { for (j = 0; j < num - 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 Int_Sort(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; } void Int_Print(int* arr,int sz) { for (int i = 0; i < sz; i++) printf("%d ", arr[i]); } void Test1() { int arr[] = { 7,6,5,4,8,9,3,1,2 }; int sz = sizeof(arr) / sizeof(arr[0]); printf("初始序列:> "); Int_Print(arr, sz); printf("\n"); Bubble_Sort(arr, sz, sizeof(arr[0]), Int_Sort); printf("排序后 :> "); Int_Print(arr,sz); } int main() { Test1(); return 0; }
9. 指针和数组笔试题解析
以下所有代码均为64平台编译
9.1 一维数组
int a[] = {1,2,3,4}; printf("%d\n",sizeof(a));//sizeof中数组名代表整个数组 printf("%d\n",sizeof(a+0));//数组名+0为第一个元素的地址,也就是指针,指针大小就是4/8个字节,当前为64位,所以是8 printf("%d\n",sizeof(*a));//对数组名进行解引用操作,获取指向数组第一个元素 printf("%d\n",sizeof(a+1));//a+1代表第二个元素的地址 printf("%d\n",sizeof(a[1]));//代表第二个元素 printf("%d\n",sizeof(&a));//对数组名取地址,代表的是指针 printf("%d\n",sizeof(*&a));//&再*解引用等于没做操作,还是整个数组 printf("%d\n",sizeof(&a+1));//对数组名取地址再+1代表的是该数组之后的地址 printf("%d\n",sizeof(&a[0]));//代表第一个元素的地址 printf("%d\n",sizeof(&a[0]+1));//代表第二个元素的地址
9.2 字符数组
char arr[] = {'a','b','c','d','e','f'}; printf("%d\n", sizeof(arr));//整个数组 printf("%d\n", sizeof(arr+0));//数组名+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));//代表第二个元素的地址
char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", strlen(arr));//因为字符数组arr中没有\0,所以在求字符串长度的时候,会一直往后找,产生的结构就是随机值 printf("%d\n", strlen(arr+0));//arr + 0是首元素的地址,和第一个一样,也是随机值 //printf("%d\n", strlen(*arr));//错误, arr是数组首元素的地址,*arr就是数组首元素,就是'a'-97 //strlen函数参数的部分需要传一个地址,当我们传递的是'a'时,'a'的ASCII码值是97,那就是将97作为地址传参 //strlen就会从97这个地址开始统计字符串长度,这就非法访问内存了 //printf("%d\n", strlen(arr[1]));//错误,同上,只不过这里传的是第二个元素b printf("%d\n", strlen(&arr));//&arr是数组的地址,数组的地址和数组首元素的地址,值是一样的,那么传递给strlen函数后,依然是从数组的第一个元素的位置开始往后统计 printf("%d\n", strlen(&arr+1));//随机值-6,减去了上面6个元素的长度 printf("%d\n", strlen(&arr[0]+1));//&arr[0] + 1是第二个元素的地址。结果也是随机值
char arr[] = "abcdef"; printf("%d\n", sizeof(arr));//整个数组 printf("%d\n", sizeof(arr + 0));//arr + 0是首元素的地址 printf("%d\n", sizeof(*arr));//*arr其实就是首元素,1个字节 printf("%d\n", sizeof(arr[1]));//arr[1]是第二个元素,1个字节 printf("%d\n", sizeof(&arr));//&arr是数组的地址,是地址就是4/8个字节 printf("%d\n", sizeof(&arr + 1));//&arr + 1是跳过一个数组的地址,4/8 printf("%d\n", sizeof(&arr[0] + 1));//&arr[0] + 1是第二个元素的地址 4/8
char arr[] = "abcdef"; printf("%d\n", strlen(arr));//整个字符串的长度 printf("%d\n", strlen(arr+0));//首元素的地址开始,所以结果同上 //printf("%d\n", strlen(*arr));//错误, arr是数组首元素的地址,*arr就是数组首元素 //printf("%d\n", strlen(arr[1]));//错误,同上,只不过这里传的是第二个元素b printf("%d\n", strlen(&arr));//整个数组的地址,还是从首元素开始 printf("%d\n", strlen(&arr+1));//整个数组后开始计算,所以是随机值 printf("%d\n", strlen(&arr[0]+1));//从第二个元素地址开始算
9.3 字符指针
也可以说是字符数组,数组本身也是指针
char *p = "abcdef"; printf("%d\n", sizeof(p));//p为指针变量,大小为4/8 printf("%d\n", sizeof(p+1));//p+1是'b'的地址 printf("%d\n", sizeof(*p));//*p 就是字符a printf("%d\n", sizeof(p[0]));//同上 printf("%d\n", sizeof(&p));//*p的地址 printf("%d\n", sizeof(&p+1));//*p之后的地址 printf("%d\n", sizeof(&p[0]+1));//&p[0] + 1得到是'b'的地址
char *p = "abcdef"; 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));//从第二个字符开始算
9.4 二维数组
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)); //a[0]作为第一行的数组名,没有单独放在sizeo内部,没有& //a[0]表示数组首元素的地址,也就是a[0][0]的地址 //所以a[0]+1是第一行第二个元素的地址,是地址就是4/8个字节
int a[3][4] = { 0 }; printf("%d\n", sizeof(*(a[0] + 1)));//计算的是就是第一行第2个元素的大小 printf("%d\n", sizeof(a + 1));//a是数组首元素的地址,是第一行的地址 int(*)[4],a+1 就是第二行的地址 printf("%d\n", sizeof(*(a + 1)));//计算的是第二行的大小 printf("%d\n", sizeof(&a[0] + 1));//&a[0]是第一行的地址 int(*)[4],&a[0]+1 是第二行的地址 int(*)[4] printf("%d\n", sizeof(*(&a[0] + 1)));//计算的是第二行的大小 printf("%d\n", sizeof(*a));//计算的是第一行的大小 printf("%d\n", sizeof(a[3]));//计算的是一行的大小,并不存在越界,因为实际并没有访问内存
总结:
数组名的意义:
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3.除此之外所有的数组名都表示首元素的地址。
10. 指针笔试题
10.1 笔试题1
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 + 1)访问的第二个元素,*ptr是跳过整个数组后的地址
10.2 笔试题2
struct Test { int Num; char* pcName; short sDate; char cha[2]; short sBa[4]; }*p= (struct Test*)0x10000000; int main() { printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; }
假设p 的值为0x10000000。 如下表达式的值分别为多少?
已知,结构体Test类型的变量大小是20个字节
第一个结果是跳过整个结构体,所以直接加20,地址是以十六进制打印,所以是10000014
第二个结果是先将结构体地址强转为长整形,而整形计算则是直接加1
第三个结果是先将结构体地址强制转换为指针,加1则跳过一个指针的大小,即4/8个字节
10.3 笔试题3
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; }
10.4 笔试题4
int main() { int a[3][2] = { (0, 1), (2, 3), (4, 5) }; int* p; p = a[0]; printf("%d", p[0]); return 0; }
这里需要注意的是,在初始化时,我们只初始化了前3个值,因为这里面放的是()而不是{},所以编译时只将逗号表达式中的数字,即1,3,5,而后都应是0;所以打印指向第一行的值,就只打印a[0][0],即1。
10.5 笔试题5
int main() { int a[5][5]; int(*p)[4]; p = a; printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); return 0; }
10.6 笔试题6
int main() { int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int* ptr1 = (int*)(&aa + 1); int* ptr2 = (int*)(*(aa + 1)); printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1)); return 0; }
&aa+1跳过的是整个数组,所以再-1,打印的整数值即为aa数组最后一个元素
aa+1是跳过二维数组第一行,即6的起始地址,,所以再-1,打印的整数值即为aa数组第一行最后一个元素
10.7 笔试题7
int main() { char* a[] = { "work","at","alibaba" }; char** pa = a; pa++; printf("%s\n", *pa); return 0; }
首先char* a[]存储的就是三个首字母的地址,而char** pa存储的是指针数组中首元素w的地址,所以pa++,pa指向的就是第二个元素a的地址,解引用后打印即为at。
10.8 笔试题8
int main() { char* c[] = { "ENTER","NEW","POINT","FIRST" }; char** cp[] = { c + 3,c + 2,c + 1,c }; char*** cpp = cp; printf("%s\n", **++cpp); printf("%s\n", *-- * ++cpp + 3); printf("%s\n", *cpp[-2] + 3); printf("%s\n", cpp[-1][-1] + 1); return 0; }
结语
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!