在嵌入式开发中,轻量级的键值对存储往往比直接引入MySQL或Redis更切实际。本文将用数据库(Key-Value)的思想,从头用C语言实现一个基于二进制文件的简易数据库,并完成增、删、改、查全部功能。
1 简易数据库介绍
数据库(Database)是按特定结构组织、存储和管理数据的集合。在嵌入式软件开发中,如果不想引入重量级数据库,又想方便地管理少量数据,自己动手实现一个简易数据库是很好的选择。
数据可以直接保存在文件中,基本操作无非增、删、改、查。简单数据库最常见的存储形式是键值对(key-value),通过一个键(key)来对应一个具体值(value)。为了让value更实用,可以支持多种类型,编程中最常用的是整型、浮点型和字符串。
2 代码实现
我们将用C语言定义数据结构和数据库接口。
2.1 数据结构
数据存储采用二进制文件,既节省空间,又提高读写效率。每条记录包含键名、值类型、值长度和值数据。支持的三种基本类型:int、float和string。
首先用枚举定义值类型:
// 定义值类型
typedef enum {
TYPE_INT,
TYPE_FLOAT,
TYPE_STRING
} ValueType;
// 定义值的联合体
typedef union {
int int_val;
float float_val;
char* str_val;
} Value;
// 定义键值对结构
typedef struct {
char* key; // key
ValueType type; // 数据类型
Value value; // 数据的值
} KVNode;
// 数据库结构
typedef struct {
FILE* file; // 文件句柄
char* filename; // 文件名
} KVDB;
每条数据在文件中的存储结构如下:
#define KEY_NAME_LEN (64)
#define VALUE_DATA_LEN (128)
// 每条记录在文件中的存储格式
typedef struct {
char key[KEY_NAME_LEN]; // 键名,最长63个字符
ValueType type; // 值类型
int value_len; // 值的长度(主要用于字符串)
unsigned char data[VALUE_DATA_LEN]; // 存储值的数据
} FileRecord;

2.2 访问接口
数据库需要提供基本的操作接口:
- 打开/关闭数据库
- 插入/更新键值对(kvdb_set)
- 查询键值对(kvdb_get)
- 删除键值对(kvdb_delete)
- 遍历所有键值对(kvdb_iterate)
// 打开数据库
KVDB* kvdb_open(const char* filename);
// 关闭数据库
void kvdb_close(KVDB* db);
// 插入或更新键值对
bool kvdb_set(KVDB* db, const char* key, ValueType type, Value value);
// 获取键对应的值
bool kvdb_get(KVDB* db, const char* key, KVNode* result);
// 删除键值对
bool kvdb_delete(KVDB* db, const char* key);
// 遍历所有键值对
void kvdb_iterate(KVDB* db);
2.2.1 打开/关闭数据库
打开数据库的步骤很简单:
- 根据传入文件名构建KVDB信息
- 尝试fopen打开db文件,如果不存在则先创建一个空文件
- 将KVDB返回供后续使用
// 打开数据库(传入DB文件的名称)
KVDB* kvdb_open(const char* filename)
{
KVDB* db = (KVDB*)malloc(sizeof(KVDB));
if (!db)
{
return NULL;
}
db->filename = (char*)malloc(strlen(filename) + 1);
if (!db->filename)
{
free(db);
return NULL;
}
strcpy(db->filename, filename);
// 以读写方式打开文件,如果不存在则创建
db->file = fopen(filename, "rb+");
if (!db->file)
{
db->file = fopen(filename, "wb+");
if (!db->file)
{
printf("[%s] db file:%s not exist, create fail\n", __func__, filename);
free(db->filename);
free(db);
return NULL;
}
printf("[%s] db file:%s not exist, create ok\n", __func__, filename);
}
printf("[%s] db file:%s exist, open ok\n", __func__, filename);
return db;
}
关闭数据库就简单了,释放资源即可:
// 关闭数据库
void kvdb_close(KVDB* db)
{
if (db)
{
if (db->file)
{
fclose(db->file); // 关闭文件
}
free(db->filename); // 释放文件名
free(db); // 释放KVDB结构
}
}
2.2.2 插入/更新
插入或更新一条记录,核心思路是:
- 根据key、type和value构建一条FileRecord
- 遍历db文件中的每一行,匹配key是否存在
- 若key已存在,则更新该条记录的值
- 若key不存在,则在文件末尾追加新记录
// 插入或更新键值对
bool kvdb_set(KVDB* db, const char *key, ValueType type, Value value)
{
if (!db || !db->file || !key)
{
printf("[%s] NULL ptr\n", __func__);
return false;
}
// 检查键长度
if (strlen(key) >= KEY_NAME_LEN)
{
printf("[%s] key:%s len > %d\n", __func__, key, KEY_NAME_LEN);
return false;
}
FileRecord record;
strcpy(record.key, key);
record.type = type;
memset(record.data, 0, sizeof(record.data));
// 根据类型处理值
switch (type)
{
case TYPE_INT:
{
record.value_len = sizeof(int);
memcpy(record.data, &value.int_val, sizeof(int));
break;
}
case TYPE_FLOAT:
{
record.value_len = sizeof(float);
memcpy(record.data, &value.float_val, sizeof(float));
break;
}
case TYPE_STRING:
{
if (!value.str_val) return false;
record.value_len = strlen(value.str_val) + 1; // 包含终止符
if (record.value_len > sizeof(record.data)) return false;
strcpy((char*)record.data, value.str_val);
break;
}
default:
return false;
}
// 先尝试找到并替换现有记录
fseek(db->file, 0, SEEK_SET);
long pos;
while ((pos = ftell(db->file)) != EOF)
{
FileRecord existing;
if (fread(&existing, sizeof(FileRecord), 1, db->file) != 1)
{
break;
}
if (strcmp(existing.key, key) == 0)
{
// 找到相同的键,替换记录
fseek(db->file, pos, SEEK_SET);
if (fwrite(&record, sizeof(FileRecord), 1, db->file) == 1)
{
print_db_set_info(__func__, key, type, value, 0, 1);
fflush(db->file);
return true;
}
print_db_set_info(__func__, key, type, value, 0, 0);
return false;
}
}
// 如果没找到,则在文件末尾添加新记录
fseek(db->file, 0, SEEK_END);
if (fwrite(&record, sizeof(FileRecord), 1, db->file) == 1)
{
fflush(db->file);
print_db_set_info(__func__, key, type, value, 1, 1);
return true;
}
print_db_set_info(__func__, key, type, value, 1, 0);
return false;
}
配套的打印辅助函数:
void print_db_set_info(const char *func, const char *key, ValueType type, Value value, int is_insert, int is_success)
{
char *state = is_success ? "ok" : "fail";
char *insertOrUpdate = is_insert ? "insert" : "update";
if (TYPE_INT == type)
{
printf("[%s] %s key:%s value:%d [%s]\n", func, insertOrUpdate, key, value.int_val, state);
}
else if (TYPE_FLOAT == type)
{
printf("[%s] %s key:%s value:%f [%s]\n", func, insertOrUpdate, key, value.float_val, state);
}
else if (TYPE_STRING == type)
{
printf("[%s] %s key:%s value:%s [%s]\n", func, insertOrUpdate, key, value.str_val, state);
}
}
2.2.3 查询
查询操作就是遍历文件记录,找到匹配的key后把数据拷出来:
- 遍历db文件的每一行(FileRecord)
- 如果key匹配,则将数据填充到KVNode的result中并返回
- 如果遍历完还没找到,返回失败
// 获取键对应的值
bool kvdb_get(KVDB* db, const char* key, KVNode* result)
{
if (!db || !db->file || !key || !result)
{
printf("[%s] NULL ptr\n", __func__);
return false;
}
fseek(db->file, 0, SEEK_SET);
FileRecord record;
while (fread(&record, sizeof(FileRecord), 1, db->file) == 1)
{
// 找到了key
if (strcmp(record.key, key) == 0)
{
result->key = (char*)malloc(strlen(record.key) + 1);
if (!result->key)
{
printf("[%s] db no key:%s\n", __func__, key);
return false;
}
// 赋值result->key
strcpy(result->key, record.key);
// 赋值result->type
result->type = record.type;
// 赋值result->value
switch (record.type)
{
case TYPE_INT:
{
memcpy(&result->value.int_val, record.data, sizeof(int));
break;
}
case TYPE_FLOAT:
{
memcpy(&result->value.float_val, record.data, sizeof(float));
break;
}
case TYPE_STRING:
{
result->value.str_val = (char*)malloc(record.value_len);
if (!result->value.str_val)
{
print_db_get_info(__func__, key, result, 0);
free(result->key);
return false;
}
strcpy(result->value.str_val, (char*)record.data);
break;
}
default:
{
print_db_get_info(__func__, key, result, 0);
free(result->key);
return false;
}
}
print_db_get_info(__func__, key, result, 1);
return true;
}
}
// 未找到键
return false;
}
查询结果的打印函数:
void print_db_get_info(const char *func, const char *key, const KVNode* result, int is_success)
{
if (is_success)
{
if (TYPE_INT == result->type)
{
printf("[%s] get key:%s value:%d [ok]\n", func, key, result->value.int_val);
}
else if (TYPE_FLOAT == result->type)
{
printf("[%s] get key:%s value:%f [ok]\n", func, key, result->value.float_val);
}
else if (TYPE_STRING == result->type)
{
printf("[%s] get key:%s value:%s [ok]\n", func, key, result->value.str_val);
}
}
else
{
if (TYPE_INT == result->type)
{
printf("[%s] get key:%s [fail]\n", func, key);
}
else if (TYPE_FLOAT == result->type)
{
printf("[%s] get key:%s [fail]\n", func, key);
}
else if (TYPE_STRING == result->type)
{
printf("[%s] get key:%s [fail]\n", func, key);
}
}
}
2.2.4 删除
删除操作稍微复杂一点,采用“临时文件”方案:
- 创建一个临时文件
- 遍历原db文件的每一行(FileRecord),如果不是要删除的key,就拷贝到临时文件;如果是,直接跳过
- 处理完所有记录后,删除原db文件
- 将临时db文件重命名为原db文件名,并重新打开db文件

// 删除键值对
bool kvdb_delete(KVDB* db, const char* key)
{
if (!db || !db->file || !key)
{
printf("[%s] NULL ptr\n", __func__);
return false;
}
// 创建一个临时文件
char temp_filename[256];
sprintf(temp_filename, "%s.tmp", db->filename);
FILE* temp_file = fopen(temp_filename, "wb+");
if (!temp_file)
{
printf("[%s] fopen tmp file:%s fail\n", __func__, temp_filename);
return false;
}
bool found = false;
fseek(db->file, 0, SEEK_SET);
FileRecord record;
// 复制所有不匹配的记录到临时文件
while (fread(&record, sizeof(FileRecord), 1, db->file) == 1)
{
if (strcmp(record.key, key) != 0)
{
fwrite(&record, sizeof(FileRecord), 1, temp_file);
}
else
{
printf("[%s] delete key:%s\n", __func__, key);
found = true;
}
}
// 关闭文件
fclose(db->file);
fclose(temp_file);
// 删除原文件,重命名临时文件
printf("[%s] remove old db, then rename tmp db to norml db\n", __func__);
remove(db->filename);
rename(temp_filename, db->filename);
// 重新打开数据库文件
db->file = fopen(db->filename, "rb+");
if (!db->file)
{
return false;
}
return found;
}
2.2.5 遍历数据库
为了方便查看db中存储了哪些数据,可以写一个遍历函数,把所有键值对打印出来。
// 遍历所有键值对
void kvdb_iterate(KVDB* db)
{
if (!db || !db->file)
{
return;
}
fseek(db->file, 0, SEEK_SET);
FileRecord record;
KVNode node;
while (fread(&record, sizeof(FileRecord), 1, db->file) == 1)
{
node.key = record.key;
node.type = record.type;
switch (record.type)
{
case TYPE_INT:
{
memcpy(&node.value.int_val, record.data, sizeof(int));
break;
}
case TYPE_FLOAT:
{
memcpy(&node.value.float_val, record.data, sizeof(float));
break;
}
case TYPE_STRING:
{
node.value.str_val = (char*)record.data;
break;
}
}
print_node(&node);
}
}
对应的打印函数:
// 打印键值对
void print_node(KVNode* node)
{
if (!node)
{
return;
}
printf("Key: %s,\t Type: ", node->key);
switch (node->type)
{
case TYPE_INT:
{
printf("int,\t Value: %d\n", node->value.int_val);
break;
}
case TYPE_FLOAT:
{
printf("float,\t Value: %.2f\n", node->value.float_val);
break;
}
case TYPE_STRING:
{
printf("string,\t Value: %s\n", node->value.str_val);
break;
}
default:
printf("unknown\n");
}
}
2.3 测试代码
写一个简单的main函数来验证功能:
int main()
{
char *db_file = "test_kv.db";
// 打开数据库
KVDB* db = kvdb_open(db_file);
if (!db)
{
printf("[%s] 无法打开数据库文件\n", __func__);
return 1;
}
printf("[%s] open db file:%s ok\n", __func__, db_file);
Value val;
// 插入一些数据
val.int_val = 20;
kvdb_set(db, "count", TYPE_INT, val);
val.float_val = 3.14159f;
kvdb_set(db, "pi", TYPE_FLOAT, val);
val.str_val = "25-09-13 13:48";
kvdb_set(db, "time", TYPE_STRING, val);
// 查询并打印数据
KVNode result;
if (kvdb_get(db, "count", &result))
{
printf("[%s] 查询到 age: %d\n", __func__, result.value.int_val);
free(result.key); // 释放分配的内存
}
if (kvdb_get(db, "pi", &result))
{
printf("[%s] 查询到 pi: %.2f\n", __func__, result.value.float_val);
free(result.key);
}
if (kvdb_get(db, "time", &result))
{
printf("[%s] 查询到 time: %s\n", __func__, result.value.str_val);
free(result.value.str_val); // 释放字符串内存
free(result.key);
}
// 更新数据
val.int_val = 30;
kvdb_set(db, "count", TYPE_INT, val);
if (kvdb_get(db, "count", &result))
{
printf("[%s] 更新后 count: %d\n", __func__, result.value.int_val);
free(result.key);
}
// 遍历所有数据
printf("\n所有数据:\n");
kvdb_iterate(db);
// 删除数据
kvdb_delete(db, "pi");
printf("\n删除 pi 后所有数据:\n");
kvdb_iterate(db);
// 关闭数据库
kvdb_close(db);
return 0;
}
3 运行测试
在Ubuntu下编译运行,输出效果如下:

可以看到,程序正确完成了插入、查询、更新、删除和遍历的全部流程。
4 总结
通过C语言从零实现这个Key-Value数据库,我们深入理解了文件I/O、二进制存储以及数据结构的实际应用。这种轻量级方案非常适合嵌入式或资源受限的环境,也为进一步扩展(如索引、事务)打下了基础。
本文由云栈社区整理提供,更多C/C++、数据库等底层技术干货,欢迎访问 yunpan.plus 一起交流学习。