C++14 最被低估的部分就是这些 **"小而美" 的语法改进 **—— 它们没有改变 C++ 的编程范式,却能显著提升代码的可读性、可维护性和开发效率。本篇讲解的三个特性(二进制字面量、数字分隔符、聚合类默认初始化),是每个现代 C++ 开发者每天都会用到的基础工具。

它们的共同设计目标是:让代码更接近人类的思维方式,减少机械性的重复劳动

1:二进制字面量

1:基础语法

C++14 首次原生支持二进制字面量,使用0b0B作为前缀,后面跟随 0 和 1 组成的数字序列。在此之前,C++ 只支持十进制、八进制和十六进制字面量。

#include <iostream>
#include <bitset>

int main() {
    // 四种进制字面量对比(都表示十进制的42)
    int d = 42;          // 十进制
    int o = 052;         // 八进制(前缀0)
    int x = 0x2a;        // 十六进制(前缀0x/0X)
    int b = 0b101010;    // 二进制(C++14新增,前缀0b/0B)
    int c = 0;
    std::cout << d << "\n" << o << "\n" << x << "\n" << b << "\n"; // 全部输出42

    const int FLAG_A = 0b0001; // 1
    const int FLAG_B = 0b0010; // 2
    const int FLAG_C = 0b0100; // 4
    const int FLAG_D = 0b1000; // 8

    int flags = FLAG_A | FLAG_C; // 0b0101
    std::cout << "标志位: " << std::bitset<4>(flags) << "\n"; // 输出0101

    if (flags & FLAG_A) {
        std::cout << "FLAG_A 被设置\n";
    }
    return 0;
}

2:为什么需要原生二进制支持

1:解决位操作的可读性

在 C++14 之前,所有位级操作都只能用十六进制表示,这会导致严重的可读性问题。例如,要表示 "第 0 位和第 2 位置 1",C++11 中只能写0x5,而 C++14 中可以直接写0b0101,一眼就能看出哪些位被置位。

// C++11 写法:需要心算十六进制对应的二进制位
const uint8_t GPIO_DIR_OUTPUT = 0x55; // 谁能立刻看出哪些位是输出?

// C++14 写法:一目了然
const uint8_t GPIO_DIR_OUTPUT = 0b01010101; // 偶数位输出,奇数位输入
2:核心应用场景

二进制字面量在底层系统编程中无处不在:

嵌入式寄存器配置:直接对应硬件手册中的位定义

// 配置STM32 GPIO端口模式寄存器(MODER)
// 每两位控制一个引脚:00=输入,01=输出,10=复用,11=模拟
GPIOA->MODER = 0b0100000000000000; // PA7设置为输出模式
  • 网络协议头解析:直接对应协议规范中的位字段
  • 位图操作:图像像素、文件权限位、状态标志位
  • 算法实现:位运算算法(如布隆过滤器、哈夫曼编码)
3:与std::bitset的配合

std::bitset是 C++ 标准库中用于处理固定长度位序列的工具,和二进制字面量结合使用效果极佳:

#include <bitset>

// 定义一个8位的状态寄存器
std::bitset<8> status_reg = 0b10100011;

// 操作单个位
status_reg.set(3);    // 第3位置1 → 0b10101011
status_reg.reset(7);  // 第7位置0 → 0b00101011
status_reg.flip(0);   // 第0位取反 → 0b00101010

// 检查位状态
if (status_reg.test(1)) {
    std::cout << "第1位已置位\n";
}

3:常见陷阱与注意事项

  1. 前缀是0b不是b:不能写成b1010,必须加前导 0
  2. 大小写不敏感0b10100B1010完全等价
  3. 只能包含 0 和 1:出现其他字符会编译错误
  4. C++20 扩展:支持与数字分隔符结合使用(见下一节),例如0b1010'1010

2:数字分隔符

1:基础语法

C++14 允许在数字字面量中使用 ** 单引号'** 作为分隔符,将长数字分成易读的组,分隔符不会影响数字的实际值。

#include <iostream>
#include <bitset>

int main() {
    // 十进制大数字:每三位分隔(国际通用)
    int million = 1'000'000; // 1000000
    long long gdp = 123'456'789'123'456LL;

    // 十六进制:每四位分隔(对应一个字节)
    long hex_value = 0xDEAD'BEEF; // 0xDEADBEEF
    uint32_t ip_address = 0xC0'A8'01'01; // 192.168.1.1

    // 二进制:每四位或八位分隔
    unsigned long long big_binary = 0b1010'1010'1010'1010; // 0xAAAA

    // 浮点数:整数部分和小数部分都可以分隔
    double pi = 3.141'592'653'589'793;

    std::cout << "一百万: " << million << "\n";
    std::cout << "十六进制值: 0x" << std::hex << hex_value << std::dec << "\n";
    std::cout << "π: " << pi << "\n";
    return 0;
}

2:使用规则与最佳实践

1:语法规则
  • 可以出现在数字字面量的任意位置(除了开头和结尾)
  • 可以连续出现吗?不可以1''000会编译错误
  • 不影响数字的数值和类型:1'0001000完全等价
  • 适用于所有进制:十进制、八进制、十六进制、二进制
  • 适用于所有数字类型:整数、浮点数、无符号数、长整数
2:不同进制的最佳分隔方式
进制 推荐分隔方式 示例 说明
十进制 每三位分隔 1'234'567 符合国际通用的千分位习惯
十六进制 每四位分隔 0xDEAD'BEEF 每四位对应一个字节,便于查看内存布局
二进制 每四位或八位分隔 0b1010'1010 每四位对应一个十六进制位,每八位对应一个字节
浮点数 整数部分每三位,小数部分每三位 3.141'592'653 便于阅读小数精度
3:为什么用单引号不用下划线

很多其他语言(Java、Python、JavaScript)使用下划线_作为数字分隔符,C++ 选择单引号的原因是向后兼容性

  • C++98 中已经允许在标识符中使用下划线,如果用_作为分隔符,会导致歧义(例如1_000会被解析为标识符_000和数字1
  • 单引号在 C++98 中不能出现在数字字面量中,不会有兼容性问题

3:常见陷阱与注意事项

  1. 不能使用下划线1_000是编译错误,必须用1'000
  2. 不要过度分隔:例如1'2'3'4'5反而会降低可读性
  3. 统一团队规范:在项目中明确规定不同进制的分隔方式,避免混乱
  4. 编译器支持:所有支持 C++14 的编译器都完全支持数字分隔符

3:带默认非静态成员初始化器的聚合类

1:基础语法

这是 C++14 对聚合类的最重要改进,彻底解决了 C++11 中聚合类无法使用默认成员初始化器的痛点。

1:什么是聚合类

聚合类是满足以下四个条件的类(包括结构体):

  1. 没有用户提供的构造函数
  2. 没有私有或受保护的非静态数据成员
  3. 没有基类
  4. 没有虚函数

聚合类的核心特点是可以使用花括号初始化列表直接初始化成员,例如:

// 聚合类
struct Point {
    int x;
    int y;
};

// 聚合初始化
Point p1 = {1, 2}; // C++98支持
Point p2{3, 4};    // C++11支持的统一初始化语法
2:C++14的改进:运行默认成员初始化器

C++11 及之前,聚合类不能有任何非静态成员初始化器,否则就不是聚合类。C++14 放宽了这个限制:

#include <iostream>
#include <string>

// C++14 聚合类:包含默认成员初始化器
struct Employee {
    std::string name = "Unknown"; // 默认值
    int id = -1;                  // 默认值
    double salary = 0.0;          // 默认值
};

int main() {
    // 完全初始化:所有成员都指定值
    Employee e1 = {"张三", 1001, 15000.0};

    // 部分初始化:未指定的成员使用默认值
    Employee e2{"李四"};          // name="李四", id=-1, salary=0.0
    Employee e3{"王五", 1002};    // name="王五", id=1002, salary=0.0

    // 值初始化:所有成员使用默认值
    Employee e4{};                // name="Unknown", id=-1, salary=0.0

    std::cout << e2.name << ", " << e2.id << ", " << e2.salary << "\n";
    return 0;
}
3:初始化规则
  • 初始化列表优先级高于默认值:如果初始化列表中指定了某个成员的值,就使用该值
  • 未指定的成员使用默认值:如果初始化列表中没有指定某个成员,就使用它的默认成员初始化器
  • 初始化顺序严格匹配成员声明顺序:初始化列表中的值按成员在类中声明的顺序依次赋值

2:聚合类的演进和最佳实践

1:C++17和20对聚合初始化的进一步改进

C++14 只是聚合类演进的第一步,后续版本又做了重大优化:

版本 改进内容 示例
C++17 支持基类聚合 聚合类可以有基类,只要基类也是聚合类
C++20 支持指定初始化 可以按成员名初始化,不用按顺序
C++20 支持嵌套聚合初始化 嵌套的聚合类也可以使用指定初始化

C++20 指定初始化示例(解决了必须按顺序初始化的痛点):

// C++20 支持
Employee e5{.id = 1003, .salary = 20000.0}; 
// name="Unknown", id=1003, salary=20000.0

// 嵌套聚合初始化
struct Inner {
    int a;
    int b;
};

struct Outer {
    Inner i;
    int c;
};

Outer o{.i = {.a = 1, .b = 2}, .c = 3};
2:核心应用场景

聚合类在现代 C++ 中被广泛用于以下场景:

  1. 数据传输对象(DTO):用于在函数、模块之间传递数据
  2. 配置类:存储程序的配置参数
  3. 简单值类型:如点、矩形、颜色等数学对象
  4. 与 C API 交互:可以直接传递给 C 函数,因为内存布局和 C 结构体完全一致
  5. 与 C++17 结构化绑定结合
// C++17 结构化绑定
auto [name, id, salary] = e1;
std::cout << name << "的工号是" << id << "\n";
3:为什么优先使用聚合类而不是带构造函数的类
  • 更简洁:不需要写构造函数、析构函数等样板代码
  • 更高效:编译器可以生成更优化的代码
  • 更灵活:支持部分初始化、指定初始化
  • 更安全:默认成员初始化器可以避免未初始化的垃圾值

3: 常见陷阱与注意事项

1:初始化顺序必须与声明顺序一致(C++20之前)
// 错误:初始化顺序和声明顺序不匹配
// Employee e{"张三", 15000.0, 1001}; // id=15000, salary=1001,逻辑错误
2:默认成员初始化器不能依赖其他成员
struct Bad {
    int a = 10;
    int b = a * 2; // 未定义行为!因为成员初始化顺序是a然后b,但这里依赖于a的初始化
};
3:只要写了用户定义的构造函数,就不是聚合类
struct NotAggregate {
    int x;
    int y;
    // 写了构造函数,不再是聚合类
    NotAggregate(int x_, int y_) : x(x_), y(y_) {}
};

// 错误:不能使用聚合初始化
// NotAggregate p{1, 2};
4:聚合类不能有虚函数和私有成员

否则会失去聚合类的特性

4:总结

特性 解决的问题 核心优势
二进制字面量 位操作可读性差 直接对应硬件和协议的位定义
数字分隔符 长数字难以阅读 让大数字和十六进制值一目了然
聚合类默认初始化 聚合类无法设置默认值 既保留聚合初始化的灵活性,又避免未初始化错误
  • 位操作必须使用二进制字面量:永远不要用十六进制表示位标志
  • 超过 4 位的数字必须使用分隔符:尤其是大整数、内存地址和浮点数
  • 简单数据类型优先使用聚合类:只有在需要封装或复杂初始化时才写构造函数
  • C++20 及以上优先使用指定初始化:避免因顺序错误导致的逻辑 bug
  • 统一团队代码规范:明确数字分隔符的使用方式和聚合类的设计原则
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐