特殊工具与技术

这部分内容的特点是:

  • 不一定天天写
  • 但体现 C++ 的底层能力和语言灵活性
  • 很适合出概念题、判断题、细节题

复习策略:

先抓用途,再抓语法,再抓限制

控制内存分配

C++ 中对象通常通过以下方式创建:

  • 自动对象:定义在函数内部,离开作用域自动销毁
  • 静态对象:程序开始时创建,程序结束时销毁
  • 动态对象:通过 new 分配,通过 delete 释放

所谓“控制内存分配”,主要就是指:

自定义对象的分配和释放行为

newdelete 的基本过程

1
2
T *p = new T;
delete p;

new T 一般做两件事:

  1. 分配原始内存
  2. 调用构造函数构造对象

delete p 一般做两件事:

  1. 调用析构函数销毁对象
  2. 释放内存

为什么要控制内存分配

某些场景下,我们希望:

  • 提高效率
  • 使用专用内存池
  • 跟踪内存使用
  • 限制对象只能在堆上/栈上创建
  • 调试内存泄漏

于是可以通过重载 new / delete 来改变默认行为。

重载 newdelete

基本形式

类可以定义自己的 operator newoperator delete

1
2
3
4
5
class Foo {
public:
static void* operator new(std::size_t size);
static void operator delete(void* p);
};

注意:

这两个函数通常是 static
实际上即使不显式写 static,它们本质上也不依赖对象

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
class Foo {
public:
static void* operator new(size_t size) {
cout << "custom new\n";
return ::operator new(size); // 调用全局 operator new
}

static void operator delete(void* p) {
cout << "custom delete\n";
::operator delete(p); // 调用全局 operator delete
}
};

使用:

1
2
Foo* p = new Foo;
delete p;

作用范围

全局重载

可以重载全局 operator new / operator delete,影响全局动态分配。

类内重载

只影响该类对象的动态分配。

不能改变的部分

重载 operator new / operator delete 改变的是:

  • 分配/释放内存的方式

但不能改变:

  • new 先分配后构造 的整体语义
  • delete 先析构后释放 的整体语义

new 失败时

标准 operator new 分配失败时通常抛出:

1
bad_alloc

例如:

1
2
3
4
5
6
try {
int* p = new int[1000000000000];
}
catch (const bad_alloc& e) {
cout << e.what() << endl;
}

nothrow 版本

如果不想抛异常,可以写:

1
2
3
4
int* p = new (nothrow) int[100];
if (!p) {
// 分配失败
}

需要头文件:

1
#include <new>

placement new(定位 new)

这是控制内存分配中的重点。

placement new 的作用:

在一块已经分配好的原始内存上构造对象

例如:

1
2
3
4
#include <new>

char buffer[sizeof(int)];
int* p = new (buffer) int(42);

这里:

  • buffer 提供原始内存
  • new (buffer) int(42) 在这块内存上构造一个 int

placement new 的特点

它:

  • 不会分配新内存
  • 只是在指定地址上构造对象

所以对应地:

对 placement new 构造的对象,不能直接用普通 delete

应显式调用析构函数(若是类类型):

1
p->~T();

普通 new

  • 分配内存
  • 调构造函数

placement new

  • 不分配内存
  • 只在指定地址构造对象

限制对象分配方式(了解)

有时可以通过把构造函数/析构函数/operator new 设为 private,控制对象只能:

  • 在堆上创建
  • 在栈上创建
  • 不能随意创建

这是工程技巧,复习知道即可。

运行时类型识别(RTTI)

RTTI = Run-Time Type Identification

作用:

在程序运行时识别对象的实际类型

C++ 主要提供两种 RTTI 工具:

  • dynamic_cast
  • typeid

dynamic_cast

dynamic_cast 主要用于:

在继承体系中进行安全的类型转换

通常用于:

  • 基类指针/引用 转换为 派生类指针/引用
  • 多态体系中的向下转型

基本要求

使用 dynamic_cast 的前提:

基类必须含有至少一个虚函数

也就是说,要有多态性。

例如:

1
2
3
4
5
6
class Base {
public:
virtual ~Base() {}
};

class Derived : public Base {};

指针形式

1
2
Base* pb = new Derived;
Derived* pd = dynamic_cast<Derived*>(pb);

如果转换成功:

  • pd 指向派生类对象

如果失败:

  • pd == nullptr

引用形式

1
2
Base& rb = d;
Derived& rd = dynamic_cast<Derived&>(rb);

若失败:

  • 会抛出 bad_cast 异常

所以:

指针失败返回空指针
引用失败抛异常

为什么比 static_cast 安全

static_cast 不做运行时检查,
dynamic_cast 会检查对象真实类型。

因此在多态继承体系中,向下转型更安全。

典型用途

1
2
3
if (Derived* p = dynamic_cast<Derived*>(basePtr)) {
p->derivedFunc();
}

typeid

typeid 用于获取表达式的类型信息。

例如:

1
cout << typeid(42).name() << endl;

或者:

1
2
Base* p = new Derived;
cout << typeid(*p).name() << endl;

动态类型与静态类型

如果操作对象本身,得到静态类型。
若操作的是多态类型对象的解引用,则可能得到动态类型。

例如:

1
2
Base* p = new Derived;
typeid(*p)

Base 是多态类型,则得到 Derived 的类型信息。

type_info

typeid 返回对 type_info 对象的引用,可用于:

  • 比较类型是否相同
  • 获取类型名字(实现相关)

例如:

1
2
3
if (typeid(*p) == typeid(Derived)) {
cout << "is Derived\n";
}

RTTI 的复习重点

  • dynamic_cast:安全向下转型
  • typeid:获取运行时类型信息
  • 前提通常是 多态类型

枚举类型

枚举(enum)用于定义一组相关的命名常量。

传统枚举

1
enum color { red, yellow, green };

此时:

  • red = 0
  • yellow = 1
  • green = 2

也可显式赋值:

1
enum color { red = 1, yellow = 3, green = 5 };

枚举的作用

相比直接写数字常量:

  • 语义更清晰
  • 更便于维护
  • 可限制取值范围

作用域问题

传统枚举的枚举值会暴露到外层作用域:

1
2
enum color { red, green };
int red = 10; // 可能冲突

这是传统枚举的一个缺点。

作用域枚举(强类型枚举)

C++11 引入:

1
enum class Color { red, yellow, green };

使用时必须加作用域:

1
Color c = Color::red;

强类型枚举的优点

  • 枚举值不泄漏到外层作用域
  • 类型更安全
  • 不会隐式转换为整数

例如:

1
2
Color c = Color::red;
// int x = c; // 错误,不能隐式转 int

如需转换:

1
int x = static_cast<int>(c);

指定底层类型

1
enum class Color : unsigned int { red, green, blue };

复习建议

考试里通常更推荐记:

现代 C++ 优先使用 enum class

类成员指针

类成员指针是比较容易混的一块。

它不是“指向对象的指针”,而是:

指向类成员的指针

包括:

  • 数据成员指针
  • 成员函数指针

数据成员指针

定义形式

1
2
3
4
5
6
class Screen {
public:
string contents;
};

string Screen::*pdata = &Screen::contents;

含义:

  • pdata 是一个指向 Screen 类中 string 成员的指针

使用方式

需要结合对象或对象指针使用:

1
2
3
4
Screen myScreen;
myScreen.contents = "hello";

cout << myScreen.*pdata << endl;

如果是对象指针:

1
2
Screen* p = &myScreen;
cout << p->*pdata << endl;

成员函数指针

定义形式

1
2
3
4
5
6
class Screen {
public:
char get() const { return 'A'; }
};

char (Screen::*pmf)() const = &Screen::get;

这类语法比较复杂,复习时重点识别结构。

调用方式

1
2
Screen s;
cout << (s.*pmf)() << endl;

若是对象指针:

1
2
Screen* p = &s;
cout << (p->*pmf)() << endl;

注意括号不能省。

本质理解

成员函数指针不是普通函数指针,因为成员函数调用需要隐含对象。

所以它必须和某个对象结合使用。

复习小结

成员指针必须依附对象使用:.*->*

嵌套类

一个类可以定义在另一个类内部,这叫:

嵌套类(nested class)

基本形式

1
2
3
4
5
6
7
class Outer {
public:
class Inner {
public:
void f() {}
};
};

使用时:

1
2
Outer::Inner obj;
obj.f();

作用

嵌套类常用于:

  • 表示某个类的辅助类型
  • 隐藏实现细节
  • 表达“只和外部类相关”的类型

例如迭代器类、辅助节点类等。

访问关系

嵌套类和外层类是两个独立的类。
它们不会自动互相访问私有成员,是否能访问取决于访问控制和友元关系。

这点很容易误解。

复习小结

嵌套类只是“定义位置在类内部”,并不意味着天然拥有外层类对象或全部访问权限。

union(一种节省空间的类)

union 是一种特殊的类,特点是:

所有成员共享同一块内存

因此在任意时刻,通常只有一个成员处于有效状态。

基本定义

1
2
3
4
5
union Token {
char cval;
int ival;
double dval;
};

这几个成员共享同一块存储空间。

大小特点

union 的大小至少等于其最大成员的大小。

例如:

  • char
  • int
  • double

union 通常至少和 double 一样大。

使用示例

1
2
3
Token t;
t.ival = 42;
cout << t.ival << endl;

然后若写:

1
t.dval = 3.14;

则原来的 ival 不再是有效值。

为什么节省空间

因为多个成员共用一块内存,而不是各自独立存储。

适合在“同一时刻只需要其中一种表示”的场景中使用。

union 与类

union 也可以有:

  • 成员函数
  • 构造函数
  • 析构函数
  • 访问控制符

但限制比普通类更多。

含有类类型成员时

如果 union 的成员含有类类型,尤其是带构造/析构的类型,管理会更复杂。
现代 C++ 中通常需要手动控制活动成员。

这部分属于进阶内容,复习时知道:

union 对非平凡类型的管理更复杂

匿名 union

可以定义匿名 union

1
2
3
4
union {
int i;
double d;
};

其成员可直接访问:

1
i = 10;

但匿名 union 不能有成员函数,且成员必须是公有的。

现代替代

很多时候,现代 C++ 更推荐:

  • variant(C++17)
  • 类层次结构
  • 带标签的联合体设计

不过考试里仍常考 union 的基本概念。

小结

union 的所有成员共享内存,适合“多种表示中任一时刻只用一种”的场景。

局部类

在函数内部定义的类叫:

局部类(local class)

基本形式

1
2
3
4
5
6
7
8
9
10
11
void f() {
class Local {
public:
int x;
void print() { cout << x << endl; }
};

Local obj;
obj.x = 10;
obj.print();
}

特点

局部类:

  • 只在定义它的局部作用域中可见
  • 主要用于某些局部辅助逻辑

限制

局部类和局部作用域关系密切,因此限制较多。
经典规则是:

局部类中的成员函数一般不能直接使用所在函数的普通局部变量

例如:

1
2
3
4
5
6
7
8
9
void f() {
int a = 10;
class Local {
public:
void g() {
// cout << a; // 不允许直接访问外层普通局部变量
}
};
}

原因是类成员函数不是外层函数的局部语句块一部分。

小结

局部类只在局部作用域可见,但不能随意直接访问外层函数的局部变量。

固有的不可移植的特性

C++ 提供了一些接近机器底层、与平台实现相关的特性。
这些特性能力强,但:

可移植性差,不同平台/编译器行为可能不同

因此称为“固有的不可移植的特性”。

常见内容

通常包括:

  • 位域(bit-field)
  • volatile
  • 链接指示(如 extern "C"

有些教材会把它们放在这一节中统一讲。

位域(bit-field)

位域允许把一个整数成员限定为若干二进制位。

基本写法

1
2
3
4
5
6
7
struct File {
unsigned mode : 2;
unsigned modified : 1;
unsigned prot_owner : 3;
unsigned prot_group : 3;
unsigned prot_world : 3;
};

这里每个成员只占若干位。

作用

位域适合:

  • 压缩存储
  • 表示状态标志
  • 与硬件/协议字段对应

注意点

位域的布局、对齐方式常与编译器和平台有关,
因此可移植性较差。

volatile

volatile 表示:

该对象的值可能在程序控制之外被改变

例如:

  • 硬件寄存器
  • 中断服务程序修改的变量
  • 多线程早期低级同步场景(现代一般不用它做线程同步)
1
volatile int flag;

含义是告诉编译器:

  • 不要假设它的值不会突然变化
  • 不要进行某些过度优化

重要说明

volatile 不等于线程安全
现代 C++ 线程同步应使用:

  • atomic
  • 互斥锁
  • 条件变量

所以复习时记住:

volatile 主要是告诉编译器“这个值可能被外部改变”,不是并发同步工具。

链接指示:extern "C"

C++ 支持函数重载、名字修饰(name mangling),
但 C 语言没有这些机制。

为了让 C++ 代码能和 C 代码链接,使用:

1
extern "C"

基本形式

1
extern "C" int strcmp(const char*, const char*);

表示该函数按 C 的链接方式处理。

也可用于一组声明:

1
2
3
4
extern "C" {
int f1(int);
double f2(double);
}

用途

  • 调用 C 库函数
  • C 与 C++ 混合编程
  • 避免 C++ 名字改编导致链接失败

限制

由于 C 不支持函数重载,因此 extern "C" 的函数也不能靠 C++ 重载机制区分。

本章联系图

可以把本章理解成三类工具:

偏底层控制

  • 控制内存分配
  • union
  • 位域
  • volatile

偏类型系统

  • RTTI
  • 枚举
  • 类成员指针

偏类设计结构

  • 嵌套类
  • 局部类

易错点总结

重载 operator new 改变的是内存分配方式,不改变 new 的基本语义

placement new 不分配内存,只在指定地址构造对象

dynamic_cast 一般要求基类是多态类型

即至少有一个虚函数。

dynamic_cast 指针失败返回 nullptr,引用失败抛异常

typeid(*p) 只有在多态类型下才会体现动态类型

传统 enum 枚举值会泄漏到外层作用域

现代 C++ 更推荐 enum class

成员指针不是普通指针,必须和对象一起用:.*->*

嵌套类不等于自动拥有外层类全部访问能力

union 的成员共享内存,同一时刻通常只有一个有效成员

局部类不能随意直接使用外层函数的普通局部变量

volatile 不是线程同步工具

extern "C" 用于按 C 方式链接

结论速记

控制内存分配

可通过重载 new/delete 或 placement new 改变对象构造所用内存方式

RTTI

dynamic_cast 用于安全向下转型,typeid 用于获取运行时类型信息

枚举

enum 定义命名常量组,现代更推荐 enum class

类成员指针

指向类成员而不是对象本身,使用时必须依附对象

嵌套类

定义在类内部的类,常作辅助类型

union

所有成员共享同一块内存,用空间换表达方式

局部类

定义在函数内部,只在局部作用域可见

不可移植特性

包括位域、volatileextern "C" 等与平台/实现关系较强的机制

总结

这一章介绍了 C++ 中一些偏底层、偏特殊的机制:可以自定义内存分配,可以在运行时识别对象类型,可以用枚举组织常量,用成员指针描述类成员,用嵌套类和局部类组织结构,用 union 节省空间,还能借助位域、volatileextern "C" 处理更接近底层和平台相关的问题。