使用教材
《“笨办法” 学C语言(Learn C The Hard Way)》
https://www.jianshu.com/p/b0631208a794
说明
-
EX17
,书上是没有注释的, 这个练习要求就是自己去读懂每一个函数.
目录
零、完整源码
一、代码功能
二、运行调试
三、代码说明
1、代码主要结构体与常量
2、主函数的逻辑结构
3、数据库:加载/关闭,程序终止
die
Database_open
Database_close
4、数据库:创建/查询/插入/删除/列出
(1)创建 :$ ./ex17 db.dat c
(2)查询:$ ./ex17 db.dat g 2
(3)插入:$ ./ex17 db.dat s 1 zed [email protected]
(4)删除:$ ./ex17 db.dat d 3
(5)列出:$ ./ex17 db.dat l
零、完整源码(教材作者的github
)
https://github.com/zedshaw/learn-c-the-hard-way-lectures/tree/master/ex17
一、代码功能
-
EX17
,主要利用的是文件的打开、关闭与写入操作; - 代码会创建文件,可以命名为
db.dat
,在磁盘上保存数据,不妨看做一个“静态时期的超迷你数据库”; - 运行期间,通过一个固定大小的结构体数组,来模拟数据库的创建、查看、插入、删除等操作;
- 具体表现为,每次运行都读取
db.dat
文件的全部数据到结构数组里,按命令修改数组数据,再全部回写到db.dat
文件;
二、运行调试
- 0、
action: c=create, g=get, s=set, d=del, l=list
- 1、编译程序
ex17
$ make ex17
clang -Wall -g ex17.c -o ex17
- 2、创建
数据库 db.dat
,并使用命令行选项s
,新建三条信息
$ ./ex17 db.dat c
$ ./ex17 db.dat s 1 zed [email protected]
$ ./ex17 db.dat s 2 frank [email protected]
$ ./ex17 db.dat s 3 joe [email protected]
- 3、使用命令
l
(小写的字母l
),列出当前数据库内的全部记录
$ ./ex17 db.dat l
1 zed [email protected]
2 frank [email protected]
3 joe [email protected]
- 4、使用命令行选项
d
,删除id=3
的数据,并查看
$ ./ex17 db.dat d 3
$ ./ex17 db.dat l
1 zed [email protected]
2 frank [email protected]
- 5、使用选项
g
,查看id=2
的数据
$ ./ex17 db.dat g 2
2 frank [email protected]
- 6、各种错误提示信息输出
$ ./ex17 db.dat s 2 hello [email protected]
ERROR: Already set, delete it first
$ ./ex17 db.dat g
ERROR: Need an id to get
$ ./ex17 db.dat
ERROR: USAGE: ex17 [action paprams]
$ ./ex17 db.dat a b c d e f g
ERROR: Invalid action: c=create, g=get, s=set, d=del, l=list
$ ./ex17 db.dat g 10000
ERROR: There's not that many records.
$ ./ex17 db.dat s
ERROR: Need id, name, email to set
$ ./ex17 db.dat d
ERROR: Need id to delete
三、代码说明
1、代码主要结构体与常量
-
struct Address rows[MAX_ROWS];
是一个结构体数组,也就是说,数组的每一个元素都是一个Address
类型的结构体; -
struct Connection
结构体,包括两个指针,一个是文件指针file
,另一个是指向Database
类型的指针db
;
#define MAX_DATA 512
#define MAX_ROWS 100
struct Address {
int id;
int set;
char name[MAX_DATA];
char email[MAX_DATA];
};
struct Database {
struct Address rows[MAX_ROWS];
};
struct Connection {
FILE *file;
struct Database *db;
};
2、主函数的逻辑结构
-
main()
:主要用于解析命令行参数,以执行对应的操作; - 命令行参数的基本格式
程序名 数据库名称 操作选项 操作参数
USAGE: ex17 [action paprams]
action: c=create, g=get, s=set, d=del, l=list
创建 ,用id获取, 用id设置, 用id删除, 显示全部
-
argv[0]
是程序名,在本次练习中就是ex17
; -
argv[1]
是数据库文件名,自己命名,示例中都是db.dat
-
argv[2][0]
,表示一个字符char
,是操作选项,可选的有c
g
s
d
l
; -
argv[3]
指示数据的 id值;
int atoi( const char *str );
if(argc > 3) id = atoi(argv[3]);
https://en.cppreference.com/w/c/string/byte/atoi
字符串转为int值
3、数据库:加载/关闭,程序终止
die :终止程序并输出错误信息
void die(const char *message)
{
if(errno) {
perror(message);
} else {
printf("ERROR: %s\n", message);
}
exit(1);
}
-
exit()
:终止程序
https://en.cppreference.com/w/c/program/exit
-
errno
以及perror
errno is a preprocessor macro that expands to a thread-local (since C11) modifiable lvalue of type int.
https://en.cppreference.com/w/c/error/errno
Prints a textual description of the error code currently stored in the system variable errno to stderr.
https://en.cppreference.com/w/c/io/perror
Database_open
In main: struct Connection *conn = Database_open(filename, action);
struct Connection *Database_open(const char *filename, char mode)
{
struct Connection *conn = malloc(sizeof(struct Connection));
if(!conn)
die("Memory error");
conn->db = malloc(sizeof(struct Database));
if(!conn->db)
die("Memory error");
if(mode == 'c') {
conn->file = fopen(filename, "w");
} else {
conn->file = fopen(filename, "r+");
if(conn->file) {
Database_load(conn);
}
}
if(!conn->file)
die("Failed to open the file");
return conn;
}
使用
malloc
,给结构体Database
以及Connection
分配内存;-
按照指定模式
mode
,创建或者打开数据库文件
FILE *fopen( const char *filename, const char *mode );
https://en.cppreference.com/w/c/io/fopen
以r+
模式打开数据库文件时,继续调用Database_load
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream );
参数
buffer - 指向要读取的数组中首个对象的指针
size - 每个对象的字节大小
count - 要读取的对象数
stream - 读取来源的输入文件流
从给定输入流 stream 读取count 个对象到数组 buffer 中
https://en.cppreference.com/w/c/io/fread
- 函数功能:从打开的数据库文件
conn->file
中,读取1
个
struct database
对象到数组conn->db
中
void Database_load(struct Connection *conn)
{
int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);
if(rc!=1)
die("Failed to load Database");
}
- buffer :
conn->db
来源于Database_open
的conn->db = malloc(sizeof(struct Database));
,意思是刚刚有一块热乎的内存分了出来; - size:
sizeof(struct Database)
- count :
1
struct Database {
struct Address rows[MAX_ROWS];
};
- stream :
conn->file
,来源于Database_open
的conn->file = fopen(filename, "r+");
,刚刚打开的数据库文件;
Database_close
void Database_close(struct Connection *conn)
{
if(conn) {
if(conn->file)
fclose(conn->file);
if(conn->db)
free(conn->db);
free(conn);
}
}
-
fclose
:关闭指定的文件流
https://en.cppreference.com/w/c/io/fclose
-
free
:Database_open
有两次malloc
,这里就有两次free
Deallocates the space previously allocated by
malloc()
https://en.cppreference.com/w/c/memory/free
free(conn->db);
对应 Database_open : conn->db = malloc(sizeof(struct Database));
free(conn);
对应 Database_open: struct Connection *conn = malloc(sizeof(struct Connection));
4、数据库:创建/查询/插入/删除/列出
(1)创建 :$ ./ex17 db.dat c
case 'c':
Database_create(conn);
Database_write(conn);
break;
struct Address {
int id;
int set;
char name[MAX_DATA];
char email[MAX_DATA];
};
struct Database {
struct Address rows[MAX_ROWS];
};
void Database_create(struct Connection *conn)
{
int i = 0;
for(i = 0; i < MAX_ROWS; i++) {
// make a prototype initialize it
struct Address addr = {.id = i, .set = 0};
// then just assign it
conn->db->rows[i] = addr;
}
}
void Database_write(struct Connection *conn)
{
rewind(conn->file);
int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
if(rc != 1)
die("Failed to write database.");
rc = fflush(conn->file);
if(rc == -1)
die("Cannot flush database.");
}
Database_create
这里创建了一个全新的数据库文件
db.dat
,还什么都没有;Database结构体
本质是一个Address结构体
数组;声明并初始化一个
Address对象
,struct Address addr = {.id = i, .set = 0};
,作为数据库每条记录的初始值;id值
,各个不同,是区别每条记录的唯一标识,在初始化时,就是使用的i
,依次递增赋的值;set
字段,标识本条记录是否可以被覆盖,set=0时
,可以在此处写入数据,set=1时
,表示此处已经有记录了不能写;
Database_write
- 将内存中的整个数据写入到空空的数据库文件中去;
-
rewind
: 达到从头开始重新读文件的效果
void rewind( FILE *stream );
Moves the file position indicator to the beginning of the given file stream.
https://en.cppreference.com/w/c/io/rewind
-
int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
,将数组conn->db
中的1
个struct database
对象写到数据库文件conn->file
中
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
参数
buffer - 指向数组中要被写入的首个对象的指针
size - 每个对象的大小
count - 要被写入的对象数
stream - 指向输出流的指针
写 count 个来自给定数组 buffer 的对象到输出流stream
https://en.cppreference.com/w/c/io/fwrite
rc = fflush(conn->file);
int fflush( [FILE] *stream );
https://en.cppreference.com/w/c/io/fflush
对于输出流(及最后操作为输出的更新流),从 stream 的缓冲区写入未写的数据到关联的输出设备。
(2)查询:$ ./ex17 db.dat g 2
case 'g':
if(argc!=4)
die("Need an id to get");
Database_get(conn, id);
break;`
void Database_get(struct Connection *conn, int id)
{
struct Address *addr = &conn->db->rows[id];
if(addr->set) {
Address_print(addr);
} else {
die("ID is not set");
}
}
struct Address {
int id;
int set;
char name[MAX_DATA];
char email[MAX_DATA];
};
struct Database {
struct Address rows[MAX_ROWS];
};
Database_get
&
取出地址:struct Address *addr = &conn->db->rows[id];
打印记录
void Address_print(struct Address *addr)
{
printf("%d %s %s\n", addr->id, addr->name, addr->email);
}
(3)插入:$ ./ex17 db.dat s 1 zed [email protected]
case 's':
if(argc != 6)
die("Need id, name, email to set");
Database_set(conn, id , argv[4], argv[5]);
Database_write(conn);
break;
void Database_set(struct Connection *conn, int id, const char *name, const char *email)
{
struct Address *addr = &conn->db->rows[id];
if(addr->set)
die("Already set, delete it first");
addr->set = 1;
// WARING: bug, read the "How To Break It" and fix this
char *res = strncpy(addr->name, name, MAX_DATA);
// demonstrate the strncpy bug
if(!res)
die("Name copy failed");
res = strncpy(addr->email, email, MAX_DATA);
if(!res)
die("Email copy failed");
}
Database_set
- 使用
id
值,找到要插入的位置; -
set
字段为1
,表示要插入要先删除旧的记录;
if(addr->set)
die("Already set, delete it first");
-
char *res = strncpy(addr->name, name, MAX_DATA);
res = strncpy(addr->email, email, MAX_DATA);
char *strncpy( char *dest, const char *src, size_t count );
https://en.cppreference.com/w/c/string/byte/strncpy
复制 src 所指向的字符数组的至多 count 个字符(包含空终止字符,但不包含后随空字符的任何字符)到 dest 所指向的字符数组。
Database_write
- 插入了新数据后,要把整个内存中的数据,回写到数据库文件中;
(4)删除:$ ./ex17 db.dat d 3
case 'd':
if(argc!=4)
die("Need id to delete");
Database_delete(conn, id);
Database_write(conn);
break;
Database_delete
void Database_delete(struct Connection *conn, int id)
{
struct Address addr = { .id = id, .set = 0};
conn->db->rows[id] = addr;
}
- 所谓删除,本质是覆盖,
id值
不变,set
设置为0
(表示可以此处可以修改了)
Database_write
- 删除了数据后,要把整个内存中的数据,回写到数据库文件中;
(5)列出:$ ./ex17 db.dat l
case 'l':
Database_list(conn);
break;
void Database_list(struct Connection *conn)
{
int i = 0 ;
struct Database *db = conn->db;
for(i = 0 ;i < MAX_ROWS; i++) {
struct Address *cur = &db->rows[i];
if(cur->set) {
Address_print(cur);
}
}
}
- 访问数据库中每条记录,如果
set
值为1,表示存在有记录,就调用Address_print
进行输出;
笔记
-
malloc
和free
要成对出现; - 返回一个
*ptr
,接下来一定要写一个if(!ptr) ...