enum class 强类型枚举
enum class叫强类型枚举 / 作用域枚举 ,C++11 新增,解决传统普通enum的两大缺陷:无作用域、隐式转 int 不安全。二、普通 enum 与 enum class 核心对比
- 传统 enum(弱枚举,缺点很多)
cpp
运行
enum Color { Red, Green, Blue }; // 1. 成员直接暴露在当前作用域,无需前缀 int a = Red; // 2. 自动隐式转为 int,能和数字、其他枚举随便比较运算 if (Red == 0) {} // 3. 不同枚举之间能随意对比,编译器不报错 enum Size { Small, Big }; if (Red == Small) {} // 语法合法,但逻辑完全错误缺陷总结:
枚举成员污染外层命名空间,容易重名冲突;
枚举值自动隐式转整型,类型无隔离,极易写出错误代码。
enum class(强枚举)
cpp
运行
enum class Color { Red, Green, Blue };两大核心特性:作用域隔离 + 类型安全
三、作用域规则(访问方式)
enum class 的枚举成员被包裹在枚举类型内部,必须 类型名::枚举值 访问,不能单独使用:
cpp
运行
enum class Color { Red, Green, Blue }; int main() { Color c = Color::Red; // 正确 // Color c = Red; // 编译报错,找不到标识符Red return 0; }优势:多个枚举里可以重名,不会冲突
cpp
运行
enum class Color { Red }; enum class Light { Red }; // 合法,互不干扰四、类型安全 & 禁止隐式转换
- 不能隐式转 int
cpp
运行
enum class Color { Red = 1 }; int x = Color::Red; // 报错,不存在隐式转换
- 不同枚举不能互相比较、赋值
cpp
运行
enum class Color { Red }; enum class Light { Red }; if(Color::Red == Light::Red) // 直接编译报错,类型不匹配编译器严格区分不同枚举类型,杜绝无意义对比。
五、枚举底层基础类型 + 显式强制转换
- 指定底层存储类型
默认底层是 int,也可以手动指定
char/short/long等:cpp
运行
// 底层用char存储 enum class Color : char { Red, Green };
- 转整数必须显式强制转换
需要拿到数字时,手动
static_cast转换:cpp
运行
enum class Color { Red = 5 }; int num = static_cast<int>(Color::Red); // num = 5为什么强制要求显式转换?
从根源避免枚举和数字、其他枚举混用,强制程序员清晰区分 "枚举含义" 和 "底层数字",提升代码安全性。
六、完整示例代码
cpp
运行
#include <iostream> using namespace std; // 定义强枚举 enum class Color : int { Red = 1, Green = 2, Blue = 3 }; int main() { // 正确访问方式:类型::成员 Color c = Color::Green; // 显式转int int val = static_cast<int>(c); cout << val << endl; // 错误示范 // int x = Color::Red; // 隐式转换报错 // if(Color::Red == 2) // 枚举和数字对比报错 return 0; }极简总结
enum class= 作用域枚举,成员必须枚举名::值访问,不会污染命名空间;无隐式转 int,不同枚举不能互相比较,类型安全;
转整型只能用
static_cast<int>(枚举值)显式转换;传统 enum 无作用域、自动隐式转 int,开发中优先使用 enum class。
static
一、static 三大使用场景
- 函数内部:静态局部变量
- 全局 / 命名空间作用域:静态全局变量 / 静态函数(限制作用域为本文件)
- class 内部:静态成员变量、静态成员函数
二、场景 1:函数内静态局部变量 static
- 生命周期与初始化
存储位置:静态存储区,整个程序运行期间一直存在
初始化:仅第一次调用函数时执行 1 次,后续调用不再初始化,值保留上次结果
作用域:仅函数内部可见,外部无法访问
cpp
运行
#include <iostream> void test() { static int cnt = 0; // 只初始化一次 cnt++; std::cout << cnt << " "; } int main() { test(); // 1 test(); // 2 test(); // 3 return 0; }普通局部变量每次进函数都会重新创建赋值,static 不会重置。
三、场景 2:全局 / 命名空间 static(文件作用域限制)
- static 全局变量
不加 static 的全局变量:整个项目所有 cpp 文件共享,跨文件可访问; 加
static int g_val;:仅当前源文件可见,其他文件无法 extern 引入,实现文件隔离。
- static 全局函数
static void func(){}函数仅本文件可用,其他 cpp 不能调用,避免多文件函数名冲突。生命周期
静态全局变量程序启动初始化,程序结束销毁。
四、场景 3:类中的 static(静态成员变量 / 静态成员函数)
- 静态成员变量 static 变量
核心特点:属于类,不属于任何对象,所有对象共享同一份
类内只是声明,必须在类外部全局域初始化
不随对象创建而分配内存,程序启动就分配
所有对象修改的是同一个变量
cpp
运行
#include <iostream> class Person { public: static int count; // 类内声明 Person() { count++; } }; // 类外初始化 int Person::count = 0; int main() { Person p1; Person p2; std::cout << Person::count; // 输出2,两个对象共享count return 0; }2. 静态成员函数 static 函数
static 函数真正的限制:没有 this,无法操作任何非静态成员
核心区别(静态 vs 普通成员函数)
普通成员函数:隐含
this指针,依赖对象调用,可以访问静态、非静态成员静态成员函数:没有 this 指针,不依赖对象,只能访问静态成员(静态变量 / 静态函数),不能访问普通成员变量 / 普通成员函数
调用方式两种:
cpp
运行
// 1. 类名::静态函数(推荐) Person::show(); // 2. 对象.静态函数(允许,但不推荐) Person p; p.show();示例:
cpp
运行
class Person { public: static int count; int age; static void showCount() { std::cout << count; // 合法,访问静态变量 // std::cout << age; 报错!无this,无法访问非静态age } }; int Person::count = 0;五、关键对比总结
- 静态局部变量
作用域:函数内
生命周期:全程运行
初始化:首次调用函数执行一次
多调用之间保留数值
- static 全局变量 / 函数
生命周期:程序全程
作用域:仅限当前 cpp 文件,其他文件不可见
- 类 static 成员变量
归属:类,所有对象共享一份
内存:全局静态区,不随对象创建销毁
语法:类内声明,类外初始化
- 类 static 成员函数
无 this 指针
只能操作静态成员,不能访问普通成员
可直接用
类名::函数()调用,无需创建对象六、高频考点一句话速记
函数 static 局部:只初始化一次,值持续保留;
文件域 static:变量 / 函数仅限本文件,隔离命名;
类 static 变量:全类共享,类外初始化;
类 static 函数:无 this,只能碰静态成员,不用对象也能调用。
函数指针 vs 指针函数 完整笔记
一、核心一句话区分(必考)
函数指针 :本质是指针变量 ,存函数地址;
int (*p)(int,int)指针函数 :本质是函数 ,返回值是指针;
int* func(int a)第一部分:函数指针
- 概念
每个函数加载后都有内存地址,函数指针专门存放这个地址,可以通过指针间接调用函数。 声明格式:
cpp
运行
返回值类型 (*指针名)(参数列表);括号
(*p)不能省略,代表 p 是指针,指向对应类型函数。
- 完整示例
cpp
运行
#include <iostream> using namespace std; // 普通函数 int add(int a, int b) { return a + b; } int main() { // 1. 声明函数指针:匹配 add 的类型 int(int,int) int (*fp)(int, int); // 2. 赋值:函数名本身就是函数地址,&add 等价 add fp = add; // fp = &add; 写法等价 // 3. 通过指针调用函数,两种写法都合法 int res1 = fp(3, 4); int res2 = (*fp)(5, 6); cout << res1 << endl; // 7 cout << res2 << endl; // 11 return 0; }
- 简化:typedef 给函数指针起别名
函数指针写法冗长,常用 typedef 简化:
cpp
运行
typedef int (*FuncPtr)(int, int); FuncPtr fp = add;
- 关键规则
函数指针的返回值、参数个数、参数类型必须和指向的函数完全一致;
只能指向全局普通函数、静态函数,不能直接指向类普通成员函数(成员函数带隐藏 this)。
第二部分:指针函数(返回指针的函数)
- 概念
只是一个普通函数,区别仅在于返回值是指针类型。 声明格式:
cpp
运行
返回指针类型 函数名(参数列表);
*跟返回类型绑定,没有括号包裹函数名。
- 完整示例
cpp
运行
#include <iostream> using namespace std; // 指针函数:返回 int* 整型指针 int* getMax(int &a, int &b) { if(a > b) return &a; else return &b; } int main() { int x = 10, y = 20; // 接收函数返回的指针 int *p = getMax(x, y); cout << *p << endl; // 20 return 0; }
- 重要坑:不要返回局部栈变量地址
局部变量函数执行完销毁,返回其地址会生成悬垂野指针:
cpp
运行
int* badFunc() { int temp = 99; return &temp; // 错误!temp 栈上,函数结束销毁 }安全返回:全局变量、static 静态局部变量、堆 new 出来的内存。
三、两者直观对比表
表格
项目 函数指针 int (*fp)(int,int)指针函数 int* func(int a)本质 指针变量,存函数地址 函数,可被调用执行 符号位置 (*fp)括号包裹指针名int*是整体返回类型作用 间接调用函数、回调函数 计算后返回一个内存地址 占用内存 只占一个指针大小 (8 字节) 无独立内存,调用时才执行 四、速记口诀
(*p)带括号 = 指针,指向函数 → 函数指针
int* func无括号,星号在返回类型 → 指针函数函数指针存地址,用来调用函数;指针函数跑逻辑,返回内存地址。
回调函数callback
一、核心概念
- 什么是回调函数
回调本质:把 A 函数的地址(函数指针)当作参数传给 B 函数,由 B 函数内部主动调用 A。
A:回调函数(你写的业务逻辑)
B:中间调度函数(底层 / 工具函数,不关心回调具体逻辑)
执行流程:主函数传回调 → B 内部满足条件时反向调用 A,这就是 "回调"
- 核心优势
上层业务逻辑交给底层,底层只负责通用流程,不耦合具体业务:
- 场景:按钮点击事件、排序自定义比较规则、异步任务、定时器、网络请求完成通知。
- 调用时机
回调什么时候执行,完全由接收函数(B)控制: 循环结束、条件满足、事件触发、任务完成时才调用回调。
二、前置基础:函数指针快速回顾
声明格式:
返回值 (*指针名)(参数列表)c
运行
// 接收两个int,返回int的函数指针 int (*Func)(int, int);三、最简完整回调示例(C/C++ 通用)
需求:写一个通用遍历函数,遍历数组时,把每个元素交给外部自定义回调处理。
cpp
运行
#include <iostream> using namespace std; // 1. 定义回调函数类型(统一规范) typedef void (*Callback)(int num); // 2. 底层通用函数:接收回调指针作为参数 void traverseArr(int arr[], int size, Callback cb) { for (int i = 0; i < size; i++) { // 底层内部主动调用外部传入的回调函数 cb(arr[i]); } } // 自定义回调1:打印数字 void printNum(int n) { cout << n << " "; } // 自定义回调2:判断偶数并打印 void printEven(int n) { if (n % 2 == 0) cout << n << " "; } int main() { int nums[] = {1,2,3,4,5,6}; int len = sizeof(nums)/sizeof(nums[0]); // 传入回调printNum cout << "全部数字:"; traverseArr(nums, len, printNum); cout << endl; // 更换回调逻辑,底层函数完全不用修改 cout << "偶数:"; traverseArr(nums, len, printEven); return 0; }输出:
plaintext
全部数字:1 2 3 4 5 6 偶数:2 4 6流程拆解
traverseArr是底层通用模块,不知道要怎么处理元素;main 把业务函数
printNum/printEven地址传进去;
traverseArr在循环内部执行cb(arr[i]),反向调用我们写的逻辑 → 回调。四、带返回值的回调案例(排序自定义比较)
cpp
运行
#include <iostream> using namespace std; // 回调:接收两个int,返回比较结果 typedef int (*CompareCb)(int a, int b); // 底层冒泡排序,比较规则由外部回调决定 void bubbleSort(int arr[], int size, CompareCb cmp) { for(int i=0; i<size-1; i++) { for(int j=0; j<size-1-i; j++) { // 调用外部回调判断大小 if(cmp(arr[j], arr[j+1]) > 0) { int t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } } } // 回调1:升序 int asc(int a, int b) { return a - b; } // 回调2:降序 int desc(int a, int b) { return b - a; } void show(int arr[], int size) { for(int i=0;i<size;i++) cout << arr[i] << " "; cout << endl; } int main() { int arr[] = {5,1,9,3,7}; int len = sizeof(arr)/sizeof(int); bubbleSort(arr, len, asc); show(arr, len); bubbleSort(arr, len, desc); show(arr, len); return 0; }五、关键考点总结
- 回调函数实现三步
1)定义回调函数的指针类型(统一参数、返回值规范); 2)编写底层函数,形参包含该函数指针; 3)主函数定义业务回调函数,将函数名(地址)传入底层函数。
- 函数指针赋值与调用
赋值:
cb = func;/cb = &func;等价调用:
cb(参数)/(*cb)(参数)两种写法都合法
- 回调调用时机
不由 main 控制,由接收回调的底层函数内部触发:循环、条件、事件完成时执行。
典型应用场景
GUI 界面:按钮点击、鼠标移动事件回调;
算法库:自定义比较、自定义过滤规则;
异步 IO / 网络:数据接收完成后执行回调通知上层;
定时器:定时时间到执行回调。
六、易混点区分
函数指针:只是存储函数地址的变量,是基础工具;
回调函数:一种设计模式,利用函数指针实现代码解耦; 没有函数指针就无法实现回调。
极简答题模板(作业 / 考试直接抄)
回调函数是将函数指针作为参数传递给另一个函数,由该函数在内部主动调用;
好处:底层通用逻辑与上层业务分离,同一底层接口可搭配多种自定义逻辑;
实现步骤:定义函数指针类型 → 底层函数接收指针参数 → 外部定义回调并传入;
调用时机:由接收回调的函数内部根据业务条件触发执行。
多继承,多态
三种访问权限完整区分
- private
本类内部:✅ 可以访问
子类:❌ 不能直接访问
外部对象:❌ 不能访问
- protected
本类内部:✅ 可以随便访问(和 private 一样)
子类:✅ 子类内部可以直接访问
外部对象:❌ 不能访问
- public
本类内部:✅
子类:✅
外部对象:✅
4. 继承构造、析构调用顺序
- 创建子类对象:先父构造 → 后子构造
- 销毁子类对象:先子析构 → 后父析构 原因:父类是子类的基础,必须先初始化父资源,最后释放父资源。
二、多继承
- 多继承语法
一个子类同时继承多个父类,逗号分隔
cpp
运行
class A{}; class B{}; // 同时继承A、B class C : public A, public B { };构造顺序:按继承声明顺序依次构造父类,和初始化列表顺序无关; 析构顺序与构造完全相反。
- 菱形(钻石)继承问题
结构:
plaintext
Base / \ A B \ / C问题:
子类 C 中存在两份 Base 副本,内存冗余;
访问 Base 成员时产生二义性,编译报错。
解决方案:虚拟继承
virtual publiccpp
运行
class Base{}; class A : virtual public Base{}; class B : virtual public Base{}; class C : public A, public B{};虚拟继承保证:整个继承链中只存在一份 Base,消除二义性与冗余。
操作符重载 operator override
一、基础概念
- 什么是运算符重载
内置类型(
int/double)天生支持+ - * ==等运算符; 运算符重载 :给自定义类 / 结构体,重新定义运算符的执行逻辑,让对象可以直接使用运算符运算,不用调用add()、compare()这类成员函数。示例直观对比:
cpp
运行
// 不重载:繁琐调用函数 Point a, b; Point c = a.add(b); // 重载 + 后:直观自然 Point c = a + b;本质:运算符重载本质是特殊命名的函数,编译器遇到运算符时自动调用对应函数。
- 能重载 / 不能重载的运算符
可重载常用运算符
算术:
+ - * / % ++ --比较:
== != < > <= >=位运算:
& | ^ << >>赋值:
= += -= *=单目:
! & *括号 / 下标:
() [] ->绝对不能重载(语法硬性规定)
.成员访问、.*成员指针、::作用域解析、sizeof、typeid、?:三目运算符。3. 两种重载写法
方式 1:成员函数重载(对象自身作为左操作数)
语法:
返回值 operator运算符(右操作数)cpp
运行
class Point { public: int x, y; // 成员重载 + ,左操作数是this对象,仅需传右操作数 Point operator+(const Point& other) const { return {x + other.x, y + other.y}; } }; // 使用:a + b 等价 a.operator+(b) Point a{1,2}, b{3,4}; Point c = a + b;方式 2:全局友元函数重载(支持左操作数不是本类的场景)
cpp
运行
class Point { int x, y; public: Point(int a, int b):x(a),y(b){} friend Point operator+(const Point& a, const Point& b); }; Point operator+(const Point& a, const Point& b) { return Point(a.x + b.x, a.y + b.y); } // a + b 等价 operator+(a,b)二、重载核心规则(语义与约束)
不能改变运算符原有语义、优先级、结合性
优先级、结合性固定不变:比如
+永远低于*,重载后也不会变;禁止乱改语义(坑):
cpp
运行
// 不推荐、可读性极差 Point operator+(const Point& p) { return {p.x - p.y, p.y - p.x}; // + 实际做减法,违背直觉 }规范:重载后的行为要和内置类型逻辑保持一致,
+做相加、==做相等判断。
- 不能改变操作数个数
二元运算符(
+ == <)重载后必须两个操作数;一元运算符(
++ !)只能一个操作数; 不能把二元+改成单目,也不能给单目!加两个参数。
- 至少有一个操作数是自定义类类型
不能仅给内置类型重载,比如不允许重新定义
int + int的行为。三、典型示例:重载 == 比较运算符
cpp
运行
#include <iostream> using namespace std; class Point { public: int x, y; Point(int x_ = 0, int y_ = 0) : x(x_), y(y_) {} // 成员重载 == bool operator==(const Point& other) const { return x == other.x && y == other.y; } }; int main() { Point p1(1,2), p2(1,2), p3(3,4); if (p1 == p2) cout << "p1等于p2" << endl; if (!(p1 == p3)) cout << "p1不等于p3" << endl; return 0; }四、容易踩坑的运算符
- 赋值运算符
operator=必须使用成员函数重载,不能全局函数;默认拷贝赋值有浅拷贝问题,管理堆内存时必须手动重载。
- 自增
++前置 / 后置区分
前置
++p:operator++()无参后置
p++:operator++(int)占位 int 参数,仅用于区分重载
- 流运算符
<< >>只能全局友元重载,因为左操作数是
ostream,不是自定义类,无法用成员函数。cpp
运行
friend ostream& operator<<(ostream& os, const Point& p) { os << p.x << "," << p.y; return os; }五、核心考点总结
运算符重载是特殊函数,让自定义类支持运算符;
. :: sizeof .* ?:无法重载;两种写法:成员函数(左操作数为本对象)、全局友元;
不可修改运算符优先级、操作数数量,语义尽量贴合内置类型;
流运算符只能全局重载,赋值运算符只能成员重载。