c++基础补强-Day05

enum class 强类型枚举

enum class强类型枚举 / 作用域枚举 ,C++11 新增,解决传统普通 enum 的两大缺陷:无作用域、隐式转 int 不安全。

二、普通 enum 与 enum class 核心对比

  1. 传统 enum(弱枚举,缺点很多)

cpp

运行

复制代码
enum Color { Red, Green, Blue };
// 1. 成员直接暴露在当前作用域,无需前缀
int a = Red;
// 2. 自动隐式转为 int,能和数字、其他枚举随便比较运算
if (Red == 0) {}
// 3. 不同枚举之间能随意对比,编译器不报错
enum Size { Small, Big };
if (Red == Small) {} // 语法合法,但逻辑完全错误

缺陷总结:

  1. 枚举成员污染外层命名空间,容易重名冲突;

  2. 枚举值自动隐式转整型,类型无隔离,极易写出错误代码。

  3. 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 }; // 合法,互不干扰

四、类型安全 & 禁止隐式转换

  1. 不能隐式转 int

cpp

运行

复制代码
enum class Color { Red = 1 };
int x = Color::Red; // 报错,不存在隐式转换
  1. 不同枚举不能互相比较、赋值

cpp

运行

复制代码
enum class Color { Red };
enum class Light { Red };

if(Color::Red == Light::Red) // 直接编译报错,类型不匹配

编译器严格区分不同枚举类型,杜绝无意义对比。

五、枚举底层基础类型 + 显式强制转换

  1. 指定底层存储类型

默认底层是 int,也可以手动指定 char/short/long 等:

cpp

运行

复制代码
// 底层用char存储
enum class Color : char { Red, Green };
  1. 转整数必须显式强制转换

需要拿到数字时,手动 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;
}

极简总结

  1. enum class = 作用域枚举,成员必须 枚举名::值 访问,不会污染命名空间;

  2. 无隐式转 int,不同枚举不能互相比较,类型安全;

  3. 转整型只能用 static_cast<int>(枚举值) 显式转换;

  4. 传统 enum 无作用域、自动隐式转 int,开发中优先使用 enum class。

static

一、static 三大使用场景

  • 函数内部:静态局部变量
  • 全局 / 命名空间作用域:静态全局变量 / 静态函数(限制作用域为本文件)
  • class 内部:静态成员变量、静态成员函数

二、场景 1:函数内静态局部变量 static

  1. 生命周期与初始化
  • 存储位置:静态存储区,整个程序运行期间一直存在

  • 初始化:仅第一次调用函数时执行 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(文件作用域限制)

  1. static 全局变量

不加 static 的全局变量:整个项目所有 cpp 文件共享,跨文件可访问; 加static int g_val;仅当前源文件可见,其他文件无法 extern 引入,实现文件隔离。

  1. static 全局函数

static void func(){} 函数仅本文件可用,其他 cpp 不能调用,避免多文件函数名冲突。

生命周期

静态全局变量程序启动初始化,程序结束销毁。

四、场景 3:类中的 static(静态成员变量 / 静态成员函数)

  1. 静态成员变量 static 变量

核心特点:属于类,不属于任何对象,所有对象共享同一份

  1. 类内只是声明,必须在类外部全局域初始化

  2. 不随对象创建而分配内存,程序启动就分配

  3. 所有对象修改的是同一个变量

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 普通成员函数)

  1. 普通成员函数:隐含 this 指针,依赖对象调用,可以访问静态、非静态成员

  2. 静态成员函数:没有 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;

五、关键对比总结

  1. 静态局部变量
  • 作用域:函数内

  • 生命周期:全程运行

  • 初始化:首次调用函数执行一次

  • 多调用之间保留数值

  1. static 全局变量 / 函数
  • 生命周期:程序全程

  • 作用域:仅限当前 cpp 文件,其他文件不可见

  1. 类 static 成员变量
  • 归属:类,所有对象共享一份

  • 内存:全局静态区,不随对象创建销毁

  • 语法:类内声明,类外初始化

  1. 类 static 成员函数
  • 无 this 指针

  • 只能操作静态成员,不能访问普通成员

  • 可直接用类名::函数()调用,无需创建对象

六、高频考点一句话速记

  1. 函数 static 局部:只初始化一次,值持续保留;

  2. 文件域 static:变量 / 函数仅限本文件,隔离命名;

  3. 类 static 变量:全类共享,类外初始化;

  4. 类 static 函数:无 this,只能碰静态成员,不用对象也能调用。

函数指针 vs 指针函数 完整笔记

一、核心一句话区分(必考)

  1. 函数指针 :本质是指针变量 ,存函数地址;int (*p)(int,int)

  2. 指针函数 :本质是函数 ,返回值是指针;int* func(int a)

第一部分:函数指针

  1. 概念

每个函数加载后都有内存地址,函数指针专门存放这个地址,可以通过指针间接调用函数。 声明格式:

cpp

运行

复制代码
返回值类型 (*指针名)(参数列表);

括号 (*p) 不能省略,代表 p 是指针,指向对应类型函数。

  1. 完整示例

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;
}
  1. 简化:typedef 给函数指针起别名

函数指针写法冗长,常用 typedef 简化:

cpp

运行

复制代码
typedef int (*FuncPtr)(int, int);
FuncPtr fp = add;
  1. 关键规则
  • 函数指针的返回值、参数个数、参数类型必须和指向的函数完全一致;

  • 只能指向全局普通函数、静态函数,不能直接指向类普通成员函数(成员函数带隐藏 this)。

第二部分:指针函数(返回指针的函数)

  1. 概念

只是一个普通函数,区别仅在于返回值是指针类型。 声明格式:

cpp

运行

复制代码
返回指针类型 函数名(参数列表);

* 跟返回类型绑定,没有括号包裹函数名。

  1. 完整示例

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;
}
  1. 重要坑:不要返回局部栈变量地址

局部变量函数执行完销毁,返回其地址会生成悬垂野指针

cpp

运行

复制代码
int* badFunc()
{
    int temp = 99;
    return &temp; // 错误!temp 栈上,函数结束销毁
}

安全返回:全局变量、static 静态局部变量、堆 new 出来的内存。

三、两者直观对比表

表格

项目 函数指针 int (*fp)(int,int) 指针函数 int* func(int a)
本质 指针变量,存函数地址 函数,可被调用执行
符号位置 (*fp) 括号包裹指针名 int* 是整体返回类型
作用 间接调用函数、回调函数 计算后返回一个内存地址
占用内存 只占一个指针大小 (8 字节) 无独立内存,调用时才执行

四、速记口诀

  1. (*p) 带括号 = 指针,指向函数 → 函数指针

  2. int* func 无括号,星号在返回类型 → 指针函数

  3. 函数指针存地址,用来调用函数;指针函数跑逻辑,返回内存地址。

回调函数callback

一、核心概念

  1. 什么是回调函数

回调本质:把 A 函数的地址(函数指针)当作参数传给 B 函数,由 B 函数内部主动调用 A

  • A:回调函数(你写的业务逻辑)

  • B:中间调度函数(底层 / 工具函数,不关心回调具体逻辑)

  • 执行流程:主函数传回调 → B 内部满足条件时反向调用 A,这就是 "回调"

  1. 核心优势

上层业务逻辑交给底层,底层只负责通用流程,不耦合具体业务:

  • 场景:按钮点击事件、排序自定义比较规则、异步任务、定时器、网络请求完成通知。
  1. 调用时机

回调什么时候执行,完全由接收函数(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

流程拆解

  1. traverseArr 是底层通用模块,不知道要怎么处理元素;

  2. main 把业务函数printNum/printEven地址传进去;

  3. 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. 回调函数实现三步

1)定义回调函数的指针类型(统一参数、返回值规范); 2)编写底层函数,形参包含该函数指针; 3)主函数定义业务回调函数,将函数名(地址)传入底层函数。

  1. 函数指针赋值与调用
  • 赋值:cb = func; / cb = &func; 等价

  • 调用:cb(参数) / (*cb)(参数) 两种写法都合法

  1. 回调调用时机

不由 main 控制,由接收回调的底层函数内部触发:循环、条件、事件完成时执行。

  1. 典型应用场景

  2. GUI 界面:按钮点击、鼠标移动事件回调;

  3. 算法库:自定义比较、自定义过滤规则;

  4. 异步 IO / 网络:数据接收完成后执行回调通知上层;

  5. 定时器:定时时间到执行回调。

六、易混点区分

  1. 函数指针:只是存储函数地址的变量,是基础工具;

  2. 回调函数:一种设计模式,利用函数指针实现代码解耦; 没有函数指针就无法实现回调。

极简答题模板(作业 / 考试直接抄)

  1. 回调函数是将函数指针作为参数传递给另一个函数,由该函数在内部主动调用;

  2. 好处:底层通用逻辑与上层业务分离,同一底层接口可搭配多种自定义逻辑;

  3. 实现步骤:定义函数指针类型 → 底层函数接收指针参数 → 外部定义回调并传入;

  4. 调用时机:由接收回调的函数内部根据业务条件触发执行。

多继承,多态

三种访问权限完整区分

  1. private
  • 本类内部:✅ 可以访问

  • 子类:❌ 不能直接访问

  • 外部对象:❌ 不能访问

  1. protected
  • 本类内部:✅ 可以随便访问(和 private 一样)

  • 子类:✅ 子类内部可以直接访问

  • 外部对象:❌ 不能访问

  1. public
  • 本类内部:✅

  • 子类:✅

  • 外部对象:✅

4. 继承构造、析构调用顺序

  1. 创建子类对象:先父构造 → 后子构造
  2. 销毁子类对象:先子析构 → 后父析构 原因:父类是子类的基础,必须先初始化父资源,最后释放父资源。

二、多继承

  1. 多继承语法

一个子类同时继承多个父类,逗号分隔

cpp

运行

复制代码
class A{};
class B{};
// 同时继承A、B
class C : public A, public B {
};

构造顺序:按继承声明顺序依次构造父类,和初始化列表顺序无关; 析构顺序与构造完全相反。

  1. 菱形(钻石)继承问题

结构:

plaintext

复制代码
    Base
   /    \
  A      B
   \    /
     C

问题:

  1. 子类 C 中存在两份 Base 副本,内存冗余;

  2. 访问 Base 成员时产生二义性,编译报错。

  3. 解决方案:虚拟继承 virtual public

cpp

运行

复制代码
class Base{};
class A : virtual public Base{};
class B : virtual public Base{};
class C : public A, public B{};

虚拟继承保证:整个继承链中只存在一份 Base,消除二义性与冗余。

操作符重载 operator override

一、基础概念

  1. 什么是运算符重载

内置类型(int/double)天生支持 + - * == 等运算符; 运算符重载 :给自定义类 / 结构体,重新定义运算符的执行逻辑,让对象可以直接使用运算符运算,不用调用 add()compare() 这类成员函数。

示例直观对比:

cpp

运行

复制代码
// 不重载:繁琐调用函数
Point a, b;
Point c = a.add(b);

// 重载 + 后:直观自然
Point c = a + b;

本质:运算符重载本质是特殊命名的函数,编译器遇到运算符时自动调用对应函数。

  1. 能重载 / 不能重载的运算符

可重载常用运算符

  1. 算术:+ - * / % ++ --

  2. 比较:== != < > <= >=

  3. 位运算:& | ^ << >>

  4. 赋值:= += -= *=

  5. 单目:! & *

  6. 括号 / 下标:() [] ->

绝对不能重载(语法硬性规定)

. 成员访问、.* 成员指针、:: 作用域解析、sizeoftypeid?: 三目运算符。

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)

二、重载核心规则(语义与约束)

  1. 不能改变运算符原有语义、优先级、结合性

  2. 优先级、结合性固定不变:比如 + 永远低于 *,重载后也不会变;

  3. 禁止乱改语义(坑):

cpp

运行

复制代码
// 不推荐、可读性极差
Point operator+(const Point& p)
{
    return {p.x - p.y, p.y - p.x}; // + 实际做减法,违背直觉
}

规范:重载后的行为要和内置类型逻辑保持一致,+ 做相加、== 做相等判断。

  1. 不能改变操作数个数
  • 二元运算符(+ == <)重载后必须两个操作数;

  • 一元运算符(++ !)只能一个操作数; 不能把二元 + 改成单目,也不能给单目 ! 加两个参数。

  1. 至少有一个操作数是自定义类类型

不能仅给内置类型重载,比如不允许重新定义 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;
}

四、容易踩坑的运算符

  1. 赋值运算符 operator=

必须使用成员函数重载,不能全局函数;默认拷贝赋值有浅拷贝问题,管理堆内存时必须手动重载。

  1. 自增 ++ 前置 / 后置区分
  • 前置 ++poperator++() 无参

  • 后置 p++operator++(int) 占位 int 参数,仅用于区分重载

  1. 流运算符 << >>

只能全局友元重载,因为左操作数是 ostream,不是自定义类,无法用成员函数。

cpp

运行

复制代码
friend ostream& operator<<(ostream& os, const Point& p)
{
    os << p.x << "," << p.y;
    return os;
}

五、核心考点总结

  1. 运算符重载是特殊函数,让自定义类支持运算符;

  2. . :: sizeof .* ?: 无法重载;

  3. 两种写法:成员函数(左操作数为本对象)、全局友元;

  4. 不可修改运算符优先级、操作数数量,语义尽量贴合内置类型;

  5. 流运算符只能全局重载,赋值运算符只能成员重载。