继续我们之前的项目,上一篇中我们完成了自己的String类型设计,封装了相关的功能。有朋友留言中提到C语言中有相应的字符串操作函数可以完成我们自己实现的那部分功能。这里我需要解释一下,我们的训练目的是让大家了解程序设计的思想,为了能在一个简单项目中划分出更多的层次,我们不得不把一些简单问题复杂化。
对于初学者而言,更多的去自己实现一些基本功能并不是一件坏事,它能帮助大家充分训练程序设计的基本功。
月份类型
我们在打印日历时,打印每个月的功能其实是重复的。在第七天的文章中,我们使用的方法是通过一个12次的循环打印出一年的12个月。每个月中,我们会打印下面这几个信息:
- 月份名称
- 分割线
- 星期列表
- 日期列表(最多六行)
我们可以用一个String类型的数组来保存每个月的内容。定义如下:
String month[10];
这句话定义了一个可以保存10个字符串的字符串数组。从功能上说,这个数组已经满足了我们的需求。然而,想到后面我们要创建和使用12个这样的数组大家就会比较头疼。于是我们想到了通过创建一个Month类型去简化这些操作。
typedef struct _tagMonth
{
String _strName; // 月份名称
String _arrayDays[6]; // 日期列表
int _nArraySize; // 日期列表的总行数
}Month;
又是一个神奇的结构体定义,一个Month类型表示了一个月份的全部内容。_strName是一个字符串类型,用来保存每个月份的英文名称。_arrayDays[6]是一个能够保存6个字符串的数组,用来保存每一行的字符日期。由于每个月中日期行数都不同,因此我们需要一个整形变量_nArraySize来保存我们实际上使用了多少行日期。
有了这个数据类型,我们只需要一个Month数组就能管理12个月份了。
Month month[12];
月份打印
有了Month这个类型,我们就可以写出一个统一的月份打印函数来打印每一个月的日历了。代码如下:
// 打印某个月的日历
void PrintMonth(Month* pMonth)
{
int i;
// 打印月名
printf("%27s\n", pMonth->_strName.buf);
// 打印分割线
printf("----------------------------\n");
// 打印星期列表
printf("Sun Mon Tue Wed Thu Fri Sat\n");
// 打印每行日期列表
for (i = 0; i < pMonth->_nArraySize; i++)
{
printf("%27s\n", pMonth->_arrayDays[i].buf);
}
}
这个函数负责把每个月的四部分信息按顺序打印出来。有了这个函数,我们打印出month[12]这个月份数组的代码就变得非常简单了。
int i;
for (i = 0; i < 12; i++)
{
PrintMonth(&month[i]);
printf("\n");
}
就这六行代码就完成了一个复杂的功能,是不是很不可思议。
注意:这里蕴含着一个程序设计的基本思想,把基本功能提炼成函数,通过一层层地函数调用完成功能的叠加,最终实现拥有完整功能集合的程序。
月份管理方法
在我们打印出最终结果之前,有一个重要工作就是把每个月的信息计算出来保存在Month数组中。为了简化操作,我们需要实现下面几个函数。
// 填写月份英文名
void SetMonthName(Month* pMonth, char* pBuf)
{
StringSet(&pMonth->_strName, pBuf);
}
这个函数的作用是为Month类型的变量设置月份英文名。就是给_strName赋值。
// 在Month的_arrayDays中添加一行新字符串
void AddDaysLine(Month* pMonth, String* pStr)
{
StringAppend(pStr, " ");
pStr->buf[27] = 0;
pStr->len = 27;
StringCopy(&pMonth->_arrayDays[pMonth->_nArraySize++], pStr);
}
这个函数的功能是把一行完整的日期字符串填写在_arrayDays数组中。注意,为了保证每行都有27个字符,我们在不足20个字符的字符串后面添加空格字符。
// 返回月份名称
char* GetMonthName(Month* pMonth)
{
return pMonth->_strName.buf;
}
这个函数很简单,返回pMonth的月份名称字符串。这类封装对简化程序逻辑方面帮助不大,但代码调用时写成GetMonthName()更方便阅读。这一点大家慢慢理解。
// 返回日期列表的字符串数组
String* GetDaysArray(Month* pMonth)
{
return pMonth->_arrayDays;
}
// 返回日期列表的行数
int GetDaysArraySize(Month* pMonth)
{
return pMonth->_nArraySize;
}
同样的道理,这两个函数的封装是为了提高代码可阅读性。
这样一来,我们得到了一组新的功能性文件:Month.h和Month.c。
-
Month.h
#ifndef MONTH_H_INCLUDED #define MONTH_H_INCLUDED #include "String.h" typedef struct _tagMonth { String _strName; // 月份名称 String _arrayDays[6]; // 日期列表 int _nArraySize; // 日期列表的总行数 }Month; void SetMonthName(Month* pMonth, char* pBuf); void AddDaysLine(Month* pMonth, String* pStr); char* GetMonthName(Month* pMonth); String* GetDaysArray(Month* pMonth); int GetDaysArraySize(Month* pMonth); void PrintMonth(Month* pMonth); #endif // MONTH_H_INCLUDED
-
Month.c
#include <stdio.h> #include "Month.h" // 填写月份英文名 void SetMonthName(Month* pMonth, char* pBuf) { StringSet(&pMonth->_strName, pBuf); } // 在Month的_arrayDays中添加一行新字符串 void AddDaysLine(Month* pMonth, String* pStr) { StringAppend(pStr, " "); pStr->buf[27] = 0; pStr->len = 27; StringCopy(&pMonth->_arrayDays[pMonth->_nArraySize++], pStr); } // 返回月份名称 char* GetMonthName(Month* pMonth) { return pMonth->_strName.buf; } // 返回日期列表的字符串数组 String* GetDaysArray(Month* pMonth) { return pMonth->_arrayDays; } // 返回日期列表的行数 int GetDaysArraySize(Month* pMonth) { return pMonth->_nArraySize; } // 打印某个月的日历 void PrintMonth(Month* pMonth) { int i; // 打印月名 printf("%27s\n", pMonth->_strName.buf); // 打印分割线 printf("----------------------------\n"); // 打印星期列表 printf("Sun Mon Tue Wed Thu Fri Sat\n"); // 打印每行日期列表 for (i = 0; i < pMonth->_nArraySize; i++) { printf("%27s\n", pMonth->_arrayDays[i].buf); } }
功能组合
目前,我们的项目中main.c文件之外还有四个文件,分别是:
- String.h
- String.c
- Month.h
- Month.c
此时,我们要实现一个按顺序打印12个月的程序就变得非常简单了。main.c文件如下:
#include <stdio.h>
#include "Month.h"
Month g_Month[12];
char g_month[12][10] = { "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December" };
int g_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// 通过月份数字打印月份名称
char* GetMonthStr(int month)
{
return g_month[month - 1];
}
// 判断闰年,是闰年返回1,是平年返回0
int IsLeapYear(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
return 1;
else
return 0;
}
// 返回输入年份的1月1日是周几
int GetWeek(int year)
{
return (35 + year + year / 4 - year / 100 + year / 400) % 7;
}
// 返回输入的年份中输入的月份天数
int GetDays(int year, int month)
{
if (month == 2 && IsLeapYear(year))
{
return g_days[month - 1] + 1;
}
else
{
return g_days[month - 1];
}
}
int main()
{
int i, j, k;
int week;
int days;
int year = 2015;
char* pStr;
Month* pCurMonth;
String str;
// 计算当年的1月1日是周几的公式,同时在输出过程中随时表示每一天是星期几
week = GetWeek(year);
for (i = 0; i < 12; i++)
{
// pCurMonth指向当前的月份
pCurMonth = &g_Month[i];
// 填写月份名称
pStr = GetMonthStr(i + 1);
SetMonthName(pCurMonth, pStr);
StringInit(&str);
for (k = 0; k < week; k++)
{
sprintf(pStr, " ");
StringAppend(&str, pStr);
}
// 这个月的每一天和星期对齐输出
days = GetDays(year, i + 1);
for (j = 1; j <= days; j++)
{
sprintf(pStr, "%3d ", j);
StringAppend(&str, pStr);
if (++week >= 7)
{
AddDaysLine(pCurMonth, &str);
week = week % 7;
StringInit(&str);
}
}
// 填写一行日期字符串
AddDaysLine(pCurMonth, &str);
}
// 打印12个月的日历
for (i = 0; i < 12; i++)
{
PrintMonth(&g_Month[i]);
printf("\n");
}
return 0;
}
main函数之前的几个函数我们已经讲过了。main函数内部其实是对我们封装好的所有函数的集中调用。大家看懂了吗?
课后练习
在今天程序的基础上,我们做简单的修改就能实现打印两列日历的功能了。修改如下:
-
用两个Month数组分别保存单月和双月
Month g_MonthLeft[6]; Month g_MonthRight[6];
-
修改PrintMonth函数,让它成为下面这种形式:
void PrintTwoMonth(Month* pMonthLeft, Month* pMonthRight)
这个函数的功能是每次输入两个Month变量,把这两个月的日历并排输出。并排输出的方法是左边月份输出一行,右边月份输出一行,交替打印。
-
最终打印时只需要这样既可
for (i = 0; i < 6; i++) { PrintMonth(&g_MonthLeft[i]); printf("\n"); }
请大家自行练习,下一篇中会具体讲解。
我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。
上一篇:21天C语言代码训练营(第七天)
下一篇:21天C语言代码训练营(第九天)