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 它,也别修改它指向的内容。
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:内存页大小
RtlGetVersion() 而不是 GetVersionEx(),后者在 Windows 8.1 之后被故意返回虚假版本号(为了兼容性)。我当年被这个坑过,排查了两天才发现是 API 本身的问题。
11.3 跨平台封装设计
好了,现在我们来设计一个统一的接口。我的思路是这样的:
- 定义一个结构体,包含所有需要的信息
- 提供统一的初始化函数
- 内部用条件编译区分平台
- 错误处理统一返回错误码
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() 释放,这样接口就一致了。
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 知识体系图
下面这张图展示了本章的核心逻辑:
这张图把整个封装结构讲得很清楚了。上层调用者只看到统一接口,底层实现通过条件编译自动选择平台代码。你想想看,这样设计之后,你的业务代码里就再也不用写 #ifdef _WIN32 了,多清爽。
platform/ 目录下,每个平台一个 .c 文件,而不是全挤在一个文件里用条件编译。虽然编译时只链接一个,但代码可读性会好很多。不过对于教学示例,一个文件更直观。
好了,环境变量和系统信息的跨平台封装就讲到这里。记住核心思路:对外统一接口,对内隔离差异。下次你写跨平台代码时,不妨先想想「这个 API 在不同系统上有什么不同」,然后提前封装好,省得后面到处打补丁。
公众号:蓝海资料掘金营,微信deep3321