UTF-8字面量和十六进制浮点数字面量

1. 前言

在开始学习 C++17 的新字面量特性之前,我们先要理解什么是字面量。简单来说,字面量就是在代码中直接写出来的值。例如:

  • 123整型字面量
  • 3.14浮点数字面量
  • 'A'字符字面量
  • "Hello"字符串字面量

字面量就像是烹饪中的原材料,程序员可以直接在代码中使用它们,而不需要先定义变量。在 C++17 中,标准委员会对字面量进行了两项非常实用的增强:一是让 UTF-8 字符字面量 的类型变得更规范,二是引入了 十六进制浮点数字面量

2. UTF-8字面量

2.1 语法格式

想象一下我们要开发一个国际化的应用:

  • 中国用户想看到中文”你好”
  • 小鬼子想看到鬼子文”こんにちは”
  • 俄罗斯用户想看到俄文”Привет”
  • 还有各种表情符号😀🎉🎈

在计算机里,所有东西最终都是 0 和 1。比如字母 'A',计算机存的是 01000001(十进制的 65)。但是,中文、英文、Emoji 表情这么多,计算机怎么知道哪个 01 代表哪个字呢?这就需要一本字典,我们叫它字符编码

  • GBK(本地编码): 以前的老办法。在中文 Windows 上,它说 11010110 11010000 代表“中”。但在英文电脑上,11010110 11010000 可能代表乱码。
  • UTF-8(国际通用): 现在的行业标准。全世界统一用这本字典。无论你在哪台电脑上,11100100 10111000 10100000 永远代表“中”。

UTF-8就像一个超级字符集,可以表示世界上几乎所有的文字和符号。所以写现代程序,我们希望大家都用 UTF-8

u8 是 C++11 引入,并在 C++17 中普及的一个前缀。把它加在字符串前面,就是告诉编译器:请帮我把这个字符串里的数据,转换成 UTF-8 格式的 0 和 1 存起来!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto ch1 = u8'A';          // UTF-8字符
auto ch2 = u8"Hello"; // UTF-8字符串
auto ch3 = u8"你好世界"; // UTF-8中文字符串
auto ch4 = u8"😀 Hello"; // 带表情的字符串

// 写法 A:普通字符串("本地模式")
std::string s1 = "你好";
// 问题:如果在中文 Windows 下编译,它是 GBK;在 Linux 下编译,它是 UTF-8。
// 容易导致乱码!

// 写法 B:u8 字符串("强制 UTF-8 模式")
std::string s2 = u8"你好";
// 保证:无论在哪里编译,它永远存成 UTF-8 的格式。
// 安全!放心!

在 C++17 中u8"你好" 的类型依然是 const char*。这意味着,它跟以前用的 char* 几乎没区别,我们可以直接把它塞进 std::string 里,完全兼容。

1
2
3
// C++17 完全合法
std::string msg = u8"这是一条消息";
std::cout << msg; // 只要你的终端支持 UTF-8,就能正常显示中文

到了 C++20u8 会升级为一种叫 char8_t 的新类型,但在 C++17 里,我们暂且把它当成一种带保险的 char

3. 十六进制浮点数字面量

在 C++17 之前,我们写的浮点数都是十进制的:

  • 0.5
  • 3.14159
  • 1.2e-4 (科学计数法)

这对于数学计算很友好,但对于计算机底层开发(如嵌入式、驱动、图形学)非常不友好。因为计算机内部是用二进制存储浮点数的。比如,想表示十进制 0.1,在二进制里是一个无限循环小数,不仅存储有精度误差,而且很难从字面上看出它对应的二进制位是什么。这就像用分数1/3表示成十进制小数(0.33333…),永远无法精确。十六进制浮点数可以精确表示二进制浮点数,避免舍入误差!

C++17 允许我们直接用十六进制来写浮点数!这让我们能精确控制数字的二进制表示。

语法格式:0x + 十六进制整数部分 + .+ 十六进制小数部分 + p + 指数

  • 前缀:必须以 0x0X 开头。
  • 底数:中间是十六进制数(0-9, A-F),可以包含小数点。
  • 指数标志:必须用 pP(代表 Power,2的幂),千万不能用 ee 是十进制科学计数法)。
  • 指数值p 后面跟的是十进制整数,代表 2 的多少次方。
写法 含义解析 十进制结果
0x1p0 1×20 1.0
0x1p4 1×24 (即 1×16) 16.0
0x1.8p1 1.5 (十六进制 0.8 等于十进制的 0.5)×21 3.0
0xAp2 10 (十六进制 A)×22 40.0
0x8p-1 8×2−1 (即 8×0.5) 4.0

3.1 十六进制转十进制

当我们拿到一个十六进制浮点字面量的时候,如何将其转换为十进制数呢?步骤如下:

  1. 0x 后面 p 前面的部分,按十六进制规则,转换成十进制
  2. 把第一步得到的十进制结果,乘以 2 的指数次方
  3. 得到最终十进制结果

我们用0x1.8p2这个例子再走一遍流程:

  1. 十六进制尾数 1.8
    • 1 = 1 × 16⁰ = 1
    • 8 = 8 × 16⁻¹ = 8/16 = 0.5
    • 总和 = 1 + 0.5 = 1.5(十进制尾数)
  2. 乘以 22:1.5 × 4 = 6.0

3.2 十进制转十六进制

十进制的浮点数到十六进制浮点字面量的转换对于程序员来说用手算是有点难度的,这个转换的过程就像是一道无形的门槛把使用者挡在门外,瞬间感觉不香了。下面介绍两种方法让十六进制字面量重新从鸡肋变成真香:

  1. 使用 Python

    当需要在 C++ 代码里写一个精确的十六进制浮点字面量时,如果不会写 C++ 代码去转换它,可以打开 Python 算出来,然后粘过去。Python 内置的 float.hex() 方法完美契合这个需求。

    1
    2
    3
    4
    5
    6
    7
    8
    # 在 Python 命令行中
    >>> (0.1).hex()
    '0x1.999999999999ap-4'

    # 如果是单精度 float
    >>> import struct
    >>> struct.unpack('f', struct.pack('f', 0.1))[0].hex()
    '0x1.99999ap-4'

    最后我们把 '0x1.99999ap-4' 复制到自己的 C++ 代码里就可以了。

  2. 在 C++ 运行时转换

    如果需要在程序运行时,把一个浮点数变量的内容变成十六进制字符串(比如用于日志或网络传输),C++17 引入了非常高效的 std::to_chars

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <iostream>
    #include <charconv>
    #include <array>

    int main()
    {
    double value = 0.1;

    // 缓冲区
    std::array<char, 32> buf;

    // C++17 的 std::to_chars
    // std::chars_format::hex 是关键
    auto result = std::to_chars(buf.data(), buf.data() + buf.size(), value, std::chars_format::hex);

    if (result.ec == std::errc())
    {
    // 注意:这里生成的是 "1.999999999999ap-4" (没有 0x 前缀)
    // 你可能需要手动加上 "0x"
    std::cout << "Hex float literal: 0x" << std::string(buf.data(), result.ptr) << std::endl;
    // 输出: 0x1.999999999999ap-4
    }

    return 0;
    }

    std::to_chars 是 C++17 引入的一个低开销、无 locale 依赖、无内存分配的数值转换函数,定义在头文件 <charconv> 中。std::to_chars 对整数和浮点数有两组重载。所有函数均返回 std::to_chars_result 结构体。

    • 函数返回值结构体

      1
      2
      3
      4
      5
      struct to_chars_result 
      {
      char* ptr; // 指向写入结束位置的下一个字符的指针
      std::errc ec; // 错误码,若转换成功则为 std::errc()
      };
    • 函数原型 - 整数版本

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // 1. 自动检测进制(十进制)
      std::to_chars_result to_chars(char* first, char* last,
      int value);

      std::to_chars_result to_chars(char* first, char* last,
      unsigned int value);

      // ... 以及其他所有整数类型
      // (long, long long, unsigned long, unsigned long long 等)

      // 2. 指定进制 (基数为 2 到 36)
      std::to_chars_result to_chars(char* first, char* last,
      int value,
      int base);

      std::to_chars_result to_chars(char* first, char* last,
      unsigned int value,
      int base);
      // ... 以及其他所有整数类型
    • 函数原型 - 浮点数版本

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      // 格式说明符类型 (C++23 之前是枚举,C++23 变成 struct)
      enum class chars_format
      {
      scientific = 1, // 科学计数法,如 1.23e+5
      fixed = 2, // 定点计数法,如 123.0
      hex = 4, // 十六进制浮点数,如 0x1.fp3
      general = 3 // 既可以是 scientific 也可以是 fixed
      };

      // 浮点数重载
      std::to_chars_result to_chars(char* first, char* last,
      float value,
      std::chars_format fmt);

      std::to_chars_result to_chars(char* first, char* last,
      double value,
      std::chars_format fmt);

      std::to_chars_result to_chars(char* first, char* last,
      long double value,
      std::chars_format fmt);

      // C++23 新增:指定精度
      std::to_chars_result to_chars(char* first, char* last,
      float value,
      std::chars_format fmt,
      int precision);
      // ... double 和 long double 同理
    参数名 类型 说明
    first char* 输出缓冲区的起始指针。转换后的字符串将从这里开始写入。
    last char* 输出缓冲区的结束指针(尾后指针,即 first + buffer_size)。这是写入位置的上界,任何写入都不得超过此地址。
    value 整数或浮点类型 需要被转换为字符串的数值。
    base int (仅整数) 指定进制的基数,范围必须是 2 到 36。默认为 10。数字 0-9 后跟 a-z 用于表示大于 9 的值。
    fmt std::chars_format (仅浮点) 指定浮点数的格式化方式。可以是 scientific, fixed, hex, 或 general
    precision int (仅浮点,C++23) 指定精度。对于 general 格式,它指定有效数字的最大位数;对于 scientificfixed,它指定小数点后的位数。

    函数使用示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    #include <charconv>
    #include <iostream>
    #include <array>

    int main()
    {
    // 1. 整数转十六进制
    std::array<char, 10> buf;
    auto res = std::to_chars(buf.data(), buf.data() + buf.size(), 255, 16);

    if (res.ec == std::errc())
    {
    // 手动添加 null 终止符以便打印
    *res.ptr = '\0';
    std::cout << "Hex: " << buf.data() << std::endl; // 输出: ff
    }

    // 2. 浮点数转科学计数法 (假设支持 C++17 浮点重载)
    // 注意:不同编译器对浮点 to_chars 的支持进度不同
    std::array<char, 20> fbuf;
    double val = 3.14159;
    auto res_f = std::to_chars(fbuf.data(), fbuf.data() + fbuf.size(), val, std::chars_format::scientific, 2);

    if (res_f.ec == std::errc())
    {
    *res_f.ptr = '\0';
    std::cout << "Sci: " << fbuf.data() << std::endl; // 可能输出: 3.14e+00
    }

    return 0;
    }