当前位置: 首页 > news >正文

【GL012】C++ 易混点二

1. 用栈实现队列

        核心思想:通过两个栈(输入栈和输出栈)的协作,将后进先出(LIFO)的栈操作转换为先进先出(FIFO)的队列操作。关键点在于通过元素转移操作,确保队列的头部始终在输出栈的栈顶。

#include <stack>
#include <stdexcept>

class MyQueue {
private:
    std::stack<int> inStack;   // 输入栈,用于入队操作
    std::stack<int> outStack;  // 输出栈,用于出队操作

    // 将输入栈的所有元素转移到输出栈
    void transferElements() {
        while (!inStack.empty()) {
            int element = inStack.top();
            inStack.pop();
            outStack.push(element);
        }
    }

public:
    /** 入队操作:将元素压入输入栈 */
    void push(int x) {
        inStack.push(x);
    }

    /** 出队操作:移除并返回队首元素 */
    int pop() {
        if (empty()) {
            throw std::out_of_range("Queue is empty");
        }
        if (outStack.empty()) {
            transferElements();
        }
        int front = outStack.top();
        outStack.pop();
        return front;
    }

    /** 获取队首元素但不移除 */
    int peek() {
        if (empty()) {
            throw std::out_of_range("Queue is empty");
        }
        if (outStack.empty()) {
            transferElements();
        }
        return outStack.top();
    }

    /** 判断队列是否为空 */
    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};

// 示例测试代码
int main() {
    MyQueue q;
    q.push(1);
    q.push(2);
    q.push(3);

    std::cout << q.peek() << std::endl; // 输出 1
    q.pop();                            // 移除 1

    q.push(4);
    std::cout << q.pop() << std::endl;  // 输出 2
    std::cout << q.pop() << std::endl;  // 输出 3
    std::cout << q.pop() << std::endl;  // 输出 4

    return 0;
}

        总结:通过两个栈的协作,利用输入栈接收新元素,输出栈处理出队操作,并适时进行元素转移,成功模拟了队列的FIFO特性。

2. 堆向上生长与栈向下生长的原因

在操作系统的内存布局中,堆(Heap)向高地址增长栈(Stack)向低地址增长,这一设计主要基于 硬件优化内存管理效率历史约定。以下是详细解释:

(1)内存布局示意图

典型进程的虚拟内存空间布局如下(以 Linux 为例):

高地址
+------------------+ ← 栈顶(初始位置)
|        栈         | 栈向下生长(低地址方向)
+------------------+
|     内存映射段     | (共享库、文件映射等)
+------------------+
|        堆         | 堆向上生长(高地址方向)
+------------------+
|     BSS段        | (未初始化的全局变量)
+------------------+
|     数据段        | (已初始化的全局变量)
+------------------+
|     代码段        | (程序指令)
低地址
(2)栈向下生长的原因

  1. 硬件指令优化

    • CPU 的栈操作指令(如 push/pop)默认设计为 栈指针递减(从高地址向低地址移动)。
    • 示例
      ; x86 架构的 push 操作
      push eax      ; 将 eax 的值存入栈顶,栈指针 esp 递减
  2. 函数调用链的自然扩展

    • 每次函数调用时,栈帧(局部变量、参数、返回地址)被压入栈中。
    • 向下生长 使得新栈帧的地址更低,与函数调用的嵌套顺序(先进后出)一致,便于快速分配和释放空间。
  3. 内存冲突防护

    堆和栈分别从内存两端相向生长,最大化利用空间。若二者相遇,说明内存耗尽。
(3)堆向上生长的原因 
  1. 动态内存管理的灵活性

    • 堆需要支持动态分配不同大小的内存块。从低地址向高地址扩展 使得内存管理器可以按需分配空闲块,逐步占用高地址空间。
  2. 内存碎片管理的效率

    • 堆的分配可能产生 外部碎片(分散的小块空闲内存)。
    • 向上生长时,内存管理器可以通过合并相邻空闲块或重新分配大块内存来优化空间利用率。
  3. 历史惯例与兼容性

    • 早期系统的内存布局设计被广泛接受,后续硬件和操作系统保持兼容。
 (4)实际影响与例外情况
  1. 栈溢出(Stack Overflow)

    • 当栈增长超过其最大容量(如无限递归)时,会覆盖其他内存区域(如堆或代码段),导致程序崩溃。
  2. 堆溢出(Heap Overflow)

    • 堆内存越界写入可能破坏堆的管理结构(如 malloc 的元数据),引发隐蔽的漏洞(如内存泄漏或代码注入)。
  3. 例外情况

    • 嵌入式系统:某些资源受限的系统中,堆和栈的生长方向可能不同。
    • 地址随机化(ASLR):现代系统会随机化堆栈基址以增强安全性,但生长方向不变。
(5)总结
特性栈(向下生长)堆(向上生长)
设计目的高效管理函数调用和局部变量灵活分配动态内存
硬件支持栈指针递减指令优化内存管理器按需分配
内存管理无碎片(LIFO 结构)需处理外部碎片
典型问题栈溢出(立即崩溃)堆溢出(隐蔽漏洞)

        核心结论: 堆和栈的生长方向是操作系统和硬件协同设计的结果,旨在 最大化内存利用率优化操作效率。理解这一机制有助于规避内存错误(如溢出)并优化程序性能。

3. 数据结构有什么了解

数据结构是计算机存储、组织数据的方式,直接影响程序的效率和性能。核心数据结构包括:

  • 线性结构

    • 数组:连续内存,支持快速随机访问(O(1)),插入/删除效率低(O(n))。
    • 链表:非连续内存,插入/删除高效(O(1)),但访问需遍历(O(n))。
    • 栈(Stack):后进先出(LIFO),用于函数调用、括号匹配。
    • 队列(Queue):先进先出(FIFO),用于任务调度、BFS算法。
  • 树形结构

    • 二叉树:层次化数据存储,支持高效搜索(如二叉搜索树)。
    • 平衡树(AVL、红黑树):优化树高,保证操作复杂度为O(log n)。
    • 堆(Heap):完全二叉树结构,用于优先队列、排序(堆排序)。
  • 哈希表(Hash Table):通过哈希函数映射键值对,实现平均O(1)的查找/插入。

  • 图(Graph):节点和边的集合,用于网络建模、路径搜索(如Dijkstra算法)。

问题1:数组和链表的区别是什么?
  • 回答
    • 内存分配:数组连续,链表通过指针非连续。
    • 访问效率:数组O(1),链表O(n)。
    • 插入/删除:数组需移动元素(O(n)),链表只需调整指针(O(1))。
    • 应用场景:数组适合频繁访问,链表适合频繁增删。
问题2:如何用栈实现队列?
  • 核心思路:使用两个栈(输入栈和输出栈)。
    • 入队(Enqueue):直接压入输入栈。
    • 出队(Dequeue):若输出栈为空,将输入栈元素全部弹出压入输出栈,再弹出栈顶。
    • 时间复杂度:均摊O(1)。
  • 代码示例
    class QueueWithStacks:
        def __init__(self):
            self.in_stack = []
            self.out_stack = []
        
        def enqueue(self, x):
            self.in_stack.append(x)
        
        def dequeue(self):
            if not self.out_stack:
                while self.in_stack:
                    self.out_stack.append(self.in_stack.pop())
            return self.out_stack.pop() if self.out_stack else None
问题3:堆和栈的内存生长方向为何不同?
  • 堆向上生长:动态分配内存需灵活扩展,内存管理器从低地址分配空闲块,逐步向高地址扩展。
  • 栈向下生长:函数调用链的栈帧按调用顺序从高地址向低地址分配,便于快速压入/弹出操作。
  • 设计原因:防止堆栈内存冲突,最大化空间利用率。

4. GDB 调试指南及注意事项

1. 怎么debug,怎么看内存泄漏。

2. gdb 使用 -> 多线程程序切换到某线程栈帧 -> 如何查看寄存器值

3. 怎么分析C++的core文件

4. GDB有哪些命令

5. gcc和g++的区别

6. Linux下程序有问题,如何调试?(答GDB打开,打上Breakpoint进行调试)

5.  堆栈溢出怎么处理

场景解决方案
递归过深改为迭代或限制递归深度
局部变量过大改用堆内存(malloc/new
系统栈空间不足增大栈大小(ulimit 或代码设置)
函数调用链过长重构代码减少嵌套层级

6. 内存池,线程池和线程开销

(1)内存池(Memory Pool)
1. 原理

内存池是一种预先分配大块内存并自行管理的机制,程序从中分配和释放内存,避免频繁调用系统级内存分配函数(如 malloc/free),从而提高性能。

2. 实现步骤
  1. 初始化:预先分配一大块内存作为池。
  2. 分配:从池中划分小块内存给程序使用。
  3. 释放:将内存块标记为空闲,而非真正释放。
  4. 销毁:释放整个内存池。
3. 优点
  • 性能提升:减少频繁调用 malloc/free 的开销。
  • 内存碎片减少:统一管理内存,避免外部碎片。
  • 可预测性:内存分配时间更稳定。
4. 缺点
  • 内存浪费:池中可能存在未使用的内存。
  • 复杂性:需自行实现分配和释放逻辑。
5. 应用场景
  • 高频内存分配/释放的场景(如网络服务器、游戏引擎)。
(2)线程池(Thread Pool)
1. 原理

线程池是一种预先创建一组线程并管理其任务执行的机制,避免频繁创建和销毁线程的开销。

2. 实现步骤
  1. 初始化:创建固定数量的线程,放入池中。
  2. 任务提交:将任务添加到任务队列,池中线程从队列中取出任务执行。
  3. 线程管理:线程执行完任务后,返回池中等待下一个任务。
  4. 销毁:释放所有线程。
3. 优点
  • 性能提升:避免线程创建和销毁的开销。
  • 资源控制:限制线程数量,防止系统过载。
  • 任务管理:统一管理任务队列,支持优先级调度。
4. 缺点
  • 复杂性:需实现任务队列和线程调度逻辑。
  • 不适用场景:不适合任务执行时间极短或极长的场景。
5. 应用场景
  • 高并发任务处理(如 Web 服务器、数据库连接池)。
(3)线程开销
1. 创建和销毁开销
  • 时间开销:创建线程需分配栈、初始化线程控制块(TCB)等操作,耗时较长。
  • 空间开销:每个线程需分配独立的栈空间(默认几 MB),内存消耗较大。
2. 上下文切换开销
  • CPU 开销:切换线程时需保存和恢复寄存器、栈指针等上下文信息。
  • 缓存失效:线程切换可能导致 CPU 缓存失效,影响性能。
3. 同步开销
  • 锁竞争:多线程访问共享资源时,锁竞争可能导致性能下降。
  • 死锁风险:不当的锁使用可能导致死锁。
4. 减少线程开销的方法
  • 使用线程池:避免频繁创建和销毁线程。
  • 减少线程数量:根据 CPU 核心数合理设置线程数。
  • 无锁编程:使用原子操作或无锁数据结构减少锁竞争。
(4)内存池 vs 线程池
特性内存池线程池
核心目标提高内存分配性能提高任务执行性能
主要开销内存碎片、管理复杂性线程管理复杂性、任务调度开销
适用场景高频内存分配/释放高并发任务处理
实现难度较高较高

7. 线程切换的到底是什么?

线程切换:是操作系统的核心功能之一,涉及保存当前线程状态、恢复目标线程状态以及调度器的决策。以下是线程切换的详细过程和涉及的资源:

(1)线程切换的触发条件
1.时间片用完:操作系统为每个线程分配时间片,用完后强制切换。
2.主动让出 CPU:线程调用 sleep、yield 或等待 I/O 完成。
3.阻塞操作:线程等待锁、信号量或条件变量时被挂起。
4.中断处理:硬件中断(如时钟中断、I/O 中断)触发线程切换。
5.优先级线程就绪:高优先级线程进入就绪队列时,抢占当前线程。
(2)线程切换的核心步骤
1. 保存当前线程状态
    寄存器:保存 CPU 寄存器(如通用寄存器、程序计数器、栈指针)。
    栈:保存线程的栈数据(包括局部变量和函数调用链)。
    线程控制块(TCB):将线程状态(如运行状态、优先级)保存到 TCB 中。
2. 选择目标线程
    调度器决策:根据调度算法(如轮转、优先级)选择下一个运行的线程。
3. 恢复目标线程状态
    寄存器:从目标线程的 TCB 恢复寄存器值。
    栈:恢复目标线程的栈数据。
    程序计数器:恢复目标线程的执行点。
4. 切换上下文
    CPU 状态切换:更新 CPU 的状态寄存器、页表基址寄存器(如 CR3)等。
    缓存刷新:线程切换可能导致 CPU 缓存失效,需重新加载数据。
    (3)线程切换涉及的资源
    1. CPU 寄存器
        通用寄存器:用于存储临时数据(如 EAX、EBX)。
        程序计数器(PC):指向下一条指令地址。
        栈指针(SP):指向当前线程的栈顶。
    2. 栈
        用户栈:存储线程的函数调用链和局部变量。
        内核栈:存储线程在内核模式下的调用链。
    3. 线程控制块(TCB)
        线程状态:如运行、就绪、阻塞。
        优先级:用于调度决策。
        资源指针:如打开的文件描述符、内存映射。
    4. 内存管理单元(MMU)
        页表切换:不同线程可能使用不同的虚拟地址空间,需切换页表。
      (4)线程切换的开销
      1. 时间开销
          寄存器保存/恢复:约几十到几百个 CPU 周期。
          调度器决策:取决于调度算法复杂度(如 O(1) 调度器)。
          缓存失效:线程切换可能导致 CPU 缓存失效,增加内存访问延迟。
      2. 空间开销
          TCB 存储:每个线程需额外的内存存储 TCB。
          栈空间:每个线程需独立的栈空间(默认几 MB)。
      3. 性能影响
          频繁切换:导致 CPU 利用率下降,系统吞吐量降低。
          锁竞争:多线程切换可能加剧锁竞争,增加等待时间。
        (5)减少线程切换开销的方法
        1. 优化调度策略
            减少切换频率:如增加时间片大小。
            优先级调度:让高优先级线程优先执行。
        2. 使用线程池:避免频繁创建/销毁线程:复用线程,减少切换开销。
        3. 无锁编程:减少锁竞争:使用原子操作或无锁数据结构。
        4. 协程:轻量级线程:协程切换不涉及内核态,开销更小。
          (6)示例:线程切换过程
          假设线程 A 切换到线程 B:
              1.保存线程 A 的状态:
                  寄存器值保存到 TCB_A。
                  栈指针保存到 TCB_A。
              2.选择线程 B:
                  调度器从就绪队列中选择线程 B。
              3.恢复线程 B 的状态:
                  从 TCB_B 恢复寄存器值。
                  从 TCB_B 恢复栈指针。
              4.切换上下文:
                  更新 CPU 的页表基址寄存器(CR3)。
                  刷新 CPU 缓存。

          (7)总结
          资源描述开销
          寄存器保存/恢复通用寄存器、PC、SP时间开销(几十到几百周期)
          保存/恢复线程的栈数据时间开销(内存访问延迟)
          TCB保存/恢复线程状态和优先级空间开销(额外内存)
          MMU切换页表和虚拟地址空间时间开销(缓存失效)

          核心原则

          • 线程切换是操作系统的核心功能,涉及寄存器、栈、TCB 和 MMU 等资源。
          • 频繁切换会导致性能下降,需通过优化调度、线程池和无锁编程等方法减少开销。

          8. C++子类(派生类)的构造和析构过程 

          (1)构造过程
          子类的构造函数会按照以下顺序执行:
              1.调用基类的构造函数:
                  如果子类构造函数没有显式调用基类构造函数,编译器会自动调用基类的默认构造函数。
                  如果基类没有默认构造函数,必须在子类构造函数中显式调用基类的某个构造函数。
              2.初始化子类的成员变量:
                  按照成员变量在类中声明的顺序进行初始化。
                  如果成员变量是对象,调用其构造函数。
              3.执行子类构造函数的函数体:
                  执行子类构造函数中的代码。
          (2)析构过程
          子类的析构函数会按照以下顺序执行:
              1.执行子类析构函数的函数体:
                  执行子类析构函数中的代码。
              2.析构子类的成员变量:
                  按照成员变量在类中声明的逆序进行析构。
                  如果成员变量是对象,调用其析构函数。
              3.调用基类的析构函数:
                  编译器会自动调用基类的析构函数。
          关键点总结:
          • 构造顺序:先基类,再子类。
          • 析构顺序:先子类,再基类。
          • 显式调用基类构造函数:如果基类没有默认构造函数,必须在子类构造函数中显式调用基类的某个构造函数。
          • 成员变量的构造和析构:成员变量的构造和析构顺序与其在类中声明的顺序相关。

          9. C++两个线程中的同步方式?

          (1)互斥锁(std::mutex)

          互斥锁用于保护共享资源,确保同一时间只有一个线程可以访问。

          #include <iostream>
          #include <thread>
          #include <mutex>
          
          std::mutex mtx;  // 互斥锁
          int shared_data = 0;
          
          void increment() {
              for (int i = 0; i < 10000; ++i) {
                  mtx.lock();          // 加锁
                  ++shared_data;       // 访问共享资源
                  mtx.unlock();        // 解锁
              }
          }
          
          int main() {
              std::thread t1(increment);
              std::thread t2(increment);
          
              t1.join();
              t2.join();
          
              std::cout << "Final value: " << shared_data << std::endl;  // 输出 20000
              return 0;
          }
          
          
          (2)条件变量(std::condition_variable

          条件变量用于线程间通信,允许线程等待特定条件成立后再继续执行。

          #include <iostream>
          #include <thread>
          #include <mutex>
          #include <condition_variable>
          
          std::mutex mtx;
          std::condition_variable cv;
          bool ready = false;
          
          void print() {
              std::unique_lock<std::mutex> lock(mtx);
              cv.wait(lock, [] { return ready; });  // 等待条件成立
              std::cout << "Thread is running" << std::endl;
          }
          
          void go() {
              std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟延时
              {
                  std::lock_guard<std::mutex> lock(mtx);
                  ready = true;  // 设置条件为 true
              }
              cv.notify_all();   // 通知所有等待的线程
          }
          
          int main() {
              std::thread t1(print);
              std::thread t2(go);
          
              t1.join();
              t2.join();
          
              return 0;
          }
           (3)原子操作(std::atomic

          原子操作确保对共享资源的操作是不可分割的,避免数据竞争。

          #include <iostream>
          #include <thread>
          #include <atomic>
          
          std::atomic<int> shared_data(0);
          
          void increment() {
              for (int i = 0; i < 10000; ++i) {
                  ++shared_data;  // 原子操作
              }
          }
          
          int main() {
              std::thread t1(increment);
              std::thread t2(increment);
          
              t1.join();
              t2.join();
          
              std::cout << "Final value: " << shared_data << std::endl;  // 输出 20000
              return 0;
          }
           (4)读写锁(std::shared_mutex

          读写锁允许多个线程同时读取共享资源,但写操作是独占的。

          #include <iostream>
          #include <thread>
          #include <shared_mutex>
          
          std::shared_mutex smtx;
          int shared_data = 0;
          
          void read() {
              std::shared_lock<std::shared_mutex> lock(smtx);  // 共享锁
              std::cout << "Read data: " << shared_data << std::endl;
          }
          
          void write(int value) {
              std::unique_lock<std::shared_mutex> lock(smtx);  // 独占锁
              shared_data = value;
              std::cout << "Write data: " << shared_data << std::endl;
          }
          
          int main() {
              std::thread t1(read);
              std::thread t2(write, 10);
              std::thread t3(read);
          
              t1.join();
              t2.join();
              t3.join();
          
              return 0;
          }
          (5)信号量(std::counting_semaphore,C++20)

          信号量用于控制对共享资源的访问数量。

          #include <iostream>
          #include <thread>
          #include <semaphore>
          
          std::counting_semaphore<1> semaphore(1);  // 信号量初始值为 1
          int shared_data = 0;
          
          void increment() {
              for (int i = 0; i < 10000; ++i) {
                  semaphore.acquire();  // 获取信号量
                  ++shared_data;        // 访问共享资源
                  semaphore.release();  // 释放信号量
              }
          }
          
          int main() {
              std::thread t1(increment);
              std::thread t2(increment);
          
              t1.join();
              t2.join();
          
              std::cout << "Final value: " << shared_data << std::endl;  // 输出 20000
              return 0;
          }
          (6)屏障(std::barrier,C++20)

          屏障用于同步多个线程,确保所有线程都到达某个点后再继续执行。

          #include <iostream>
          #include <thread>
          #include <barrier>
          
          std::barrier barrier(2);  // 屏障初始值为 2
          
          void task() {
              std::cout << "Task started" << std::endl;
              barrier.arrive_and_wait();  // 等待其他线程
              std::cout << "Task finished" << std::endl;
          }
          
          int main() {
              std::thread t1(task);
              std::thread t2(task);
          
              t1.join();
              t2.join();
          
              return 0;
          }
          总结:
          同步方式适用场景特点
          互斥锁保护共享资源简单易用,适合独占访问
          条件变量线程间通信适合等待特定条件
          原子操作简单共享资源的操作无需锁,性能高
          读写锁读多写少的场景提高读操作的并发性
          信号量控制并发访问数量灵活控制资源访问
          屏障多线程分阶段任务协调多个线程的执行顺序

          相关文章:

        • 分布式架构-Spring技术如何能实现分布式事务
        • 如何解决Redis缓存异常问题(雪崩、击穿、穿透)
        • k8s存储介绍(三)valume概述与emptydir
        • Java基础 3.25
        • Git更改暂存 : Git Pull 错误的快速解决方法
        • LeetCode142环形链表
        • 代码随想录算法训练营第五十六天 | 108.冗余连接 109.冗余连接II
        • 代码随想录算法训练营第四十一天|买卖股票专题:121. 买卖股票的最佳时机、122.买卖股票的最佳时机II、123.买卖股票的最佳时机III
        • 质检LIMS系统在食品生产加工企业的应用 如何保证食品生产企业的安全
        • Unity2022发布Webgl2微信小游戏部分真机黑屏
        • pytorch小记(十五):pytorch中 交叉熵损失详解:为什么logits比targets多一个维度?
        • 13 python 数据容器-元组
        • GitLab 部署说明
        • 数据抓取的缓存策略:减少重复请求与资源消耗
        • vue2相关 基础命令
        • Vue 3 组件高级语法
        • Redis通用命令+部分策略模型
        • Mybatis基于注解开发
        • Ubuntu22.04 UEFI系统配置Apache Tomcat/8.5.87为开机自动启动
        • LangChain4J开源开发框架简介
        • 免费手机网站系统/百度地图在线使用
        • 武汉网站建设网站/seo深圳优化
        • 亚马逊网站建设案例/独立站seo是什么
        • 个人网站设计图/百度搜索量统计
        • 百度网站验证怎么做/360网站推广官网