准备的基础知识 (一)

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

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



基础知识一:


【字节一面】

【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构造起来的对象数据,要显式的调用它们的析构函数来销毁;


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

在这里插入图片描述

在这里插入图片描述
父进程调用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密集型的任务

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

协程 线程和进程关于上下文切换的比较
在这里插入图片描述


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

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

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

上下文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段
栈区由编译器自动分配释放,存放函数的参数值、返回地址、局部变量值
堆区由程序员手动分配和释放,用于动态内存分配存放一些全局变量

在这里插入图片描述


【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):为获取资源设定一个超时时间,在等待超过该时间后,线程放弃等待并释放已占用的资源。这样可以避免因为一直等待而导致的死锁情况。
5、调试死锁函数调用栈打印出来
有时候会发现存在先后两段代码都会对同一个mutex进行上锁,这时候就会引发死锁;
解决死锁的办法:对代码里重复操作的函数进行拆分

我遇到的死锁的例子

//摘录于recipes/thread/test/SelfDeadLock.cc
class Request
{
public:
    void process() // __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        //...
        print(); //标记1。为了调试,加入这一个函数
    }
 
    void print() const // __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
    }
 
private:
    mutable muduo::MutexLock mutex_;
};
 
int main()
{
    Request req;
    req.process();
}

在这里插入图片描述
解决上面那个死锁的问题很简单,就是按照上面介绍的拆分函数的方法:
从Request::print()抽取出Request::printWithLockHold()
并让Request::print()和Request::process()都调用Request::printWithLockHold()即可

class Request
{
public:
    void process(){
        muduo::MutexLockGuard lock(mutex_);
        //...
        printWithLockHold(); //替换print
    }
    void print() const{
        muduo::MutexLockGuard lock(mutex_);
        printWithLockHold();
    }
    void printWithLockHold(){
        //将原本print()中完成的功能移动到此处
    }
private:
    mutable muduo::MutexLock mutex_;
};

【TCP传输的一些机制】

重点 必考

TCP三次握手、四次挥手、 可靠传输、 流量控制 、拥塞控制 (百度)【字节】

三次握手:建立连接 ;
四次挥手 :断开连接;

可靠传输】:(校验和 重传 确认 滑动窗口 超时重传)
流量控制】:TCP流量控制的目标是确保发送方和接收方之间的数据传输速率相匹配 :避免发送方过快地发送数据,导致接收方无法及时处理,从而造成数据的拥塞和丢失。通过滑动窗口和ACK延迟确认机制,TCP可以根据网络状况和接收方的处理能力来调整数据传输速率,保证数据的可靠传输。
拥塞控制】:防止过多的数据注入到网络中,以免网络中的路由器或链路过载
出现拥塞的条件:对资源需求的总和大于可用资源
(避免产生网络拥塞的四种算法)拥塞控制的四种算法:慢开始 拥塞避免 快重传 快恢复

图示:
在这里插入图片描述


TCP实现可靠传输的机制

TCP 提供的可靠数据传输服务就是要保证发送方发送的数据和接收方接收的数据是完全一致的
TCP 使用了校验和、序号、确认和重传机制、连接管理(三次握手 四次挥手)、流量控制、拥塞控制来达到这个目的

校验和
在计算校验和时,要在 TCP 数据报之前增加 12 个字节的伪首部,伪首部并不是 TCP 报文段真正的首部。只是在计算校验和时,临时添加在 TCP 数据报文段的前面,得到一个临时的 TCP 报文段。伪首部既不向下传送也不向上递交,而仅仅是为了计算校验和。
注意:IP 数据报的校验和只检验 IP 数据报的首部,但 TCP 的校验和会把首部和数据部分一起检验

序号seq
TCP 首部的序号字段用来保证数据能有序提交给应用层,TCP 把数据看成一个无结构但是有序的字节流,而序号是建立在传送的字节流之上,而不是建立在报文段之上。TCP 连接中传送的数据流中的每一个字节都编上一个序号。序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。

确认号ack
ack = seq + 1;
TCP 首部的确认号是期望收到对方的下一个报文段的数据的第一个字节的序号。TCP 默认使用累计确认,即 TCP 只确认数据流中至第一个丢失字节为止的字节。例如,接收方 B 收到了发送方 A 发送的包含字节 0~2 和 6~7 的报文段。由于某些原因,B 还没有收到字节 3~5 的报文段,此时 B 仍在等待字节 3(和其后面的字节),因此,B 到 A 的下一个报文段将确认号字段设置为 3。

重传
有两种事件会导致 TCP 对报文段进行重传:超时重传和冗余ACK
什么时候超时重传:TCP 每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到但还没有收到确认,就要重传这一报文段
什么时候冗余ACK(快速重传): 发送方通常可在超时事件发生之前通过注意冗余 ACK 来较好地检测丢包情况。发送方在定时时间之前收到多个重复的ACK确认,然后马上进行快速重传。


【TCP流量控制】

TCP 流量控制是为了确保发送方和接收方之间的数据传输速率相匹配。
TCP 提供一种基于滑动窗口协议的流量控制机制。
TCP协议要求发送方维护两个窗口 : 接收窗户rwnd和拥塞窗口cwnd
原理是:
在通信过程中,接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,这就是接收窗口rwnd,可以限制发送方向网络注入报文的速率。
同时,发送方根据其对当前网络拥塞程度的估计而确定窗口值,称为拥塞窗口 cwnd,其大小与网络的带宽和时延密切相关。


【TCP拥塞控制】
所谓的拥塞控制就是为了防止过多的数据注入网络中,以免网络中的路由器或链路过载

(避免产生网络拥塞的四种算法) 拥塞控制算法
慢开始 :由小到大逐渐增大拥塞窗口的数值
拥塞控制 :把拥塞控制为按线性规律增长,使网络比较不容易出现拥塞。拥塞窗口的单位是字节。
快重传发送方每发送一个报文段,就有一个计时器开始计时,在计时到时间之前收到多个重复的ACK确认,就会快速发送重复的报文段
快恢复:快恢复算法是TCP拥塞控制算法中的一种,用于快速恢复TCP连接的速度。快重传之后就会进入快恢复状态,在快恢复状态中,TCP发送方会继续发送数据包,但是窗口大小会以线性增长的方式增加,直到窗口大小达到拥塞窗口大小的一半(因为在快重传之后,拥塞窗口减半)。此时,TCP发送方会重新进入拥塞避免算法,以避免再次出现网络拥塞

拥塞控制与流量控制的区别:
拥塞控制是让网络能够承受现有的网络负荷,它是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素;
流量控制往往使指点对点通信量的控制,即接收端控制发送端,它所做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。


UDP如何实现可靠传输:了解即可
UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
实现确认机制、重传机制、窗口确认机制。
如果你不利用linux协议栈以及上层socket机制,自己通过抓包和发包的方式去实现可靠性传输,那么必须实现如下功能:
发送:包的分片、包确认、包的重发
接收:包的调序、包的序号确认
目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。


TCP协议的特点

重点 TCP传输控制协议 UDP用户数据报协议

可靠传输:TCP 是在不可靠的网络层之上实现的可靠的数据传输协议,它主要解决数据传输的可靠、有序、无丢失和不重复的问题。
TCP 使用了校验和、序号、确认和重传机制、连接管理(三次握手 四次挥手)、流量控制、拥塞控制来实现传输数据的可靠性
协议特点:TCP 是面向连接的、面向字节流的、全双工的,点对点(端对端)的传输层协议。

TCP 报文段:TCP传输的数据单元称为报文段。一个 TCP 报文段分为TCP首部TCP 数据两部分
TCP报文段几个重要的字段含义:
确认位 ACK只有当 ACK = 1 时,确认号字段才有效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。
同步位 SYN:SYN = 1表示这是一个连接请求或连接接收报文。
终止位 FIN:用来释放一个连接。FIN = 1 表明此报文段的发送方的数据已经发送完毕,并要求释放传输连接。
在这里插入图片描述

Linux下查看TCP的连接状态 : netstat -napt

在这里插入图片描述


TCP和UDP的区别

TCP-传输控制协议:提供的是面向连接、可靠的字节流服务,当客户和服务器彼此交换数据前,必须在双方之间建立一个TCP连接,之后才能传输数据
UDP-用户数据包协议:是一个简单的面向数据报的运输层协议。UDP不提供可靠性,他只是把相应的IP层的数据报发送出去,但是并不能保证他们能到达目的地。

TCP 是面向连接的 ;UDP是面向无连接的
TCP是点对点的 ,UDP是多对多连接交互通信
TCP由拥塞控制和流量控制保证数据传输的安全性;UDP无拥塞控制,网络拥塞不影响发送速率。
TCP 是面向字节流的 ; UDP是基于数据报的
TCP 保证数据正确性 ;UDP 可能丢包
TCP首部开销大,20个字节;UDP首部仅有8个字节(源端口、目的端口、数据长度、校验和)
TCP是可靠连接:无差错、无重复、无丢失、按序到达;UDP:尽最大努力交付,不保证可靠性。
TCP是动态报文。即TCP报文长度是根据接受方窗口大小和当前网络拥塞情况来决定的;UDP是面向报文的,不合并、不拆分,保留上面下来的报文边界。
(字节后台)TCP和UDP的使用场景
TCP传输数据可靠但是速度较慢,UDP传输速度快但不可靠
因此在选用具体协议是根据数据通信要求来决定。
若数据通信完整性需要让位与通信实时性,则选用TCP(传文件、重要状态);反之选用UDP(视频、实时通信)。


【重点 必考】

同步位SYN /初始序号seq/连接确认ACK/确认号ack/终止控制位FIN
一般确认号是上次发送信号初始序号的值加一 : ack = seq + 1
SYN包:同步序列编号,是TCP/IP建立连接时的握手信号
ack包:确认字符,表示发送的数据已确认接收无误。

三次握手 : 保证接收双方都有发送和接收数据的能力

在这里插入图片描述

我自己对过程的理解

c端想要和s端建立连接,会向s端发送一个信息:我想要和你建立通信连接(这里具体的信息是同步位SYN=1 初始序号seq=x);
然后s端收到c端的信息后,表示可以进行通信,则需要向c端发送一个可以建立连接的确认信息(这里的具体信息是 同步位SYN=1 连接确认ACK=1 初始序号seq=y 确认序号ack=x+1);
当c端收到s端可以建立通信的信号后,则需要表示已经知道这个确认信息了,然后正式进行数据传输(这里的具体的信息是 连接确认ACK=1 初始序号seq=x+1 确认号ack=y+1)

整个三次握手的过程经历了5个状态的变化:
close -- listen -- syn_sent -- syn--rcvd -- established;


四次挥手
在这里插入图片描述

我自己对这个过程的理解:

1、第一次挥手:c端想要和s端断开连接,第一次挥手是c端告诉s端我要关闭连接了,我不再向你发送信息了。具体的字段信息是FIN=1 ACK=1 seq=u ack=v
2、第二次挥手:s端在收到c端断开连接的请求后,表示可以断开连接,需要向c端发送一个确认断开的信号。具体的字段信息是ACK=1 seq=v ack=u+1 注意这个时候是处于半关闭状态的,即c端不能再向s端发送数据,但是s端可以向c端发送数据;
3、第三次挥手:s端向c端发送信号,表示也想断开连接了。具体的字段信息是 FIN=1 ACK=1 seq=w ack=u+1
4、第四次挥手:c端接收到s端要断开连接的信号后,确认可以断开连接。具体的字段信息是 ACK=1 seq=u+1 ack=w+1.
至此 连接完全断开;

半关闭状态的含义是:
通信的一端可以发送 FIN 报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送 FIN 报文段以关闭连接。


(1) 为什么四次挥手发送最后一次报文后要等待 2MSL(报文最大生存时间)的时间?【重点】

1) 为了保证 A 发送的最后一个确认报文段能够到达 B。如果 A 不等待 2MSL,若当A发送的最后确认报文段丢失,则B不会进入正常关闭状态,因为 A 此时已经关闭,不可能再收到B的重传报文了。
2) 防止出现 "已失效的连接请求报文段延误到达的现象"。A 在发送完最后一个确认报文段后,再经过 2MSL 可保证在连接持续的时间内所产生的所有报文段从网络中消失

(2) TCP 使用的是 GBN(后退N帧协议)还是 SR(选择重传协议)?
因为 TCP 使用累计确认,看起来像是 GBN。但是,正确收到但失序的报文并不会被丢弃,而是缓存起来,并且发送冗余 ACK 指明希望收到的下一个报文段,这是 TCP 方式和 GBN 的显著区别。因此,TCP 的差错恢复机制可以看成是 GBN 和 SR 协议的混合体。

(3) 为什么超时时间发生时 cwnd 被置为 1,而收到 3 个冗余 ACK 时 cwnd 只是减半?
答:首先应分析哪种情况的网络拥塞程度更严重。其实不难发现,在收到 3 个冗余 ACK 的情况下,网络虽然拥塞,但至少 ACK 报文段能够被正确交付。而当超时发生时,说明网络可能已经拥塞的连 ACK 报文段都传输不了了,发送方只能等待超时后重传数据。因此,超时时间发生时,网络拥塞更严重,所以发送方应该最大限度地抑制数据发送量,所以 cwnd 置为 1;收到 3 个冗余 ACK 时,网络拥塞相对而言不是很严重,所以 cwnd 减半即可。

4) 为什么不采用 "两次握手" 建立连接?【重点】
答:这 主要是为了防止两次握手情况下已失效的连接请求报文段突然又传送到服务端,而产生了错误 。考虑以下情况:客户 A 向服务器 B 发送 TCP 连接请求,第一个连接请求报文在网络的某个结点长时间滞留,A 超时后认为报文丢失,于是再重传一次连接请求,B 收到后建立连接。数据传输完毕后双方断开连接。此时,前一个滞留在网络中的连接请求到达了服务端 B,若采用的是 “两次握手”,则这种情况下 B 认为传输连接已经建立,并一直等待 A 传输数据,而 A 此时并无连接请求,因此不予理睬,这样就造成了 B 的资源白白浪费了。

(5) 是否 TCP 和 UDP 都需要计算往返时间 RTT?
答:往返时间 RTT 只是针对传输层 TCP 协议才很重要,因为 TCP 要根据 RTT 的值来设置超时计时器的超时时间。UDP 没有确认和重传机制,因此 RTT 对 UDP 没有什么意义。

(6) 为什么 TCP 在建立连接的时候不能每次选择相同的、固定的初始序号?
答:1) 假如 A 和 B 频繁地建立连接,传送一些 TCP 报文段后再释放连接,然后又不断的建立新的连接、传送报文段和释放连接。
2) 假如每一次建立连接时,主机 A 都选择相同的、固定的初始序号,如 1。
3) 若主机 A 发送出的某些 TCP 报文段在网络中会滞留较长的时间,以致造成主机 A 超时重传这些 TCP 报文段。
4) 若有一些在网络中滞留时间较长的 TCP 报文段最后终于到达了主机 B,但这时传送该报文段的那个连接早已释放了,而在到达主机 B 时的 TCP 连接是一条新的 TCP 连接。
以上这些情况可能会导致在新的 TCP 连接中的主机 B 有可能会接收在旧的连接传送的、已经没有意义的、过时的 TCP 报文段(因为这个 TCP 报文段的序号有可能正好处于新的连接所使用的序号范围内)。因为必须使得迟到的 TCP 报文段的序号不在新的连接中使用的序号范围内。所以,TCP 在建立新的连接时所选择的初始序号一定要和前面的一些连接所使用过的序号不一样。因此,不同的 TCP 连接不能使用相同的初始序号。

(7) 在使用 TCP 传输数据时,如果有一个确认报文段丢失了,也不一定会引起与该确认报文段对应的数据的重传。试说明理由。
答:这是因为发送方可能还未重传时,就收到了更高序号的确认。例如主机 A 连续发送两个报文段,均正确到达主机 B。B 连续发送两个确认 ACK1 和 ACK2(ACK2 的序号比 ACK1 的序号高)。但前一个确认帧在传输时丢失了。若在超时前,ACK2 被 A 接收,更高的序号代表该序号之前的所有字节都被接收了,所以 A 知道前一个报文也被正确的接收了,这种情况下 A 不会重传第一个报文段。


同一进程的线程之间共享的资源:进程代码段 进程的公有数据 堆(内存空间) 进程打开的文件描述符 全局变量 静态变量
独享的资源有 每个线程都有自己的线程栈 寄存器


【const和static关键字–重点】

static:

1、不考虑类的情况
(局部)静态变量
在函数内部声明的静态变量称为局部静态变量(静态存储区)它们在函数第一次执行时进行初始化(只进行这一次初始化),但在函数调用结束后并不被销毁,而是保留其值直到程序结束。局部静态变量仅在声明它们的函数内部可见,但其生命周期跨越多次函数调用。

2、考虑类的情况:

在类的成员变量和成员函数前加上关键字static 称为静态成员,可以分为静态成员变量和静态成员函数。静态成员变量和静态成员函数都是属于类的, 而不是属于对象的。

静态成员
不管这个类创建了多少个对象,静态成员在内存中只保留一份,静态成员用来解决同一个类中不同对象之间数据成员和函数的共享问题
1)静态成员变量(静态存储区)
在类中声明的静态成员变量是与类本身相关联的变量,而不是与类对象相关联。静态成员变量必须在类的内部声明,在类的外部进行定义和初始化,并且可以通过类名和作用域解析运算符::进行访问。**
2)静态成员函数
静态成员函数是与类本身相关联而不是与类对象函数。它们没有隐含的this指针,并且只能访问静态成员变量和其他静态成员函数,不能访问非静态成员变量和非静态成员函数。静态成员函数可以通过类名和作用域解析运算符::进行调用


const:

1、不考虑类的情况:
1)const变量在定义时必须初始化(列表初始化) 之后无法更改;
2) const形参可以接收const和非const类型的实参
2、考虑类的情况
1)const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明初始化。
2)const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用


【C++强转】

C++中强制类型转换的方式

reinterpret_cast<T*> (expression)
dynamic_cast<T*> (expression)
static_cast<T*>  (expression)
const_cast<T*>  (expression)	

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast用于const变量与非const互转,编译时执行,不是运行时执行。
2、static_cast用来执行常见的类型转换,例如基本数据类型之间的转换。它在编译时进行类型检查,但无法提供运行时的类型检查。使用静态转换时,程序员需要确保转换是安全和合理的。
3、dynamic_cast用于在继承关系中进行安全的类型转换。 例如将一个指向基类的指针转换成指向派生类的指针。如果失败返回空值。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换;
4、reinterpret_cast 与C类似的强制类型转换。
重新解释转换用于将一个类型的指针或引用重新解释为另一个类型,通常是不兼容的类型。 它提供了最低级别的类型转换,可能会导致未定义行为。因此,在使用重新解释转换时,需要非常谨慎,并确保转换是安全的。


【C++面向对象】

【C++面向对象的三大特征】

封装,继承,多态

封装是为了模块化(重用),提高可扩展性
继承是为了代码扩展和代码复用
多态是为了接口重用

【面向对象】
面向对象是一种以对象为中心的编程思想
面向对象是把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象,创建了对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为。
【面向过程】
面向过程是一种以事件为中心的编程思想
面向过程就是分析出实现需求所需要的步骤,通过函数一步一步实现这些步骤,接着依次调用即可。


C++面向对象三大特性

封装在面向对象的编程语言中,对象是封装的最基本单位面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个“模块”中,也就是一个类中,属性用变量定义,行为用方法进行定义,方法可以直接访问同一个对象中的属性


继承:
面向对象里的继承也就是父类的相关的属性(变量和方法),可以被子类重复使用,子类不必再在自己的类里面重新定义一回,父类里有的我们只要拿过来用就好了
继承是子类自动共享父类数据和方法的机制。


多态一种通过基类指针或引用来调用派生类对象的方法。子类对父类里的对象或函数进行重写,使父类对象表现出多种形式的现象;
1)多态通过继承和虚函数机制实现
当一个类通过继承另一个类时,它可以继承基类的方法和成员变量。而当基类中的方法被声明为虚函数时,派生类可以对其进行重写,实现特定于派生类的行为。
2)基类指针和引用
多态性的关键是通过基类指针或引用来访问派生类对象。基类指针或引用可以指向派生类对象,并调用相应的方法。
3 )C++的多态可以分为静态多态和动态多态
函数重载和运算符重载实现的多态属于静态多态,而通过重写虚函数可以实现动态多态。也可以把多态分为重载和重写。
重写(覆盖) 一般发生在父类和子类中,在子类里面把从父类里继承来的方法(这里的方法具体指虚函数)重新实现一遍,这样,父类里相同的方法就被覆盖了
重载 在同一个类中,存在多个同名函数,但是这些函数的形参不同,可以是形参类型不同或者形参个数不同,或者形参顺序不同,但是不能使返回值类型不同;
4)多态是运行时确定
C++ 动态多态意味着父类通过父类指针调用成员函数时,不确定是调用父类的虚函数还是子类的虚函数,而是在程序运行时进行动态的判断。也就是说,如果父类指针指向的是一个基类对象,则基类的虚函数被调用,如果父类指针指向的是一个派生类对象,则派生类的虚函数被调用。
这种同样的调用语句在实际运行时有多种不同的表现形态的机制就叫作“多态
5)静态多态与动态多态的实质区别就是函数地址是早绑定(静态多态)还是晚绑定(动态多态)
实现函数的动态联编其本质核心则是虚表指针与虚函数表

6) 实现多态的三个条件
要有继承,要有虚函数,要有父类指针指向子类对象


重载、重写

重载overload:在同一个类中,函数名相同,参数列表不同,编译器会根据这些函数的不同参数列表,将同名的函数名称做修饰,从而生成一些不同名称的预处理函数,未体现多态。
重写override:也叫覆盖,子类重新定义父类中有相同名称相同参数的虚函数,主要是在继承关系中出现的,被重写的函数必须是virtual的,重写函数的访问修饰符可以不同,尽管virtual是private的,子类中重写函数改为public,protected也可以,体现了多态。
重载和重写的区别:
重写和重载主要的区别
范围的区别:被重写和重写的函数在两个类中,重载的函数在一个类中
参数的区别:重写与被重写的函数参数列表相同,而被重载函数和重载函数的参数列表不同。
virtual 的区别:重写的基类中被重写的函数必须要有vitual修饰,而重载函数和被重载函数可以被vitual修饰,也可以没有。


【继承相关】

继承权限
public继承:

公有继承的特点是 基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类访问;

protected继承
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数和友元函数访问,基类的私有成员仍然是私有的;
private继承
私有继承的特点是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类的所访问,基类的成员只能由自己派生类访问,无法向下继承;

在这里插入图片描述


【菱形继承和虚基类】

1、菱形继承
在多重继承中,存在一个很特殊的继承方式,即菱形继承。
**比如一个类C通过继承类A和类B,但是类A和类B又同时继承于公共基类N。
菱形继承问题描述: 如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。 类C同时继承类A和类B,导致类C中继承的类N中的数据不确定是通过类A还是通过类B继承过来的,产生二义性,造成内存浪费
示意图如下:
在这里插入图片描述
2、虚基类
虚继承&&虚基类 - 【class 派生类名 : virtual public 基类名】
因此为了解决上述菱形继承带来的问题,C++中引入了虚基类,其作用是在间接继承共同基类时只保留一份基类成员,虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。
C++编译系统只执行最后的派生类对基类的构造函数调用,而忽略其他派生类对虚基类的构造函数调用。从而避免对基类数据成员重复初始化。因此,虚基类只会构造一次。


【泛型编程之函数模板和类模板】

函数模板是一种特殊的函数,可以使用不同的类型进行调用,对于功能相同的函数,不需要重复编写代码,并且函数模板与普通函数看起来很类似,区别就是类型可以被参数化;
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述


函数模板是泛型编程的一种方式,函数模板通过templatetypename两个关键字来定义,如下

在这里插入图片描述


深入理解函数模板

  • 对于函数模板中使用的类型不同,编译器会产生不同的函数
    • 编译器会对函数模板进行两次编译
      • 第一次是对函数模板本身进行编译,包括语法检查等
      • 第二次是对参数替换后的代码进行编译,这就相当于编译普通函数一样,进行类型规则检查等。

需要注意的是
- 函数模板是不允许发生隐式类型转换的,调用时类型必须严格匹配

函数模板还可以定义任意多个不同的类型参数,但是对于多参数函数模板:
- 编译器是无法自动推导返回值类型的
- 可以从左向右部分指定类型参数

引入一段代码:

#include<iostream>
using namespace std; 

template<typename T1,typename T2,typename T3>

T1 add(T2 a, T3 b)
{
	T1 ret;
	ret = static_cast<T1>(a + b);
	return ret;

}
void main()
{
	int c = 12;
	float d = 23.4;
	//cout << add(c, d) << endl;//报错 因为无法自动推导函数返回值
	cout << add<float>(c, d) << endl;//返回值在第一个类型参数中被指定
	cout << add<int, int, float>(c, d)<< endl;
	system("pause");
	//return 0;

}

在上边的代码中,我们定义了多类型参数的函数模板,调用时需要注意的是函数返回值需要在第一个参数类型中显式指定,后边的类型可自动推导或显示指定。

函数模板跟普通函数一样,也可以被重载
- C++编译器优先考虑普通函数
- 如果函数模板可以产生一个更好的匹配,那么就选择函数模板
- 也可以通过空模板实参列表<>限定编译器只匹配函数模板

关于函数模板的总结

- 函数模板是泛型编程在C++中的应用方式之一
- 函数模板能够根据实参对参数类型进行推导
- 函数模板支持显式的指定参数类型
- 函数模板是C++中重要的代码复用方式
- 函数模板通过具体类型产生不同的函数
- 函数模板可以定义任意多个不同的类型参数
- 函数模板中的返回值类型必须显式指定
- 函数模板可以像普通函数一样重载

类模板语法
在这里插入图片描述在这里插入图片描述在这里插入图片描述


【C语言与C++的区别】

对象导向编程:C++是一种面向对象的编程语言,支持类、继承、多态等面向对象的概念,而C语言是一种过程式编程语言,不直接支持面向对象编程。
标准库功能:C++标准库提供了许多丰富的功能和容器,如字符串类、容器类(如向量、链表、映射等)、输入输出流等。而C标准库功能相对较少,主要包括输入输出、字符串处理和内存管理等基本功能。
内存管理:在C++中,可以使用自动变量、动态内存分配和析构函数等来管理内存。而C语言中主要使用手动内存管理,例如malloc和free函数。
异常处理:C++提供了异常处理机制,允许在程序出现异常时捕获和处理异常。C语言没有内置的异常处理机制,错误通常通过返回特定的错误代码来处理。
兼容性:C++是C的超集,也就是说,合法的C程序也是合法的C++程序。C++可以调用C语言编写的函数,但C不能直接调用C++的函数。
3.具体语言区别:
C++相比于C,增加了很多功能,比如范围for循环,lamda表达式,auto自动类型推导,move移动、强制类型转换、智能指针,列表初始化、内联函数、函数重载、函数模板和类模板等等
函数参数默认值,C++支持,C不支持,而且C++给函数参数赋初始值必须从右开始
const: C中的const叫只读变量,只是无法做左值的变量;C++中的const是真正的常量,但也有可能退化成c语言的常量,默认生成local符号。
引用:C++支持引用。引用底层就是指针,使用时会直接解引用,可以配合const对一个立即数进行引用。
C++既有自己的成员变量和成员方法,C只有成员变量,没有成员方法。

作用域
C语言中作用域只有两个:局部,全局。
C++中则是有:全局作用域、局部作用域,类作用域,名字空间作用域三种。

总的来说,C++相比于C具有更多的特性和功能,尤其是面向对象编程的支持。然而,C在一些嵌入式系统、底层编程和需要更高的性能和内存控制的场景下仍然非常有用。


C语言的宏函数define和C++的内联函数inline
1、宏函数和内联函数的作用(相同):减少函数调用的开销
2、宏函数
特点:不存在栈帧的开辟
不存在参数的带入
不存在返回值带出
不存在参数的清除
不存在类型检查
不可以调试
3.内联函数
形式:在函数定义中把限定符inline放在函数的返回类型前面。


【数组和指针】

【请回答一下数组和指针的区别】
1、概念:
数组 是存储多个相同类型数据的集合
指针 指针相当于一个变量,但是它和变量不同,它存放的是变量在内存空间中的位置;
2、赋值 存储方式 求sizeof 初始化等方面的区别
1)赋值:同类型指针变量可以相互赋值,数组不行,只能一个个元素的赋值或复制;
2)存储方式
数组
:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组下标进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的;
数组存储方式:数组存储空间,不是在静态区就是在栈上;

指针:指针很灵活,可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
指针存储方式指针的存储空间不能确定,由于指针本身就是一个变量,再加上它所存放的也是变量,因此存储空间不确定;

3)求sizeof
数组:sizeof(数组名):求数组所占存储空间的内存或是数组的大小
指针:32位下 sizeof(指针名)=4 ;64位下 sizeof(指针名)=8
4)访问效率
数组通过下标直接访问,虽然访问效率高于指针,但灵活性很差;
指针通过地址找到指针所指的对象,再访问对象的内存单元,属于间接访问,访问效率低于数组,但是灵活性高;
5)安全性
数组安全性高于指针
数组可能存在的问题(数组访问越界
指针可能出现的问题(野指针 空悬指针 造成内存泄露


【指针和引用】

【请说一下C/C++ 中指针和引用的区别?】 重点

1)指针:指针是一个变量,用于存储另一个对象的内存地址。通过指针可以间接访问数据(指针–地址–数据); 指针的定义需要使用*符号,并在初始化时指定所指向的对象的地址。
引用:引用是一个别名,用于引用另一个对象,只是一个别名而已,本质上和原对象是同一个东西。 引用的定义需要使用&符号,并在初始化时绑定到所引用的对象。

(2)重点 引用的值不可以为空,当被创建的时候,必须初始化,并且初始化之后就不能更改其绑定的对象;
而指针可以是空值,可以在任何时候被初始化,指针的值在初始化之后是可以重新指向不同的对象的

(3)可以有const指针,但是没有const引用;
(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(5)”sizeof 引用”得到的是所引用对象的大小,而”sizeof 指针”得到的是指针本身的大小;
(6)指针和引用的自增(++)运算意义不一样;
(7)指针通常用于需要动态内存分配、数组操作、迭代器等场景,以及在函数参数中传递指针来修改调用者的变量。
引用常用于传递函数参数


引用为什么必须初始化【重点】
在定义引用时,程序把引用和它的初始值绑定在一起,而不是把初始值拷贝给引用。一旦初始化完成,引用将和它的初始值一直绑定在一起,无法令引用重新绑定另一个对象,故引用必须初始化!


【请你来说一下一个C++源文件从文本到可执行文件经历的过程?】
对于C++源文件,从文本到可执行文件一般需要四个过程:
源文件--预处理--编译--汇编--链接目标文件--生成可执行文件
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换、去掉注释,生成预编译文件。(替换宏定义的变量发生在预处理阶段)
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库链接生成最终的可执行目标文件


【C++11新特性】

1、 新增基于范围的for循环
2、 自动类型推导 auto 、decltype类型指示符
(auto 自动推导类型、 decltype的作用是让编译器自动推导出一个表达式的类型,并且不会执行该表达式;选择并返回操作数的数据类型)
3、 std::function & std::bind & lambda(匿名函数表达式
4、 后置返回类型(tailng-return-type)
5、 虚函数中override和final指示符
final,表示派生类不应当重写这个虚函数;override,表示函数应当重写基类中的虚函数,否则编译报错
6、 空指针常量 nullptr
c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针
7、 long long int类型
8、 模板改进 - 函数模板和类模板(模板的右尖括号、模板的别名、函数模板的默认模板参数)
9、 允许sizeof运算符可以在类型数据成员上使用,无需明确对象。
10、 线程支持
11、 元组类型
12、 右值引用&&重要性质:只能绑定到一个将要销毁的对象上;因此可以自由的将一个右值引用的资源移动到另一个对象中;
Move对象移动:可以移动并非拷贝对象的能力;
13、 列表初始化(直接在变量名后面加上初始化列表来进行对象的初始化)
14、 关于并发,引进了很多
15、 引入了智能指针(shared_ptr weak_ptr unique_ptr)
weak_ptr就是为了解决shared_ptr引起的循环引用问题而设计的
两个对象互相持有指向对方的shared_ptr,导致两个shared_ptr的引用计数都为2,当一个对象离开其作用域时,他们的引用计数都是1,永远不会为0,导致对象永远不会被析构掉,造成内存泄漏。

16、 Default:多数时候用于声明构造函数为默认构造函数
17、 Explicit:专用于修饰构造函数,表示只能显式构造,不可以被隐式转换
18、 正则表达式:c++11引入了regex库更好的支持正则表达式
19、 新增一些算法:
all_of:检测表达式是否对范围[first, last)中所有元素都返回true,如果都满足,则返回true;
any_of:检测表达式是否对范围[first, last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样
none_of:检测表达式是否对范围[first, last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样
find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反
copy_if:复制满足条件的元素
itoa:对容器内的元素按序递增
20 constexpr变量:将变量声明为constexpr类型以便由编译器来验证变量的值是否为一个常量表达式

【请问C++11有哪些新特性】
auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
初始化列表:使用初始化列表来对类进行初始化
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
atomic原子操作用于多线程资源互斥操作
新增STL容器array以及tuple
左值和右值
lamda 表达式
移动构造


【STL】

STL (标准模板库) 六大组件【重点】

STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
算法:各种常用的算法,如sort、find、copy、for_each等
迭代器:扮演了容器与算法之间的胶合剂,通过迭代器来遍历访问容器中的数据
仿函数:行为类似函数,可作为算法的某种策略。
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
空间配置器:负责空间的配置与管理。

它们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数

for_each
for-each是增强for循环在遍历数组/集合过程中不能修改数组中某元素的值。
for-each仅适用于遍历,不涉及有关索引(下标)的操作。
for-each核心依然是迭代器


【vector和set map】

【set集合和map映射】
在这里插入图片描述
在这里插入图片描述


【vector和list的区别】 重点
1) 内部原理实现:
vector:vector是一个动态数组由于使用连续的内存块存储元素,vector允许通过索引对数组元素进行快速随机访问
并提供了动态增长和缩小容量的能力。
list list是一个双向链表,它使用指针将元素连接在一起。每个元素包含一个值和指向前一个和后一个元素的指针。
list不支持通过索引进行随机访问,因为它需要遍历链表来找到指定位置的元素

2) 插入和删除操作:
vector在vector中,由于其内存地址是连续的,因此插入或删除元素可能导致内存的重新分配和复制。在末尾进行插入或删除操作效率较高,但在中间或开头进行操作效率较低。
list:list对于任意位置的插入和删除操作非常高效,无需进行内存复制和重新分配。无论在哪个位置插入或删除元素,它都只需要修改链表中指针的指向即可。
3) 内存占用:
vector:vector的内存占用比较高,因为它需要额外的内存来存储指向连续内存块的指针,以及用于动态扩展容量的缓冲区。
list:list的内存占用比较低,因为它只需要为每个元素分配内存,并存储前后指针。
4) 迭代器稳定性:
vector:vector的迭代器在插入或删除元素后可能失效,因为内存重分配可能导致迭代器指向无效的位置。
list:list的迭代器在插入或删除元素后仍然有效,因为链表结构保持不变,内存位置不发生变化。
根据具体的需求,选择适合的容器类是很重要的。
如果需要频繁的随机访问和动态调整容量,可以选择vector。如果需要频繁的插入和删除操作,并且对随机访问的需求较少,可以选择list。

【百度】Vector扩容实现原理:
(1)配置一块新空间
(2)将旧元素一一搬往新址
(3)把原来的空间释放还给系统


【const】

const的应用场景:

  1. 用于定义常量,const修饰的变量在定义后不可更改,因此const对象必须被初始化;
  2. 指针也可以使用const;
    【顶层const , 表示指针本身是个常量 ; 底层const,表示指针所指的对象是个常量;】
  3. 函数参数中使用const,一般在传递类对象时会传递一个const的引用或者指针,这样可以避免对对象的拷贝,也可以防止对象被修改
  4. const修饰类的成员变量,表示是成员常量,不能被修改,可以在初始化列表中被赋值。
  5. const修饰类成员函数,表示在该函数内不可以修改该类的成员变量。
  6. const修饰类对象,类对象只能调用该对象的const成员函数。

【拷贝 析构 虚函数】

【基础】定义一个空类里面也有六大默认成员函数
默认构造函数
拷贝构造函数
析构函数
赋值运算符重载operator==
取地址操作符重载
被const修饰的取地址操作符重载


【拷贝构造函数】
1)定义: 拷贝构造函数是一个特殊的构造函数,用于创建一个新的对象,并将已经存在的对象作为参数进行传递,使新的对象具有与原对象相同的属性和值。
2)性质:函数名和类名相同 func(const &func),它的唯一的一个参数(对象的引用)是不可变的(因为是const型的)
3)使用:什么时候会调用拷贝构造函数
1.一个对象以值传递的方式传入函数体
2.一个对象通过另一个对象初始化
3.一个对象以值传递方式从函数返回
4)为什么拷贝构造函数必须使用引用传递?
如果拷贝构造函数以值传递的方式进行传参,那么给形参传入的是实参的一份临时拷贝,拷贝时需要调用拷贝构造函数,那么此时会导致递归调用,栈溢出

【什么是赋值构造函数】

一个类中赋值运算符=的重载方法即就是赋值构造函数。
当用户使用内置数据类型时,使用赋值构造函数可以进行顺利的赋值运算操作。


【深浅拷贝】
默认的拷贝构造函数是浅拷贝

浅拷贝
浅拷贝概念 : 复制原对象时,增加一个指针,但是还是指向之前就存在的那块内存,复制前后的对象指向的是同一块内存地址
浅拷贝存在的问题: 因此当对复制之后的对象进行操作时,原对象也会发生相应的操作;当多个对象指向同一块内存空间时,释放一个空间会导致其他对象所使用的空间也被释放。再次释放其他对象时会产生错误。
深拷贝
深拷贝概念:复制原对象时,不仅增加了一个指针,并开辟了一块新的内存空间让指针指向新开辟的这块内存空间,复制前后指针指向的地址是不一样的,因此当对复制之后的对象进行操作时,原对象不会发生相应的变化;

如何区分深拷贝与浅拷贝,简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝;如果B没变,那就是深拷贝。


【请你来说一下C++中析构函数的作用】
析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数,释放对象内存,防止内存泄漏。
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数


引用为什么必须初始化
在定义引用时,一旦初始化完成,引用将和它的初始值一直绑定在一起,因为无法令引用重新绑定另一个对象,故引用必须初始化!


【请你回答构造函数和析构函数是否能声明为虚函数?为什么C++默认的析构函数不是虚函数 、构造函数不能声明为虚函数】
(构造函数的作用是提供初始化,实例化对象。在对象生命期仅仅运行一次)
1. 为什么构造函数不能为虚函数?
虚函数的调用需要虚函数表指针,而该指针存储在对象的内存空间中的;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有可用的虚函数表指针用来调用虚函数——构造函数了,因此不能把构造函数声明为虚函数
2. 为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题?
在实现多态时,把基类的析构函数设计为虚函数可以在基类的指针指向派生类对象时,用基类的指针删除派生类对象,避免内存泄漏,防止只析构基类而不析构派生类的情况发生。
3.析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需要考虑的因素吗?
析构函数不能也不应该抛出异常;构造函数可以抛出异常
原因:
1.如果析构函数抛出异常,那么异常点之后的程序将不被执行,那么异常点之后的程序如果涉及到某些必要的操作或释放某些资源,则这些动作将不会执行,会造成资源泄露的问题;
2.异常发生时,c++机制会调用已构造对象的析构函数来释放资源,此时若析构函数本身抛出异常,则前一个异常未处理,又有新的异常,将导致程序崩溃。


C++ 中哪些不能是虚函数?
1)普通函数只能重载,不能被重写,因此编译器会在编译时绑定函数。
2)构造函数是知道全部信息才能创建对象,然而虚函数允许只知道部分信息。
3)内联函数在编译时被展开,虚函数在运行时才能动态绑定函数。(虚函数可以是内联函数 但是当虚函数表现出多态性时不能内联)
4)友元函数 因为不可以被继承。
5)静态成员函数 只有一个实体,不能被继承。 父类和子类共有。


【请你来说一下C++中类成员的访问权限】
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员


【堆栈】

【堆、栈的区别 (百度)】 堆栈详解和函数调用相关的知识点

1、 管理方式不同
程序在运行时栈由操作系统自动管理,由编译器给程序分配空间。
而堆空间的申请、释放都是由程序员控制,通过malloc(new) / free(delete)进行操作,容易产生内存泄漏。

malloc / free 、 new / delete 的区别
1)、malloc与free是C语言的标准函数,new/delete是C++的运算符
2)、都可用于申请动态内存和释放内存,malloc在分配内存前需要用户指定内存大小,new可以自动计算出所需内存的大小。
new和malloc开辟内存的位置不同。 malloc开辟在堆区,new开辟在自由存储区域
3)、new/delete比malloc/free更加智能,其实底层也是执行的malloc/free。
为啥说new/delete更加的智能?因为new和delete在对象创建的时候自动执行构造函数,对象消亡之前会自动执行析构函数

4)、new返回指定类型的指针,不需要类型转换,是类型安全的。malloc默认返回类型为void*,必须强行转换为实际类型的指针。

new的实现方式
new会调用operator new的标准库函数进行动态内存分配,然后调用相关对象的构造函数进行初始化构造对象,最后返回指向新分配并构造后对象的指针。在C++中,operator new是用于动态分配内存的关键字。
delete的实现方式
delete对指针所指对象运行适当的析构函数,再通过调用operator delete的标准库函数释放该对象的内存。


2、 空间大小不同
栈是向低地址扩展的,是一块连续的内存空间。
堆是向高地址扩展的,是一块不连续的内存空间。对堆而言,频繁的申请释放空间势必会造成内存空间的不连续,从而造成大量碎片,使效率降低。

3、 增长方向不同
栈的增长方向是向下的,即向着内存地址减小的方向。
堆的增长方向是向上的,即向着内存地址增大的方向。

4、 分配方式不同
堆是由程序员手动分配和释放的,主要进行动态内存分配。 由malloc()函数 / new动态申请分配并由free()函数 / delete释放。
栈的分配和释放是由操作系统完成的。 栈既有动态分配,也有静态分配。静态分配是由编译器完成的(如局部变量的分配)栈的动态分配由alloca()函数实现。
栈中存放一些函数的参数值 返回值和一些局部变量等。
堆中存放一些全局变量

5、 分配效率不同
栈的效率比堆高。栈比堆快一些
栈由系统提供的,操作系统会在底层对栈提供支持:分配专门的寄存器来存放栈的地址,压栈出栈都有专门的指令执行。
堆则是由C函数库提供的,机制很复杂。在分配堆内存的时候需要一定的算法寻找合适的内存大小,并且堆内存的访问需要两次,第一次范文指针,第二次访问指针指向的对象。


补充:什么是栈溢出

栈的大小是有限的,由编译器或操作系统决定。
栈溢出(Stack Overflow)指的是当程序执行时,栈空间不足以容纳新增的栈帧(stack frame)时发生的情况。 栈帧是用来存储函数调用信息和局部变量的一块内存区域,每当有函数调用发生时,就会在栈上创建一个新的栈帧。

当递归函数层级过深、函数内部局部变量过多或数据结构较大的情况下,不仅栈帧的数量会增加,而且每个栈帧所需的空间会逐渐增加,直到超过栈的容量限制,就会发生栈溢出。

栈溢出通常会导致程序异常终止,并可能引发一些不可预料的错误行为,如崩溃、段错误、无限循环等。在某些情况下,栈溢出还可能造成系统的不稳定或安全漏洞。


【虚拟内存】

【虚拟内存与分页 分段 和页表

  • 虚拟内存是一种计算机内存管理技术,它允许程序访问超过物理内存容量的内存数量。
    它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,内存通常被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要的时候进行数据交换。
    为了解决上述问题,现代操作系统就是采用虚拟内存的方式使程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,以此达到内存地址空间隔离的效果;

  • 虚拟内存的好处
    1、使用更大的内存空间:虚拟内存技术使得程序可以使用比实际物理内存更大的空间,从而使程序可以使用更大的数据结构和更复杂的算法。
    2、程序间互相独立(进程地址空间隔离):虚拟内存技术允许不同的程序使用相同的虚拟地址,但实际使用的物理地址不同,从而保证程序间的独立性。


映射机制的解决—虚拟地址和物理地址的一一映射-----页表
页表是一种特殊的数据结构,存放着各个虚拟页的状态,是否映射,是否缓存.。
进程要知道哪些内存地址上的数据在物理内存上,哪些不在,还有在物理内存上的哪里,这就需要用页表来记录。
页表的每一个表项分为两部分,第一部分记录此页是否在物理内存上,第二部分记录在物理内存页的地址(如果在的话)。当进程访问某个虚拟地址,就会先去看页表,如果发现对应的数据不在物理内存中,则发生缺页异常。
在这里插入图片描述


在这里插入图片描述
两个进程 都有相同的变量a
A进程中: int a=1 B进程中:int a=2
并且它们的地址是一样的,但是输出的值却是不一样的;
因为:它们的地址实际上是虚拟地址,通过MMU(分页内存管理单元) 将两个进程的变量映射到不同的物理地址上,从而输出的值是不一样的。从而实现了进程隔离问题;


【内存泄漏与定位】

参考资料
内存泄漏 【字节】
一般我们常说的内存泄漏是指堆内存的泄漏

  • C++内存泄漏是指程序在分配内存后没有释放,导致程序持续占用内存,最终导致系统崩溃。
    内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序

  • 常见的内存泄漏问题和解决方案

    1、忘记释放动态分配的内存:当使用 new、malloc 或者 calloc 等函数动态分配内存时,必须使用 delete、free 或者 realloc 函数释放内存。如果忘记释放内存,就会导致内存泄漏问题。解决方案是在代码中添加必要的 delete、free 或者 realloc 函数,确保内存被正确释放。
    2、指针赋值问题:如果在动态分配内存的时候使用了指针,并且在程序执行过程中将该指针赋值给其他指针或变量,而没有释放原指针指向的对象内存,就可能导致内存泄漏问题。解决方案是在赋值前先释放该指针所指向的内存,或者使用智能指针等辅助工具来避免这种问题。
    3、循环引用问题(shard_ptr和weak_ptr):如果程序中存在循环引用的对象,在对象离开其作用域后引用计数永远不为0,导致对象无法正常析构。解决方案是使用智能指针等辅助工具来管理内存,确保循环引用的对象能够正确释放。
    4、重载操作符问题:如果在重载操作符时,没有正确释放内存,就会导致内存泄漏问题。解决方案是在操作符重载函数中添加必要的内存释放操作。

  • 避免内存泄露的几种方式
    1、 使用智能指针(使用引用计数的方法自动析构对象内存空间):使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
    2、一定要将基类的析构函数声明为虚函数,防止只析构基类不析构派生类的情况
    3、对象数组的释放一定要用delete []
    4、有new就有delete,有malloc就有free,保证它们一定 成对出现
    5、检测工具 Linux下可以使用Valgrind工具 Windows下可以使用CRT库


【避免内存泄露 溢出的方式】

参考资料

尽早释放无用对象的引用。
程序进行字符串处理时,尽量避免使用String,而应使用StringBuffer。因为每一个String对象都会独立占用内存一块区域
尽量少用静态变量。因为静态变量是全局的,GC不会回收。
避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作。
尽量运用对象池(内存池 线程池)技术以提高系统性能。----核心是减少new/delete的次数
可以适当的使用hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃


【网络模型相关】

在这里插入图片描述

请回答OSI七层模型和TCP/IP四层模型,每层列举2个协议

在这里插入图片描述


2. 五层网络模型 (OSI参考模型)
( 1 )应用层:负责给应用程序提供统一的接口,为网络用户或应用程序提供服务。 应用层协议有很多,如FTP、SMTP、HTTP、DNS(域名解析协议)、DHCP协议(使用UDP传输消息)
( 2) 传输层 :负责端到端的数据传输;这一层中的协议有面向连接的 TCP (传输控制协议)、无连接的 UDP (用户数据报协议);数据传输的单位称为报文段或用户数据报
( 3 )网络层主要作用是发送数据时,将传输层中的报文段或用户数据报封装成 IP 数据报,并进行数据的路由、转发和分片。
这一层中的主要协议有,IP ICMP(因特网控制报文协议)
( 4) 数据链路层:负责将网络层的 IP 数据报组装成帧,还会进行差错检测和MAC寻址。
主要包括的协议为ARP-地址解析协议 、RARP-逆地址解析协议 ; 这里规定最大传输单元 MTU = 1500字节
( 5)物理层:进行透明地传输比特流。


TCP/IP四层网络模型(TCP/IP协议族)
在这里插入图片描述

在这里插入图片描述


TCP头部 实习考到了 要记住!!!!
TCP报文分为TCP头部和数据两部分
在这里插入图片描述
首先,源端口号和目标端口号 是不可少的,如果没有这两个端口号,数据就不知道应该发给哪个应用。

接下来有 数据包的序号(seq), 这个是为了解决数据包乱序的问题

还有应该有的是 确认号(ack),目的是确认发出去对方是否有收到。如果没有收到就应该重新发送,直到送达,这个是为了解决丢包的问题

接下来还有一些 状态位 (SYN 、ACK、FIN、RST)。例如 SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等。TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。

还有一个重要的就是 窗口大小 (接收窗口cwnd和拥塞窗口rwnd)。TCP 要做流量控制,通信双方各声明一个窗口(缓存大小),标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我


一个数据从应用层 -- 传输层 -- 网络层 -- 网络接口层依次加上HTTP报文、TCP头部、IP头部、MAC头部,变成以下报文格式,此时就是一个完整的需要发送的网络包的形式

网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。因此,我们需要将数字信息转换为电信号,才能在网线上传输,也就是说,这才是真正的数据发送过程。
负责执行这一操作的是网卡,要控制网卡还需要靠网卡驱动程序
数据链路层-将IP数据包组装成帧网卡驱动获取网络包之后,会将其复制到网卡内的缓存区中,接着会在其开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列。

在这里插入图片描述


应用层数据在每一层的封装格式:
在这里插入图片描述


一个完成的数据要经过以下路径才能顺利从客户端到达服务器端:

数据 – HTTP – DNS – 协议栈 – TCP – IP-- MAC – 网卡 -- 交换机 -- 路由器

数据在客户端和服务器端的传输过程:

在这里插入图片描述


搜索baidu,会用到计算机网络中的什么层?每层是干什么的

浏览器中输入URL,首先会解析URL(由服务器名称和数据文件路径名组成),确定Web服务器和文件名

1、浏览器要将URL解析为IP地址,解析域名就要用到DNS协议(应用层)
(DNS域名解析:本地缓存–操作系统缓存–本地域名服务器–根域名服务器)
DNS服务器是基于UDP的,因此会用到UDP协议。DNS是在应用层的;

2、得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议(应用层),http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层(传输层)处理,所以还会用到TCP协议。如果采用https还会使用https协议先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。
3、TCP的数据包然后会发送给IP层(网络层),用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。

其中
1、DNS协议,http协议,https协议属于应用层
应用层是体系结构中的最高层。应用层确定进程之间通信的性质以满足用户的需要。应用层直接为用户的应用进程提供服务
2、TCP/UDP属于传输层
传输层的任务就是负责主机中两个进程之间的通信。因特网的传输层可使用两种不同协议:即面向连接的传输控制协议TCP,和无连接的用户数据报协议UDP。面向连接的服务能够提供可靠的交付,但无连接服务则不保证提供可靠的交付,它只是“尽最大努力交付”。这两种服务方式都很有用,备有其优缺点。在分组交换网内的各个交换结点机都没有传输层。
3、IP协议,ARP协议属于网络层
网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将运输层产生的报文段或用户数据报封装成IP数据报并选择合适的路由。使源主机运输层所传下来的分组能够交付到目的主机。
4、数据链路层
当发送数据时,数据链路层的任务是将在网络层交下来的IP数据报组装成帧,在两个相邻结点间的链路上传送以帧为单位的数据。
5、物理层
物理层的任务就是透明地传送比特流。在物理层上所传数据的单位是比特。传递信息所利用的一些物理媒体,如双绞线、同轴电缆、光缆等,并不在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第0层。


【Linux相关】

基础指令

如何创建文件夹和文件
Mkdir 文件夹名
Touch 文件名
例如test文件夹下创建文件main.cpp
Mkdir test
Cd test/ 进入到该文件夹下 cd … 返回上一层目录
Touch main.cpp

Linux下编译运行一段c程序
Gcc test.c –o test
编译运行一段C++程序
G++ hello.C –o hello –Who-deprecated


Linux如何查看进程占用多少内存、cpu、跑了多少时间,命令。
查看一个进程打开了哪些文件:lsof
【linux查看当前有哪些网络处于监听状态用什么命令】
1,netstat -an
2,lsof –i
【linux下查看负载的主要命令有下面一些】
top, uptime,w,vmstat
【linux查看系统用了哪些信号量用什么命令?】
ipcs –a
linux查看系统目前分配了哪些共享内存用什么命令
ipcs –mp
CPU使用情况查看】
top 命令
内存使用情况查看】
free 命令
通过 free –m/free -g 命令查看系统的内存使用情况
磁盘空间大小查看(du命令用于显示目录或文件的大小)
du命令
pmap命令用于报告进程的内存映射关系


Shell常用指令

目录信息查看命令ls
ls -a 显示目录所有文件及文件夹,包括隐藏文件,比如以.开头的
ls test1/查看指定路径
【目录切换命令cd】
当前路径显示命令pwd
【系统信息查看命令uname】
【清理屏幕命令clear】
显示文件内容命令cat
【切换用户身份命令sudo】
【文件拷贝命令cp】
【切换用户命令su】
普通用户下:sudo su
root用户下:su qjy 切换回来~
【移动文件命令mv】(重命名也用这个)
使用mv a.c b.c:就将a.c改成了b.c
使用mv test/ test1/:就将test改成了test1(修改了文件夹的名字)
使用mv a.c test1/:将文件移动到文件夹下(ls test1/查看文件)
创建文件夹命令mkdir
创建文件命令touch
【删除命令rm】
使用rm -r:删除目录(不包括子文件,会有提示是否删除子文件)
使用rm -rf:删除整个目录(包括子文件不提示)
14、目录删除命令rmdir
显示网络配置信息命令ifconfig
16、重启命令reboot
17、关机命令poweroff
18、系统帮助命令man
man printf
数据同步写入磁盘命令sync
正常情况下数据先写入缓冲区,再到磁盘,为了延长磁盘寿命
【查找文件命令find】
常用find -name a.c
查找内容命令grep
-r是包含子目录 -n是在文件的位置 -i不区分大小写
例如: grep –i “被查找的字符串” 文件名
文件夹大小查看命令du
常用 -sh 显示我们常见的大小方式
磁盘空间检查命令df
24、使用gedit打开某个文件命令gedit
是个类似记事本的软件
gedit a.c
自动打开软件进行编辑
当前的系统进程查看命令ps
常用ps -au
26、进程实时运行状态 查看命令top
类似win下的任务管理器
27、文件类型查看命令file
查看一个文件是什么类型/ 版本,比如是不是arm系统能用的


IO多路复用模型之 select poll epoll

select poll epoll三者详解 (【重点】百度、字节跳动)网络编程基础

select具体的实现机制

select,poll,epoll都是IO多路复用的机制。所谓IO多路复用机制,就是说通过一种机制,可以监视多个文件描述符的状态变化,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

select- 传统的IO多路复用模型,epoll - 事件驱动的IO多路复用模型,

select 机制原理概述:
select 的核心功能是调用tcp文件系统的poll函数,不停的查询,如果没有想要的数据,主动执行一次调度(防止一直占用cpu),直到有一个连接有想要的消息为止。从这里可以看出select的执行方式基本就是不停的调用poll,直到有需要的消息为止。


【重点考察缺点】
1、每次调用select,都需要把fd集合从用户态拷贝到内核态,同时也要遍历传进来的所有fd,这个开销在fd很多时会很大;
每次调用 select 都需要将进程加入到所有监视 socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,有一定的开销。
2、select模型的主要限制是文件描述符集合的大小有限,通常由文件描述符的最大值限定。
此外,select模型在处理大量文件描述符时的性能较差,因为每次调用select函数时都需要遍历整个文件描述符集合。

【优点】
1、select的可移植性更好,在某些Unix系统上不支持poll()。
2、select对于超时值提供了更好的精度:微秒,而poll是毫秒。

在这里插入图片描述
【Select 的实现思路】很直接
假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。(第一次遍历)
当任何一个Socket 收到数据后,中断程序将唤起进程A;唤起后将进程A从所有等待队列中移除,再加入到工作队列中;(第二次遍历)
经由这些步骤,当进程A被唤醒后,它至少知道有一个Socket接收了数据;程序只需遍历一遍Socket列表,就可以得到就绪的Socket;


poll
原理概述
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义;
2、与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
优点:
1、poll() 不要求开发者计算最大文件描述符加一的大小。
2、poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
3、它没有最大连接数的限制,原因是它是基于链表来存储的。


epoll详解
epoll(事件驱动的I/O多路复用)是Linux特有的一种高效的IO多路复用模型。它相比于传统的select和poll模型,在处理大量文件描述符时具有更高的性能
优点:epoll模型相比于select和poll模型的优势在于,它使用了事件驱动的方式,只在有事件发生时才会唤醒进程,避免了遍历整个文件描述符集合带来的性能问题。此外,Epoll模型还提供了一些更高级的功能,如边缘触发(ET)模式和水平触发(LT)模式,以及支持更大的文件描述符数量等。
实现原理
epoll维护一个就绪列表,每当一个socket接收到数据之后,就将其加入到这个列表中。当我们需要的时候,直接去就绪列表中去拿这个就绪的socket就可以了,这样避免了对socket一个个的遍历去寻找哪个socket是就绪的,减少了遍历次数,提高效率。select之所以低效,是因为它不知道哪个socket是接收到数据的,它需要一个个的去遍历查找。

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。【重点】


Epoll的设计思路
Epoll相对select 改进的措施
措施一:功能分离
select低效的原因是将维护等待队列和阻塞进程两个步骤合二为一
在这里插入图片描述
如上图所示:每次调用Select都需要这两步操作,而epoll将这两个操作分开,先用epoll_ctl维护两个等待队列,再调用epoll_wait阻塞进程,这样效率就得到了提升;

Epoll的代码实现思路:
如下的代码中
1、先用 epoll_create 创建一个 Epoll 对象 Epfd
2、再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中
3、最后调用 epoll_wait 等待数据:

int s = socket(AF_INET, SOCK_STREAM, 0);    
bind(s, ...)  //分配套接字和文件描述符
listen(s, ...) //监听套接字
 
int epfd = epoll_create(...); //创建一个epoll对象 
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中 
 
while(1){ 
    int n = epoll_wait(...) //阻塞 等待数据的到来
    for(接收到数据的socket){ 
        //处理 
    } 
} 

措施二 就绪列表
Select 低效的原因在于程序不知道那些Socket收到数据,只能一个个遍历,如果内核维护一个就绪列表,这个表存储收到数据的Socket,就能避免遍历
这个就序列表的工作:
当某个socket收到数据后,中断程序就会让这个就绪列表Rdlist引用这个socket,当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。


Linux网络协议栈

从图中的的网络协议栈,你可以看到:

应用程序需要通过系统调用,来跟 socket 层进行数据交互;
socket 层的下面就是传输层、网络层和网络接口层;
最下面的一层,则是网卡驱动程序和硬件网卡设备;

在这里插入图片描述


【网络编程和套接字】

  • 网络编程:编写程序使两台联网的计算机相互交换数据

  • 套接字(socket)
    套接字(socket)是计算机网络编程中的一种通信机制,它提供了一种标准的接口,使得应用程序能够通过网络进行通信。套接字是一种抽象层,它隐藏了底层网络通信的细节,使得应用程序可以独立于网络协议和硬件设备而进行通信。
    套接字是通过一个 IP 地址和一个端口号来标识网络上的一个进程 。IP 地址用于标识网络中的主机,而端口号则用于标识主机上的一个进程。当一个进程需要和另一个进程进行通信时,它首先需要创建一个套接字,并指定目标主机的 IP 地址和端口号。然后,它就可以通过套接字向目标进程发送数据,或从目标进程接收数据。
    套接字可以使用不同的协议进行通信,例如 TCP 和 UDP。

  • TCP 初始化过程
    1、调用socket建立一个socket描述符
    2、然后填充sockaddr结构
    3、调用bind绑定sockaddr结构和socket描述符
    4、调用listen监听
    5、调用accept 建立连接

  • 字节序与网络字节序
    1)大端序(big endian):高位字节序放到低位地址 0x1234
    2)小端序(little endian):高位字节序放到高位地址 0x3412
    在这里插入图片描述

在这里插入图片描述

我们在进行网络通讯传输数据时约定了一种统一的方式:统一为大端序! 这种约定就叫:网络字节序
我们在网络传输之前先把数据数组转变成大端序格式,再进行网络传输,因此所有计算机接收数据是应识别该数据是网络字节序格式!(其实也就是大端序格式)


  • TCP客户端的默认函数调用顺序

在这里插入图片描述

  • TCP服务器端的默认函数调用顺序
    在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mr.liang呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值