项目概述与设计思路
一个简单的电子表格程序需要具备以下核心功能:

(图片来源网络,侵删)
- 单元格网格:一个由行和列组成的二维网格,每个格子可以存储数据。
- 数据类型:单元格不仅能存储文本(字符串),还能存储数字,并能进行简单的数学运算。
- 公式计算:支持以 开头的公式,
=A1+B2。 - 用户交互:允许用户通过命令行或简单的图形界面选择单元格、输入和编辑内容。
- 显示:能将整个表格清晰地展示在屏幕上。
设计思路
- 数据结构是核心:如何表示一个单元格是关键,一个单元格需要存储它的(可能是字符串、数字或公式)和显示值(计算后的结果)。
- 解析与计算:需要能够解析用户输入的公式(如
=A1+B2),提取出单元格引用,获取这些单元格的值,并执行计算。 - 交互与渲染:需要一个循环来持续接收用户输入,并根据用户指令更新或显示表格。
核心数据结构
我们使用一个二维结构体数组来表示整个表格。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
// 定义最大行数和列数
#define MAX_ROWS 20
#define MAX_COLS 10
// 定义单元格的数据类型
typedef enum {
CELL_TYPE_EMPTY,
CELL_TYPE_STRING,
CELL_TYPE_NUMBER,
CELL_TYPE_FORMULA
} CellType;
// 单元格结构体
typedef struct {
CellType type;
char raw_input[256]; // 存储用户原始输入,如 "Hello", "123", "=A1+B2"
double value; // 存储计算后的数值,用于显示和公式计算
char display_text[256]; // 存储要显示的文本,如 "Hello", "123", "15"
} Cell;
// 电子表格结构体
typedef struct {
Cell cells[MAX_ROWS][MAX_COLS];
} Spreadsheet;
结构体解析:
CellType:枚举类型,明确单元格的类型,方便后续处理。Cell:这是核心,它包含了:type:单元格的类型。raw_input:保留用户输入的原始字符串,这对于重新计算公式至关重要。value:一个double类型的值,无论原始输入是数字还是公式的结果,都将其转换为数值存储,这使得公式计算非常方便。display_text:最终显示给用户的文本,对于文本和数字,它和raw_input一样;对于公式,它是计算结果。
Spreadsheet:一个Cell的二维数组,构成了整个表格。
关键功能实现
1 初始化表格
创建一个函数,将所有单元格初始化为空。
void init_spreadsheet(Spreadsheet *sheet) {
for (int i = 0; i < MAX_ROWS; i++) {
for (int j = 0; j < MAX_COLS; j++) {
sheet->cells[i][j].type = CELL_TYPE_EMPTY;
sheet->cells[i][j].raw_input[0] = '\0';
sheet->cells[i][j].value = 0;
sheet->cells[i][j].display_text[0] = '\0';
}
}
}
2 解析单元格引用 (如 "A1")
这是实现公式功能的基础,我们需要一个函数,将类似 "A1" 的字符串转换为行和列的索引。

(图片来源网络,侵删)
// 将单元格引用 (如 "A1") 转换为 (row, col) 索引
// 返回 1 表示成功,0 表示失败
int parse_cell_reference(const char *ref, int *row, int *col) {
if (strlen(ref) < 2 || !isalpha(ref[0])) {
return 0; // 无效格式
}
// 解析列 (A, B, C, ...)
*col = toupper(ref[0]) - 'A';
// 解析行 (1, 2, 3, ...)
char *end_ptr;
long r = strtol(ref + 1, &end_ptr, 10);
if (*end_ptr != '\0' || r < 1 || r > MAX_ROWS) {
return 0; // 行号无效或格式错误
}
*row = (int)r - 1; // 转换为 0-based 索引
return 1;
}
3 公式计算引擎
这是最复杂的部分,当用户输入一个公式时,我们需要:
- 识别出公式中的单元格引用(如
A1,B2)。 - 递归或迭代地获取这些引用单元格的值。
- 执行计算。
为了简化,我们先实现一个不处理括号和运算符优先级的版本。
// 获取单元格的值(用于公式计算)
// 如果是公式,会尝试计算
double get_cell_value(Spreadsheet *sheet, int row, int col) {
if (row < 0 || row >= MAX_ROWS || col < 0 || col >= MAX_COLS) {
return 0; // 单元格越界,返回0
}
Cell *cell = &sheet->cells[row][col];
if (cell->type == CELL_TYPE_NUMBER) {
return cell->value;
} else if (cell->type == CELL_TYPE_FORMULA) {
// 如果是公式,重新计算它
// 这里我们直接调用 evaluate_formula,可能会造成循环依赖
// 在更完善的实现中,需要一个依赖图和拓扑排序来处理
evaluate_formula(sheet, cell);
return cell->value;
}
// 其他类型(字符串、空)无法参与数值计算,返回0
return 0;
}
// 计算公式 (非常简化的版本,仅支持 + 和 -)
void evaluate_formula(Spreadsheet *sheet, Cell *cell) {
char formula[256];
strcpy(formula, cell->raw_input + 1); // 去掉 '='
char *token = strtok(formula, "+-");
double result = 0;
int op = 1; // 1 for +, -1 for -
char *rest = formula;
while (token != NULL) {
// 去除 token 前后的空格
while (*rest == ' ') rest++;
char *token_start = rest;
while (*token != '+' && *token != '-' && *token != '\0') {
rest++;
token = strtok(NULL, "+-");
}
int token_len = rest - token_start;
char current_token[256];
strncpy(current_token, token_start, token_len);
current_token[token_len] = '\0';
// 检查 token 是否是单元格引用
int ref_row, ref_col;
if (parse_cell_reference(current_token, &ref_row, &ref_col)) {
result += op * get_cell_value(sheet, ref_row, ref_col);
} else {
// 如果不是引用,尝试解析为数字
double num = atof(current_token);
result += op * num;
}
// 确定下一个运算符
if (*rest == '+') op = 1;
else if (*rest == '-') op = -1;
rest++;
}
cell->value = result;
snprintf(cell->display_text, sizeof(cell->display_text), "%.2f", result);
}
4 设置单元格内容
这个函数是用户输入的入口,它会根据输入的内容决定单元格的类型,并触发相应的计算。
void set_cell_content(Spreadsheet *sheet, int row, int col, const char *input) {
if (row < 0 || row >= MAX_ROWS || col < 0 || col >= MAX_COLS) {
return;
}
Cell *cell = &sheet->cells[row][col];
strcpy(cell->raw_input, input);
// 检查是否是公式
if (input[0] == '=') {
cell->type = CELL_TYPE_FORMULA;
evaluate_formula(sheet, cell);
}
// 检查是否是数字
else if (atof(input) != 0 || strcmp(input, "0") == 0) {
cell->type = CELL_TYPE_NUMBER;
cell->value = atof(input);
strcpy(cell->display_text, input);
}
// 否则视为文本
else {
cell->type = CELL_TYPE_STRING;
cell->value = 0; // 文本没有数值意义
strcpy(cell->display_text, input);
}
}
5 渲染(打印)表格
将表格以美观的格式打印到控制台。

(图片来源网络,侵删)
void print_spreadsheet(Spreadsheet *sheet) {
// 打印列标题 (A, B, C, ...)
printf(" ");
for (int j = 0; j < MAX_COLS; j++) {
printf(" %c ", 'A' + j);
}
printf("\n");
for (int i = 0; i < MAX_ROWS; i++) {
// 打印行号
printf("%2d |", i + 1);
for (int j = 0; j < MAX_COLS; j++) {
Cell *cell = &sheet->cells[i][j];
printf(" %s ", cell->display_text);
}
printf("\n");
}
}
主程序与用户交互
将以上功能组合起来,形成一个可以运行的程序。
int main() {
Spreadsheet sheet;
init_spreadsheet(&sheet);
printf("C Spreadsheet (v0.1)\n");
printf("Commands: set <cell> <content>, print, quit\n");
printf("Example: set A1 100\n");
printf("Example: set B1 '=A1+50'\n");
char command[256];
while (1) {
printf("\n> ");
fgets(command, sizeof(command), stdin);
command[strcspn(command, "\n")] = 0; // 去掉换行符
if (strncmp(command, "quit", 4) == 0) {
break;
} else if (strncmp(command, "print", 5) == 0) {
print_spreadsheet(&sheet);
} else if (strncmp(command, "set ", 4) == 0) {
char cell_ref[10];
char content[256];
// 简单解析 "set A1 Hello World"
// 注意:这个解析很脆弱,不能处理内容中带空格的情况
if (sscanf(command, "set %s %[^\n]", cell_ref, content) == 2) {
int row, col;
if (parse_cell_reference(cell_ref, &row, &col)) {
set_cell_content(&sheet, row, col, content);
printf("Cell %s set to '%s'\n", cell_ref, content);
} else {
printf("Invalid cell reference: %s\n", cell_ref);
}
} else {
printf("Invalid set command format.\n");
}
} else {
printf("Unknown command.\n");
}
}
return 0;
}
如何编译和运行
- 将以上所有代码片段保存为一个文件,
cspreadsheet.c。 - 使用 GCC 编译:
gcc cspreadsheet.c -o cspreadsheet -lm
-lm是必须的,因为我们使用了math.h中的atof等函数。
- 运行程序:
./cspreadsheet
示例交互:
C Spreadsheet (v0.1)
Commands: set <cell> <content>, print, quit
Example: set A1 100
Example: set B1 '=A1+50'
> set A1 100
Cell A1 set to '100'
> set A2 Hello
Cell A2 set to 'Hello'
> set B1 '=A1+50'
Cell B1 set to '=A1+50'
> set B2 '=A1+A2'
Cell B2 set to '=A1+A2'
> print
A B C D E F G H I J
1 | 100 | | | | | | | | |
2 | Hello | | | | | | | | |
3 | | | | | | | | | |
4 | | | | | | | | | |
5 | | | | | | | | | |
6 | | | | | | | | | |
7 | | | | | | | | | |
8 | | | | | | | | | |
9 | | | | | | | | | |
10 | | | | | | | | | |
11 | | | | | | | | | |
12 | | | | | | | | | |
13 | | | | | | | | | |
14 | | | | | | | | | |
15 | | | | | | | | | |
16 | | | | | | | | | |
17 | | | | | | | | | |
18 | | | | | | | | | |
19 | | | | | | | | | |
20 | | | | | | | | | |
> quit
(注意:上面的 print 输出中,B1 和 B2 的公式没有被正确计算和显示,因为我们之前的 evaluate_formula 实现非常简陋,这是一个需要改进的地方。)
进一步的扩展与改进方向
这个 CSpreadsheet 是一个非常好的起点,但要成为一个功能完善的程序,还有很多工作可以做:
-
更强大的公式引擎:
- 运算符优先级:实现 和 的优先级高于 和 ,可以使用逆波兰表达式(RPN)或递归下降解析器。
- 更多函数:实现
SUM(),AVERAGE(),MAX(),MIN()等常用函数。 - 错误处理:处理循环引用(如
A1的公式是=B1,B1的公式是=A1)、无效的单元格引用、语法错误等。
-
改进的用户界面:
- 光标移动:使用 ANSI 转义码实现一个可以在表格中移动的光标,并高亮显示当前选中的单元格。
- 直接编辑:允许用户直接在单元格位置输入内容,而不是通过
set命令。
-
文件 I/O:
- 保存:将表格内容保存到文件(如 CSV 或自定义二进制格式)。
- 加载:从文件中读取表格内容。
-
数据类型增强:
- 支持日期/时间类型。
- 支持布尔值 (
TRUE/FALSE),用于逻辑判断(如=IF(A1>10, "Yes", "No"))。
-
代码重构与健壮性:
- 内存管理:如果单元格内容可能很长,可以考虑使用
malloc动态分配内存,而不是固定大小的字符数组。 - 模块化:将代码拆分为多个文件(如
cell.c,formula.c,ui.c),提高可维护性。 - 更健壮的解析:改进
set命令的解析,使其能正确处理带空格的内容。
- 内存管理:如果单元格内容可能很长,可以考虑使用
这个项目从零开始,每一步都能让你深入理解 C 语言的指针、结构体、字符串处理和算法设计,是一个极佳的练手项目,祝你编程愉快!
