C语言GetLastError函数

Windows GetLastError函数全面解析:从错误码范围到实战应用

一、GetLastError函数基础

GetLastError是Windows API中至关重要的错误处理函数,它返回线程最后一次执行的错误代码。与许多函数不同,它不直接提供错误描述,而是返回一个DWORD类型的数值错误码。理解这些错误码的范围分类比死记硬背具体值更为实用。

1.1 函数原型与基本用法

复制代码
#include <windows.h>

DWORD GetLastError(VOID);

基本调用方法非常简单:

复制代码
// 示例:检查文件操作错误
HANDLE hFile = CreateFile("example.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
    DWORD dwError = GetLastError();
    printf("文件打开失败,错误代码: %lu\n", dwError);
}

1.2 错误码的基本特性

Windows错误码是32位无符号整数,具有以下特征:

  • 高位:表示错误严重性(成功/信息/警告/错误)

  • 第29位:客户代码标志(Microsoft/第三方)

  • 第28位:保留位

  • 低16位:具体错误代码

二、错误码范围分类详解

Windows错误码采用分层结构设计,不同范围的错误码对应不同的系统模块和错误类型。以下是完整的错误码范围分类:

2.1 系统基础错误码 (0-999)

这是最常见的错误码范围,涵盖了Windows操作系统的基本功能:

错误码范围 分类描述 常见错误示例
0-99 基本操作错误 0: 成功,1: 功能错误
100-199 输入输出错误 100: 无法创建系统信号灯
200-299 进程线程错误 231: 所有管道实例忙
300-399 网络基础错误 300: 操作锁定请求被拒绝
400-499 注册表错误 400: 线程已处于后台处理模式
500-599 设备I/O错误 534: 算术结果超过32位
600-699 安全认证错误 600: 令牌已存在且无法替代
700-799 数据库错误 701: 指定的缓冲区无效
800-899 系统服务错误 800: 指定的设备已存在
900-999 RPC基础错误 995: 已放弃I/O操作

典型错误详解

  • ERROR_ACCESS_DENIED (5): 拒绝访问,通常因权限不足或文件被锁定

  • ERROR_INVALID_HANDLE (6): 无效句柄,通常因已关闭的句柄被再次使用

  • ERROR_NOT_ENOUGH_MEMORY (8): 内存不足,系统无法分配请求的存储空间

2.2 网络与通信错误码 (1000-1999)

此范围主要涵盖网络相关操作错误:

复制代码
// 网络错误处理示例
BOOL networkResult = ConnectToServer("192.168.1.1");
if (!networkResult) {
    DWORD err = GetLastError();
    if (err >= 1000 && err <= 1999) {
        HandleNetworkError(err);
    }
}

主要网络错误分类:

  • 1000-1099: Windows Socket基础错误

  • 1100-1199: RPC(远程过程调用)错误

  • 1200-1299: DHCP客户端错误

  • 1300-1399: 安全认证错误

  • 1400-1499: 分布式文件系统错误

  • 1500-1599: 浏览器服务错误

  • 1600-1699: 集群服务错误

  • 1700-1799: 事务处理错误

  • 1800-1899: 日志服务错误

  • 1900-1999: 目录服务错误

2.3 窗口与图形界面错误码 (14000-15999)

图形用户界面相关的错误码:

错误码范围 子系统 典型错误
14000-14499 窗口管理 14000: 窗口句柄无效
14500-14999 用户界面 14501: 菜单项未找到
15000-15499 图形设备 15001: 设备上下文无效
15500-15999 主题服务 15501: 主题文件损坏

2.4 Active Directory与目录服务 (8000-8999)

Active Directory相关操作错误:

  • 8000-8099: LDAP操作错误

  • 8100-8199: 目录复制错误

  • 8200-8299: 目录服务核心错误

  • 8300-8399: 目录服务SAM错误

  • 8400-8499: 目录服务数据库错误

2.5 HTTP与Internet服务错误 (12000-12999)

HTTP服务相关错误码范围:

  • 12000-12099: HTTP服务基础错误

  • 12100-12199: HTTP过滤器错误

  • 12200-12299: HTTP SSL错误

  • 12300-12399: HTTP日志错误

  • 12400-12499: HTTP状态错误

2.6 Windows更新相关错误 (0x80070000-0x8007FFFF)

Windows Update服务特有错误范围,通常以HRESULT形式返回:

复制代码
// Windows Update错误处理示例
HRESULT hr = InstallWindowsUpdate();
if (FAILED(hr)) {
    if ((hr & 0xFFF00000) == 0x80070000) {
        DWORD win32Error = hr & 0x0000FFFF;
        HandleUpdateError(win32Error);
    }
}

三、动态获取错误码信息的方法

在实际开发中,不建议硬编码错误码值,而应通过系统API动态获取错误描述。

3.1 使用FormatMessage获取错误描述

复制代码
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void PrintLastError() {
    DWORD errorCode = GetLastError();
    
    if (errorCode == 0) {
        _tprintf(_T("操作成功完成,无错误发生。\n"));
        return;
    }
    
    LPTSTR errorMsg = NULL;
    DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                  FORMAT_MESSAGE_FROM_SYSTEM | 
                  FORMAT_MESSAGE_IGNORE_INSERTS;
    
    DWORD msgLength = FormatMessage(
        flags,
        NULL,
        errorCode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&errorMsg,
        0,
        NULL
    );
    
    if (msgLength > 0) {
        _tprintf(_T("错误代码: %lu (0x%08lX)\n"), errorCode, errorCode);
        _tprintf(_T("错误描述: %s"), errorMsg);
        
        // 清理FormatMessage分配的内存
        LocalFree(errorMsg);
    } else {
        _tprintf(_T("无法获取错误代码 %lu 的描述。\n"), errorCode);
    }
}

3.2 增强版错误信息获取函数

复制代码
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

// 获取完整的错误信息,包括可能的模块信息
void GetCompleteErrorInfo(DWORD errorCode, LPTSTR moduleName) {
    _tprintf(_T("=== 错误分析报告 ===\n"));
    _tprintf(_T("错误代码: %lu (0x%08lX)\n"), errorCode, errorCode);
    
    // 获取系统错误描述
    LPTSTR sysErrorMsg = NULL;
    DWORD sysMsgLength = FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&sysErrorMsg, 0, NULL);
    
    if (sysMsgLength > 0) {
        _tprintf(_T("系统描述: %s"), sysErrorMsg);
        LocalFree(sysErrorMsg);
    }
    
    // 如果提供了模块名,尝试从模块获取错误描述
    if (moduleName != NULL) {
        HMODULE hModule = GetModuleHandle(moduleName);
        if (hModule != NULL) {
            LPTSTR moduleErrorMsg = NULL;
            DWORD moduleMsgLength = FormatMessage(
                FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE,
                hModule, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                (LPTSTR)&moduleErrorMsg, 0, NULL);
            
            if (moduleMsgLength > 0) {
                _tprintf(_T("模块描述: %s"), moduleErrorMsg);
                LocalFree(moduleErrorMsg);
            }
        }
    }
    
    // 错误严重性分析
    _tprintf(_T("错误严重性: "));
    if (errorCode == 0) {
        _tprintf(_T("成功\n"));
    } else if (errorCode & 0x20000000) {
        _tprintf(_T("警告\n"));
    } else {
        _tprintf(_T("错误\n"));
    }
    
    _tprintf(_T("客户代码: %s\n"), 
        (errorCode & 0x20000000) ? _T("第三方") : _T("Microsoft"));
}

3.3 批量获取系统所有错误码信息

如果需要获取系统中定义的所有错误码,可以通过遍历错误码范围:

复制代码
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void DumpAllErrorCodes(DWORD startRange, DWORD endRange, const TCHAR* filename) {
    FILE* logFile = _tfopen(filename, _T("w"));
    if (!logFile) {
        _tprintf(_T("无法创建日志文件: %s\n"), filename);
        return;
    }
    
    _ftprintf(logFile, _T("错误码范围: %lu - %lu\n\n"), startRange, endRange);
    
    for (DWORD code = startRange; code <= endRange; code++) {
        LPTSTR errorMsg = NULL;
        DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                      FORMAT_MESSAGE_FROM_SYSTEM | 
                      FORMAT_MESSAGE_IGNORE_INSERTS;
        
        DWORD msgLength = FormatMessage(
            flags,
            NULL,
            code,
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&errorMsg,
            0,
            NULL
        );
        
        if (msgLength > 0) {
            _ftprintf(logFile, _T("0x%08lX (%lu): %s"), code, code, errorMsg);
            LocalFree(errorMsg);
        }
        
        // 每1000个错误码输出进度
        if (code % 1000 == 0) {
            _tprintf(_T("已处理错误码至: %lu\n"), code);
        }
        
        // 避免过于频繁的输出
        if (code % 100 == 0) {
            Sleep(10);  // 小延迟避免过度占用CPU
        }
    }
    
    fclose(logFile);
    _tprintf(_T("错误码已导出至: %s\n"), filename);
}

// 使用示例
void ExampleUsage() {
    // 导出系统基础错误码
    DumpAllErrorCodes(0, 999, _T("system_errors.txt"));
    
    // 导出网络错误码
    DumpAllErrorCodes(1000, 1999, _T("network_errors.txt"));
}

3.4 错误码查询工具的使用

除了编程方式,Windows还提供了命令行工具来查看错误码:

3.4.1 使用net.exe命令
复制代码
@echo off
REM 查看特定错误码
net helpmsg 5
REM 输出: 拒绝访问。

REM 批处理获取一系列错误码
for /l %%i in (1,1,100) do @net helpmsg %%i
3.4.2 使用PowerShell查询错误码
复制代码
# PowerShell错误码查询函数
function Get-ErrorDescription {
    param([int]$ErrorCode)
    
    $ErrorText = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
    return "错误代码 $ErrorCode : $($ErrorText.Message)"
}

# 使用示例
Get-ErrorDescription -ErrorCode 5
Get-ErrorDescription -ErrorCode 2

# 批量查询
1..10 | ForEach-Object { Get-ErrorDescription -ErrorCode $_ }
3.4.3 使用Visual Studio错误查找工具

Visual Studio自带的错误查找工具(ErrLook.exe)可以快速查询错误码含义。

四、错误处理最佳实践与框架

4.1 立即保存错误码模式

由于GetLastError返回的是线程的最后错误代码,在调用其他API前必须立即保存:

复制代码
#include <windows.h>
#include <stdio.h>

// 错误的安全处理方式
void UnsafeErrorHandling() {
    HANDLE hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        // 错误:在调用其他函数后再获取错误码
        printf("创建文件失败\n");
        DWORD err = GetLastError();  // 可能已经被printf修改
        // ... 
    }
}

// 正确的错误处理方式
void SafeErrorHandling() {
    HANDLE hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        DWORD err = GetLastError();  // 立即保存错误码
        
        // 使用保存的错误码
        printf("创建文件失败,错误代码: %lu\n", err);
        HandleFileError(err);
    }
}

4.2 错误处理封装宏

创建一套完整的错误处理宏可以显著提高代码质量:

复制代码
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

// 基础错误检查宏
#define CHECK_API_RESULT(api_call) \
    do { \
        if (!(api_call)) { \
            DWORD __err = GetLastError(); \
            LogError(__FILE__, __LINE__, __err, _T(#api_call)); \
            return FALSE; \
        } \
    } while(0)

// 带错误处理的API调用宏
#define CALL_WITH_ERROR_HANDLE(api_call, error_handler) \
    do { \
        BOOL __result = (api_call); \
        if (!__result) { \
            DWORD __err = GetLastError(); \
            error_handler(__err, _T(#api_call)); \
        } \
    } while(0)

// 错误日志记录函数
void LogError(const char* file, int line, DWORD errorCode, const TCHAR* apiName) {
    LPTSTR errorMsg = NULL;
    
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&errorMsg, 0, NULL);
    
    _tprintf(_T("[ERROR] %s:%d - %s 失败 (代码: %lu)\n"), 
             file, line, apiName, errorCode);
    _tprintf(_T("        %s\n"), errorMsg ? errorMsg : _T("无法获取错误描述"));
    
    if (errorMsg) LocalFree(errorMsg);
}

4.3 高级错误处理框架

对于大型项目,建议实现完整的错误处理框架:

复制代码
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <stdarg.h>

typedef enum {
    ERROR_SEVERITY_INFO,
    ERROR_SEVERITY_WARNING,
    ERROR_SEVERITY_ERROR,
    ERROR_SEVERITY_CRITICAL
} ERROR_SEVERITY;

typedef struct {
    DWORD errorCode;
    ERROR_SEVERITY severity;
    TCHAR moduleName[64];
    TCHAR functionName[128];
    SYSTEMTIME timestamp;
    DWORD threadId;
    TCHAR description[512];
} ERROR_INFO;

class ErrorHandler {
private:
    static CRITICAL_SECTION cs;
    static HANDLE hLogFile;
    
public:
    static BOOL Initialize(const TCHAR* logFilePath) {
        InitializeCriticalSection(&cs);
        
        hLogFile = CreateFile(logFilePath, GENERIC_WRITE, FILE_SHARE_READ, 
                             NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hLogFile == INVALID_HANDLE_VALUE) return FALSE;
        
        SetFilePointer(hLogFile, 0, NULL, FILE_END);
        return TRUE;
    }
    
    static void LogError(ERROR_SEVERITY severity, const TCHAR* module, 
                        const TCHAR* function, DWORD errorCode, const TCHAR* format, ...) {
        EnterCriticalSection(&cs);
        
        ERROR_INFO errorInfo = {0};
        errorInfo.errorCode = errorCode;
        errorInfo.severity = severity;
        _tcscpy_s(errorInfo.moduleName, module);
        _tcscpy_s(errorInfo.functionName, function);
        errorInfo.threadId = GetCurrentThreadId();
        GetLocalTime(&errorInfo.timestamp);
        
        // 格式化描述信息
        va_list args;
        va_start(args, format);
        _vstprintf_s(errorInfo.description, format, args);
        va_end(args);
        
        // 写入日志文件
        if (hLogFile != INVALID_HANDLE_VALUE) {
            DWORD bytesWritten;
            WriteFile(hLogFile, &errorInfo, sizeof(ERROR_INFO), &bytesWritten, NULL);
        }
        
        // 控制台输出
        OutputErrorToConsole(&errorInfo);
        
        LeaveCriticalSection(&cs);
    }
    
    static void Cleanup() {
        if (hLogFile != INVALID_HANDLE_VALUE) {
            CloseHandle(hLogFile);
        }
        DeleteCriticalSection(&cs);
    }
    
private:
    static void OutputErrorToConsole(const ERROR_INFO* errorInfo) {
        const TCHAR* severityText[] = {_T("信息"), _T("警告"), _T("错误"), _T("严重")};
        
        _tprintf(_T("[%s] %04d-%02d-%02d %02d:%02d:%02d [线程: %lu]\n"),
                severityText[errorInfo->severity],
                errorInfo->timestamp.wYear, errorInfo->timestamp.wMonth, errorInfo->timestamp.wDay,
                errorInfo->timestamp.wHour, errorInfo->timestamp.wMinute, errorInfo->timestamp.wSecond,
                errorInfo->threadId);
        
        _tprintf(_T("  模块: %s, 函数: %s\n"), errorInfo->moduleName, errorInfo->functionName);
        _tprintf(_T("  错误代码: %lu (0x%08lX)\n"), errorInfo->errorCode, errorInfo->errorCode);
        
        LPTSTR systemDescription = NULL;
        if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                         NULL, errorInfo->errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                         (LPTSTR)&systemDescription, 0, NULL)) {
            _tprintf(_T("  系统描述: %s"), systemDescription);
            LocalFree(systemDescription);
        }
        
        _tprintf(_T("  描述: %s\n\n"), errorInfo->description);
    }
};

// 静态成员初始化
CRITICAL_SECTION ErrorHandler::cs;
HANDLE ErrorHandler::hLogFile = INVALID_HANDLE_VALUE;

// 使用示例
void ExampleUsage() {
    ErrorHandler::Initialize(_T("app_errors.log"));
    
    HANDLE hFile = CreateFile(_T("example.txt"), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        DWORD err = GetLastError();
        ErrorHandler::LogError(ERROR_SEVERITY_ERROR, _T("FileModule"), _T("CreateFile"), 
                              err, _T("无法打开文件 example.txt"));
    }
    
    ErrorHandler::Cleanup();
}

五、调试技巧与高级主题

5.1 Visual Studio调试技巧

在Visual Studio调试时,可以使用特殊变量查看错误信息:

  1. 查看最近错误 :在Watch窗口添加@err,hr

  2. 查看TEB信息@err显示当前线程的最后错误码

  3. 使用内存窗口 :查看GetLastError返回的内存位置

5.2 错误码与HRESULT的转换

Windows中除了GetLastError返回的Win32错误码,还有COM使用的HRESULT:

复制代码
#include <windows.h>
#include <winerror.h>

// HRESULT转换为Win32错误码
DWORD HResultToWin32(HRESULT hr) {
    if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
        return HRESULT_CODE(hr);
    }
    return hr; // 如果不是Win32错误,直接返回
}

// Win32错误码转换为HRESULT
HRESULT Win32ToHResult(DWORD errorCode) {
    return HRESULT_FROM_WIN32(errorCode);
}

// 判断错误严重性
BOOL IsCriticalError(DWORD errorCode) {
    // 严重错误通常需要立即处理
    DWORD criticalErrors[] = {ERROR_OUTOFMEMORY, ERROR_HANDLE_DISK_FULL, 
                             ERROR_NO_SYSTEM_RESOURCES, ERROR_TOO_MANY_OPEN_FILES};
    
    for (int i = 0; i < sizeof(criticalErrors)/sizeof(criticalErrors[0]); i++) {
        if (errorCode == criticalErrors[i]) return TRUE;
    }
    return FALSE;
}

5.3 自定义错误码的最佳实践

在开发自己的API时,可以定义自定义错误码:

复制代码
#include <windows.h>

// 自定义错误码范围 (0x2000-0x2FFF)
#define MYAPP_ERROR_BASE 0x2000

enum {
    MYAPP_ERROR_FILE_CORRUPT = MYAPP_ERROR_BASE + 1,
    MYAPP_ERROR_CONFIG_INVALID,
    MYAPP_ERROR_DATABASE_CONNECTION,
    MYAPP_ERROR_LICENSE_EXPIRED,
    // ... 更多自定义错误
};

// 注册自定义错误码描述
BOOL RegisterCustomErrorMessages() {
    // 在资源文件中定义错误描述
    // 使用FormatMessage的FORMAT_MESSAGE_FROM_HMODULE标志
    return TRUE;
}

// 设置自定义错误码
void SetCustomError(DWORD customErrorCode) {
    // 确保自定义错误码在正确范围内
    if (customErrorCode >= 0x2000 && customErrorCode <= 0x2FFF) {
        SetLastError(customErrorCode);
    }
}

六、总结与实用速查表

6.1 错误处理核心原则

  1. 立即性原则:调用API后立即保存错误码

  2. 完整性原则:记录完整的错误上下文信息

  3. 适当性原则:根据错误严重性采取适当措施

  4. 可恢复性原则:设计可恢复的错误处理机制

6.2 错误码范围速查表

错误码范围 主要用途 相关模块
0x00000000-0x000003FF 系统基础错误 Kernel32, User32
0x00000400-0x000007FF 网络基础错误 Winsock
0x00001000-0x00001FFF RPC相关错误 RPC运行时
0x00002000-0x00002FFF 安全相关错误 安全子系统
0x00003000-0x00003FFF 注册表错误 配置管理器
0x00004000-0x00004FFF 设备I/O错误 设备管理器
0x00005000-0x00005FFF 服务控制错误 服务控制管理器
0x00006000-0x00006FFF 打印相关错误 打印后台处理程序
0x00008000-0x00008FFF Active Directory LDAP服务
0x0000A000-0x0000AFFF HTTP服务错误 HTTP.sys
0x0000B000-0x0000BFFF 证书服务错误 加密API
0x0000C000-0x0000CFFF 终端服务错误 终端服务
0x0000D000-0x0000DFFF 复制服务错误 文件复制服务
0x0000E000-0x0000EFFF 目录服务错误 Active Directory
0x0000F000-0x0000FFFF 图形设备错误 GDI, DirectX
0x80070000-0x8007FFFF Windows Update WU组件

6.3 常用错误码快速参考

复制代码
// 常见错误码处理示例
void HandleCommonErrors(DWORD errorCode) {
    switch (errorCode) {
        case ERROR_SUCCESS:
            // 操作成功
            break;
        case ERROR_FILE_NOT_FOUND:
            // 文件未找到
            break;
        case ERROR_ACCESS_DENIED:
            // 权限不足
            break;
        case ERROR_OUTOFMEMORY:
            // 内存不足 - 需要紧急处理
            break;
        case ERROR_DISK_FULL:
            // 磁盘空间不足
            break;
        case ERROR_NETWORK_UNREACHABLE:
            // 网络不可达
            break;
        default:
            // 其他错误
            break;
    }
}

通过本文的全面介绍,您应该已经掌握了GetLastError函数的深入知识,从基础用法到高级错误处理框架,从错误码分类到实战调试技巧。在实际开发中,良好的错误处理是构建稳定、可靠Windows应用程序的基石。

相关推荐
Hua-Jay几秒前
OpenCV联合C++/Qt 学习笔记(十七)----凸包检测、直线检测及点集拟合
c++·笔记·qt·opencv·学习·计算机视觉
basketball6164 分钟前
C++ Lambda 表达式完全指南
开发语言·c++·算法
不知名的老吴6 分钟前
C++中emplace函数的不适场景总结(三)
开发语言·c++·算法
Java面试题总结16 分钟前
Go 里什么时候可以“panic”?
开发语言·后端·golang
rit843249917 分钟前
基于MATLAB平台的指纹识别系统实现
开发语言·matlab
南境十里·墨染春水20 分钟前
linux学习进展 网络编程——HTTPS (补充)
linux·网络·学习
沐知全栈开发27 分钟前
TypeScript String
开发语言
ch.ju1 小时前
Java程序设计(第3版)第三章——数组的动态获取
java·开发语言
吃好睡好便好1 小时前
说说损伤膝盖的行为和保护膝盖的方法
学习
曹牧1 小时前
Java:PDF文件扁平化处理
java·开发语言·pdf