特殊工具与技术
掌握内存管理底层机制、运行时类型识别(RTTI)、枚举与联合、函数指针与成员指针等C++高级特性——全书压轴章节(高级主题篇第3章)
学习目标
- 能用 placement new 在已分配内存上构造对象并正确管理生命周期——预分配原始内存、在指定地址构造、显式析构但不 delete
- 能用 RTTI(
typeid和dynamic_cast)在运行时安全地识别和转换类型——用基类指针调用派生类特有方法而不崩 - 能用
enum class定义类型安全的枚举,正确管理union活跃成员,并说明嵌套类与局部类的作用域和访问规则 - 能声明和使用成员指针,并识别位域、
volatile与语言链接这些固有不可移植特性的边界 - 能回答:
enum class的值为什么不能隐式转成int?dynamic_cast失败时指针版和引用版分别发生什么?
机制总览
特殊工具与技术:机制路径
- 1
为什么需要这些特殊工具
你已经走完了 C++ 的核心旅程——变量、容器、算法、类、模板、继承。这些构成了你日常编程的 95%。但还有一批"特种工具"——它们不常用,可一旦进入某些特定领域,不用它就写不出正确代码。这些工具是 C++ 的"精密手术刀"——普通场景用不上,关键场景少了不行。
- 2
placement new:在指定地址上构造对象
普通的 new 做了两件事——先向系统要一块内存( operator new ),在这块内存上调构造函数初始化对象。两者一步完成——你写 new T() ,对象创建好、内存也分配好了, delete 时析构加释放也是一步到位。但有时你需要把这两步拆开——比如你要重复使用一块已分配的大内存,不希望每次…
- 3
RTTI:运行时的类型识别与安全导航
当程序在运行时——一个基类指针 Base bp 可能真的指向 Base 对象、也可能指向 Derived 对象、甚至指向更远的孙子类。编译期只知道 bp 的 静态类型 是 Base ——它"是什么"在编译时并没有确定。但程序运行中——你可能需要知道" bp 到底指向什么"和"能不能把它当作某种派生类来用"。
章级决策实验
特殊工具与技术:机制与证据
切换《特殊工具与技术》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么需要这些特殊工具
你已经走完了 C++ 的核心旅程——变量、容器、算法、类、模板、继承。这些构成了你日常编程的 95%。但还有一批"特种工具"——它们不常用,可一旦进入某些特定领域,不用它就写不出正确代码。这些工具是 C++ 的"精密手术刀"——普通场景用不上,关键场景少了不行。
可核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「为什么需要这些特殊工具」的契约。
学完《特殊工具与技术》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
特殊工具与技术:失效与核验
为什么需要这些特殊工具
典型失效
若把「为什么需要这些特殊工具」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「为什么需要这些特殊工具」的契约。
placement new:在指定地址上构造对象
典型失效
若把「placement new:在指定地址上构造对象」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「placement new:在指定地址上构造对象」的契约。
RTTI:运行时的类型识别与安全导航
典型失效
若把「RTTI:运行时的类型识别与安全导航」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「RTTI:运行时的类型识别与安全导航」的契约。
为什么需要这些特殊工具
你已经走完了 C++ 的核心旅程——变量、容器、算法、类、模板、继承。这些构成了你日常编程的 95%。但还有一批"特种工具"——它们不常用,可一旦进入某些特定领域,不用它就写不出正确代码。这些工具是 C++ 的"精密手术刀"——普通场景用不上,关键场景少了不行。
想象工厂流水线上有四个特种工位:精确安放站把零件精确安放在工作台上某个位置再开始加工(placement new)、身份验证站在传送带上分拣——"这个箱子里装的是精密仪器还是一般零件"并据此绕去不同车间(RTTI)、分拣模具站每个槽只能同时放一种形状的零件、上一件取出才换下一件(union)、分类标签站给零件打上防混淆的专属标签——没有标签的零件混进整条线数不清(enum class)。 还有两个辅助工具:指挥调度台上的快捷拨号键——每个键按下去不是直接打电话,而是指向某个特定人的分机号码(成员指针)——按下去就拨那个人;流水线上的隔离小舱——舱外的人只知道舱门编号,不知道里面具体放着什么机器零件(嵌套类)。
这一章解决什么问题? 把六种仅在高级场景出现的工具逐个讲透——不是让你"知道有这个东西",而是"遇到场景时知道该用哪个、怎么用、有什么坑"。你不用把这些全记住——但你需要有能力在遇到它们的时候自己读懂、自己改对。
官方主线包括控制内存分配、运行时类型识别、枚举与成员指针、嵌套类/union/局部类,以及位域等固有的不可移植特性。
placement new:在指定地址上构造对象
普通的 new 做了两件事——先向系统要一块内存(operator new),在这块内存上调构造函数初始化对象。两者一步完成——你写 new T(),对象创建好、内存也分配好了,delete 时析构加释放也是一步到位。但有时你需要把这两步拆开——比如你要重复使用一块已分配的大内存,不希望每次构造新对象都重新申请内存、析构旧对象时也不希望释放整块内存——只要析构这个对象,把位置腾出来给下一个对象用。
<Term def="标准的非分配 placement new 形式不取得新存储,而是在调用方提供且满足大小与对齐要求的地址上开始对象构造。非平凡对象结束生命周期时需调用对应析构函数,存储仍由原所有者释放。">placement new</Term> 就是这个“只在既有存储中构造”的工具。调用者必须同时保证地址、容量、对齐和生命周期都正确。
operator new 与 operator delete 的底层机制
<Term def="C++ 内存分配的低层函数——operator new(size_t) 只分配指定字节数的原始内存(void*),不调用构造函数。operator delete(void*) 只释放内存,不调用析构函数。new 表达式 = operator new + 构造函数,delete 表达式 = 析构函数 + operator delete。placement new 只调构造函数,跳过 operator new。">operator new / operator delete</Term> 就是被 new/delete 表达式在背后调用的底层内存函数。它们只关心字节——分配指定字节数的原始内存(返回 void*),或释放由 operator new 分配的内存。它们不关心类型、不调构造/析构函数。
#include <new>
// operator new——只分配原始字节,不构造对象
void* raw = ::operator new(sizeof(std::string));
// placement new——在 raw 地址上构造一个 string
std::string* s = new (raw) std::string("hello");
// 用完:必须先析构再释放
s->~string(); // 显式调析构——释放 string 管理的堆内存::
operator delete(raw); // 释放原始字节placement new 的真正价值
下面用 Stepper 展示 placement new 的核心理念——把内存管理和对象构造分成独立的两步:
① 普通 new——分配+构造不分离
new T() 一步两步合一——从堆上申请新内存、在申请到的位置构造对象。delete 也是析构+释放一步完成。易用但频繁 new/delete 时内存碎片和开销大。
RTTI:运行时的类型识别与安全导航
当程序在运行时——一个基类指针 Base* bp 可能真的指向 Base 对象、也可能指向 Derived 对象、甚至指向更远的孙子类。编译期只知道 bp 的静态类型是 Base*——它"是什么"在编译时并没有确定。但程序运行中——你可能需要知道"bp 到底指向什么"和"能不能把它当作某种派生类来用"。
<Term def="运行时类型识别机制,主要由 typeid、type_info 和 dynamic_cast 组成。对多态对象求 typeid 可得到动态类型,向下或横向 dynamic_cast 会检查目标子对象是否存在且可访问。">RTTI(运行时类型识别)</Term> 让程序在需要时查询多态对象的动态类型,或验证某条继承转换是否合法。
typeid:查询对象"你是谁"
typeid(e) 返回一个 type_info 对象的引用——描述表达式 e 的实际类型。type_info 提供 name()(返回类型名,不同编译器格式不同)、before()(排序用)、== / !=(类型比较):
#include <typeinfo>
Base* bp = new DerivedA();
if (typeid(*bp) == typeid(DerivedA))
std::cout << "bp really points to DerivedA\n";
// typeid(*bp)——解引用查动态类型(需要虚函数)
// typeid(bp)——不查动态类型,只返回 Base*<Term def="typeid 表达式返回的类型描述对象;可比较类型相等性,name 返回实现定义的字符串,before 提供实现定义的排序关系。程序通过 const 引用观察它。">type_info</Term> 的 name() 格式不具可移植性,不应作为持久化标识或协议字段。
dynamic_cast:安全向下转型
typeid 只能查询类型,<Term def="按继承关系执行受检查转换的运算符。指针向下或横向转换失败返回空指针,引用转换失败抛 bad_cast;需要运行时检查的源类型必须是多态类型。">dynamic_cast</Term> 则验证目标子对象是否存在且可访问:
Base* bp = new DerivedB();
// 指针版:失败返回 nullptr
auto* db = dynamic_cast<DerivedB*>(bp);
if (db) db->derivedOnlyMethod(); // 安全——转换成功才调用
// 引用版:失败抛 std::bad_cast
try {
DerivedB& ref = dynamic_cast<DerivedB&>(*bp);
ref.derivedOnlyMethod();
} catch (const std::bad_cast&) {
// 转换失败——处理异常
}下面用 Stepper 逐步展示 RTTI 的完整工作流程:
① 问题:基类指针看不到派生类成员
Base* bp = new DerivedB();——编译期只看到 bp 是 Base*,只能调 Base 的方法。派生类特有的方法通过 bp 直接调用——编译报错。
enum class 与 union:类型安全和共享存储
enum class:带作用域的安全枚举
C 风格的 enum 有个经典的尴尬——你可以把 Red 直接当整数写 int c = Red;,两个枚举里的 Red 会撞名。C++11 引入的 <Term def="限定作用域的枚举类型——C++11 引入。成员必须通过作用域限定符访问(如 Color::Red),不能隐式转为整数(需 static_cast),默认底层类型为 int 且可显式指定(如 enum class Perm : uint8_t)。支持前置声明。比传统 enum 更类型安全——是 C++ 推荐的新代码默认选择。">enum class</Term>(限定作用域枚举)解决了这两个痛点——成员必须写 Color::Red(不怕重名),不能偷偷转成 int(不经过 static_cast 就不通过编译):
// 传统 enum:成员暴露、可隐式转 int
enum TraditionalColor { Red, Green, Blue };
int c = Red; // 编译通过——Red 隐式转 0
// enum 不加 class 直接在当前作用域创建 Red/Green/Blue——两个枚举有同名成员会撞
// enum class:限定作用域、不能隐式转换
enum class SafeColor { Red, Green, Blue };
auto sc = SafeColor::Red; // "Red" 在 SafeColor 作用域内——不污染全局
int x = static_cast<int>(sc); // 转整数必须显式写——不写就编译报错enum class 允许显式指定底层类型——用 : type 语法。这带来了两个好处:你能控制枚举在内存中占多大(uint8_t 只占 1 字节),还能在没看到所有枚举值的情况下前置声明:
enum class Permission : uint8_t { Read = 1, Write = 2, Exec = 4 };
// sizeof(Permission) == 1——省内存
// 前置声明:enum class Permission : uint8_t;——可在头文件中只声明不定义下面这张图把 enum class、传统 enum、union 在六个维度上并排对比——作用域、隐式转换、底层类型、前置声明、存储模型一目了然:
union:所有成员共享同一块内存
<Term def="所有非静态数据成员共享存储的特殊类类型。一次通常只有一个活跃成员;包含非平凡成员时,程序必须明确开始和结束对应对象生命周期。联合体大小足以容纳最大成员并可能包含对齐填充。">union(联合体)</Term> 的所有非静态数据成员从同一地址开始,整体大小足以容纳任一成员,并可能因对齐大于最大成员的裸大小:
union IntOrDouble {
int i;
double d;
};
// sizeof(IntOrDouble) == sizeof(double) == 8(兼容最大的成员)
// i 和 d 从同一个地址开始——写入 i 后再读 d 得到的是覆盖后的垃圾值
IntOrDouble u;
u.i = 42; // 活跃成员是 i
u.d = 3.14; // 活跃成员变成 d——i 的值被覆盖
// std::cout << u.i; // 未定义行为!读的不是活跃成员C++11 的重要改进——union 可以包含有非平凡特殊成员函数的类型(比如 std::string)。但这意味着你得手动管理生命周期——用 placement new 构造、显式调用析构、用额外的 tag(标签)变量追踪当前活跃的成员:
class Token {
enum class Tag { INT, DOUBLE, STRING } tag;
union {
int i;
double d;
std::string s; // C++11: union 可以有非平凡成员
};
public:
Token(int v) : tag(Tag::INT) { new (&i) int(v); }
Token(const std::string& v) : tag(Tag::STRING) { new (&s) std::string(v); }
~Token() {
if (tag == Tag::STRING) s.std::string::~string(); // 只析构 string
// int 和 double 是 trivial 类型——无需析构
}
Token(const Token&) = delete; // 简化为不可拷贝——实际产品需实现深拷贝
};成员指针与嵌套类
成员指针:与对象结合后定位成员
<Term def="表示某个类的数据成员或成员函数,不绑定具体对象。必须用点星或箭头星运算符与兼容对象结合后才能访问或调用;其内部表示由实现决定,不保证等于字节偏移或普通函数地址。">成员指针(pointer to member)</Term> 不是普通地址。它编码“类的哪个成员”,只有和具体对象结合才产生一次访问或调用:
struct Widget {
int value;
void show() const { std::cout << value << '\n'; }
};
// 声明成员指针——不指定具体对象
int Widget::*pValue = &Widget::value; // 数据成员指针
void (Widget::*pShow)() const = &Widget::show; // 成员函数指针
Widget w{42};
w.*pValue = 100; // 通过对象使用数据成员指针
(w.*pShow)(); // 通过对象调用成员函数——输出 100
Widget* pw = &w;
pw->*pValue = 200; // 通过指针使用——->* 操作符
(pw->*pShow)(); // 输出 200成员函数指针最实用的场景——回调表。把函数名到成员函数指针的映射存起来,运行时按名字查找并调用:
#include <map>
#include <string>
class Menu {
using Action = void (Menu::*)();
std::map<std::string, Action> actions;
public:
void add(const std::string& name, Action act) { actions[name] = act; }
void invoke(const std::string& name) {
auto it = actions.find(name);
if (it != actions.end()) (this->*(it->second))();
}
void save() { std::cout << "save\n"; }
void load() { std::cout << "load\n"; }
};
// Menu m; m.add("save", &Menu::save); m.invoke("save");嵌套类:把辅助类型藏在类内部
<Term def="定义在另一个类作用域内的独立类。嵌套类成员可访问外层类私有成员,但没有隐式外层对象;它仍需通过对象、指针或引用访问非静态成员。外层类不会反向自动获得嵌套类私有访问权。">嵌套类(nested class)</Term> 把辅助类型的名字放进外层类作用域。它拥有访问外层私有成员的权限,但不像某些语言的内部类那样自动绑定某个外层实例:
class BinaryTree {
struct Node { // 嵌套类——外部不可见
int data;
Node* left = nullptr;
Node* right = nullptr;
Node(int v) : data(v) {}
};
Node* root = nullptr;
public:
void insert(int v) { /* 用 Node 构建树 */ }
bool find(int v) const { /* 在树中查找 */ }
};
// 外部代码完全不知道 Node 的存在——只能通过 BinaryTree 的接口操作嵌套类适合迭代器、节点和构建器等辅助类型。私有 Node 不进入全局作用域;若它需要读取某棵树的非静态状态,仍要显式接收 BinaryTree& 或指针。
局部类:把类型限制在一个函数中
<Term def="定义在函数体内的类,其类型名只在所在局部作用域可见。局部类不能含静态数据成员,也不能直接使用函数的普通自动局部变量;可使用类型名、静态变量和枚举常量。">局部类(local class)</Term> 适合只服务一个函数的短小策略或适配器。它不能捕获外层函数变量;需要状态时应通过构造函数存入成员。与 lambda 相比,局部类更冗长,但可以显式声明多个成员函数和类型。
int count_long_names(const std::vector<std::string> &names) {
class LongerThan {
std::size_t limit_;
public:
explicit LongerThan(std::size_t n) : limit_(n) {}
bool operator()(const std::string &s) const {
return s.size() > limit_;
}
} predicate(8);
return std::count_if(names.begin(), names.end(), predicate);
}固有的不可移植特性:先确认 ABI 边界
<Term def="类成员声明中用冒号和常量表达式指定宽度的成员。位域可紧凑表示协议或设备寄存器字段,但分配顺序、对齐、跨存储单元方式以及普通 int 的有符号性由实现决定,也不能取得位域地址。">位域(bit-field)</Term> 看起来适合直接映射二进制格式,但布局并不跨编译器、目标架构或 ABI 稳定。持久化文件和网络协议通常应使用整数掩码、移位与显式字节序;只有目标 ABI 明确规定时才用位域映射硬件或系统结构。
struct DeviceFlags {
unsigned ready : 1;
unsigned mode : 3;
unsigned error : 1;
};
// sizeof、位的左右顺序和填充都不能据此跨平台推断<Term def="类型限定符,表示对象可能被当前执行流之外的机制修改,因此每次表达式求值都要遵守 volatile 访问规则。它不提供原子性、线程同步或内存顺序。">volatile 限定符</Term> 常用于实现定义的内存映射 I/O 或信号处理场景,不能替代 std::atomic。多线程共享数据仍需要原子类型或互斥同步。
<Term def="声明函数名和函数类型采用指定语言的链接约定。C 语言链接常用于调用 C ABI,但参数类型、结构布局和二进制兼容仍需平台协议共同约束。">语言链接(language linkage)</Term> 通过 extern "C" 等语法声明。它主要影响函数类型与名字链接规则,不会把 C++ 函数体变成 C,也不保证任意 C++ 类型能跨 C ABI 传递。
实战:写一个 TaggedPointer——placement new + RTTI + union 的三合一
下面是一个把本章三个核心概念串联起来的完整示例——一个可以安全存储三种完全不同类型值(int、double、std::string)的对象,通过 RTTI 在运行时获取当前存储的是什么类型。这其实就是 std::variant 的极简手工版——你敲完这段代码就能理解 variant 内部的大概原理。
第一部分:用 union 共享内存 + Tag 追踪类型
#include <string>
#include <new> // placement new
#include <typeinfo> // RTTI
#include <stdexcept>
class TaggedPointer {
enum class Type { NONE, INT, DOUBLE, STRING } tag_ = Type::NONE;
union Data {
int i;
double d;
std::string s;
// C++11: union 可以有非平凡成员——但要手动管理生命周期
Data() {} // union 含非平凡成员(string)时——必须自己声明构造/析构
~Data() {} // 否则 union 的构造/析构被隐式删除,外层类无法编译
} data_;union 确保三种类型的值共享同一块内存——任何时候只有一个是"活跃"的。tag_ 用 enum class 记录当前活跃成员是哪个——防止你错读成员引发未定义行为。
第二部分:placement new 构造 + 显式析构管理生命周期
public:
TaggedPointer(int v) : tag_(Type::INT) { new (&data_.i) int(v); }
TaggedPointer(double v) : tag_(Type::DOUBLE) { new (&data_.d) double(v); }
TaggedPointer(const std::string& v) : tag_(Type::STRING) {
new (&data_.s) std::string(v); // placement new 构造 string
}
~TaggedPointer() {
if (tag_ == Type::STRING) data_.s.std::string::~string();
// int 和 double 是 trivial 类型——不需要显式析构
}new (&data_.i) int(v)——placement new 在 data_.i 的地址上直接构造 int。析构时只有 string 需要显式调析构函数——因为它自己内部还有堆上分配的内存。int 和 double 是基础类型——析构等于什么都不做。
第三部分:RTTI——用 typeid 安全获取类型
const std::type_info& type() const {
switch (tag_) {
case Type::INT: return typeid(data_.i);
case Type::DOUBLE: return typeid(data_.d);
case Type::STRING: return typeid(data_.s);
default: return typeid(void);
}
}
int& get_int() {
if (tag_ != Type::INT) throw std::bad_cast();
return data_.i;
}
double& get_double() {
if (tag_ != Type::DOUBLE) throw std::bad_cast();
return data_.d;
}
std::string& get_string() {
if (tag_ != Type::STRING) throw std::bad_cast();
return data_.s;
}
};type() 用 RTTI 对外报告类型,三个访问器则以 tag_ 验证活跃成员。C++11 没有 if constexpr,显式访问器能让每条返回语句保持单一静态类型,也不会实例化无效分支。
使用时——同一个对象在不同时刻可以存不同类型,通过 typeid 在运行时探查实际类型:
TaggedPointer tp(42);
std::cout << tp.type().name() << '\n'; // "int"(或编译器特定格式)
std::cout << tp.get_int() << '\n'; // 42——安全,tag 匹配
// 另建一个存 string 的对象——本类未实现拷贝/移动赋值,不能写 tp = ...
TaggedPointer tp2(std::string("hello")); // 直接构造一个存 string 的对象
std::cout << tp2.get_string() << '\n'; // "hello"
// tp2.get_int(); // 抛出 bad_cast——tp2 当前存的是 string 不是 int这个示例囊括了本章四分之三的技术——union 共享内存 + enum class 追踪活跃成员 + placement new 手动管理生命周期 + RTTI 查询类型。你写一遍这段代码,这四种工具的用法就全都握在手里了。
容易踩的坑
小结
- 存储与生命周期:
operator new/delete管原始存储,placement new 在满足大小与对齐的地址构造对象;非平凡对象先结束生命周期,再由原所有者释放存储 - RTTI:对多态对象用
typeid查询动态类型,用dynamic_cast做受检查的向下或横向转换;空指针和失败路径必须显式处理 - 枚举、union 与成员指针:限定作用域枚举提供类型边界,tag 管理 union 活跃成员,成员指针要与兼容对象结合,内部表示不等于普通地址
- 嵌套类与局部类:嵌套类可访问外层私有成员但没有隐式外层对象;局部类只在函数作用域可见且不能捕获普通自动变量
- 不可移植边界:位域布局依赖实现,
volatile不提供线程同步,语言链接还需 ABI 协议配合
练习
问题 1(改代码型) 下面的代码在 delete 处有问题——找出并写出正确的析构方法。
#include <new>
#include <string>
void* buf = ::operator new(1024);
auto* s = new (buf) std::string("hello");
*s = "world";
delete s; // 这行有什么问题?问题 2(独立实现题) 写一个 TypeChecker 类——用 typeid 和 dynamic_cast 实现。有一个 set(Base*) 方法存基类指针,提供 is_type<T>() 和 as<T>(),并处理空指针和类型不匹配。
问题 3(改代码型) 下面的代码把 enum class 当传统 enum 用了——找出三个错误并修正。
enum class Level { Low = 1, Medium = 5, High = 10 };
void print_bonus(Level lv) {
int bonus = lv * 100; // 错误 1
if (lv == High) // 错误 2
std::cout << "Max bonus\n";
}
int main() {
Level l = 1; // 错误 3
}名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- placement new
标准的非分配 placement 形式在调用方提供的地址构造对象。调用方负责大小、对齐和存储所有权;非平凡对象结束生命周期时需显式析构,但不能用
delete释放不属于该对象的独立分配。- operator new / operator delete
C++ 内存分配的低层函数。
operator new(size_t)只分配指定字节数的原始内存(返回void*),不调用构造函数。operator delete(void*)只释放内存,不调用析构函数。new表达式 =operator new+ 构造函数,delete表达式 = 析构函数 +operator delete。placement new 跳过了operator new——只做构造这一步。- RTTI(运行时类型识别)
由
typeid、type_info和dynamic_cast组成的运行时类型识别机制。对多态表达式求typeid可得到动态类型;需要运行时检查的向下或横向转换要求源类型多态。- type_info
std::type_info类——存储 C++ 类型的标识信息。通过typeid获取引用。提供name()返回类型名(不同编译器格式不同——只用于调试),before()提供实现定义的排序,==/!=比较两个type_info是否描述同一个类型。不能拷贝、不能移动——只能从typeid取得引用。- dynamic_cast
按继承关系执行受检查转换的运算符。指针向下或横向转换失败返回
nullptr,引用转换失败抛std::bad_cast;需要运行时检查的源类型必须是多态类型。- enum class
C++11 引入的限定作用域枚举。成员必须通过作用域前缀访问(
Color::Red),不能隐式转为整数(必须static_cast<int>),默认底层类型是int(可显式指定,如: uint8_t),支持前置声明。比传统 C 风格enum更类型安全、作用域更清晰。新代码一律用enum class——老代码中的传统enum只在维护时碰到。- union(联合体)
所有非静态数据成员共享存储的类类型。整体大小足以容纳任一成员并可能含对齐填充;非平凡成员要求程序显式追踪活跃成员并管理构造、拷贝、赋值和析构。
- 成员指针(pointer to member)
表示某个类的数据成员或成员函数,但不绑定具体对象。必须通过
.*或->*与兼容对象结合;内部表示由实现决定,不保证是字节偏移或普通函数地址。- 嵌套类(nested class)
定义在另一个类作用域内的独立类。嵌套类成员可访问外层类私有成员,但没有隐式外层对象;外层类也不会反向自动获得嵌套类私有访问权。
- 局部类(local class)
定义在函数体内、类型名只在局部作用域可见的类。它不能含静态数据成员,也不能像 lambda 一样捕获普通自动局部变量,需要通过构造参数保存状态。
- 位域(bit-field)
用冒号和常量宽度声明的类成员。分配顺序、对齐、是否跨存储单元以及普通
int的有符号性由实现决定,且不能取得位域地址。- volatile 限定符
表示对象可能被当前执行流之外的机制修改,并约束表达式中的访问。它不提供原子性、线程同步或内存顺序,不能替代
std::atomic。- 语言链接(language linkage)
指定函数名和函数类型采用某种语言链接约定,例如 C 语言链接。它不保证结构布局或任意 C++ 类型能跨 ABI,仍需遵循目标平台协议。