JAVA 异常处理:从原理到实战最佳实践

JAVA 异常处理:从原理到实战最佳实践

JAVA 异常处理:从原理到实战最佳实践

在这里插入图片描述

1.1 本章学习目标与重点

💡 掌握异常的分类与核心概念,理解异常处理的设计思想。
💡 熟练运用 try-catch-finallythrowsthrow 处理异常。
💡 掌握自定义异常的编写与使用场景,规范异常处理流程。
⚠️ 本章重点是 异常处理的最佳实践避免常见误区,这是提升代码健壮性的核心技能。

1.2 异常的核心概念与分类

1.2.1 什么是异常

💡 异常是指程序运行过程中出现的非正常情况,它会中断程序的正常执行流程。
比如文件找不到、数组下标越界、空指针访问等,这些情况都会触发异常。
Java 中所有异常都是 Throwable 类的子类,异常处理的本质是捕获并处理这些非正常情况,保证程序可以继续运行或优雅退出。

1.2.2 异常的分类

Java 中的异常体系分为三大类,它们的父类都是 Throwable

    • 是 JVM 内部的严重错误,属于不可恢复的异常,程序无法处理。
    • 常见的有 OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出)。
    • 示例:递归调用没有终止条件,会导致栈溢出
  1. Checked Exception(受检异常)
    • 也称编译时异常,编译器会强制要求程序处理这类异常。
    • 常见的有 IOException(IO 异常)、SQLException(数据库异常)。
    • 处理方式:要么用 try-catch 捕获,要么用 throws 声明抛出。
  2. Unchecked Exception(非受检异常)
    • 也称运行时异常,继承自 RuntimeException,编译器不强制处理。
    • 常见的有 NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组下标越界)、ClassCastException(类型转换异常)。
    • 这类异常通常是程序逻辑错误导致的,建议通过代码规范来避免,而不是捕获处理。

Error(错误)

publicclassErrorDemo{publicstaticvoidrecursion(){recursion();// 无限递归}publicstaticvoidmain(String[] args){recursion();// 抛出 StackOverflowError}}

✅ 核心结论:异常体系的继承关系为 ThrowableError / ExceptionRuntimeException

1.2.3 常见异常及其产生原因

异常类型产生原因
NullPointerException调用了 null 对象的方法或访问了 null 对象的属性
ArrayIndexOutOfBoundsException数组下标超出了数组的长度范围
ClassCastException强制类型转换失败,比如将 String 转为 Integer
ArithmeticException算术运算错误,比如整数除以 0
IOException文件读写失败,比如文件不存在或权限不足

1.3 异常处理的核心语法

1.3.1 try-catch:捕获并处理异常

💡 try-catch 是最基础的异常处理结构,try 块包裹可能抛出异常的代码,catch 块捕获并处理对应的异常。

基本语法
try{// 可能抛出异常的代码}catch(异常类型1 变量名1){// 处理异常类型1的逻辑}catch(异常类型2 变量名2){// 处理异常类型2的逻辑}
代码实操:捕获单个异常

以空指针异常为例,演示 try-catch 的使用:

publicclassTryCatchSingleDemo{publicstaticvoidmain(String[] args){String str =null;try{// 可能抛出空指针异常的代码System.out.println(str.length());}catch(NullPointerException e){// 处理异常:打印异常信息System.out.println("发生空指针异常:"+ e.getMessage());// 打印异常堆栈信息,便于调试 e.printStackTrace();}// 异常处理后,程序可以继续执行System.out.println("程序继续运行...");}}

输出结果

发生空指针异常:null java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null at TryCatchSingleDemo.main(TryCatchSingleDemo.java:6) 程序继续运行... 
代码实操:捕获多个异常

当一段代码可能抛出多种异常时,可以使用多个 catch 块,注意异常类型要从小到大排列(子类在前,父类在后):

publicclassTryCatchMultiDemo{publicstaticvoidmain(String[] args){int[] arr ={1,2,3};try{// 可能抛出数组下标越界异常System.out.println(arr[5]);// 可能抛出空指针异常String str =null;System.out.println(str.length());}catch(ArrayIndexOutOfBoundsException e){System.out.println("数组下标越界:"+ e.getMessage());}catch(NullPointerException e){System.out.println("空指针异常:"+ e.getMessage());}catch(Exception e){// 父类异常,捕获所有未处理的异常System.out.println("发生未知异常:"+ e.getMessage());}}}

⚠️ 注意事项:如果父类异常写在子类异常前面,会导致子类异常的 catch 块永远无法执行,编译器会报错。

1.3.2 finally:无论是否异常都会执行

💡 finally 块用于执行必须要完成的操作,比如关闭流、释放资源等。无论 try 块是否抛出异常,finally 块都会执行。

基本语法
try{// 可能抛出异常的代码}catch(异常类型 变量名){// 处理异常}finally{// 必须执行的代码}
代码实操:finally 释放资源

以文件读取为例,演示 finally 关闭流资源:

importjava.io.FileInputStream;importjava.io.IOException;publicclassFinallyDemo{publicstaticvoidmain(String[] args){FileInputStream fis =null;try{ fis =newFileInputStream("test.txt");// 读取文件内容int data = fis.read();System.out.println("读取到数据:"+ data);}catch(IOException e){System.out.println("文件读取异常:"+ e.getMessage());}finally{// 无论是否异常,都要关闭流if(fis !=null){try{ fis.close();System.out.println("流资源已关闭");}catch(IOException e){ e.printStackTrace();}}}}}

输出结果(文件不存在时)

文件读取异常:test.txt (系统找不到指定的文件。) 

输出结果(文件存在时)

读取到数据:97 流资源已关闭 

✅ 核心结论:finally 块的唯一例外是 System.exit(0),它会直接终止 JVM,finally 块不会执行。

1.3.3 throws:声明抛出异常

💡 throws 用于在方法声明上声明该方法可能抛出的异常,将异常的处理责任交给调用者。
它适用于不想在当前方法处理异常,或者无法处理异常的场景。

基本语法
public 返回值类型 方法名(参数列表)throws 异常类型1, 异常类型2{// 方法体}
代码实操:throws 声明异常

定义一个读取文件的方法,声明抛出 IOException,由调用者处理异常:

importjava.io.FileInputStream;importjava.io.IOException;publicclassThrowsDemo{// 声明抛出 IOException,由调用者处理publicstaticvoidreadFile()throwsIOException{FileInputStream fis =newFileInputStream("test.txt");int data = fis.read();System.out.println("读取到数据:"+ data); fis.close();}publicstaticvoidmain(String[] args){try{// 调用声明异常的方法,必须处理异常readFile();}catch(IOException e){System.out.println("处理文件读取异常:"+ e.getMessage());}}}

⚠️ 注意事项:

  1. 非受检异常(RuntimeException)可以不用 throws 声明,编译器不会强制要求。
  2. 子类重写父类方法时,抛出的异常不能超过父类方法声明的异常范围(子类异常 ≤ 父类异常)。

1.3.4 throw:主动抛出异常

💡 throw 用于在方法内部主动抛出一个异常对象,通常用于满足特定业务条件时触发异常。

基本语法
thrownew 异常类型("异常描述信息");
代码实操:throw 主动抛异常

模拟用户登录场景,当用户名或密码错误时,主动抛出异常:

publicclassThrowDemo{publicstaticvoidlogin(String username,String password){if(!"admin".equals(username)){// 主动抛出用户名错误异常thrownewRuntimeException("用户名错误");}if(!"123456".equals(password)){// 主动抛出密码错误异常thrownewRuntimeException("密码错误");}System.out.println("登录成功!");}publicstaticvoidmain(String[] args){try{login("admin","123");// 密码错误}catch(RuntimeException e){System.out.println("登录失败:"+ e.getMessage());}}}

输出结果

登录失败:密码错误 

✅ 核心结论:throws方法层面的异常声明,throw代码层面的异常抛出,两者经常配合使用。

1.4 异常处理的进阶特性

1.4.1 try-with-resources:自动释放资源

💡 JDK 7 引入的 try-with-resources 语法,可以自动关闭实现了 AutoCloseable 接口的资源,无需手动在 finally 块中关闭。
常见的资源如 FileInputStreamBufferedReaderConnection 等都实现了该接口。

基本语法
try(资源声明语句){// 使用资源的代码}catch(异常类型 变量名){// 处理异常}
代码实操:自动关闭文件流

对比传统 try-catch-finallytry-with-resources 的写法:

importjava.io.FileInputStream;importjava.io.IOException;publicclassTryWithResourcesDemo{publicstaticvoidmain(String[] args){// 传统写法:手动关闭资源FileInputStream fis =null;try{ fis =newFileInputStream("test.txt");System.out.println("读取数据:"+ fis.read());}catch(IOException e){ e.printStackTrace();}finally{if(fis !=null){try{ fis.close();}catch(IOException e){ e.printStackTrace();}}}// try-with-resources 写法:自动关闭资源try(FileInputStream fis2 =newFileInputStream("test.txt")){System.out.println("读取数据:"+ fis2.read());}catch(IOException e){ e.printStackTrace();}}}

✅ 核心结论:try-with-resources 语法更简洁,代码可读性更高,是推荐的资源释放方式

1.4.2 异常链:传递异常信息

💡 异常链是指在捕获一个异常后,抛出另一个异常并将原异常作为原因传递,这样可以保留异常的完整调用链,便于调试。
使用 initCause() 方法或异常构造方法传递原异常。

代码实操:异常链的使用

模拟业务层调用数据访问层,捕获数据访问层异常并包装为业务异常:

publicclassExceptionChainDemo{// 数据访问层方法publicstaticvoidqueryData()throwsSQLException{thrownewSQLException("数据库连接失败");}// 业务层方法publicstaticvoidprocessBusiness(){try{queryData();}catch(SQLException e){// 包装为业务异常,传递原异常信息RuntimeException businessException =newRuntimeException("业务处理失败"); businessException.initCause(e);throw businessException;}}publicstaticvoidmain(String[] args){try{processBusiness();}catch(RuntimeException e){System.out.println("异常信息:"+ e.getMessage());System.out.println("根因异常:"+ e.getCause().getMessage());// 打印完整异常堆栈 e.printStackTrace();}}}

输出结果

异常信息:业务处理失败 根因异常:数据库连接失败 java.lang.RuntimeException: 业务处理失败 at ExceptionChainDemo.processBusiness(ExceptionChainDemo.java:12) at ExceptionChainDemo.main(ExceptionChainDemo.java:19) Caused by: java.sql.SQLException: 数据库连接失败 at ExceptionChainDemo.queryData(ExceptionChainDemo.java:4) at ExceptionChainDemo.processBusiness(ExceptionChainDemo.java:9) ... 1 more 

1.5 自定义异常

1.5.1 为什么需要自定义异常

💡 Java 提供的内置异常无法满足所有业务场景的需求,比如用户注册时的“用户名已存在”、“密码长度不足”等异常,需要自定义异常来描述。
自定义异常的优势:

  1. 异常信息更贴合业务,便于开发者定位问题。
  2. 可以区分系统异常业务异常,便于统一处理。

1.5.2 自定义异常的编写步骤

  1. 定义一个类,继承 Exception(受检异常)或 RuntimeException(非受检异常)。
  2. 提供无参构造方法和带异常信息的构造方法。
  3. (可选)提供带异常信息和根因异常的构造方法。

1.5.3 代码实操1:自定义受检异常

定义一个 UsernameExistException,表示用户名已存在异常:

// 自定义受检异常,继承 ExceptionpublicclassUsernameExistExceptionextendsException{// 无参构造publicUsernameExistException(){super();}// 带异常信息的构造publicUsernameExistException(String message){super(message);}// 带异常信息和根因的构造publicUsernameExistException(String message,Throwable cause){super(message, cause);}}

1.5.4 代码实操2:自定义非受检异常

定义一个 PasswordLengthException,表示密码长度不足异常:

// 自定义非受检异常,继承 RuntimeExceptionpublicclassPasswordLengthExceptionextendsRuntimeException{publicPasswordLengthException(){super();}publicPasswordLengthException(String message){super(message);}publicPasswordLengthException(String message,Throwable cause){super(message, cause);}}

1.5.5 自定义异常的使用场景

模拟用户注册功能,使用自定义异常处理业务逻辑:

importjava.util.HashSet;importjava.util.Set;publicclassCustomExceptionDemo{// 模拟已存在的用户名privatestaticfinalSet<String>EXIST_USERNAMES=newHashSet<>();static{EXIST_USERNAMES.add("admin");EXIST_USERNAMES.add("user1");}// 注册方法publicstaticvoidregister(String username,String password)throwsUsernameExistException{// 检查用户名是否存在if(EXIST_USERNAMES.contains(username)){thrownewUsernameExistException("用户名["+ username +"]已存在");}// 检查密码长度if(password.length()<6){thrownewPasswordLengthException("密码长度不能小于6位");}// 注册成功EXIST_USERNAMES.add(username);System.out.println("用户["+ username +"]注册成功!");}publicstaticvoidmain(String[] args){try{register("admin","123456");// 用户名已存在}catch(UsernameExistException e){System.out.println("注册失败:"+ e.getMessage());}catch(PasswordLengthException e){System.out.println("注册失败:"+ e.getMessage());}try{register("user2","123");// 密码长度不足}catch(UsernameExistException e){System.out.println("注册失败:"+ e.getMessage());}catch(PasswordLengthException e){System.out.println("注册失败:"+ e.getMessage());}}}

输出结果

注册失败:用户名[admin]已存在 注册失败:密码长度不能小于6位 

⚠️ 注意事项:自定义异常尽量继承 RuntimeException,避免强制调用者处理,减少代码冗余。

1.6 异常处理的最佳实践

1.6.1 核心原则

  1. 只捕获能处理的异常:对于无法处理的异常,应该向上抛出,让上层调用者处理。
  2. 捕获具体异常,而非笼统的 Exception:避免捕获所有异常,导致无法定位具体问题。
  3. 不要忽略异常:禁止空的 catch 块,至少要打印异常信息。
  4. 释放资源:使用 try-with-resourcesfinally 块确保资源被正确释放。
  5. 提供有意义的异常信息:异常信息要明确,便于开发者定位问题,比如“用户名[admin]已存在”。

1.6.2 常见误区

❌ 误区1:空 catch 块,忽略异常

try{// 可能抛出异常的代码}catch(NullPointerException e){// 空的catch块,异常被忽略,无法定位问题}

✅ 正确做法:打印异常信息

try{// 可能抛出异常的代码}catch(NullPointerException e){ e.printStackTrace();}

❌ 误区2:捕获 Exception,处理所有异常

try{// 可能抛出多种异常的代码}catch(Exception e){System.out.println("发生异常");}

✅ 正确做法:捕获具体异常

try{// 可能抛出多种异常的代码}catch(NullPointerException e){System.out.println("空指针异常");}catch(ArrayIndexOutOfBoundsException e){System.out.println("数组下标越界");}

❌ 误区3:过度使用异常,替代正常逻辑判断

// 不好的写法:用异常处理替代空指针判断publicstaticintgetLength(String str){try{return str.length();}catch(NullPointerException e){return0;}}

✅ 正确做法:提前进行逻辑判断

publicstaticintgetLength(String str){if(str ==null){return0;}return str.length();}

1.7 实战案例:统一异常处理框架

1.7.1 需求分析

💡 在实际项目中,需要一个统一的异常处理框架,将系统异常和业务异常进行统一封装,返回友好的错误信息。
需求如下:

  1. 定义全局异常枚举,包含错误码和错误信息。
  2. 定义统一的返回结果类,封装接口返回数据。
  3. 定义全局异常处理器,捕获所有异常并统一处理。

1.7.2 代码实现

步骤1:定义异常枚举
/** * 全局异常枚举 */publicenumErrorCode{SUCCESS(200,"操作成功"),USERNAME_EXIST(1001,"用户名已存在"),PASSWORD_LENGTH_ERROR(1002,"密码长度不能小于6位"),SYSTEM_ERROR(500,"系统内部异常");privatefinalint code;privatefinalString message;ErrorCode(int code,String message){this.code = code;this.message = message;}publicintgetCode(){return code;}publicStringgetMessage(){return message;}}
步骤2:定义统一返回结果类
/** * 统一返回结果类 */publicclassResult<T>{// 错误码privateint code;// 错误信息privateString message;// 数据privateT data;// 私有构造privateResult(){}// 成功响应publicstatic<T>Result<T>success(T data){Result<T> result =newResult<>(); result.code =ErrorCode.SUCCESS.getCode(); result.message =ErrorCode.SUCCESS.getMessage(); result.data = data;return result;}// 失败响应publicstatic<T>Result<T>fail(ErrorCode errorCode){Result<T> result =newResult<>(); result.code = errorCode.getCode(); result.message = errorCode.getMessage();return result;}// 失败响应,自定义错误信息publicstatic<T>Result<T>fail(int code,String message){Result<T> result =newResult<>(); result.code = code; result.message = message;return result;}// getter和setterpublicintgetCode(){return code;}publicvoidsetCode(int code){this.code = code;}publicStringgetMessage(){return message;}publicvoidsetMessage(String message){this.message = message;}publicTgetData(){return data;}publicvoidsetData(T data){this.data = data;}@OverridepublicStringtoString(){return"Result{"+"code="+ code +",+ message +'\''+", data="+ data +'}';}}
步骤3:定义全局业务异常
/** * 全局业务异常 */publicclassBusinessExceptionextendsRuntimeException{privatefinalint code;publicBusinessException(ErrorCode errorCode){super(errorCode.getMessage());this.code = errorCode.getCode();}publicBusinessException(int code,String message){super(message);this.code = code;}publicintgetCode(){return code;}}
步骤4:定义全局异常处理器
/** * 全局异常处理器 */publicclassGlobalExceptionHandler{/** * 处理业务异常 */publicstaticResult<?>handleBusinessException(BusinessException e){returnResult.fail(e.getCode(), e.getMessage());}/** * 处理系统异常 */publicstaticResult<?>handleSystemException(Exception e){// 打印异常堆栈,便于调试 e.printStackTrace();returnResult.fail(ErrorCode.SYSTEM_ERROR);}/** * 统一异常处理入口 */publicstaticResult<?>handleException(Throwable e){if(e instanceofBusinessException){returnhandleBusinessException((BusinessException) e);}else{returnhandleSystemException((Exception) e);}}}
步骤5:测试统一异常处理框架
publicclassGlobalExceptionDemo{publicstaticvoidregister(String username,String password){if("admin".equals(username)){// 抛出自定义业务异常thrownewBusinessException(ErrorCode.USERNAME_EXIST);}if(password.length()<6){thrownewBusinessException(ErrorCode.PASSWORD_LENGTH_ERROR);}}publicstaticvoidmain(String[] args){// 测试用户名已存在异常try{register("admin","123456");}catch(Throwable e){Result<?> result =GlobalExceptionHandler.handleException(e);System.out.println(result);}// 测试密码长度不足异常try{register("user2","123");}catch(Throwable e){Result<?> result =GlobalExceptionHandler.handleException(e);System.out.println(result);}// 测试系统异常try{String str =null;System.out.println(str.length());}catch(Throwable e){Result<?> result =GlobalExceptionHandler.handleException(e);System.out.println(result);}}}

输出结果

Result{code=1001, message='用户名已存在', data=null} Result{code=1002, message='密码长度不能小于6位', data=null} java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null at GlobalExceptionDemo.main(GlobalExceptionDemo.java:28) Result{code=500, message='系统内部异常', data=null} 

1.7.3 案例总结

✅ 这个统一异常处理框架是实际项目中的标准做法,它的优势在于:

  1. 统一了异常处理逻辑,减少重复代码。
  2. 区分了业务异常和系统异常,便于问题定位。
  3. 返回友好的错误信息,提升用户体验。

1.8 本章总结

  1. 异常分为 ErrorChecked ExceptionUnchecked Exception,其中 Error 无法处理,Unchecked Exception 建议通过代码规范避免。
  2. 异常处理的核心语法包括 try-catch-finallythrowsthrowtry-with-resources 是推荐的资源释放方式。
  3. 自定义异常要贴合业务场景,优先继承 RuntimeException
  4. 异常处理的最佳实践是:捕获具体异常、不忽略异常、释放资源、提供有意义的异常信息。
  5. 实际项目中要搭建统一异常处理框架,实现异常的统一封装和处理。

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk