特殊工具与技术

掌握内存管理底层机制、运行时类型识别(RTTI)、枚举与联合、函数指针与成员指针等C++高级特性——全书压轴章节(高级主题篇第3章)

学习目标

  • 能用 placement new 在已分配内存上构造对象并正确管理生命周期——预分配原始内存、在指定地址构造、显式析构但不 delete
  • 能用 RTTI(typeiddynamic_cast)在运行时安全地识别和转换类型——用基类指针调用派生类特有方法而不崩
  • 能用 enum class 定义类型安全的枚举,正确管理 union 活跃成员,并说明嵌套类与局部类的作用域和访问规则
  • 能声明和使用成员指针,并识别位域、volatile 与语言链接这些固有不可移植特性的边界
  • 能回答:enum class 的值为什么不能隐式转成 intdynamic_cast 失败时指针版和引用版分别发生什么?

机制总览

特殊工具与技术:机制路径

  1. 1

    为什么需要这些特殊工具

    你已经走完了 C++ 的核心旅程——变量、容器、算法、类、模板、继承。这些构成了你日常编程的 95%。但还有一批"特种工具"——它们不常用,可一旦进入某些特定领域,不用它就写不出正确代码。这些工具是 C++ 的"精密手术刀"——普通场景用不上,关键场景少了不行。

  2. 2

    placement new:在指定地址上构造对象

    普通的 new 做了两件事——先向系统要一块内存( operator new ),在这块内存上调构造函数初始化对象。两者一步完成——你写 new T() ,对象创建好、内存也分配好了, delete 时析构加释放也是一步到位。但有时你需要把这两步拆开——比如你要重复使用一块已分配的大内存,不希望每次…

  3. 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> 就是这个“只在既有存储中构造”的工具。调用者必须同时保证地址、容量、对齐和生命周期都正确。

内存管理三种方式:普通 new vs placement new栈 Stackint x = 42;自动分配,自动析构离开作用域即释放堆 Heap(普通 new)auto* p = new T();① operator new——分配内存② 构造函数——初始化对象delete p: 析构 + 释放内存预分配内存池 + Placement Newvoid* buf = operator new(N * sizeof(T));预分配一大块原始内存(无类型、未初始化)① 普通 new:operator new 分配内存 + 构造函数初始化 —— 两步合一核心差异:普通 new 分配+构造一步完成;delete 析构+释放一步完成。易用但内存频繁分配/释放——开销大。
第一步:普通 new = 分配内存 + 构造对象(两步合一)

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 的核心理念——把内存管理和对象构造分成独立的两步:

分步1 / 2

① 普通 new——分配+构造不分离

内存管理三种方式:普通 new vs placement new栈 Stackint x = 42;自动分配,自动析构离开作用域即释放堆 Heap(普通 new)auto* p = new T();① operator new——分配内存② 构造函数——初始化对象delete p: 析构 + 释放内存预分配内存池 + Placement Newvoid* buf = operator new(N * sizeof(T));预分配一大块原始内存(无类型、未初始化)① 普通 new:operator new 分配内存 + 构造函数初始化 —— 两步合一核心差异:普通 new 分配+构造一步完成;delete 析构+释放一步完成。易用但内存频繁分配/释放——开销大。
第一步:普通 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 的完整工作流程:

分步1 / 3

① 问题:基类指针看不到派生类成员

RTTI:运行时类型识别与安全向下转型类的继承关系class Base { virtual ~Base() ... }class DerivedAclass DerivedB通过基类指针操作派生类对象Base* bp = new DerivedB();// bp 是 Base*——只能调 Base 的方法① 问题:基类指针 bp 看不到派生类特有成员——编译期只知道静态类型 Base*向下或横向 dynamic_cast 的源类型必须是多态类型,常通过虚析构函数满足。
第一步:基类指针只能看到基类接口,派生类方法暂时不可见

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、传统 enumunion 在六个维度上并排对比——作用域、隐式转换、底层类型、前置声明、存储模型一目了然:

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 的三合一

下面是一个把本章三个核心概念串联起来的完整示例——一个可以安全存储三种完全不同类型值(intdoublestd::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 需要显式调析构函数——因为它自己内部还有堆上分配的内存。intdouble 是基础类型——析构等于什么都不做。

第三部分: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 类——用 typeiddynamic_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(运行时类型识别)

typeidtype_infodynamic_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,仍需遵循目标平台协议。

讨论

评论区加载中…