【C++后端面试题】

C++程序从编译到执行的过程

  1. 预处理(Preprocessing): 预处理器将对源代码进行预处理。预处理器执行以 # 开头的预处理指令,例如 #include#define 等,并将宏展开、头文件包含等操作应用到源代码中。预处理器产生的输出通常称为预处理后的代码
  2. 编译(Compilation)编译器接收预处理后的代码作为输入,将其转换为汇编语言或者直接转换为目标机器的机器代码。编译器会进行词法分析、语法分析、语义分析以及优化等步骤,最终生成目标代码
  3. 汇编(Assembly)汇编器将编译器生成的目标代码转换为机器指令,生成目标文件。目标文件通常包含了可重定位的机器代码和一些元信息,例如符号表、重定位表等。
  4. 链接(Linking)链接器接收一个或多个目标文件以及可能的库文件作为输入,将它们链接在一起,解析符号引用,生成最终的可执行文件。链接器还负责处理重定位、符号解析、库文件链接等工作,生成可执行文件,该文件包含了程序的全部指令和数据,可以在操作系统上运行。
  5. 执行(Execution)操作系统加载可执行程序到内存中,并分配必要的资源(如内存空间、文件描述符等)。然后,操作系统按照程序的入口点开始执行程序的指令,程序按照预定义的逻辑和流程执行相应的操作,操作数据,实现预期的功能。在执行过程中,程序可能会与外部交互、调用系统功能、读写文件等操作。最终,程序执行完成后,操作系统回收程序所占用的资源,并将程序从内存中移除,程序的执行结束。

指针和引用的区别

  1. 语法和操作符
  2. 指针是⼀个实体,引⽤只是⼀个别名
  3. 初始化(赋空值)
    • 指针可以是空指针(即指向空地址),表示不指向任何有效的对象。
    • 引用在初始化时必须指向一个有效的对象,不能是空引用。引用本身就是所引用对象的别名,因此引用不存在空值的概念。
  4. 可变性
    • 引用一旦初始化后,就不能再引用其他对象,因此无法改变引用的对象。但是,通过引用可以访问所引用对象的可变性,如果对象本身是可变的,则可以通过引用修改对象的值。
  5. 空间占用
    • 指针通常需要占用额外的内存空间来存储地址值。
    • 引用在编译时会被替换成对应变量的地址,不会额外占用内存空间。
  6. sizeof
    • 对指针类型使用 sizeof 操作符将返回指针本身所占用的内存大小,即指针变量的字节大小。
    • 对引用类型使用 sizeof 操作符将返回引用所引用对象的大小
  7. 作为参数
    • 指针参数传递本质上是值传递,它所传递的是⼀个地址值(指针的地址值)。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从⽽形成了实参的⼀个副本(替身)。被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。
    • 引⽤参数传递过程中,被调函数的形式参数也作为局部变ᰁ在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址,因此,被调函数对形参的任何操作都会影响主调函数中的实参变量

volatile 和 extern 关键字

  • volatile
    • volatile 是一个类型限定符,用于声明一个变量是易变的(volatile variable)。
    • 当一个变量被声明为 volatile 类型时,意味着该变量的值可能在程序的执行过程中被意外地更改,而这种更改不是由当前代码引起的,例如外部硬件中断、多线程访问等。
    • 使用 volatile 声明的变量,编译器在编译时不会对其进行优化每次使用时都会从内存中读取其值,以保证变量的实时性
  • extern
    • extern 是一个存储类说明符,用于声明一个变量或函数是在其他文件中定义的
    • 当在一个文件中使用 extern 声明一个变量或函数时,意味着该变量或函数是在其他文件中定义的,并且可以在当前文件中使用。
    • 使用 extern 声明的变量或函数的定义可以在其他文件中找到,编译器在链接阶段会将所有外部定义的变量和函数链接在一起,形成最终的可执行文件。

c++中什么情况下会导致空指针异常,如何解决?

空指针异常(Null Pointer Exception)通常发生在尝试访问空指针所指向的内存位置时,这可能导致程序崩溃或产生未定义的行为。

解引用空指针:当尝试通过空指针访问其指向的对象或数据时,会导致空指针异常。

int* ptr = nullptr;
*ptr = 10; // 尝试解引用空指针,会导致空指针异常

调用空指针所指向对象的成员或方法:当尝试通过空指针访问其所指向对象的成员或方法时,也会导致空指针异常。

struct Foo {
    void bar() { std::cout << "Hello, World!" << std::endl; }
};

Foo* ptr = nullptr;
ptr->bar(); // 尝试调用空指针所指向对象的方法,会导致空指针异常

为了解决空指针异常,可以采取以下几种方法:
检查指针是否为空:在使用指针之前,始终确保它不是空指针。

int* ptr = nullptr;
if (ptr != nullptr) {
    *ptr = 10;
}

使用智能指针:智能指针(如std::shared_ptr、std::unique_ptr等)可以在指针生命周期结束时自动释放资源,并且不会出现空指针异常的问题。

#include <memory>

std::shared_ptr<int> ptr = nullptr; // 使用智能指针,不需要手动释放资源

使用异常处理:可以捕获空指针异常并进行处理,避免程序崩溃。

try {
    int* ptr = nullptr;
    *ptr = 10;
} catch (const std::exception& e) {
    std::cerr << "Exception caught: " << e.what() << std::endl;
}

多线程、多进程和多协程都是用于实现并发执行的编程模型,但它们之间有一些重要的区别:

  1. 多线程(Multithreading):
    • 多线程是在同一进程内创建多个线程来执行不同的任务。
    • 多个线程共享进程的地址空间,可以直接访问共享的内存数据。
    • 线程切换开销小,通信更方便,适合在共享内存的环境下进行并发编程。
    • 由于共享内存,需要考虑线程安全性和同步机制,如互斥锁、信号量等。
  2. 多进程(Multiprocessing):
    • 多进程是在操作系统中创建多个独立的进程来执行不同的任务。
    • 每个进程都有自己独立的地址空间,数据不共享,需要通过进程间通信(Inter-Process Communication,IPC)来传递数据。
    • 进程切换开销大,但更加稳定,一个进程崩溃不会影响其他进程。
    • 由于数据不共享,编程复杂度较高,但不需要考虑线程安全性。
  3. 多协程(Coroutine):
    • 多协程是在同一个线程内通过协程调度器(Coroutine Scheduler)实现多个独立的执行流。
    • 协程执行过程中可以随时暂停和恢复,不需要切换线程或进程。
    • 协程之间共享线程的地址空间,可以直接访问共享的内存数据,但需要注意协程切换时的数据安全性。
    • 相比于线程和进程,协程的切换开销更小,但通常不适合用于 CPU 密集型任务,更适合 IO 密集型任务,如网络编程和异步IO。

进程线程的区别

  1. 进程(Process)
    • 进程是计算机中资源分配的基本单位,它是正在运行的程序的一个实例。每个进程都有独立的地址空间、内存、文件描述符、堆栈等系统资源。
    • 进程之间是相互独立的,彼此之间不能直接访问对方的内存和数据。进程之间的通信需要使用特殊的机制,如管道、消息队列、共享内存等。
    • 创建进程的开销比较大,包括分配内存空间、加载程序、初始化资源等。每个进程都有自己的进程控制块(PCB)来维护其状态信息。
  2. 线程(Thread)
    • 线程是进程内的执行单元,是 CPU 调度的基本单位。同一个进程内的多个线程共享进程的地址空间和系统资源,包括内存、文件描述符等。
    • 线程之间可以直接访问共享的内存和数据,因此线程之间的通信相对容易,可以通过共享内存、全局变量等来实现。
    • 创建线程的开销比创建进程小得多,因为线程共享了进程的大部分资源,不需要重新分配和初始化资源。每个线程有自己的线程控制块(TCB)来维护其状态信息。

TCP和UDP之间的区别

  1. 连接性:
    • TCP是一种面向连接的协议,通信双方在传输数据之前需要先建立连接,然后进行数据传输,最后再释放连接。
    • UDP是一种无连接的协议,通信双方不需要先建立连接,直接发送数据包,也不需要确认是否收到,每个数据包都是独立的。
  2. 可靠性:
    • TCP提供可靠的数据传输,通过序列号、确认应答、重传机制等方式确保数据的可靠性,可以保证数据的顺序不变、不丢失、不重复。
    • UDP不提供数据传输的可靠性,数据包可能丢失、重复、顺序错乱,因此应用程序需要自行处理这些问题。
  3. 流量控制和拥塞控制:
    • TCP具有流量控制和拥塞控制机制,可以根据网络情况调整发送数据的速率,避免网络拥塞。
    • UDP没有流量控制和拥塞控制,发送数据的速率受限于应用程序和网络带宽,可能会导致网络拥塞和丢包。
  4. 头部开销:
    • TCP的头部较大,包含了序列号、确认号、窗口大小等字段,通常有20字节的固定头部,还可能有选项字段。
    • UDP的头部较小,只有8字节的固定头部,包含了源端口、目的端口、长度和校验和字段。
  5. 应用场景:
    • TCP适用于要求可靠传输、顺序传输和流量控制的应用,如网页浏览、文件下载、电子邮件等。
    • UDP适用于实时性要求较高、传输数据量较小、能容忍少量丢失的应用,如视频会议、在线游戏、音频流传输等。

http和TCP的区别

HTTP(超文本传输协议)和TCP(传输控制协议)是互联网通信中的两个不同层次的协议,它们之间有以下主要区别:

  1. 层次不同:
    • TCP是传输层的协议,负责在通信的两端之间提供可靠的、面向连接的数据传输服务。
    • HTTP是应用层的协议,建立在TCP之上,用于定义客户端和服务器之间的通信规则,实现超文本资源的传输和交互。
  2. 功能不同:
    • TCP负责将数据分割成合适的数据块并传输,确保数据的可靠性、顺序性和完整性,以及流量控制和拥塞控制。
    • HTTP定义了客户端和服务器之间的请求和响应消息的格式和语义,包括请求方法、状态码、消息头、消息体等,实现了在Web上的文档传输和交互。
  3. 面向连接与无连接:
    • TCP是一种面向连接的协议,通信双方在传输数据之前需要先建立连接,然后进行数据传输,最后再释放连接。
    • HTTP是一种无连接的协议,每个HTTP请求和响应都是独立的,没有持久的连接,每次请求都需要建立新的TCP连接,完成请求后立即关闭连接。
  4. 数据格式:
    • TCP是一个字节流协议,没有消息的概念,数据以字节流的形式进行传输,应用程序需要自行定义数据的分割和组装规则。
    • HTTP是一种文本协议,请求和响应消息都是以文本形式进行传输,采用HTTP头部和HTTP正文的格式进行组织。

综上所述,TCP是HTTP协议的基础,负责提供可靠的数据传输服务,而HTTP则是在TCP之上定义了请求和响应的格式和语义,实现了Web上的文档传输和交互。HTTP协议是建立在TCP连接之上的应用层协议,利用TCP提供的可靠传输服务进行通信

C++ this指针的作用?

this 指针是一个隐式指针,它指向当前对象的地址。this 指针在类的成员函数中起到了很重要的作用,主要有以下几个方面:

  1. 区分成员变量和局部变量

    • 在类的成员函数中,可能存在成员变量和局部变量的命名冲突。使用 this 指针可以明确指示成员变量,从而避免与局部变量产生歧义。

      class MyClass {
      public:
          int num;
          void setNum(int num) {
              this->num = num; // 使用 this 指针明确指示成员变量
          }
      };
      
      
  2. 在成员函数中访问成员变量

    • 在类的成员函数中,可以通过 this 指针来访问当前对象的成员变量

      class MyClass {
      public:
          int num;
          void printNum() {
              std::cout << "Num: " << this->num << std::endl; // 使用 this 指针访问成员变量
          }
      };
      
      
  3. 在成员函数中返回当前对象的引用

    • 在链式调用中,可以通过返回 this 指针来返回当前对象的引用,从而实现链式调用。

      class MyClass {
      public:
          MyClass& increment() {
              this->num++;
              return *this; // 返回当前对象的引用
          }
      private:
          int num;
      };
      
      MyClass obj;
      obj.increment().increment(); // 链式调用
      
      

拷贝构造函数和赋值构造函数

拷贝构造函数

  • 使用场景 :拷贝构造函数用于创建一个对象的副本,通常在以下情况下被调用:
    1. 通过值传递的方式传递对象给函数参数。
    2. 函数返回对象时,返回对象的副本。
    3. 通过另一个对象初始化新对象。
  • 实现方式:拷贝构造函数的声明通常形如 ClassName(const ClassName& other),其中 other 是要复制的对象。在拷贝构造函数中,通常会复制传递的对象的所有成员变量,以创建一个新的对象副本。
浅拷贝

浅拷贝是指将一个对象的所有成员变量值复制到另一个对象中,包括基本数据类型和指针类型的成员变量。浅拷贝只是简单地将源对象的内存空间中的值逐个复制到目标对象的内存空间中,如果对象中包含指针类型的成员变量,浅拷贝只是复制指针的值,而不会复制指针所指向的内存内容。因此,浅拷贝会导致多个对象共享同一块内存空间,当其中一个对象释放这块内存时,其他对象如果继续使用该内存空间,就会出现悬空指针的问题。
浅拷贝通常是在默认的拷贝构造函数或赋值运算符重载函数中实现的,它适用于对象中没有动态分配内存或者对象之间共享同一块内存不会造成问题的情况。

深拷贝

深拷贝是指在复制对象时,不仅复制对象本身的成员变量值,还会为对象中的指针类型的成员变量分配新的内存空间,并将指针所指向的内存内容也复制一份到新的内存空间中,使得源对象和目标对象的内存空间完全独立,互不影响。
深拷贝通常需要我们自定义拷贝构造函数或者赋值运算符重载函数来实现,确保在拷贝对象时进行动态内存分配和内容复制。通过深拷贝,每个对象都有自己独立的内存空间,不会出现对象共享内存导致的问题。

赋值构造函数

  • 使用场景:赋值运算符重载函数用于将一个对象的内容复制给另一个对象,通常在以下情况下被调用:
    1. 将一个对象赋值给另一个对象
    2. 将一个对象作为另一个对象的成员进行赋值
  • 实现方式:赋值运算符重载函数的声明通常形如 ClassName& operator=(const ClassName& other),其中 other 是要复制的对象。在赋值运算符重载函数中,通常会复制传递的对象的所有成员变量到当前对象,以完成赋值操作。
#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int* agePtr; // 使用指针来模拟动态分配内存

public:
    // 构造函数
    Person(const std::string& n, int a) : name(n), agePtr(new int(a)) {}

    // 拷贝构造函数
    Person(const Person& other) : name(other.name), agePtr(new int(*(other.getAge()))) {
        std::cout << "Deep copy constructor called" << std::endl;
    }

    // 赋值运算符重载函数
    Person& operator=(const Person& other) {
        if (this != &other) { // 避免自我赋值
            name = other.name;
            *agePtr = *(other.getAge()); // 深拷贝
        }
        std::cout << "Deep copy assignment operator called" << std::endl;
        return *this;
    }

    // 获取年龄的公有访问函数
    int* getAge() const {
        return agePtr;
    }

    // 打印人的信息
    void printInfo() const {
        std::cout << "Name: " << name << ", Age: " << *agePtr << std::endl;
    }

    // 析构函数
    ~Person() {
        delete agePtr; // 释放动态分配的内存
    }
};

int main() {
    // 创建一个 Person 对象
    Person person1("Alice", 30);
    // 使用深拷贝构造函数创建一个新的 Person 对象
    Person person2(person1);
    // 修改 person1 的年龄
    *person1.getAge() = 35;
    Person person3 = person2;//赋值构造函数
    // 打印信息
    std::cout << "Person 1: ";
    person1.printInfo();
    std::cout << "Person 2: ";
    person2.printInfo();
    std::cout << "Person 3: ";
    person3.printInfo();
    return 0;
}

什么样的函数不能是虚函数?

  1. 静态成员函数:静态成员函数是属于类的函数,而不是属于类的对象。虚函数是用于实现运行时多态性的机制,它需要在运行时根据对象的实际类型来确定调用的函数版本。但是静态成员函数不与任何特定的对象实例相关联,因此不适合声明为虚函数。
  2. 友元函数:友元函数是在类外部声明的,它具有访问类的私有成员的权限,但它不是类的成员函数,也不受类的继承关系影响。虚函数机制依赖于对象的类型和继承关系,因此无法应用于友元函数。
  3. 内联函数:内联函数是在编译期间展开的,它在调用点被直接替换为函数体,不存在运行时的函数调用。而虚函数需要在运行时根据对象的实际类型来确定调用的函数版本,因此虚函数不适合声明为内联函数。
  4. 构造函数:虚函数机制在构造函数期间不起作用。在对象的构造期间,对象的类型是已知的,因此不需要虚函数来实现多态性。

C++四种强制类型转换

  1. 静态转换(static_cast)
    • 用于显式转换类型,通常用于较为安全的转换,如基本数据类型之间的转换,以及具有继承关系的类指针或引用之间的转换。
    • 编译时进行类型检查,因此不能用于转换不相关的指针类型
    • 不能用于动态类型转换(如基类到派生类的转换)。
  2. 动态转换(dynamic_cast)
    • 主要用于具有继承关系的类指针或引用之间的转换,特别是将基类指针或引用转换为派生类指针或引用
    • 运行时进行类型检查,如果转换失败,会返回空指针(对于指针)或引发 std::bad_cast 异常(对于引用)。
    • 只能用于转换类的指针或引用并且必须存在虚函数
  3. 常量转换(const_cast)
    • 用于去除对象的 const 属性或 volatile 属性
    • 主要用于修正传递给函数的参数类型,以便在函数内部修改它们。
    • 通常被认为是不安全的,因为它可以绕过 const 限定符的安全性。
  4. 重新解释转换(reinterpret_cast)
    • 执行低级别的位操作,用于将一个指针或引用转换为另一种指针或引用,甚至不需要进行类型检查
    • 非常危险,通常情况下应该尽量避免使用,因为它不提供任何类型安全性保证。
    • 通常用于处理底层的内存操作,如将指针转换为整数类型以进行位操作。

new和malloc的区别

  1. 类型安全性
    • 执⾏ new 实际上执⾏两个过程:1.分配未初始化的内存空间(malloc);2.使⽤对象的构造函数对空间进⾏初始化;返回空间的⾸地址。如果在第⼀步分配空间中出现问题,则抛出 std::bad_alloc 异常,或被某个 设定的异常处理函数捕获处理;如果在第⼆步构造对象时出现异常,则⾃动调⽤ delete 释放内存。
    • 执⾏ delete 实际上也有两个过程:1. 使⽤析构函数对对象进⾏析构;2.回收内存空间(free)。
    • malloc 和 free 是 C 语言中的函数,它们只是简单地执行内存分配和释放,不会调用对象的构造函数和析构函数。
  2. 返回类型
    • new 返回的是指向动态分配的对象的指针,因此可以直接使用。
    • malloc 返回的是 void* 类型的指针,因此需要进行显式的类型转换才能使用。
  3. 大小计算
    • new 根据类型自动计算分配的内存大小。
    • malloc 需要手动指定要分配的内存大小。
  4. 数组方面:new[] 和 delete[]
    • new[] 用于动态分配数组,并会调用每个元素的构造函数进行初始化。
    • delete[] 用于释放通过 new[] 分配的数组内存,并会调用每个元素的析构函数进行清理。
    • 对于数组,malloc 和 free 不会调用任何元素的构造函数和析构函数。它们只是分配和释放内存。

堆内存和栈内存的区别


1. 由编译器进行管理,在需要时由编译器自动分配空间,在不需要时候⾃动回收空间,⼀般保存的是局部变量和函数参数等。
2. ⼤多数编译器中,参数是从右向左入栈,函数调用完成后,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地 址,程序由该点继续运行,不会产⽣碎片。
3. 栈是⾼地址向低地址扩展,栈低⾼地址,空间较小


1. 由程序员手动管理(new malloc delete free) 进行分配和回收,如果不进行回收的话,会造成内存泄漏的问题。
2. 不连续的空间,实际上系统中有⼀个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第⼀个⼤于等于申请⼤⼩的空间分配给程序,如果有剩余的,也会将剩余的插⼊到空闲链表中,这也是产⽣内存碎片的原因。
3. 堆是低地址向⾼地址扩展,空间较大,较为灵活。

面向对象的三大特性

  1. 封装(Encapsulation):
    • 封装是指将数据和操作数据的方法封装在一起,对外部隐藏对象的内部细节,只提供公共接口供外部访问。
    • 在C++中,封装可以通过类的访问控制权限实现。私有(private)成员只能在类的内部访问,公有(public)成员可以在类的外部访问。
  2. 继承(Inheritance):
    • 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而扩展或者修改父类的功能。
    • 在C++中,通过关键字classstruct进行继承,派生类可以访问基类的公共和保护成员,但不能访问私有成员。
  3. 多态(Polymorphism):
    • 多态是指同一个操作作用于不同的对象上会产生不同的行为,即同一个接口可以有多个实现方式。
    • 在C++中,多态可以通过虚函数(virtual function)实现。虚函数是在基类中声明为虚函数的成员函数,在派生类中可以重写该函数以实现多态。

多态的实现

多态其实⼀般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数⽣成符号表时的不同规则,重载只是⼀种语⾔特性,与多态⽆关,与⾯向对象也⽆关,但这⼜是 C++中增加的新规则,所以也算属于 C++,所以如果⾮要说重载算是多态的⼀种,那就可以说:多态可以分为静态多态和动态多态。
静态多态其实就是重载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表来决定;
动态多态是指通过
⼦类重写⽗类的虚函数来实现的
,因为是在运⾏期间决定调⽤的函数,所以称为动态多态,

在 C++ 中,虚函数表(Virtual Function Table,VTBL 或 VTABLE)是用于实现多态的一种机制。虚函数表是一个存储了类中虚函数地址的数组当一个类含有虚函数时,编译器会在该类的对象中生成一个虚函数表,并在对象中存储一个指向该虚函数表的指针。这个指针通常被称为虚函数指针(Virtual Function Pointer,VPTR)。

具体来说,当一个类派生自一个包含虚函数的基类时,编译器会为这个类生成一个新的虚函数表,并将基类的虚函数表的地址复制到这个新的虚函数表中,然后将新的虚函数表的地址保存在派生类对象的虚函数指针中。如果派生类覆盖了(重写)基类的虚函数,那么在派生类的虚函数表中对应的位置会存储派生类的虚函数地址如果派生类有自己的虚函数,就追加⾃身的虚函数到⾃身的虚函数表中

通过虚函数表和虚函数指针,C++ 实现了动态绑定(Dynamic Binding),使得在运行时可以正确地调用对象的虚函数,即使在编译时无法确定具体调用的是哪个函数。

STL容器,常用的有哪些,vector的扩容原理是什么

  1. 序列容器(Sequence Containers):
    • vector: 动态数组,支持随机访问,尾部插入和删除的时间复杂度为O(1)。
    • deque: 双端队列,支持在头部和尾部进行插入和删除操作,也支持随机访问。
    • list: 双向链表,支持快速的插入和删除操作,但不支持随机访问。
    • forward_list: 单向链表,只支持单向遍历和插入操作,没有双向链表的功能。
  2. 关联容器(Associative Containers):
    • set: 基于红黑树实现的有序集合,不允许重复元素。
    • map: 基于红黑树实现的有序映射,每个键唯一对应一个值。
    • multiset: 基于红黑树实现的有序多重集合,允许重复元素。
    • multimap: 基于红黑树实现的有序多重映射,每个键可以对应多个值。
  3. 无序容器(Unordered Containers):
    • unordered_set: 基于哈希表实现的无序集合,不允许重复元素。
    • unordered_map: 基于哈希表实现的无序映射,每个键唯一对应一个值。
    • unordered_multiset: 基于哈希表实现的无序多重集合,允许重复元素。
    • unordered_multimap: 基于哈希表实现的无序多重映射,每个键可以对应多个值。
  4. 堆(Heap Containers):
    • priority_queue: 基于堆实现的优先队列,可以高效地获取最大或最小值。
  5. 适配器容器(Adapter Containers):
    • stack: 栈,后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。
    • queue: 队列,先进先出(FIFO)的数据结构,只能在队列的尾部插入,在头部删除。
    • priority_queue: 优先队列,基于堆实现,可以高效地获取最大或最小值

对于vector的扩容原理,其实现主要分为以下几个步骤:

  1. 当需要向vector中插入元素时,如果当前容量不足,会触发扩容操作。
  2. 扩容操作会分配一个更大的内存空间,通常是当前容量的两倍或者是一个增量。
  3. 将原有元素拷贝到新的内存空间中。
  4. 释放原有内存空间。
  5. 插入新元素。

频繁对 vector 调⽤ push_back() 性能影响

向 vector 的尾部添加元素,很有可能引起整个对象 存储空间的重新分配,重新分配更⼤的内存,再将原数据拷⻉到新空间中,再释 放原有内存,这个过程是耗时耗⼒的,频繁对 vector 调⽤ push_back()会导致性能的下降。
push_back() 需要先构造对象,然后再将对象拷贝或移动到容器中。emplace_back() 在容器内部直接构造元素,避免了额外的构造和拷贝操作,可以直接调用元素的构造函数。

进程通信的方式有哪些

  1. 管道(Pipes)
    • 匿名管道(Anonymous Pipes):在父子进程或兄弟进程之间创建的单向通信管道。
    • 命名管道(Named Pipes):由文件系统中的命名管道文件表示,可以在不同进程之间进行通信。
  2. 消息队列(Message Queues):进程之间通过消息传递进行通信的机制,消息在队列中按顺序存储,可以实现进程间的异步通信。
  3. 共享内存(Shared Memory):多个进程可以将同一块内存映射到它们的地址空间,从而实现数据共享。
  4. 信号量(Semaphores):用于控制多个进程对共享资源的访问,包括二进制信号量和计数信号量。
  5. 套接字(Sockets):通过网络套接字进行进程间通信,可以在本地主机或不同主机上的进程之间进行通信。

线程同步的方式

  1. 互斥锁(Mutex)
    • 互斥锁用于保护临界区(Critical Section),一次只允许一个线程访问共享资源。
    • 线程在进入临界区之前会尝试获取互斥锁,如果锁已被其他线程持有,则线程会被阻塞直到锁可用。
    • 互斥锁通常是二进制的,即只有两种状态:锁定和未锁定。
  2. 信号量(Semaphore)
    • 信号量是一种计数器,用于控制对共享资源的访问数量。
    • 当线程想要访问资源时,会尝试获取信号量。如果信号量的计数器大于零,则线程可以继续执行并将信号量计数器减一;如果计数器为零,则线程会被阻塞直到信号量可用。
    • 信号量可以是二进制的(用于互斥访问)或是计数型的(用于限制访问数量)。
  3. 条件变量(Condition Variable)
    • 条件变量用于线程间的等待和通知机制。
    • 线程可以在条件变量上等待某个特定条件成立,当条件不满足时,线程会被阻塞;当条件满足时,其他线程可以通知等待线程,使其继续执行。
    • 通常与互斥锁一起使用,以保护对条件的访问。
  4. 读写锁(Read-Write Lock)
    • 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
    • 当有线程正在写入时,其他线程无法读取或写入;当有线程正在读取时,其他读取线程可以同时进行。
  5. 屏障(Barrier)
    • 屏障用于确保在多个线程都达到某个点之前,所有线程都暂停执行。
    • 当所有线程都到达屏障点时,才允许它们继续执行。
  6. 原子操作(Atomic Operations)
    • 原子操作是不可中断的操作,可以保证在多线程环境下对共享变量的操作是原子的。
    • 原子操作通常用于实现简单的同步机制,如自增、自减等。

锁有哪几种

  1. 互斥锁(Mutex)
    • 互斥锁是最常见的锁类型,用于保护临界区,一次只允许一个线程访问共享资源。当一个线程获得了互斥锁后,其他线程必须等待该线程释放锁才能继续访问共享资源。
  2. 读写锁(Read-Write Lock)
    • 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。当有线程正在写入时,其他线程无法读取或写入;当有线程正在读取时,其他读取线程可以同时进行。
  3. 递归锁(Recursive Lock)
    • 递归锁允许同一个线程多次获取锁,而不会造成死锁。当一个线程多次获取递归锁时,必须相应地多次释放锁,只有当所有锁都被释放时,其他线程才能获取该锁。
  4. 条件变量(Condition Variable)
    • 条件变量不是严格意义上的锁,而是一种线程间的等待和通知机制。条件变量允许一个或多个线程等待某个条件的发生,当条件不满足时,线程会被阻塞;当条件满足时,其他线程可以通知等待线程,使其继续执行。
  5. 屏障(Barrier)
    • 屏障不是严格意义上的锁,而是一种同步机制,用于确保在多个线程都达到某个点之前,所有线程都暂停执行。当所有线程都到达屏障点时,才允许它们继续执行。
  6. 自旋锁(Spinlock)
    • 自旋锁是一种基于忙等待(busy-waiting)的锁机制,在获取锁时,如果锁已经被其他线程持有,则当前线程会一直忙等待直到锁可用。与传统的互斥锁(Mutex)不同,互斥锁在无法获取锁时会将线程挂起,从而释放 CPU 时间片给其他线程执行;而自旋锁则是在获取锁失败时,通过不断尝试获取锁,直到获取到锁为止,期间一直占用 CPU 时间片

计算机网络中常见的 I/O 模型

  1. 阻塞式 I/O(Blocking I/O)
    • 在阻塞式 I/O 模型中,应用程序在执行 I/O 操作时会被阻塞,直到数据准备好并被复制到应用程序的缓冲区中才会返回。
    • 在阻塞式 I/O 中,通常会使用 read()write() 等系统调用进行数据读写操作。
  2. 非阻塞式 I/O(Non-blocking I/O)
    • 在非阻塞式 I/O 模型中,应用程序发起 I/O 操作后会立即返回,即使数据尚未准备好也会立即返回。应用程序需要反复轮询或使用系统调用如 select()poll()epoll() 等来检查数据是否准备好。
    • 非阻塞式 I/O 适用于同时处理多个连接的情况,可以提高系统的并发性能。
  3. 多路复用 I/O(Multiplexing I/O)
    • 多路复用 I/O 模型允许应用程序同时监视多个 I/O 通道,当其中任何一个通道准备好读写时,应用程序就可以进行相应的处理。常见的多路复用系统调用包括 select()poll()epoll() 等。
    • 多路复用 I/O 可以同时处理多个连接,而不需要为每个连接创建一个线程或进程。
  4. 信号驱动 I/O(Signal-driven I/O)
    • 在信号驱动 I/O 模型中,应用程序发起 I/O 操作后可以继续执行其他任务,当数据准备好时,操作系统会向应用程序发送一个信号来通知数据已经准备好。
    • 信号驱动 I/O 适用于需要异步处理 I/O 事件的场景。
  5. 异步 I/O 模型(Asynchronous I/O Model):
    • 应用程序发起 I/O 操作后可以立即返回,并继续执行其他任务。
    • 当操作完成时,操作系统会通知应用程序,并将数据传送给应用程序。
    • 与非阻塞 I/O 不同的是,异步 I/O 操作完成后不需要应用程序轮询或者等待数据准备好

三次握手和四次挥手

三次握手(建立连接)

  1. 客户端向服务器发送连接请求 :

    • 客户端向服务器发送一个带有SYN(同步)标志的数据包,表示请求建立连接,并选择一个初始序列号(ISN)。
  2. 服务器响应连接请求 :

    • 服务器收到客户端的SYN数据包后,会回复一个带有SYN和ACK(确认)标志的数据包,表示接收到连接请求,并确认客户端的ISN,同时选择自己的ISN。
  3. 客户端确认连接 :

    • 客户端收到服务器的SYN/ACK数据包后,会发送一个带有ACK标志的数据包,表示确认收到服务器的响应,连接建立成功。

四次挥手(关闭连接)

  1. 客户端发送关闭连接请求:
    • 客户端向服务器发送一个带有FIN(结束)标志的数据包,表示请求关闭连接,并停止发送数据。
  2. 服务器确认关闭请求:
    • 服务器收到客户端的FIN数据包后,会发送一个带有ACK标志的数据包,表示确认收到关闭请求,但仍可以向客户端发送数据。
  3. 服务器发送关闭连接请求:
    • 当服务器准备好关闭连接时,会发送一个带有FIN标志的数据包给客户端,表示服务器已经完成发送数据的任务,希望关闭连接。
  4. 客户端确认关闭请求:
    • 客户端收到服务器的FIN数据包后,会发送一个带有ACK标志的数据包给服务器,表示确认收到服务器的关闭请求,并等待一段时间(TIME_WAIT状态),确保服务器收到ACK后可以关闭连接。

TCP拥塞控制

TCP拥塞控制是一种网络流量管理机制,旨在避免网络拥塞并维护网络的稳定性和公平性。TCP拥塞控制通常通过调整发送数据的速率来应对网络拥塞情况,以确保网络的可靠性和性能。

  1. 慢启动(Slow Start)
    • 在连接建立时,发送端会以较小的速率发送数据,随着每次收到对端的确认应答,发送窗口的大小指数级增加,即指数增长。这样可以避免在网络容量未知的情况下一开始就发送大量的数据,导致网络拥塞。
  2. 拥塞避免(Congestion Avoidance)
    • 一旦慢启动阶段结束,发送端会进入拥塞避免阶段。在该阶段,发送端以线性增长的速率逐渐增加发送窗口的大小,即线性增长。这样可以避免在网络容量逐渐增大的情况下过快地增加发送数据的速率,导致网络拥塞。
  3. 快速重传(Fast Retransmit)
    • 当发送端连续收到对端的重复确认应答时,意味着某个数据段丢失,发送端会立即重传该丢失的数据段,而不是等待超时时间到达。这样可以快速恢复由于数据丢失导致的拥塞情况。
  4. 快速恢复(Fast Recovery)
    • 在执行快速重传后,发送端不会回到慢启动阶段,而是进入快速恢复阶段。在该阶段,发送端会将拥塞窗口大小减半,并继续进行拥塞避免,以逐渐增加发送窗口的大小。
  5. 拥塞超时(Congestion Timeout)
    • 如果发送端在等待对端确认数据时超过了一定的时间(超时时间),则认为发生了拥塞。此时发送端会将拥塞窗口大小设置为1,并重新开始慢启动过程。

https和http的区别

HTTP(超文本传输协议)和HTTPS(安全超文本传输协议)是用于在网络上传输数据的两种协议

  1. 安全性
    • HTTP是一种明文传输协议,数据在传输过程中是以纯文本形式传输的,容易受到中间人攻击,数据被窃听或篡改的风险较高。
    • HTTPS通过使用SSL/TLS协议对数据进行加密和身份认证,提供了更高的安全性。传输过程中的数据经过加密,中间人无法轻易窃听或篡改,同时可以验证服务器的身份,防止恶意攻击者伪装成服务器。
  2. 加密机制
    • HTTP不提供数据加密功能,所有数据都是以明文形式传输的。
    • HTTPS使用SSL/TLS协议对数据进行加密,通过对称加密和非对称加密机制确保数据的保密性和完整性。
  3. 端口号
    • HTTP默认使用端口号80进行通信。
    • HTTPS默认使用端口号443进行通信。
  4. 证书
    • 在使用HTTPS时,服务器必须拥有有效的数字证书,证明其身份的真实性。
    • 客户端在与服务器建立连接时会验证服务器的证书,确保连接的安全性。

TCP/IP 五层结构,每层的作用,协议的基本作用

  1. 应用层(Application Layer)
    • 作用:为应用程序提供网络服务和接口,实现特定的网络应用功能。
    • 常见协议:HTTP、HTTPS、FTP、SMTP、DNS等。
    • 基本作用:实现应用程序之间的数据交换和通信,例如Web浏览、文件传输、电子邮件发送等。
  2. 传输层(Transport Layer)
    • 作用:提供端到端的数据传输服务,确保可靠的数据传输。
    • 常见协议:TCP(传输控制协议)、UDP(用户数据报协议)。
    • 基本作用:TCP提供可靠的、面向连接的数据传输服务,UDP提供无连接的数据传输服务,用于快速传输但不需要可靠性的应用。
  3. 网络层(Internet Layer)
    • 作用:实现在网络上的寻址和路由,实现不同网络之间的数据包转发和路由选择。
    • 常见协议:IP(Internet Protocol)、ICMP(Internet Control Message Protocol)、ARP(Address Resolution Protocol)等。
    • 基本作用:IP负责数据包的分组和转发,ICMP用于网络状态的检测和错误处理,ARP用于IP地址到MAC地址的映射。
  4. 数据链路层(Data Link Layer)
    • 作用:负责通过物理介质传输数据帧,管理物理地址(MAC地址)和差错检测。
    • 常见协议:以太网协议、PPP(Point-to-Point Protocol)、SLIP(Serial Line Internet Protocol)等。
    • 基本作用:在相邻节点之间提供可靠的数据传输,封装网络层的数据包为帧,并负责物理介质的访问。
  5. 物理层(Physical Layer)
    • 作用:负责传输原始比特流,定义物理介质和传输速率等细节。
    • 常见协议:Ethernet、WiFi、光纤等。
    • 基本作用:在物理介质上传输数据比特流,将数据转换为适合传输的信号形式,并确保物理连接的稳定性和可靠性。
  • 32
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在提供的引用内容中,有一个关于STL面试题的代码示例。这个示例展示了如何使用STL中的allocator类来进行内存分配和对象构造销毁的操作。在这个示例中,使用了一个Test类作为示例对象。首先,使用allocator的allocate方法来申请三个单位的Test内存,并将其赋值给指针pt。然后,使用allocator的construct方法来构建三个Test对象,并使用默认值或拷贝构造函数来初始化这些对象。最后,使用allocator的destroy方法来销毁这些对象,并使用deallocate方法释放之前分配的内存。这个示例展示了如何使用allocator来实现自定义内存管理和对象构造销毁的操作。 关于C++ STL面试题,根据提供的引用内容,我无法找到具体的面试题。请提供更具体的问题或者引用内容,以便我能够给出更准确的答案。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++ STL程序员面试题](https://download.csdn.net/download/kpxingxing/3697052)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++面试题 STL篇](https://blog.csdn.net/qq_31442743/article/details/109575971)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值