1. 写在前面

我是一个大二上的学生,学 C 快一年了,学得很慢。说实话,到现在才把线性数据结构学完,正准备学树形结构。老实说挺迷茫的——因为和周围软工方向的同学比,我发现自己更适合走底层路线:数据结构与算法学完后,赶紧去了解操作系统及其内核的知识,以后的研究方向和大三大四的项目都准备向系统层面靠拢。我觉得自己不适合去卷应用层的软件工程,所以大一学得很慢,没有去快速地学习各种技术栈。

这种路线确实挺折磨的。我习惯抛出问题去间接了解各种知识,而不是被动接收,但这导致我的进度和成就根本比不上周围同学。老实说,怎么着我都不怕,但一不留神就从网上了解到计算机就业的艰难,就忍不住怀疑自己的路线是不是走错了。照这个节奏,想打比赛都要等到大二下学期了,总得为考研复试作准备,就是很迷茫。

写这篇博客,一是因为写博客是程序员的必要素养,二是因为我确实需要一点反馈。所以我把最近手写的一个输入库分享出来,只想让大家评价一下这个学习成果有没有意义——毕竟太久没有反馈了,谢谢大家。

2. 我为什么要手写一个输入库

在开始之前,先说说我为什么会有这个想法:

  • 想深入理解底层原理:当初入门C语言的时候,就被库函数scanf坑过,所以有了这个想法。
  • 锻炼调试能力:手写代码意味着要自己面对各种边界情况和 bug,这是最好的练习。
  • 为后续学习打基础:输入处理是很多程序的基础模块,自己写一遍印象会深很多。
  • 只会 C,就把它学透:我目前只会 C(Java 学到集合,C++ 还没碰),与其焦虑自己会得少,不如先把 C 这门最接近底层的语言吃透。手写输入库,就是一次很好的「把 C 学透」的实践。

3. 我的输入库:功能与代码结构

这个输入库大约 200 行,核心功能包括:

  • 读取用户输入并做基础校验
  • 支持多种数据类型(整数、浮点数、字符串)
  • 对非法输入给出友好提示,而不是直接崩溃

下面是完整代码(约 200 行,我一个字都没改):

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

#define BUFFER_SIZE 256

//上下文结构体
//类
enum InputStatus {
	INPUT_OK, //0,无异常
	INPUT_EOF,  //1,输入流结束
	INPUT_READ_ERROR, //2,读取标准输入流失败
};

typedef struct InputContext {
	char buf[BUFFER_SIZE];
	char* cp;
	enum InputStatus status;
}Inp;

//构造函数
//创建对象
Inp* input_create(void) {
	Inp* ip = (Inp*)malloc(sizeof(Inp));
	if (!ip) return NULL;
	ip->cp = NULL;
	ip->status = INPUT_OK;
	return ip;
}

//析构函数
//GC
void input_destroy(Inp* ip) {
	if (ip) free(ip);
}

//读取输入流,共享字符串,非必要不读取
//私有辅助函数
static int secure_buf(Inp* ip) {
	if (ip->cp != NULL && *(ip->cp) != '\0') return 1;
	if (!fgets(ip->buf, sizeof(ip->buf), stdin)) {
		ip->cp = NULL;
		ip->status = feof(stdin) ? INPUT_EOF : INPUT_READ_ERROR;
		return 0;
	}
	else {
		if (strchr(ip->buf, '\n') == NULL) {
			int ch;
			while ((ch = getchar()) != '\n' && ch != EOF);
			ip->buf[sizeof(ip->buf) - 1] = '\0';
		}
		else ip->buf[strcspn(ip->buf, "\n")] = '\0';
		ip->cp = ip->buf;
		ip->status = INPUT_OK;
		return 1;
	}
}

//字符串下标移动
//私有辅助函数
static void secure_cp(Inp* ip) {
	while (*(ip->cp) == ' ' || *(ip->cp) == '\t' || *(ip->cp) == '\v'
		|| *(ip->cp) == '\f' || *(ip->cp) == '\n' || *(ip->cp) == '\r') (ip->cp)++;
}

//状态查询函数
//异常处理
static const char* secure_status(Inp* ip) {
	switch (ip->status) {
	case INPUT_OK:
		return "无异常\n";
		break;
	case INPUT_EOF:
		return "输入流结束!\n";
		break;
	case INPUT_READ_ERROR:
		return "标准输入流读取失败!\n";
		break;
	default:
		return "其他异常!\n";
		break;
	}
}

//char型
char input_char(Inp* ip) {
	while (1) {
		if (secure_buf(ip)) {
			secure_cp(ip);
			char ret = 0;
			int loc = 0;
			if (sscanf_s(ip->cp, "%c%n", &ret, 1, &loc) == 1) {
				ip->cp += loc;
				return ret;
			}
			else {
				if (*(ip->cp) != '\0') (ip->cp)++;
			}
		}
		else {
			const char* error = secure_status(ip);
			printf("%s\n", error);
			return 0;
		}
	}
}

//int型
int input_int(Inp* ip) {
	while (1) {
		if (secure_buf(ip)) {
			secure_cp(ip);
			int ret = 0, loc = 0;
			if (sscanf_s(ip->cp, "%i%n", &ret, &loc) == 1) {
				ip->cp += loc;
				return ret;
			}
			else {
				if (*(ip->cp) != '\0') (ip->cp)++;
			}
		}
		else {
			const char* error = secure_status(ip);
			printf("%s\n", error);
			return 0;
		}
	}
}

//float型
float input_float(Inp* ip) {
	while (1){
		if (secure_buf(ip)) {
			secure_cp(ip);
			float ret = 0.0;
			int loc = 0;
			if (sscanf_s(ip->cp, "%f%n", &ret, &loc) == 1) {
				ip->cp += loc;
				return ret;
			}
			else {
				if (*(ip->cp) != '\0') (ip->cp)++;
			}
		}
		else {
			const char* error = secure_status(ip);
			printf("%s\n", error);
			return 0;
		}
	}
}

//double型
double input_double(Inp* ip) {
	while (1) {
		if (secure_buf(ip)) {
			secure_cp(ip);
			double ret = 0.0;
			int loc = 0;
			if (sscanf_s(ip->cp, "%lf%n", &ret, &loc) == 1) {
				ip->cp += loc;
				return ret;
			}
			else {
				if (*(ip->cp) != '\0') (ip->cp)++;
			}
		}
		else {
			const char* error = secure_status(ip);
			printf("%s\n", error);
			return 0;
		}
	}
}

//字符串型
void input_string(Inp* ip, char* arr, rsize_t arr_size) {
	if (!arr_size) return;
	while (1) {
		if (secure_buf(ip)) {
			secure_cp(ip);
			int loc = 0;
			if (sscanf_s(ip->cp, "%s%n", arr, (unsigned int)(arr_size - 1), &loc) == 1) {
				ip->cp += loc;
				return;
			}
			else {
				if (*(ip->cp) != '\0') (ip->cp)++;
				arr[0] = '\0';
				return;
			}
		}
		else {
			const char* error = secure_status(ip);
			printf("%s\n", error);
			arr[0] = '\0';
			return;
		}
	}
}

简单说一下设计思路:

  • 用结构体 Inp 封装上下文:缓冲区、当前指针、状态码都放在一起,类似「对象」的概念——虽然 C 没有 class,但可以用结构体 + 函数指针模拟面向对象。
  • secure_buf 负责按需读取:只有当缓冲区里的内容被消费完才去 fgets,避免频繁读流。
  • secure_cp 负责跳过空白:让 input_intinput_float 等函数能直接解析。
  • secure_status 统一处理异常:把 EOF、读取失败等状态转成清晰的中文提示。
  • 每个 input_xxx 都是一个状态机:读不到就重试,出错就提示,绝不静默崩溃。

老实说,我还想实现 Java 里看到的泛型,让一个函数能处理所有类型,但越改越复杂,最后只能先把异常处理这块做好。这也让我体会到:好的设计不是一步到位的,是在不断取舍中慢慢长出来的。## 4. 我在异常处理中学到的道理

这部分是我最想分享的,也是我踩坑最多的地方。下面是我总结的几个要点:

4.1 异常不是"错误",是程序的正常分支

刚开始写代码时,我总觉得出现异常就是程序写得不好。后来我明白了:异常是程序流程的一部分,关键是如何优雅地处理它。比如用户输入了非数字字符,这不是程序 bug,而是输入流里本来就可能出现的情况——我的 input_int 会跳过非法字符继续解析,而不是直接崩溃。

4.2 给用户友好的提示

程序崩溃时显示一大段堆栈信息,对普通用户很不友好。我在输入库中做了处理:捕获异常后给出清晰的中文提示,让用户知道该怎么重新输入。比如 secure_status 会把 EOF 转成「输入流结束!」,把读取失败转成「标准输入流读取失败!」——用户一看就懂,而不是面对一堆看不懂的报错。

5. 我对内存管理的理解

作为入门者,内存管理听起来很高深,但通过这个小项目,我有了自己的体会:

5.1 及时释放不再使用的资源

在 C 里没有垃圾回收,malloc 出来的内存必须自己 free。我的 input_destroy 就是干这个的——用完 Inp 对象后调用它,把内存还给系统。虽然这个库很小,但「谁分配、谁释放」的意识要从小养成。

5.2 避免不必要的对象创建

在循环中反复创建对象会浪费内存。我的 secure_buf 只在缓冲区被消费完时才去 fgets 读新数据,而不是每次调用都重新分配——复用同一个 buf,减少不必要的开销。

5.3 理解引用与垃圾回收

虽然高级语言有垃圾回收机制,但理解「引用」的概念很重要。在 C 里,指针就是「引用」,free 之后指针就悬空了,必须置 NULL 避免误用。这个库让我对「内存是谁的、什么时候还」有了更具体的认识。

6. 图片内容说明

我在文中提到的异常处理和内存管理的详细笔记,以图片形式附在下方。这些图片记录了我手写的总结,包含更多细节和示例:

异常处理笔记

这张图是我在写逆波兰表达式时和 AI 讨论出的、软工中有关异常处理的细节。我觉得用计科去面对底层,但软工中的业务逻辑和思维也是很不错的补充。

在这里插入图片描述
最近在研究程序中各个对象内存管理的知识,毕竟也是操作系统五大核心功能之一

7. 总结与下一步计划

通过这次手写输入库的经历,我收获了很多:

  • 动手实践是最好的学习方式:光看书不动手,永远学不会。
  • 异常处理是程序健壮性的关键:好的异常处理让程序更友好、更可靠。
  • 内存管理意识要从小养成:虽然高级语言帮我们做了很多,但好习惯很重要。
  • 慢一点没关系,方向对了就行:与其焦虑自己会得少,不如把手里的 C 学透。

下一步,我计划:

  • 为输入库增加更多功能,比如范围校验、正则匹配
  • 继续用 C 深挖底层,比如实现一个简单的链表或栈,巩固数据结构
  • 把这次的经验整理成更系统的学习笔记,为考研复试做准备

8. 一些担忧

最后还是有一些客观上的问题想请教:

  • 展示的思考成果如何?:这个输入库是我从零手写的,代码约 200 行,想请大家客观评价一下,这个水平对学 C 快一年的人来说算不算合格?
  • 数据结构与算法学完后,一边做项目学习操作系统原理怎么样?:我计划学完树形结构后,边做小项目边啃操作系统,不知道这个路线是否合理?
  • 从零基础到今天有十个月了,我这个进度问题有多大?:十个月才学到线性数据结构,和同龄人比是不是太慢了?
  • 有哪位大佬可以提供后续的研究方向甚至工作的建议?:我倾向走底层/系统方向,想听听过来人的建议。

看到这里谢谢各位,只是一个双非二本学生的提问和建议采纳,辛苦!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐