c++基础语法
构造函数
//入门写法
Point(int xVal, int yVal) {
x = xVal; // 默认构造 x(0) -> 赋值 x(xVal)
y = yVal; // 默认构造 y(0) -> 赋值 y(yVal)
}
为何这个说是入门的写法,有如下原因:
- 编译器必须先隐式调用 x 和 y 的默认构造函数(把 0 塞进去),然后再执行 = 进行赋值操作。做了两次多余的工作(先构造,再覆盖)。
- 语法限制:有些变量“不赋值就没法用”,如果成员是 const 常量、引用(&),或者是没有默认构造函数的自定义类对象,它们必须在定义时被初始化,不能在后面赋值。
class Player { const int id; // const 常量 int& gold; // 引用 public: Player(int uid, int g) { id = uid; // ❌ 报错!const 变量不能被赋值 gold = g; // ❌ 报错!引用必须一开始就绑定 } };
所以c++提供了一个更高效的写法,这个只针对构造函数有效
Point(int xVal, int yVal) : x(xVal), y(yVal) {}
这个冒号 : 引导的语法叫做“成员初始化列表”
数据类型
基础数据类型


派生数据类型

标准库

c++11新增数据类型

动态转换
动态转换是 C++ 中用于在继承层次结构中进行向下转换的一种机制。
为何引入
在面向对象设计中,经常会出现这种情况:
-
你有一个基类指针(比如 Entity*),它可能指向任意派生类对象(Player*、Monster*、NPC*)。
-
你想调用派生类特有的方法(比如 Player::useSkill()),但基类接口中没有这个方法。
-
你不能盲目地用 static_cast 强制转换,因为万一指针指向的不是 Player,程序就会崩溃。
动态转换就是为了解决这个“向下转型”的安全性问题而引入的。 它在运行时检查对象的实际类型,如果转换合法则返回有效指针,否则返回 nullptr(对指针)或抛出 std::bad_cast 异常(对引用)。
游戏中引用场景举例
假设你有一个通用的 Task 基类,派生出 AttackTask、MoveTask、HealTask。任务管理器持有 Task* 列表,在处理时需要根据实际类型做不同处理:
class Task {
public:
virtual ~Task() = default;
virtual void execute() = 0;
};
class AttackTask : public Task {
public:
void execute() override { /* 攻击逻辑 */ }
void setTarget(int targetId) { /* 设置目标 */ }
};
// 在任务调度器中
void dispatch(Task* task) {
task->execute(); // 虚函数,多态调用,没问题
// 但如果需要调用 AttackTask 特有的 setTarget()
AttackTask* atk = dynamic_cast<AttackTask*>(task);
if (atk) {
atk->setTarget(123);
} else {
// 不是攻击任务,忽略或报错
}
}
如果没有动态转换,你就得用 typeid 或自己维护类型枚举,代码会变得脆弱且难以维护。
注意事项
-
必须有虚函数:dynamic_cast 依赖虚函数表,基类至少要有一个虚函数(通常是虚析构函数)。
-
性能开销:运行时类型检查有少量成本,但在游戏后端中通常可接受,不建议在高频循环中使用。
-
失败处理:
-
对指针:转换失败返回 nullptr,一定要检查。
-
对引用:转换失败抛出 std::bad_cast,需要捕获。
-
-
不要滥用:能用虚函数解决的问题尽量用虚函数,dynamic_cast 意味着设计上可能需要重构(比如违反开闭原则)。
常量转换
常量转换用于将 const 类型的对象转换为非 const 类型的对象。
常量转换只能用于转换掉 const 属性,不能改变对象的类型。
重新解释转换
重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。
int i = 10;
float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型
不同转换方式的对比
存储类
为何要有存储类
程序需要对变量的生命周期、作用域和链接性进行精细控制,以满足不同的编程场景。
常用类型
现在市面上基本上都是c++11以上的版本,auto和register基本上很少使用,17版本还废弃了register。所以下面就列出常用的
| 存储类 | 解决需求 | 典型场景 |
|---|---|---|
| static | 控制变量的生命周期(延长到程序结束)和链接性(内部链接)。 | • 函数内 static 局部变量:只初始化一次,保存状态。文件作用域 static 全局变量:仅当前文件可见。• 类 static 成员:所有对象共享一份。 |
| extern | 声明变量在其他编译单元定义,实现跨文件共享。 | 全局配置、日志级别等在多个 .cpp 中访问。 |
| mutable | 允许在 const 成员函数中修改某些成员变量。 | 缓存、互斥锁、引用计数等逻辑上的“非状态”数据。 |
| thread_local(C++11) | 每个线程拥有独立的变量副本,生命周期与线程相同。 | 线程局部缓存、每线程随机种子、避免锁争用。 |
游戏业务中都怎么使用
全局配置
config.h – 声明
#pragma once
// 全局配置,在 config.cpp 中定义,其他文件通过 extern 声明访问
struct ServerConfig {
int port = 8888;
int maxPlayers = 4096;
};
extern ServerConfig g_serverConfig; // 声明:告诉编译器这个变量在别处定义
config.cpp – 定义
#include "config.h"
// 定义全局配置对象,外部链接,整个项目可见
ServerConfig g_serverConfig = { 8888, 8192 };
玩家管理器
#include <iostream>
#include <thread>
#include "config.h"
// 一个简单的玩家结构
struct Player {
int id;
int score;
// mutable 成员:允许在 const 成员函数中修改
mutable int cachedExp; // 缓存的经验值(仅用于演示 mutable)
};
// 创建一个新玩家,并返回指向它的指针
Player* createPlayer(int id, int score) {
// static 局部变量:只初始化一次,每次调用都会递增
static int totalPlayers = 0; // 记录总共创建了多少玩家
totalPlayers++;
// 动态分配一个玩家(为了简单,用 new,实际项目中应考虑智能指针)
Player* p = new Player;
p->id = id;
p->score = score;
p->cachedExp = 0; // 初始缓存为 0
std::cout << "Created player #" << totalPlayers
<< " (id=" << id << ", score=" << score << ")\n";
return p;
}
// 一个 const 成员函数(演示 mutable)
void printPlayerInfo(const Player* p) {
// 假设我们想缓存一个计算结果,但函数是 const 的
// 使用 mutable 成员就可以修改
p->cachedExp = p->score * 10; // 计算经验值并缓存
std::cout << "Player " << p->id
<< " score=" << p->score
<< " exp=" << p->cachedExp << "\n";
}
// 线程本地变量:每个线程拥有独立的副本
thread_local int t_threadId = 0;
void workerFunction(int threadId) {
// 设置当前线程的 ID
t_threadId = threadId;
// 使用 extern 全局配置
std::cout << "Thread " << t_threadId
<< " sees port=" << g_port
<< " maxPlayers=" << g_maxPlayers << "\n";
// 创建一个玩家(演示 static 局部变量)
Player* p = createPlayer(threadId * 100 + 1, threadId * 50);
// 演示 mutable
printPlayerInfo(p);
delete p; // 记得释放内存
}
int main() {
// 使用 extern 全局配置
std::cout << "Main thread: server port=" << g_port
<< ", max players=" << g_maxPlayers << "\n";
// 启动两个工作线程
std::thread t1(workerFunction, 1);
std::thread t2(workerFunction, 2);
t1.join();
t2.join();
return 0;
}
运算符

++和–这两个自增和自减,如果放置的位置不同,产生的效果不一样,下面举例说明
int a = 1;
int b = 2;
int c = a--; // 此时先将a赋值给c,然后自减
int d = --b; // 先自减,再赋值给d
cout << c << endl;
cout << d << endl;
当上面的代码被编译和执行时,它会产生以下结果:
1
1
循环
for循环
- 传统下标循环(索引遍历)
最基础的形式,适用于数组或支持 operator[] 的容器(如 vector、string)。
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {10, 20, 30, 40};
// 传统 for 循环:用索引遍历
for (int i = 0; i < nums.size(); ++i) {
std::cout << nums[i] << " ";
}
// 输出:10 20 30 40
}
适用场景:需要知道元素下标,或需要同时访问前后元素(如滑动窗口)。
- 迭代器循环(C++98 起)
适用于所有标准库容器(vector、list、map 等),不依赖下标。
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {10, 20, 30, 40};
// 使用迭代器遍历
for (std::vector<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << " ";
}
// 输出:10 20 30 40
}
- 范围 for 循环(C++11 起)
最简洁、最安全的遍历方式,自动遍历整个容器或数组。
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {10, 20, 30, 40};
// 范围 for 循环(只读)
for (int x : nums) {
std::cout << x << " ";
}
// 输出:10 20 30 40
// 如果需要修改元素,使用引用
for (int& x : nums) {
x *= 2;
}
// nums 变为 {20, 40, 60, 80}
}
- 使用 auto 的类型推导(C++11 起)
配合范围 for 或迭代器,让代码更通用。
#include <vector>
#include <map>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3};
for (auto x : nums) { // auto 自动推导为 int
std::cout << x << " ";
}
std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 85}};
for (const auto& kv : scores) { // kv 类型为 pair<const string, int>&
std::cout << kv.first << ": " << kv.second << "\n";
}
}
适用场景:容器元素类型较长或不确定时,用 auto 简化代码。
- 使用 std::for_each 算法(C++98 起,C++11 后配合 lambda)
函数式风格,适合对每个元素执行相同操作。
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4};
// 用 lambda 表达式(C++11)
std::for_each(nums.begin(), nums.end(),
[](int x) { std::cout << x << " "; });
// 输出:1 2 3 4
// 也可以用函数指针
void print(int x) { std::cout << x << " "; }
std::for_each(nums.begin(), nums.end(), print);
}
适用场景:需要将遍历逻辑抽象为可复用的操作,或配合管道式编程。
- 逆向遍历(反向迭代器)
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {10, 20, 30, 40};
// 使用反向迭代器
for (auto it = nums.rbegin(); it != nums.rend(); ++it) {
std::cout << *it << " ";
}
// 输出:40 30 20 10
// 范围 for 不支持直接逆序,但可用 std::reverse 或下标递减
for (int i = nums.size(); i > 0; --i) {
std::cout << nums[i - 1] << " ";
}
}
适用场景:需要从尾部向前遍历。
- 多维数组遍历(嵌套循环)
#include <iostream>
int main() {
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
// 传统嵌套 for
for (int row = 0; row < 2; ++row) {
for (int col = 0; col < 3; ++col) {
std::cout << matrix[row][col] << " ";
}
std::cout << "\n";
}
// 范围 for 也可用于二维数组(C++11)
for (const auto& row : matrix) {
for (int val : row) {
std::cout << val << " ";
}
std::cout << "\n";
}
}
适用场景:矩阵、图像、棋盘等二维数据结构。
- 无限循环
for (;;) {
// 等价于 while(true),常用于服务器事件循环
// 需要内部 break 退出
}
适用场景:游戏主循环、事件监听循环,直到收到终止信号才退出。
字符串
推荐使用std::string,不用c风格的字符串。但少量场景使用c风格字符串
-
与 C 库函数交互:很多系统 API(如 open、read、write、pthread_create)和标准 C 库函数(printf、sscanf、strtok)都要求 char* 参数,无法直接传 std::string。
-
固定大小缓冲区:在网络编程、文件 I/O、协议解析中,常需要预分配固定大小的 char 数组作为缓冲区,避免动态内存分配的开销。
-
嵌入式或资源受限环境:可能没有完整的 C++ 标准库支持,只能使用 C 风格字符串。
-
性能敏感的热路径:std::string 的动态分配和拷贝有一定开销,在某些高频调用场景下,用栈上分配的 char 数组更可控。
-
遗留代码或接口:维护旧项目时,大量代码仍使用 char*,必须兼容。
指针(我一直不太懂的)
指针就是一个存储“内存地址”的变量
想象一排带编号的邮箱(内存单元),每个邮箱有一个唯一的编号(地址),里面可以放东西(数据)。
-
普通变量:比如 int x = 42; —— 相当于你有一个邮箱,编号是 0x7ffee123,里面放了数字 42。
-
指针变量:比如 int* ptr = &x; —— 相当于你有一个新的邮箱,里面放的不是数字,而是另一个邮箱的编号(即 x 的地址 0x7ffee123)。
所以指针本身也是一个变量,它存储的是地址,而不是普通的数据值。
常见的使用方式
动态分配对象(堆上创建)
struct Player {
int id;
int hp;
};
Player* createPlayer(int id) {
Player* p = new Player;
p->id = id;
p->hp = 100;
return p; // 返回指针,调用者负责释放
}
int main() {
Player* player = createPlayer(1);
// 使用 player->id, player->hp
delete player; // 用完释放
return 0;
}
说明:new 返回的是对象的地址(指针),通过 -> 访问成员。注意:现代 C++ 更推荐用智能指针(std::unique_ptr),但初学阶段先理解 raw pointer 的原理。
修改函数外部的变量
C++ 默认传参是拷贝,如果想在函数内部修改外部的变量,可以传指针(或引用)。
void addScore(int* score, int delta) {
*score += delta; // 通过解引用修改外部变量
}
int main() {
int total = 0;
addScore(&total, 10); // 传入地址
addScore(&total, 20);
// total 现在是 30
return 0;
}
说明:&total 取地址传给指针参数,函数内部用 *score 解引用修改。这是 C 风格的输出参数,C++ 更常用引用,但指针仍然常见。
访问数组元素
数组名在大多数情况下退化为指向第一个元素的指针。
void printArray(int* arr, int size) {
for (int i = 0; i < size; ++i) {
std::cout << *(arr + i) << " "; // 指针算术
}
}
int main() {
int data[] = {10, 20, 30};
printArray(data, 3); // data 就是指针
return 0;
}
说明:data 等价于 &data[0],是一个 int*。指针加 i 移动到下一个元素,* 解引用取值。这是 C 风格数组遍历,现代 C++ 更推荐用 std::vector 和迭代器,但理解指针与数组的关系很重要。
实现多态(基类指针指向派生类对象)
面向对象编程中,用基类指针调用虚函数,实现运行时多态。
struct Animal {
virtual void speak() const { std::cout << "...\n"; }
virtual ~Animal() = default;
};
struct Dog : Animal {
void speak() const override { std::cout << "Woof!\n"; }
};
void makeSound(const Animal* animal) {
animal->speak(); // 根据实际类型调用对应函数
}
int main() {
Dog dog;
makeSound(&dog); // 输出 Woof!
return 0;
}
说明:Animal* 可以指向 Dog 对象,调用虚函数时动态分派。这是游戏开发中实体组件系统的基石。
遍历链表或树形结构
指针是构建动态数据结构(链表、树、图)的核心。
struct Node {
int value;
Node* next; // 指向下一个节点
};
Node* createList() {
Node* head = new Node{1, nullptr};
head->next = new Node{2, nullptr};
head->next->next = new Node{3, nullptr};
return head;
}
void printList(Node* node) {
while (node != nullptr) {
std::cout << node->value << " ";
node = node->next;
}
}
说明:每个节点通过 next 指针链接到下一个,nullptr 表示链表结束。指针在这里起到了“连接”作用。
作为函数返回值
有时函数需要返回一个指针,表示成功或失败,或返回动态创建的对象。
Player* findPlayer(int id) {
// 假设有一个全局玩家数组
static Player players[10] = { ... };
for (int i = 0; i < 10; ++i) {
if (players[i].id == id)
return &players[i]; // 返回指向已有对象的指针
}
return nullptr; // 未找到
}
int main() {
Player* p = findPlayer(5);
if (p != nullptr) {
// 使用 p
}
return 0;
}
说明:返回指针可以表示“可能存在也可能不存在”,nullptr 表示失败。这是 C 风格的做法,C++ 中也可以用 std::optional 或异常,但指针仍广泛使用。
困扰我的指针问题
多级指针如何理解
假设你有一本通讯录(指针),里面记着朋友家的地址(指向的数据)。现在你想让另一个人修改这本通讯录本身——比如把通讯录换成一本新的,而不是修改里面的地址。
-
一级指针:你把通讯录的复印件给别人,他只能看,改不了原件。
-
二级指针:你告诉别人你的通讯录放在哪个抽屉里(指针的地址),他打开抽屉,拿出通讯录,换成一本新的。这样你的原件就被改了。
void func(int* p) {
// p 是外部指针 ptr 的副本,但 p 存储的地址与 ptr 相同
*p = 20; // 修改 p 指向的变量,外部变量 a 也被修改
p = nullptr; // 只修改局部副本 p,外部指针 ptr 不受影响
}
如果希望修改指针中的变量,就需要传入双指针参数,我的理解如下;
单指针参数无法通过双重解引用(**p)来修改外部指针的指向,因为:
-
单指针 p 存储的是变量的地址(比如 &a)。
-
对 p 解引用一次得到变量 a(类型 int),不是指针,不能再解引用。
-
要想修改外部指针本身(让它指向别处),必须传入指针的地址(即 int** 或 int*&)。
引用
- 必须初始化,且不能为 null。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象
- 引用不需要 * 来解引用,直接用就是原对象。
如何和指针区分
-
指针 = 一张写着别人家地址的纸条。你可以擦掉地址,写上另一个地址;也可以把纸条烧掉(设为 nullptr);还可以把纸条给别人。
-
引用 = 一个人的正式绰号(比如“张三”也叫“阿三”)。绰号永远指同一个人,不能改;而且绰号必须对应一个真实存在的人(不能为空)。
常用场景
函数参数(避免拷贝 + 允许修改)
void swap(int& a, int& b) { // 引用参数
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 1, y = 2;
swap(x, y); // 直接传变量,不需要 & 或 *
// x=2, y=1
}
好处:调用时不用写 &,函数内部也不用 *,代码更干净。
函数返回值(避免拷贝大对象)
struct BigData { int arr[1000]; };
BigData& getData() { // 返回引用,避免拷贝
static BigData data;
return data;
}
int main() {
BigData& d = getData(); // d 是 data 的引用,没有拷贝
d.arr[0] = 42;
}
范围 for 循环中修改容器元素
std::vector<int> nums = {1, 2, 3};
for (int& n : nums) { // 引用,可以修改原元素
n *= 2;
}
// nums 变为 {2, 4, 6}
如果不加 &,n 是拷贝,修改不会影响原容器。
什么时候用引用,什么时候用指针?

数据结构
vector 的三个核心坑
坑 1:扩容导致迭代器失效
vector 扩容时会重新分配内存,所有迭代器、指针、引用全部失效:
std::vector<int> v = {1, 2, 3};
auto it = v.begin() + 1; // 指向 2
v.push_back(4); // 可能触发扩容!
std::cout << *it; // ❌ 未定义行为!it 已失效
解决:push_back 后不要继续使用之前的迭代器,或者在 reserve 后操作。
坑 2:中间插入/删除是 O(n)
vector 中间插入或删除元素,需要移动后续所有元素,复杂度 O(n):
// ❌ 在 vector 头部插入 1000 个元素 = 灾难
for (int i = 0; i < 1000; ++i) {
v.insert(v.begin(), i); // 每次都要搬移整个数组
}
// ✅ 如果需要频繁头插,改用 std::deque
std::deque<int> d;
d.push_front(i); // O(1)
坑 3:遍历删除的正确写法
// ❌ 错误:erase 后 it 失效,++it 是未定义行为
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 30) {
v.erase(it); // it 失效!
}
}
// ✅ 正确:利用 erase 返回的新迭代器
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 30) {
it = v.erase(it); // erase 返回下一个有效迭代器
} else {
++it;
}
}
std::deque:双端队列
-
适用:需要在头部和尾部都高效插入删除(O(1))
-
典型场景:游戏中的消息队列、滑动窗口、撤销栈
-
注意:内存是分段的,不是一片连续内存,缓存友好性略逊于 vector
std::deque<Message> messageQueue;
messageQueue.push_front(msg); // 头部插入
messageQueue.push_back(msg); // 尾部插入
std::list / std::forward_list:链表
-
适用:需要已知位置的 O(1) 插入删除,且不在意随机访问
-
典型场景:较少用。游戏后端里偶尔用于需要稳定迭代器(插入删除不使其他迭代器失效)的场景
-
注意:不支持 [] 随机访问,缓存极差(节点分散在堆上)
⚠️ 实际开发中,list 很少作为默认选择。很多人以为"我插入删除多,用 list",但 vector 的缓存优势 + 批量操作往往更快。
std::map:红黑树(有序)
-
适用:需要按键排序或范围查询
-
复杂度:插入/查找/删除 O(log n)
-
典型场景:按玩家 ID 范围查询、排行榜(需要有序遍历)
std::map<int, Player> players; // 按 ID 有序
players[1001] = Player(...); // operator[] 会插入(注意坑 4)
std::unordered_map:哈希表(无序)
-
适用:高频键值查找,不关心顺序
-
复杂度:平均 O(1),最坏 O(n)
-
典型场景:游戏后端最常用!玩家 ID → Player* 映射、配置表加载、缓存
std::unordered_map<int, Player*> playerMap;
playerMap[1001] = new Player(...); // O(1) 平均
Player* p = playerMap[1001]; // O(1) 平均
unordered_map 的三个坑:
- operator[] 会隐式插入:如果 key 不存在,playerMap[key] 会自动插入一个默认值!如果只是想查,用 find():
// ❌ 危险:即使 key=9999 不存在,也会插入一个默认 Player* Player* p = playerMap[9999]; // p 是 nullptr,但 map 里多了一条记录 // ✅ 安全:用 find auto it = playerMap.find(9999); if (it != playerMap.end()) { Player* p = it->second; } - rehash 导致所有迭代器失效:当元素数量超过负载因子,unordered_map 会 rehash,所有迭代器失效:
auto it = playerMap.begin(); playerMap[2002] = new Player(...); // 可能触发 rehash std::cout << it->first; // ❌ it 可能已失效
解决:如果事先知道大概数量,用 reserve() 预留桶:
playerMap.reserve(10000); // 预留 10000 个桶,减少 rehash
- 自定义 key 类型需要提供哈希函数和 operator==:两者必须保持一致,否则查找失败或重复插入。
#include <iostream>
#include <unordered_map>
#include <string>
struct Person {
std::string name;
int age;
// 重载 operator==
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
};
// 特化 std::hash 用于 Person
namespace std {
template<>
struct hash<Person> {
std::size_t operator()(const Person& p) const {
// 同时哈希 name 和 age,保证一致性
return hash<string>()(p.name) ^ (hash<int>()(p.age) << 1);
}
};
}
int main() {
std::unordered_map<Person, int> salary; // 现在可以直接用了!
salary[{ "Alice", 25 }] = 8000;
salary[{ "Bob", 30 }] = 9500;
std::cout << salary.size() << "\n"; // 2
// 查找
auto it = salary.find({ "Alice", 25 });
if (it != salary.end())
std::cout << it->second; // 8000
}
关键点:std::hash 的特化必须与 operator== 使用的字段一致(这里都是 name 和 age)。如果哈希只用了 name,而 operator== 用了 name 和 age,就会出现之前说的“不一致”问题。
如果不重载 operator==,而是提供独立的相等比较器
有时候不想修改 Person 结构体(比如它是第三方库的),可以单独提供相等比较器:
#include <iostream>
#include <unordered_map>
#include <string>
struct Person {
std::string name;
int age;
};
struct PersonHash {
std::size_t operator()(const Person& p) const {
return std::hash<std::string>()(p.name) ^ (std::hash<int>()(p.age) << 1);
}
};
struct PersonEqual {
bool operator()(const Person& a, const Person& b) const {
return a.name == b.name && a.age == b.age;
}
};
int main() {
// 显式指定四个模板参数
std::unordered_map<Person, int, PersonHash, PersonEqual> salary;
salary[{ "Alice", 25 }] = 8000;
salary[{ "Alice", 26 }] = 8200; // 不同 age,视为不同 key
std::cout << salary.size(); // 2
}
注意:PersonEqual 和 PersonHash 必须基于相同的字段(这里是 name 和 age),否则会出现之前说的“哈希冲突但相等比较不一致”的问题。
不一致会导致什么后果?
struct BadHash {
std::size_t operator()(const Person& p) const {
return std::hash<std::string>()(p.name); // 只基于 name
}
};
struct BadEqual {
bool operator()(const Person& a, const Person& b) const {
return a.age == b.age; // 只基于 age
}
};
// 插入两个 Person:{"Alice", 25} 和 {"Alice", 27}
// 哈希值相同(因为 name 相同),所以进入同一个桶
// 但相等比较器只比较 age,所以 25≠27,认为它们不同
// 所以 map 里会有两条记录,但 key 都是 "Alice"(不同 age)
// 这破坏了 key 唯一的语义,查找时可能混乱
更多推荐


所有评论(0)