准备的基础知识 (一)

这里是总结了20年五月份为了实习二准备的一些基础知识,之前的版本比较乱,现在趁着有时间好好整理一下。

内容涵盖:计网 计原 OS 数据结构和算法 Linux基础 C++基础 设计模式等面试常考问题


文章目录

  • 【define、const、typedef、inline的使用方法?他们之间有什么区别?】
  • 【C++中的内存分配方式和new的类型】
  • 【进程线程的状态转换图】
  • 【fork函数】
  • 【define宏】
    • 【define宏定义和const的区别】
    • 【宏定义和函数的区别】
    • 【宏定义和typedef 别名 的区别】
  • 【协程 [进程与线程](https://blog.csdn.net/zhaohong_bo/article/details/89552188)】
  • 【进程 进程并发 上下文切换 用户模式和内核模式】
  • 【进程内存分区】
  • 【C++内存管理机制】
  • 【C++内存分区】
  • 【Linux机制的几种锁】
  • 【死锁相关】
  • 【TCP传输的一些机制】
    • TCP实现可靠传输的机制
    • TCP协议的特点
    • TCP和UDP的区别
  • 【const和static关键字--重点】
  • 【C++强转】
  • 【C++面向对象】
    • 封装,继承,多态
    • 重载、重写
  • 【继承相关】
  • 【菱形继承和虚基类】
  • 【泛型编程之函数模板和类模板】
  • 【C语言与C++的区别】
  • 【数组和指针】
  • 【指针和引用】
  • 【C++11新特性】
  • 【STL】
  • 【vector和set map】
  • 【拷贝 析构 虚函数】
  • 【堆栈】
  • 【虚拟内存】
  • 【内存泄漏与定位】
  • 【避免内存泄露 溢出的方式】
  • 【网络模型相关】
  • 【Linux相关】
    • 基础指令
    • Shell常用指令
    • IO多路复用模型之 select poll epoll
    • Linux网络协议栈
  • 【网络编程和套接字】

基础知识一:


【字节一面】

【define、const、typedef、inline的使用方法?他们之间有什么区别?】

define 关键字 宏定义:发生在预处理阶段,没有类型,不存在类型的检查,只进行文本的替换; 可以用来为类型起别名,还可以用来定义常量、变量、编译开关,还可以用来防止文件重复引用

一、const与#define的区别:

  1. const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
  2. define只在预处理阶段起作用,简单的文本替换;而const在编译、链接过程中起作用;
  3. define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
  4. define预处理后,占用代码段空间,const占用数据段空间;
  5. const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
  6. define独特功能,比如可以用来防止文件重复引用。

二、#define和别名typedef的区别

  1. 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查
  2. 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。
    #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等
  3. 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。
    而typedef有自己的作用域。

三、 define与inline的区别

  1. #define是关键字,inline是函数;
  2. 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
  3. inline函数有类型检查,相比宏定义比较安全;

【C++中的内存分配方式和new的类型】

一、C++中三种内存分配方式:

一.从静态存储区分配内存
从静态存储区分配的内存在 程序编译的时候就已经被分配完毕了,这块内存在程序的整个运行期间都会存在(例如全局变量,static变量)
二.在栈上创建内存空间 : 存放函数的参数值、返回值和一些局部变量
在执行函数时, 函数内局部变量的存储单元可以在栈上创建,函数执行结束的时候,这些内存单元会自动被释放,栈内存分配运算内置于处理器的指令集,效率高,但是 分配的内存容量有限。
三.在堆上分配内存(动态内存分配) :存放一些全局变量
在堆上分配内存亦被称为动态分配内存,程序在运行的时候使用malloc或者new申请 任意大小的内存,程序员自己负责在何时使用free和delete进行动态分配的内存的释放。
动态内存的生命周期是由程序员决定的,而且动态内存的申请和释放的使用过程非常灵活
如果在堆上分配了空间,则必须对堆上分配的内存进行回收,因为系统是无权对堆上的内存进行管理的,若只是申请了动态内存却不对内存进行释放,程序将会出现内存泄漏,且 频繁的分配和释放不同大小的堆空间将产生内存碎片。

二、new的几种类型

plain_new:我们最常用的new,在空间分配失败的情况下会抛出异常;
nothrow_new:在空间分配失败的情况下不会抛出异常而是返回NULL;
placement_new :允许在一块已经分配成功的内存上重新构造对象或对象数组
placement_new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事儿就是调用对象的构造函数;
使用placement_new应注意两点:
1)placement_new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象
2)placement_new构造起来的对象数据,要显式的调用它们的析构函数来销毁;


【进程线程的状态转换图】

准备的基础知识 (一)_第1张图片

准备的基础知识 (一)_第2张图片
父进程调用fork()创建子进程,子进程进入创建态,此时系统为进程分配地址和资源后将进程加入就绪队列,进入就绪态
就绪态的进程得到CPU时间片调度正式运行,进入执行态

执行态有如下4中结果:
1.当时间片耗光,或被其他进程抢占,则重新进入就绪态,等待下一次CPU时间片。
2.由于某些资源暂时不可获得,而进入睡眠态,等待资源可得后再唤醒。唤醒后进入就绪态。
3.收到SIGSTOP/SIGTSTP信号进入暂停态,直到收到SIGCONT信号重新进入就绪态。
4.进程执行结束,通过内核调用do_exit()进入僵尸态,等待系统回收资源。
当父进程调用wait/waitpid()后接受结束子进程,子进程进入死亡态


【fork函数】

在C语言中,fork()函数是用于创建一个新的进程的系统调用。它在调用时会复制当前进程,创建一个新的子进程,并且在父进程和子进程中返回不同的值。
fork()函数的行为如下:
当调用fork()函数时,操作系统会创建一个新的子进程。子进程几乎完全复制了父进程的内容,包括代码、数据、堆栈信息和打开的文件描述符等
在父进程中,fork()函数返回新创建的子进程的进程ID(PID)。该PID大于0,可以通过它来识别父子进程的关系。
在子进程中,创建子进程成功 fork()函数返回0,可以通过这个返回值来区分父子进程。
如果fork()函数调用失败,它会返回一个负值,表示创建子进程失败
经典的fork()函数用法是创建一个子进程来执行不同的任务,例如在网络编程中,父进程负责监听连接,而子进程负责处理具体的客户端请求。
需要注意的是,fork()函数的调用可能会导致进程数的翻倍,因此在使用fork()函数时应格外注意资源的管理和控制,以避免过多的进程导致系统性能下降


【define宏】

【define宏定义和const的区别】

const 定义的是变量而不是常量,只是这个变量的值不允许改变,是个常变量;带有类型,编译运行的时候存在类型检查;
define 宏定义:定义的是不带类型的常数,只进行简单的字符替换,在预处理阶段起作用,不存在类型检查;

区别:
1)编译器处理方式不同
define 宏是在预处理阶段使用;
const常量是编译运行阶段使用;
2)类型和安全检查不同
define 宏没有类型 不做任何的类型检查
const常量带有具体的类型 会进行类型检查
3)存储方式不同
define仅仅是展开,有多少地方使用,就展开多少次,不会分配内存;
const常量会在内存中分配
4)const可以节省空间 避免不必要的内存分配
const定义的常量在程序运行过程中只有一份拷贝,而define定义的常量在内存中有若干拷贝;
5)define 宏替换只作替换,不做计算,不做表达式求解
宏在预处理时就替换了,程序运行时,并不分配内存;


【宏定义和函数的区别】

宏在定义时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快。函数调用在运行时需要跳转到具体的调用函数。
宏定义属于在机构中插入代码,没有返回值。函数调用具有返回值
宏定义参数没有类型,不进行类型检查。函数参数具有类型,需要类型检查。


【宏定义和typedef 别名 的区别】

宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名
宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
宏不进行类型检查;typedef会检查数据类型;
宏不是语句,不在最后加分号;typedef是语句 加分号;


【协程 进程与线程】

协程:协程不是进程,也不是线程,它就是一个可以在某个地方挂起的特殊函数,并且可以重新在挂起处继续运行。
一个进程包含多个线程,一个线程可以包含多个协程。一个线程内的多个协程的运行是串行的,当一个协程运行时,其他协程必须挂起,协程不能利用多核,所以协程不适用于计算密集型的场景,协程适用于Io阻塞型;
协程是编程语言提供的特性,在用户态操作。协程适用于IO密集型的任务

协程用来解决线程较多时带来的问题:
一是系统线程会占用非常多的内存空间;二是过多的线程切换会占用大量的系统时间。
优点
协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程;
而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。

协程 线程和进程关于上下文切换的比较
准备的基础知识 (一)_第3张图片


【进程 进程并发 上下文切换 用户模式和内核模式】

进程计算机中正在执行的程序的实例
在操作系统中,进程是进行资源分配的基本单位。每个进程都有自己的内存空间、执行状态、指令指针和相关的系统资源;

进程并发是说一个进程的指令和另一个进程的指令是交错执行的

上下文context:上下文简单说来就是一个环境,相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。

上下文切换当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换。即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递给新进程,新进程就会从上次停止的地方开始。

所谓“进程上下文”,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

所谓“中断上下文”,就是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上下文,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)


用户模式&&内核模式

为了使操作系用内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
  当设置了模式位时,进程就运行在内核模式中,即超级用户模式。
  一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。
  没有设置模式位时,进程就运行在用户模式中用户模式中的进程不允许执行特权指令,比如停止处理器,改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
  运行应用程序代码的进程初始时是在用户模式中的。
  进程从用户模式变为内核模式的唯一方法是通过诸如中断,故障或者陷入系统调用这样的异常,当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。
  处理程序运行在内核模式中,当他返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。


进程(又称为进程实体):是进程中正在执行程序的实例。由三部分组成:PCB(进程控制块)、程序段和数据段
(一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程)


进程和线程的区别【字节】

线程是程序执行的最小单位,线程作为CPU资源调度和分配的基本单位
进程是资源分配的最小单位,进程作为拥有资源的基本单位;

多进程的程序比多线程的程序健壮每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系
线程不够稳健,一个线程的崩溃可能影响到整个程序的稳定性;
进程切换时需要跨进程边界,耗费资源大,效率也要低一些,而线程切换无需跨进程边界,消耗比较小
线程之间的通信更简洁方便;而进程之间的通信需要以进程通信的方式IPC)进行,比较复杂。
并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

进程的三种基本状态是:就绪态 执行态 阻塞态
进程的特征:动态性 并发性 独立性 异步性


进程通信IPC
共享内存--管道通信--消息队列---信号--信号量---网络套接字socket
注:【线程通信方式】 共享内存--信号量--互斥量--条件变量--事件--队列

1 管道
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
2 消息队列
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
3 信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,常作为一种锁机制,可以用来控制多个进程对共享资源的访问。
信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作
3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4)支持信号量组。
4 信号
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
5 共享内存 share memory
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
共享内存是最快的 IPC 方式它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信
特点:
1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取
2)因为多个进程可以同时操作,所以需要进行同步
3)信号量+共享内存通常结合在一起使用,信号量用来实现对共享内存的同步访问
共享内存的通信原理
在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。不同进程之间共享的通常是一块物理内存
6 套接字SOCKET
socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信


【线程通信方式】
共享内存--信号量--互斥量--条件变量--事件--队列
注:【进程通信IPC共享内存--管道通信--消息队列---信号--信号量---网络套接字socket

在线程编程中,存在多种方式用于线程之间的通信。以下是几种常见的线程通信方式:
共享内存,多个线程共享同一块内存区域,通过在内存中读写数据来进行通信。这需要对共享数据进行同步和互斥操作,以避免竞争条件和数据不一致的问题。
信号量(Semaphore):信号量是一个计数器,用于控制对共享资源的访问。信号量数值的大小用来表示可用资源的数量。线程可以通过等待信号量或释放信号量来进行通信。
互斥量(Mutex):互斥量用于保护共享资源,确保同一时间只有一个线程可以访问该资源。线程在访问共享资源之前需要获取互斥量的锁,访问完毕后释放锁,以保证资源的独占性。
条件变量(Condition):条件变量用于在线程之间进行复杂的协调和通信。它可以让一个线程等待某个条件的发生,而其他线程在满足条件时发送信号通知等待的线程。
事件(Event):事件是一种线程同步的机制,用于线程之间的通信和控制。一个线程可以等待事件的发生,而其他线程可以触发事件。
队列(Queue):队列可以作为线程之间的缓冲区,用于存储需要共享的数据。一个线程可以将数据放入队列,而其他线程可以从队列中取出数据,实现线程之间的数据传递。

这些线程通信方式可以根据具体的应用场景和需求选择合适的方式。在实际的线程编程中,通常会结合多种通信方式来实现复杂的线程交互和协作。


【进程内存分区】

进程的内存分布:代码区--数据区(全局初始化数据区Data段/未初始化数据区Bss段)--栈区--堆区
代码区:加载的是可执行文件的代码段
全局初始化数据区Data段:加载的是可执行文件的数据段
未初始化数据区BSS段:加载的是可执行文件的BSS段
栈区由编译器自动分配释放,存放函数的参数值、返回地址、局部变量值
堆区由程序员手动分配和释放,用于动态内存分配存放一些全局变量

准备的基础知识 (一)_第4张图片


【C++内存管理机制】

C++的内存管理机制包括栈内存管理、堆内存管理、自定义内存管理、智能指针和RAII:

栈内存管理:栈内存由操作系统自动分配和释放,主要是用于函数调用和局部变量的内存区域。
每个函数调用时,栈会为该函数分配一个栈帧栈帧包含了函数的局部变量、函数参数和函数调用的返回地址、EBP(指针寄存器)栈的大小是有限(栈帧的数量也是有限的)的,当函数调用结束时,对应的栈帧会被销毁。

堆内存管理:堆内存的分配和释放需要手动管理,C++中通过动态内存分配操作符(new、new[])来在堆上分配内存,而通过对应的释放操作符(delete、delete[])来释放堆上的内存。堆内存的分配要确保在分配后及时释放,避免出现内存泄漏和空悬指针。

自定义内存管理:C++还允许通过重载operator new和operator delete来自定义内存管理。这可以用于实现自定义的内存分配策略,例如使用对象池或内存池等技术来提高内存分配效率和减少内存碎片。

智能指针:C++提供了智能指针(smart pointer)来帮助管理动态分配的内存。智能指针可以自动进行内存的引用计数,并在引用计数为零时自动释放内存。常用的智能指针有shared_ptr、unique_ptr和weak_ptr。使用智能指针可以减少手动管理内存的工作,可以自动进行内存的释放,并避免内存泄漏和悬空指针等问题。

RAII(资源获取即初始化):RAII是一种C++的编程技术,通过对象的构造函数来获取资源,而通过对象的析构函数来释放资源。RAII可以帮助管理资源的生命周期,包括内存资源和其他资源,如文件句柄、网络连接等。通过使用RAII,可以确保资源的正确分配和释放,避免资源泄漏

总之,合理利用这些机制,可以有效管理内存,提高程序的性能和健壮性。


【C++内存分区】

注:每个区域存放内容是重点

在C++中,内存分成5个区,他们分别是堆,栈,全局/静态存续区,常量存续区,代码区

栈: 栈内存由操作系统自动分配和释放,主要是用于函数调用和局部变量的内存区域。
每个函数调用时,栈会为该函数分配一个栈帧栈帧包含了函数的局部变量、函数参数和函数调用的返回地址、EBP(指针寄存器)栈的大小是有限(栈帧的数量也是有限的)的,当函数调用结束时,对应的栈帧会被销毁。
堆: 进行动态内存分配,就是由 malloc /new 分配的内存块,由程序员控制它的分配和释放,存放一些全局变量;
全局/静态存储区(.bss 段和 .data 段): 存放一些全局变量和静态变量,全局/静态存储区的内存分配和释放是自动进行的。
常量存储区(.data 段): 存放的是常量,不允许修改,程序运行结束自动释放。
代码区(.text 段): 存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。


【Linux机制的几种锁】

互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。


【死锁相关】

死锁的定义 必要条件和处理方法 字节
死锁(Deadlock)是指在多线程编程中,两个或多个线程因互相等待对方释放资源而无法继续执行的情况,导致程序无法继续运行下去

什么时候发生死锁对资源的竞争、进程推进顺序非法、信号量的使用不当、对不可剥夺资源的不合理分配。
具体场景:当线程在获取资源时发现资源已被其他线程占用,它会进入等待状态,等待其他线程释放资源。然而,如果所有的线程都进入了等待状态,并且没有一个线程能够继续执行并释放所占用的资源,那么就会发生死锁。
死锁发生的必要条件互斥条件、 请求和保持条件 、不可抢占条件 、循环等待条件
互斥条件 一个资源每次只能被一个进程使用。
请求和保持条件 一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可剥夺条件 进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
循环等待条件 若干进程之间形成一种头尾相接的循环等待资源关系。


死锁的解决方案:保证上锁的顺序是合理一致的 ;
1、预防死锁(破坏三个必要条件来预防)
破坏请求和保持条件:资源一次性分配,在进程运行期间不允许再提出资源请求;或进程再申请新资源前必须先释放他所占有的资源
破坏不可剥夺的条件:当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

2、避免死锁 :通过合理地分配资源和避免产生死锁的状态,可以预防死锁的发生。
银行家算法 核心思想:在分配资源之前,首先判断这次分配是否会让系统进入不安全状态,以此决定是否答应资源分配的请求 )
3、死锁检测与恢复(Deadlock Detection and Recovery):在系统中引入死锁检测机制,周期性地检测系统资源的状态,判断是否存在死锁。如果检测到死锁的存在,可以通过回滚(Rollback)操作或终止(Abort)相关线程来恢复系统到一个安全的状态
4、引入超时机制(Timeouts):为获取资源设定一个超时时间,在等待超过该时间后,线程放弃等待并释放已占用的资源。这样可以避免因为一直等待而导致的死锁情况

你可能感兴趣的:(数据结构与算法,C++基础,LInux学习,面试,c++,数据结构)