目录
一、数组名的理解
不知道童鞋们有没有看过、写过这样的代码:
int arr[10] = { 0 };
int* p = &arr[0];
这里我们通过&arr[0]
拿到了arr数组的第一个元素的地址,但其实通过arr
我们也能直接拿到arr数组的第一个元素的地址,因为arr
,即数组名就是地址,且一般情况下代表的就是数组首元素的地址。
我们来做个测试:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);//%p以十六进制打印地址
printf("arr = %p\n", arr);
return 0;
}
输出结果(x86即32位系统环境下输出结果):
(地址不唯一,向内存申请空间后临时分配)
这时候有同学会有疑问?数组名如果是数组⾸元素的地址,那下⾯的代码怎么理解呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
输出的结果是:40
如果arr
是数组⾸元素的地址,那输出结果应该是4或者8才对。(地址在32位环境下有32/8个字节,在64位环境下有64/8个字节)
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节
&数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
这个知识点知道以后,可能有好奇的童鞋想问:整个数组的地址和数组首元素的地址的区别在哪里呢?童鞋们可以试试下面的代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
尝试过后,童鞋们可能会发现这三个打印结果都相同,那么到底arr
和&arr
的区别在哪里呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);//打印arr[0]的地址
printf("&arr[0]+1 = %p\n", &arr[0] + 1);//打印arr[0]的地址加1
printf("arr = %p\n", arr);//打印arr
printf("arr+1 = %p\n", arr + 1);//打印arr加1
printf("&arr = %p\n", &arr);//打印&arr
printf("&arr+1 = %p\n", &arr + 1);//打印&arr加1
return 0;
}
输出结果:
对于结果1、2和结果3、4,看过上一篇《C语言修炼——还不会指针?一次讲明白!第一弹!!》中指针运算一节的童鞋应该都知道,&arr[0]
和&arr[0]+1
相差4个字节,arr
和arr+1
相差4个字节,是因为&arr[0]
和arr
都是⾸元素的地址,+1
就是跳过⼀个元素。
但是&arr
和&arr+1
相差40个字节,这就是因为&arr
是数组的地址,+1
操作是跳过整个数组的。到这⾥⼤家应该搞清楚数组名的意义了吧。
数组名是数组⾸元素的地址,但是有2个例外。
二、使用指针访问数组
有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);//数组大小为40,首元素大小为4,得到数组长度
int* p = arr;//指针p指向数组首元素的地址
//输入
for (int i = 0; i < sz; i++)
{
scanf("%d", p+i);//指针访问数组的方法
}
//输出
for (int i=0; i < sz; i++)
{
printf("%d ", *(p+i));//指针解引用访问数组元素
}
return 0;
}
这个代码搞明⽩后,我们再试⼀下,如果我们再分析⼀下,数组名arr
是数组⾸元素的地址,可以赋值给p
,其实数组名arr
和p
在这⾥是等价的。那我们可以使⽤arr[i]
可以访问数组的元素,那p[i]
是否也可以访问数组呢?
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
//输入
for(int i = 0; i < sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i=0; i<sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
运行后,我们发现,这段代码也可以正常打印,说明指针p
和数组名arr
本质上是一致的,所以对于数组arr[i]
是与arr + i
等价的,对于指针p + i
是与p[i]
等价的。
数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。所以我们以后在使用数组或者指针的时候,ptr + i
与ptr[i]
都是可以的。
三、一维数组传参的本质
数组我们学过了,之前也讲了(C语言修炼——数组与函数全析!!!),数组是可以传递给函数的,这个⼩节我们讨论⼀下数组传参的本质。
⾸先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,函数内部求数组的元素个数吗?
#include<stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz2);
}
int main()
{
int arr[10] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz1);
test(arr);
return 0;
}
代码结果:
显然,在test
函数内部是没有办法求出数组长度的。为什么呢?
在前面,我们已经说过,除了两种特殊情形外,数组名在一般情况下代表的是数组首元素的地址。
在此处,test(arr)
中传递的参数arr
就是arr数组首元素的地址。所以我们在test
函数内部得到的形参arr
实际上代表的是arr
数组首元素的地址,所以我们在计算的时,实际上是拿地址的大小除以int
类型的大小(地址的大小在x86(32位)环境下是4个字节,在x64(64位)环境下是8个字节),此处是x86环境,所以第二行的结果是1,如果在x64环境下则结果为2。
所以我们可以得到结论:
void test(int arr[])
等价于
void test(int* arr)
即参数写出数组形式,但其本质上还是指针。
总结:
一维数组传参,形参可以写成数组形式,也可以写成指针形式。
四、冒泡排序
下面我们借用指针的知识来写个冒泡排序熟悉一下所讲的知识。
#include<stdio.h>
void bubble_sort(int* arr, int sz)
{
for (int i = 0; i < sz - 1; i++)
{
int flag = 1;//假定数组有序,flag为1
for (int j = 0; j < sz - 1 - i; j++)
{
//当if语句一次也不执行,数组已经有序
if (arr[j] > arr[j + 1])
{
flag = 0;
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1)//数组有序,提前跳出循环
break;
}
}
int main()
{
int arr[] = { 0, 8, 9, 6, 5, 2, 1, 4, 3, 11 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组长度
printf("排序前:\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);//也可以使用*(arr+i)的形式
}
bubble_sort(arr, sz);//传递arr数组首元素的地址和数组长度
printf("\n排序后:\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);//也可以使用*(arr+i)的形式
}
return 0;
}
这里的冒泡排序函数bubble_sort
,我在这里做了一个优化,我们容易知道,对于数列1 0 2 3 4 5 6 7 8 9
和数列9 8 7 6 5 4 3 2 1 0
两者的“无序程度”是不同的(这里单就从小到大排序而言),所以对于前者,无序程度小的数列,我们需要判断数列是否有序,以避免无意义的循环判断并提前跳出排序,在这里flag
就起到了记录数列是否有序的作用,我们假定数列有序,如果执行了一次if
语句就代表数列无序,故令flag = 0
;如果一次if
语句都没有执行,就容易知道,数列已经有序,我们可以判断后面的循环与判断都无意义,flag
仍为1
,进入if (flag == 0)
语句内,执行breka
跳出循环,提前结束排序。
总结:
我们需要准确理解指针、地址、数组名之间的关系,三者在一定意义下是等价关系的。
五、二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
这就得讲到二级指针了。
假设存储变量a
、一级指针变量pa
、二级指针变量paa
的内存空间地址分别为0x00efca0c
、0x00efca00
、0x00efcc00
(无实际意义),那么很容易知道,a
的内存空间中存储的为10
,pa
的内存空间中存储的为a
的地址,paa
的内存空间中存储的为pa
的地址。
对于二级指针的运算有:
*ppa
通过对ppa
解引用得到ppa
内存空间中存储的pa
的地址,找到pa
, 即*ppa
得到指针变量pa
。**ppa
先通过*ppa
得到指针变量pa
,然后对pa
进⾏解引⽤操作:*pa
,得到变量a
。
六、指针数组
我们知道,整型有:short,int,long,long long
;字符型有:char
;浮点型有:float,double
;布尔型有:bool
。整型、字符型的各个类型还有有无符号之分,即signed
或unsigned
,同时这些类型都可以作为数组元素的类型创建数组。
我们需要知道指针变量也是变量,只是因为这个变量的类型是指针类型,所以我们特别称呼它为指针变量而已。上一篇《C语言修炼——还不会指针?一次讲明白!第一弹!!》的2.2.2节中我们已经讲过了如何拆解指针类型,而指针类型有哪些也都是建立在前面我们已知的类型之上。
这一part的重点就在于如何理解指针数组,数组指针到底是指针还是数组?
其实我们类比一下就知道了,整型数组,是存放整型变量的数组;字符数组,是存放字符变量的数组。
那指针数组呢?存放指针变量的数组。
int arr[5] = { int, int, int, int, int };//整型数组
char arr[5] = { char, char, char, char, char };//字符数组
int* arr[5] = { int*, int*, int*, int*, int* };//整型指针数组
容易知道,指针数组的每个元素都是用来存放地址(指针)的。指针数组的每个元素都是地址,同时又指向一块内存空间。
七、指针数组模拟二维数组
我们在之前的篇章中(C语言修炼——数组与函数全析!!)已经知道,数组在内存中都是连续存放的,二维数组当然也是。
int arr[3][3] = { };
当我们直接创建一个二维数组时,该数组在内存中的存储如上图所示,连续存储且arr[0],arr[1],arr[2]
相当于二维数组中存储的对应的一维数组数组名。
之前说过,如果我们把⼀维数组做为数组的元素,这时候就是⼆维数组,这就与二级指针联系起来了。显然二维数组中存储的是一维数组,那每个元素,即每个一维数组应该也有自己的地址。由此,我们可以借用指针数组,存储每个一维数组的首元素的地址,通过指针数组结合指针运算与解引用操作,我们既可以找到需要的一维数组,也可以找到对应一维数组中的所有元素。
//arr1,arr2,arr3为对应一维数组首元素地址,类型为int*
int* parr[3] = { arr1, arr2, arr3 };
代码演示:
#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*
//int*指针类型拆解:
// int* arr,'*'结合'arr'代表arr变量是指针变量
// int代表该指针变量指向一个整型变量
int* parr[3] = { arr1, arr2, arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
需要注意,指针数组模拟出来的二维数组本质上并不是真正的二维数组,因为任何数组在内存中存储都是连续的,而指针数组模拟的二维数组中的每个元素—一维数组的存储并不连续。
八、字符指针变量
在指针类型中我们知道有一种指针叫作字符指针char*
。
一般情况下使用:
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 's';
return 0;
}
还有一种使用方法如下:
int main()
{
//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
const char* pstr = "hello world.";
printf("%s\n", pstr);
return 0;
}
其实这里非常容易让童鞋们误会。const char* pstr = "hello world."
本质上其实是把字符串"hello world."
首字符的地址放到了pstr
中。
《剑指offer》中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char* str3 = "hello world.";
const char* str4 = "hello world.";
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;
}
看出名堂的童鞋不要急,没看出来的童鞋也不要慌。
我们来分析一下:
首先,char str1[]
和char str2[]
很明显是两个字符数组,我们不要被两个数组存储的相同的字符所迷惑,因为本质上我们是向内存申请分配了两块空间,创建了两个数组,但刚好两块空间存储的字符串一致而已。
但是,对于const char*
类型的两个字符指针变量str3
和str4
而言,
const char* str3 = "hello world.";
const char* str4 = "hello world.";
这两段语句所做的都是将字符串hello world.
首字符的地址存储到str3
与str4
中,所以两者最终指向的都是同一片空间。
因此,很明显,我们可以知道,str1
与str2
是!=
的,而str3
与str4
是==
的。
九、数组指针变量
9.1 数组指针变量是什么?
前面我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
那么数组指针变量是指针变量?还是数组?
答案是:指针变量。
我们已经熟悉:
- 整形指针变量:
int* p
, 存放的是整型变量的地址,能够指向整型数据的指针。 - 浮点型指针变量:
float* p
,存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
下⾯代码哪个是数组指针变量?
int* p1[10];
int (*p2)[10];
思考⼀下:p1, p2分别是什么?
数组指针变量:
int (*p)[10];
解释:p
先和*
结合,说明p
是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p
是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[]
的优先级要⾼于*
号的,所以必须加上()
来保证p
先和*
结合。
所以我们可以知道上面的p1,p2分别是指针数组、数组指针。
9.2 数组指针变量的初始化
数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们学习过的&
数组名 。
int arr[10] = { 0 };
int (*p)[10] = &arr;//得到数组的地址
而&arr
与p
的类型是完全一致的,都为int[10] *
。
数组指针类型解析:
int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
十、二维数组传参的本质
有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},
{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是int [5]
,所以第⼀⾏的地址的类型就是数组指针类型int(*)[5]
。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。
如下:
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},
{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
十一、函数指针变量
11.1 函数指针变量的创建
通过前面学过的整型指针,字符指针,数组指针,我们很容易知道,所谓的函数指针变量不过是存储函数的地址的指针罢了。
有的童鞋可能有疑惑了,怎么函数也有地址呢?
我们可以测试一下这段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
结果:
可以看到,确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过&
函数名的⽅式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针⾮常类似。
如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;//加不加&皆可
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//形参的变量名x、y写上或者省略都可以
函数指针类型解析:
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向的函数 的参数类型和个数的交代
| 函数指针变量名
pf3指向的函数 的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
11.2 函数指针变量的使用
通过函数指针调⽤指针指向的函数:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}
结果:
十二、两段有趣的代码
代码1:
(*(void (*)())0)();
我们一层一层的剥开来看:
*(void (*)())0
一个数字0的前面有一个括号和一个*
,0代表什么呢?
我们知道十六进制的地址一般是这种形式0x00ef00ca
,那么0
可不可以是地址呢?
如果这样一想,这里是不是很像强制类型转换呢?没错,0
的类型为整型,这里实际上是将0
当作地址强制类型转换成函数指针类型void (*)()
,然后通过*
进行解引用访问,而徘徊在左边括号外,靠近分号的这个括号是不是也很容易就可以知道它是我们平时用来传参的括号呢。
所以这一条语句其实是一条函数调用语句,意思是在0
这个地址上存储着一个返回值类型为void
,参数为空的函数(当然0这个地址是不能访问的,所以这是一条错误语句)。
代码2:
void (*signal(int, void(*)(int)))(int);
这个该如何理解呢?我们可以知道signal
肯定是个符号,是个名称,再结合signal
后面的括号,我们可以判断signal
一定是个函数,而( int, void(*)(int) )
中的int
与void(*)(int)
都是形参。
不知道大家还记不记得函数指针变量的创建呢?
是不是void (*p)()
或int (*p)(int, int)
呢?再一结合signal
函数的返回值类型,我们是不是恍然大悟呢?
原来这是一条函数声明语句,不规范的表示一下
void(*)(int) signal(int, void(*)(int))
这样是不是就很容易理解了呢?所以这条语句本质上是创建了一个signal
函数,函数的形参为整型int
与函数指针类型void(*)(int)
,返回值类型为函数指针类型void(*)(int)
。
两段代码均出⾃:《C陷阱和缺陷》这本书。有兴趣的同学建议可以搜来看看。
12.1 typedef关键字
typedef
是⽤来类型重命名的,可以将复杂的类型,简单化。
我们通常是不会直接用前面那么复杂的类型的,毕竟这非常不方便且可读性比较差。这时我们就用到了typedef
了。
typedef void(*pfun_t)(int);//重定义类型名称
pfun_t signal(int, pfun_t);//直接用重定义的名称创建该类型变量或函数
十三、函数指针数组
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,
比如:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])();//是它吗?
int *parr2[3]();//还是它?
int (*)() parr3[3];//或者是它?
答案是:parr1。
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("*** 0:exit ***\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;
}
这是比较繁杂的写法,但我们现在学了函数指针数组,我们可以发现case1,case2,case3,case4
其实是非常相似的,
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
其内在逻辑都是输入操作数——调用函数得到返回值——打印并break。我们是否可以用函数指针数组简化一下这段代码呢?
#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 }; //转移表
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf( "请选择:" );
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输⼊操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
printf( "ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出计算器\n");
}
else
{
printf( "输⼊有误\n" );
}
}while (input);
return 0;
}