21天C语言代码训练营(第八天)

继续我们之前的项目,上一篇中我们完成了自己的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语言代码训练营(第九天)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,216评论 0 4
  • 上一篇中,我们学习了如果打印一年12个月日历,这段代码已经有一个小项目的样子了。最后留的练习题很多人邮件反馈说太难...
    天花板阅读 4,313评论 19 48
  • 上一篇中我们用项目思维把我们的程序做了大的调整,收到的大部分反馈邮件都说基本能看懂。今天我们来实现最后一部分功能。...
    天花板阅读 3,878评论 17 49
  • 最近情绪总是不好,因为同事总是做一些事情来惹恼你,老公的随便一句话也足够刺激你,一件小事就反应过激。为何你的情绪总...
    夕米可可阅读 340评论 0 2
  • 一颗颤抖的心, 茫然坠下, 不知归宿。 只能傻傻的期盼: 有童话般美好结局。 伸出手, 一不小心, 两颗心接触在一...
    向晫阅读 272评论 0 2