用 C 工程标准实现几个简单的 C 语言项目。
前言
因为转过专业的原因,有一门 C 语言实训还没有补,学弟帮我找了个老师的实训,跟完后感觉或许是我校为数不多的优质实训之一。
可以锻炼 make、google test、第三方库引入、debug 的应用能力,这是对低年级学生的珍贵礼物。
可以意识到项目的模块划分是一个噩梦,可以线上学习经典的软件工程课程: MIT 6.092 (20h) + MIT 6.031(130h) 。
C 工程标准实现一个 windows 时钟显示器
核心流程
热身项目,我们看一下它的核心流程
main() → 初始化 (srand)
↓
进入主循环
├── clear_screen() 清屏
├── get_current_time() 获取当前系统时间
│ └── GetLocalTime / snprintf 生成时间字符串 "HH:MM:SS"
├── display_time(time_str) 显示时间
│ └── 遍历时间字符串的每个字符 c
│ └── get_font(c) 获取数字点阵
│ └── putchar 逐行打印
├── Sleep(1000) 等待 1 秒
└── 循环
模块划分
这个项目最困难点在于模块划分。我们可以基于 display.c 划分模块,显示器模块只负责现实的基本工作。对于整个项目,拆分掉显示器的工作后,剩下一个字符模块和时钟模块是合理的,我们这里以划分出 clock.c 和 font.c 模块。
这里我们隐藏具体实现,只暴露出最核心的功能。功能划分如下
时钟模块:时钟模块的核心功能为获取系统时间,并以 "HH:MM:SS" 字符长的格式返回。
#ifndef CLOCK_H
#define CLOCK_H
// 获取当前系统时间,格式为 "HH:MM:SS"
// 返回值:指向静态缓冲区的指针,每次调用会覆盖之前的内容
const char* get_current_time(void);
#endif
字体模块:核心功能为,根据传入的字符,返回改字符对应的点整。
#ifndef FONT_H
#define FONT_H
// 点阵尺寸
#define FONT_ROWS 5
#define FONT_COLS 3
// 获取字符对应的点阵:1 表示亮,0 表示暗
// 支持 '0'~'9' 和 ':'
const int (*get_font(char))[FONT_COLS];
#endif
显示模块:负责显示前的清屏,接受时间字符串并进行具体绘制
#ifndef DISPLAY_H
#define DISPLAY_H
// 清屏
void clear_screen(void);
// 在屏幕上绘制时间字符串(格式 "HH:MM:SS")
void display_time(const char* time_str);
#endif
测试
虽然要求项目本体严格使用 C 语言,但是并没有限制测试工具的使用。这里我们使用 C++ 测试工具 Google Test ,下载到本地第三方 third_party 中。
需要提供一下 C 对 C++ 的接口头文件
#ifndef C_API_H
#define C_API_H
extern "C" {
#include "font.h"
#include "clock.h"
}
#endif
测试是一个项目中非常重要的一块,和模块划分一样,可以实现得不优美,但不能没有。
如何写出具体的测试也是非常伤脑筋的事情,这依然提示着我们学习软将工程的必要性。
比如我在这里实现了常见的边界测试、指针测试、格式测试,还有各种不知道有没有意义的测试。
#include <gtest/gtest.h>
#include <cstring>
#include <cctype>
#include "c_api.h"
// 测试获取时间返回非空指针
TEST(ClockTest, ReturnsNonNull) {
EXPECT_NE(get_current_time(), nullptr);
}
// 测试时间字符串长度为 8(HH:MM:SS)
TEST(ClockTest, LengthIs8) {
const char* t = get_current_time();
ASSERT_NE(t, nullptr);
EXPECT_EQ(std::strlen(t), 8u);
}
// 测试格式:字符应为数字或冒号,且冒号在正确位置
TEST(ClockTest, FormatCorrect) {
const char* t = get_current_time();
ASSERT_NE(t, nullptr);
for (int i = 0; i < 8; ++i) {
if (i == 2 || i == 5) {
EXPECT_EQ(t[i], ':');
} else {
EXPECT_TRUE(std::isdigit(t[i])) << "Expected digit at position " << i;
}
}
}
Makefile
Makefile 本身并没有多大的值得说明的,只是一些基础的 dirtywork ,熟悉 g++、gcc 的命令,make 本身的语法规则,项目从从编译开始到最后链接成一个可执行文件的基础流程。
额外需要稍微深刻地了解一些静态链接(这里只需要用到静态链接)。把各个模块单独编译成可重定位的目标文件,然后链接(静态链接)成可执行文件。
后面的项目也大同小异。
# ============================================================
# 点阵时钟 (Dot Matrix Clock) - Makefile
# 使用 MinGW-w64 在 Windows 上编译本项目。
#
# 主要目标:
# all - 编译主程序和测试程序(默认)
# clean - 删除所有编译产物和可执行文件
#
# 可执行文件输出到 bin/ 目录。
# ============================================================
# 编译器设置
CC = gcc # C 编译器
CXX = g++ # C++ 编译器
# 编译选项
# -g : 生成调试信息
# -Wall : 启用所有常用警告
CFLAGS = -g -Wall
CXXFLAGS = -g -Wall
# 头文件搜索路径
# -Isrc : 项目源码目录
# -Itests : 测试代码目录
# -Ithird_party/gtest/... : Google Test 头文件路径
INCLUDES = -Isrc \
-Itests \
-Ithird_party/gtest/googletest/include \
-Ithird_party/gtest/googletest
# 最终可执行文件(输出到 bin/ 目录)
TARGET = bin/dot_clock.exe # 主程序
TEST_TARGET = bin/font_test.exe # 测试程序(包含 font 和 clock 测试)
# 主程序依赖的目标文件
MAIN_OBJS = src/main.o \
src/font.o \
src/clock.o \
src/display.o
# 测试程序依赖的目标文件
TEST_OBJS = src/font.o \
src/clock.o \
src/display.o \
tests/font_test.o \
tests/clock_test.o \
third_party/gtest/googletest/src/gtest-all.o \
third_party/gtest/googletest/src/gtest_main.o
# ============================================================
# 默认目标:编译主程序和测试程序
# ============================================================
all: $(TARGET) $(TEST_TARGET)
# ============================================================
# 链接主程序
# 依赖所有主程序目标文件,链接成可执行文件
# ============================================================
$(TARGET): $(MAIN_OBJS)
$(CC) $(CFLAGS) -o $@ $^
# ============================================================
# 链接测试程序
# ============================================================
$(TEST_TARGET): $(TEST_OBJS)
$(CXX) $(CXXFLAGS) -o $@ $^
# ============================================================
# 各模块的编译规则(显式指定头文件依赖)
# ============================================================
src/main.o: src/main.c
$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
src/clock.o: src/clock.c src/clock.h
$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
src/font.o: src/font.c src/font.h
$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
src/display.o: src/display.c src/display.h
$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
tests/font_test.o: tests/font_test.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) -c -o $@ $<
tests/clock_test.o: tests/clock_test.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) -c -o $@ $<
third_party/gtest/googletest/src/gtest-all.o: third_party/gtest/googletest/src/gtest-all.cc
$(CXX) $(CXXFLAGS) $(INCLUDES) -c -o $@ $<
third_party/gtest/googletest/src/gtest_main.o: third_party/gtest/googletest/src/gtest_main.cc
$(CXX) $(CXXFLAGS) $(INCLUDES) -c -o $@ $<
# ============================================================
# 清理规则:删除所有编译产物和可执行文件
# 使用 Windows 反斜杠作为路径分隔符
# ============================================================
clean:
del /Q src\main.o \
src\font.o \
src\clock.o \
src\display.o \
tests\font_test.o \
tests\clock_test.o \
third_party\gtest\googletest\src\gtest-all.o \
third_party\gtest\googletest\src\gtest_main.o \
bin\dot_clock.exe \
bin\font_test.exe
项目结构
补上项目的 LISENCE、README.md 文件对整个项目起到一个基本的解释和说明,之后一个 C 语言项目就这样结束了。
作业一/
├── src/ # 源代码
│ ├── main.c # 程序入口,主循环
│ ├── clock.c/clock.h # 获取系统时间
│ ├── display.c/display.h # 清屏与绘制点阵
│ └── font.c/font.h # 数字点阵定义
├── tests/ # 测试代码
│ ├── c_api.h # 包裹头,统一引入 C 模块
│ ├── font_test.cpp # font 模块测试
│ └── clock_test.cpp # clock 模块测试
├── third_party/gtest/ # 本地 Google Test 源码
├── Makefile # 构建规则
├── README.md
└── LICENSE
浙公网安备 33010602011771号