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

作者: 天花板 | 来源:发表于2015-12-03 10:40 被阅读2890次

    上一篇中,我们学习了如果打印一年12个月日历,这段代码已经有一个小项目的样子了。最后留的练习题很多人邮件反馈说太难了,接下来的几篇中,我们通过这个练习题的讲解完成一个真正的小项目。

    1. 题目分析

    想要把12个月的日历打印成6行2列的形式是有难度的。由于printf只能按顺序在屏幕上打印,即使有回到上一行这样的光标移动方法,操作起来也比较复杂。而我们在计算每个月的日期时,是按顺序计算出的,不能两个月同时进行。因此,我们最直接的想到使用12个二维字符数组来保存每个月的日历内容,之后逐行打印。

    这里的逐行打印指的是:

    • 打印一月份的第一行
    • 打印'\t'
    • 打印二月份的第一行
    • 打印'\n'
    • 打印月份的第二行
      ...

    这个思路应该是最基本的实现方法。

    2. 字符二维数组功能封装

    2.1. 为什么要进行功能封装

    在编码过程中,我们希望在设计功能的时候不用过多地思考细节。比如,在main函数中,我们希望只考虑整体程序流程的问题。

    int main()
    {
        // 变量初始化
    
        //  计算每个月的日历保存在二维数组中
    
        // 把二维数组内容打印在屏幕上
    
        return 0;
    }
    

    上面这段代码就是一个理想中的main函数。如果这里的每一个部分都只有一个函数,那么它的思路将会非常清晰。原则是,我们在考虑流程的时候,不要考虑具体的操作,如二维数组怎么使用,怎么遍历之类的。

    要想做到这点就需要把这些具体的工作都用函数封装起来,这样在后期使用的时候只需要简单的函数调用即可。

    这样做还有另一个好处,把功能相似的代码都抽象成了具体的函数,能够大大减少重复代码的几率,降低后期维护的难度。

    这些都是程序设计中的经验之谈,初学者可能不太理解。希望大家先按照这些原则去做,今后代码写多了自然就会体会到它的好处了。

    2.2. 字符串数组

    C语言中,字符串处理是比较麻烦的,这一点大家肯定已经发行了。二维的字符数组就更加繁琐了。在C++中,有一个string类能够管理字符串,如果使用它,一个二维的字符数组就变成了一个一维的字符串数组。这样会简单很多。

    我们首先就是要写一个类似于string的一组函数来帮我们管理字符串。

    2.3. String的实现

    #define BUF_SIZE 100
    
    typedef struct _tagString
    {
        char buf[BUF_SIZE];
        int len;
    }String;
    

    这里定义了一个名为String的结构体,它里面有两个元素:一个能保存100个字符的数组和一个整型变量,这个变量用来保存字符串长度。

    我们再看看它的使用需要哪些函数。

    void StringInit(String* pStr)
    {
        pStr->buf[0] = 0;
        pStr->len = 0;
    }
    

    String初始化函数,把内部的的字符数组设为空,同时长度设为0。

    void StringSet(String* pStr, char* pBuf)
    {
        int i = 0;
        while (*pBuf != 0)
        {
            pStr->buf[i++] = *pBuf++;
        }
    
        pStr->buf[i] = 0;
        pStr->len = i;
    }
    

    这个函数把一个字符串pBuf复制给一个String结构体pStr。

    char* StringGetBuffer(String* pStr)
    {
        return pStr->pBuf;
    }
    

    这个函数帮我们得到String中的字符串指针。

    void StringCopy(String* pDes, String* pSrc)
    {
        StringSet(pDes, pSrc->buf);
    }
    

    这个函数复制把一个String pSrc复制到另一个String pDes,两个参数都是指向String类型的指针。

    void StringAppend(String* pStr, char* pBuf)
    {
        int i = pStr->len;
    
        while (*pBuf != 0)
        {
            pStr->buf[i++] = *pBuf++;
        }
    
        pStr->buf[i] = 0;
        pStr->len = i;
    }
    

    这个函数的功能是把字符串pBuf中的内容填写在String pStr之后。这是个非常有用的功能。

    3. 代码文件

    在实际项目中,我们建议把拥有独立功能的代码写成独立的文件。这样做有以下好处:

    • 多人协同工作时,方便模块划分和共同开发
    • 方便代码移植,同一份代码如果需要使用在多个项目中可以直接拷贝文件
    • 单个文件内容相对较小,方便维护

    我们把String功能写在一组独立的文件中,一般是一个.h文件和一个.c文件。

    3.1. 头文件

    新建文件String.h,内容如下:

    #ifndef STRING_H_INCLUDED
    #define STRING_H_INCLUDED
    
    #define BUF_SIZE 100
    
    typedef struct _tagString
    {
        char buf[BUF_SIZE];
        int len;
    }String;
    
    void StringInit(String* pStr);
    void StringSet(String* pStr, char* pBuf);
    char* StringGetBuffer(String* pStr);
    void StringCopy(String* pDes, String* pSrc);
    void StringAppend(String* pStr, char* pBuf);
    
    #endif // STRING_H_INCLUDED
    

    前两行和最后一行是为了保证这个头文件只被引用一次。假如在工程中有两处地方用到了#include "String.h",那么编译器会自动忽略掉后出现的一次,保证程序正确编译。

    这个头文件包括两个部分,第一部分是结构体String的定义,第二部分是相关函数的声明。由于使用函数前需要先声明,因此把它放在头文件中,方便在调用时include的同时实现声明的工作。

    那么我们的函数代码写在哪里呢?

    3.2. 代码文件

    新建文件String.c,代码如下:

    #include "String.h"
    
    void StringInit(String* pStr)
    {
        pStr->buf[0] = 0;
        pStr->len = 0;
    }
    
    void StringSet(String* pStr, char* pBuf)
    {
        int i = 0;
        while (*pBuf != 0)
        {
            pStr->buf[i++] = *pBuf++;
        }
    
        pStr->buf[i] = 0;
        pStr->len = i;
    }
    
    char* StringGetBuffer(String* pStr)
    {
        return pStr->pBuf;
    }
    
    void StringCopy(String* pDes, String* pSrc)
    {
        StringSet(pDes, pSrc->buf);
    }
    
    void StringAppend(String* pStr, char* pBuf)
    {
        int i = pStr->len;
    
        while (*pBuf != 0)
        {
            pStr->buf[i++] = *pBuf++;
        }
    
        pStr->buf[i] = 0;
        pStr->len = i;
    }
    

    这里需要注意,在第一行中一定加入#include "String.h"这句话,否则会出现String类型不认识无法编译成功的情况。

    3.3. 使用

    在我们的主程序main.c中,加入#include "String.h"之后,我们就可以调用String相关的方法了。请看下面这个main.c文件。

    #include <stdio.h>
    #include "String.h"
    
    int main(void)
    {
        char a[] = "ABCDE";
        char b[] = "FGHIJ";
    
        String str;
    
        StringInit(&str);
        StringSet(&str, a);
    
        printf("%s\n", str.buf);
    
        StringAppend(&str, b);
    
        printf("%s\n", str.buf);
    
        return 0;
    }
    

    执行结果如下:

    执行结果

    你看懂了吗?

    今天先讲到这里,请大家自己阅读多做练习。从今天起我们讲到的都是C语言知识的综合运用,如果有什么地方不懂的一定要先搞清楚再往下看,否则会越来越困惑。

    我是天花板,让我们一起在软件开发中自我迭代。
    如有任何问题,欢迎与我联系。


    上一篇:21天C语言代码训练营(第六天)
    下一篇:21天C语言代码训练营(第八天)

    相关文章

      网友评论

      • printfkxm:代码都是写在同一个目录下的,但是编译的时候提示unknown type name ‘String’
        命令是gcc dd.c
        printfkxm:大神求指点
      • 普通人也要努力呀:看的懂自己写不出来😭😭
      • KQX:为什么有的图片看不了全部的??
        天花板:@KQX 你换个平台试一下吧
        KQX: @天花板 额,我是说,有的程序看不见全部的,尾部有的看不到,是因为手机端的原因吗?
        天花板:@KQX 什么图片?这篇文章里没有图片呀~
      • kbigbus:char* StringGetBuffer(String* pStr)
        {
        return pStr->pBuf;
        }

        获取字符串指针 不是应该 是 pStr->buf ?
        天花板:@kbigbus 不成功是什么现象?具体可以微信沟通
        kbigbus:@kbigbus 追加的一直不成功 麻烦大神帮忙看看 https://github.com/kbigbus/cproj/blob/master/tran7/String.c
        kbigbus:@kbigbus
        void StringAppend 的 while 循环内 貌似也少了 len++ :smile:
      • 不言说的怦然心动:我们学的好浅
      • 天花板:第一句是给字符数组的最后一位填上‘\0’,第二句是为了后面扩展时可以通过len直接得到字符串长度。
      • GZTommy:文/天花板(简书作者)
        原文链接:http://www.jianshu.com/p/1b3e4c3e642a
        著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

        void StringSet(String* pStr, char* pBuf)
        {
        int i = 0;
        while (*pBuf != 0)
        {
        pStr->buf[i++] = *pBuf++;
        }

        pStr->buf[i] = 0;
        pStr->len = i;
        }
        为什么还要写这句pStr->buf[i] = 0;
        pStr->len = i;
      • bdb27209c62e:为什么使用时没有输出果??
        bdb27209c62e: @天花板 是我自己的问题,问题已经解决
        天花板:@羽天刃雪 这要看你的代码具体怎么实现的了~
      • e83a8fd3ed44:strcpy strcat
      • e83a8fd3ed44:main.c中#include "string.c"
      • 邓琼:字符串set和append函数都没考虑溢出的情况。
        邓琼:@天花板 恩,写得很好,深入浅出,
        天花板: @邓琼 目前的讲解不考虑容错,重点在分析逻辑。后面会逐渐追加
      • 三六九分雨:棒棒哒~学渣的救赎啊
      • 554b97acbd33:什么是c语言?
      • 2217ba70c84c:挺详细的

      本文标题:21天C语言代码训练营(第七天)

      本文链接:https://www.haomeiwen.com/subject/efhshttx.html