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应用程序的基石。

相关推荐
Elastic 中国社区官方博客1 小时前
从向量到关键词:在 LangChain 中的 Elasticsearch 混合搜索
大数据·开发语言·数据库·elasticsearch·搜索引擎·ai·langchain
梵刹古音2 小时前
【C++】多态
开发语言·c++
hello 早上好2 小时前
07_JVM 双亲委派机制
开发语言·jvm
前端程序猿i2 小时前
第 8 篇:Markdown 渲染引擎 —— 从流式解析到安全输出
开发语言·前端·javascript·vue.js·安全
kronos.荒2 小时前
滑动窗口:寻找字符串中的字母异位词
开发语言·python
好好学习天天向上~~2 小时前
8_Linux学习总结_进程
linux·运维·学习
_codemonster2 小时前
java web修改了文件和新建了文件需要注意的问题
java·开发语言·前端
知识分享小能手2 小时前
SQL Server 2019入门学习教程,从入门到精通,Transact-SQL数据的更新 —语法详解与实战案例(SQL Server 2019)(10)
数据库·学习·sqlserver
甄心爱学习2 小时前
【python】list的底层实现
开发语言·python