c++面试题汇总(不定期更新...)

文章目录

0 引言

本文主要汇总c++面试题,包括基础部分、进程线程部分和STL库等。

1 c++基础

1.1 c和c++的区别

tip: 代表性的3点即可

相似之处:

  1. 语法结构:C和C++在语法结构上非常相似,包括标识符的命名规则、控制语句(如if、for、while)、函数定义和调用等。这意味着从C到C++的迁移相对容易,大部分C代码可以在C++中直接使用
  2. 基本数据类型:C和C++都支持相同的基本数据类型,如整型、浮点型、字符型等。它们的大小和行为在很大程度上是相同的,因此在处理基本数据类型时,C和C++之间的差异较小。
  3. 指针和内存管理:C和C++都支持指针和直接对内存进行操作的能力。它们都允许动态分配和释放内存,使用malloc和free(C)或new和delete(C++)等操作。这使得它们在对内存进行底层控制和性能优化方面非常相似。

不同之处:

  1. 面向对象编程支持C++引入了面向对象编程的概念,包括类、对象、继承、多态等。这使得C++在处理复杂系统和大型项目时更加方便和灵活。C语言没有直接支持面向对象编程,需要使用结构体和函数指针等技巧来模拟。
  2. 异常处理C++引入了异常处理机制,可以使用try-catch语句来捕获和处理异常。这使得处理错误和异常情况更加方便和可靠。C语言没有内置的异常处理机制,错误通常通过返回错误码的方式来处理。
  3. 标准库的差异:C和C++都有自己的标准库,但C++的标准库更加庞大和功能丰富。C++标准库提供了许多容器类(如vector、list、map)、算法库(如排序、查找等)和输入输出流等功能,而C的标准库相对较小,只提供了基本的输入输出、字符串处理和数学函数等。

1.2 结构体struct和类class的区别

相似之处:

  1. 成员变量结构体和类都可以包含成员变量,用于存储数据。这些成员变量可以是不同的数据类型,例如整数、浮点数、字符等。无论是结构体还是类,都可以在其内部定义和使用这些成员变量。
  2. 成员函数结构体和类都可以包含成员函数,用于操作和访问数据。这些函数可以在结构体或类的定义内部定义,并可在外部进行调用。成员函数可以访问结构体或类的成员变量,并且可以执行特定的操作。
  3. 访问控制结构体和类都支持访问修饰符,例如public、private和protected。这些修饰符用于控制成员变量和成员函数的访问权限。默认情况下,结构体的成员是公共的(public),而类的成员是私有的(private),但可以根据需要进行修改。

不同之处:

  1. 默认访问控制结构体的默认访问控制是公共的(public),这意味着其成员变量和成员函数在结构体之外可以直接访问。而类的默认访问控制是私有的(private),这意味着其成员变量和成员函数在类之外不能直接访问,需要使用公共的接口函数进行访问。
  2. 继承:类支持继承的概念,可以通过派生类扩展或修改已有的类。派生类可以继承基类的成员变量和成员函数,并可以添加自己的成员。结构体不支持继承,它们只能包含自己定义的成员。
  3. 默认构造函数当创建一个类的对象时,如果没有显式定义构造函数,编译器会自动生成一个默认构造函数。默认构造函数会初始化类的成员变量。而对于结构体,如果没有显式定义构造函数,它们的成员变量将保持未初始化的状态。

1.3 结构体struct和共同体union的区别

相似之处:

  1. 成员变量结构体和共同体都可以包含成员变量,用于存储数据。这些成员变量可以是不同的数据类型,例如整数、浮点数、字符等。无论是结构体还是共同体,都可以在其内部定义和使用这些成员变量。
  2. 成员访问结构体和共同体的成员可以通过成员运算符(.)进行访问。可以使用结构体或共同体的实例名称,后跟成员变量的名称来访问特定的成员。
  3. 内存布局:结构体和共同体都使用连续的内存空间来存储其成员变量。成员变量按照定义的顺序依次存储在内存中。这意味着可以通过指针操作来访问结构体或共同体的成员,以及进行内存操作。

不同之处:

  1. 内存共享共同体的成员变量共享相同的内存空间,这意味着同一时间只能存储一个成员的值。共同体的大小等于其最大成员的大小。只有最后一次赋值的成员在内存中是有效的。而结构体的成员变量分别占用自己的内存空间,每个成员可以独立存储值
  2. 成员访问共同体的成员变量共享内存,因此只能同时访问一个成员。当一个成员被赋值后,其他成员的值将变得不可预测。而结构体的成员可以同时访问和操作,每个成员都可以独立设置和获取值
  3. 数据类型:共同体的成员变量可以是不同的数据类型,但每次只能使用其中一个成员。结构体的成员变量可以是不同的数据类型,并且每个成员都可以独立使用。结构体更适合用于组织相关的数据,而共同体更适合在不同的数据类型之间进行转换和存储

1.4 c++指针pointer和引用reference的区别

  1. 初始化和赋值: 指针可以在声明时不必初始化,也可以在任何时候重新赋值为另一个地址。而引用在声明时必须进行初始化,并且不能在后续重新绑定到另一个变量。
  2. 空值(null): 指针可以具有空值(null),表示它不指向任何有效的内存地址。引用必须始终引用一个有效的对象,不能为null
  3. 指向的对象: 指针可以指向不同类型的对象,也可以指向没有具体类型的void指针。而引用必须与所引用的对象类型相同,它们在声明时必须指定类型。
  4. 操作符和语法: 指针使用*来解引用和->来访问指向对象的成员。引用不需要特殊操作符来访问引用的值,因为它们就是原始变量的别名。
  5. 空间需求: 指针本身需要额外的内存来存储地址信息。而引用只是原始变量的别名,不需要额外的内存空间。
  6. 安全性: 指针可以存在空指针和悬垂指针等问题,需要谨慎使用以避免错误。引用在声明时必须初始化,并且不会存在空引用的情况,因此更加安全。

1.5 c++中new和delete是如何实现的

c++中newdelete是运算符而不是函数,它们通过调用相应的运算符函数来实现动态内存分配和释放。

new的实现步骤如下:

  1. 首先,它调用名为operator new的运算符函数,该函数负责分配所需大小的内存块。
  2. 然后,它调用用于构造对象的构造函数,将对象在分配的内存块上初始化
  3. 最后,new返回指向已构造对象的指针。

delete的实现步骤如下:

  1. 首先,它调用对象的析构函数,对 对象进行析构。
  2. 然后,它调用名为operator delete的运算符函数,释放之前分配的内存块

1.6 c++中#define和const的区别

  1. 预处理器 vs. 编译器: #define预处理器指令,而 const编译器关键字#define 在编译之前进行文本替换,它不会进行类型检查,也不会分配内存。const 常量在编译器处理时会进行类型检查,并分配内存。
  2. 作用域和可见性: #define 定义的常量在整个程序中都是可见的,而 const 常量的作用域可以是全局的、局部的或类的,根据其定义的位置而定。这使得 const 更加灵活,可以在不同的作用域中定义不同的常量。
  3. 类型安全: #define 定义的常量没有类型信息,因此可以进行一些不安全的操作,例如进行不同类型的混合运算。而 const 常量具有明确定义的类型,可以进行类型检查,从而提供更好的类型安全性。
  4. 调试和可读性: #define 定义的常量在预处理阶段被替换为常量值,因此在调试过程中可能会导致困惑,因为无法查看常量的实际值。而 const 常量在运行时保留其值,可以进行调试,并提供更好的可读性。

tips: 使用 const 来定义常量更加推荐,因为它提供了更好的类型安全性、作用域控制和可读性。#define 主要可以用于宏定义和条件编译等特定的用途。

1.7 c++中关键字static的作用

  1. 静态变量(Static Variables):在函数内部使用static关键字声明的变量是静态变量。静态变量在程序的整个生命周期内都存在,并且只会被初始化一次。它们在函数调用之间保持其值不变。静态变量的作用域限制在声明它的函数内部,但是它在多次调用函数时仍然保持原来的值。例如:
void foo()
{
    static int counter = 0;  // 静态变量
    counter++;
    cout << "Counter: " << counter << endl;
}

int main()
{
    foo();  // 输出:Counter: 1
    foo();  // 输出:Counter: 2
    foo();  // 输出:Counter: 3
    return 0;
}
  1. 静态函数(Static Functions):在类中使用static关键字修饰的函数是静态函数。静态函数不依赖于类的实例,可以直接通过类名调用。与普通成员函数不同,静态函数没有隐含的this指针,因此无法访问非静态成员变量。静态函数通常用于实现与类相关的全局功能,不需要访问特定对象的状态。例如:
class MathUtils
{
public:
    static int add(int a, int b)
    {
        return a + b;
    }
};

int main()
{
    int result = MathUtils::add(3, 4);
    cout << "Result: " << result << endl;  // 输出:Result: 7
    return 0;
}
  1. 静态成员变量(Static Member Variables):在类中使用static关键字声明的成员变量是静态成员变量。静态成员变量与类的实例无关,所有类的实例共享同一个静态成员变量。它们在程序的整个执行期间保持其值不变。静态成员变量必须在类的外部进行初始化,并且在访问时通常使用作用域解析运算符::。例如:
class MyClass
{
public:
    static int count;  // 静态成员变量声明
};

int MyClass::count = 0;  // 静态成员变量初始化

int main()
{
    MyClass obj1;
    MyClass obj2;

    obj1.count = 5;
    cout << "obj1.count: " << obj1.count << endl;  // 输出:obj1.count: 5
    cout << "obj2.count: " << obj2.count << endl;  // 输出:obj2.count: 5

    obj2.count = 10;
    cout << "obj1.count: " << obj1.count << endl;  // 输出:obj1.count: 10
    cout << "obj2.count: " << obj2.count << endl;  // 输出:obj2.count: 10

    return 0;
}

1.8 堆Heap和栈Stack的区别

是一种自动分配和释放内存的数据结构,它的管理方式是由编译器自动处理的。在栈上分配的内存空间通常用于存储局部变量、函数参数以及函数调用的上下文信息。栈是一种高效的数据结构,因为它的内存分配和释放速度非常快,只需要移动栈指针即可。

栈的特点如下

  1. 自动分配和释放内存:栈上的内存分配和释放是由编译器自动处理的,无需手动管理。
  2. 有限的大小:栈的大小是有限的,通常在编译时确定,并且一般比堆小得多。
  3. 后进先出(LIFO):栈采用后进先出的原则,最后进入的数据最先被释放。
  4. 快速访问:由于栈上的内存分配是连续的,所以对栈上的数据访问速度较快。

是一种动态分配和释放内存的数据结构,它的管理方式由开发人员手动操作。在堆上分配的内存空间通常用于存储动态创建的对象,例如使用new运算符创建的对象。堆上的内存需要手动释放,否则会产生内存泄漏。

堆的特点如下

  1. 手动分配和释放内存:堆上的内存分配和释放需要手动进行,开发人员负责管理内存的分配和释放。
  2. 无限的大小:堆的大小通常受限于系统的可用内存空间,相对于栈而言,堆的大小可以更大。
  3. 任意访问:由于堆上的内存分配是非连续的,所以对堆上的数据访问速度较慢,并且需要通过指针进行访问。
  4. 灵活性:堆上的内存可以在任何时候进行分配和释放,可以动态地增加或减少堆的大小。

总的来说
栈和堆都是用于存储程序运行时所需的数据的内存空间,但它们在分配和释放方式、大小限制以及访问方式上存在区别。栈由编译器自动管理,分配和释放速度快,大小有限而堆由开发人员手动管理,分配和释放需要显式操作,大小相对无限。选择使用栈还是堆取决于所需的内存生命周期、大小和访问方式等因素。

1.9 定义Definition和声明Declaration的区别

声明是指对变量、函数或类的存在进行提前声明,告诉编译器它们的名称和类型,以便在后续的代码中使用。声明通常包括实体的名称和类型信息,但不分配内存或定义实体的具体实现。声明主要用于告知编译器有关实体的信息,以便在编译过程中进行语法检查和符号解析。

定义是指为变量、函数或类分配内存并给出具体的实现代码。定义包括声明的所有信息,并且在编译过程中将为实体分配内存空间。定义实际上是实体的创建过程,它给出了实体的具体实现细节。

总的来说

  • 声明用于提前告知编译器有关实体的名称和类型,以便在后续的代码中使用,但不分配内存或定义实体的具体实现。
  • 定义是对实体进行创建和内存分配的过程,它包括声明的所有信息,并给出了实体的具体实现细节。

举例说明

// 声明变量,告知编译器该变量的存在和类型
extern int a;

// 定义变量,分配内存并给出具体实现
int a = 10;

// 声明函数,告知编译器该函数的存在和类型
int sum(int x, int y);

// 定义函数,分配内存并给出具体实现
int sum(int x, int y) {
    return x + y;
}

// 类的声明
class MyClass;

// 类的定义
class MyClass {
    // 类的成员和实现
};

1.10 c++中的内存泄露Memory Leak的几种情况

几种常见的导致内存泄漏的情况:

  1. 未释放动态分配的内存:在使用newmalloc等操作符动态分配内存后,如果没有使用对应的deletefree等操作符释放内存,就会导致内存泄漏。例如:

    int* ptr = new int;
    // 没有释放ptr指向的内存
    ```
    
  2. 循环引用:当存在两个或多个对象之间相互引用,并且这些对象都使用动态分配的内存时,如果没有正确处理引用关系,就会导致内存泄漏。这种情况常见于使用智能指针或自定义的内存管理机制时。例如:

    class A {
        std::shared_ptr<B> bPtr;
    };
    
    class B {
        std::shared_ptr<A> aPtr;
    };
    
    // A和B对象之间形成循环引用
    // 当没有其他引用指向A和B时,它们的内存不会被释放
    ```
    
  3. 异常处理不当:在函数中发生异常时,如果没有正确处理异常导致提前退出函数,就可能导致动态分配的内存没有被释放。这种情况下,应该使用try-catch块或者使用智能指针等资源管理类来确保在异常发生时也能正确释放内存。

  4. 容器不正确地管理内存:使用容器(如std::vectorstd::list等)存储动态分配的对象时,如果不正确地管理容器中的元素,就可能导致内存泄漏。例如,在从容器中移除元素时没有释放对应的内存。

  5. 全局变量的内存泄漏如果全局变量在程序结束时仍然持有动态分配的内存,就会导致内存泄漏。这种情况下,可以在程序结束前显式地释放全局变量占用的内存。

1.11 栈溢出Stack Overflow的原因和解决办法

栈溢出通常是由以下原因导致的:

  1. 递归调用层数过多:在递归函数中,每次递归调用都会在栈上分配一段内存用于保存函数的局部变量和返回地址。如果递归调用层数过多,栈空间会被耗尽,导致栈溢出
  2. 大量的局部变量:如果函数中使用了大量的局部变量,这些变量会占用栈空间。当函数嵌套调用或递归调用时,栈上的局部变量会累积,超过栈的容量导致溢出
  3. 过大的数组:在函数中声明过大的数组,尤其是在栈上分配而不是使用动态内存分配,会导致栈空间不足,引发栈溢出。

解决栈溢出问题的方法包括:

  1. 优化递归算法:对于递归调用层数过多导致的栈溢出,可以通过优化递归算法,减少递归深度或改用迭代方式实现,从而避免栈溢出
  2. 使用动态内存分配将大的数据结构或数组等从栈上分配改为使用动态内存分配,例如使用newdelete或智能指针来管理内存,避免占用过多的栈空间。
  3. 增加栈的大小限制:在编译和链接时,可以增加栈空间的大小限制。但需要注意,栈的大小是有限的,过大的栈空间可能导致系统资源不足或其他问题。
  4. 减少局部变量的使用:尽量减少函数中的局部变量的数量和大小,特别是在递归调用或函数嵌套较深的情况下。
  5. 使用循环替代递归:对于递归算法,可以考虑使用循环来代替递归,从而避免栈溢出的风险。

1.12 指针数组Pointer Array和数组指针Array Pointer的区别

  1. 指针数组

    是指一个数组,其中的每个元素都是指针类型。它是一个固定长度的数组,每个元素都可以指向不同的对象或内存地址。声明一个指针数组时,需要指定数组的长度和元素类型。例如:

    int* ptrArray[5];  // 声明一个包含5个指针元素的指针数组
    /*
    在上述示例中,`ptrArray`是一个包含5个指针元素的指针数组,每个元素都是指向`int`类型的指针。
    
    可以通过下标来访问指针数组的元素,例如`ptrArray[0]`、`ptrArray[1]`等,这些元素是指针类型,可以用于存储指向不同对象的地址。
    */
    
  2. 数组指针

    是指一个指针,它指向一个数组。它是一个指向数组的指针,而不是一个数组本身。声明一个数组指针时,需要指定数组的类型。例如:

    int (*arrPtr)[5];  // 声明一个指向包含5个int元素的数组的指针
    /*
    在上述示例中,`arrPtr`是一个指向包含5个`int`元素的数组的指针。
    
    可以通过解引用数组指针来访问数组的元素,例如`(*arrPtr)[0]`、`(*arrPtr)[1]`等。这样可以使用数组的下标方式来访问数组元素。
    */
    

总的来说

  • 指针数组是一个数组,其中的每个元素都是指针类型,可以指向不同的对象或内存地址。
  • 数组指针是一个指针,它指向一个数组,可以通过解引用来访问数组的元素。

1.13 全局变量和局部变量有什么区别

  1. 生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
  2. 使用方式不同:通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。
  3. 内存分配位置不同:全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面

1.14 sizeof 和 strlen 的区别

sizeofstrlen是两个不同的操作符,用于获取对象的大小和字符串的长度,它们的使用方式和作用有所区别:

  1. sizeof

    是C++的一个操作符,用于获取对象或类型的大小(以字节为单位)。它可以用于编译时确定对象或类型的大小,不需要实际运行程序。sizeof操作符可以用于获取各种类型的大小,包括基本类型、数组、结构体、类、指针等。例如:

    int size1 = sizeof(int);  // 获取int类型的大小
    int arr[5];
    int size2 = sizeof(arr);  // 获取数组arr的大小
    class MyClass {
        int a;
        double b;
    };
    int size3 = sizeof(MyClass);  // 获取类MyClass的大小
    ```
    上述示例中,`sizeof`操作符用于获取不同对象或类型的大小,它返回的结果是一个编译时常量。
    
  2. strlen

    是一个C标准库函数,用于获取以null字符(‘\0’)结尾的字符串的长度,即字符串中的字符个数。strlen函数需要在运行时对字符串进行遍历,直到遇到null字符才会停止计数。例如:

    const char* str = "Hello";
    int len = strlen(str);  // 获取字符串str的长度
    ```
    上述示例中,`strlen`函数会计算字符串"Hello"的长度,结果是5

1.15 头文件中的 #ifndef / #define / #endif 主要作用

总的来说:头文件中的#ifndef / #define / #endif是一种条件编译指令,用于防止头文件的重复包含。

详细解释:当一个源文件(例如.cpp文件)包含一个头文件(例如.h文件)时,预处理器会将头文件的内容插入到源文件中。如果在同一个源文件中多次包含相同的头文件,就会导致头文件内容的重复插入,从而引发编译错误

为了避免这种情况,可以使用#ifndef / #define / #endif指令来保护头文件内容。具体作用如下:

  1. ifndef(if not defined):用于检查某个标识符是否已经定义过。如果该标识符尚未定义,则执行后续代码。例如:
#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H

// 头文件内容

#endif
  1. define:在ifndef指令的条件为真时,定义一个标识符。这通常是一个宏定义,用于确保只有第一次包含头文件时,该标识符被定义。例如,上述代码中的MY_HEADER_FILE_H就是一个标识符。

  2. endif:结束条件编译块。用于标记条件编译的结束。

当源文件第一次包含头文件时,ifndef条件为真,define指令会定义标识符。此后,如果其他源文件再次包含相同的头文件,由于标识符已经定义,ifndef条件为假,头文件的内容将被跳过,从而避免了重复插入。

1.16 c++中左值引用和右值引用

左值引用是对左值的引用。左值是指具有名称、可以寻址的对象。左值引用使用&符号声明。它可以绑定到左值,并提供对其所引用的对象的别名。左值引用可以用于修改所引用的对象,因为它们是对对象的别名。

下面是一个使用左值引用的示例:

int x = 10;
int& ref = x;  // 左值引用
ref = 20;     // 修改所引用的对象

在上面的示例中,refx的左值引用,通过修改ref可以修改x的值。

右值引用是对右值的引用。右值是指临时对象、表达式的结果或无法寻址的对象。右值引用使用&&符号声明。右值引用允许获取右值的引用,以便进行移动语义和完美转发。

右值引用的主要应用之一是支持移动语义,它允许高效地转移资源所有权而不需要进行深拷贝。移动语义对于管理动态分配的资源(如内存)或具有大量数据的对象非常有用。

下面是一个使用右值引用的示例:

std::string getName() {
    return "ZPILOTE";
}

std::string&& rref = getName();  // 右值引用

在上面的示例中,getName()返回一个临时的std::string对象,而rref是对该临时对象的右值引用。

右值引用还可以与std::move一起使用,将左值转换为右值,以便进行移动语义的操作。例如:

std::string str1 = "Hello";
std::string str2 = std::move(str1);  // 使用 std::move 将 str1 转换为右值

在上面的示例中,std::movestr1转换为右值,使得str2可以高效地获取str1的资源所有权。

总的来说

  • 左值引用是对左值的引用,可以修改所引用的对象
  • 右值引用是对右值的引用,用于支持移动语义和完美转发
  • 右值引用对于处理临时对象和资源管理非常有用,可以通过移动语义高效地转移资源所有权。
  • 右值引用通常与std::move一起使用,用于将左值转换为右值。

1.17 c++中main函数执行之前,还会执行什么代码

  1. 静态对象的构造函数:C++中的静态对象(全局变量、静态变量)会在程序启动时自动创建,并调用它们的构造函数进行初始化。这些对象的构造函数会在main函数执行之前被调用。
  2. 全局变量的初始化:全局变量(包括静态全局变量)在程序启动时会被初始化。它们的初始化顺序取决于它们的声明顺序。
  3. 调用__attribute__((constructor))属性修饰的函数:某些编译器提供了一种特殊的属性__attribute__((constructor)),它可以用于标记函数,在main函数执行之前自动调用。这些函数可以用于执行一些初始化操作,例如设置全局变量、注册回调函数等。

1.18 深拷贝和浅拷贝的区别

  1. 浅拷贝是一种简单的拷贝方式,它只复制对象的值或指针,而不复制对象所指向的内容。浅拷贝仅复制对象中的数据,而不会为指向的资源(如堆内存)创建新的副本。这意味着原始对象和拷贝对象将共享相同的资源。当进行浅拷贝时,如果原始对象和拷贝对象指向相同的资源,那么对其中一个对象的修改会影响到另一个对象。默认情况下,C++编译器提供的拷贝构造函数和赋值操作符执行的是浅拷贝
  2. 深拷贝是一种更复杂的拷贝方式,它不仅复制对象的值或指针,还复制对象所指向的内容。深拷贝会为每个指向的资源(如堆内存)创建新的副本,确保原始对象和拷贝对象具有独立的资源。这意味着原始对象和拷贝对象之间的修改互不影响。对于包含动态分配内存或指向其他资源的指针的对象,通常需要使用深拷贝来确保数据的完整性

tips: 为了实现深拷贝,通常需要自定义拷贝构造函数和赋值操作符,以确保指向的资源被逐个复制而不是简单复制指针。

1.19 如何理解封装、继承和多态

  1. 封装是将数据和操作封装在一个单元中的概念。通过封装,可以将相关的数据和函数(或方法)组合成一个类,隐藏内部实现细节,并提供公共接口供外部使用。封装可以通过访问修饰符(如public、private和protected)来控制成员的可访问性。这样,封装提供了数据的安全性和代码的模块化,使得对象的使用和维护更加方便。
  2. 继承是一种通过从现有类派生出新类的机制。通过继承,新类(称为派生类或子类)可以继承基类(称为基类或父类)的属性和行为,并可以在此基础上添加新的成员或修改已有成员。继承实现了代码的重用性和层次结构的概念,使得类之间可以建立一种"is-a"的关系。派生类可以访问基类的公共和保护成员,但不能访问基类的私有成员。
  3. 多态是指同一操作在不同对象上产生不同的行为。它通过运行时的动态绑定来实现,允许使用基类的指针或引用来引用派生类的对象,并根据对象的实际类型来调用相应的成员函数。多态性可以通过虚函数(使用virtual关键字声明的成员函数)和函数重写(在派生类中重新定义基类的虚函数)来实现。多态提供了灵活性和可扩展性,使得代码可以根据具体对象的类型来执行不同的操作

简述来说

  • 封装将数据和操作封装在一个单元中,隐藏内部实现细节,并提供公共接口供外部使用。
  • 继承通过从现有类派生出新类,实现属性和行为的继承,并可以添加新成员或修改已有成员。
  • 多态同一操作在不同对象上产生不同的行为,通过运行时的动态绑定和虚函数来实现。

1.20 构造函数和析构函数的执行顺序

  1. 构造函数的执行顺序

    • 首先,基类的构造函数会在派生类的构造函数之前被调用。这意味着在派生类的构造函数中,基类的成员已经被初始化。
    • 接着,按照派生类继承的顺序,依次调用各个基类的构造函数。
    • 最后,调用派生类自身的构造函数。

    简述来说,构造函数的执行顺序是先基类,再派生类

  2. 析构函数的执行顺序

    • 首先,派生类自身的析构函数会被调用。
    • 接着,按照派生类继承的顺序,依次调用各个基类的析构函数。
    • 最后,基类的析构函数会在派生类的析构函数之后被调用。

    简述来说,析构函数的执行顺序是先派生类,再基类

1.21 如何理解虚函数

虚函数(Virtual Function)是一种特殊的成员函数,用于实现多态(Polymorphism)。通过使用虚函数,可以在基类中声明一个函数,然后在派生类中进行重写,以便在运行时根据对象的实际类型来调用相应的函数。

以下是理解虚函数的关键要点:

  1. 虚函数的声明:在基类中,将希望派生类进行重写的成员函数声明为虚函数。使用关键字virtual来修饰函数声明,示例:virtual void myFunction();
  2. 函数的重写:在派生类中,通过重新定义基类的虚函数,实现对函数的重写。重写时需要使用相同的函数签名(函数名、参数列表和返回类型),并使用**关键字override**来明确指示重写的意图,示例:void myFunction() override;
  3. 运行时多态性:当使用基类的指针或引用指向派生类的对象时,通过调用虚函数,可以根据对象的实际类型来调用相应的函数。这种动态绑定发生在运行时,使得程序能够根据对象的实际类型执行不同的函数,实现多态性。
  4. 虚函数表(Virtual Function Table):编译器为具有虚函数的类创建一个虚函数表,其中存储了函数指针。每个对象都包含一个指向虚函数表的指针(通常称为虚函数表指针),用于在运行时查找并调用正确的虚函数。
  5. 虚析构函数:当一个类中包含虚函数时,通常应该将析构函数声明为虚析构函数。这样,在通过基类指针删除指向派生类对象的对象时,可以确保调用派生类的析构函数来正确释放资源。

1.22 静态绑定和动态绑定的区别

  1. 静态绑定
    • 在编译时确定函数调用的目标。
    • 使用静态绑定时,函数调用根据变量的静态类型(声明时的类型)来确定。
    • 静态绑定适用于非虚函数和静态成员函数。
    • 静态绑定的优点是执行效率高,因为在编译时已经确定了函数调用的目标,无需运行时的查找和判断。
  2. 动态绑定
    • 在运行时根据对象的实际类型确定函数调用的目标。
    • 使用动态绑定时,函数调用根据变量的实际类型来确定,即运行时类型。
    • 动态绑定适用于虚函数和使用基类指针或引用调用的函数。
    • 动态绑定的优点是实现了多态性,能够根据对象的实际类型调用相应的函数,具有灵活性和可扩展性。

总的来说

  • 静态绑定在编译时确定函数调用的目标,根据变量的静态类型进行绑定,适用于非虚函数和静态成员函数
  • 动态绑定在运行时确定函数调用的目标,根据对象的实际类型进行绑定,适用于虚函数和使用基类指针或引用调用的函数

1.22 如何计算类的大小

sizeof运算符可以用于计算任何类型的大小,包括类。以下是关于类大小计算的要点:

  1. 使用方法:使用sizeof运算符后面跟着类的名称或对象的名称,例如sizeof(MyClass)sizeof(myObject)
  2. 计算结果:sizeof运算符返回一个size_t类型的值,表示类的大小(以字节为单位)。
  3. 类大小计算规则:
    • 类的大小包括其成员变量(包括静态成员变量)和可能存在的额外字节对齐空间。
    • 字节对齐是为了优化内存访问和处理器的对齐要求。编译器可能会在成员变量之间插入额外的字节来满足对齐要求。
    • 对于继承关系,派生类的大小包括其自身的成员变量和从基类继承的成员变量
    • 对于虚函数,通常会有一个指针(通常被称为虚表指针)用于指向虚函数表。虚函数表的大小不计入类的大小中

1.23 c++空类默认有哪些成员函数

  1. 默认构造函数(Default Constructor)
  2. 析构函数(Destructor)
  3. 拷贝构造函数(Copy Constructor)
  4. 拷贝赋值运算符(Copy Assignment Operator)
  5. 移动构造函数(Move Constructor)
  6. 移动赋值运算符(Move Assignment Operator)

1.24 c++的五种构造函数

  • 默认构造函数、参数化构造函数、拷贝构造函数、移动构造函数和析构函数
  • 默认构造函数用于创建对象的默认状态。
  • 参数化构造函数用于在对象创建时进行自定义初始化。
  • 拷贝构造函数用于对象之间的值拷贝。
  • 移动构造函数用于转移对象的资源或状态。
  • 析构函数用于在对象销毁时执行清理操作。

1.25 如果虚函数是有效的,那为什么不把所有函数设为虚函数

  1. 性能开销:虚函数调用涉及虚表(v-table)的查找,这会引入一定的性能开销。相比于非虚函数的直接调用,虚函数的调用通常需要额外的指针查找和跳转操作。对于频繁调用的函数或需要高性能的场景,这种开销可能会显著影响程序的性能。
  2. 内存占用:每个包含虚函数的类都会在对象中保留一个虚表指针(vptr)作为额外的内存开销。对于大量的对象或者内存受限的环境,这可能会导致不必要的内存占用。
  3. 设计清晰性:将所有函数都设为虚函数可能会导致代码结构不清晰,使得程序的逻辑和设计变得复杂。虚函数的使用应该是有目的性的,用于实现多态性和基于继承的行为扩展。

2 多线程和多进程

2.1 进程和线程的区别

  1. 资源拥有:进程是独立的实体,拥有独立的内存空间和系统资源,而线程是进程的一部分,共享进程的内存空间和系统资源
  2. 数据共享:进程之间不能直接共享数据,需要通过进程间通信(IPC)的机制来进行数据交换。线程之间可以直接共享数据,共享内存区域可以实现线程间的通信
  3. 执行单元:进程是一个程序的执行实例,可以包含多个线程。线程是进程中的一个执行单元,多个线程可以在同一个进程中并发执行。
  4. 切换开销:进程之间的切换开销较大,需要保存和恢复大量的状态信息。线程之间的切换开销较小,不需要保存和恢复完整的进程状态,只需切换线程的上下文环境。
  5. 隔离性和安全性每个进程都有自己的地址空间,进程之间相互隔离,保证了进程之间的隔离性和安全性。线程共享进程的地址空间,需要通过同步机制来确保线程之间的数据访问的正确性和安全性。

2.2 进程之间的通信方式

管道(Pipe)

  • 定义:管道是一种半双工的通信方式,用于在两个相关的进程之间传递数据。它可以是匿名管道(在父进程和子进程之间建立)或命名管道(在无关进程之间建立)。
  • 特点
    1. 管道是一种单向通信方式,数据只能在一个方向上流动。
    2. 匿名管道只能在有亲缘关系的进程之间使用,而命名管道可以在无关进程之间使用。
    3. 管道通信是基于字节流的,没有记录边界。
    4. 管道的容量有限,当管道已满时,写入进程会被阻塞,直到管道有空间可用。
  • 示例
int pipefd[2];
pipe(pipefd);

命名管道(Named Pipe)

  • 定义:命名管道是一种具有特定名称的管道,用于在无关的进程之间进行通信。它被映射到文件系统中的一个路径,进程可以通过打开该路径的方式进行通信。
  • 特点
    1. 命名管道可以在无关进程之间使用,提供了一种进程间的通用通信机制。
    2. 命名管道通信是基于字节流的,没有记录边界。
  • 示例
mkfifo("/path/to/named_pipe", 0666);

消息队列(Message Queue)

  • 定义:消息队列是一种按照消息的类型进行排序的通信方式。进程可以通过消息队列发送和接收消息,每个消息都有一个特定的类型和优先级
  • 特点
    1. 消息队列提供了一种异步通信方式,发送进程不需要等待接收进程的响应。
    2. 消息队列可以存储一定数量的消息,如果队列已满,发送进程可以选择阻塞或丢弃消息
    3. 消息队列的消息可以按照类型进行排序和优先级处理。
  • 示例
int msgid = msgget(key, IPC_CREAT | 0666);

共享内存(Shared Memory)

  • 定义:共享内存是一种进程间共享数据的高效方式。多个进程可以将同一块内存映射到它们的地址空间中,从而实现数据的共享
  • 特点
    1. 共享内存提供了进程间直接共享数据的方式,无需进行数据拷贝。
    2. 进程可以通过读写共享内存来进行通信,对共享内存的修改会立即对其他进程可见。
    3. 共享内存需要通过同步机制(如信号量)来保证多个进程对共享内存的访问的正确性和安全性。
  • 示例
int shmid = shmget(key, size, IPC_CREAT | 0666);
void* shmaddr = shmat(shmid, NULL, 0);

信号量(Semaphore)

  • 定义:信号量是一种用于进程间同步和互斥的机制,用于控制对共享资源的访问。进程可以通过操作信号量来进行互斥、同步和资源分配
  • 特点
    1. 信号量可以用于实现进程间的互斥(Mutex)和同步(Semaphore)。
    2. 进程可以通过等待和释放信号量来实现对共享资源的访问控制。
    3. 信号量可以用于解决经典的同步问题,如生产者-消费者问题、读者-写者问题等。
  • 示例
int semid = semget(key, num_sems, IPC_CREAT | 0666);

2.3 线程之间的通信方式

共享内存(Shared Memory)

  • 定义:共享内存是一种线程间共享数据的高效方式。多个线程可以将同一块内存映射到它们的地址空间中,从而实现数据的共享
  • 特点
    1. 共享内存提供了线程间直接共享数据的方式,无需进行数据拷贝。
    2. 线程可以通过读写共享内存来进行通信,对共享内存的修改会立即对其他线程可见。
    3. 共享内存需要通过同步机制(如互斥锁、读写锁、条件变量)来保证多个线程对共享内存的访问的正确性和安全性。

互斥锁(Mutex)

  • 定义:互斥锁是一种用于线程间互斥访问共享资源的机制。只能有一个线程获得互斥锁,其他线程需要等待锁被释放才能访问共享资源。
  • 特点:
    1. 互斥锁用于保护临界区,确保同一时间只有一个线程能够进入临界区执行代码。
    2. 当一个线程获得互斥锁时,其他线程需要等待锁被释放才能继续执行。
    3. 互斥锁可以解决线程间的竞争条件和数据不一致性问题

条件变量(Condition Variable)

  • 定义:条件变量是一种线程间同步和通信的机制,用于在线程之间等待特定条件满足或发出信号通知其他线程。
  • 特点:
    1. 条件变量用于线程间的等待和通知机制,允许线程等待某个条件满足后再继续执行。
    2. 线程可以通过条件变量等待特定条件的发生,而不需要忙等待或占用CPU资源。
    3. 条件变量通常与互斥锁一起使用,在等待条件时会释放锁,等到条件满足时重新获得锁。

信号量(Semaphore)

  • 定义:信号量是一种用于线程间同步和互斥的机制,用于控制对共享资源的访问。线程可以通过操作信号量来进行互斥、同步和资源分配
  • 特点:
    1. 信号量可以用于实现线程间的互斥和同步。
    2. 线程可以通过等待和释放信号量来实现对共享资源的访问控制。
    3. 信号量可以用于解决经典的同步问题,如生产者-消费者问题、读者-写者问题等。

消息队列(Message Queue)

  • 定义:消息队列是一种按照消息的类型进行排序的通信方式。线程可以通过消息队列发送和接收消息,每个消息都有一个特定的类型和优先级
  • 特点:
    1. 消息队列提供了一种异步通信方式,发送线程不需要等待接收线程的响应。
    2. 消息队列可以存储一定数量的消息,如果队列已满,发送线程可以选择阻塞或丢弃消息。
    3. 消息队列的消息可以按照类型进行排序和优先级处理。

2.4 造成线程不安全的原因

竞态条件(Race Condition)

  • 原因:线程不安全的竞态条件通常是由于缺乏适当的同步机制(如互斥锁、原子操作等)导致的。当多个线程同时读写共享资源时,没有合适的保护机制来确保数据的一致性和正确性,从而导致线程间的竞争条件。

数据依赖性(Data Dependency)

  • 原因:线程不安全的数据依赖通常涉及共享数据的读取和写入。当一个线程正在读取或写入共享数据时,其他线程也在读取或写入相同的数据,而没有适当的同步措施来保护数据的一致性和正确性。

死锁(Deadlock)

  • 原因:线程不安全的死锁通常是由于线程持有资源并等待其他线程所持有的资源,形成循环等待的情况。如果没有适当的资源分配策略和同步机制,就可能导致线程间的死锁情况。

缓存一致性(Cache Coherence)

  • 原因:线程不安全的缓存一致性问题通常是由于缓存数据的复制和更新导致的。当多个线程同时对共享数据进行读写操作时,缓存中的数据可能不同步,从而导致线程间的不一致性和不安全性。

2.5 c++中有几类线程锁

互斥锁(Mutex)

  • 定义:互斥锁是一种用于线程间互斥访问共享资源的同步机制。只有一个线程可以获得互斥锁,其他线程需要等待锁被释放才能访问共享资源
  • 特点
    1. 互斥锁是最常见和基本的线程锁,用于保护临界区,确保同一时间只有一个线程能够进入临界区执行代码。
    2. 当一个线程获得互斥锁时,其他线程需要等待锁被释放才能继续执行。
    3. 互斥锁可以解决线程间的竞争条件和数据不一致性问题。

递归锁(Recursive Lock)

  • 定义:递归锁是一种特殊的互斥锁,允许同一线程多次获得锁。同一线程可以多次对递归锁进行加锁和解锁操作,而不会发生死锁
  • 特点
    1. 递归锁允许同一线程在持有锁的情况下再次获得锁,形成嵌套的加锁和解锁操作。
    2. 在多层函数调用或递归操作中,递归锁可以方便地保证同一线程对临界区的访问。
    3. 在使用递归锁时,需要注意锁的加锁和解锁操作要成对出现,否则可能导致死锁或未预期的行为。

读写锁(Read-Write Lock)

  • 定义:读写锁是一种提供共享读和独占写访问的线程锁。多个线程可以同时获取读锁进行并发读取,但只有一个线程可以获取写锁进行独占写入
  • 特点
    1. 读写锁适用于读多写少的场景,可以提高并发性能。
    2. 多个线程可以同时获得读锁,允许并发读取共享资源,不会相互阻塞。
    3. 写锁是独占的,一旦有线程获得写锁,其他线程无法获得读锁或写锁,直到写锁被释放。

2.6 什么是死锁,如何解决

死锁通常由以下四个必要条件引起:

  1. 互斥条件(Mutual Exclusion):资源一次只能被一个线程或进程占用。
  2. 请求与保持条件(Hold and Wait):线程或进程在持有资源的同时,又请求获取其他线程或进程占用的资源。
  3. 不可抢占条件(No Preemption):资源不能被强制性地从一个线程或进程中剥夺,只能由持有者主动释放。
  4. 循环等待条件(Circular Wait):存在一组线程或进程,每个线程或进程都在等待下一个线程或进程所持有的资源。

解决方法

  1. 预防死锁:通过破坏死锁产生的四个必要条件之一来预防死锁。例如,避免循环等待条件,确保线程或进程按照相同的顺序请求资源,或者引入资源优先级来防止死锁。
  2. 避免死锁:使用算法来检测潜在的死锁情况,并在执行前进行安全性检查。例如,银行家算法(Banker’s Algorithm)可以预测资源分配是否会导致死锁,并避免分配会导致死锁的资源。
  3. 检测与恢复死锁:在运行时,检测死锁的存在并采取适当的措施。这可以通过资源分配图(Resource Allocation Graph)或死锁检测算法来实现。一旦检测到死锁,可以采取策略来恢复死锁,如终止某些进程、回滚操作或进行资源剥夺。
  4. 避免资源竞争:合理地设计程序和数据结构,避免出现数据竞争和资源争用的情况,从根本上减少死锁的可能性。

3 STL库

3.1 SLT库的六大组件有哪些

  • 容器(Container)
  • 算法(Algorithm)
  • 迭代器(Iterator)
  • 仿函数(Function object)
  • 适配器(Adaptor)
  • 空间配置器(allocator)

3.2 常用的STL容器有哪些

C++中的STL(标准模板库)提供了一组丰富的容器类,用于存储和操作数据。以下是C++中常用的STL容器:

  1. vector(动态数组):vector是一个动态大小的数组,支持快速随机访问和在尾部插入/删除元素。
  2. list(双向链表):list是一个双向链表,支持在任意位置插入/删除元素,但随机访问效率较低。
  3. deque(双端队列):deque是一个双端队列,支持在两端插入/删除元素,也支持快速随机访问。
  4. queue(队列):queue是一个先进先出(FIFO)的队列,支持在一端插入,在另一端删除元素。
  5. stack(栈):stack是一个后进先出(LIFO)的栈,支持在一端插入和删除元素。
  6. set(集合):set是一个按照键值自动排序的集合,不允许重复元素。
  7. multiset(多重集合):multiset是一个允许重复元素的集合,元素按键值自动排序。
  8. map(映射):map是一种键值对的关联容器,按照键自动排序,不允许重复的键。
  9. multimap(多重映射):multimap是一种允许重复键的映射容器,按照键自动排序。
  10. unordered_set(无序集合):unordered_set是一个哈希表实现的集合,不允许重复元素。
  11. unordered_multiset(无序多重集合):unordered_multiset是一个哈希表实现的多重集合,允许重复元素。
  12. unordered_map(无序映射):unordered_map是一种哈希表实现的键值对关联容器,不允许重复的键。
  13. unordered_multimap(无序多重映射):unordered_multimap是一种哈希表实现的允许重复键的映射容器。

3.3 常用的STL算法有哪些

C++的STL还提供了一组强大的算法,用于对容器中的元素进行各种操作和处理。以下是C++中常用的STL算法:

  1. for_each:对容器中的每个元素应用指定的函数。
  2. find:在容器中查找指定值的第一个匹配项。
  3. find_if:在容器中查找满足指定条件的第一个元素。
  4. count:计算容器中等于指定值的元素数量。
  5. count_if:计算容器中满足指定条件的元素数量。
  6. sort:对容器中的元素进行排序。
  7. reverse:反转容器中元素的顺序。
  8. transform:对容器中的每个元素应用指定的函数,并将结果存储到另一个容器中。
  9. copy:将容器中的元素复制到另一个容器中。
  10. remove:从容器中删除等于指定值的元素。
  11. remove_if:从容器中删除满足指定条件的元素。
  12. unique:从容器中删除连续的重复元素。
  13. replace:将容器中等于指定值的元素替换为新值。
  14. replace_if:将容器中满足指定条件的元素替换为新值。
  15. max_element:找到容器中的最大元素。
  16. min_element:找到容器中的最小元素。
  17. accumulate:计算容器中元素的累加和。
  18. binary_search:在已排序的容器中执行二分查找。
  19. merge:将两个已排序的容器合并为一个有序容器。
  20. random_shuffle:随机打乱容器中的元素顺序。

3.4 STL中map 、set、multiset、multimap的底层原理(关联式容器)

这些容器的底层实现都基于平衡二叉搜索树(BST)或红黑树,以提供高效的查找和插入操作。

红黑树是一种自平衡的二叉搜索树,具有以下特性:

  • 节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 所有叶子节点(NIL节点)都是黑色。
  • 如果一个节点是红色的,则其子节点必须是黑色的。
  • 从根节点到叶子节点的任意路径上,黑色节点的数量相同。

红黑树的平衡性质保证了插入、查找和删除操作的时间复杂度为O(log N),其中N是树中节点的数量。红黑树的插入和删除操作会对树进行旋转和重新着色,以保持平衡。

3.5 hash_map与map的区别

  1. 底层实现map使用红黑树作为底层数据结构,而hash_map(在C++11之后被推荐使用unordered_map)使用哈希表作为底层数据结构。
  2. 查找操作的复杂度:在map中,查找操作的平均时间复杂度为O(log N),其中N是元素的数量。因为红黑树保持了元素的有序性,所以它提供了高效的查找操作。而在hash_map中,查找操作的平均时间复杂度为O(1),即常数时间。哈希表通过将元素映射到桶(bucket)来实现快速的查找。
  3. 元素顺序map中的元素按照键的顺序进行排序,因此可以进行范围查询和有序遍历。而hash_map中的元素是无序的,没有固定的顺序。
  4. 插入和删除操作的复杂度:对于map,插入和删除操作的平均时间复杂度也为O(log N),因为红黑树需要保持平衡。而对于hash_map,插入和删除操作的平均时间复杂度为O(1),即常数时间。
  5. 哈希冲突:在hash_map中,由于哈希函数的映射可能导致不同的元素散列到同一个桶中,称为哈希冲突。为了解决冲突,hash_map使用链表或其他方法来处理具有相同哈希值的元素。而在map中,由于使用红黑树作为底层数据结构,不需要处理哈希冲突的问题。

3.6 unordered_map和map的区别

  1. 底层实现unordered_map使用哈希表(散列表)作为底层数据结构,而map使用红黑树(一种自平衡的二叉搜索树)作为底层数据结构。
  2. 查找操作的复杂度:在unordered_map中,查找操作的平均时间复杂度为O(1),即常数时间。哈希表根据元素的键直接计算哈希值,并通过哈希函数将元素映射到桶(bucket)。因此,unordered_map提供了非常快速的查找操作。而在map中,查找操作的平均时间复杂度为O(log N),其中N是元素的数量。红黑树通过比较键的大小来进行查找操作,保持了元素的有序性。
  3. 元素顺序unordered_map中的元素是无序的,没有固定的顺序。而map中的元素按照键的顺序进行排序,因此可以进行范围查询和有序遍历。
  4. 插入和删除操作的复杂度:对于unordered_map,插入和删除操作的平均时间复杂度为O(1),即常数时间。因为哈希表能够快速定位到元素应该插入或删除的位置。而对于map,插入和删除操作的平均时间复杂度为O(log N),因为红黑树需要保持平衡。
  5. 哈希冲突:在unordered_map中,由于哈希函数的映射可能导致不同的元素散列到同一个桶中,称为哈希冲突。为了解决冲突,unordered_map使用链表、开放地址法或其他方法来处理具有相同哈希值的元素。而在map中,由于使用红黑树作为底层数据结构,不需要处理哈希冲突的问题。

3.7 vector和list的区别

  1. 底层实现vector是使用动态数组实现的,它在内存中是连续存储的,可以通过索引进行快速访问。而list是使用双向链表实现的,每个元素都包含指向前一个和后一个元素的指针。
  2. 内存分配vector在创建时会分配一块连续的内存空间,可以在常数时间内访问任意元素。而list的元素在内存中可以是分散的,无法通过索引直接访问元素,需要遍历链表来访问特定位置的元素。
  3. 插入和删除操作的复杂度:对于vector,在尾部插入或删除元素的平均时间复杂度为O(1),即常数时间。然而,在中间或开头插入或删除元素时,需要将后续元素进行移动,平均时间复杂度为O(N),其中N是元素数量。而对于list,在任何位置插入或删除元素的时间复杂度都是O(1),因为只需要更改相邻元素的指针。
  4. 迭代器的稳定性vector的迭代器在插入和删除操作后可能失效,因为元素的移动可能导致迭代器指向无效的位置。而list的迭代器在插入和删除操作后仍然有效,因为链表结构不会改变。
  5. 空间占用:由于vector使用连续的内存空间存储元素,它的空间占用相对较小。而list的每个元素都需要额外的指针来链接前后元素,因此它的空间占用相对较大。
  6. 随机访问和顺序访问:由于vector的元素是连续存储的,它提供了快速的随机访问能力,可以通过索引直接访问任意位置的元素。而list只能通过遍历链表进行顺序访问,不能进行随机访问。

3.8 vector使用注意事项

  1. 动态增长的开销vector是一个动态数组,它可以根据需要自动增长大小。但是,每次增长大小都会涉及重新分配内存和复制元素的操作,这可能会导致性能开销。如果事先知道vector需要存储的元素数量,可以使用reserve()函数预留足够的容量,以避免频繁的重新分配。
  2. 避免迭代器失效:在向vector插入或删除元素时,要注意迭代器的失效问题。插入或删除元素后,之前获取的迭代器可能会失效,指向无效的位置。为了避免迭代器失效,可以使用索引来访问元素,或者在插入或删除元素后更新迭代器。
  3. 避免越界访问vector提供了通过索引随机访问元素的能力,但要确保不要越界访问。访问超出vector的有效索引范围将导致未定义行为,可能导致程序崩溃或产生不可预测的结果。
  4. 内存占用vector在存储元素时需要连续的内存空间,因此,如果存储大量元素或元素大小较大,可能会导致内存占用较大。如果内存是一个关键因素,可以考虑使用reserve()函数预留足够的容量来减少内存重新分配的次数。
  5. 使用移动语义:C++11引入了移动语义,可以通过移动而不是复制元素来提高性能。在插入、删除或返回vector中的元素时,可以使用移动语义来避免不必要的复制操作,提高效率。
  6. 避免频繁插入和删除头部元素vector在头部插入或删除元素时,需要将后续元素进行移动,这可能导致性能下降。如果需要频繁在头部进行插入和删除操作,考虑使用dequelist等其他容器,它们对头部操作有更好的性能。

3.9 什么情况下用vector,什么情况下用list,什么情况下用deque

  1. 使用vector的情况
    • 需要随机访问元素,通过索引快速访问。
    • 频繁在尾部插入或删除元素,但不需要在中间或头部进行频繁插入或删除操作。
    • 需要连续的内存存储,对内存占用有限制。
    • 元素数量事先已知或可以预估,并且不会频繁改变。
  2. 使用list的情况
    • 需要频繁在任意位置插入或删除元素,而不关心随机访问性能。
    • 需要保持迭代器在插入或删除操作后的有效性。
    • 元素的数量不确定或经常改变,不需要连续的内存存储。
    • 对于相对较小的元素,list的指针开销可以被忽略。
  3. 使用deque的情况
    • 需要在头部和尾部进行频繁的插入和删除操作,而不关心中间位置的性能。
    • 需要快速在头部和尾部进行插入和删除操作,并且对于随机访问的性能有一定要求。
    • 需要连续的内存存储,但同时也需要更好的头部或尾部操作性能。
    • 元素数量事先已知或可以预估,并且不会频繁改变。

3.10 vector 中迭代器失效的情况

  1. 插入和删除元素:当在vector中插入或删除元素时,可能会导致迭代器失效。插入或删除元素会导致内存重新分配和元素的移动,这可能会使之前获取的迭代器指向无效的位置。
  2. 动态增长:当vector的大小超过当前容量时,需要进行动态增长,即重新分配更大的内存空间,并将原有元素复制到新的内存空间中。这个过程会导致之前获取的迭代器失效。
  3. 使用引用或指针:如果在使用迭代器期间,对vector进行了插入或删除元素的操作,那么迭代器指向的元素可能会发生改变,从而使之前的引用或指针失效。

为了避免迭代器失效,可以采取以下措施

  1. 使用索引代替迭代器:如果不需要迭代器提供的灵活性,可以使用索引来访问vector中的元素,因为索引访问不会受到插入或删除元素的影响。
  2. 更新迭代器:在进行插入或删除元素操作后,需要更新迭代器,使其指向有效的位置。可以通过重新获取迭代器或使用迭代器的++--操作符进行更新。
  3. 使用插入和删除返回的迭代器vector的插入和删除操作会返回一个指向插入或删除位置的迭代器,可以使用这个迭代器来保持有效的迭代器。



不定期更新…


Reference




⭐️👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍🌔

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZPILOTE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值