2017 年 5 月

第 32 卷,第 5 期

C++ - 使用新式 C++ 访问 Windows 注册表

作者 Giovanni Dicanio

Windows OS 公开了一系列 C 接口 API,以方便开发者访问注册表。其中一些 API 的级别相当低,需要程序员注意许多细节。自 Windows Vista 起,这一大家庭又新增了一种更高级别的 API:RegGetValue 函数 (bit.ly/2jXtfpJ)。在引入此 API 之前,若要读取注册表中的值,必须先调用 RegOpenKeyEx 来打开包含值的相应注册表项。然后,必须调用 RegQueryValueEx API,同时处理许多复杂的细节。例如,如果使用 RegQueryValueEx 读取字符串值,则不能保证返回的字符串以 NUL 正确结尾,这可能会导致代码中出现一系列危险的安全 bug。为了防止这种情况发生,务必要检查返回的字符串是否以 NUL 结尾;如果不是,需要手动添加。此外,还务必要使用 RegCloseKey 正确关闭已打开的注册表项。

当然,打开注册表项可能会失败。因此,为了处理此问题,还必须添加代码。RegGetValue API 简化了这一工作流,因为它会自动打开并在使用后关闭所需的注册表项,同时还会先在字符串末尾正确添加 NUL,然后再将字符串返回给调用方。虽然进行了这样的简化,但 RegGetValue 函数仍是低级别 C 接口函数;此外,该函数实际上可以处理多种不同类型的注册表值(包括 DWORD、字符串和二进制数据),因此接口编程起来十分复杂。

万幸的是,可以使用新式 C++ 在此 RegGetValue Win32 API 基础之上正确生成更高级别的抽象,提供一个用于读取注册表中不同类型值的简便接口。

使用异常表示错误

RegGetValue API 是 C 接口 API,因此会使用返回代码向调用方指明错误条件。特别是,此函数返回 LONG 类型的值: 如果成功,返回 ERROR_SUCCESS(即值为零);如果出错,返回其他值。例如,如果调用方提供的输出缓冲区不够大,导致 API 无法写入数据,此函数返回 ERROR_MORE_DATA。为了在此 C API 基础之上生成更高级别的 C++ 接口,可以定义用于表示错误的 C++ 异常类。此类可以派生自标准的 std::runtime_error 类,能够在其中嵌入 RegGetValue 返回的 LONG 错误代码:

class RegistryError
  : public std::runtime_error
{
public:
  ...
private:
  LONG m_errorCode;
};

此外,还可以在异常对象中嵌入其他信息;例如,HKEY 和子项的名称。这就是基本实现代码。

可以添加构造函数,使用失败的 RegGetValue 调用生成的错误消息和返回代码创建此异常类的实例:

RegistryError(const char* message, LONG errorCode)
  : std::runtime_error{message}
  , m_errorCode{errorCode}
{}

可以向使用只读取值函数 (getter) 的客户端公开错误代码:

LONG ErrorCode() const noexcept
{
  return m_errorCode;
}

至此,你已生成此异常类,可以继续将 RegGetValue C API 包装在更高级别的 C++ 接口中,此接口不仅更易于使用,还能减少出错。

读取注册表中的 DWORD 值

让我们从下面的简单操作入手:使用 RegGetValue API 读取注册表中的 DWORD 值。此示例中的使用模式相当简单。但首先,让我们看看可以在 C++ 中定义什么类型的接口来管理这种情况。

下面是 RegGetValue API 原型:

LONG WINAPI RegGetValue(
  _In_        HKEY    hkey,
  _In_opt_    LPCTSTR lpSubKey,
  _In_opt_    LPCTSTR lpValue,
  _In_opt_    DWORD   dwFlags,
  _Out_opt_   LPDWORD pdwType,
  _Out_opt_   PVOID   pvData,
  _Inout_opt_ LPDWORD pcbData
);

如你所见,此 C 接口函数需要使用高度泛型数据,如 void* 输出缓冲区 (pvData) 和输入/输出缓冲区大小参数 (pcbData)。此外,还有标识注册表项及其下特定值名称的 C 样式字符串(lpSubKey 和 lpValue)。可以略微调整一下此 C 函数原型,使其更易于 C++ 调用方使用。

首先,由于要指明抛出 C++ 异常的错误条件,C++ 包装器可以直接以返回值的形式返回从注册表中读取的 DWORD 值。这样一来,便自动无需使用原始 void* 输出缓冲区参数 (pvData) 和相关大小参数 (pcbData)。

此外,由于要使用 C++,最好使用 std::wstring 类(而不是 C 样式的原始指针)表示 Unicode (UTF-16) 字符串。所以,可以定义更为简单的 C++ 函数,从而读取注册表中的 DWORD 值:

DWORD RegGetDword(
  HKEY hKey,
  const std::wstring& subKey,
  const std::wstring& value
)

如你所见,其中没有 PVOID 和 LPDWORD 参数;输入字符串通过常量引用传递到 std::wstring 对象,从注册表中读取的值则以 DWORD 的形式由此 C++ 函数返回。这绝对是一个更简单的更高级别接口。

现在,让我们深入探究实现代码。如前所述,在此示例中,RegGetValue 的调用模式相当简单。只需声明一个 DWORD 变量,用于存储从注册表中读取的值:

DWORD data{};

然后,需要声明另一个 DWORD 变量,用于表示 RegGetValue 写入的输出缓冲区的大小(以字节为单位)。请注意,这个简单示例中的输出缓冲区就是以前的“数据”变量,它的大小始终是 DWORD 的大小:

DWORD dataSize = sizeof(data);

不过,请注意,不能将 dataSize 标记为常量,因为它是 RegGetValue 的输入和输出参数。

然后,可以调用 RegGetValue API:

LONG retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_DWORD,
  nullptr,
  &data,
  &dataSize
);

输入 wstring 对象通过 wstring::c_str 方法转换成原始 C 样式字符串指针。RRF_RT_REG_DWORD 标志将注册表值的类型限制为 DWORD。如果要读取的注册表值为不同类型,那么出于稳妥考虑 RegGetValue 函数调用会失败。

最后两个参数表示输出缓冲区的地址(在此示例中,为数据变量的地址)和用于存储输出缓冲区大小的变量的地址。实际上,在返回时,RegGetValue 会报告写入输出缓冲区的数据的大小。在读取简单 DWORD 值的示例中,数据大小始终为 4 字节,即 sizeof(DWORD)。然而,此大小参数对大小可变的值(如字符串)更为重要;我将在本文后面对此进行介绍。

调用 RegGetValue 函数后,可以检查返回代码,并在出错时抛出异常:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read DWORD from registry.", retCode};
}

请注意,RegGetValue 返回的错误代码 (retCode) 已嵌入异常对象中,稍后可由处理异常的代码进行检索。

相反,如果成功,DWORD 数据变量可以直接返回给调用方:

return data;

以上便是对函数实现代码的介绍。

调用方可以使用下面的代码直接调用此 C++ 包装器函数:

DWORD data = RegGetDword(HKEY_CURRENT_USER, subkey, L"MyDwordValue");

请注意,与原始 RegGetValue C API 调用相比,此代码非常简单。只需将句柄传递给打开的注册表项(在此示例中,为 HKEY_CURRENT_USER 预定义项),即包含子项和值名称的字符串。如果成功,DWORD 值会返回给调用方。然而,如果出错,则会抛出 RegistryError 类型的自定义异常。与调用 RegGetValue 相比,这种代码不仅级别更高,而且要简单得多。实际上,RegGetValue的复杂性是被隐藏在这个自定义 RegGetDword C++ 包装器函数中了。

可以通过同样的方式从注册表中读取 QWORD(64 位数据)值;在这种情况下,只需将注册表值的 DWORD 类型替换为 64 位 ULONGLONG。

读取注册表中的字符串值

读取注册表中的 DWORD 值相当简单:只需调用一次 RegGetValue Win32 API 就已足够。这主要是因为 DWORD 值的大小不变,DWORD 的大小始终为 4 字节。相比之下,读取注册表中的字符串又加了一层复杂性,因为字符串是大小可变的数据。在这种情况下,我的想法是调用 RegGetValue API 两次: 在第一次调用中,请求此 API 返回输出字符串缓冲区的相应大小。接下来,动态分配适当大小的缓冲区。最后,第二次调用 RegGetValue,将字符串数据写入之前分配的缓冲区中。(我在之前的文章“在 Win32 API 边界使用 STL 字符串”(msdn.com/magazine/mt238407) 中详细介绍过这种模式)。

首先,让我们来看看 C++ 更高级别包装器函数的原型:

std::wstring RegGetString(
  HKEY hKey,
  const std::wstring& subKey,
  const std::wstring& value
)

与 DWORD 示例一样,这相对于原始复杂的 RegGetValue C API 原型更为简单。字符串值以 std::wstring 实例的形式从函数返回。反之,如果出错,则会抛出异常。子项名称和值名称同样作为输入wstring 常量引用参数传递。

现在,我将介绍实现代码。

如前所述,我的想法是先调用 RegGetValue API,获取用于存储字符串值的输出缓冲区的大小:

DWORD dataSize{};
LONG retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_SZ,
  nullptr,
  nullptr,
  &dataSize
);

可以看到与上面的 DWORD 值示例类似的调用语法。wstring 对象通过 wstring::c_str 方法调用转换成 C 样式字符串指针。此示例中的 RRF_RT_REG_SZ 标志将有效注册表类型限制为字符串类型 (REG_SZ)。如果成功,RegGetValue API 会在 dataSize 变量中写入相应的输出缓冲区大小(以字节为单位)。

如果失败,则需要抛出自定义 RegistryError 类的异常:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read string from registry", retCode};
}

至此,你已了解相应的输出缓冲区大小,现在可以为输出字符串分配所需大小的 wstring 对象:

std::wstring data;
data.resize(dataSize / sizeof(wchar_t));

请注意,虽然 RegGetValue 返回的 dataSize 值以字节为单位,但 wstring::resize 方法应使用以 wchar_t 计数为单位的大小值。因此,必须从字节扩展为 wchar_t,用原字节大小值除以 sizeof(wchar_t)。

至此,你已经有分配了足够空间的字符串,可以将指向内部缓冲区的指针传递给 RegGetValue API,这一次将在所提供的缓冲区中写入实际的字符串数据:

retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_SZ,
  nullptr,
  &data[0],
  &dataSize
);

&data[0] 是 wstring 内部缓冲区的地址,将由 RegGetValue API 写入。

和往常一样,请务必验证 API 调用的结果,并在出错时抛出异常:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read string from registry", retCode};
}

请注意,如果成功,RegGetValue 会在 dataSize 变量中写入实际结果字符串大小(以字节为单位)。必须根据此大小重设 wstring 对象的大小。由于 dataSize 是以字节为单位,因此在处理 wstring 时最好转换成相应的 wchar_t 计数:

DWORD stringLengthInWchars = dataSize / sizeof(wchar_t);

此外,dataSize 包括输出字符串的终止符 NUL。不过,由于 wstring 对象已经以 NUL ­结尾,因此必须注意避免读取字符串出现虚假的双 NUL 终止符。必须删除 RegGetValue 写入的 NUL 终止符:

stringLengthInWchars--; // Exclude the NUL written by the Win32 API
data.resize(stringLengthInWchars);

请注意,如果成功,RegGetValue API 会保证字符串以 NUL 结尾,即使注册表中存储的原始字符串不以 NUL 结尾,也不例外。这是比旧的 RegQueryValueEx API 更为安全的行为,旧 API 不保证返回的字符串以 NUL 结尾。因此,调用方必须编写其他代码来妥善处理此情况,这就增加了代码的整体复杂性和出错概率。

至此,数据变量包含从注册表中读取的字符串值,可以在函数退出之前将它返回给调用方:

return data;

在 RegGetValue 低级别 C API 基础之上生成此便捷 RegGetString C++ 包装器后,可以按以下方式进行调用:

wstring s = RegGetString(HKEY_CURRENT_USER, subkey, L"MyStringValue");

与 DWORD 示例一样,你已在 RegGetValue Win32 API 基础之上提升了抽象级别,提供了易于使用且难以滥用的 C++ 包装器函数,以便于读取注册表中的字符串值。处理 RegGetValue API 涉及的所有细节和复杂性都安全地隐藏在这一自定义 RegGetString C++ 函数的主体中。

读取注册表中的多字符串值

另一种类型的注册表值就是所谓的“多字符串”: 一般来说,这是一组用一个注册表值打包的以双 NUL 结尾的字符串。这种以双 NUL 结尾的字符串数据结构由一系列以 NUL 结尾的 C 样式字符串组成,这些字符串占据相邻内存位置。此序列的末尾还标记有一个 NUL 终止符,因此整个结构是以两个 NUL 结尾。有关此数据结构的更多详细信息,请参阅博文“什么是以双 Null 结尾的字符串格式(不含字符串)?”(bit.ly/2jCqg2u)。

此示例中的 RegGetValue Win32 API 使用模式与前面的单字符串示例非常相似。也就是说,先调用 RegGetValue API 获取包含相应数据(在此示例中,是以双 NUL 结尾的相邻字符串的整个序列)的整个目标缓冲区的大小。然后,动态分配此类大小的缓冲区。最后,第二次调用 RegGetValue 函数,传递之前分配的缓冲区的地址,以便 API 可以在相应缓冲区中写入实际的多字符串数据。

在此示例中,必须注意存储以双 NUL 结尾的字符串的数据结构。实际上,虽然 std::wstring 可以妥善包含嵌入的 NUL(可用于存储以双 NUL 结尾的字符串结构),但我希望提升抽象级别,将以双 NUL 结尾的字符串解析成级别更高且更便捷的 vector<wstring>。

所以,读取注册表中多字符串值的 C++ 包装器函数的原型如下所示:

std::vector<std::wstring> RegGetMultiString(
  HKEY hKey,
  const std::wstring& subKey,
  const std::wstring& value
)

如果成功,多字符串会以理想的 vector<wstring> 形式返回给调用方。然而,如果出错,则会抛出常规 RegistryError 形式的异常。

在 C++ 包装器函数主体中,先调用 RegGetValue API,获取用于存储多字符串的相应输出缓冲区的大小:

DWORD dataSize{};
LONG retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_MULTI_SZ,
  nullptr,
  nullptr,
  &dataSize
);

请注意,这一次使用 RRF_RT_REG_MULTI_SZ 标志来指定多字符串注册表值类型。

和往常一样,如果出错,则会抛出异常(错误代码已嵌入 RegistryError 对象中):

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read multi-string from registry", retCode};
}

如果成功,则分配适当大小的缓冲区来存储整个多字符串:

std::vector<wchar_t> data;
data.resize(dataSize / sizeof(wchar_t));

我认为在表示多字符串原始缓冲区方面,vector<wchar_t> 比 wstring 更加清楚。此外,还请注意,由于 RegGetValue API 返回的大小值以字节为单位,因此必须先将它正确转换成 wchar_t 计数,然后才能传递给 vector::resize 方法。

然后,可以第二次调用 RegGetValue API,在之前分配的缓冲区中写入实际的多字符串数据:

retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_MULTI_SZ,
  nullptr,
  &data[0],
  &dataSize
);

&data[0] 自变量指向输出缓冲区的开头。

同样,必须检查 API 返回代码,并指明抛出 C++ 异常的错误条件:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read multi-string"
    from registry", retCode};
}

最好也使用作为输出参数由 RegGetValue API 返回的 dataSize 值适当地重设数据缓冲区的大小:

data.resize( dataSize / sizeof(wchar_t) );

此时,数据变量(即 vector<wchar_t>)存储以双 NUL 结尾的字符串序列。最后一步是,解析此数据结构,并将它转换成级别更高且更便捷的 vector<wstring>:

// Parse the double-NUL-terminated string into a vector<wstring>
std::vector<std::wstring> result;
const wchar_t* currStringPtr = &data[0];
while (*currStringPtr != L'\0')
{
  // Current string is NUL-terminated, so get its length with wcslen
  const size_t currStringLength = wcslen(currStringPtr);
  // Add current string to result vector
  result.push_back(std::wstring{ currStringPtr, currStringLength });
  // Move to the next string
  currStringPtr += currStringLength + 1;
}

最后,可以将结果 vector<wstring> 对象返回给调用方:

return result;

可以按下面的方式直接调用此 RegGetMultiString C++ 包装器:

vector<wstring> multiString = RegGetMultiString(
  HKEY_CURRENT_USER,
  subkey,
  L"MyMultiSz"
);

同样,Win32 RegGetValue API 的所有复杂性是被隐藏在更高级别的便捷 C++ 接口后面了。

枚举注册表项下的值

另一常见的 Windows 注册表操作是枚举给定注册表项下的值。Windows 为此提供了 RegEnumValue API (bit.ly/2jB4kaV)。在本文中,我将介绍如何使用此 API 来获取给定注册表项下值的名称和类型列表(枚举过程包装在更高级别的便捷 C++ 函数中)。自定义 C++ 函数可以将与要枚举的项相关的有效 HKEY 句柄用作输入。如果成功,此自定义 C++ 包装器函数会返回一对向量: 这对向量中的第一项是包含值名称的 wstring,第二项是表示值类型的 DWORD。所以,此 C++ 包装器函数的原型如下所示:

std::vector<std::pair<std::wstring, DWORD>> RegEnumValues(HKEY hKey)

现在,我将详细介绍枚举过程。我的想法是先调用 RegQueryInfoKey (bit.ly/2jraw2H) API 来获取一些有用的枚举前信息,如给定注册表项下的值总计数和值名称最大长度,如图 1 中所示。

图 1:调用 RegQuery­InfoKey API

DWORD valueCount{};
DWORD maxValueNameLen{};
LONG retCode = ::RegQueryInfoKey(
  hKey,
  nullptr,    // No user-defined class
  nullptr,    // No user-defined class size
  nullptr,    // Reserved
  nullptr,    // No subkey count
  nullptr,    // No subkey max length
  nullptr,    // No subkey class length
  &valueCount,
  &maxValueNameLen,
  nullptr,    // No max value length
  nullptr,    // No security descriptor
  nullptr     // No last write time
);

请注意,我对不关注的各条信息传递的是 nullptr。当然,必须检查返回值,并在调用上述 API 出错时抛出异常:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot query key info from"
    the registry", retCode};
}

根据 Windows 开发人员中心 RegQueryInfoKey 函数页 (bit.ly/2lctUDt),针对值名称最大长度返回的大小(存储在前面代码的 maxValueNameLen 变量中)不包括终止符 NUL;因此,让我们来调整一下此值,即添加一个值,以在分配缓冲区来读取值名称时将终止符 NUL 考虑在内:

maxValueNameLen++;

然后,可以分配适当大小的缓冲区,在每个枚举步骤读取值名称;成本低、高效率的 std::unique_ptr<wchar_t[]> 可用于这一目的:

auto nameBuffer = std::make_unique<wchar_t[]>(maxValueNameLen);

采用值名称和值类型成对形式的枚举结果可以存储在 std::vector 中:

std::vector<std::pair<std::wstring, DWORD>> values;

你在枚举过程中将内容分批添加到此向量中,然后在枚举完成时将“值”返回给调用方。

然后,可以使用 for 循环,重复调用 RegEnumValue API,并在每个循环访问步骤枚举一个新值:

for (DWORD index = 0; index < valueCount; index++)
{
  // Call RegEnumValue to get data of current value ...
}

请注意,valueCount 是通过枚举前最初的 RegQueryInfoKey 调用获取而来。

在 for 循环主体中,可以调用 RegEnumValue API 来获取当前值的相应信息。在此上下文中,关注的是值名称和值类型。值名称是从之前分配的 nameBuffer 中读取;值类型存储在简单的 DWORD 中。所以,在 for 循环主体中,可以编写如下代码:

DWORD valueNameLen = maxValueNameLen;
DWORD valueType{};
retCode = ::RegEnumValue(
  hKey,
  index,
  nameBuffer.get(),
  &valueNameLen,
  nullptr,    // Reserved
  &valueType,
  nullptr,    // Not interested in data
  nullptr     // Not interested in data size

和往常一样,最好检查 API 返回值,并在出错时抛出异常:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot get value info from the registry", retCode};
}

如果成功,RegEnumValue API 会在提供的 nameBuffer 中写入值名称,并在 valueType 变量中写入值类型。因此,可以使用这两条信息生成一对<wstring, DWORD>,然后将此信息对添加到枚举结果向量中:

values.push_back(std::make_pair(
  std::wstring{ nameBuffer.get(), valueNameLen },
  valueType
));

在 for 循环结束后,可以将结果“值”向量返回给调用方:

return values;

然后,调用方只需按下面的方式调用 C++ 包装器函数,即可枚举注册表项下的所有值:

auto values = RegEnumValues(hKey);
// For each value
for (const auto& v : values)
{
  // Process v.first (value's name) and v.second (value's type)
  // ...
}

类似的编码模式可用于枚举给定注册表项下的子项;在这种情况下,必须使用 Win32 RegEnumKeyEx (bit.ly/2k3VEX8) API,而不是前面介绍的 RegEnumValue。与本文相关联的下载内容中提供了此子项枚举函数代码。

原始 HKEY 句柄的安全资源管理器

可以安全便捷地将原始 HKEY Win32 句柄类型表示的注册表项包装在 C++ 资源管理器类中。类析构函数会对包装的原始句柄正确调用 RegCloseKey API,从而自动关闭句柄。此外,移动构造函数和移动赋值运算符等移动语义运算可以定义为在 C++ 资源管理器类的不同实例之间高效转让已包装句柄的所有权。为了提高效率,所有不会抛出异常的类方法均标记为 noexcept,以便 C++ 编译器可以生成更为优化的代码。这个便捷的项资源管理器 C++ 类 RegKey 在本文随附的 Registry.hpp 文件中实现。在这个可重用的纯标头文件中,还可以找到几个帮助程序函数的实现代码: RegOpenKey 和 RegCreateKey 分别包装 Win32 API RegOpenKeyEx 和 RegCreateKeyEx,同时返回安全包装在上述 C++ 资源管理器类中的 HKEY 句柄。如果出错,这些 C++ 函数会抛出 RegistryError 异常,同时包装原始 C 接口 Win32 API 返回的错误代码。

总结

与 RegQueryValueEx 等低级别 API 相比,RegGetValue Win32 API 提供级别相对更高的接口来读取 Windows 注册表中的值。此外,RegGetValue 提供的接口还更为安全,例如,可保证返回的字符串以正确的 NUL 结尾。尽管如此,RegGetValue 仍是 C 接口低级别 API,程序员需要注意很多细节,对它进行编程可能会生成更易出错的复杂代码。本文介绍了如何生成便捷易用且难以滥用的新式 C++ 接口来隐藏 RegGetValue API 的复杂性,同时简化了对 Windows 注册表的访问。此外,RegEnumValue API 也包装在更高级别的便捷 C++ 函数中,可用于枚举给定注册表项下的所有值。包含本文中介绍的函数和类的实现代码的源代码位于与本文相关联的可重用纯标头形式下载内容(Registry.hpp 文件)中。


Giovanni Dicanio 是一位计算机程序员,专门研究 C++ 和 Windows OS,而且他还是 Pluralsight 作者 (bit.ly/GioDPS) 和 Visual C++ MVP。除了编程和编写课程,他还乐于在 C++ 论坛和社区中帮助其他人。通过电子邮件地址 giovanni.dicanio@gmail.com 与他联系。他还在 msmvps.com/gdicanio 上发布博文。

衷心感谢以下技术专家对本文的审阅: David Cravey 和 Marc Gregoire
David Cravey 是 GlobalSCAPE 的企业架构师,负责多个 C++ 用户组,并曾四次荣膺 Visual C++ MVP。

Marc Gregoire 是一位来自比利时的高级软件工程师,他是比利时 C++ 用户组的创始人,编写了“Professional C++”(Wiley) 并合著了“C++ 标准库快速参考”(Apress),并且是许多书籍的技术编辑,在 2007 年因其 VC++ 方面的专业素养荣获年度 MVP 大奖。可以通过 marc.gregoire@nuonsoft.com 与 Marc 取得联系。