当前位置:网站首页>深入探究指针及指针类型
深入探究指针及指针类型
2022-07-06 03:01:00 【iYYu】
目录
前言
本篇为指针的进阶,如果初阶指针还不太明白的伙伴们请戳:初阶指针。
指针的基本概念:
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针变量的大小是4/8个字节(根据32/64位平台)。
- 指针是有类型的,指针的类型决定了指针±整数的的步长,指针解引用操作时可以访问的范围。
- 指针的简单运算。
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
int main()
{
char ch = 'a';
char* pc = &ch;
*pc = 'b';
return 0;
}
还可以写成下面这种形式:
int main()
{
char* pc = "abcd";
//是指针里存了一个字符串吗
printf("%s\n", pc);
return 0;
}
并不是,其实是把字符串的首元素地址a存放到指针pc里,%s只要给出字符串的起始地址,一直打印到\0之前。
因为此字符串为常量字符串,不能被修改,放在未被修饰的指针变量不安全,此时在前面加上const修饰,使其无法被修改,这样就很好的保护了字符串。
const char* pc = "abcd";
一道面试题:
int main()
{
char* p1 = "abcd";
char* p2 = "abcd";
char arr1[] = "abcd";
char arr2[] = "abcd";
if (p1 == p2)
puts("p1 == p2");
else
puts("p1 != p2");
if (arr1 == arr2)
puts("arr1 == arr2");
else
puts("arr1 != arr2");
return 0;
}
//输出的结果是什么?
那么这两种形式的区别在哪?画图分析:
2. 数组指针
在探究数组指针前,看回顾一下什么是指针数组:
指针数组 - 是数组。是用来存放指针变量的数组。
int arr[10];//整形数组
char brr[10];//字符数组
...
int* crr[10];//整形指针数组 - 存放整形指针的数组
char* drr[10];//字符指针数组
指针数组的作用:
int main()
{
int arr[] = {
1,2,3 };
int brr[] = {
2,3,4 };
int crr[] = {
3,4,5 };
//定义指针数组
int* prr[3] = {
arr, brr, crr };
//数组名相当于首元素地址
//把三个数组首元素放在prr里
//是地址,因此prr的类型为整形指针类型
return 0;
}
这种写法,其实也就模拟写成了一个二维数组,这该怎么理解?
那段代码所对应的意义,知道了该指针数组的布局,那么访问并打印prr数组里的内容几乎和二维数组无差:
int main()
{
int arr[] = {
1,2,3 };
int brr[] = {
2,3,4 };
int crr[] = {
3,4,5 };
int* prr[3] = {
arr, brr, crr };
//三个元素,对应下标0 1 2
//找到对应的首元素地址
for (int i = 0; i < 3; ++i)
{
//再用一个下标向后偏移分别找到该数组里的每个元素
for (int j = 0; j < 3; ++j)
{
//prr[i][j] == *(prr[i] + j) == *(*(prr+i)+j)
printf("%d ", *(prr[i] + j));
}
printf("\n");
}
return 0;
}
用一个指针数组巧妙地把三个一维数组结合在一起,好像一个二维数组,这是指针数组的一个作用。
二维数组在内存中是连续存放的,这三个一维数组不一定是,所以说是模拟二维数组。
2.1 数组指针的定义
在了解了指针数组之后,接下来探究数组指针:
整形指针 - 指向整形的指针
字符指针 - 指向字符的指针
…
数组指针 - 是指针 - 指向数组的指针。
下面两条语句分别是什么含义:
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
[ ]的优先级要高于星号的,所以必须加上()来保证p先和*结合。
2.2 & 数组名 VS 数组名
要弄清楚数组指针如何使用,就必须再次理解数组名和&数组名的含义:
数组名通常是代表首元素的地址,但是有两个意外:
- sizeof(数组名)
这里的arr是整个数组,计算的是数组的总大小。
必须是内部单独放一个数组名
- &数组名
这里的数组名表示的依然是整个数组,&数组名取出的是整个元素的地址。(单位是字节)
具体有什么区别?
代码说明:
int main()
{
int arr[5] = {
0 };
//数组首元素地址放在int*的指针
int* p1 = arr;
//取出整个数组的地址放在数组指针p2里
int (*p2)[5] = &arr;
//这两个指针的区别:
//p1存放的是第一个整形元素的地址,指向第一个元素,不能代表整个数组。
//而p2存放的是整个数组arr的地址,指向的是一个数组。
return 0;
}
去掉名字就是它的类型:int* \ int (*)[5]。
后面的方括号必须要带数组的元素个数,否者会警告。
2.3 数组指针的应用
了解了数组指针的基本概念,紧接着就来探究数组指针的用法:
打印数组元素:
int main()
{
int arr[5] = {
1,2,3,4,5 };
int (*p)[5] = &arr;
//存放整个数组的地址
for (int i = 0; i < 5; ++i)
{
printf("%d ", *(*p + i));
///首先解引用p找到了数组arr首元素的地址
//即*p == arr
//地址+i在解引用向后偏移找到每个元素
}
return 0;
}
这种用法用起来很别扭,其实是非常不合理的写法。
正常的写法应该是:
int main()
{
int arr[5] = {
1,2,3,4,5 };
int* p = arr;
for (int i = 0; i < 5; ++i)
{
printf("%d ", *(p + i));
//存放首元素地址p的指针
//p+i找每个元素地址
//解引用找到元素
}
return 0;
}
这种方法和第一种比起来清晰程度不言自明。
数组指针的作用并不体现在一维数组上,在二维或者多维数组才是数组指针的妙用。
接下来探究数组指针正确的用法。
函数打印二维数组:
//有两种写法:
//数组传参数组接收:int arr[3][4]
//数组传参实际上传过来的是首元素地址
//这里形参写成指针的形式该怎么写?
void print(???, int r, int c)
{
}
int main()
{
int arr[3][4] = {
1,2,3,4,2,3,4,5,3,4,5,6 };
print(arr, 3, 4);
return 0;
}
既然二维数组数组名是一它第一行一维数组的地址,那么形参就要设置一个指向一维数组地址的指针:
(*pa)是指针,指向数组,数组有四个元素(*pa)[4],每个元素是int类型
int (*pa)[4]
void print(int (*pa)[4], int r, int c)
{
//pa+i指向第i行
for (int i = 0; i < r; ++i)
{
//pa+i解引用得到当前行的首元素地址
for (int j = 0; j < c; ++j)
{
//首元素地址+j再解引用得到当前元素
printf("%d ", *(*pa + i) + j));
//printf("%d ", pa[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][4] = {
1,2,3,4,2,3,4,5,3,4,5,6 };
print(arr, 3, 4);
return 0;
}
为什么数组指针+1跳过一个数组:
pa 的类型是一个数组指针:int (*)[4]
pa是指向一个数组,数组4个整形元素
因此pa+1跳过一个4个int元素的数组.
了解了指针数组和数组指针来回顾并看看下面代码的意思:
int arr[5];
//arr是整形数组
int *parr1[10];
//parr1是整形指针数组
int (*parr2)[10];
//parr2是整形数组指针
int (*parr3[10])[5];
//parr3是存放数组指针的数组
int main()
{
int arr[] = {
1,2,3 };
int brr[] = {
1,0,1 };
int crr[] = {
1,2,3 };
int (*parr[3])[3] = {
&arr, &brr, &crr };
for (int i = 0; i < 3; ++i)
{
int (*pa)[3] = parr[i];
//把parr数组的元素依次赋值给数组指针pa
for (int j = 0; j < 3; ++j)
{
//pa是第i个数组的地址
//先解引用得到当前数组首元素地址
//再利用j偏移解引用分别得到每个元素
printf("%d ", (*pa)[j]);
}
printf("\n");
}
return 0;
}
3. 数组传参和指针传参
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
3.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);
return 0;
}
上面的传参形式都是正确的。
3.2 二维数组传参
void test(int arr[3][5])//ok?1
{
}
void test(int arr[][])//ok?2
{
}
void test(int arr[][5])//ok?3
{
}
void test(int *arr)//ok?4
{
}
void test(int* arr[5])//ok?5
{
}
void test(int (*arr)[5])//ok?6
{
}
void test(int **arr)//ok?7
{
}
int main()
{
int arr[3][5] = {
0};
test(arr);
return 0;
}
2:error,原因:二维数组传参,函数形参的设计只能省略第一个[ ]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。
4:error,原因:二维数组的数组名表示的是首元素地址,是第一行的地址。
第一行可以看成是包含5个整形元素的数组的地址,既然是数组的地址放在一级指针肯定是错的,应该用数组指针。
5:error,原因:arr先和[ ]结合,是数组,每个元素是int*,因此错误。
7:error,原因:二级指针是存放一级指针的,而数组的地址没法存放到二级指针。
3.3 一级指针传参
如果函数的形参部分是一级指针,那么调用时实参可以写成什么?
void print(int *p)
{
}
int main()
{
> int a = 10;
print(&a);
> int* pa = &a;
print(pa);
> int arr[10];
print(arr);
return 0;
}
如果形参是一级指针,实参可以传整形的地址、整形指针和数组名(本质都是指针)。
3.4 二级指针传参
如果函数的形参部分是二级指针,那么调用时实参又可以写成什么?
void test(int** ptr)
{
}
int main()
{
int* p1;
test(&p1);
int* *p2 = &p1;
test(p2);
int* arr[10];
test(arr)
return 0;
}
形参是二级指针,实参可以:取地址一级指针、二级指针和指针数组。
4. 函数指针
数组指针是指向数组的指针,而函数指针就是指向函数的指针。
&数组名取出的是数组的地址:
int main()
{
int arr[5] = {
0 };
int(*pa)[5] = &arr;//数组指针
return 0;
}
那么&函数名取出的是函数的地址吗?
int Add(int x, int y)
{
return x + y;
}
int main()
{
//取出函数的地址
printf("%p\n", &Add);
return 0;
}
因此函数也是有地址的。
对于函数来说:&函数名和函数名都是函数的地址。
因此和printf("%p\n", Add);
没有区别。
取出了函数的地址,要把它存起来,该怎么写?
其实是和数组指针非常类似:
(*pf)说明它是指针,(*pf)()这一对圆括号说明指向的是函数(函数调用操作符()),它指向的函数参数类型是int, int,返回类型是int,int (*pf)(int, int),指针pf存放的函数Add的地址。
int (*pf)(int, int) = &Add
int (*pf)(int x, int y) 参数名可以省略不写,只要参数类型即可。
既然有函数指针这个类型,那么就一定会有它的作用,接下来探究函数指针的作用:
把一个整形的地址取出来放在一个指针里,对指针解引用就可以找到并且修改它,对于函数指针来说,道理是一样的。
比如说利用指针来间接调用函数:
int Add(int x, int y)
{
return x + y;
}
int main()
{
//直接调用
//int ret = Add(2, 3);
//取地址放到指针pf里
int (*pf)(int, int) = &Add;
//这个&符可以省去
//利用指针间接调用
//对指针解引用,调用函数的同时传参
int ret = (*pf)(2, 3);
printf("%d\n", ret);
return 0;
}
注:这种写法也是对的,
int ret = pf(2, 3);
,可以不写*号,完全等同于int ret = Add(2, 3);
这也就解释了为什么可以省略不写星号,上面带星号是为了更方便理解。
但是如果写上面那种形式就必须要带括号,否则就不是函数指针了。
通过上面代码的理解,也许会觉得函数指针有点多此一举,反正都一样,通过指针调用还把代码搞复杂了,其实函数指针在这个环境下是比较复杂。
但在其他环境里函数指针还是非常有作用的,之后会介绍它的其它妙用。
先来看看两段有趣的代码:
//代码1
( *( void (*)() )0 )();
//代码2
void ( *signal(int , void(*)(int) ) )(int);
第一个:
第二个:
虽然解释了它的含义,但还不是不太方便理解,函数参数是函数指针,返回类型也是函数指针,有些绕。
这时可以使用typedef来把该函数声明进行优化:
typedef unsigned int u_int;
//其实就是把unsigned int重命名为u_int
//把函数指针优化
void(*)(int)
//把void (*)(int)类型重命名为pf_t
下面改造该代码:
typedef void (*pf_t)(int);
int main()
{
void (* signal(int, void (*)(int) ) )(int);
pf_t signal(int, pf_t);
return 0;
}
这里来介绍一下函数指针的作用。
写一个简易的计算器,这个计算机包含加法和减法(乘法除法原理于与之相同,明白它的含义就好,不再实现):
//选1加法
//选2减法
//选0退出
int Sub(int x, int y)
{
return x - y;
}
int Add(int x, int y)
{
return x + y;
}
void menu()
{
puts("*******************");
puts("***** 1. Add ****");
puts("***** 2. Sub ****");
puts("***** 0. exit ****");
puts("*******************");
}
int main()
{
int input = 0;
do
{
menu();
printf("请输入功能:");
there:
scanf("%d", &input);
int x = 0;
int y = 0;
switch (input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", Add(x, y));
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", Sub(x, y));
break;
case 0:
printf("Exit calcu!\n");
break;
default:
printf("错误!请重新输入:\n");
goto there;
break;
}
} while (input);
return 0;
}
测试结果:
虽然运行结果没问题,但是不难发现代码的实现有些冗余,如果加上了乘法除法的代码的重复部分会更多。
这时就体现出了函数指针的巧妙:
封装一个函数calc来负责加减计算,因为函数名就是函数地址,可以把实现加减的函数地址作为calc的参数,选择1传入Add函数的地址,选择2传入Sub的地址,再利用函数指针来找到并调用当前函数。
void menu()
{
puts("********************");
puts("***** 1. Add *****");
puts("***** 2. Sub *****");
puts("***** 0. exit *****");
puts("********************");
}
int Sub(int x, int y)
{
return x - y;
}
int Add(int x, int y)
{
return x + y;
}
//计算
//形参要设置函数指针
void calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", pf(x, y));
}
int main()
{
int input = 0;
do
{
menu();
printf("请输入功能:");
there:
scanf("%d", &input);
int x = 0;
int y = 0;
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 0:
printf("Exit calcu!\n");
break;
default:
printf("错误!请重新输入:\n");
goto there;
break;
}
} while (input);
return 0;
}
这里就巧妙地使用了函数指针来实现相对于简洁的代码,如果没有函数指针的概念,固然是没办法写成这种形式。
这种形式也叫做回调函数,通过函数指针,在适当的时候回头调用它所指向的函数。
5. 函数指针数组
把函数指针放在数组中就是函数指针数组。
上面的两个函数Add和Sub,因为函数名就是函数的地址,把这两个函数放进数组里,数组的元素就是函数指针,而数组就叫做函数指针数组:
不妨来推导一下它的类型该怎么写,其实和函数指针比较类似:
int (*pf)(int, int) = Add;
- 这是函数指针。
实际上把pf换成数组的形式,就是函数指针数组:int (*arr[2])(int, int) = {Add, Sub};
(元素个数可以省略)。
arr先和[ ]结合,是数组,把数组名arr和[ ]拿开,就是它的类型:函数指针,它的每个元素是函数指针。
int Sub(int x, int y)
{
return x - y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
//函数指针数组
int (*arr[2])(int, int) = {
Add, Sub};
return 0
}
访问这个数组:
int Sub(int x, int y)
{
return x - y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*arr[2])(int, int) = {
Add,Sub };
for (int i = 0; i < 2; ++i)
{
int ret = arr[i](8, 4);
printf("%d\n", ret);
}
return 0;
}
既然有函数指针数组这种形式,也就一定会有它的作用,利用这个数组,再把上面的计算器进行优化。
不仅仅想要实现加减乘除,还想要实现x & y、x | y、x ^ y…等等
如果要增加的多的功能,那么对应的case语句也必然也会增加,整体的代码就不容易阅读了,那可以用什么办法来简化代码?
这时利用函数指针数组,可以很大程度上优化代码,让其变得十分简洁。
代码的实现:首先需要创建一个函数指针数组 -int (*arr[])(int, int) = { 0,Add,Sub };
在最前面放个0,把两个函数的下标向后面推了一位,因此就可以根据input输入的值来直接对应其下标。
如果input = 0;直接退出计算器,再如果input>=1 && input <=2,用数组下标实现对应的·函数调用,否则重新输入,代码实现:
void menu()
{
puts("********************");
puts("***** 1. Add *****");
puts("***** 2. Sub *****");
puts("***** 0. exit *****");
puts("********************");
}
int Sub(int x, int y)
{
return x - y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pfArr[])(int, int) = {
0,Add,Sub };
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请选择功能:");
there:
scanf("%d", &input);
if (!input)
{
puts("Exit calc!");
}
else if (input >= 1 && input <= 2)
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
int ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else
{
puts("请重新输入!");
goto there;
}
} while (input);
return 0;
}
非常巧妙,利用函数指针数组大大简化了代码。
后面如果想增加一些功能,只需要实现要增加的函数功能,再数组里加上函数名,再把判断条件修改一下,调用起来非常方便且代码简洁。
6. 指向函数指针数组的指针
函数指针数组,也是数组,那么就会有它的地址,取出该数组的地址,就应该存放到指向【函数指针数组】的指针,那么该指针的形式该怎么写?
int (*pfArr[])(int, int) = { 0,Add,Sub };
这是函数指针数组,根据这个形式来改:
首先它是个指针(*ppfArr),指向的是一个数组(*ppfArr)[ ],数组的每个元素是函数指针( *(*ppfArr)[ ] )(),函数的参数是int, int,返回值是intint ( *(*ppfArr)[] )(int, int) = &pfArr;
这便是指向函数指针数组的指针。
int main()
{
//这是函数指针数组
int (*pfArr[])(int, int) = {
0,Add,Sub };
//指向函数指针数组的指针
int (*(*ppfArr)[3])(int, int) = &pfArr;
return 0;
}
既然是指针,它也可以存放在一个数组里去,也就是【指向函数指针数组的指针】的数组,这也是个数组,既然是数组也可…
7. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
首先介绍qsort函数的用法。
qsort是C语言的一个库函数,使用快速排序的思想实现的一个排序函数,它可以排序任意数据。
qsort参数:void qsort( void *base, size_t num, size_t width, int (*cmp)(const void *e1, const void *e2 ) );
四个参数:void* base
- 需要排序的数据的起始位置size_t num
- 待排序数据的元素个数size_t width
- 待排序数据元素的大小(字节)int (*cmp)(const void *e1, const void *e2 )
- 函数指针(比较函数),调用该指针指向的函数,参数e1和e2是要比较的两个元素的地址,因此该函数什么类型的数据都可以比较。注意:第四个函数指针指向的函数其参数是
void*
的指针,而void*
的指针是无具体类型的指针,可以接收任意类型的地址,又因如此,所以void*
的指针是无法进行解引用操作,也不能±整数的操作(因为并不知道传的是什么类型的地址,之能设置为void*
类型),因此就需要强制类型转换为目标类型指针。该比较函数的返回值:
利用qsort来实现数组排序:
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);
}
int main()
{
int arr[] = {
9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(int), cmp_int);
for (int i = 0; i < sz; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
qsort排序结构体成员:
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
//利用strcmp来比较字符串
return strcmp( ((struct Stu*)e1)->name, ((struct Stu*)e2)->name );
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int main()
{
struct Stu s[] = {
{
"zhangsan", 15}, {
"lisi",20}, {
"wangwu",25} };
int sz = sizeof(s) / sizeof(s[0]);
//通过名字排序
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
//通过年龄排序
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
return 0;
}
名字排序
年龄排序:
在了解了qsort函数的使用,接下来基于冒泡排序的思想改造qsort函数:
void Swap(char* p1, char* p2, int width)
{
for (int i = 0; i < width; ++i)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
++p1;
++p2;
}
}
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void bubble_sort(void* base, int num, int width, int(*cmp)(const void* e1, const void* e2))
{
for (int i = 0; i < num - 1; ++i)
{
int f = 1;
for (int j = 0; j < num - i - 1; ++j)
{
if (cmp( (char*)base + j * width, (char*)base + (j + 1) * width ) > 0)
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
f = 0;
}
}
if (f == 1)
{
break;
}
}
}
int main()
{
int arr[] = {
9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(int), cmp_int);
for (int i = 0; i < sz; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果:
以上便是对指针深入的探究,指针类型有很多一不小心就容易搞混,所以在学习指针时要认真且大量的反复观看一熟练掌握指针。
边栏推荐
猜你喜欢
BUUCTF刷题笔记——[极客大挑战 2019]EasySQL 1
Sign SSL certificate as Ca
建模规范:命名规范
Introduction to robotframework (II) app startup of appui automation
Linear regression and logistic regression
主数据管理(MDM)的成熟度
2345 file shredding, powerful file deletion tool, unbound pure extract version
[network security interview question] - how to penetrate the test file directory through
How to accurately identify master data?
My C language learning records (blue bridge) -- files and file input and output
随机推荐
codeforces每日5题(均1700)-第六天
Apt installation ZABBIX
RobotFramework入门(一)简要介绍及使用
八道超经典指针面试题(三千字详解)
Differences and application scenarios between resulttype and resultmap
纯Qt版中国象棋:实现双人对战、人机对战及网络对战
Redis SDS principle
[matlab] access of variables and files
My C language learning records (blue bridge) -- files and file input and output
How to improve the enthusiasm of consumers when the member points marketing system is operated?
1003 emergency (25 points), "DIJ deformation"
[ruoyi] set theme style
JS events (add, delete) and delegates
Prototype design
How does yyds dry inventory deal with repeated messages in the consumption process?
js 正则过滤和增加富文本中图片前缀
【 kubernets series】 a Literature Study on the Safe exposure Applications of kubernets Service
C language - Blue Bridge Cup - promised score
Pure QT version of Chinese chess: realize two-man, man-machine and network games
这些不太会