11、环境变量与系统信息:getenv()、uname()、GetSystemInfo() 的跨平台封装

环境变量和系统信息,说白了就是程序跟操作系统打交道的两个基本入口。你想想看,一个程序跑起来,总得知道自己活在什么系统里吧?总得知道用户配了哪些环境变量吧?

我在做跨平台项目时,最头疼的就是这些看似简单的 API。Windows 一套,Linux 一套,macOS 又跟 Linux 差不多但细节上总给你挖坑。嗯,今天我们就来聊聊怎么把这些东西封装好,让你写一次代码,到处编译。

11.1 环境变量读取:getenv() 与 GetEnvironmentVariable()

环境变量这东西,说白了就是操作系统给进程的一张「便签纸」。程序启动时,系统会把当前环境变量复制一份给进程,之后进程可以读,也可以改(但只影响自己)。

标准 C 库提供了 getenv(),Windows 提供了 GetEnvironmentVariable()。两者行为有细微差别,我踩过坑,下面细说。

11.1.1 标准 C 的 getenv()

#include <stdlib.h>
char *getenv(const char *name);

这个函数很简单,传入变量名,返回字符串指针。如果没找到,返回 NULL。但有个坑——返回的指针指向的是系统内部缓冲区,你千万别去 free 它,也别修改它指向的内容。

注意: getenv() 返回的字符串在有些实现中是静态分配的,多线程环境下可能被覆盖。我个人习惯是拿到后立刻 strdup() 一份。

11.1.2 Windows 的 GetEnvironmentVariable()

DWORD GetEnvironmentVariable(
  LPCSTR lpName,
  LPSTR  lpBuffer,
  DWORD  nSize
);

Windows 这个函数设计得其实更安全。你需要自己提供缓冲区,它把结果写进去。返回值告诉你实际需要的长度。如果变量不存在,返回 0。

我曾经在项目里直接用 getenv() 去读 Windows 的环境变量,结果发现某些系统变量 getenv() 根本读不到。后来查文档才知道,Windows 下 getenv() 只读「用户环境变量」,而 GetEnvironmentVariable() 能读到系统级变量。嗯,这就是第一个坑。

11.2 系统信息获取:uname() 与 GetSystemInfo()

系统信息包括操作系统名称、版本、架构、处理器数量等。不同平台获取方式完全不同。

11.2.1 POSIX 系统的 uname()

#include <sys/utsname.h>
int uname(struct utsname *buf);

struct utsname 包含以下字段:

字段 含义 示例(Linux) 示例(macOS)
sysname 操作系统名称 Linux Darwin
nodename 主机名 my-server MacBook-Pro
release 内核版本 5.15.0 21.6.0
version 版本详情 #1 SMP ... Darwin Kernel ...
machine 硬件架构 x86_64 arm64

这里有个经典陷阱:macOS 的 sysname 返回的是 "Darwin" 而不是 "macOS"。我第一次看到时还以为是程序写错了,查了半天才发现这是历史原因——macOS 的内核叫 XNU,但 uname 的 sysname 字段沿用了 Darwin 的名字。

11.2.2 Windows 的 GetSystemInfo()

void GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);

SYSTEM_INFO 结构体包含处理器架构、页面大小、最小/最大应用程序地址等。常用的字段:

  • wProcessorArchitecture:PROCESSOR_ARCHITECTURE_AMD64、PROCESSOR_ARCHITECTURE_INTEL、PROCESSOR_ARCHITECTURE_ARM64 等
  • dwNumberOfProcessors:逻辑处理器数量
  • dwPageSize:内存页大小
小技巧: Windows 下获取操作系统版本推荐用 RtlGetVersion() 而不是 GetVersionEx(),后者在 Windows 8.1 之后被故意返回虚假版本号(为了兼容性)。我当年被这个坑过,排查了两天才发现是 API 本身的问题。

11.3 跨平台封装设计

好了,现在我们来设计一个统一的接口。我的思路是这样的:

  1. 定义一个结构体,包含所有需要的信息
  2. 提供统一的初始化函数
  3. 内部用条件编译区分平台
  4. 错误处理统一返回错误码

11.3.1 统一数据结构

typedef struct {
    char os_name[64];        // "Windows", "Linux", "macOS"
    char os_version[64];     // "10.0.19041", "5.15.0", "21.6.0"
    char architecture[16];   // "x86_64", "arm64", "i386"
    int  processor_count;    // 逻辑 CPU 数量
    char hostname[256];      // 主机名
} SystemInfo;

11.3.2 核心实现

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

#ifdef _WIN32
    #include <windows.h>
#else
    #include <sys/utsname.h>
    #include <unistd.h>
#endif

int get_system_info(SystemInfo *info) {
    if (!info) return -1;
    memset(info, 0, sizeof(SystemInfo));

#ifdef _WIN32
    // Windows 实现
    SYSTEM_INFO sys_info;
    GetSystemInfo(&sys_info);

    strcpy(info->os_name, "Windows");

    // 获取版本号
    OSVERSIONINFOEXW ver = { sizeof(ver) };
    #pragma warning(push)
    #pragma warning(disable: 4996)
    GetVersionExW((OSVERSIONINFOW*)&ver);
    #pragma warning(pop)
    snprintf(info->os_version, sizeof(info->os_version),
             "%d.%d.%d", ver.dwMajorVersion, ver.dwMinorVersion, ver.dwBuildNumber);

    // 架构
    switch (sys_info.wProcessorArchitecture) {
        case PROCESSOR_ARCHITECTURE_AMD64:
            strcpy(info->architecture, "x86_64"); break;
        case PROCESSOR_ARCHITECTURE_INTEL:
            strcpy(info->architecture, "i386"); break;
        case PROCESSOR_ARCHITECTURE_ARM64:
            strcpy(info->architecture, "arm64"); break;
        default:
            strcpy(info->architecture, "unknown");
    }

    info->processor_count = sys_info.dwNumberOfProcessors;

    // 主机名
    DWORD size = sizeof(info->hostname);
    GetComputerNameA(info->hostname, &size);

#else
    // POSIX 实现(Linux / macOS)
    struct utsname uts;
    if (uname(&uts) != 0) return -1;

    // 判断是 Linux 还是 macOS
    if (strcmp(uts.sysname, "Linux") == 0) {
        strcpy(info->os_name, "Linux");
    } else if (strcmp(uts.sysname, "Darwin") == 0) {
        strcpy(info->os_name, "macOS");
    } else {
        strncpy(info->os_name, uts.sysname, sizeof(info->os_name) - 1);
    }

    strncpy(info->os_version, uts.release, sizeof(info->os_version) - 1);
    strncpy(info->architecture, uts.machine, sizeof(info->architecture) - 1);
    strncpy(info->hostname, uts.nodename, sizeof(info->hostname) - 1);

    // 获取 CPU 数量
    info->processor_count = (int)sysconf(_SC_NPROCESSORS_ONLN);
    if (info->processor_count <= 0) info->processor_count = 1;

#endif

    return 0;
}
核心要点: 这个封装把平台差异全部隐藏在条件编译内部。调用者只需要包含一个头文件,调用一个函数,就能拿到所有信息。这就是跨平台设计的精髓——对外统一,对内隔离。

11.4 环境变量读取的封装

环境变量读取相对简单,但要注意线程安全和缓冲区管理。

char* get_env_safe(const char *name) {
    if (!name) return NULL;

#ifdef _WIN32
    // Windows 实现:先查询长度,再分配缓冲区
    DWORD len = GetEnvironmentVariableA(name, NULL, 0);
    if (len == 0) return NULL;

    char *buf = (char*)malloc(len);
    if (!buf) return NULL;

    GetEnvironmentVariableA(name, buf, len);
    return buf;
#else
    // POSIX 实现:getenv + strdup
    const char *val = getenv(name);
    if (!val) return NULL;

    char *buf = strdup(val);
    return buf;
#endif
}

void free_env_safe(char *ptr) {
    free(ptr);
}

为什么我要自己分配内存?因为 getenv() 返回的指针不能 free,而 Windows 的 GetEnvironmentVariable() 又需要你提供缓冲区。统一成 malloc 分配,调用者用完后 free_env_safe() 释放,这样接口就一致了。

注意: 这个函数返回的内存必须由调用者释放。我曾经见过有人直接拿返回值当普通字符串用,用完不 free,结果内存泄漏。嗯,所以我在函数名里加了 _safe 后缀,提醒调用者这是动态分配的。

11.5 完整示例

#include <stdio.h>

int main() {
    SystemInfo info;
    if (get_system_info(&info) != 0) {
        fprintf(stderr, "获取系统信息失败\n");
        return 1;
    }

    printf("操作系统: %s\n", info.os_name);
    printf("版本: %s\n", info.os_version);
    printf("架构: %s\n", info.architecture);
    printf("CPU 数量: %d\n", info.processor_count);
    printf("主机名: %s\n", info.hostname);

    // 读取环境变量
    char *path = get_env_safe("PATH");
    if (path) {
        printf("PATH: %s\n", path);
        free_env_safe(path);
    }

    return 0;
}

11.6 知识体系图

下面这张图展示了本章的核心逻辑:

跨平台系统信息与环境变量封装 统一接口:get_system_info() / get_env_safe() 条件编译分支:#ifdef _WIN32 / #else Windows 实现 • GetSystemInfo() → 架构/CPU数 • GetVersionEx() / RtlGetVersion() • GetEnvironmentVariableA() • GetComputerNameA() ⚠ 注意版本 API 兼容性问题 POSIX 实现 • uname() → 系统名/版本/架构 • sysconf(_SC_NPROCESSORS_ONLN) • getenv() + strdup() • gethostname() ⚠ macOS 返回 "Darwin"

这张图把整个封装结构讲得很清楚了。上层调用者只看到统一接口,底层实现通过条件编译自动选择平台代码。你想想看,这样设计之后,你的业务代码里就再也不用写 #ifdef _WIN32 了,多清爽。

个人建议: 在实际项目中,我习惯把这类封装放在一个独立的 platform/ 目录下,每个平台一个 .c 文件,而不是全挤在一个文件里用条件编译。虽然编译时只链接一个,但代码可读性会好很多。不过对于教学示例,一个文件更直观。

好了,环境变量和系统信息的跨平台封装就讲到这里。记住核心思路:对外统一接口,对内隔离差异。下次你写跨平台代码时,不妨先想想「这个 API 在不同系统上有什么不同」,然后提前封装好,省得后面到处打补丁。


公众号:蓝海资料掘金营,微信deep3321