👥User模块

搭建储存和操作用户信息的底层数据结构

当前进度

.---+- include -+- tools -+- color.h
    |           |         +- hint.h
    |           |         +- info.h
    |           | 
    |           +- user  -+- user.h <---
    |           |
    |           +- config.h <---
    |
    +-   src   -+- tools -+- color.c
                |         +- hint.c
                |         +- info.c 
                |
                +- data  -+- id.txt
                |         +- user.txt <---
                |
                +- user  -+- user.c <---
                |
                +- main.c

设计数据类型

根据前面所说的要求,我么可以在user.h中定义如下的用户类型:

这里,我们引入了一个头文件config.h,这个头文件是储存一些全局设定的,这里我们约定本项目中所有的字符串长度均不超过MAX_LEN,我们将其定义在config.h中。

随着程序的不断丰富,我们可能会需要一些其他的全局设定或者约定,我们都可以放在config.h中。这样一来,如果之后我们要更改这些约定,只需要修改config.h头文件即可。

选择数据结构

我选用了数组结构,因此我们需要约定用户的总数不超过MAX_USER,并且在config.h中定义这个宏。

我们在user.c中定义这个数据结构,同时定义用户的初始总数为0:

这里采用全局数组和全局变量将数据结构储存主存的静态数据区,而不是栈区。因为静态数据区变量的生存期是贯穿程序始终的,并且静态数据的空间会比栈区大。

除此之外,用static关键字限制这两个变量的文件作用域为本文件内,这样就只有user模块本身的函数方法可以访问着两个数据结构了。外部模块只有通过本模块提供的方法或者说接口才能读取或者修改数据内容。

这个过程就是模块的封装和抽象

设计对外接口

接口设计

根据项目刚开始时的需求汇总,我们对于用户的数据结构提供如下一些接口,声明在user.h头文件中。

其实我们设计接口的时候可能并不是一开始就能考虑全面的,往往是先设计一些基本的操作,在项目进行的过程中发现现有的接口还不能满足要求,慢慢往里面加一些别的接口。最终才形成了上面的成形的接口设计。

接口实现

我分项展示各个接口的实现方式的时候会将实现过程中需要用到的头文件都包含以下,不过在实际的src/user/user.c文件中同一种头文件只需要包含一次即可。

一般的规范是先包含自定义的头文件,再包含系统头文件。

比如说:

之后的各个模块不再重复上述内容。

从外部文件导入信息

我们约定,用户的信息储存在文件src/data/user.txt中。另外,别忘了包含user.h头文件,包含其相对于include文件夹的相对路径。

这里使用static关键字,将filePath的作用域限制在本模块内部(C语言以.c文件作为模块划分的基本单位),这样在其他模块里面就可以继续使用filePath这个标识符作为文件路径的常量名了。

这种手法在编写界面模块的时候会体现的淋漓尽致,可以期待一下😄

将用户信息导出到外部文件

为了避免浮点数打印精度不确定导致打印格式不统一的问题,我们约定在本项目中,所有的浮点数保留小数点后1位。哪怕用户输入了更高精度的浮点数,因为打印规格符统一使用.1f,故而当该输入被打印到标准输出的时候总是1位小数。同时,在经历一次文件读写之后,实际精度也会变成一位小数。

获取用户对象

打印详细的用户信息

打印内容需要注意对齐和美观。

添加新用户

因为这里用到了genID函数来生成新用户的ID,所以需要包含info.h头文件。当然,用到了字符串处理的库函数,自然也需要包含string.h头文件。

这里值得提一下的是,所有的字符串的赋值都不可以直接用等号,而应当使用strcpy函数,因为字符串是复杂数据类型,变量中存的只是首地址,而不是字符串本身。不过结构体是可以直接赋值的。结构体赋值的原则是将整个结构体所指向的内存直接拷贝。

当然,这里可以直接赋值其实取决与我结构体中数组的定义方式。

我举一个简单的例子:

定义两个上述类型的结构体变量:

然后我们将p1赋值给p2:

其实上面的赋值操作就相当于:

不过需要注意的是,如果我这样定义结构体:

直接赋值的时候就不是拷贝字符串,而只是拷贝字符指针了。因为,此时:

因为我们需要保证所有的用户用户名不同,所以我们需要写一个searchUserName函数来查找用户名,只有找不到的时候,才可以为这个新的用户生成一个独特的ID并把新用户放到我们的数组中去。

因为这个searchUserName其实只在addUser函数中用到了,相当于添加新用户的时候检查是否用户已存在的一个帮助函数,因此使用static关键字将其作用域限制在本模块内,并且不在头文件中给处该函数的声明。

删除给定ID的用户

who中存放的是行为主体的ID,只有管理员有权限删除用户(管理员ID为NULL)。

打印所有用户的信息

这里我们将表头header和表格分割线divide抽象出来,作为两个模块局部变量,在GoodOrder模块中也有相同名称的局部变量。

这样设计是为了简化代码,提高通用性,因为表头和分割线会被使用很多次(这个模块不明显,因为只有管理员有权限查看所有的用户,不过在Good模块的时候,买家、卖家和管理员查看到的商品列表是不一样的,也就需要很多不同的print函数,所以会重复的要用到表头和分隔符)。

除此之外,我还将打印表头和打印分隔符的操作封装成了两个宏print_headerprint_divide,放在info.h头文件中。在所有的数据结构模块中都可以使用这两个宏。

因为headerdivide都是static作用域,所以在不同的数据结构模块里面使用这两个宏,所指向的headerdivide都是模块内自己定义的那两个,所以这两个宏只需要定义一遍就可以供三个数据结构模块多次使用,大大减少了不必要的重复性的代码。

检查用户名和密码是否匹配

这里除了返回密码匹配是否成功之外,还需要返回和哪一个用户匹配成功了。

一个函数就只能有一个返回值,所以我们通过指针参数来返回到底和哪一个用户匹配成功了。

当我们要反馈不止一个值的时候,除了函数返回值以外,还可以诉诸于指针,通过传址调用的方式利用函数的副作用返回数据。

用户充值

最后更新于

这有帮助吗?