Linux内核、操作系统、单片机面试八股

Linux内核

1 Linux内核的组成⭐⭐

  1. 进程管理:负责创建、调度、终止和管理进程。包括进程调度、进程间通信(IPC)和进程状态管理。
  2. 内存管理:负责分配和释放系统内存,以及虚拟内存的管理。包括物理内存管理、虚拟内存管理、页面置换算法等。
  3. 文件系统:负责管理文件和目录,以及提供对文件的读写和操作。它支持各种文件系统类型,如ext4、FAT32等,并隐藏了各种硬件的具体细节,为所有设备提供了统一的接口。
  4. 设备驱动:允许内核与硬件设备进行通信。Linux内核包含了大量的设备驱动程序,用于管理和控制硬件设备。
  5. 网络子系统:提供了对各种网络标准的存取和各种网络硬件的支持。它可分为网络协议和网络驱动程序,其中网络协议部分负责实现每一种可能的网络传输协议。

2用户空间与内核通信方式有哪些?⭐⭐⭐⭐⭐

  1. 系统调用:这是用户空间程序请求内核服务的一种方式。用户空间程序通过系统调用来与内核进行通信,从而获取内核提供的各种服务,如文件操作、进程控制等。
  2. 信号:当内核空间出现一些异常情况时,会发送信号给进程。这些信号可以用于通知用户空间程序某些事件的发生,如进程终止、定时器到期等。
  3. /proc接口:/proc文件系统是一个伪文件系统,它允许用户空间程序读取内核空间的配置信息和运行状态,甚至可以设置部分属性的值。通过这种方式,用户空间程序可以获取内核的一些状态和参数,实现与内核的通信。
  4. 文件操作:虽然这不是直接的用户空间与内核空间的通信方式,但用户空间程序可以通过对特定文件的读写操作来实现与内核的间接通信。例如,一些设备文件就允许用户空间程序通过读写操作与内核中的设备驱动程序进行通信。
  5. netlink套接字:netlink套接字提供了一种在用户空间和内核空间之间进行通信的机制。它类似于普通的socket通信方式,但特别适用于内核与用户空间之间的通信。netlink套接字可以支持大量的数据传输,并且可以实现相对复杂的通信功能。
  6. ioctl系统调用:ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。通过ioctl,用户空间的程序可以对设备的I/O通道进行管理,例如配置设备、读取设备的状态等。ioctl机制可以在驱动中扩展特定的ioctl消息,用于将一些状态从内核反映到用户态。然而,ioctl不适合传输大量的数据,通常与内存映射机制结合使用来完成大量数据交换过程。

3系统调用read()/write(),内核具体做了哪些事情⭐⭐

  1. 参数验证:首先,内核会检查传递给 read() 的参数是否有效。这包括检查文件描述符是否有效,以及缓冲区指针和大小是否合法。

  2. 查找文件描述符对应的文件对象:内核会根据文件描述符找到对应的文件对象,这通常涉及查找进程的文件描述符表。

  3. 权限检查:确保调用进程有权限读取目标文件或设备。

  4. 读取(写入)数据

  5. 更新文件偏移量:对于普通文件,读取(写入)操作完成后,内核会更新文件的当前偏移量,以便下一次读取(写入)操作从正确的位置开始

  6. 返回读取(写入)的字节数:内核将实际读取的字节数返回给调用进程。如果到达文件末尾或发生错误,返回的值会小于请求的字节数。

    额外考虑:

  • 并发和同步:在多进程环境中,内核需要确保对文件的并发访问是安全的,可能需要使用锁或其他同步机制。

  • 缓存和缓冲:为了提高性能,内核可能会使用缓存或缓冲区来存储待读取或待写入的数据。这可以减少对磁盘或其他设备的直接访问次数。

  • 错误处理:如果在读取或写入过程中发生错误(如磁盘故障、内存不足等),内核会进行相应的错误处理,并可能向调用进程返回错误代码。

4系统调用的作用⭐⭐⭐⭐⭐

  1. 用户空间与内核空间的交互:系统调用是用户空间程序与内核空间进行交互的唯一方式。通过系统调用,用户空间的程序可以请求内核执行某些操作,如文件操作、进程管理、设备控制等。这种交互机制确保了用户空间程序能够利用内核提供的服务和资源

  2. 资源管理和保护:内核是操作系统的核心部分,负责管理系统的硬件和软件资源。通过系统调用,内核可以对资源进行管理和保护,防止用户空间的程序滥用资源或执行非法操作。例如,系统调用可以限制用户空间程序对文件的访问权限,确保文件的安全性和完整性。

  3. 提供抽象和统一接口:系统调用为用户空间程序提供了抽象和统一的接口,使得程序可以不必关心底层硬件的细节和操作系统的实现方式。这样,程序员可以更加专注于应用程序的开发,而不需要关心底层的复杂性和差异性。

  4. 实现操作系统功能许多操作系统的功能都是通过系统调用来实现的。例如,进程创建、终止、调度,内存分配和释放,文件读写,设备控制等都需要通过系统调用来完成。这些功能对于用户空间程序来说是至关重要的,它们使得程序能够正常运行并与其他程序和硬件设备进行交互。

  5. 安全性和稳定性:系统调用由内核实现,并经过严格的测试和验证,以确保其安全性和稳定性。通过系统调用,用户空间的程序可以获得经过内核审核和管理的服务,从而降低程序出错的可能性,提高系统的整体稳定性和安全性。

5内核态,用户态的区别⭐⭐⭐⭐⭐

  1. 权限级别
    • 内核态:具有最高权限级别,可以执行任何指令和访问任何内存地址。内核态下的代码通常是操作系统内核的一部分,负责管理系统资源、提供系统服务、处理硬件中断等。
    • 用户态:具有较低的权限级别,只能执行部分指令,并且只能访问分配给该用户程序的内存区域。用户态下的代码通常是应用程序的一部分,用于实现特定的功能。
  2. 功能职责
    • 内核态:负责系统的整体管理和控制,包括进程调度、内存管理、设备驱动、文件系统等。内核态下的代码需要处理各种系统级的事务,确保系统的稳定性和安全性。
    • 用户态:主要负责实现用户的具体需求,如文本编辑、图像处理、网络通信等。用户态下的代码主要关注于应用功能的实现,不需要关心底层系统的细节。
  3. 安全性
    • 内核态:由于具有最高权限,一旦内核态的代码出现错误或受到攻击,可能会对整个系统造成严重的后果。因此,内核态下的代码需要经过严格的测试和验证,以确保其安全性和稳定性。
    • 用户态:由于权限较低,即使用户态的代码出现错误或受到攻击,其影响通常也局限于该用户程序本身,不会对整个系统造成太大的影响。
  4. 执行效率
    • 内核态:由于内核态下的代码需要处理系统级的事务,其执行效率通常较高。然而,频繁地进入和退出内核态会导致上下文切换的开销,从而可能影响系统的整体性能。
    • 用户态:用户态下的代码通常不需要频繁地进入和退出内核态,因此其执行效率可能相对较高。但是,如果需要执行系统调用等需要与内核交互的操作,仍然需要切换到内核态

6 bootloader内核 根文件的关系⭐⭐⭐⭐

Bootloader(引导加载程序)是启动计算机系统的第一阶段软件,通常存储在计算机的固件中(如BIOS或UEFI)。它的主要任务是初始化硬件,包括CPU、嵌入式板级设备等,并建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态。在完成这些初始化任务后,Bootloader负责将Linux内核加载到系统内存中,并传递控制权给内核

内核是操作系统的核心,它管理系统的硬件和提供基本的系统服务。当内核从Bootloader中加载到内存中并执行时,它会进一步初始化硬件,建立虚拟内存映射等。内核还包括设备驱动程序、调度器、文件系统、网络协议栈等核心功能。

根文件系统(Rootfs)是文件系统的顶层,包含了操作系统运行所需的基本文件和目录结构。在Linux内核完成初始化后,需要挂载某个文件系统作为根文件系统。这个根文件系统提供了内核运行所需的各种库、配置文件、应用程序等。

因此,Bootloader、内核和根文件在嵌入式Linux系统中的关系可以概括为:Bootloader负责初始化硬件并加载内核;内核在启动时进一步初始化硬件,并挂载根文件系统;而根文件系统则提供了操作系统运行所需的基本文件和目录结构。这三个部分协同工作,共同构成了一个完整的嵌入式Linux系统。

7 Bootloader多数有两个阶段的启动过程:⭐⭐⭐

第一阶段主要使用汇编语言实现,其目标是完成一些依赖于CPU体系结构的初始化工作,并调用第二阶段的代码。在这一阶段,Bootloader会进行硬件设备初始化,为加载第二阶段的代码准备RAM空间,复制第二阶段的代码到RAM中,设置好栈,并跳转到第二阶段代码的C入口点。

第二阶段则通常使用C语言来实现,以提供更复杂的功能和更好的代码可读性与移植性。在这一阶段,Bootloader会初始化本阶段要使用的硬件设备,检测系统内存映射,并将内核映像和根文件系统从Flash上读到RAM空间中。此外,它还会为内核设置启动参数,并最终调用内核启动

8 linux的内核是由bootloader装载到内存中的?⭐⭐⭐

Linux的内核是由Bootloader装载到内存中的。Bootloader是系统启动或复位以后执行的第一段代码,它主要负责初始化处理器及外设,并将Linux内核从非易失性存储器(如Flash或DOC等)中拷贝到RAM中,然后跳转到内核的第一条指令处继续执行,从而启动Linux内核

Bootloader的是连接硬件和软件的关键桥梁,确保内核能够正确加载并运行。

9为什么需要BootLoader⭐⭐⭐⭐

  1. 硬件初始化:BootLoader是系统启动后运行的第一段代码,它负责初始化系统的硬件环境。这包括设置CPU的工作模式、初始化中断向量表、设置内存控制器的功能寄存器、设置时钟源和频率、初始化串口等。这些初始化步骤对于确保系统能够正常运行至关重要。

  2. 引导加载内核:BootLoader的核心任务之一是加载并启动操作系统的内核。它负责将内核映像从Flash存储器或其他存储介质中读取到RAM中,然后跳转到内核的入口点来启动内核。这个过程确保了操作系统内核能够在正确的环境中执行。

  3. 设置系统环境:除了加载内核,BootLoader还需要为内核的执行准备好正确的环境。这可能包括设置CPU寄存器和内存空间映射,初始化内核所需的数据结构,以及准备必要的参数传递给内核。这些参数可能包括内存布局、命令行参数、设备树等,它们对于内核的正确初始化至关重要。

  4. 支持多种操作系统:在一些复杂的嵌入式系统中,可能需要支持多种不同的操作系统或内核。BootLoader可以提供一个统一的启动框架,使得不同的操作系统或内核可以在相同的硬件平台上启动。这提高了系统的灵活性和可维护性。

  5. 提供调试接口:在嵌入式系统的开发过程中,调试是一个重要的环节。BootLoader通常提供了一些调试接口,如串口通信、JTAG接口等,使得开发者可以方便地与系统进行交互,查看系统状态、执行调试命令等。这对于快速定位和解决问题非常有帮助。

10 Linux内核同步方式总结⭐⭐⭐⭐

  1. 自旋锁(Spinlock):主要用于对行为时间很短的情况进行同步,即当需要读取或修改的数据只需要一瞬间就能使用时。当多个进程或线程同时请求同一个资源时,会不断地忙等待(自旋),直到该资源被占用者释放。由于不涉及内核状态转换,因此能够快速访问共享资源。
  2. 互斥锁(Mutex):具有已锁定和未锁定两种状态。在Linux内核中,互斥锁可以使用mutex_tsemaphore实现。
  3. 读写锁(Read-Write Lock):对读写操作进行区分,允许多个读取者同时访问资源,但写入者在进行写操作时会独占资源。
  4. 信号量(Semaphore):用于控制多个进程或线程访问同一资源的同步机制,可以限制同时访问特定资源的进程数量
  5. 顺序锁(SeqLock):也称为seq锁,提供一种简单的机制用于读写共享数据。它主要依靠一个序列计数器来实现,当需要写入数据时,会获得一个锁并增加序列值;在读取数据前后,序列值都会被读取,如果读取的序列值相同,则说明读操作过程中没有新的数据写入
  6. 关闭内核抢占:对于单处理器,在访问临界区期间不允许内核抢占也是一种同步方法。
  7. 大内核锁(Big Kernel Lock, BKL):在早期的Linux内核中,BKL是一个全局锁,用于保护整个内核。
  8. 完成变量(Completion Variable):如果在内核中一个任务需要发出信息号通知另一任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法。

11为什么自旋锁不能睡眠 而在拥有信号量时就可以?⭐⭐⭐⭐

自旋锁的设计初衷是为了保证数据修改的原子性,因此它不允许在锁住的区域内进行睡眠。当自旋锁被占用时,如果某个进程试图获取该锁,它会一直自旋等待(即忙等待),直到锁被释放。这种机制避免了进程状态的切换,从而提高了效率。然而,这也意味着在自旋锁持有期间,进程不能进入睡眠状态,因为睡眠会导致处理器被其他进程抢占,从而破坏了使用锁的目的

相比之下,信号量则是一种睡眠锁。当某个任务试图获取一个已被占用的信号量时,它会被推入一个等待队列并进入睡眠状态。这样,处理器可以重获自由去执行其他代码。当信号量变得可用时,处于等待队列中的任务会被唤醒并获得该信号量。这种机制允许在等待资源时释放处理器,提高了系统的负载能力。

此外,自旋锁通常用于短时间内等待的情况,因为它的忙等待特性不适合用于长时间的临界区代码。而信号量则更适用于运行时间较长的临界区代码,因为它允许进程在等待时进入睡眠状态。

12 linux下检查内存状态的命令⭐⭐⭐

  1. free命令
    free 命令是最常用且基础的内存查看命令,用于显示系统当前的内存使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。
  2. top命令
    top 命令是一个动态监视系统状态和性能的命令,它可以实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。在top命令的输出中,你可以看到内存使用的实时数据。
  3. htop命令
    htop 是 top 命令的一个增强版,提供了一个彩色的界面和更多的交互选项,包括水平和垂直滚动,以便更轻松地查看和管理进程。
  4. vmstat命令
    vmstat 命令报告关于进程、内存、分页、块IO、陷阱和CPU活动的信息。
  5. sar命令
    sar 命令是系统活动报告程序,它可以用来收集、报告和保存系统活动信息。通过 sar,你可以查看历史的内存使用情况
  6. cat /proc/meminfo命令
    /proc/meminfo 文件包含了关于系统内存使用的详细信息。你可以使用 cat 命令查看这个文件的内容。

操作系统

1大小端的区别以及各自的优点,哪种时候用⭐⭐⭐⭐⭐

大端模式(Big Endian):高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。这种模式的优点在于符号位在所表示的数据的内容的第一个字节中,便于快速判断数据的正负和大小。此外,大端模式也被广泛用作网络传输的字节序,如TCP/IP协议规定网络传输的字节序为大端字节序。

小端模式(Little Endian):低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。小端模式的优点在于,当CPU做数值运算时,从内存中依次从低到高取数据进行运算直到最后刷新最高位的符号位,这种运算方式会更高效。此外,在强制类型转换数据时,小端模式不需要调整字节的内容,这在某些情况下可以提高处理效率。许多现代处理器,如Intel的x86系列和ARM的芯片,默认使用小端模式,但ARM的芯片也可以切换到大端模式。

2 一个程序从开始运行到结束的完整过程(四个过程)⭐⭐⭐⭐⭐

  1. 加载与预处理
    • 程序首先被加载到内存中。
    • 预处理阶段处理源代码中的预编译指令,如#include(包含头文件)、#define(宏定义)以及条件编译指令如#if#else#endif等。
    • 宏被替换,条件编译指令根据条件决定是否包含某部分代码。
    • 预处理结束后,生成一个修改后的源代码文件
  2. 编译
    • 编译阶段对预处理后的文件进行语法分析、词法分析、语义分析,并生成符号汇总。
    • 编译器将源代码转换为汇编代码,这是一个中间表示,比高级语言更接近机器语言。
    • 在这一过程中,所有的语法和语义错误都会被检测并报告。
  3. 汇编与链接
    • 汇编阶段将汇编代码转换成机器码,即二进制文件,这是机器可以直接执行的代码。
    • 汇编后生成的目标文件(如.o文件在Linux下,.obj文件在Windows下)包含了程序的二进制代码以及符号表,其中符号表为程序中的函数、变量等分配了虚拟地址。
    • 链接阶段将目标文件与系统中提供的库文件(如标准库、第三方库等)链接起来,解决目标文件中引用的外部符号问题。
    • 链接完成后,生成一个可执行文件(如.exe文件在Windows下)。
  4. 执行
    • 可执行文件被加载到内存中,操作系统为其分配所需的资源。
    • 程序开始执行,根据程序中的指令和数据,CPU进行计算和控制。
    • 程序在执行过程中可能会与操作系统、其他程序或硬件设备进行交互。
    • 程序执行完成后,返回操作系统,释放所占用的资源。

3什么是堆,栈,内存泄漏和内存溢出?⭐⭐⭐⭐

  1. 堆(Heap)

    • 堆是程序运行时用于动态内存分配的区域。在C、C++等语言中,使用mallocnew等函数或操作符在堆上分配内存。堆内存的生命周期完全由程序员管理,因此程序员必须显式地释放不再需要的内存,以防止内存泄漏
    • 堆内存的管理通常比栈内存复杂,因为它涉及到内存的分配、回收和碎片整理等问题。
  2. 栈(Stack)

    • 栈是程序运行时用于存储局部变量和函数调用的区域。栈内存的管理是自动的,当函数被调用时,其参数和局部变量会被推入栈中;当函数返回时,这些数据会从栈中弹出。
    • 由于栈内存的管理是自动的,因此程序员通常不需要关心栈内存的分配和释放。然而,如果函数递归过深或者局部变量过大,可能会导致栈溢出
  3. 内存泄漏(Memory Leak)

    • 内存泄漏是指程序在申请内存后,未能释放已不再使用的内存空间。这通常发生在堆内存中,因为堆内存的管理需要程序员显式地进行。如果程序员忘记释放已分配的内存,或者由于某些原因(如程序异常终止)未能释放内存,那么这部分内存就无法被再次使用,造成内存泄漏
    • 内存泄漏可能会导致程序运行变慢,甚至耗尽系统资源,使程序崩溃。
  4. 内存溢出(Memory Overflow)

    • 内存溢出是指程序试图访问或操作其内存空间之外的内存,这通常是由于程序中的错误(如数组越界访问、指针错误等)导致的。内存溢出可能会导致程序崩溃或产生不可预测的行为。
    • 内存溢出也可能发生在栈中,当递归调用过深或局部变量过大时,可能会导致栈空间不足,从而发生栈溢出。

4堆和栈的区别⭐⭐⭐⭐⭐

  1. 管理方式
    • 栈由系统自动分配和释放,其操作方式类似于数据结构中的栈。栈的分配和释放遵循先进后出(LIFO)的原则。
    • 堆的分配和释放由程序员负责,若程序员不释放,程序结束时可能由操作系统回收。因此,如果不正确地管理堆内存,很容易发生内存泄漏。
  2. 空间大小
    • 一般来说,在32位系统中,每个进程拥有4GB的内存空间,其中栈的大小通常为2MB左右,而堆的大小则远大于栈,理论上接近4GB。
    • 栈的空间大小是固定的,如果申请的空间超过其剩余空间大小,就会发生栈溢出。
  3. 碎片问题
    • 对于堆来讲,频繁的new/delete会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于这一点,栈则是相对好的,因为它是先进后出的队列,它们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出。
  4. 生长方向
    • 对于栈来讲,它的生长方向是向上的,也就是向着内存地址增加的方向;对于堆来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
  5. 分配方式
    • 堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
  6. 分配速度
    • 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。
  7. 存储内容
    • 栈在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行
    • 堆中存放的一般是由程序员分配的内存块,其大小由程序员决定,当程序员使用如malloc或new等函数指明一个大小后,该函数就会返回一个指针,该指针指向的内存就是程序员申请的内存。由于堆是由程序员管理的,因此程序员要防止发生内存泄露。 

5死锁的原因、条件 创建一个死锁,以及如何预防⭐⭐⭐⭐⭐

死锁产生的原因主要有:

  1. 系统资源不足。如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
  2. 进程运行推进的顺序不合适。进程运行推进顺序与速度不同也可能产生死锁。
  3. 资源分配不当

产生死锁的四个必要条件是:

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件(占有等待):一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件(不可抢占):进程已获得的资源在未使用完之前不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

创建一个死锁的例子

考虑一个场景,其中有两个进程A和B,以及两个资源R1和R2。进程A锁定资源R1并等待资源R2,而进程B锁定资源R2并等待资源R1。在这种情况下,两个进程都无法继续执行,因为它们都在等待对方释放资源,从而产生了死锁。

预防死锁的方法包括:

  1. 避免使用多个资源:尽量将程序设计成只需要一个资源的情况,减少了死锁的产生可能性。
  2. 规定加锁顺序:当线程需要获取多个资源时,规定一个固定的加锁顺序,并确保所有线程都按照这个顺序来获取锁。这样可以避免循环等待的情况。
  3. 引入超时机制:对于需要获取资源的线程,设定一个超时时间。如果在这个时间内无法获取到资源,线程就放弃资源的获取,并稍后重试。这样可以防止某个线程一直等待某个资源,从而避免死锁的发生。

6硬链接与软链接的区别;⭐⭐⭐⭐⭐

首先,从定义和性质上看,硬链接(类似复制)实际上是同一个文件的不同名字,即多个文件名链接到同一个文件。这种链接方式使多个文件名能够同时修改同一个文件,一个文件名的修改会反映在所有其他与其有硬链接的文件名上。而软链接(类似快捷方式),也称为符号链接,是一个包含另一个文件路径名的文件。这个链接可以是任意文件或目录,甚至可以链接到不同文件系统的文件。

其次,它们在创建和删除时表现不同。硬链接不可以跨设备或分区创建,而且创建硬链接会增加文件的链接数。当删除硬链接时,原始文件依然可以使用,因为硬链接和原始文件在文件系统上是平等的。当原文件被删除时,实际上只是删除了该文件的目录项,而文件的inode和数据块并不会立即被删除只要还有其他的硬链接指向这个inode,文件的数据就仍然可以被访问。只有当最后一个指向该inode的链接(无论是原文件还是硬链接)被删除时,inode和数据块才会被系统回收,文件内容才会真正消失。

相反,软链接可以跨设备、分区甚至跨网络创建,创建软链接并不会改变文件的链接数。然而,当删除软链接所指向的原始文件时,软链接会失效,因为它只是原始文件的一个标记或快捷方式。

再者,从文件系统的角度看,硬链接和原始文件共享同一个inode号(文件在文件系统上的唯一标识),这意味着它们在文件系统中被视为相同的实体。而软链接则不共享inode,它们有自己的inode,但这个inode包含的是另一个文件的路径。

最后,文件夹(目录)的链接方式也存在差异。文件夹不能创建硬链接,但可以创建软链接

7虚拟内存,虚拟地址与物理地址的转换⭐⭐⭐⭐

虚拟内存是计算机系统内存管理的一种技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。大多数操作系统都使用了虚拟内存技术,如Windows家族的“虚拟内存”以及Linux的“交换空间”等。

虚拟地址与物理地址之间的转换过程涉及几个关键步骤:

  1. 进程访问虚拟地址时,该虚拟地址包括两部分:高位部分是页表号,低位部分是偏移量。页表号用于找到对应的页表项,而偏移量表示在该页表项指向的页中的偏移位置
  2. 当进程访问虚拟地址时,硬件会进行页表查找,以确定对应的物理页框。操作系统维护了每个进程的页表,其中记录了虚拟页号与物理页框的映射关系。
  3. 通过页表查找,找到对应的页表项后,就可以得到物理页框号。然后,将物理页框号与偏移量组合起来,得到真正的物理地址。

8计算机中,32bit与64bit有什么区别⭐⭐⭐

  1. 位数定义与数据处理能力:32位系统指的是计算机系统中处理器和操作系统使用的32位数据单元,能够处理32位宽度的数据。而64位系统则使用64位数据单元,能够处理64位宽度的数据。因此,64位系统理论上具有更强的数据处理能力,能够处理更大规模的数据集
  2. 内存寻址能力:32位系统的地址总线宽度为32位,其最大内存寻址能力为4GB(2的32次方)。而64位系统的地址总线宽度为64位,其最大内存寻址能力可达16EB(2的64次方),远远超过32位系统的寻址范围。
  3. 兼容性:32位计算机系统能够运行32位的操作系统和软件程序,而64位计算机系统则可以支持32位和64位的操作系统和软件程序。这意味着在64位系统上,你可以运行专门为64位系统设计的软件,以充分利用其性能优势,同时也可以运行旧的32位软件。
  4. 性能表现:对于普通用户而言,32位系统通常能够满足一般的日常需求,如浏览网页、办公应用等。然而,在处理大规模数据、运行复杂软件或进行高性能计算时,64位系统通常能够提供更好的性能和稳定性

9中断和异常的区别⭐⭐⭐⭐⭐

  1. 触发方式:中断通常是由外部事件引发的,如硬件设备请求服务、定时器超时等。它使CPU暂时中止当前程序的执行,转而处理该事件。而异常则是由正在执行的指令内部引发的,通常是由于程序执行过程中的错误或异常条件导致的,如除数为零、数组越界等。
  2. 处理机制:当中断发生时,CPU会保存当前程序的执行上下文(如程序计数器、寄存器状态等),然后跳转到中断服务程序(ISR)进行处理。处理完成后,CPU会恢复之前的执行上下文并继续执行程序。而异常的处理通常涉及到错误检测、错误报告和可能的程序恢复或终止。在处理异常时,CPU可能会执行一些特定的错误处理代码,或者将控制权交给操作系统或其他高级软件进行处理。

10中断怎么发生,中断处理大概流程⭐⭐⭐⭐

  1. 中断请求:当外部设备或内部事件产生中断请求时,CPU会接收到这个请求,并判断是否需要响应。
  2. 中断响应:CPU根据中断优先级等因素决定是否响应中断。如果决定响应,CPU会保存当前程序的执行状态(如程序计数器、寄存器等),然后跳转到中断服务程序(ISR)的入口地址。
  3. 中断服务程序执行:CPU开始执行中断服务程序,处理中断请求对应的事件。这可能包括读取设备状态、发送数据、执行特定的操作等。
  4. 恢复现场:当中断服务程序执行完毕后,CPU需要恢复之前保存的程序执行状态,包括程序计数器、寄存器等。
  5. 返回原程序:最后,CPU返回到被中断的程序中,继续执行原来的任务。

11 Linux 操作系统挂起、休眠、关机相关命令⭐⭐

挂起(Suspend)在Linux系统中是一种很常见的操作,它允许用户暂时中断电脑的活动,并将其置于低功耗状态。挂起可以大大节省电力,并便于快速恢复到之前的工作状态。

  1. systemctl suspend:这个命令是Linux系统中挂起计算机的常用方法。它会将系统置于睡眠状态,暂停CPU和其他硬件设备的工作,并将RAM中的数据存储到磁盘中。当需要恢复系统时,可以快速从挂起状态唤醒,而不需要重新启动系统。
  2. Ctrl + Z:在终端中,按下Ctrl + Z组合键可以将前台进程挂起到后台。被挂起的进程会停止执行,但并未终止,可以使用jobs命令查看挂起的进程列表。
  3. kill -SIGSTOP 进程ID:kill命令用于终止正在运行的进程,但也可以通过发送SIGSTOP信号来挂起进程。这个信号会暂停进程的执行,但不会终止进程。
  4. pkill -STOP 进程名pkill命令可以根据进程名或者其他条件来挂起进程。

休眠(Sleep)是Linux系统中的另一个功能,通过sleep命令实现。sleep命令可以让程序暂停执行一定的时间,从而达到控制程序执行节奏的效果。sleep NUMBER[SUFFIX],其中NUMBER指的是休眠的时间,可以是整数或小数,SUFFIX则表示时间单位,默认为秒。

至于关机,Linux提供了多个命令选项。shutdown命令是Linux系统中最常用的关机命令之一,它可以在指定的时间关机,也可以立即关机。例如,shutdown -h now命令会立即关机,而shutdown -h +10命令则会在10分钟后关机。此外,halt命令和poweroff命令也可以用来关机,halt命令会立即关闭系统,而poweroff命令会先执行系统的关机脚本,然后关闭电源。init命令是Linux系统的初始化进程,也可以用来关机和重启系统,例如init 0命令会关机,而init 6命令会重启系统

12数据库为什么要建立索引,以及索引的缺点⭐⭐

  1. 加快查询速度:索引允许数据库引擎直接定位到满足查询条件的数据行,从而避免了全表扫描,大大提高了查询效率。
  2. 提高排序和分组效率:除了用于查找特定值,索引还可以用于加速排序和分组操作。数据库可以利用索引中的排序顺序或分组信息,无需对整个数据集进行排序或分组,从而提高这些操作的效率。
  3. 保证数据的唯一性:通过创建唯一性索引,可以确保数据库表中每一行数据的唯一性,这对于某些应用场景来说是非常重要的。

然而,索引也带来了一些缺点和需要权衡的因素:

  1. 创建和维护索引需要时间:随着数据量的增加,创建和维护索引所需的时间也会增加。这可能会影响到数据库的插入、修改和删除操作的效率。
  2. 占用物理空间:索引本身需要占用一定的存储空间。除了数据表本身占用的空间,每一个索引也会占用一定的物理空间。对于大型数据库来说,这可能会成为一个需要考虑的因素。
  3. 降低数据维护速度:当对表中的数据进行增加、删除和修改操作时,索引也需要动态地维护,这可能会降低数据的维护速度。

单片机

1 CPU 内存 虚拟内存 磁盘/硬盘 的关系⭐⭐⭐

  1. CPU(中央处理器):CPU是计算机的“大脑”,负责执行程序中的指令和处理数据。它从内存中获取指令和数据,然后执行相应的操作。CPU的性能直接影响计算机的整体运行速度和处理能力。
  2. 内存(RAM):内存是计算机中的临时存储设备,用于存储CPU正在处理或即将处理的数据和指令。CPU无法直接访问硬盘上的数据,因此需要通过内存来读取和写入数据。内存的大小和速度对计算机的性能有很大影响,内存越大,CPU可以处理的数据量就越多,处理速度也越快。
  3. 虚拟内存:虚拟内存是一种计算机内存管理技术,它将部分硬盘空间作为内存来使用。当物理内存(RAM)不足时,操作系统会将部分暂时不使用的数据从内存中移到硬盘上的虚拟内存区域,从而释放物理内存空间供其他程序使用。虚拟内存的存在使得计算机可以在有限的物理内存条件下运行更大的程序或处理更多的数据。
  4. 磁盘/硬盘:硬盘是计算机的主要存储设备,用于永久保存数据和程序。硬盘的容量通常比内存大得多,可以存储大量的文件和信息。当需要读取或写入数据时,CPU会向硬盘发出请求,硬盘会将相应的数据加载到内存中供CPU处理。

CPU从内存中获取指令和数据进行处理,当内存不足时,虚拟内存会提供额外的存储空间,而硬盘则是长期存储数据和程序的主要设备。这些组件共同构成了计算机系统的核心部分,决定了计算机的性能和功能。

2 CPU内部结构⭐⭐⭐⭐

  1. 内核(Die):也称为核心,是CPU最重要的组成部分。从结构上讲,内核主要分为两部分:运算器和控制器。运算器中的算术逻辑运算单元(ALU)主要完成对二进制数据的定点算术运算(加减乘除)、逻辑运算(与或非异或)以及移位操作;浮点运算单元(FPU)则主要负责浮点运算和高精度整数运算。此外,通用寄存器组用于保存参加运算的操作数和中间结果,而专用寄存器则是一些状态寄存器,通常由CPU自己控制。
  2. 基板:基板是承载CPU内核的电路板,是核心和针脚的载体。它负责内核芯片和外界的通信,并决定这一颗芯片的时钟频率。基板上还有电容、电阻以及决定CPU时钟频率的电路桥(俗称金手指)。在基板的背面或者下沿,还有用于和主板连接的针脚或者卡式接口。
  3. 填充物:位于CPU内核和基板之间,用于缓解散热器的压力、固定芯片和电路基板。其优劣直接影响整个CPU的质量。
  4. 封装:CPU封装技术是一种将集成电路用绝缘的塑料或陶瓷材料打包的技术。封装不仅固定、密封、保护芯片,还增强了导热性能。同时,它也是沟通芯片内部与外部电路的桥梁

此外,CPU还包括总线,用于连接CPU和其他设备,如内存、输入输出设备等。输入输出控制器(IO)负责控制计算机与外部设备之间的数据传输,可以向外部设备发送数据和指令,也可以将外部设备传输的数据和指令传输到CPU中。

3 ARM结构处理器简析 ⭐⭐

ARM结构处理器是一种基于精简指令集(RISC)架构的微处理器设计。其设计理念在于尽量简化指令集,以提高指令执行效率,从而实现更高的运行速度和更低的功耗。这种设计使得ARM处理器在功耗、成本以及性能之间达到了较好的平衡,因此被广泛应用于各种电子设备中。

从内部结构来看,ARM处理器同样包含控制单元、程序计数器(PC)、指令寄存器(IR)、数据通道、存储器等典型微处理器的基本组成部分。其指令执行过程一般分为取指、译码、执行和存储等阶段。在一些先进的ARM处理器中,指令实现流水线作业,以提高处理效率。

4 波特率是什么,为什么双方波特率要相同,高低波特率有什么区别;⭐⭐⭐⭐

波特率(Baud rate)在电子通信领域是一个关键概念,它指的是有效数据信号调制载波的速率,即单位时间内载波调制状态变化的次数。它是衡量串行数据速度快慢的重要指标,相当于通讯的采样率。波特率越高,表示单位时间内传输的码元符号个数越多,数据传输速度越快。然而,提高波特率也会增加传输误码率,降低数据的可靠性。

提高波特率(即提高信息的传输速率)可以提高有效性,但可能会以牺牲可靠性为代价。

至于为什么通信双方的波特率需要相同,这主要是为了确保数据能够准确、可靠地传输。如果双方的波特率不一致,那么接收方可能无法正确解析发送方发送的数据,从而导致数据传输错误或通信失败。因此,在建立通信连接之前,双方通常会进行波特率协商,以确保使用相同的波特率进行数据传输。

高低波特率的区别主要在于数据传输速度和可靠性的权衡。高波特率可以提供更快的数据传输速度,但可能降低数据的可靠性;而低波特率虽然数据传输速度较慢,但通常具有更高的可靠性。因此,在选择波特率时,需要根据具体的应用场景和需求进行权衡和选择。

5 arm和dsp有什么区别⭐⭐

ARM是一种基于RISC架构的微处理器,其设计理念是尽量简化指令集,提高指令执行效率,以获得更高的运行速度和更低的功耗。ARM处理器具有低成本、低功耗和高可靠性等特点,被广泛应用于各种电子设备中。它具有较强的事务管理功能,可以用于运行界面和应用程序等,其外围接口丰富,标准化和通用性较好,适合在消费电子品等场景中使用。此外,ARM处理器还能很好地兼容8位/16位器件,并带有指令Cache和数据Cache,其指令执行速度较快,可以兼顾性能、价格、功耗、代码密度等方面。

而DSP(数字信号处理器)则具有其独特的特点。它使用数字表示信号和进行计算处理,能够实现高精度的数值计算和精确的信号重构,避免了模拟信号可能受到的噪音、干扰和衰减等问题。DSP处理器具有可编程性,可以根据不同应用的需求进行编程和算法的优化,适用于各种领域的信号处理应用,如音频/视频编解码、通信系统、图像处理等。此外,DSP处理器通常具备较高的计算性能和并行处理能力,能在实时性要求较高的应用中进行快速的信号处理

综上所述,ARM和DSP在应用场景、功能特性和性能要求等方面存在显著的差异。ARM更侧重于控制和通用计算,而DSP则更专注于数字信号处理和高精度计算

6 ROM RAM的概念浅析⭐⭐⭐

ROM,即只读存储器(Read-Only Memory),是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦数据被存入后,就不能被更改或删除,通常用在不需经常变更数据的电子或电脑系统中,并且数据不会因为电源关闭而消失。ROM所存数据,一般是装入整机前事先写好的,整机工作过程中只能读出,而不像随机存储器那样能快速地、方便地加以改写。ROM所存数据稳定 ,断电后所存数据也不会改变;其结构较简单,读出较方便,因而常用于存储各种固定程序和数据。

RAM,即随机存取存储器(Random Access Memory),是与CPU直接交换数据的内部存储器。它可以随时读写(刷新时除外),而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储介质。RAM工作时可以随时从任何一个指定的存储单元中读取数据(包括写入的数据),也可以把数据写入任何一个指定的存储单元中。当电源关闭时,RAM不能保留数据。如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。RAM的工作特点是通电后,随时可在任意位置单元存取数据信息,断电后内部所存储的数据也随之丢失。

7 IO口工作方式:上拉输入 下拉输入 推挽输出 开漏输出⭐⭐⭐⭐

  1. 上拉输入:这种方式是通过将一个电阻与电源VCC相连,将不确定的信号嵌位在高电平。当IO口为输入模式且为上拉电阻时,IO口的常态为高电平
  2. 下拉输入:这是将一个电阻与地线GND相连,将不确定的信号固定在低电平。当IO口为输入模式且为下拉电阻时,IO口的常态为低电平
  3. 推挽输出:推挽输出既可以输出高电平,也可以输出低电平,可以直接驱动功耗不大的数字器件。其工作原理是两个三极管或MOSFET以推挽方式存在于电路中,每次只有一个三极管导通,所以导通损耗小、效率高。
  4. 开漏输出:开漏输出是指IO口可以输出高电平(Vcc)和悬空状态(高阻态),而无法输出低电平(地)。当IO口处于高电平状态时,输出引脚处于高阻态,不会直接连接到电源电压上;当IO口处于低电平状态时,输出引脚被拉低,连接到地。开漏输出的优势在于可以与其他开漏输出或外部电路连接,形成逻辑电路,也可以实现多个IO口的并行输出,提高输出能力。

8扇区 块 页 簇的概念⭐⭐⭐⭐

扇区:扇区是磁盘或硬盘上的最小物理存储单位。每个磁道被等分为若干个弧段,这些弧段便是磁盘的扇区。硬盘的读写操作以扇区为基本单位进行。扇区的大小一般为512字节或其整数倍,这是由历史原因和兼容性决定的。扇区是磁盘驱动器在向磁盘读取和写入数据时使用的最小操作单元。

:块是上层软件(如操作系统)在处理文件时使用的最小操作单元。它的大小可以在格式化时设置,常见的有512B、1KB、4KB等。块是文件系统对数据处理的基本单位,多个扇区可能组成一个块。块的概念来自文件系统,用于有效地管理磁盘空间和提高数据读写效率。

:在计算机中,页主要用于内存管理。它是操作系统中用于管理内存的基本单位之一,代表一块连续的内存区域,具有固定的大小。操作系统使用页作为内存管理的基本单元,来管理进程的虚拟内存和物理内存之间的映射关系。通过页的概念,操作系统可以有效地进行内存分配、保护和共享等操作。

:簇是磁盘上最小的可分配存储单位,它通常由多个扇区组成。在现代计算机系统中,簇的概念对于文件系统的性能和空间利用起着至关重要的作用。文件系统的设计者会根据磁盘的特性、操作系统要求以及文件大小等因素来确定簇的大小。簇的大小会影响文件系统的性能和存储空间的利用率。较小的簇可以提供更精细的空间分配,但会增加管理复杂性和开销;而较大的簇则可以减少管理开销,但可能会浪费一些存储空间。

9简述处理器在读内存的过程中,CPU核、cache、MMU如何协同工作?画出CPU核、cache、MMU、内存之间的关系示意图加以说明⭐⭐

  1. CPU核发起请求:当CPU核需要读取某个内存地址的数据时,它会发出一个读取请求。
  2. MMU处理虚拟地址:MMU(内存管理单元)接收到CPU核的请求后,首先检查这个地址是否是虚拟地址。如果是,MMU会负责将这个虚拟地址翻译成对应的物理地址。这个翻译过程通常涉及查找页表,确定虚拟地址对应的物理地址以及访问权限。
  3. Cache查找:得到物理地址后,CPU会先检查Cache(缓存)中是否已经有这个地址的数据。Cache通常分为多个层级(如L1、L2、L3),CPU会按照层级顺序进行查找。如果Cache命中,即所需数据已经在Cache中,那么CPU会直接从Cache中读取数据,避免了访问主存的延迟。
  4. 主存读取:如果Cache未命中,CPU会向主存(内存)发出读取请求,从主存中读取所需数据。此时,主存会按照CPU提供的物理地址找到对应的数据,并将其返回给CPU。
  5. 数据更新与Cache填充:CPU读取到数据后,除了满足自身的处理需求,还会将数据写入Cache中,以便后续再次访问时可以直接从Cache中读取,提高访问速度。

10请说明总线接口USRT、I2C、USB的异同点(串/并、速度、全/半双工、总线拓扑等)⭐⭐⭐⭐⭐

11什么是异步串口和同步串口⭐⭐⭐⭐⭐

  1. 异步串口:

数据传送以字符为单位,字符与字符间的传送是完全异步的,但位与位之间的传送基本上是同步的。

相邻两字符间的间隔是任意长。

因为一个字符中的比特位长度有限,所以需要的接收时钟和发送时钟只要相近就可以。

异步方式的特点简单来说就是:字符间异步,字符内部各位同步

异步串口通过数据首尾的起始和停止位进行同步,每个数据字节都有一个起始位和一个或多个停止位,当停止位出现时,数据传输结束。

异步串口不需要时钟同步,但数据传输速度相对较慢。

2.同步串口:

数据传送是以数据块(一组字符)为单位,字符与字符之间、字符内部的位与位之间都同步。

同步串口在传输时使用外部时钟信号来进行同步,数据被划分为完整块的数据帧,发送方和接收方通过这个时钟信号来进行同步,确保数据能够被准确的传输和接收。

数据传输速度相对较快。

12 I2C时序图⭐⭐⭐⭐⭐

继承、多态相关面试题

C/C++的变量作用域

  1. 局部作用域(Local Scope)
    • 在函数或代码块(由一对大括号 {} 包围的区域)内部定义的变量具有局部作用域。
    • 这些变量只能在其定义的函数或代码块内部访问
    • 当函数或代码块执行完毕后,这些变量的生命周期结束,它们所占用的内存会被释放
  1. 全局作用域(Global Scope)
    • 在所有函数外部定义的变量具有全局作用域。
    • 这些变量可以在整个程序中的任何位置访问。
    • 全局变量的生命周期是整个程序的执行期间

        2.文件作用域(File Scope)

  1. 在C中,使用 static 关键字在函数外部定义的变量具有文件作用域。
    • 这样的变量只能在定义它的源文件中访问
    • 在C++中,文件作用域与全局作用域相同,但 static 关键字在类中有不同的含义。
  2. 块作用域(Block Scope)
    • 在C++中,块作用域与局部作用域类似,但还包括了由 ifforwhile 等语句创建的代码块。
    • 在这些代码块中定义的变量具有块作用域。
  3. 函数原型作用域(Function Prototype Scope)
    • 在函数原型中声明的参数只在函数原型中具有作用域
    • 这些参数的作用域在函数原型结束后就结束了。
  4. 类作用域(Class Scope)
    • 在C++中,类的成员变量具有类作用域
    • 这些变量可以在类的任何成员函数内部访问,并且可以通过类的对象或指针来访问

1继承和虚继承 ⭐⭐⭐⭐⭐

普通继承

普通继承(非虚继承)是最直接的继承方式。在普通继承中,派生类会包含基类的所有成员(包括私有、保护和公有成员),但基类的私有成员在派生类中是不可访问的。如果基类中有公有成员,那么在派生类中它们仍然是公有的;如果基类中有保护成员,那么在派生类中它们会变成保护的。

class Derived : public Base {  
public:  
    void bar() { /* ... */ }  
};  

虚继承

虚继承是为了解决多重继承中的菱形问题而引入的。在多重继承中,如果一个类从多个基类继承,而这些基类又共同继承自同一个更基础的类,那么最终派生类将包含多个基类的拷贝,这可能导致数据冗余和访问不一致的问题。

虚继承通过在继承时引入一个间接层(虚基类表),使得最终派生类只包含一个共享的基类子对象。这样,无论通过哪个路径继承,最终派生类中的基类子对象都是同一个

class VirtualBase {  
public:  
    void show() { /* ... */ }  
};  
  
class Base1 : virtual public VirtualBase { /* ... */ };  
class Base2 : virtual public VirtualBase { /* ... */ };  
  
class Derived : public Base1, public Base2 { /* ... */ };  
  
int main() {  
    Derived d;  
    d.show(); // 只会调用一次VirtualBase的show函数,避免了数据冗余  
}

注意事项

  1. 虚继承通常会增加一些开销,因为需要维护虚基类表
  2. 虚继承中的构造函数调用顺序与多重继承相同,但虚基类只会被构造一次。
  3. 虚继承主要用于解决多重继承中的菱形继承问题,普通情况下不需要使用虚继承。
  4. 在使用虚继承时,需要特别注意构造函数和析构函数的调用顺序,确保资源得到正确的初始化和释放。

通过合理地使用继承和虚继承,可以创建出更加灵活和可维护的类结构,从而实现更复杂的面向对象编程任务

2多态的类,内存布局是怎么样的 ⭐⭐⭐⭐⭐

多态是面向对象编程中的一个核心概念,它允许我们使用父类类型的引用(或指针)来操作子类对象,从而执行子类覆盖的父类方法。多态的实现依赖于继承、虚函数和动态绑定等机制。

  1. 虚函数表(vtable):当一个类包含虚函数时,编译器通常会为该类创建一个虚函数表。这个表是一个指针数组,其中每个指针指向该类的一个虚函数的实现。对于包含虚函数的派生类,它们的vtable通常包含基类的虚函数和它们自己新增或覆盖的虚函数
  2. 对象内存布局:一个包含虚函数的对象在内存中通常包含一个指向其vtable的指针(通常被称为vptr)。这个vptr是在对象创建时由编译器设置的,它使得对象能够知道如何调用其虚函数。vptr通常位于对象的内存布局的开始部分,但具体位置取决于编译器和平台。
  3. 继承与内存:当类通过继承关系形成层次结构时,派生类对象在内存中通常包含基类部分和自己的新增部分。基类部分包含基类的数据和虚函数表指针(如果有的话)。派生类部分则包含它自己的数据和可能的新增或覆盖的虚函数。
  4. 多态调用:当我们使用基类指针或引用来操作派生类对象时,实际调用的虚函数是通过vptr和vtable来确定的。这样,即使指针或引用的类型是基类,调用的也是派生类中覆盖的虚函数版本(如果存在的话)。(动态绑定

3被隐藏的基类函数如何调用或者子类调用父类的同名函数和父类成员变量 ⭐⭐⭐⭐⭐

要调用被隐藏的基类函数,你需要使用作用域解析运算符 :: 

要访问基类的成员变量,你可以直接在派生类中使用它,前提是它是公有的(public)或受保护的(protected)。如果是私有的(private),那么你将无法直接访问。

如果基类的成员变量是私有的,你需要提供一个公有的访问器函数(getter和setter)来间接访问它。

class Base {  
private:  
    int baseVar;  
public:  
    int getBaseVar() const { return baseVar; }  
    void setBaseVar(int value) { baseVar = value; }  

};  
  
class Derived : public Base {  
    // 可以直接调用基类提供的访问器函数  
};  
  
int main() {  
    Derived d;  
    d.setBaseVar(42); // 使用基类提供的setter函数设置变量  
    int value = d.getBaseVar(); // 使用基类提供的getter函数获取变量值  

}

4多态实现的三个条件、实现的原理 ⭐⭐⭐⭐⭐

多态实现的三个条件

  1. 继承:多态是建立在继承的基础上的。一个类(派生类或子类)必须继承自另一个类(基类或父类)。

  2. 虚函数基类中必须至少有一个虚函数。虚函数是通过在基类的成员函数声明前加上 virtual 关键字来声明的。虚函数允许在派生类中重写(覆盖)该函数,并在运行时根据对象的实际类型来决定调用哪个版本。

  3. 指针或引用:必须使用基类类型的指针或引用来操作派生类对象。这样,当调用虚函数时,才能根据对象的实际类型来实现动态绑定。

多态实现的原理

多态的实现原理主要依赖于动态绑定和虚函数表(vtable)。

  1. 虚函数表(vtable):编译器为每个包含虚函数的类创建一个虚函数表。这个表是一个函数指针数组,其中每个指针指向该类的一个虚函数的实现。对于包含虚函数的派生类,它们的vtable通常包含基类的虚函数和它们自己新增或覆盖的虚函数

  2. 虚函数指针(vptr):每个包含虚函数的类的对象在内存中都有一个指向其vtable的指针(vptr)。这个vptr是在对象创建时由编译器设置的,它使得对象能够知道如何调用其虚函数。vptr通常位于对象的内存布局的开始部分,但具体位置取决于编译器和平台。

  3. 动态绑定:当使用基类类型的指针或引用来调用虚函数时,实际调用的函数版本是根据对象的vptr和vtable来确定的。在运行时,通过vptr查找vtable,然后找到并调用相应的虚函数实现。这样,即使指针或引用的类型是基类,调用的也是派生类中覆盖的虚函数版本(如果存在的话)。

5对拷贝构造函数 深浅拷贝 的理解 拷贝构造函数作用及用途?什么时候需要自定义拷贝构造函数?⭐⭐⭐

拷贝构造函数是C++中的一个特殊构造函数,用于创建对象作为现有对象的副本。拷贝构造函数的名字与类名相同,且它有一个参数,这个参数是对同类对象的常量引用。

拷贝构造函数的主要作用就是进行对象的复制。在C++中,当对象需要被复制时,就会调用拷贝构造函数。这通常发生在以下情况:

  1. 当一个对象作为函数参数进行值传递时。
  2. 当一个对象作为函数返回值进行值返回时。
  3. 当使用对象进行初始化时,如 A a = b;

深浅拷贝是拷贝构造函数实现时需要考虑的两个重要概念:

  • 浅拷贝(Shallow Copy):只是简单地将源对象的所有成员变量复制到新对象中。如果对象中含有动态分配的内存(例如指针指向的内存),那么浅拷贝会导致两个对象共享同一块内存。这可能会导致问题,因为当其中一个对象释放这块内存时,另一个对象就会访问到无效的内存。
  • 深拷贝(Deep Copy):在复制对象时,不仅复制对象的所有成员变量,而且为所有动态分配的内存创建新的副本。这样,每个对象都有自己的内存空间,互不影响。

在以下情况下,你可能需要自定义拷贝构造函数

  1. 类中含有指针成员,且需要复制指针指向的内容时,应使用深拷贝
  2. 当类需要执行特定的复制行为时,例如,你可能需要复制某些成员变量,但忽略其他成员变量,或者复制对象时需要进行一些额外的操作。
  3. 当类中含有动态分配的内存时,为了避免浅拷贝带来的问题,应使用深拷贝。

#include <iostream>  
#include <cstring>  
  
class String {  
public:  
    String(const char* str = "") {  
        if (str) {  
            size = strlen(str);  
            data = new char[size + 1];  
            strcpy(data, str);  
        } else {  
            size = 0;  
            data = nullptr;  
        }  
    }  
      
    // 自定义拷贝构造函数实现深拷贝  
    String(const String& other) {  
        size = other.size;  
        data = new char[size + 1]; // 分配新的内存  
        strcpy(data, other.data); // 复制内容  
    }  
      
    // 注意:还需要定义赋值操作符的重载以避免类似问题  
    String& operator=(const String& other) {  
        if (this != &other) { // 防止自赋值  
            delete[] data; // 释放原有内存  
            size = other.size;  
            data = new char[size + 1]; // 分配新的内存  
            strcpy(data, other.data); // 复制内容  
        }  
        return *this;  
    }  
      
    ~String() {  
        delete[] data;  
    }  
      
    void print() const {  
        std::cout << data << std::endl;  
    }  
  
private:  
    char* data;  
    int size;  
};  
  
int main() {  
    String s1("Hello");  
    String s2 = s1; // 使用自定义的深拷贝构造函数  
      
    // 即使s1的内存被释放,s2仍然安全,因为它拥有自己的内存副本  
    String s3;  
    s3 = s1; // 使用自定义的赋值操作符重载进行深拷贝  
      
    // 正常的析构过程,每个对象释放自己的内存  
    return 0;  
}

6析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?⭐⭐⭐

析构函数不应该抛出异常。这主要基于以下几个原因:

  1. 资源泄露:如果在析构函数中抛出异常,而该异常没有被捕获,那么程序将终止。如果析构函数在执行一些必要的清理工作(如释放内存)时抛出异常,那么这些清理工作可能无法完成,从而导致资源泄露。
  2. 异常安全:C++中的异常安全通常分为三种级别:基本保证、强保证和无泄露保证。如果析构函数抛出异常,那么它可能破坏这些保证,使得程序在异常处理过程中更难以预测和管理。
  3. 对象销毁顺序:当局部对象或动态分配的对象在作用域结束时被销毁时,它们的析构函数会按照特定的顺序被调用。如果在这个过程中某个析构函数抛出异常,那么可能会打乱析构函数的调用顺序,导致对象状态的不一致。
  4. 编程习惯和可读性:从编程习惯和可读性的角度来看,析构函数的主要任务是进行清理工作,而不是报告错误或处理异常情况。如果析构函数抛出异常,那么可能会使代码的阅读和维护变得更加困难。

除了资源泄露,还需要考虑以下因素:

  • 异常处理复杂性:如果析构函数抛出异常,那么需要仔细考虑如何处理这个异常。在析构函数中处理异常可能比在普通函数中更复杂,因为析构函数可能在程序的多个地方被调用,而每个地方都可能有不同的异常处理需求。
  • 程序稳定性:析构函数抛出异常可能会降低程序的稳定性。在程序即将结束时抛出异常可能导致程序异常终止,而不是优雅地结束。

7什么情况下会调用拷贝构造函数(三种情况)⭐⭐⭐

  1. 对象初始化:当创建一个新对象作为现有对象的副本时,会调用拷贝构造函数。这通常发生在以下几种情况:

    • 使用等号初始化对象时,如 MyClass obj2 = obj1;
    • 函数返回对象时,拷贝构造函数用于创建返回值的副本。
    • 当对象作为函数参数以值传递方式传递时,会调用拷贝构造函数来创建参数的副本。
  2. 数组初始化:当使用现有对象来初始化对象数组时,会为每个数组元素调用拷贝构造函数

        MyClass obj1;MyClass arr[3] = {obj1, obj1, obj1}; // 这里会调用三次拷贝构造函数

        3.编译器生成的临时对象:在某些情况下,编译器会自动生成临时对象,并调用拷贝构造函数。例如,在涉及多个返回值的表达式中,或者在对象被用作复合表达式的组成部分时。

8析构函数一般写成虚函数的原因⭐⭐⭐⭐⭐

析构函数一般被写成虚函数(virtual destructor)的主要原因是为了支持基类的指针或引用能够正确地销毁派生类的对象,即实现多态性的析构。当使用基类指针或引用来指向派生类对象,并通过该指针或引用调用析构函数时,如果析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类特有的资源没有得到正确的清理,从而引发资源泄露等问题。

另外,需要注意的是,一旦基类的析构函数被声明为虚函数,那么该基类就不能被作为数组的元素类型了,因为C++标准规定含有虚函数的类不能作为数组的元素类型。这是因为数组元素的析构需要确定大小,而虚函数的存在使得对象的大小在运行时才能确定。

9构造函数为什么一般不定义为虚函数⭐⭐⭐⭐⭐

  1. 构造时机:虚函数机制依赖于虚函数表(vtable),这个表通常在对象构造完成之后才初始化。如果构造函数是虚的,那么在构造对象时就需要访问这个尚未初始化的虚函数表,这将导致未定义的行为。

  2. 继承与多态:构造函数的调用与对象的构造顺序紧密相关。当我们创建一个派生类对象时,首先会调用基类的构造函数来初始化基类部分,然后再调用派生类的构造函数来初始化派生类特有的部分。这个过程是确定的,与多态无关。因此,构造函数不需要虚函数机制来实现多态性。

  3. 效率考虑:虚函数调用通常涉及运行时开销,因为需要查找虚函数表以确定要调用的具体函数。构造函数在对象生命周期的开始就被调用,如果每次构造对象都需要这样的开销,将会影响程序的性能。

  4. 语义上的不合适:构造函数的主要目的是初始化对象的状态。而虚函数的主要目的是实现多态性,允许通过基类指针或引用来调用派生类的成员函数。从语义上讲,将这两者结合起来并不合适。

10什么是纯虚函数⭐⭐⭐⭐⭐

纯虚函数是一种特殊的虚函数,它在基类中不给出具体实现,而是声明为纯虚函数,将其实现留给该基类的派生类去完成。纯虚函数也可以被称为抽象函数,它通常只有函数名、参数和返回值类型,不需要函数体,即没有函数的实现部分。

含有纯虚函数的基类是不能用来定义对象的,因为纯虚函数没有实现部分,不能产生对象。因此,含有纯虚函数的类也被称为抽象类。纯虚函数的主要作用是在基类中定义一个接口,要求所有的派生类都必须实现这个接口,从而实现多态性。

在设计和使用纯虚函数时,需要确保派生类正确实现了这些纯虚函数,以避免运行时错误或未定义行为。

11静态绑定和动态绑定的介绍⭐⭐⭐⭐

静态绑定,也被称为前期绑定或编译时绑定,是在程序编译过程中,把的过程。在程序执行前,即编译期就已经确定了调用方法所属类。因此,静态绑定不能利用任何运行期的信息,它主要针对函数调用和函数的主体,或变量与内存中的区块。静态绑定适用于静态方法和非虚函数,编译器会根据函数或方法的名称和参数类型来确定调用方式。

动态绑定,也被称为后期绑定或运行时绑定,是指在程序运行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法动态绑定是和类的继承以及多态相联系的在继承关系中,子类是父类的一个特例,所以父类对象可以出现的地方,子类对象也可以出现。因此,动态绑定允许在运行时根据对象的实际类型来确定调用哪个函数或方法,它适用于虚函数和多态。

12 C++所有的构造函数 ⭐⭐⭐

  1. 默认构造函数:不带任何参数,用于创建无参对象。当没有显式地定义构造函数时,编译器会自动生成一个默认构造函数。默认构造函数可以用于在类中的数据成员都有默认值,或者当类中的数据成员需要进行初始化时使用。
  2. 带参构造函数(Parameterized Constructor):接受一个或多个参数,用于在创建对象时进行初始化操作。这些参数通常用于设置对象的属性值。
  3. 拷贝构造函数(Copy Constructor):用于创建一个新对象,并使用已有对象的值来初始化新对象。拷贝构造函数通常接受一个对同类对象的常量引用作为参数
  4. 移动构造函数(Move Constructor):用于将一个对象的资源所有权转移给另一个对象,而不是进行复制。移动构造函数通常接受一个对同类对象的右值引用作为参数。
  5. 复制赋值构造函数(Copy Assignment Constructor):用于将一个对象的值复制给另一个已存在的对象。它通常作为类的成员函数实现,接受一个对同类对象的常量引用作为参数,并返回对调用对象的引用。
  6. 移动赋值构造函数(Move Assignment Constructor):用于将一个对象的资源所有权转移给另一个已存在的对象,而不是进行复制赋值操作。它通常接受一个对同类对象的右值引用作为参数,并返回对调用对象的引用。

13重写、重载、覆盖的区别⭐⭐⭐⭐⭐

  1. 重载(Overload)
    • 发生在同一个类中。
    • 方法名必须相同,但参数列表必须不同(参数的数量、类型或顺序不同)。
    • 重载的方法可以有不同的返回值类型和修饰符,但这些不是区分重载方法的依据。
    • 重载的主要目的是在同一个类中为相同的行为提供多种实现方式,以提高代码的可读性和灵活性。
  2. 重写(Override)
    • 发生在具有继承关系的两个类之间,子类重写父类的方法。
    • 方法名、参数列表、返回值类型都必须与父类中被重写的方法保持一致
    • 重写方法的访问权限范围必须大于或等于父类方法。
    • 重写方法的抛出异常类型范围不能大于父类方法。
    • 重写的主要目的是当父类方法无法满足子类的需求时,子类通过重写父类方法来实现自己的需求。
  3. 覆盖(有时指重定义Redefine)
    • 也是发生在具有继承关系的两个类之间,但更侧重于隐藏父类的方法。
    • 子类中的方法与父类中的方法同名,但参数列表可能相同或不同(如果参数列表相同,则为重定义;如果参数列表不同,则为另一种形式的隐藏)。
    • 如果子类中的方法与父类中的方法具有相同的参数列表,并且父类方法被声明为虚函数,则子类方法会覆盖父类方法。
    • 覆盖或重定义的主要目的是在子类中隐藏或改变父类方法的行为。

14成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?⭐⭐⭐⭐

  1. 直接初始化:成员初始化列表提供了直接初始化的能力,这意味着它可以直接使用对象的构造函数进行初始化,而不是先构造一个临时对象,然后再将临时对象赋值给成员变量。对于某些类型,直接初始化比拷贝赋值更高效,因为它避免了不必要的拷贝操作。

  2. 常量成员和引用成员:常量成员和引用成员必须在构造时初始化,因为它们不能被赋值。使用成员初始化列表是初始化这些成员的唯一方法

  3. 效率:对于某些复杂类型的成员,特别是那些涉及到动态内存分配或者需要执行大量计算的类型,成员初始化列表可以避免构造函数体中的额外操作,从而提高了效率。

  4. 初始化顺序:使用成员初始化列表可以更明确地控制成员变量的初始化顺序。成员的初始化顺序是按照它们在类中声明的顺序进行的,而不是它们在初始化列表中出现的顺序。这个特性有助于避免依赖顺序问题的潜在错误。

  5. 构造函数链:当类有多个构造函数,并且一个构造函数需要调用另一个构造函数进行初始化时,成员初始化列表特别有用。通过委托构造函数(C++11以后),一个构造函数可以将其初始化工作委托给另一个构造函数,从而避免代码重复。

15如何避免编译器进行的隐式类型转换;(explicit)⭐⭐⭐⭐

在C++中,为了避免编译器进行的隐式类型转换,你可以使用explicit关键字来修饰类的构造函数。当构造函数被声明为explicit时,它不能用于隐式类型转换,只能用于显式转换。

使用explicit的关键点在于防止因为不必要的类型转换而引入的逻辑错误或性能问题。特别是当构造函数接受一个参数时,这个构造函数可能会意外地被用作隐式转换,导致一些不期望的行为。

网络编程

TCP UDP

1 TCP、UDP的区别 ⭐⭐⭐⭐⭐

  1. 连接方式:TCP是面向连接的协议,这意味着在数据通信之前,必须先建立连接。这种连接是一个“三次握手”的过程,确保发送方和接收方都准备好进行通信。而UDP则是一个无连接的协议,数据在发送前不需要建立连接,每个数据报都是一个独立的信息,包含完整的源地址和目的地址,因此在网络上以独立的方式传输。
  2. 可靠性:TCP提供可靠的数据传输服务。它通过序列号、确认应答、重发控制等功能确保数据按序到达,且没有丢失或重复。这种可靠性是通过在发送端对数据进行分包、编号、添加校验和,并在接收端进行确认、排序、重传等机制来实现的。而UDP则不保证数据包的顺序、可靠性或重复性。如果数据包在传输过程中丢失或损坏,UDP不会重新发送或进行错误检查。
  3. 效率:由于TCP需要建立连接和进行各种控制操作,其效率相对较低。尤其是在数据量较小的情况下,TCP的开销可能显得较大。而UDP则没有这些额外的开销,因此它的传输效率更高,尤其适用于对实时性要求较高的应用,如实时音视频传输、在线游戏等。
  4. 流量控制:TCP提供了流量控制功能,以确保发送方的发送速率不会超过接收方的处理能力。这有助于防止网络拥塞和数据丢失。而UDP则没有流量控制功能,发送方可以随意发送数据,不受接收方处理能力的限制
  5. 应用场景:TCP通常用于需要可靠数据传输的场景,如文件传输、电子邮件等。而UDP则更适用于对实时性要求高、对可靠性要求相对较低的场景,如实时音视频通信、在线游戏等。

2 TCP、UDP的优缺点⭐⭐⭐

TCP的优点:

  1. 可靠性高:TCP通过序列号、确认应答、超时重传等机制,确保数据按序、无差错地到达接收端,为应用程序提供可靠的字节流服务。

  2. 流量控制:TCP通过滑动窗口机制实现流量控制,能够避免发送方发送速率过快导致接收方无法及时处理的情况,有效防止网络拥塞

  3. 拥塞控制:TCP具有拥塞控制功能,能够根据网络状况调整发送速率,避免网络拥塞的发生,提高网络的整体性能。

TCP的缺点:

  1. 开销较大:TCP在建立连接、传输数据和断开连接过程中需要进行多次握手和确认,这增加了额外的开销,尤其是在小数据包传输时,这种开销可能更加明显。

  2. 传输速度慢:由于TCP的可靠性保障机制,如确认应答和超时重传,可能导致传输速度相对较慢,尤其是在网络状况不佳时

UDP的优点:

  1. 传输速度快:UDP无需建立连接和进行复杂的控制操作,因此传输速度较快,适用于对实时性要求较高的应用,如实时音视频通信、在线游戏等。

  2. 开销小:UDP没有TCP那样的连接建立和断开过程,每个数据报都是独立传输的,因此开销较小,适用于小数据包传输。

  3. 无需维护连接状态:UDP无需维护连接状态,简化了网络编程的复杂度,使得应用程序更加灵活和高效。

UDP的缺点:

  1. 不可靠:UDP不提供可靠的数据传输服务,数据包在传输过程中可能丢失、乱序或重复。如果应用程序需要确保数据的完整性,需要自行实现可靠性保障机制。

  2. 无流量控制和拥塞控制:UDP没有流量控制和拥塞控制机制,发送方可能以超出接收方处理能力的速率发送数据,导致数据丢失和网络拥塞。

3 TCP UDP适用场景⭐⭐⭐

TCP的适用场景:

  1. 文件传输:由于TCP的可靠性、流量控制和拥塞控制特性,它非常适合用于文件传输,如FTP(文件传输协议)就使用了TCP。文件传输通常需要确保数据的完整性和顺序性,TCP正好能够提供这样的保障。

  2. 电子邮件:SMTP(简单邮件传输协议)也是基于TCP的,因为它需要确保邮件的完整性和可靠传输。电子邮件对于数据的准确性要求很高,TCP的可靠性特性能够满足这一需求。

  3. 远程登录:如SSH(安全外壳协议)和Telnet等远程登录工具,也依赖于TCP的可靠性。这些工具需要确保用户输入和服务器响应的准确传输。

  4. 流媒体服务:虽然UDP通常用于实时流媒体传输,但在某些需要确保数据完整性的情况下,TCP也可能被使用。例如,一些高质量的视频流服务可能会选择TCP以确保视频数据的准确传输。

UDP的适用场景:

  1. 实时音视频通信:如VoIP(网络电话)、视频会议等应用,UDP因其低延迟和高效的特性而被广泛使用。在这些应用中,即使偶尔丢失一些数据包,也不会严重影响用户体验。

  2. 在线游戏:许多在线游戏使用UDP进行通信,因为它能够提供快速且实时的数据传输。在游戏中,快速的响应比数据的完整性更重要。

  3. DNS查询:DNS(域名系统)使用UDP进行域名解析。DNS查询通常很短小,且对实时性要求较高,因此UDP是一个合适的选择。

  4. 实时数据监测:在一些需要实时监测数据的应用中,如股票交易、网络监控等,UDP的快速传输特性能够确保数据的实时性。

4 TCP为什么是可靠连接⭐⭐⭐⭐

  1. 序列号与确认机制:TCP为发送的每一个数据包分配一个唯一的序列号,并在接收端通过确认机制来确保每一个数据包都被正确接收。如果某个数据包丢失或损坏,接收端会请求发送端重新发送该数据包。

  2. 流量控制:TCP通过滑动窗口机制实现流量控制,确保发送方的发送速率不会超过接收方的处理能力。这有助于避免数据丢失和网络拥塞,从而增强了连接的可靠性。

  3. 拥塞控制:TCP还具备拥塞控制功能,能够在网络拥塞时调整发送速率,以减少数据丢失和重传的可能性。这有助于保障网络资源的合理利用,提高数据传输的可靠性。

  4. 超时重传:当TCP发送一个数据包后,如果在一定时间内没有收到接收端的确认,它会认为该数据包已经丢失,并重新发送该数据包。这种超时重传机制有助于确保数据的完整性。

  5. 连接建立与断开过程:TCP在数据传输之前会进行三次握手来建立连接,确保发送方和接收方都准备好进行通信。在数据传输完成后,还会进行四次挥手来断开连接,确保所有数据都被正确处理和释放资源。

5典型网络模型,简单说说有哪些;⭐⭐⭐

典型的网络模型主要包括OSI七层参考模型和TCP/IP四层参考模型

OSI七层参考模型从下到上依次为:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。每一层都负责不同的通信功能,共同确保数据在网络中的准确传输。物理层负责传输原始比特流;数据链路层将数据分成帧进行传输,并负责帧的同步、差错控制等;网络层负责将数据包从源地址路由到目的地址;传输层提供可靠的端到端的数据传输服务;会话层负责建立、管理和终止会话;表示层处理数据的表示形式,如加密、压缩等;应用层则负责为用户提供各种网络服务。

TCP/IP四层参考模型则包括:网络接口层、网络层、运输层和应用层。网络接口层负责数据在物理网络上的传输;网络层负责数据的路由和转发;运输层提供可靠的数据传输服务,确保数据的完整性和顺序性;应用层则负责为用户提供各种应用服务。

6 Http1.1和Http1.0的区别⭐⭐⭐

  1. 持久连接:HTTP 1.0默认使用短连接,即每个请求/响应后都会关闭连接。而HTTP 1.1则默认使用持久连接,允许在同一个连接上发送多个请求和接收多个响应。这种特性避免了每次客户端与服务器请求都要重复建立释放TCP连接的开销,提高了网络利用率。
  2. 请求管道化:HTTP 1.1支持请求管道化,这意味着在一个持久连接上可以同时发送多个请求,而HTTP 1.0则不支持这一特性。
  3. 缓存处理:HTTP 1.0的缓存处理相对简单,主要通过“Expires”和“Cache-Control”头字段进行缓存控制。而HTTP 1.1则引入了更多的缓存控制机制,如“ETag”和“If-None-Match”等,提供了更丰富的缓存管理能力。
  4. Host头字段:在HTTP 1.0中,没有Host头字段。而HTTP 1.1则要求每个请求都包含Host头字段,以指示请求的目标主机

7 URI(统一资源标识符)和URL(统一资源定位符)之间的区别⭐⭐

  1. 定义和概念:URI是以一种抽象的、高层次概念来定义统一资源标识,它代表了一种字符串,用于唯一标识网络上的资源,可以表示各种类型的资源,如文件、目录、数据库记录等。而URL则是具体的资源标识的方式,是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。每一个互联网上的文件都有一个唯一的URL
  2. 组成和格式:URI通常由协议和路径两部分组成,协议指定了如何访问资源的类型,常见的协议包括HTTP、FTP、SSH等。而URL的格式一般由协议(或称为服务方式)、存有该资源的主机IP地址(有时也包括端口号)以及主机资源的具体地址三部分组成。
  3. 抽象程度:URL是一种URI,是URI的子集。URI以一种更广泛、更抽象的方式来标识资源,而URL则提供了具体的资源访问方法。

8. TCP四大拥塞控制算法

  1. 慢启动(Slow Start):这是TCP拥塞控制算法的起始阶段。当主机刚开始发送报文段时,它会先探测网络的拥塞程度,通过逐渐增大拥塞窗口的大小来避免一开始就发送过多数据导致网络拥塞。具体来说,每收到一个对新报文段的确认后,拥塞窗口会增加至多一个最大报文段MSS的数值。
  2. 拥塞避免(Congestion Avoidance)拥塞窗口大小cwnd大于等于慢启动阈值ssthresh,就进入拥塞避免算法,此算法的目的是让拥塞窗口cwnd(congestion window)缓慢地增大,避免突然增加网络负载而引发拥塞。每经过一个往返时间RTT(Round-Trip Time),发送方的拥塞窗口cwnd就会增加1,而不是加倍。这有助于减少因超时而导致的数据传输延迟,并避免拥塞窗口因超时而重新从1开始增长
  3. 快重传(Fast Retransmit):当发送方连续收到三个重复的ACK(Acknowledgment)报文时,它会意识到数据包已经丢失,并立即重传丢失的数据包,而不需要等待重传计时器超时。这种算法可以显著减少数据包丢失导致的延迟。
  4. 快恢复(Fast Recovery):在快重传发生后,快恢复算法会调整拥塞窗口的大小,并尝试快速恢复数据的正常传输。与慢启动和拥塞避免算法不同,快恢复算法不会将拥塞窗口重置为最小值,而是将其设置为当前窗口的一半,并随后在每次收到新的ACK时增加拥塞窗口的大小

9 HTTP请求方式

HTTP/1.0定义了三种请求方法,分别是:GET、POST和HEAD。而HTTP/1.1在HTTP/1.0的基础上增加了OPTIONS、PUT、DELETE、TRACE和CONNECT方法,因此其请求方法总共有八种。

以下是这些请求方式的简单介绍:

  • GET:用于从服务器获取数据。请求的数据会附加在URL后面,因此这种方式并不安全
  • POST:用于发送包含用户提交数据的请求,有可能对服务器的数据进行更改。POST请求的数据放在HTTP包体中。
  • HEAD:类似于GET请求,但返回的响应中没有具体的内容,主要用于获取报头。
  • PUT:用于向指定资源位置上传其最新内容
  • DELETE:用于删除文件,与PUT方法相反。
  • OPTIONS:用于查询服务器针对特定资源所支持的HTTP请求方法。
  • TRACE:用于回显服务器收到的请求,主要用于测试或诊断。
  • CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器

三次握手、四次挥手

1什么是三次握手⭐⭐⭐⭐⭐

  1. 客户端向服务器发送一个SYN包,并进入SYN_SENT状态,等待服务器确认。
  2. 服务器收到SYN包,确认客户的SYN(ACK=客户序列号+1),同时自己也发送一个SYN包,即SYN+ACK包,此时服务器进入SYN_RECV状态。
  3. 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=服务器序号+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

2为什么三次握手中客户端还要发送一次确认呢?可以二次握手吗?⭐⭐⭐⭐

3为什么服务端易受到SYN攻击?⭐⭐⭐⭐

服务端易受到SYN攻击的原因主要在于SYN攻击利用了TCP协议的三次握手机制。攻击者会伪造大量的IP地址,并向服务器发送SYN包,请求建立连接。由于这些IP地址是伪造的,服务器在收到请求后会回复确认,并等待客户端的确认。然而,由于源地址不存在,服务器无法收到客户端的确认,因此会不断重发确认消息,直到超时

这些伪造的SYN包会长时间占用服务器的未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而造成网络拥塞。服务器在尝试与这些不存在的IP地址建立连接时,会消耗大量的资源和时间,如果攻击者发送的请求数量足够大,服务器的资源将被耗尽,导致正常的服务无法提供,从而达到攻击的目的。

因此,服务端需要采取一系列防御措施来应对SYN攻击,例如限制来自同一IP地址的连接请求频率、使用SYN Cookie技术来验证连接请求的有效性等。这些措施有助于减少服务器受到SYN攻击的风险,保障服务的正常运行。

4什么是四次挥手⭐⭐⭐⭐⭐

  1. 客户端发送一个FIN报文段给服务器,表示客户端不再向服务器发送数据,但是仍然愿意接收数据,然后客户端进入FIN_WAIT_1状态。
  2. 服务器收到FIN报文段后,发送一个ACK报文段给客户端,确认序号为收到序号+1,服务器进入CLOSE_WAIT状态。
  3. 服务器发送一个FIN报文段给客户端,表示服务器也没有数据要发送了,服务器进入LAST_ACK状态。
  4. 客户端收到FIN报文段后,发送一个ACK报文段给服务器,确认后进入TIME_WAIT状态,服务器收到ACK报文段后进入CLOSED状态,连接结束。

5为什么客户端最后还要等待2MSL?⭐⭐⭐⭐

  1. 保证最后的ACK报文段能够到达:在四次挥手的最后阶段,客户端发送一个ACK报文段给服务器以确认收到服务器的FIN报文段。等待2MSL可以确保这个ACK报文段有足够的时间在网络中传输,即使出现网络延迟,也能保证服务器能够收到这个确认信息。
  2. 防止已失效的连接请求报文段出现在新连接中:在网络通信中,可能存在一些由于各种原因(如网络拥堵、延迟等)而未能及时到达的报文段。等待2MSL可以确保这些旧的、可能已经失效的报文段在网络中消失,从而避免它们对新建立的连接造成干扰。
  3. 确保客户端和服务器都进入CLOSED状态:等待2MSL后,客户端可以确保服务器已经收到并处理了它的ACK报文段,此时双方都进入CLOSED状态,可以安全地关闭连接。如果客户端不等待这段时间就直接关闭,可能会出现服务器还在等待客户端的ACK报文段的情况,导致连接状态不一致

6为什么建立连接是三次握手,关闭连接确是四次挥手呢?⭐⭐⭐⭐

首先,我们来看三次握手的原因:

  • 三次握手的主要目的是确认双方的通信能力,确保客户端和服务器都准备好进行数据传输。通过三次握手,客户端和服务器可以交换信息并确认对方的接收和发送能力,从而建立可靠的连接。
  • 三次握手还能避免旧连接的延迟影响。如果只进行两次握手,客户端发送的连接请求可能会因网络延迟而未能及时到达服务器,导致客户端误以为连接已建立,而服务器实际上并未收到请求。通过引入第三次握手,可以确保双方都对连接状态有明确的认知。
  • 此外,三次握手还能防止重复连接的建立。在网络延迟的情况下,如果只有两次握手,客户端可能会因未收到服务器的确认而重复发送连接请求,导致资源浪费和数据混乱。

接下来,我们分析为什么关闭连接需要四次挥手:

  • 四次挥手的主要目的是为了确保数据能够完全传输并释放连接资源。在数据传输完毕后,客户端首先发起关闭请求,服务器收到请求后进行确认,并等待数据传输完成后也发起关闭请求,最后客户端再次确认。
  • 这种四次挥手的机制有助于确保双方都能正确地关闭连接,并释放相关的资源。如果只有三次挥手,可能会出现一方已经关闭了连接,而另一方还在发送数据的情况,导致数据丢失或资源浪费。

STL库相关

1 vector list异同⭐⭐⭐⭐⭐

  1. 存储元素:两者都用于存储一系列的元素。
  2. 动态大小:它们都可以根据需要动态地增长和缩小
  3. 迭代器支持:都支持迭代器(iterator)来访问和操作容器中的元素。

不同之处

  1. 内存布局
    • vector:在内存中连续存储元素。因此,通过索引访问元素非常快(常数时间复杂度)。但是,在 vector 中间插入或删除元素可能需要移动大量元素,这通常是一个较昂贵的操作(线性时间复杂度)。
    • list:通过双向链表实现,每个元素都包含指向其前一个和后一个元素的指针。因此,list 在任何位置插入或删除元素都非常快(常数时间复杂度),但访问特定元素通常比 vector 慢,因为需要从某个已知位置开始遍历链表。
  2. 内存分配
    • vector:当添加新元素并超出当前容量时,vector 通常会分配更大的内存块并将现有元素复制到新位置。这可能导致内存分配和复制的开销
    • list:由于 list 的元素是分散存储的,它不需要像 vector 那样进行整体的内存分配或复制操作。
  3. 缓存效率
    • vector:由于其连续的内存布局,vector 往往能更好地利用 CPU 缓存,这在处理大量数据时很重要。
    • list:由于元素是分散存储的,list 可能不那么缓存友好。
  4. 迭代器稳定性
    • vector:在插入或删除元素时,指向 vector 中元素的迭代器、指针或引用可能会失效,因为元素可能会被移动
    • list:在 list 中插入或删除元素时,指向 list 中其他元素的迭代器、指针或引用仍然有效,因为操作只涉及修改相邻元素的指针。
  5. 空间利用率
    • vector:通常具有更好的空间利用率,因为它只存储元素本身,不需要额外的指针或空间来维护链表结构
    • list每个元素都需要额外的空间来存储指向前一个和后一个元素的指针,这增加了空间开销。

2 vector内存是怎么增长的vector的底层实现⭐⭐⭐⭐

  1. 初始分配:当创建 vector 时,它通常会分配一小块初始内存来存储元素。这个初始大小可能是固定的,也可能是根据某种策略计算得出的。

  2. 容量检查:当向 vector 添加元素(例如使用 push_back)时,vector 会检查其当前容量是否足够。如果当前容量足以容纳新元素,则直接在新位置构造元素。

  3. 重新分配:如果当前容量不足以容纳新元素,vector 会分配一个新的、更大的内存块。新的容量通常是当前容量的两倍(这是一个常见的策略,但并非必须),这样可以在多次添加元素时减少重新分配的次数。然后,vector 会将现有元素复制到新内存块中,并释放旧内存块。

  4. 构造新元素:在新分配的内存块中,vector 在适当的位置构造新元素。

3 vector和deque的比较⭐⭐⭐⭐

  1. 内存布局与访问
    • vector:在内存中连续存储元素,因此可以通过下标快速访问任意元素,时间复杂度为O(1)。由于其连续存储特性,CPU高速缓存命中率较高,访问效率较高。
    • deque由多个固定大小的块组成,这些块在内存中并不一定是连续的。因此,虽然deque也支持随机访问,但由于其内部结构的复杂性,访问速度可能略慢于vectordeque的随机访问需要首先确定元素所在的块,然后再在该块内进行下标访问。
  2. 头部和尾部操作
    • vector:在尾部添加或删除元素时效率较高,因为通常只需要调整一次内存分配。然而,在头部或中间插入或删除元素时,可能需要移动大量元素,因此效率较低
    • deque:在头部和尾部添加或删除元素时都非常高效,因为deque的设计允许在两端进行快速的插入和删除操作。然而,在deque的中间插入或删除元素仍然可能涉及多个块的调整,因此效率不如在两端操作。
  3. 内存分配与扩容
    • vector:当现有容量不足以容纳新元素时,vector会分配一个新的、更大的内存块,并将现有元素复制到新位置。这可能导致在添加大量元素时发生多次内存分配和复制操作,从而影响性能。
    • deque:通过多个固定大小的块来管理内存,当需要更多空间时,只需在适当的位置添加新的块。这种策略避免了像vector那样的大规模内存分配和复制操作。
  4. 使用场景
    • vector:适用于需要频繁访问元素且主要在尾部进行插入和删除操作的情况。例如,软件历史操作记录的存储等。
    • deque:适用于需要在头部和尾部频繁进行插入和删除操作的情况。例如,排队购票系统等。

4为什么stl里面有sort函数list里面还要再定义一个sort⭐⭐⭐

STL(Standard Template Library)中的 sort 函数通常用于对连续内存区域中的元素进行排序,比如 vectorarray 和 deque 等容器。这些容器在内存中是连续存储的,因此可以使用高效的比较和交换算法来排序元素。

然而,list 是一种双向链表结构,它的元素在内存中是分散存储的。这意味着你不能像对连续内存区域那样直接对 list 的元素进行排序。因此,为了对 list 中的元素进行排序,STL 提供了一个专门为 list 设计的 sort 成员函数。

list 的 sort 成员函数使用了一种不同的排序算法,该算法利用了链表的特性。它通常基于归并排序或其他链表友好的排序算法,这些算法在遍历链表时交换元素,而不是像 std::sort 那样直接操作连续的内存区域。

总结一下,STL 中既有通用的 sort 函数,又有 list 专用的 sort 成员函数,是因为不同容器在内存布局和访问方式上存在根本差异,需要不同的排序算法来高效地处理。通用的 sort 函数适用于连续内存区域的容器,而 list 的 sort 成员函数则专为链表结构设计。

5 STL底层数据结构实现⭐⭐⭐⭐

1. vector

  • 底层实现:动态数组。通常使用连续的内存块来存储元素。当需要更多空间时,vector会分配一个新的、更大的内存块,并将现有元素复制到新位置。
  • 特性:随机访问元素快,尾部插入/删除效率高,但中间插入/删除可能效率较低(因为可能需要移动大量元素)。

2. list

  • 底层实现:双向链表。每个元素包含指向其前一个和后一个元素的指针。
  • 特性:在头部和尾部插入/删除元素效率高,但随机访问元素较慢(因为需要从已知位置开始遍历链表)。

3. deque

  • 底层实现:双端队列,通常由多个固定大小的块组成,块之间通过指针连接。这允许在队列的两端都能高效地插入和删除元素。
  • 特性:在头部和尾部插入/删除元素效率高,随机访问元素也较快,但通常不如vector快。

4. set 和 multiset

  • 底层实现:通常基于红黑树实现。红黑树是一种自平衡的二叉搜索树,能够保持树的高度相对较低,从而提供高效的查找、插入和删除操作。
  • 特性:元素自动排序,插入、删除和查找操作的对数时间复杂度。

5. map 和 multimap

  • 底层实现:通常也基于红黑树实现,其中键(key)用于排序,并与值(value)相关联。
  • 特性:按键自动排序,提供高效的查找、插入和删除键值对操作。

6. unordered_setunordered_multisetunordered_map 和 unordered_multimap

  • 底层实现:基于哈希表实现。哈希表使用哈希函数将键映射到存储桶中,从而提供平均情况下的常数时间复杂度查找、插入和删除操作。
  • 特性:不保证元素顺序,但提供高效的查找、插入和删除操作。

7. stack 和 queue(默认deque)

  • 底层实现:通常基于dequelistvector实现,但提供特定的接口以限制对容器内容的访问方式。
  • 特性stack提供后进先出(LIFO)的访问方式,而queue提供先进先出(FIFO)的访问方式。

8. priority_queue(默认vector)

  • 底层实现:通常基于堆(heap)实现,特别是最大堆或最小堆。
  • 特性:提供元素的优先级排序,允许高效地从队列中检索最高(或最低)优先级的元素。

6利用迭代器删除元素会发生什么?⭐⭐⭐⭐

对于vectordeque这样的序列容器,当你使用迭代器指向的元素被删除后,该迭代器会变得无效。这是因为删除操作可能会导致容器中的其他元素被移动以填补被删除元素的位置,从而改变元素的内存布局。如果你继续使用已经变得无效的迭代器,可能会导致未定义的行为,包括程序崩溃。

对于listforward_list这样的链表容器,情况有所不同。删除一个元素只会影响指向该元素及其相邻元素的迭代器。其他迭代器仍然有效。然而,即使在这种情况下,你也应该避免使用已经指向被删除元素的迭代器。

7 map是如何实现的,查找效率是多少⭐⭐⭐⭐⭐

std::map在C++ STL中通常是通过红黑树(Red-Black Tree)来实现的。红黑树是一种自平衡的二叉搜索树,它通过特定的颜色和规则来保持树的平衡,从而确保查找、插入和删除操作的时间复杂度都是对数级别的。

具体来说,红黑树满足以下性质:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色。
  3. 所有叶子节点(NIL或空节点)是黑色。
  4. 如果一个节点是红色的,则它的两个子节点都是黑色。
  5. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。

这些性质保证了树的高度大致是平衡的,从而保证了操作的效率

std::map中,键(key)用于排序,并与值(value)相关联。由于红黑树的性质,std::map查找、插入和删除操作的时间复杂度都是O(log n),其中n是map中元素的数量。

8几种模板插入的时间复杂度 ⭐⭐⭐⭐⭐

  1. std::vector
    • 尾部插入:平均时间复杂度为O(1),但在容量不足导致重新分配内存时,可能会达到O(n)。这是因为vector在尾部插入时,如果当前容量足够,则直接添加;如果不足,则需要分配新的内存块并将旧元素复制到新位置。
    • 中间或头部插入:时间复杂度为O(n),因为需要移动插入位置之后的所有元素
  2. std::deque
    • 头部和尾部插入:时间复杂度为O(1),因为deque是由多个固定大小的块组成的,可以在头尾快速添加元素。
    • 中间插入:时间复杂度为O(n),因为可能需要移动多个块中的元素
  3. std::list 和 std::forward_list
    • 任何位置插入:时间复杂度为O(1),因为链表结构允许在任意位置通过修改指针来插入元素,不需要移动其他元素。
  4. std::map 和 std::multimap(基于红黑树实现)
    • 插入:时间复杂度为O(log n),因为红黑树是一种自平衡的二叉搜索树,插入操作需要保持树的平衡。
  5. std::unordered_map 和 std::unordered_multimap(基于哈希表实现)
    • 插入:平均时间复杂度为O(1),但在哈希冲突较多时可能退化到O(n)。哈希表通过计算键的哈希值来确定元素存储的位置,插入操作通常很快,但在哈希冲突处理上可能会花费额外时间。
  6. std::set 和 std::multiset(基于红黑树实现)
    • 插入:时间复杂度为O(log n),同样是因为红黑树的性质。
  7. std::unordered_set 和 std::unordered_multiset(基于哈希表实现)
    • 插入:平均时间复杂度为O(1),但在哈希冲突较多时可能退化到O(n)。

面试题参考
链接:111道嵌入式面经题全解析软件开发面经C++面经_牛客网
来源:牛客网

  • 11
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值