CPP学习笔记—单例模式
1. 什么是单例模式?单例模式是一种创建型设计模式,其核心思想是确保一个类在任何情况下都只有一个实例,并提供一个全局访问点来获取这个唯一的实例。 可以把它想象成一个国家的总统或者一个学校的校长,在整个系统运行期间,这个角色只能有一个人担任。无论你从哪个部门、哪个流程去“找校长”,最终找到的都是同一个人。 为了在 C++ 中实现这一点,通常需要满足三个关键条件: 私有的构造函数:为了防止外部代码通过 new 操作符随意创建类的实例。 一个私有的、静态的、指向本类实例的指针或对象:这是存放那个唯一实例的地方。 一个公有的、静态的、用于获取实例的方法:这是全局唯一的访问点,负责创建并返回那个唯一的实例。 2. 为什么需要单例模式?单例模式主要用于解决那些“全局唯一”且需要被频繁共享访问的资源或服务。常见的应用场景包括: 日志记录器(Logger):整个应用程序通常只需要一个日志记录器,所有模块都通过它来写入日志文件。 配置管理器(Configuration Manager):读取和管理应用的配置信息(如数据库连接字符串、API...
CPP学习笔记—初始化方式
C++ 的初始化是一个庞大而精细的主题,从 C 语言的简单赋值风格,到 C++11 引入的统一初始化,其发展历程旨在解决二义性、提高类型安全性和代码一致性。 为什么初始化如此重要?在 C++ 中,一个未经初始化的变量(非静态局部变量)拥有一个不确定的值。读取这个值会导致未定义行为 (Undefined Behavior, UB),这是 C++ 中最危险的陷阱之一,可能导致程序崩溃、数据损坏或看似正常运行但结果错误。 C++ 初始化方式的演变与分类我们可以大致将初始化方式分为两大类:C++11 之前的传统方式和 C++11 及其之后引入的统一初始化(大括号初始化)。 第一部分:C++11 之前的传统初始化方式在 C++11 之前,主要有以下几种初始化方式,它们的语法不统一,有时会带来困惑。 1. 默认初始化 (Default Initialization)当一个变量在定义时没有提供显式的初始值时,就会发生默认初始化。 语法: 1T object_name; 行为: 对于局部变量(在函数内定义):如果 T 是内置类型(如 int, double,...
CPP学习笔记—priority_queue
1. priority_queue 是什么?std::priority_queue 是 C++ 标准模板库(STL)中的一个容器适配器,它提供了一种特殊的队列:优先级队列。 核心概念:优先出队与普通的队列(std::queue)遵循“先进先出”(FIFO)的原则不同,优先级队列中的元素并非根据其进入队列的顺序出队,而是根据其优先级。每次从队列中取出的元素(通过 top() 访问,pop() 移除),都是当前队列中优先级最高的那个。 默认情况下,对于数字,“大”的元素优先级更高;对于其他类型,使用 std::less 作为比较函数,即通过 operator< 来判断优先级。 底层数据结构:堆 (Heap)priority_queue 的高效实现得益于其底层的堆数据结构。通常是最大堆 (Max-Heap)。 堆:一个特殊的完全二叉树,满足“堆属性”:父节点的值总是大于或等于(最大堆)或小于或等于(最小堆)其所有子节点的值。 最大堆 (Max-Heap):根节点是整个堆中最大的元素。std::priority_queue 默认就是最大堆。 最小堆...
CPP学习笔记—unordered_map
1. unordered_map 是什么?std::unordered_map 是 C++ 标准模板库(STL)中的一个关联容器,它存储了由“键(Key)”和“值(Value)”组成的元素对(std::pair<const Key, Value>)。它的核心特点是: 无序性:元素在容器内的存储顺序是无序的,遍历 unordered_map 时,得到的元素顺序与插入顺序无关,并且在不同编译环境下可能不同。 基于哈希表:其内部实现是一个哈希表(Hash Table)。 高效查询:由于使用了哈希表,它提供了非常快的平均时间复杂度的操作: 插入、删除、查找:平均 O(1) 最坏情况:O(N),其中 N 是容器中元素的数量(当发生严重的哈希冲突时)。 核心概念:哈希表为了理解 unordered_map,需要了解哈希表的工作原理: 哈希函数 (Hash Function):当插入一个键值对时,unordered_map 会使用一个哈希函数将“键”转换成一个整数,这个整数被称为哈希值(Hash Code)。 桶 (Bucket):unordered_map...
算法模板
拓扑排序1234567891011121314151617181920212223# 返回有向无环图(DAG)的其中一个拓扑序# 如果图中有环,返回空列表# 节点编号从 0 到 n-1def topologicalSort(n: int, edges: List[List[int]]) -> List[int]: g = [[] for _ in range(n)] in_deg = [0] * n for x, y in edges: g[x].append(y) in_deg[y] += 1 # 统计 y 的先修课数量 topo_order = [] q = deque(i for i, d in enumerate(in_deg) if d == 0) # 没有先修课,可以直接上 while q: x = q.popleft() topo_order.append(x) for y in g[x]: in_deg[y] -= 1 #...
数据结构与算法—树状数组
1. 树状数组是什么?为什么需要它?想象一个场景:你有一个数组 a,需要频繁地做两件事: 修改数组中某个元素的值。 查询数组中任意一个区间的和(例如,从索引 l 到 r 的所有元素之和)。 我们可以用一些朴素的方法来解决: 普通数组: 修改元素:O(1),非常快。 查询区间和:O(n),需要遍历区间,如果查询次数很多,会非常慢。 前缀和数组: 我们预处理一个 preSum 数组,preSum[i] = a[1] + ... + a[i]。 查询区间和 [l, r]:O(1),只需计算 preSum[r] - preSum[l-1],非常快。 修改元素 a[i]:O(n),因为 a[i] 的改变会影响到 preSum[i] 及之后的所有元素,你需要更新 preSum[i], preSum[i+1], ..., preSum[n],非常慢。 可以看到,这两种方法在“修改”和“查询”上都有一个操作是 O(n) 的,无法同时做到高效。 树状数组 (Fenwick Tree) 就是为了解决这个问题而生的。它是一种巧妙的数据结构,可以在 O(log n) 的时间复杂度内完成...
CPP学习笔记—智能指针
1. 为什么需要智能指针?在 C/C++ 中,内存管理是一个核心但又极易出错的话题。传统的内存管理依赖于程序员手动使用 new 和 delete (或 malloc 和 free) 来申请和释放内存。这导致了几个经典的问题: 内存泄漏 (Memory Leaks):忘记调用 delete。当一个动态分配的对象不再被使用,但其内存没有被释放时,这块内存就无法被再次使用,造成了内存泄漏。长时间运行的程序中,内存泄漏会耗尽系统资源,导致程序崩溃。 悬挂指针 (Dangling Pointers):一个指针指向的内存已经被释放,但指针本身没有被置为 nullptr。如果之后不小心通过这个悬挂指针访问或修改内存,会导致未定义行为(Undefined Behavior),通常表现为程序崩溃或数据损坏。 重复释放 (Double Free):对同一块内存调用两次或多次 delete。这也是一种严重的未定义行为,通常会导致程序崩溃。 异常安全问题:在 new 和 delete 之间如果发生异常,delete 语句可能永远不会被执行,从而导致内存泄漏。 12345678void...
CPP学习笔记—特殊成员函数
1. 引言:对象生命周期与 RAIIC++ 中,对象有明确的生命周期:它被创建,然后在使用结束后被销毁。构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数这六个函数(通常被称为特殊成员函数)就是用来管理这个生命周期的。它们控制着对象的: 创建 (Construction) 销毁 (Destruction) 拷贝 (Copying) 移动 (Moving) 它们是实现 C++ 核心设计哲学 RAII (Resource Acquisition Is Initialization) 的基石。RAII 意味着在对象的构造函数中获取资源(如内存、文件句柄、锁),并在其析构函数中释放资源,从而将资源的生命周期与对象的生命周期绑定在一起,避免资源泄漏。 2. 核心示例:一个简单的 Buffer 类我们将使用一个简单的 Buffer 类来贯穿整个讲解。这个类在堆上分配了一块整数数组,因此它需要手动管理内存资源。 1234567891011121314151617181920#include <iostream>#include...
CPP学习笔记—深拷贝与浅拷贝
C++ 中的深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是一个 C++ 实际开发中至关重要的核心概念,因为它直接关系到程序的正确性和内存安全。 1. 问题的根源:指针和动态内存要理解深拷贝和浅拷贝,首先必须明白问题的根源在哪里。如果一个类只包含基本数据类型(如 int, double)或者不持有动态资源的对象(如 std::string,它自己内部处理好了深拷贝),那么我们通常不需要关心这个问题。 问题出现在当一个类的成员变量是指针,并且这个指针指向了在堆(Heap)上动态分配的内存时。 来看一个简单的例子,一个自定义的字符串包装类: 123456789101112131415161718class MyString {private: char* _data; size_t _len;public: MyString(const char* s = "") { _len = strlen(s); _data = new char[_len + 1]; //...
CPP学习笔记—多态
什么是多态?多态,从字面意思上看,是“多种形态”的意思。在 C++ 中,多态是面向对象编程(OOP)的三大核心特性之一(另外两个是封装和继承)。它允许你使用一个统一的接口来处理不同类型的对象,使得程序具有更好的可扩展性和灵活性。 一个通俗的例子是:你有一个通用的“遥控器”(接口),这个遥控器上有个“开/关”按钮。当你用这个遥控器对着电视机按“开/关”,电视机就会打开或关闭;当你用同一个遥控器对着空调按“开/关”,空调就会打开或关闭。遥控器本身并不知道它控制的是电视机还是空调,它只知道发出一个“开/关”的指令。具体执行这个指令的是哪个设备,以及这个设备如何执行(电视机是点亮屏幕,空调是启动压缩机),是在运行时才决定的。 在 C++ 中,这个“遥控器”就是基类指针或引用,而“电视机”和“空调”就是派生类对象。 C++ 中的多态分类C++ 中的多态主要分为两类: 静态多态(编译时多态):在程序编译期间就已经确定了函数调用的地址。它的执行速度快,但灵活性稍差。 动态多态(运行时多态):在程序运行期间才能确定调用哪个函数。这是我们通常所说的...















