一步一步,写出优雅的程序(三)——老王开店
文/Edward
我们在前两篇文章“一步一步,写出优雅的程序(一)”和“一步一步,写出优雅的程序(二)”里面介绍了新手入门如何从无到有来写一个可以满足功能的程序。但是这两篇文章对于程序设计的讨论都是基于编码风格以及程序结构等方面来讲述的。而且例举的例子太过于简单,程序结构也比较简单。本篇文章将会讲述如何善用循环结构,数组等来处理多次重复的计算。善用循环结构,可以大大降低代码的重复性,提高代码的阅读性,使得程序变得更加优雅,美观。
问题模型
假设老王开了一家小超市,这家小超市里面日常经营一些杂货,小吃,生鲜,烟,酒,水果等商品的买卖。最初的时候,老王每天进出货都可以通过手写笔记的形式做收货物管理,但是随着经营品类的不断增加和细分,传统的手写账本已经无法满足日常货物的管理了,请帮其做一个用于货物管理的软件,用来实现货物的增,减,删,查等操作。
这个问题如果使用数据库来做的话,很容易就能完成,但是如果要自己使用C语言去实现,任务就有些繁重了。一般我们的做法为,动态创建一个链表,然后将货物信息以数据节点的形式一个个地增加进去。然而在我们文章中,如果使用链表去实现,必然会引入链表的复杂性,而且,老王经营的是一家小卖部,商品品类最多一两百个,因此我们选用结构体数组来实现这些数据的存储。当然,如果大家想要去让这个示例程序可以在平时的使用中用到,那么只需要将结构体数组改成链表,并且使用文件IO做文件的本地化存储即可。
建立结构体表格模型
要建立一张可以用来存储信息的结构体表格,首先要来建立这个结构体的存储类型。这个结构体的存储类型即为我们需要保存的关于商品信息的维度。
对于一件商品来说,我们关注的维度有以下几点:
条码号
品类
品名
价格
库存
除了上述的五个维度的信息之外,还需要增加一点的即是表格的序号。
对于上面的这些结构体的数据来说,序号的存储类型应该用整型,条码号的存储类型应该用字符串(为什么用字符串,这里解释下,因为后续条码枪扫描商品条码的时候,输出的往往都是字符串),品类,品名这两个都应该使用字符串去存储,价格应该使用浮点型变量存储,库存应该使用整型变量存储。基于上面的描述,我们可以定义出我们需要的结构体类型。如图1所示。
图1 定义单个商品存储特征的结构体
在我们定义好了上述的结构体之后,我们就可以用这种类型在程序里面定义变量了。假设,我们允许老王的小店里面最多可以存储1000件品类的商品,那我们只需要用这个结构体类型去定义一个长度为1000的数组,同时,还需要顺便将数组索引一起定义,如图2所示。
图2 定义结构体数组
结构体数组的操作
建立了以上模型之后,我们可以看到,对整个货物管理系统的操作,归根到底就是对这个结构体数组的操作。因此,接下来我们可以依次来实现数组的操作函数。在一个软件公司中,团队交流的最佳方式,就是明确定义好函数的接口,我们自己写代码的过程中,这一步也是需要去仔细斟酌的,切不可胡乱写一通。
首先,我们来定义第一个结构体操作函数,这个函数的功能为,对表格顺序追加数据项。我们姑且就将这个函数的名称定义为“TblAddDataItem”,这个函数的接口很明确,第一个参数本应该为表格的指针,但是由于我们这个例子中只有一张全局表格,因此可以省去这一个参数。接下来的参数,本来应该为条码号,品类,品名,价格,库存等这些变量,但是如果真的这样写,那么函数的输入参数太过于长,影响易读性。最佳的做法是,我们输入商品信息的时候,将输入的内容存储在一个“productType”类型的变量中,然后将这个变量作为函数的参数传入进去,最后将所有的输入信息复制到表格的某一项。定义到这里,还有一个最重要的没有考虑到,即函数的返回值类型。函数可以根据需要去返回相应的类型,而在我们这个函数中将其返回值写成void即可。最后可以得到函数原型为:
void TblAddDataItem(productType productItems);
这个函数需要实现的功能也很简单,即将productItems中的每一项(序号除外)都复制到productList里即可。对于复制内容,如果是字符串,那么使用strcpy函数来实现,整型和浮点型直接赋值即可。表格中每一项的序号,即当前的数组索引,在增加一项操作完成之后,需要将索引加1。最终可以得到的代码如图3所示。
图3 TblAddDataItem函数
第二个需要定义的函数是打印表函数,这个函数可以将我们之前输入表格的信息全部打印出来。我们为其取一个函数名TblPrintItems。由于考虑到有时候,我们不需要将全部的表格信息都打印出来,而只想看到从n开始,往后偏移m项的数据,因此我们需要传入两个参数,一个参数为起始项,另一个参数为偏移量。关于返回值,如果表格需要打印的范围在当前的productListIndex范围之内,则可以正常打印,返回为0,而一旦当这个打印超出了productListIndex范围,我们就让这个程序返回一个错误代码1。因此函数的原型可以写为:
int TblPrintItems (int startIndex length);
实现这个函数的方式很简单,即先判断startIndex和startIndex + length的结果有没有超出productListIndex的范围,如果超出了,则返回-1。如果没有超出则遍历数组的startIndex到startIndex + length项进行打印。如图4所示。
图4 TblPrintItems函数
测试
到此为止,我们完成了表格的增加新项目和表格打印,接着可以编写一个简单的应用程序来测试以下这个代码,测试程序写在main.c函数里面,即进行三次信息录制,录入完成之后将信息打印出来。测试代码如图5所示。
图5 测试程序
我们直接使用VSCode搭建一个编译环境,launch.json和task.json文件如图6所示。如果相应着重学习这两个文件是怎么产生的,请查看之前的文档:
链接:https://mp.weixin.qq.com/s/jheRrA6sXG5LJ628PwFJnw
图6 launch.json和task.json文件
最后,我们需要在main.c里面插入一个断点,断点位置插入在TblPrintItems(0, 3);语句前面,如图7所示。
图7 断点位置
最后运行整个测试程序,并且在弹出的窗口中输入测试数据,如图8所示。
图8 测试结果
由于篇幅原因,后续还有很多内容和知识点将会在下一篇文章中更新,敬请期待。
源码附录:
main.c
#include <stdio.h>
#include "db.h"
int main(void) {
productType inputBuf;
int i = 0;
for(i = 0; i < 3; i ++) {
printf("Input serial number:");
scanf("%s", &inputBuf.sSerialNumber);
printf("Input product kind:");
scanf("%s", &inputBuf.sProductKind);
printf("Input product name:");
scanf("%s", &inputBuf.sProductName);
printf("Input price:");
scanf("%f", &inputBuf.fProductPrice);
printf("Input quantity:");
scanf("%s", &inputBuf.iProductQuantity);
TblAddDataItem(inputBuf); // 将输入值添加到表格内
}
TblPrintItems(0, 3); // 打印表格
getchar();
}
db.c
#include "db.h"
#include <string.h>
#include <stdio.h>
/*********************Global definition********************/
/*定义productList[1000]数组变量*/
productType productList[1000];
/*定义productListIndex变量*/
int productListIndex = 0;
/*
* @ function: void TblAddDataItem(productType productItems)
* @ parameter: productType strcut
* @ note: add new data items to product list table
* @ retval: void
*/
void TblAddDataItem(productType productItems) {
productList[productListIndex].iNumber = productListIndex; // 利用当前表格的索引来得出序号
strcpy(productList[productListIndex].sSerialNumber, productItems.sSerialNumber);
strcpy(productList[productListIndex].sProductKind, productItems.sProductKind);
strcpy(productList[productListIndex].sProductName, productItems.sProductName);
productList[productListIndex].fProductPrice = productItems.fProductPrice;
productList[productListIndex].iProductQuantity = productItems.iProductQuantity;
productListIndex ++; //索引加1
}
/*
* @ function: int TblPrintItems (int startIndex , int length)
* @ parameter: startIndex:start index of table you wanna to print
length:length you wanna to print
* @ note: print table items
* @ retval: 0:success
* 1:fail
*/
int TblPrintItems (int startIndex , int length) {
int retval = 0;
int i;
if(productListIndex >= (startIndex + length) && startIndex >= 0 && length >= 1) {
printf("%5s\t%20s\t%30s\t%30s\t%10f($)\t%20s(pcs)\n", "No.", \
"Serial number", \
"Product Kind", \
"Product Name", \
"Price", \
"Quantity");
for(i = startIndex; i < startIndex + length; i ++) {
printf("%5d\t%20s\t%30s\t%30s\t%10f($)\t%20d(pcs)\n", productList[i].iNumber,\
productList[i].sSerialNumber,\
productList[i].sProductKind,\
productList[i].sProductName,\
productList[i].fProductPrice,\
productList[i].iProductQuantity);
}
} else {
retval = 1;
}
return retval;
}
db.h
#ifndef __DB_H_
#define __DB_H_
/*********************Global declaration********************/
typedef struct {
int iNumber;
char sSerialNumber[50];
char sProductKind[50];
char sProductName[50];
float fProductPrice;
int iProductQuantity;
} productType;
/*声明productList[1000]数组变量*/
extern productType productList[1000];
/*声明productListIndex变量*/
extern int productListIndex;
void TblAddDataItem(productType productItems);
int TblPrintItems (int startIndex , int length);
#endif
launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "build and debug", // 配置名称,将会在启动配置的下拉菜单中显示
"type": "cppdbg", // 配置类型,这里只能为cppdbg
"request": "launch", // 请求配置类型,可以为launch(启动)或attach(附加)
"program": "${fileDirname}/${fileBasenameNoExtension}.exe",
// 将要进行调试的程序的路径
"args": [], // 程序调试时传递给程序的命令行参数,一般设为空即可
"stopAtEntry": false, // 设为true时程序将暂停在程序入口处,一般设置为false
"cwd": "${fileDirname}", // 调试程序时的工作目录,一般为${fileDirname}即代码所在目录
"environment": [],
"externalConsole": true, // 调试时是否显示控制台窗口,一般设置为true显示控制台
"MIMode": "gdb",
"miDebuggerPath": "D:/software/mingw64/bin/gdb.exe", // miDebugger的路径,注意这里要与MinGw的路径对应
"preLaunchTask": "gcc", // 这里需要添加一个参数,
//调试会话开始前执行的任务,一般为编译程序,c++为g++, c为gcc
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
task.json
{
"version": "2.0.0",
"label": "C/C++: gcc.exe build active file",
"command": "gcc",
"args": [
"-g",
"${file}",
"${fileDirname}\\db.c", //增加对a.c文件的编译
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],
"problemMatcher": [
"$gcc"
],
"group": "build",
"detail": "compiler: D:\\software\\mingw64\\bin\\gcc.exe",
}