C++ 学习笔记

1. 虚函数(Virtual Function)​

  • 定义:用 virtual 声明,允许派生类重写(覆盖)基类函数,实现运行时多态

  • 核心特性

    • 动态绑定:通过基类指针/引用调用虚函数时,实际调用的是对象类型的函数(运行时确定)
    • 虚函数表(vtable)​:每个包含虚函数的类有一个虚函数表,存储虚函数地址;对象内存中包含指向该表的指针(vptr)
    • 虚析构函数:若基类指针指向派生类对象,基类析构函数必须为虚函数,否则可能导致资源泄漏
class Animal {
public:
    virtual void speak() { cout << "Animal speaks" << endl; }
};
class Dog : public Animal {
public:
    void speak() override { cout << "Dog barks" << endl; }
};

2. 纯虚函数(Pure Virtual Function)​

  • 定义:在基类中声明但没有实现的虚函数,形式为 virtual 返回类型 函数名() = 0;

  • 核心特性

    • 抽象类:包含纯虚函数的类称为抽象类,​不能实例化,只能作为基类
    • 强制派生类实现:派生类必须实现所有纯虚函数,否则仍为抽象类
    • 接口规范:纯虚函数定义接口,派生类提供具体实现
      class Shape {
      public:
          virtual double area() const = 0; // 纯虚函数
      };
      class Circle : public Shape {
      public:
          double area() const override { return 3.14 * r * r; }
      };

      3.若基类有其他虚函数(如普通虚函数或纯虚函数),则析构函数必须为虚函数。

若基类析构函数非虚,通过基类指针删除派生类对象时,只会调用基类析构函数,导致派生类独有的资源(如动态内存、文件句柄)泄漏。

class AbstractBase {
public:
    virtual ~AbstractBase() = 0; // 声明为纯虚析构函数
};
// 必须提供纯虚析构函数的实现
AbstractBase::~AbstractBase() { 
    // 基类析构逻辑(可空)
}

class Derived : public AbstractBase {
public:
    ~Derived() override { 
        // 派生类析构逻辑
    }
};

int main() {
    AbstractBase* obj = new Derived();
    delete obj; // 正确调用Derived和AbstractBase的析构函数
    return 0;
}

2.友元(Friend)

允许非成员函数或另一个类访问当前类的私有成员。

  • 派生类不能继承基类的友元关系。
  • 友元类的派生类也无法访问原类的私有成员。
class A {
  int secret;
  friend void printSecret(const A& a); // 友元函数
  friend class B; // 友元类
};
void printSecret(const A& a) {
  cout << a.secret; // 合法访问私有成员
}

3.Singleton

确保一个类只有一个实例,并提供全局访问点,常用于需要全局唯一对象或共享资源的场景。

#include 

class Singleton {
public:
    // 删除拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 获取唯一实例的静态方法
    static Singleton& getInstance() {
        static Singleton instance; // C++11 保证局部静态变量的线程安全性
        return instance;
    }

    // 示例方法
    void doSomething() {
        // 业务逻辑
    }

private:
    // 私有化构造函数
    Singleton() = default;
    ~Singleton() = default;
};

4.线程与进程

  1. 进程

    • 是操作系统资源分配的最小单位,代表程序的一次执行实例​(如运行中的微信程序)。
    • 拥有独立的内存空间(代码段、数据段、堆栈等)、文件句柄和系统资源
    • 进程间完全隔离,一个进程崩溃不会影响其他进程
  2. 线程

    • CPU调度的最小单位,属于进程内的执行单元(  如数据库应用中处理查询的多个子任务)。
    • 共享进程的资源(内存、文件等),但拥有独立的栈和程序计数器
    • 线程崩溃可能导致整个进程终止

5.什么是缓存

答:缓存,就是数据交换的缓冲区,是一种用于临时存储数据的高效存储机制,其主要目的是加快访问速度、减轻后台系统压力,从而提升整体性能。我们平时说的缓存大多是指内存。目的是,把读写速度慢的介质的数据保存在读写速度快的介质中(这里的快与慢是相对概念),从而提高读写速度,减少时间消耗。例如:

  • CPU高速缓存:告诉缓存的读写速度远高于内存。
    • CPU读数据时,如果在高速缓存中找到所需数据,就不需要读内存
    • CPU写数据时,先写到高速缓存,再写回内存。
  • 磁盘缓存:磁盘缓存其实就把常用的磁盘数据保存在内存中,内存读写速度也是远高于磁盘的。
    • 读数据时从内存中读取
    • 写数据时,可先写回内存,定时或定量写回到磁盘,或者是同步写回。

6.智能指针

作用:自动管理动态内存,避免内存泄漏(基于RAII机制)。
常见类型

  1. ​**unique_ptr**
    • 独占所有权,不可复制,仅支持移动(std::move)。
    • 适用于单一所有权的场景(如工厂模式返回对象)。
  2. ​**shared_ptr**
    • 共享所有权,通过引用计数管理资源,计数为0时释放内存。
    • 可能产生循环引用问题(需结合weak_ptr解决)。
  3. ​**weak_ptr**
    • 不增加引用计数,解决shared_ptr循环引用问题。
    • 需通过lock()转为shared_ptr以访问对象。

底层原理

  • 利用RAII(资源获取即初始化),在析构函数中自动释放资源。
  • 推荐使用make_shared/make_unique(避免直接new,更高效且安全)

7.多态(运行时多态)​

核心机制:通过虚函数表(vtable)​虚表指针(vptr)​实现动态绑定。

  1. 虚函数表(vtable)​
    • 每个含虚函数的类有一个vtable,存储该类虚函数的地址。
    • 派生类继承基类vtable,并覆盖重写的虚函数地址。
  2. 虚表指针(vptr)​
    • 每个对象内部含一个vptr,指向所属类的vtable。
    • 调用虚函数时,通过vptr查找vtable,再执行对应的函数。

实现条件

  • 基类定义虚函数(virtual关键字)。
  • 派生类重写(override)基类虚函数。
  • 通过基类指针引用调用虚函数。

8.堆和栈的区别

1. 内存管理方式

  • :由编译器自动管理(隐式分配/释放)。
    函数中的局部变量、参数、返回值等由编译器自动压栈(分配)和弹栈(释放),无需手动干预。:向低地址方向增长(地址递减)

    cpp

    void func() {
        int a = 10;  // 栈上分配,函数结束自动释放
    }
  • :需程序员手动管理(显式分配/释放)。:向高地址方向增长(地址递增)
    通过 new/malloc 申请内存,delete/free 释放,忘记释放会导致内存泄漏。

    cpp

    void func() {
        int* p = new int(10);  // 堆上分配,需手动 delete
        delete p;  // 必须显式释放
    }

2. 分配与释放效率

  • :高效且严格有序。
    分配/释放仅需移动栈指针(如 push/pop 操作),无碎片问题。
  • :低效且灵活。
    需在运行时动态查找可用内存块,频繁分配/释放不同大小内存会导致外碎片。

3. 内存大小限制

  • :空间较小(默认几MB)。
    由编译器或操作系统预设,超出会引发栈溢出(Stack Overflow)。
  • :空间较大(受系统虚拟内存限制)。
    理论上可达数GB(如64位系统),但受物理内存和程序逻辑约束。

4. 生命周期

  • :与作用域绑定。
    变量随函数调用结束自动销毁(如局部变量)。
  • :与显式释放操作绑定。
    内存生命周期由程序员控制,可跨函数传递(如动态对象)。

5. 访问方式

  • :直接通过变量名访问。
    内存地址连续,硬件优化支持(如CPU缓存)。
  • :通过指针间接访问。
    内存地址可能分散,访问速度稍慢。

6. 碎片问题

  • :无碎片。
    严格的先进后出(FILO)机制保证内存连续。
  • :可能存在外碎片。
    频繁分配/释放不同大小内存块会导致空闲内存不连续。

7. 应用场景


  • 适合局部变量、函数调用、临时对象(如 std::string 的短字符串优化)。

  • 适合动态数据结构(如链表、树)、大块内存(如图像缓存)、需长期存在的对象。

总结回答示例

“栈由编译器自动管理,分配高效但空间有限,适合局部变量;堆需手动管理,空间大但可能碎片化,适合动态内存需求。栈变量随作用域结束销毁,堆内存需显式释放。实际开发中,优先使用栈,避免不必要的堆分配以提高性能。”

维度 堆(Heap) 栈(Stack)
管理方式 手动管理(new/mallocdelete/free 编译器自动管理(压栈/弹栈)
分配效率 低(需动态查找可用内存块) 高(仅移动栈指针)
内存大小 受系统虚拟内存限制(理论上可达数GB) 默认较小(如Windows默认1MB,Linux 8MB)
生命周期 由程序员控制(显式释放前一直存在) 与作用域绑定(如函数结束时自动释放)
碎片问题 可能存在外碎片(频繁分配不同大小内存块) 无碎片(严格先进后出)
访问速度 较慢(需指针间接访问) 极快(CPU缓存优化,直接寻址)
典型用途 动态数据结构(链表、树)、大内存对象(图像缓存) 局部变量、函数参数、临时对象

9.内存碎片问题

造成堆内存利用率很低的一个主要原因就是内存碎片化。内存碎片化就是计算机程序在运行过程中,频繁地内存分配与释放引起的内存空间不连续性问题,可能导致内存利用率降低甚至无法分配所需的内存。内存碎片主要分为内碎片和外碎片两种类型。

1.内碎片


•定义:内碎片指已分配的内存块未被实际使用的部分。即程序请求的内存小于分配的内存块大小时,多余的部分形成内碎片。
•产生原因:内存分配器通常按固定的对齐规则分配内存块(如对齐到4字节或8字节),分配大小往往是申请大小的倍数。
•举例:程序需要13字节内存,但内存分配器按16字节对齐规则分配了16字节。多出的3字节就是内碎片

2.外碎片


•定义:外碎片是指系统中有足够总量的空间内存,但这些空闲内存不连续,无法满足一个较大的分配请求。
•产生原因:频繁的小内存块的分配和释放导致内存分布变得零散和不连续的小块空闲内存无法自动组合成足够大的连续块。
•举例:系统中有多个小块空闲内存,总量为100MB,但是由于这些空闲内存块彼此不连续,无法分配一个需要50MB的大块。

3.tip:

内存池的固定大小块分配等机制,可以减少有效外碎片,内存池的内存分配策略根据实际需求制定的越精细产生的内碎片越少。ps:内存碎片是不可能减少的。

10.程序内存的五大区域

区域 存储内容 特点
​**.text段** 编译后的机器代码(函数、指令) 只读,不可修改
​**.data段** 已初始化的全局变量、静态变量(如 int a = 10; 程序启动时加载,生命周期与程序一致
​**.bss段** 未初始化的全局变量、静态变量(如 int b; 程序启动时清零,不占磁盘空间
堆(Heap)​ 动态分配的内存(new/malloc分配的对象) 手动管理,需显式释放
栈(Stack)​ 局部变量、函数参数、返回值等 自动管理,随作用域结束释放

11.  new/delete   vs   malloc/free

特性 new/delete(C++) malloc/free(C)
本质 C++运算符 C标准库函数
构造/析构 调用对象的构造函数和析构函数 仅分配/释放内存,不处理对象生命周期
类型安全 类型明确(如 new int),无需计算内存大小 需手动计算字节数(如 malloc(sizeof(int))
异常处理 分配失败时抛出 std::bad_alloc 异常 返回 NULL,需手动检查
内存对齐 按类型对齐规则处理 需手动指定对齐方式
重载 支持运算符重载(自定义内存分配逻辑) 不可重载
示例 cpp int* p = new int(10); delete p; c int* p = malloc(sizeof(int)); free(p);

12.什么是内存池?

内存池是一种预分配内存并进行重复利用的技术,通过减少频繁的动态内存分配与释放操作,从而提高程序运行效率。内存池通常预先分配一块大的内存区域,将其划分为多个小块,每次需要分配内存时直接从这块区域中分配,而不是调用系统的动态分配函数(如new或malloc)。简单来说就是申请一块较大的内存块(不够继续申请),之后将这块内存的管理放在应用层执行,减少系统调用带来的开销。

为什么要做内存池?

性能优化:
·1减少动态内存分配开销:系统级内存分配(如malloc/new)需要处理复杂逻辑(如内存合并、碎片整理),导致性能较低,而内存池通过预分配和简单的管理逻辑显著提高了分配和释放的效率。
·2避免内存碎片:动态分配内存会产生内存碎片,尤其在大量小对象频繁分配和释放的场景中,导致的后果就是:当程序长时间运行时,由于所申请的内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池通过管理固定大小的内存块,可以有效避免碎片化。
·3降低系统调用频率:系统级内存分配(如malloc)需要进入内核态,频繁调用会有较高的性能开销。内存池通过减少系统调用频率提高程序效率。

确定性(实时性):
·4稳定的分配时间:使用内存池可以使分配和释放操作的耗时更加可控和稳定,适合实时性有严格要求的系统。

内存池的应用场景:

高频小对象分配:
·游戏开发:游戏中大量小对象(如粒子、子弹、NPC)的动态分配和释放非常频繁,使用内存池可以显著优化性能。
·网络编程:网络编程中,大量请求和响应对象(如消息报文)和频繁创建和销毁非常适合使用内存池。
·内存管理库:一些容器或数据结构(如std::vector或std::deque)在内部可能使用内存池来优化分配性能。

13.互斥锁(mutex与thread)

#include 
#include 
#include 

std::mutex mtx;
int shared_data = 0;

void increment() {
    std::lock_guard guard(mtx);  // 自动加锁
    for (int i = 0; i < 10000; ++i) {
        ++shared_data;
    }
}

int main() {
    std::thread t1(increment);     //分别是独立的线程
    std::thread t2(increment);
    t1.join();                     //会阻塞主线程(main函数),直到执行完毕
    t2.join();
    std::cout << "Result: " << shared_data << std::endl;  // 正确输出 20000
    return 0;
}

互斥锁与自旋锁的区别?

    • 互斥锁:线程阻塞等待,适用于临界区较长或高竞争场景
  • 自旋锁:忙等待(不释放 CPU),适用于短临界区且多核环境

14.RAII 机制

  • 什么是 RAII?它的核心思想是什么?

    • 答案:RAII(Resource Acquisition Is Initialization)即“资源获取即初始化”,核心思想是将资源(如内存、文件句柄、锁)的生命周期与对象的生命周期绑定。对象构造时获取资源,析构时自动释放资源
    • 优势:防止资源泄漏,简化代码,提高异常安全性。例如 std::lock_guard 和智能指针均基于 RAII
  • 举例说明 RAII 的实际应用场景。​

    • 答案
      1. 智能指针(如 std::unique_ptr)管理堆内存
      2. 文件流(如 std::fstream)自动关闭文件
      3. 互斥锁(如 std::lock_guard)自动加锁/解锁

深入问题:

  • RAII 如何避免内存泄漏?
    • 答案:通过对象析构确保资源释放,即使发生异常或提前返回也能执行
  • RAII 在 STL 中的应用有哪些?
    • 答案:容器(如 std::vector)自动管理元素内存;智能指针封装动态资源

你可能感兴趣的:(学习,笔记,c++)