C++篇

静态变量和全局变量、局部变量的区别、在内存上是怎么分布的

  1. 静态局部变量
  • 特点:
    • 作用域:仅限于声明它们的函数或代码块内部。
    • 生命周期:静态局部变量在程序的整个运行期间都存在,只初始化一次(在第一次使用前)。
    • 初始化:在首次进入函数时初始化,并保持值直到程序结束。
  • 使用场景:
    • 当你需要一个仅在函数内部使用,但希望其值在函数调用之间保持不变的变量时。
    • 适用于需要缓存数据以提高性能的情况。
  • 内存分布:静态局部变量存储在全局/静态存储区。
  1. 局部变量
  • 特点:
    • 作用域:局部变量仅在声明它们的函数或代码块内部可见。
    • 生命周期:局部变量在函数调用时创建,函数调用结束后销毁。
    • 初始化:必须在使用前显式初始化。
  • 使用场景:
    • 需要临时存储数据,且这些数据只在当前作用域内使用时。
    • 作为循环计数器或中间计算结果。
  • 内存分布:局部变量存储在栈上,与它们所在的作用域(如函数)相关联。
  1. 全局变量
  • 特点:
    • 作用域:全局变量在整个程序中都是可见的,可以在任何函数或代码块中访问。
    • 生命周期:全局变量同样具有静态存储期,它们在程序的整个运行期间都存在。
    • 初始化:通常在程序启动时初始化。
  • 使用场景:
    • 当你需要在程序的多个部分共享数据时。
    • 适用于存储配置信息或程序的状态信息。
    • 需要注意全局变量可能导致代码难以测试和维护。
  • 内存分布:全局变量也存储在全局/静态存储区。

C++内存分区

很多情况下,提到 C++ 程序的内存分区时,会简化为下面五个主要区域

  • 栈(Stack): 用于存储局部变量和函数调用的上下文。栈的内存分配是自动的,由编译器管理。
  • 堆(Heap): 用于动态内存分配。程序员可以使用 newmalloc 等操作符或函数从堆上分配内存,并使用 deletefree 释放内存。
  • 全局/静态存储区(Global/Static Storage): 存储全局变量和静态变量,包括:
    • 数据段:存储初始化的全局变量和静态变量。
    • BSS 段 :存储未初始化的全局变量和静态变量。
  • 常量存储区(Constant Data): 存储程序中的常量数据,如字符串字面量。
  • 代码段(Code Segment 或 Text Segment): 存储程序的可执行代码和函数的二进制指令。

指针和引用的区别

  1. 从概念上来说:
    • 指针是一个存储另一个【变量地址】的变量,它指向内存中的一个位置。
    • 引⽤就是变量的别名,从⼀⽽终,不可变,必须初始化
  2. 空状态:
    • 指针可以被初始化为NULL或nullptr,表示它不指向任何地址。
    • 引用在定义时必须被初始化,不能引用NULL或不存在的内存地址。
  3. 可变性:
    • 指针: 可以改变指针的指向,使其指向不同的内存地址。
    • 引⽤: ⼀旦引⽤被初始化,它将⼀直引⽤同⼀个对象,不能改变绑定。
  4. 操作
    • 指针: 可以通过解引⽤操作符 * 来访问指针指向的变量的值,还可以通过地址运算符 & 获取变量的地址。
    • 引用: 引⽤在声明时被初始化,并在整个⽣命周期中⼀直引⽤同⼀个变量。不需要使⽤解引⽤操作符,因为引⽤本身就是变量的别名。
  5. 用途:
    • 指针: 通常⽤于动态内存分配、数组操作以及函数参数传递。
    • 引⽤: 通常⽤于函数参数传递、操作符重载以及创建别名。

static关键字和const关键字的作用

staticconstC++ 中两个常用的关键字, 有以下作用:

  1. static 关键字: 用于控制变量和函数的生命周期、作用域和访问权限。

    • 声明静态变量:静态变量的生命周期直到程序结束。当在函数内部声明静态变量时,即使函数执行完了也不会释放它,下次调用该函数时会保留上次的状态。
    • 在类中,被static声明的成员被称为静态成员。
      • 静态成员变量:在类中使用static关键字修饰的成员变量,表示该变量属于类而不是类的实例,所有实例共享同一份数据
      • 静态成员函数:在类内使用static关键字修饰的成员函数,所有对象共享同一个函数;静态成员函数只能访问静态成员变量;静态成员函数调用可以不需要通过创建类的实例,而是直接通过类名调用。
    • static变量如果被多个线程访问,需要特别注意线程安全问题。
  2. const: 关键字用于定义常量,即一旦初始化后其值不能被修改:

    • 常量变量:声明变量,使变量的值不能修改(只读)
    • 常量成员函数,表示该函数不会修改对象的成员变量
    • 常量指针:可以指向一个 const 类型的值,或者是一个指向 const 值的指针,表明指针指向的值不能通过这个指针被修改。
    • const变量由于其不可变性,天然具有线程安全性。
  3. 有时候staticconst 可以组合使用,如static const变量,表示一个静态的常量。

常量指针和指针常量之间有什么区别

  1. 常量指针是指指针所指向的数据是常量,不能通过这个指针来修改它指向的数据。但是,指针本身的值(即它所指向的地址)是可以改变的。
  2. 指针常量是指指针本身的值是常量,一旦被初始化后就不能指向其他地址。但是,它所指向的数据是可以修改的(除非那个数据本身是常量)

结构体和类之间有什么区别

  1. struct 只能包含成员变量,不能包含成员函数。而在 C++ 中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
  2. 不同点:
  • class 中的成员默认都是 private 的,而 struct 中的成员默认都是 public 的;
  • class 继承默认是 privatestruct 继承默认是 public
  • class 可以用于定义模板函数,而 struct 不行。
  • struct用于表示 数据聚合(将相关数据打包在一起),通常不封装复杂逻辑,适合轻量级对象(如坐标、颜色等),常用于 POD(Plain Old Data) 类型(可直接进行内存操作)。class用于封装 数据+行为,强调抽象和信息隐藏。通常包含成员函数、构造函数、虚函数等复杂逻辑。适合需要访问控制或继承的复杂对象

什么是智能指针,C++有哪几种智能指针

智能指针是C++中用来自动管理动态分配内存的一种模板类,维护着一个指向动态分配对象的指针,并在智能指针对象被销毁时,自动释放该内存,从而避免内存泄漏。

C++有以下几种智能指针:

  • **std::unique_ptr**:独占式拥有指针,保证同一时间只有一个unique_ptr指向特定内存,适用于不需要共享所有权的场景,如栈上对象的管理。
  • **std::shared_ptr**:多个shared_ptr可以共享同一内存,使用引用计数机制来管理内存的生命周期,适用于多个对象需要共享同一个资源的场景。
  • **std::weak_ptr**:弱引用,用于解决shared_ptr可能导致的循环引用问题,它不拥有所指向的对象。

智能指针的实现原理是什么

  1. std::unique_ptr
    • unique_ptr代表独占所有权的智能指针,同一时间只能有一个unique_ptr实例指向特定资源。
    • 它通过析构函数来管理资源的释放。当unique_ptr超出作用域时,会自动调用删除操作符来释放其指向的内存。
    • std::unique_ptr 通过删除复制构造函数和复制赋值运算符来确保所有权的唯一性,但提供移动构造函数和移动赋值运算符,允许所有权的转移。
  2. std::shared_ptr
    • shared_ptr允许多个指针实例共享对同一资源的所有权,使用引用计数机制来跟踪有多少个shared_ptr指向同一资源。
    • 内部维护一个控制块,通常包含引用计数和资源的原始指针。每当创建一个新的shared_ptr或将一个shared_ptr赋值给另一个时,引用计数增加。
    • shared_ptr被销毁或通过标准库提供的reset成员函数重置时,引用计数减少。当引用计数降到零时,控制块会释放资源并自我销毁。
  3. std::weak_ptr
    • std::weak_ptr 是一种不拥有对象的智能指针,它观察 std::shared_ptr 管理的对象,但不增加引用计数。
    • 它用于解决 std::shared_ptr 之间可能产生的循环引用问题,因为循环引用会导致引用计数永远不会达到零,从而造成内存泄漏。

堆区和栈区的区别

(Heap) 和栈 (Stack) 是程序运行时两种不同的内存分配区域

  • 内存分配:
      • 是由编译器自动管理的,用于存储局部变量和函数调用的上下文信息。
      • 栈上的对象在定义它们的块或函数调用结束后自动销毁。
      • 栈的内存分配和释放速度很快,因为栈的内存管理是连续的,不需要搜索空闲内存。
      • 由程序员手动管理的,用于存储动态分配的对象。
      • 堆上的对象需要程序员手动释放,否则可能导致内存泄漏。
      • 堆的内存分配和释放速度通常比栈慢,因为可能需要搜索合适的内存块,并且涉及内存碎片整理。
  • 大小限制:
    • 栈的大小通常有限制,远小于堆的大小,且在不同系统和编译器中可能不同。
    • 堆的大小通常很大,受限于系统可用内存。
  • 使用场景:
    • 栈主要用于存储函数参数、局部变量等。
    • 堆用于存储生存期不受限于单个函数调用的对象,如使用 newmalloc 分配的对象。

new和malloc的区别

newmalloc在C++中都用于动态内存分配,但它们之间有几个关键的区别:

  1. 语法层面
    • new是C++的操作符,可以直接用来分配对象或数组。
    • malloc是一个函数,通常需要包含头文件<cstdlib>,并且只分配原始内存。
  2. 类型安全
    • new是类型安全的,它会根据分配的对象类型进行正确的内存分配和构造函数调用。
    • malloc 不是类型安全的,它只分配原始内存,不调用构造函数。返回类型是 void*,需要强制类型转换为具体的指针类型。
  3. 构造与析构
    • 使用 new 分配的对象在对象生命周期结束时需要使用 delete 来释放,delete 会自动调用对象的析构函数。
    • 使用 malloc 分配的内存需要使用 free 来释放,free 不会自动调用析构函数,因此如果分配的是对象数组,需要手动调用析构函数。
  4. 异常安全性
    • new在分配失败时会抛出std::bad_alloc异常。
    • malloc在分配失败时返回NULL指针。
  5. 管理机制
    • C++中的newdelete通常由编译器实现,可能包含一些额外的内存管理机制。
    • C语言的mallocfree由C标准库提供,与编译器无关。

总结来说,newmalloc都是动态内存分配的手段,但new提供了类型安全和构造/析构的自动化,而malloc则提供了更底层的内存分配方式,需要手动管理构造和析构。在C++中,推荐使用new来分配对象,以保持类型安全和自动化的资源管理。

delete与free的区别

deletefree都是用来释放动态分配的内存,但它们有不同的使用方式:

  1. 语法
    • delete是C++中的关键字,用于释放由new分配的对象。
    • free是C语言中的函数,通常包含在<stdlib.h>头文件中,用于释放由malloc分配的内存。
  2. 对象销毁
    • 当使用 delete 释放对象内存时,C++ 编译器会自动调用对象的析构函数,释放与对象相关的资源,并执行对象的清理工作。
    • free 仅释放内存,不调用析构函数。因此,如果使用 malloc 分配了 C++ 对象的内存,需要手动调用析构函数后再调用 free
  3. 数组处理
    • 如果是数组,C++提供了delete[]来释放整个数组的内存,而C语言中仍然使用free,没有区分单个对象和数组。
  4. 返回值:
    • free 没有返回值,即使内存释放失败,也不会反馈任何信息。
    • delete 之后,指针会自动置为 nullptr
  5. 类型检查:
    • delete 进行类型检查,确保删除的对象类型与 new 分配时的类型一致。
    • free 不进行类型检查,因为它只处理 void* 类型的指针。

总结来说,deletefree都是用来释放动态内存的,但它们分别用于C++和C语言中的内存管理。delete适用于C++对象,会自动调用析构函数;而free适用于C语言分配的内存,不涉及对象的析构。

什么是内存泄漏, 如何检测和防止?

  1. **如果程序的某一段代码在内存池中动态申请了一块内存而没有及时将其释放,就会导致那块内存一直处于被占用的状态而无法使用,造成了资源的浪费。
  2. 什么操作会导致内存泄漏
    • 使用 newmalloc 等分配内存后,没有使用 deletefree 释放内存。
    • 子类继承父类时,没有将基类的析构函数定义为虚函数。
    • 指针被赋值为 nullptr 或重新赋值后,丢失了对先前分配内存的引用,导致无法释放。
    • 在使用引用计数的智能指针(如 std::shared_ptr)时,循环引用会导致引用计数永远不会归零,从而无法释放内存。
    • 使用不匹配的内存释放函数: 使用 delete 释放由 new[] 分配的内存,或使用 delete[] 释放由 new 分配的内存,这可能导致未定义行为。
    • 资源未关闭:对于文件、网络连接等资源,如果没有正确关闭,虽然不直接导致内存泄漏,但会占用系统资源,可能导致资源耗尽。
  3. 如何检测:使用工具如Valgrind、AddressSanitizer或Visual Studio的诊断工具来检测内存泄漏。
  4. 如何避免
    • 使用智能指针:优先使用 std::unique_ptrstd::shared_ptr 等智能指针来自动管理内存。
    • 确保资源释放: 对于手动分配的内存,确保在不再需要时使用 deletefree 释放。
    • 内存泄漏检测工具: 在开发和测试阶段,定期使用内存泄漏检测工具检查程序。

什么是野指针?如何避免?

  1. 什么是野指针
    野指针是指“指向已经被释放的或无效的内存地址的指针”。使用野指针可能会导致程序崩溃、数据损坏或者其他一些不可预测的行为。
  2. 在什么情况下会产生野指针?
    • 在释放后没有置空指针: 使用 deletefree 释放了内存后,没有将指针设置为 nullptr,指针仍然指向已释放的内存地址。
    • 返回局部变量的指针 : 如果函数返回了指向其局部变量的指针,一旦函数返回,这些局部变量的生命周期结束,返回的指针成为野指针。
    • 越界访问:指针访问的内存超出了其合法的内存块边界。
    • 函数参数指针被释放。
  3. 如何避免野指针
    • 在释放内存后将指针置为 nullptr
    • 避免返回局部变量的指针。
    • 使用智能指针(如 std::unique_ptrstd::shared_ptr )。
    • 注意函数参数的生命周期,小心在函数内处理通过指针或引用传递的资源,尤其是避免在函数内部释放传递进来的指针所指向的内存。

C++面向对象三大特性

面向对象编程是C++的核心特性之一。面向对象编程具有封装、继承和多态三个主要特性:

  1. 封装:将客观事物封装成为抽象的类, 类把自己数据和方法进行隐藏,仅对外公开接口来和对象进行交互,防止外界干扰或不确定性访问。
  2. 继承:指一个类(称为子类或派生类)可以从另一个类(称为父类或基类)中继承属性和行为的能力。通过继承,子类可以重用父类的代码,并且可以在不修改父类的情况下添加新的功能或修改已有的功能。继承使得代码具有层次性和可扩展性,能够建立起类之间的层次关系。
  3. 多态:多态是指同一个操作作用于不同的对象时,可以有不同的解释和行为。多态性允许以统一的方式处理不同类型的对象,从而提高了代码的可扩展性和可维护性。在C++中,多态性通常通过虚函数(virtual functions)来实现。通过基类中定义虚函数,并在派生类中重新定义该函数,可以实现运行时的动态绑定

简述一下 C++的重载和重写,以及区别

  1. 重载:在同一个类或命名空间中,声明多个同名函数, 但是参数列表不同。编译器根据参数的类型、数量或顺序来区分不同的函数。
  2. 重写:重写发生在继承体系中,在子类中,声明一个与父类中虚函数具有相同名称、相同参数列表和相同返回类型的函数,并在子类函数前加上 override 关键字。
  3. 区别:
  • 作用域:重载发生在同一个作用域内,而重写发生在继承体系中。
  • 参数列表:对于重载的函数,参数列表必须不同;对于重写的函数,参数列表必须与被重写的函数完全相同。
  • 返回类型:重载函数的返回类型可以不同,但重写函数的返回类型必须与被重写的函数相同(或与之兼容,C++中称为协变返回类型)。
  • 虚函数:重写通常与虚函数一起使用,以实现运行时多态性;而重载是编译时多态性,由编译器在编译期间确定调用哪个函数。
  • 关键字:重写函数可以使用 override 关键字,明确指出该函数是对父类虚函数的重写。

C++怎么实现多态

  1. C++ 的多态分为编译时多态(也被称为静态多态)和运行时多态(也被称为动态动态)
  2. 编译时多态
  • 函数重载Function Overloading):允许在同一作用域内声明多个同名函数,只要它们的参数列表不同。
  • 运算符重载(Operator Overloading): 允许为自定义类型定义或修改运算符的行为。
  • 模板(Templates): 允许创建泛型类和函数,它们可以在多种数据类型上使用。

编译时多态在编译期间就确定了具体的函数或类型,由编译器根据函数的签名或模板实例化来选择正确的函数或实例。

  1. 运行时多态: 运行时多态主要通过虚函数和抽象类实现,父类中定义声明虚函数,子类实现对虚函数的重写。由虚函数表和虚函数指针在运行时确定调用哪个函数。
  • 当类包含虚函数时,编译器会自动为该类创建一个虚函数表,表中包含类中所有虚函数的地址。
  • 子类的虚函数表继承了父类的虚函数表,但会使用自己重写的虚函数将虚函数表中对应的虚函数进行覆盖。
  • 当通过基类指针或引用调用虚函数时,程序会根据对象的实际类型在运行时查找正确的函数地址并调用相应的函数,实现多态。

虚函数和纯虚函数的区别

虚函数和纯虚函数都用于实现多态。

  1. 声明方式
  • 虚函数是在普通函数之前加一个 virtual 关键字
  • 纯虚函数是在虚函数后面加一个 =0
  1. 是否实现:
    • 虚函数是在基类中声明的,提供函数声明和实现,即提供虚函数的默认实现。派生类可以选择是否重写覆盖虚函数的默认实现,也可以直接使用默认实现。
    • 纯虚函数没有函数具体实现,只提供函数声明。派生类必须提供具体实现,否则他们也变成抽象类。
  2. 实例化:
    • 包含纯虚函数的类是抽象类,不能被实例化,但可以声明这种类型的指针或引用。
    • 而包含虚函数的类不一定是抽象类,可以被实例化,除非它也包含纯虚函数。
  3. 目的:
    • 虚函数用于提供一个可以在派生类中被重写的方法实现;
    • 通过纯虚函数,抽象类提供一种接口规范,要求派生类必须提供具体实现。
  4. 动态绑定:
    • 通过虚函数,可以在基类指针或引用中实现动态绑定,即在运行时确定调用哪个类中的函数实现。
    • 纯虚函数由于没有实现,它们本身不参与动态绑定,但可以作为接口的一部分,影响整个类的多态性

虚函数是怎么实现的

虚函数的实现依赖于一种称为虚函数表的机制。

  1. 虚函数表的创建: 当一个类包含虚函数时,编译器会自动为这个类创建一个虚函数表。这个表是一个函数指针数组,每个指针指向一个虚函数的实现。
  2. 虚函数表指针: 编译器会在对象的内存布局中添加一个隐式的虚函数表指针(通常是一个指向 vtable 的指针),这样每个对象都可以通过这个指针访问到类的虚函数表。
  3. 虚函数的声明: 在类中声明虚函数时,可以使用 virtual 关键字。如果一个函数被声明为虚函数,编译器会在类的 vtable 中为这个函数分配一个入口。
  4. 重写虚函数: 当从基类继承并创建派生类时,可以在派生类中重写基类的虚函数。重写的函数会替换掉 vtable 中对应的基类实现。
  5. 动态绑定: 当通过基类指针或引用调用虚函数时,程序会使用对象的虚函数表指针来查找正确的函数实现。这个过程称为动态绑定或晚期绑定。
  6. 调用虚函数: 程序运行时,当调用一个虚函数时,会先通过对象的虚函数表指针找到 vtable,然后在 vtable 中查找对应的函数指针,并调用该函数。

简短来说,每个类都有一个虚表,里面有这个类的虚函数地址;每个对象都有指向它的类的虚表的指针,这个指针称为虚指针。 当调用虚函数时,编译器会调用对象的虚指针查找虚表,通过虚函数的地址来执行相应的虚函数

虚函数表是什么

  1. 虚函数表是 C++ 中实现运行时多态(动态绑定)的关键机制之一。
  2. 虚函数表是一个或多个函数指针的集合,它存储了类中所有虚函数的地址。当类包含虚函数时,编译器会自动为这个类创建一个虚函数表。
  3. 虚函数表的主要目的是在运行时能够确定通过基类指针或引用调用的是哪个派生类中的虚函数实现,从而实现动态绑定。
  4. 原理
  • 创建虚函数表:当类声明至少一个虚函数时,编译器会为这个类生成一个虚函数表。
  • 虚函数表指针:编译器会为包含虚函数的类的对象添加一个隐藏的虚函数表指针(通常是一个指针或引用),指向类的虚函数表。
  • 调用虚函数:当通过基类指针或引用调用虚函数时,程序会使用对象的虚函数表指针来查找并调用正确的函数实现。

什么是构造函数和析构函数?构造函数、析构函数可以是虚函数嘛

  1. 构造函数
  • 构造函数是创建对象时自动调用的成员函数,它的作用是初始化成员变量,为对象分配资源,执行必要的初始化操作。

  • 特点

    • 函数名必须与类名相同,且没有返回类型;
    • 可以有多个构造函数;
    • 如果没有为类定义一个构造函数,编译器会自动生成一个默认构造函数,它没有参数,也可能执行一些默认的初始化操作。
  • 构造函数不能是虚函数。

    • 虚函数的调用需要虚函数表指针vptr,而该指针存放在对象的内存空间中,若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表vtable地址用来调用虚构造函数了
    • 构造函数的核心任务是初始化对象内存和成员变量。在对象尚未完全构造时(如基类构造函数执行期间),派生类的成员可能处于未初始化状态。此时若通过虚函数机制调用派生类的方法,会导致未定义行为
    • 多态性发生在运行时,但对象类型必须在编译期确定才能正确分配内存和调用构造函数。虚函数机制无法在编译阶段解析构造函数的动态类型
    • C++规定构造函数调用顺序为从基类到派生类。若允许构造函数为虚函数,这种顺序可能被破坏,导致基类构造依赖于派生类未初始化的数据,引发逻辑错误
  1. 析构函数
  • 析构函数是对象生命周期结束时自动调用的函数,它的作用是释放对象占用的资源,执行一些必要的清理操作。

  • 析构函数特点:

    • 函数名为 ~类名
    • 没有参数;
    • 如果没有为类定义一个析构函数,编译器会自动生成一个默认析构函数,执行简单的清理操作。
  • 析构函数可以是虚函数。

    • 虚析构函数可以在运行时实现多态性;
    • 如果基类的析构函数不是虚函数,当通过基类指针去删除派生类对象时,不会调用派生类的析构函数。可能会导致派生类的资源未被正确释放,从而造成资源泄漏

C++构造函数有几种,分别什么作用

在C++中,构造函数有几种不同的类型,每种都有其特定的作用:

  1. 默认构造函数:没有参数的构造函数,用于创建对象的默认实例。
  2. 参数化构造函数:带参数的构造函数,允许在创建对象时初始化成员变量。
  3. 拷贝构造函数:以同一类的实例为参数的构造函数,用于复制已有对象。
  4. 移动构造函数:以同一类的实例的右值引用为参数,用于利用即将销毁的对象的资源。
  5. 转换构造函数:允许将其他类型或值隐式转换为当前类类型的实例。
  6. 委托构造函数:一个构造函数调用另一个构造函数来完成初始化,可以是同一个类的其他构造函数。
  7. 初始化列表构造函数:使用成员初始化列表来初始化成员变量,这是最高效的初始化方式。
  8. 常量构造函数:声明为const的构造函数,可以用于创建常量对象。
  9. constexpr构造函数:允许在编译时初始化对象,用于定义和初始化字面量类型的对象。

每种构造函数的使用场景不同,例如:

  • 默认构造函数用于快速创建对象,而不需要显式提供任何初始化参数。
  • 参数化构造函数提供了灵活性,允许在创建对象时定制其状态。
  • 拷贝构造函数移动构造函数分别用于对象的复制和移动,是实现资源管理的关键。
  • 转换构造函数委托构造函数提供了更灵活的对象初始化方式。
  • 初始化列表构造函数是C++中推荐的成员初始化方式,因为它可以提高效率。

STL 容器了解哪些

  1. 顺序容器(Sequential Containers)
  • vector

    • 特点:动态数组,内存连续,支持快速随机访问(O(1)时间复杂度);尾部插入/删除高效(O(1)),但中间或头部操作效率低(O(n))。
    • 底层实现:动态数组,自动扩容时重新分配内存并复制元素
    • 适用场景:需要随机访问且频繁在尾部增删(如动态数组、数据缓存)。
  • deque

    • 特点:双端队列,支持头尾快速插入/删除(O(1)),随机访问效率略低于vector;内存由多个连续块组成。
    • 适用场景:需两端操作且需要随机访问(如任务队列、滑动窗口算法)
  • list

    • 特点:双向链表,任意位置插入/删除高效(O(1)),但不支持随机访问;内存不连续。
    • 适用场景:频繁在任意位置增删元素(如链表式任务调度、LRU缓存)
  • **forward_list**(C++11引入)

    • 特点:单向链表,仅支持单向遍历,比list更省内存。
    • 适用场景:内存敏感且仅需单向操作的场景(如稀疏图邻接表)
  • array
    • 特点:固定大小的数组,具有静态分配的内存。
  1. 关联容器(Associative Containers)
  • set/multiset

    • 特点:基于红黑树实现,元素自动排序;set不允许重复,multiset允许重复。查找/插入/删除时间复杂度为O(log n)。
    • 适用场景:需要有序且快速查找/去重的场景(如关键词过滤、排行榜)
  • map/multimap

    • 特点:存储键值对,键唯一(map)或可重复(multimap);基于红黑树实现,按键排序。
    • 适用场景:键值对映射且需有序访问(如字典、配置管理)
  • unordered_set/unordered_multiset

    • 特点:基于哈希表实现,元素无序;插入/查找/删除平均O(1)时间复杂度(最坏O(n))。
    • 适用场景:无需排序但需快速查找(如缓存、去重)
  • unordered_map/unordered_multimap

    • 特点:键值对存储,基于哈希表实现,无序;性能与哈希函数设计强相关。
    • 适用场景:快速键值查找(如缓存系统、词频统计)
  1. 容器适配器(Container Adapters)
  • stack

    • 底层容器:默认deque,也可用vectorlist
    • 特点:后进先出(LIFO),仅允许栈顶操作。
    • 适用场景:函数调用栈、括号匹配等
  • queue

    • 底层容器:默认deque,也可用list
    • 特点:先进先出(FIFO),仅允许队首删除、队尾插入。
    • 适用场景:任务队列、BFS算法
  • priority_queue

    • 底层容器:默认vector,基于堆实现。
    • 特点:元素按优先级排序,最高优先级元素先出队。
    • 适用场景:任务调度、Dijkstra算法

深拷贝与浅拷贝的区别

  1. 浅拷贝
  • 定义:浅拷贝仅复制对象本身,不复制对象所指向的动态分配的内存。换句话说,它只复制内存中的对象副本,而不复制对象内部指向的任何动态分配的资源。
  • 实现:通常通过复制构造函数或赋值运算符实现。
  • 特点:
    • 速度快,因为只涉及基本数据类型的复制。
    • 如果原始对象包含指针,浅拷贝会导致两个对象尝试管理相同的动态内存,这可能导致多重释放和悬空指针问题
  1. 深拷
  • 定义:深拷贝不仅复制对象本身,还递归地复制对象所指向的所有动态分配的内存。这意味着每个对象都有自己的独立资源副本。
  • 实现:通常需要自定义复制构造函数或赋值运算符来确保所有动态分配的资源都被正确复制。
  • 特点:
    • 速度慢,因为需要递归地复制所有资源。
    • 可以安全地使用复制出的对象,而不担心资源管理问题。

vector和list的区别

  1. vector
  • 基于动态数组,在内存中连续存储元素。

  • 随机访问:提供快速的随机访问能力,可以通过索引快速访问任何元素。

  • 内存分配:通常在内存分配上更紧凑,因为元素紧密排列,没有额外的空间用于链接或指针。

  • 时间复杂度:

    • 元素访问:O(1),即常数时间复杂度。
    • 插入和删除:在 vector 的末尾是 O(1),但如果需要在中间插入或删除元素,则可能需要 O(n),因为可能需要移动后续所有元素。
  • 扩容时需重新分配内存(通常翻倍)并拷贝原有数据,导致性能损耗
  1. list
  • 基于双向链表,每个元素通过节点链接到前一个和后一个元素。

  • 非连续存储:元素在内存中不是连续存储的,每个元素包含指向前一个和后一个元素的指针。

  • 时间复杂度:

    • 元素访问:O(n),需要从头开始遍历到所需位置。
    • 插入和删除:非常快速,特别是当需要在列表中间插入或删除元素时,操作是 O(1),前提是已经拥有指向待插入或删除元素的迭代器。
  • 内存管理:由于元素间通过指针链接,内存分配可能更分散,但插入和删除操作不需要移动其他元素。

  1. 使用场景
  • std::vector

    • 当你需要快速随机访问元素时。
    • 当你需要在末尾快速添加或删除元素时。
    • 当你关心内存使用效率时。
  • std::list

    • 当你需要在列表中间高效地插入或删除元素时。
    • 当你不需要随机访问元素时。
    • 当你需要一个灵活的容器,可以动态地添加和删除元素而不会引起大量的内存复制或移动。

vector 底层原理和扩容过程

  1. 底层原理
  • vectorC++ 标准库中的一个容器,可以看作是一个动态数组,它的大小可以根据元素的增加而增长。它通过在堆上分配一段连续的内存空间存放元素,支持时间复杂度为 O(1 ) 的随机访问。
  • vector 底层维护了三个迭代器和两个变量,这三个迭代器分别指向对象的起始字节位置,最后一个元素的末尾字节和整个 vector 分配空间的末尾字节。两个变量分别是 sizecapacitySize 表示当前存储元素的数量,capacity 表示当前分配空间的大小。当创建一个 vector 对象时,会分配一个初始化大小的空间存放元素,初始化空间可以通过构造函数的参数指定,缺省情况下为 0。当对 vector 容器进行增加和删除元素时,只需要调整末尾元素指针,而不需要移动整个内存块。
  1. 扩容机制
  • 当添加元素的数量达到当前分配空间的大小时,vector 会申请一个更大的内存块,然后将元素从旧的内存块拷贝到新的内存块中,并释放旧的内存块。 扩容可能导致原有迭代器和指针失效,扩容完成后,容器返回指向新内存区域的迭代器或指针。

  • vector 扩容的机制分为固定扩容和加倍扩容。

    • 固定扩容就是在每次扩容时在原容量的基础上增加固定容量。但是固定扩容可能会面临多次扩容(扩容的不够大)的情况,时间复杂度较高。
    • 加倍扩容就是在每次扩容时原容量翻倍,优点是使得正常情况下扩容的次数大大减少,时间复杂度低,缺点是空间利用率低

push_back()和emplace_back()的区别

push_back()emplace_back()都是C++标准库容器(如std::vector)中用来添加元素的方法,但它们在添加元素的方式上有所不同:

  1. push_back()

    • 传入的是一个已经存在的对象,然后将其添加到容器的末尾。

      • 若传递左值(如变量),会调用拷贝构造函数
      • 若传递右值(如临时对象或 std::move 结果),会调用移动构造函数(若存在)
    • 这个方法需要先构造一个元素的副本或移动构造一个临时对象,然后再将其添加到容器中。

  2. emplace_back()

    • 传入的是构造新元素所需的参数列表。

    • 通过完美转发,以就地构造的方式,直接在容器的内存空间中构造新元素。

    • 这个方法避免了元素的复制或移动操作,因为它直接在容器的末尾空间构造新元素。

  3. 性能:

    • emplace_back()通常比push_back()更高效,因为它避免了额外的复制或移动操作。
    • 当构造函数需要大量资源时,emplace_back()的优势更加明显。

map dequeu list的实现原理

  1. std:: map
  • 基于一种自平衡的二叉搜索树——红黑树实现。
  • 元素按照键的顺序自动排序,通常是按照小于(<)运算符定义的顺序。
  • 每个键都是唯一的,不允许有重复的键。
  • 时间复杂度:提供对数时间复杂度 (O(log n)) 的查找、插入和删除操作。
  • 迭代器:由于 std::map 是基于树的,迭代器在遍历时是有序的。
  1. std::list
  • 基于双向链表,每个元素都持有指向前一个和后一个元素的指针。
  • 元素在容器中没有特定的顺序。
  • 插入和删除:提供高效的插入和删除操作,特别是当需要在容器中间插入或删除元素时。
  • 时间复杂度:提供线性时间复杂度 (O(n)) 的查找操作,但插入和删除可以在 O(1) 时间内完成,前提是已经拥有指向待插入或删除元素的迭代器。
  • 迭代器:由于 std::list 是线性结构,迭代器在遍历时是顺序的,但不支持随机访问。
  1. std::deque
  • 是一个基于动态数组的序列容器,可以高效地从两端添加或删除元素。
  • 允许序列操作:可以快速地在队列的前端和后端添加或删除元素。
  • 时间复杂度:提供常数时间复杂度 (O(1)) 的前端和后端插入和删除操作。中间插入或删除操作可能需要 O(n) 时间。

map 和 unordered_map的区别和实现机制

  1. map
  • 基于红黑树std::map 基于一种自平衡的二叉搜索树(通常是红黑树)实现,可以保持元素有序。
  • 有序容器:元素按照键的顺序自动排序,可以通过键值进行有序遍历。
  • 元素访问:提供对元素的快速查找、插入和删除操作,时间复杂度为 O(log n)
  • 唯一键:每个键都是唯一的,不允许有重复的键。
  • 迭代器稳定性:由于基于树结构,迭代器在遍历时是稳定的,即使容器发生插入或删除操作,迭代器指向的元素也不会改变,除非该元素被删除。
  1. unordered_map
  • 基于哈希表std::unordered_map 基于哈希表实现,通过哈希函数将键分布到数组的槽位中。
  • 无序容器:元素在容器中是无序的,不能按键的顺序进行遍历。
  • 元素访问:理想情况下,提供平均时间复杂度为 O(1) 的快速查找、插入和删除操作。最坏情况下,性能可能下降到 O(n)
  • 允许重复键:实际上,std::unordered_map 不允许有重复的键,因为哈希表的设计不允许两个元素具有相同的哈希值。如果发生哈希冲突,会通过某种方式(如链表或开放寻址)解决。
  • 迭代器稳定性:由于基于哈希表,迭代器的稳定性不如 std::map。在发生哈希表的重新哈希 (rehashing) 时,迭代器可能会失效。
  • 遍历顺序与创建该容器时输入元素的顺序是不一定一致的,遍历是按照哈希表从前往后依次遍历的。
  1. 使用场景
  • 当需要元素有序且对性能有较高要求时,应选择 **std::map**
  • 当元素的顺序不重要,且需要快速访问元素时,应选择 **std::unordered_map**
  1. 实现机制
  • std::map 的实现依赖于红黑树的旋转和颜色变换来保持树的平衡,确保操作的时间复杂度。
  • std::unordered_map 的实现依赖于一个良好的哈希函数来最小化冲突,并通过解决冲突的机制(如链表或开放寻址)来存储具有相同哈希值的元素。

C++11新特性有哪些

  1. 类型推导:
    • auto关键字,允许编译器根据初始化表达式推导变量类型。
    • decltype 分析表达式并得到它的类型,却不实际计算表达式的值。
  2. 基于范围的 for 循环:提供了一种更简洁的遍历容器的方法。
  3. lambda 表达式:允许在需要的地方定义匿名函数。
  4. 智能指针(如 std::unique_ptrstd::shared_ptr):提供了自动内存管理。
  5. 右值引用和移动语义:优化资源的移动操作,高效的将资源从一个对象转移到另一个对象,减少拷贝的开销。
  6. nullptr: 空指针,明确表示空指针的关键字

移动语义有什么作用,原理是什么

  1. 移动语义是 C++11 引入的一项特性,对于大型对象或包含资源的对象(如动态分配的内存、文件句柄等),复制构造函数可能会非常昂贵。移动语义允许对象的资源被“移动”到新对象,而不是进行深拷贝,从而节省资源和时间。其主要作用是优化资源的利用,特别是在对象的复制操作中。除此之外,在模板编程中,移动语义可以保留参数的值类别(左值或右值),避免不必要的拷贝。

  2. 移动语义通过移动构造函数和移动赋值运算符实现。在移动构造或移动赋值过程中,源对象的资源被“拿走”,并转移到目标对象,源对象变为无效状态。

左值引用和右值引用的区别

在C++中,左值和右值是两种不同类型的表达式,它们分别对应着不同的引用方式:

  1. 左值引用:

    • 左值引用使用&符号
    • 左值引用绑定到左值表达式上,即那些具有持久存储期的表达式,如变量或者对象。
    • 它们在内存中有一个持久的地址,可以被赋值和修改。
  2. 右值引用:

    • 右值引用使用两个&符号
    • 右值引用绑定到右值表达式上,通常是临时对象或即将销毁的对象,它们没有持久的存储期。
    • 右值引用的主要目的是通过移动语义来利用这些临时资源,避免不必要的复制。
  3. 区别总结

    • 绑定对象:左值引用绑定到具有持久状态的对象,而右值引用绑定到临时或即将销毁的对象。
    • 生命周期:左值引用延长了它所引用对象的生命周期,右值引用则表示对一个临时值的引用。
    • 可修改性:左值引用可以被用来修改其所引用的对象,而右值引用通常用于移动语义,不涉及修改。
    • 标准库支持:C++11 标准库中的某些函数和算法(如 std::move)特别设计来与右值引用配合使用。

说一下lambda函数

lambda 表达式(也称为匿名函数)是在 C++11 标准中引入的一种方便的函数编写方式。Lambda允许在代码中直接定义匿名函数对象,具有简洁、匿名、捕获上下文等特点,适合用于定义简单的函数或作为参数传递

1
[capture](parameters) -> return_type { body }

捕获列表用于将上下文中的变量捕获到Lambda表达式中。捕获方式有以下几种

  • 按值捕获:使用 = 捕获变量的值。
  • 按引用捕获:使用 & 捕获变量的引用。
  • 混合捕获:可以同时按值和按引用捕获变量。
  • 默认捕获:使用 [&] 捕获所有变量的引用,使用 [=] 捕获所有变量的值
    使用场景:
  • 作为参数传递:Lambda表达式常用于作为高阶函数的参数,如 std::for_eachstd::transformstd::sort 等。
  • 简化代码:在需要定义简单函数时,使用Lambda表达式可以简化代码。
  • 捕获上下文变量:在需要使用上下文中的变量时,Lambda表达式可以方便地捕获这些变量。

C++如何实现一个单例模式

单例模式(Singleton Pattern)是一种常用的设计模式,其核心思想是确保一个类只有一个实例,并提供一个全局访问点。单例模式在软件开发中非常有用,尤其是在需要全局共享资源(如配置管理器、日志记录器、线程池等)的场景中。C++实现单例模式需要满足以下几点要求:

  • 私有化构造函数:将类的构造函数定义为私有,防止外部通过new关键字创建多个实例。
  • 静态实例:在类内部提供一个静态私有实例,这个实例将作为整个程序的唯一实例。
  • 公有访问方法:提供一个公有的静态方法,通常称为getInstance,用于获取类的唯一实例。
  • 删除拷贝构造函数和赋值操作符:为了防止通过拷贝或赋值来创建新的实例,需要将拷贝构造函数和赋值操作符定义为私有或删除。

单例模式有懒汉式和饿汉式两种实现。

  • 懒汉式 类实例只有在第一次被使用时才会创建,这个时候需要注意多线程下的访问,需要利用互斥锁来加以控制。
  • 饿汉式 类实例在类被加载时就进行创建。

什么是菱形继承

多继承体系中,当两个派生类继承同一个基类,然后有一个最派生类同时继承这两个派生类时,就形成了菱形继承的结构。这种结构会导致基类的成员在最派生类中出现两次,因为两个派生类各自继承了基类的成员,而最派生类又继承了这两个派生类。菱形继承如果没有适当处理,会导致二义性问题,比如基类的构造函数和析构函数调用顺序问题。为了解决这个问题,可以使用虚继承。虚继承确保了基类只被继承一次,无论在继承链中出现多少次。

C++中的多线程同步机制

  1. 互斥锁(Mutex) 互斥锁是最常用的同步机制之一,- 当一个线程尝试访问共享资源时,它需要先获取互斥锁。如果互斥锁已被其他线程占用,则当前线程会阻塞,直到锁被释放。获取锁后,线程可以安全地访问共享资源,完成操作后释放锁。它可以确保在任意时刻只有一个线程能够访问共享资源。通过调用std::mutex类的lock()和unlock()方法,可以将对共享资源的访问保护起来。

  2. 条件变量(Condition Variable) 条件变量是一种机制,用于线程间的通信和同步。它允许一个或多个线程等待某个特定条件的发生,并在条件满足时被唤醒。

  3. 原子操作 原子操作是一种不可分割的操作,不会被中断。C++11引入了原子操作库,其中定义了一些原子类型,如std::atomic_int。

  4. 读写锁(Reader-Writer Lock):通过std::shared_mutex和std::shared_lock、std::unique_lock提供,用于在读多写少的情况下提高并发性。

如何在C++中创建和管理线程?

在C++中,可以使用标准库中的头文件来创建和管理线程,创建和管理线程步骤如下。

  1. 包含头文件
  2. 定义线程函数,作为线程的入口点。
  3. 创建线程对象,使用std::thread来创建线程。你可以将线程函数传递给std::thread的构造函数来指定线程执行的任务
  4. 启动线程:使用join()方法或detach()方法
  5. 等待线程结束(可选):使用join()方法启动线程,主线程将会阻塞直到新线程执行完毕,如果调用detach()方法,新线程将会变成分离状态,主线程不再与其同步,并且不再需要等待线程的结束。
Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2023-2025 John Doe
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信