目录
第1关:登月纸桥
任务描述
本关任务:编写一个函数,计算需要把纸折叠多少次(假设纸张足够大,可以无限次折叠),其厚度才能搭建一座登月纸桥,考虑到将来需要到更远的星球,所以函数需要根据具体距离计算纸张折叠的次数并返回。
相关知识
函数是一个命名的程序代码块,是程序完成其操作的一种功能单位。在程序设计中,有许多算法是通用的,例如求一个数的平方根,求一个三角函数等,经常会将这些算法定义为一个函数,这样在程序中需要这些算法的地方就可以直接使用(通过函数调用)它们了。
虽然 C 和 C++ 的库函数已经提供了丰富的功能,但很多时候程序员还是需要根据具体问题的需求定义自己的函数。函数需要先定义再使用(函数调用)。
函数的定义
函数定义的一般格式为:
<返回值类型> <函数名>(<参数列表>)
<函数体>
-
<返回值类型>
、<函数名>
及<参数列表>
构成了函数头。 -
<返回值类型>
说明函数返回值的数据类型,也称为函数的返回类型。
它可以是任一基本数据类型或用户自定义的数据类型;如果无返回值,则用关键字 void 说明。默认的返回类型是 int,即若未指定返回类型,则返回类型是 int 。
-
<函数名>
是程序员为该函数指定的名字,函数名需要遵守标识符命名规定。 -
<参数列表>
指明函数的参数的个数、名称和类型,函数定义中的参数称为形式参数,简称形参。
当有多个形式参数时,用逗号分隔。如果函数没有形参,参数列表为空,函数名后面的圆括号不能少,这时也可在括号中加上关键字 void,表示这是一个无参函数。
<函数体>
描述函数的功能,即函数所完成的具体操作,它由一系列说明语句和执行语句组成。
函数体实际上是一个复合语句,花括号不能少,它指明函数体的开始和结束。函数执行时,如同执行一个复合语句一样,顺序执行函数体,直到遇到 return 语句或者遇到表示函数体结束的那个右花括号为止,函数执行完毕后,返回调用程序继续执行。
例如,下面的程序定义了一个名为 max 的函数,该函数有3个形式参数 a、 b 和 c,类型均为 int,返回值也是 int 类型。函数体由一个变量说明语句、一个赋值语句和一个返回语句构成。函数的功能是求 a 、b 和 c 中最大的值:
int max(int a, int b, int c)
{
int m;
m = (a > b) ? a : b;
return (m > c) ? m : c;
}
注意:
-
所有函数的定义是并列的、平行的,在一个函数定义内部不允许定义另外一个函数。但可以对别的函数进行调用或作引用说明;
-
函数定义中声明的所有变量都是局部变量,只在声明语句所处的程序块中有效。大多数函数都有一组参数,函数定义时指明了每个形式参数的名字、类型,函数调用时提供的实在参数要与形式参数一一对应(函数调用时需要进行参数传递,将实参的值一个个传给对应的形参)。函数的形式参数也可视为局部变量,在函数体范围内有效;
-
函数可以有返回值,也可以没有返回值;
-
函数没有返回值时,返回类型必须用关键字 void 说明。这类函数的函数体内没有 return 语句或 return 语句中无表达式。当函数执行到 return 语句时,程序返回到调用该函数的地方;如果函数体没有 return 语句,当执行完函数体最后的语句后,程序再返回到调用该函数的地方;
-
函数有返回值时,必须指明返回值类型(不能是 void ),此时函数体必须包含带表达式的 return 语句。表达式的值将返回给调用程序,该值的数据类型必须与返回值类型一致;
-
函数体可有多个 return 语句。调用函数执行时,只要遇到一个 return 语句,就马上忽略剩余代码,立刻返回到调用程序;
-
函数需要先定义后使用,但函数的相互调用关系很难保证所有函数都是先定义后使用的。这时就需要先声明函数原型了。
声明函数
为了让 main 程序(或者其他准备调用函数的程序)知道函数的相关信息,需要在调用程序的前面,以“函数原型”的方式对被调用的函数进行声明。
在 C 和 C++ 中,函数原型的一般形式为:
<返回值类型> <函数名>(<参数列表>);
函数原型形式上只是比函数头多了一个分号,函数原型参数列表中形参的名称可以省略。函数原型告诉编译器后面将会定义这么一个函数,其函数名、返回值类型、参数列表分别是什么,便于编译器检查函数调用的正确性。
例如,前面函数 max 的函数原型可以为:
int max(int , int , int);
建议:如果一个文件中定义多个函数时,把主函数之外的其它函数的函数原型都放在文件中所有函数定义的前面,这样所有函数调用就都符合“先声明,后使用”的原则了。
#include <iostream>
using namespace std;
// foldTimes-计算建纸桥的折叠次数
// 参数:dis-星际距离(千米),thick-纸的厚度(毫米)
// 返回值:建桥需要折叠的次数
int foldTimes(double dis, double thick);
int main()
{
double dis, thick;
cin >> dis >> thick;
cout << "需要折叠" << foldTimes(dis,thick) << "次" << endl;
return 0;
}
int foldTimes(double dis, double thick)
{
// 请在这里补充代码,实现函数foldTimes
/********** Begin *********/
int sum = 0;
dis = 1000000 * dis;
while (thick < dis)
{
thick *= 2 ;
sum++;
}
return sum;
/********** End **********/
}
第2关:几点几分了?
任务描述
本关任务:编写一个函数 whatTime,该函数通过秒表显示的秒数,计算当前的时间(几点几分几秒),计算得到的时间通过参数返回。
相关知识
本关内容涉及传引用,是 C++ 对 C 语言的扩充部分, C 语言不包括这部分内容。
由于 C++ 的函数只能返回一个值,而本题要求返回当前时间(几点几分几秒),需要返回三个值,所以没法通过 return 语句一次返回(如果学了结构也可以把三个数据一起打包返回),但 C++ 函数参数提供了另一种返回值的方式:引用参数。
函数调用时,需要进行参数传递,即把实参的值逐个传给对应的形参。对 C++ 而言参数的传递方式有两种:传值和传引用。
参数传值
传值是指值的复制,即把实参的值传递给形参,实参和形参是不同的变量,有各自独立的存储空间,因此函数被调用执行时,只能访问形式参数对应的内存单元,不能访问或修改实在参数的值。
下面的 swap 函数试图交换两参数的值。
#include <iostream>
using namespace std;
// 交换 x 和 y 的值
void swap(int x,int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10, b = 20;
// 调用函数,试图交换
swap(a, b);
cout << "a:" << a << " b:" << b << endl;
return 0;
}
程序的运行结果显示 a 和 b 的值并没被交换。
这是因为 main 函数在执行swap(a,b)
时,是把实在参数 a 和 b 的值传给了 swap 中形式参数 x 和 y,swap 函数中交换的也只是形参 x 和 y 这两个局部变量的值,跟 a 、b 两个变量无关,当 swap 函数执行完后,就返回到 main 函数的调用语句处,继续向下执行,输出 a 、b 的值,因此依然是原来的10和20。
参数传引用
传引用是指调用函数时,把对实参变量的引用传给形式参数,即将实参变量的地址存放到对应的形参的形式单元中。
当程序转入到被调用函数后,在执行函数体过程中,对形式参数的任何引用或赋值都被处理成对相应形式单元的间接访问,即根据形式单元中存放的实参变量的地址找到实参单元,对形式参数的引用或赋值被处理成对该实参单元的访问。
也就是说,对形参的访问实际上就是对实参的访问,可以通过修改实参的方式把数据传出被调用函数。
提示:传引用要求实在参数必须是变量。
修改上面的 swap 函数,实现真正的交换。
#include <iostream>
using namespace std;
// 交换x和y的值
void swap(int &x, int &y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10, b = 20;
// 调用函数,试图交换
swap(a, b);
cout << "a:" << a << " b:" << b << endl;
return 0;
}
输出结果为:a:20 b:10
程序中函数 swap 的参数定义为int &x,int &y
,其中 x 、y 前面的 & 为引用符号,表示 x 、y 是引用变量,调用该函数时,对应参数的传递采用传引用方式。这时函数 swap 中修改 x 和 y 的值,实际上就是在修改实参 a 和 b 的值
#include <iostream>
using namespace std;
void whatTime(int secs, int &h, int &m, int &s)
{
// 请在这里补充代码,设计并实现函数whatTime,使main函数中的函数调用正确
/********** Begin *********/
h = secs / 3600;
m = (secs / 60) - (h * 60);
s = secs % 60;
/********** End **********/
}
int main()
{
int secs; // secs秒表上的秒数
int h, m, s; // 当前时间:h-小时,m-分,s-秒
cin >> secs; // 输入秒表上的秒数
whatTime(secs,h,m,s); // 计算当前时间
cout << h << ":" << m << ":" << s << endl; // 输出当前时间
return 0;
}
第3关:这天星期几?
任务描述
本关任务:编写函数 whatDay,计算某年某月的1号是星期几并返回。
相关知识
要知道某一天是星期几,可以用已知的某一天进行推导。
例如已知公元1年1月1日是星期一,公元2年1月1日则是在星期一基础上加上一整年的天数(要考虑闰年,闰年一年366天,非闰年365天),如果不是1月,例如7月,则要加上1到6月的所有天数,这里也要考虑闰年,因为闰年的二月是29天,非闰年是28天。
这样就可以算出从公元1年1月1日到该年月过了多少天,而过了7天星期不变,所以可以用这种方法推导出公元元年之后的任何一天是星期几。
下面的程序可以计算星期一过了 n 天后是星期几:
w = 1; // 从星期一开始
w = w + n; // n天后
w = w % 7; // 得到0-6,其中0为星期天
// 调整星期天
if(w == 0) w = 7;
#include <iostream>
using namespace std;
// 函数leapYear
int leapYear(int y)
{
if(y % 4 == 0 && y % 100 != 0 || y % 400 == 0)
return 1;
return 0;
}
// 函数whatDay:计算某年某月某日是星期几
// 参数:year-年,month-月
// 返回值:--7分别表示星期一到星期日
int whatDay(int year, int month)
{
// 请在这里补充代码,实现函数whatDay
int n = 0, w = 0, i;
for (i = 1; i < year; i++)
{
if(leapYear(i))
{
n += 366;
}
else
n += 365;
}
if(leapYear(year))
switch(month)
{
case 12:
n += 336;
break;
case 11:
n += 306;
break;
case 10:
n += 275;
break;
case 9:
n += 245;
break;
case 8:
n += 214;
break;
case 7:
n += 183;
break;
case 6:
n += 153;
break;
case 5:
n += 122;
break;
case 4:
n += 92;
break;
case 3:
n += 61;
break;
case 2:
n += 31;
break;
case 1:
n += 1;
break;
}
else
switch(month)
{
case 12:
n += 335;
break;
case 11:
n += 305;
break;
case 10:
n += 274;
break;
case 9:
n += 244;
break;
case 8:
n += 213;
break;
case 7:
n += 182;
break;
case 6:
n += 152;
break;
case 5:
n += 121;
break;
case 4:
n += 91;
break;
case 3:
n += 60;
break;
case 2:
n += 32;
break;
case 1:
n += 1;
break;;
}
w += n;
w = w % 7;
if (w == 0) w = 7;
return w;
}
int main()
{
int y, m, xq; // 年、月、星期几
cin >> y >> m; // 输入年月
xq = whatDay(y,m); // 计算星期几
cout << y << "年" << m << "月1日是星期"; // 输出星期
if(xq == 7)
cout << "日" << endl;
else
cout << xq << endl;
return 0;
}
第4关:打印日历
任务描述
本关任务:根据输入的年份和月份来输出该年月的日历。
相关知识
日历的格式如下图所示:
上图中每个汉字(一、二...日)占四个字节,右对齐,由于汉字显示本身就占2个字节,所以只需要在汉字前面多输出两个空格就好了。每个日期数字占4个字节,也是右对齐,这样能使输出的日历上下对齐。
剩下的事情就是循环输出这个月的所有日期了,这个月有多少天可以专门用一个函数实现,注意闰年二月(判闰年的函数这里又可以用一次了)是29天。
注意:在 1 号前应该留多少空位(如果 1 号是星期 n ,则留 n-1 个空位,每个空位是一个日期的宽度),注意什么时候换行(日期加 1 号前空位数量是 7 的倍数则换行)。
// 包含两种I/O库,可以使用任一种输入输出方式
#include <cstdio>
#include <iostream>
using namespace std;
// 函数printMonth:按要求的格式打印某年某月的日历
// 参数:year-年,month-月
// 返回值:无
void printMonth(int year, int month);
// leapYear:判断闰年
// 参数:y-年
// 返回值:1-是闰年,0-不是闰年
int leapYear(int y)
{
if(y % 4 == 0 && y % 100 != 0 || y % 400 == 0)
return 1;
return 0;
}
// 函数whatDay:计算某年某月的1号是星期几
// 参数:year-年,month-月
// 返回值:1到7--星期1到星期日
int whatDay(int year, int month)
{
// 1年月日是星期一
int w = 1;
int i;
// 1到year-1都是全年
for(i = 1; i < year; i++)
{
if(leapYear(i))
w += 366;
else
w += 365;
}
switch(month)
{
case 12: // 加月的
w += 30;
case 11: // 加月的
w += 31;
case 10: // 加月的
w += 30;
case 9: // 加月的
w += 31;
case 8: // 加月的
w += 31;
case 7: // 加月的
w += 30;
case 6: // 加月的
w += 31;
case 5: // 加月的
w += 30;
case 4: // 加月的
w += 31;
case 3: // 加月的
if(leapYear(year))
w += 29;
else
w += 28;
case 2: // 加月的天
w += 31;
case 1: // 1月不加了
;
}
// 得到-6,其中为星期天
w = w % 7;
// 调整星期天
if(w == 0)
w = 7;
return w;
}
// 请在下面补充代码,实现函数printMonth
/*************** Begin **************/
void printMonth(int year, int month)
{
int d = whatDay(year, month);
int n = 0;
cout << " " << "一 " << "二 " << "三 "<< "四 " << "五 " << "六 " << "日" << endl;
for (int i = 0; i < d - 1; i++)
{
cout << " ";
}
switch (month)
{
case 1:
n = 31;
break;
case 2:
if (leapYear(year))
n = 29;
else
n = 28;
break;
case 3:
n = 31;
break;
case 4:
n = 30;
break;
case 5:
n = 31;
break;
case 6:
n = 30;
break;
case 7:
n = 31;
break;
case 8:
n = 31;
break;
case 9:
n = 30;
break;
case 10:
n = 31;
break;
case 11:
n = 30;
break;
case 12:
n = 31;
break;
}
for (int i = 1; i < n + 1; i++)
{
if (i < 10)
cout << " " << i;
else
cout << " " << i;
switch (d)
{
case 1:
if ((i % 7) == 0)
cout << endl;
break;
case 2:
if ((i % 7) == 6)
cout << endl;
break;
case 3:
if ((i % 7) == 5)
cout << endl;
break;
case 4:
if ((i % 7) == 4)
cout << endl;
break;
case 5:
if ((i % 7) == 3)
cout << endl;
break;
case 6:
if ((i % 7) == 2)
cout << endl;
break;
case 7:
if ((i % 7) == 1)
cout << endl;
break;
}
}
cout << endl;
}
/*************** End **************/
int main()
{
// 年、月
int y, m;
// 输入年月
cin >> y >> m;
// 输出该年月的日历
printMonth(y,m);
return 0;
}
第5关:拆开了输出整数
任务描述
本关任务:输入一个正整数,然后把该整数的每一位数字从高位到低位顺序输出,每个数字占一行。
例如:输入:123
,程序输出:
1
2
3
相关知识
对于将一个整数各位数字拆开的问题,由于该数字的位数未知,采用取余的方式也只能获得最低位,最高位很难获取(如果使用即将学习的数组,也可以实现,但不建议这样做)。而这里要求先输出最高位,所以不能用循环实现。但递归函数可以很好的解决这个问题。
递归函数
直接调用自己或通过其它函数间接调用自己的函数称为递归函数。
递归函数适合于求解递归问题,所谓递归问题:是指一类比较复杂的问题,其问题的解决又依赖于类似问题的解决,只不过后者的复杂程度或规模较原来的问题更小,而且一旦将问题的复杂程度和规模化简到足够小时,问题的解法其实非常简单。
例如,计算某个自然数 n 的阶乘的公式写为:
n!=n×(n−1)×(n−2)×...×2×1
这种阶乘的计算用如下循环结构来实现:
int factorial = 1;
for (int counter=1; counter <= n; counter ++)
{
factorial=factorial * counter;
}
也可以从另外一个角度来看待阶乘的计算,n 的阶乘可以通过递归定义为:
n!=n×(n -1)! (n>1)
n!=1 (n=1)
例如为了计算5!,要先计算出4!,要计算4!,又要先计算出3!,要计算3!,则要先计算2!,而2!又需要先计算1!。
根据定义,1!为1,有了1!就可以计算2!了,有了2!的结果就可以计算出3!的值,有了3!的值就可以算出4!的值,最后可以得到5!的结果。
从上述递归计算过程可以看到,一个复杂的问题,被一个规模更小、更简单的类似的问题替代了,经过逐步分解,最后得到了一个规模非常小、非常简单的、更容易解决的类似的问题,将该问题解决后,再逐层解决上一级问题,最后解决了较复杂的原始的问题。
递归性质
对于递归函数而言其调用过程与一般函数的调用过程完全一样,同时函数的递归调用具有下面两个性质:
-
函数调用时,调用程序在函数调用处暂时挂起,程序控制离开调用程序转入被调用函数执行,只有当被调用函数执行完后,才返回到调用程序的调用处继续向下执行,所以调用函数一定要在被调用函数执行完成后才能继续执行并结束。由于递归调用中调用函数和被调用函数可能是同一个函数,同一个函数的两次调用可以理解为函数的两个不同的副本;
-
一个函数被调用,系统会为该函数的这次执行分配存储空间,包括为该函数的形式参数和局部变量分配单元。因此,递归函数执行时,在某一时刻,计算机内可能有该递归函数的多个活动的同时存在,每个活动(即函数的每次调用执行)都有自己对应的存储空间,也就是说,函数的形式参数和局部变量,在函数的每次调用执行中都有不同的存储空间。
使用递归函数求阶乘的程序为:
#include <iostream>
using namespace std;
// 递归函数 fac: 求 num 的阶乘
// 返回值:num 的阶乘
int fac(int num)
{
if (num <= 1) // 递归终止,直接给出结果
return 1;
else // 递归调用
return (num * fac(num - 1));
}
int main()
{
int i;
cin >> i;
cout << i << "! = " << fac(i) << endl;
return 0;
}
#include <iostream>
using namespace std;
// 递归函数splitNum:顺序输出n的各位数字,每个数字占一行
// 返回值:无
void splitNum(unsigned int n)
{
// 请在这里补充代码,实现递归函数splitNum
/********** Begin *********/
if (n >= 10)
{
splitNum(n / 10);
}
cout << n % 10 << endl;
/********** End **********/
}
int main()
{
unsigned n;
cin >> n; // 输入正整数n
splitNum(n); // 调用splitNum函数,顺序输出n的各位数字
return 0;
}
第6关:递归求P函数
任务描述
本关任务:编写函数 funP,完成数学函数P(n,x)函数的计算,定义如下:
相关知识
递归是一个很常见的数学概念,很多数学函数本身就是递归的,例如著名的斐波拉契数列:
a
a
a
同样可以用递归函数计算斐波拉契数列第 n 项:
// 递归函数fib:计算斐波拉契数列第 n 项并返回
int fib(int n)
{
// 递归终止,直接返回
if(n == 1 || n == 2)
return 1;
// 两次递归调用,求和并返回
return fib(n - 1) + fib(n - 2);
#include <iostream>
using namespace std;
// 函数funP:实现数学函数P函数
// 返回值:返回P(n,x)的值
double funP(int n, double x)
{
// 请在这里补充代码,实现递归函数funP
/********** Begin *********/
if (n == 0)
return 1;
if (n == 1)
return x;
if (n > 1)
{
int a = 2 * n - 1;
int b = n - 1;
return ((a * funP(n - 1, x) - b * funP(n - 2, x)) / n);
}
else
return 0;
/********** End **********/
}
int main()
{
int n;
double x;
cin >> n >> x; // 输入n、x
cout << "P("<<n<<", "<<x<<")=" << funP(n,x) << endl;
return 0;
}