1. 项目概述
这个命令行日记本项目是一个典型的Linux系统编程实践案例,它综合运用了文件I/O操作、多线程编程、时间处理等核心Linux编程技术。作为一个嵌入式开发者,掌握这些基础系统编程技能至关重要,因为嵌入式开发本质上就是在Linux环境下与硬件资源打交道。
项目从最简单的"Hello World"开始,逐步实现了一个功能完整的命令行日记本,包含以下核心功能:
- 创建和管理日记文件
- 写入和追加日记内容
- 读取和浏览历史日记
- 自动保存草稿功能
- 基本的错误处理和用户交互
这个项目特别适合正在学习Linux系统编程的开发者,尤其是准备进入嵌入式开发领域的初学者。通过这个项目,你可以系统地练习Linux环境下的C语言编程,掌握实际开发中常用的技术点。
2. 环境准备与项目初始化
2.1 开发环境配置
在开始项目前,我们需要确保开发环境已经正确配置:
bash复制# 检查gcc编译器是否安装
gcc --version
# 如果没有安装,使用以下命令安装
sudo apt update
sudo apt install build-essential
对于中文用户,还需要配置中文输入法支持:
bash复制# 安装中文语言包
sudo apt install language-pack-zh-hans
# 安装fcitx输入法框架
sudo apt install fcitx fcitx-googlepinyin
安装完成后,在系统设置中添加中文输入法并重启系统。
2.2 项目目录结构
良好的项目结构是开发的基础。我们按照以下步骤初始化项目:
bash复制# 创建项目工作区
mkdir -p ~/my_workspace/diary_project
# 进入项目目录
cd ~/my_workspace/diary_project
# 初始化Git仓库
git init
# 创建.gitignore文件
echo "*.o" > .gitignore
echo "diary" >> .gitignore
echo "*.autosave" >> .gitignore
这个目录结构将包含:
main.c:主程序文件.gitignore:指定Git忽略的文件- 后续生成的日记文件(如
日记_20240315.txt)
3. 基础框架实现
3.1 创建主程序文件
我们首先创建一个基本的C程序框架:
c复制#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
printf("====== 命令行日记本 ======\n");
if (argc == 1) {
printf("用法: %s <命令>\n", argv[0]);
printf("可用命令:\n");
printf(" write - 写日记\n");
printf(" read - 读日记\n");
printf(" help - 显示帮助\n");
return 0;
}
if (strcmp(argv[1], "write") == 0) {
printf("你选择了:写日记\n");
}
else if (strcmp(argv[1], "read") == 0) {
printf("你选择了:读日记\n");
}
else if (strcmp(argv[1], "help") == 0) {
printf("你选择了:显示帮助\n");
}
else {
printf("错误:未知命令 '%s'\n", argv[1]);
}
return 0;
}
编译并测试这个基础框架:
bash复制gcc main.c -o diary
./diary
./diary write
./diary read
./diary help
3.2 代码结构优化
随着功能增加,我们需要优化代码结构,将不同功能模块化:
c复制// 函数声明
void show_help(void);
void write_diary(void);
void read_diary(void);
int main(int argc, char *argv[])
{
printf("====== 命令行日记本 v0.2 ======\n");
if (argc == 1) {
show_help();
return 0;
}
if (strcmp(argv[1], "write") == 0) {
write_diary();
}
else if (strcmp(argv[1], "read") == 0) {
read_diary();
}
else if (strcmp(argv[1], "help") == 0) {
show_help();
}
else {
printf("错误:未知命令 '%s'\n", argv[1]);
show_help();
}
return 0;
}
void show_help(void)
{
printf("\n使用方法:\n");
printf(" ./diary write - 写日记\n");
printf(" ./diary read - 读日记\n");
printf(" ./diary help - 显示帮助\n");
printf("\n");
}
这种模块化的结构使得代码更易于维护和扩展。
4. 写日记功能实现
4.1 基本文件操作
实现写日记功能需要掌握Linux下的文件操作API:
c复制#include <stdio.h>
#include <string.h>
#include <time.h>
void write_diary(void)
{
char filename[100];
char content[1024];
time_t now;
struct tm *timeinfo;
// 获取当前时间并生成文件名
time(&now);
timeinfo = localtime(&now);
strftime(filename, sizeof(filename), "日记_%Y%m%d.txt", timeinfo);
printf("正在写入: %s\n", filename);
// 打开文件
FILE *fp = fopen(filename, "w");
if (fp == NULL) {
printf("错误:无法创建文件!\n");
return;
}
// 写入时间标记
char datetime[100];
strftime(datetime, sizeof(datetime), "%Y年%m月%d日 %H:%M:%S", timeinfo);
fprintf(fp, "=== %s ===\n\n", datetime);
// 读取用户输入
printf("请输入你的日记内容(输入空行结束):\n");
printf("> ");
while (fgets(content, sizeof(content), stdin) != NULL) {
if (strcmp(content, "\n") == 0) {
break;
}
fputs(content, fp);
printf("> ");
}
fclose(fp);
printf("日记已保存到: %s\n", filename);
}
4.2 追加模式与错误处理
完善写日记功能,增加追加模式和更健壮的错误处理:
c复制void write_diary(void)
{
char filename[100];
char content[1024];
char answer[10];
time_t now;
struct tm *timeinfo;
time(&now);
timeinfo = localtime(&now);
strftime(filename, sizeof(filename), "日记_%Y%m%d.txt", timeinfo);
printf("日记文件: %s\n", filename);
// 检查文件是否已存在
FILE *check_fp = fopen(filename, "r");
if (check_fp != NULL) {
fclose(check_fp);
printf("今天已经写过日记了。\n");
printf("是否追加到文件末尾?(y/n): ");
fgets(answer, sizeof(answer), stdin);
answer[strcspn(answer, "\n")] = 0;
if (answer[0] != 'y' && answer[0] != 'Y') {
printf("操作取消\n");
return;
}
}
// 打开文件(追加或新建)
FILE *fp = fopen(filename, check_fp ? "a" : "w");
if (fp == NULL) {
printf("错误:无法打开文件 %s\n", filename);
printf("可能原因:\n");
printf(" - 磁盘已满\n");
printf(" - 没有写入权限\n");
printf(" - 文件名包含非法字符\n");
return;
}
// 写入时间标记
char datetime[100];
strftime(datetime, sizeof(datetime), "\n--- %Y年%m月%d日 %H:%M:%S ---\n", timeinfo);
if (check_fp == NULL) {
fprintf(fp, "========== 我的日记 ==========\n");
fprintf(fp, "开始日期: %s", datetime + 1);
} else {
fputs(datetime, fp);
}
// 读取用户输入
printf("请输入你的日记内容(输入空行结束):\n");
printf("> ");
while (fgets(content, sizeof(content), stdin) != NULL) {
if (strcmp(content, "\n") == 0) {
break;
}
fputs(content, fp);
printf("> ");
}
fclose(fp);
printf("日记已成功保存到: %s\n", filename);
}
5. 读日记功能实现
5.1 列出日记文件
实现读日记功能的第一步是能够列出所有日记文件:
c复制#include <dirent.h>
void list_diary_files(void)
{
DIR *dir;
struct dirent *entry;
int count = 0;
dir = opendir(".");
if (dir == NULL) {
printf("无法打开当前目录\n");
return;
}
printf("\n找到的日记文件:\n");
printf("------------------------------\n");
while ((entry = readdir(dir)) != NULL) {
if (strncmp(entry->d_name, "日记_", 4) == 0) {
char *ext = strrchr(entry->d_name, '.');
if (ext != NULL && strcmp(ext, ".txt") == 0) {
count++;
char date_str[20];
strncpy(date_str, entry->d_name + 4, 8);
date_str[8] = '\0';
printf("%d. %s-%s-%s\n", count,
date_str, date_str + 4, date_str + 6);
}
}
}
closedir(dir);
if (count == 0) {
printf("还没有写过日记。\n");
}
printf("------------------------------\n");
}
5.2 分页显示日记内容
实现分页显示功能,提升长日记的阅读体验:
c复制void read_diary(void)
{
char filename[100];
char choice[10];
int file_number;
int line_count = 0;
int page_size = 20;
char buffer[1024];
printf("读日记功能\n");
printf("==============================\n");
list_diary_files();
printf("请选择:\n");
printf("1. 查看今天的日记\n");
printf("2. 查看指定日期的日记\n");
printf("3. 按编号选择日记\n");
printf("请输入数字 (1-3): ");
fgets(choice, sizeof(choice), stdin);
if (choice[0] == '1') {
time_t now;
struct tm *timeinfo;
time(&now);
timeinfo = localtime(&now);
strftime(filename, sizeof(filename), "日记_%Y%m%d.txt", timeinfo);
} else if (choice[0] == '2') {
printf("请输入日期 (格式: YYYYMMDD): ");
char date_str[20];
fgets(date_str, sizeof(date_str), stdin);
date_str[strcspn(date_str, "\n")] = 0;
snprintf(filename, sizeof(filename), "日记_%s.txt", date_str);
} else if (choice[0] == '3') {
printf("请输入日记编号: ");
char num_str[10];
fgets(num_str, sizeof(num_str), stdin);
file_number = atoi(num_str);
DIR *dir = opendir(".");
struct dirent *entry;
int current = 0;
int found = 0;
if (dir != NULL) {
while ((entry = readdir(dir)) != NULL) {
if (strncmp(entry->d_name, "日记_", 4) == 0) {
char *ext = strrchr(entry->d_name, '.');
if (ext != NULL && strcmp(ext, ".txt") == 0) {
current++;
if (current == file_number) {
strcpy(filename, entry->d_name);
found = 1;
break;
}
}
}
}
closedir(dir);
}
if (!found) {
printf("无效的编号\n");
return;
}
} else {
printf("无效的选择\n");
return;
}
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
printf("文件不存在或无法读取!\n");
return;
}
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
line_count++;
}
rewind(fp);
printf("\n正在查看: %s\n", filename);
printf("总共有 %d 行,每页显示 %d 行\n", line_count, page_size);
printf("按 Enter 键继续,输入 q 退出\n");
printf("------------------------------\n");
int lines_shown = 0;
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
lines_shown++;
if (lines_shown % page_size == 0 && lines_shown < line_count) {
printf("------------------------------\n");
printf("-- 按 Enter 继续,输入 q 退出 --\n");
char cmd[10];
fgets(cmd, sizeof(cmd), stdin);
if (cmd[0] == 'q' || cmd[0] == 'Q') {
printf("退出查看\n");
break;
}
}
}
fclose(fp);
printf("------------------------------\n");
printf("阅读完成,共显示 %d 行\n", lines_shown);
}
6. 多线程自动保存功能
6.1 线程与互斥锁
实现后台自动保存功能需要使用多线程和互斥锁:
c复制#include <pthread.h>
#include <unistd.h>
// 全局共享变量
char g_draft_buffer[1024 * 1024];
int g_draft_size = 0;
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
int g_auto_save_enabled = 1;
char g_current_filename[256];
// 自动保存线程函数
void* auto_save_thread(void *arg)
{
char auto_save_file[300];
while (g_auto_save_enabled) {
sleep(5);
pthread_mutex_lock(&g_mutex);
if (g_draft_size > 0) {
snprintf(auto_save_file, sizeof(auto_save_file),
"%s.autosave", g_current_filename);
FILE *fp = fopen(auto_save_file, "w");
if (fp != NULL) {
fwrite(g_draft_buffer, 1, g_draft_size, fp);
fclose(fp);
int char_count = 0;
for (int i = 0; i < g_draft_size; i++) {
if (g_draft_buffer[i] == '\n') char_count++;
}
printf("\n[自动保存] 已保存 %d 字节 (%d 行)\n",
g_draft_size, char_count);
printf("> ");
fflush(stdout);
}
}
pthread_mutex_unlock(&g_mutex);
}
return NULL;
}
6.2 集成到写日记功能
将自动保存线程集成到写日记功能中:
c复制void write_diary(void)
{
char filename[256] = {0};
char content[1024];
time_t now;
struct tm *timeinfo;
pthread_t save_thread;
time(&now);
timeinfo = localtime(&now);
strftime(filename, sizeof(filename), "日记_%Y%m%d.txt", timeinfo);
strcpy(g_current_filename, filename);
printf("\n日记文件: %s\n", filename);
printf("自动保存已开启(每5秒保存一次草稿)\n");
FILE *check_fp = fopen(filename, "r");
FILE *fp;
if (check_fp != NULL) {
fclose(check_fp);
printf("今天已经写过日记了。\n");
printf("是否追加到文件末尾?(y/n): ");
char answer[10];
fgets(answer, sizeof(answer), stdin);
answer[strcspn(answer, "\n")] = 0;
if (answer[0] != 'y' && answer[0] != 'Y') {
printf("操作取消\n");
return;
}
fp = fopen(filename, "a");
} else {
fp = fopen(filename, "w");
}
if (fp == NULL) {
printf("无法打开文件 %s\n", filename);
return;
}
// 初始化自动保存
memset(g_draft_buffer, 0, sizeof(g_draft_buffer));
g_draft_size = 0;
g_auto_save_enabled = 1;
if (pthread_create(&save_thread, NULL, auto_save_thread, NULL) != 0) {
printf("无法创建自动保存线程\n");
g_auto_save_enabled = 0;
}
// 写入时间标记
char datetime[100];
strftime(datetime, sizeof(datetime), "\n--- %Y年%m月%d日 %H:%M:%S ---\n", timeinfo);
if (check_fp == NULL) {
fprintf(fp, "========== 我的日记 ==========\n");
fprintf(fp, "开始日期: %s", datetime + 1);
} else {
fputs(datetime, fp);
}
// 读取用户输入
printf("\n请输入你的日记内容(输入空行结束):\n");
printf("> ");
fflush(stdout);
while (fgets(content, sizeof(content), stdin) != NULL) {
if (strcmp(content, "\n") == 0) {
break;
}
pthread_mutex_lock(&g_mutex);
if (g_draft_size + strlen(content) < sizeof(g_draft_buffer)) {
strcpy(g_draft_buffer + g_draft_size, content);
g_draft_size += strlen(content);
}
pthread_mutex_unlock(&g_mutex);
fputs(content, fp);
printf("> ");
fflush(stdout);
}
// 停止自动保存
printf("\n⏳ 正在停止自动保存线程...\n");
g_auto_save_enabled = 0;
pthread_join(save_thread, NULL);
// 最终备份
pthread_mutex_lock(&g_mutex);
if (g_draft_size > 0) {
char auto_save_file[300];
snprintf(auto_save_file, sizeof(auto_save_file), "%s.autosave", filename);
FILE *backup_fp = fopen(auto_save_file, "w");
if (backup_fp != NULL) {
fwrite(g_draft_buffer, 1, g_draft_size, backup_fp);
fclose(backup_fp);
}
}
pthread_mutex_unlock(&g_mutex);
fprintf(fp, "\n");
fclose(fp);
printf("日记已成功保存到: %s\n", filename);
}
7. 项目总结与经验分享
7.1 关键知识点总结
通过这个项目,我们实践了以下Linux系统编程核心技术:
- 文件I/O操作:使用标准C库函数进行文件创建、读写和追加
- 时间处理:获取系统时间并格式化输出
- 目录操作:遍历目录内容,查找特定格式的文件
- 多线程编程:创建和管理线程,使用互斥锁保护共享资源
- 用户交互:处理命令行参数和用户输入
7.2 常见问题与解决方案
在实际开发中,可能会遇到以下问题:
-
中文乱码问题
- 解决方案:确保系统locale设置为UTF-8
bash复制
locale-gen zh_CN.UTF-8 update-locale LANG=zh_CN.UTF-8 -
文件权限问题
- 现象:无法创建或写入文件
- 解决方案:检查当前用户对目标目录的权限,必要时使用
chmod修改权限
-
线程同步问题
- 现象:程序偶尔崩溃或数据不一致
- 解决方案:确保所有共享资源的访问都受到互斥锁保护
-
内存越界问题
- 现象:程序随机崩溃
- 解决方案:使用工具如Valgrind检查内存访问
bash复制
valgrind --leak-check=full ./diary write
7.3 项目扩展建议
这个日记本还可以进一步扩展:
- 加密功能:使用加密库如OpenSSL对日记内容加密
- 网络同步:将日记同步到远程服务器
- Markdown支持:支持Markdown格式的日记
- 搜索功能:实现按关键词搜索日记内容
- 图形界面:使用GTK或Qt开发图形界面版本
8. Git版本控制实践
8.1 基本Git工作流
在项目开发过程中,我们使用Git进行版本控制:
bash复制# 初始化Git仓库
git init
# 添加文件到暂存区
git add main.c
# 提交更改
git commit -m "实现基础框架"
# 查看提交历史
git log --oneline
# 创建并切换到新分支
git checkout -b feature/autosave
# 合并分支
git checkout main
git merge feature/autosave
# 推送到远程仓库
git remote add origin <仓库URL>
git push -u origin main
8.2 实用的Git技巧
-
撤销本地修改
bash复制# 撤销工作区修改 git checkout -- <文件名> # 撤销暂存区修改 git reset HEAD <文件名> -
修改最后一次提交
bash复制# 修改提交信息 git commit --amend -m "新的提交信息" # 添加漏掉的文件 git add <漏掉的文件> git commit --amend --no-edit -
查看文件修改历史
bash复制git log -p <文件名> -
暂存当前工作
bash复制
git stash git stash pop
9. 编译与调试技巧
9.1 编译选项优化
使用合适的编译选项可以提升程序质量和调试效率:
bash复制# 基本编译
gcc main.c -o diary
# 启用所有警告
gcc -Wall -Wextra main.c -o diary
# 调试信息
gcc -g main.c -o diary
# 优化级别
gcc -O2 main.c -o diary
# 多线程支持
gcc -pthread main.c -o diary
9.2 GDB调试技巧
使用GDB调试程序:
bash复制# 启动GDB
gdb ./diary
# 常用命令
break main # 在main函数设置断点
run write # 运行程序并传入参数
next # 单步执行
print variable # 打印变量值
backtrace # 查看调用栈
quit # 退出GDB
9.3 内存调试工具
使用Valgrind检查内存问题:
bash复制valgrind --leak-check=full ./diary write
10. 实际开发中的经验分享
在开发这个日记本项目的过程中,我积累了一些宝贵的经验:
-
逐步验证:每实现一个小功能就立即编译测试,不要等到全部写完再测试
-
错误处理:对每个可能失败的系统调用都要检查返回值,并给出有意义的错误信息
-
资源释放:确保每个打开的文件、分配的内存都能正确释放,避免资源泄漏
-
线程安全:多线程环境下,对共享资源的访问必须加锁保护
-
用户友好:交互式程序应该提供清晰的提示和反馈,让用户知道程序在做什么
-
日志记录:在关键位置添加日志输出,便于调试和问题排查
-
边界检查:对所有用户输入和缓冲区操作都要进行边界检查,防止缓冲区溢出
-
版本控制:频繁提交小改动,每次提交都有明确的目的和清晰的描述
这个项目虽然不大,但涵盖了Linux系统编程的许多核心概念。通过实际动手实现,我对文件操作、多线程编程等有了更深入的理解。建议读者在学习过程中,不仅要理解代码,还要思考每个设计决策背后的原因,这样才能真正掌握系统编程的精髓。