C++重点

C++基础

1 C和C++有什么区别?

  • C++是面向对象,C面向过程
  • C++引入new/delete运算符,取代了C中的malloc/free库函数;
  • C++有引用的概念,C没有
  • C++有类的概念,C没有
  • C++有函数重载,C没有

2 a和&a有什么区别?

比如int a[10]; int (*p)[10] = &a

  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
  • &a是数组的地址,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

3 static关键字

  • 修饰局部变量时,使得该变量在静态存储区分配内存;只能在首次函数调用中进行首次初始化,之后的函数调用不再进行初始化;其生命周期与程序相同,但其作用域为局部作用域,并不能一直被访问;
  • 修饰全局变量时,使得该变量在静态存储区分配内存;在声明该变量的整个文件中都是可见的,而在文件外是不可见的;
  • 修饰函数时,在声明该函数的整个文件中都是可见的,而在文件外是不可见的,从而可以在多人协作时避免同名的函数冲突;
  • 修饰成员变量时,所有的对象都只维持一份拷贝,可以实现不同对象间的数据共享;不需要实例化对象即可访问;不能在类内部初始化,一般在类外部初始化,并且初始化时不加static;
  • 修饰成员函数时,该函数不接受this指针,只能访问类的静态成员;不需要实例化对象即可访问。

4 #define和const有什么区别?

  • 编译器处理方式不同:#define宏是在预处理阶段展开,不能对宏定义进行调试,而const常量是在编译阶段使用;
  • 类型和安全检查不同:#define宏没有类型,不做任何类型检查,仅仅是代码展开,可能产生边际效应等错误,而const常量有具体类型,在编译阶段会执行类型检查;
  • 存储方式不同:#define宏仅仅是代码展开,在多个地方进行字符串替换,不会分配内存,存储于程序的代码段中,而const常量会分配内存,但只维持一份拷贝,存储于程序的数据段中。
  • 定义域不同:#define宏不受定义域限制,而const常量只在定义域内有效。

5 静态链接和动态链接有什么区别?

  • 静态链接是在编译链接时直接将需要的执行代码拷贝到调用处;
    优点在于程序在发布时不需要依赖库,可以独立执行,缺点在于程序的体积会相对较大,而且如果静态库更新之后,所有可执行文件需要重新链接;
  • 动态链接是在编译时不直接拷贝执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定代码时,在共享执行内存中寻找已经加载的动态库可执行代码,实现运行时链接;
    优点在于多个程序可以共享同一个动态库,节省资源;
    缺点在于由于运行时加载,可能影响程序的前期执行性能。

9 sizeof 和strlen 的区别

  • sizeof是一个操作符,strlen是库函数。
  • sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串作参数。
  • 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
  • 数组做sizeof的参数不退化,传递给strlen就退化为指针了

13 全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?

  • 全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放);
  • 而局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。
  • 操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。

15 什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?

智能指针是一个RAII类模型,用于动态分配内存,其设计思想是将基本类型指针封装为(模板)类对象指针,并在离开作用域时调用析构函数,使用delete删除指针所指向的内存空间。

智能指针的作用是,能够处理内存泄漏问题和空悬指针问题。

分为auto_ptr、unique_ptr、shared_ptr和weak_ptr四种,各自的特点:

  • 对于auto_ptr,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象;但auto_ptr在C++11中被摒弃,其主要问题在于:

    • 对象所有权的转移,比如在函数传参过程中,对象所有权不会返还,从而存在潜在的内存崩溃问题;
    • 不能指向数组,也不能作为STL容器的成员。
  • 对于unique_ptr,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象,因为无法进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值;

  • 对于shared_ptr,实现共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象及相关资源会在其所指对象不再使用之后,自动释放与对象相关的资源;

  • 对于weak_ptr,解决shared_ptr相互引用时,两个指针的引用计数永远不会下降为0,从而导致死锁问题。而weak_ptr是对对象的一种弱引用,可以绑定到shared_ptr,但不会增加对象的引用计数。

16 shared_ptr是如何实现的?

  • 构造函数中计数初始化为1;
  • 拷贝构造函数中计数值加1;
  • 赋值运算符中,左边的对象引用计数减1,右边的对象引用计数加1;
  • 析构函数中引用计数减1;
  • 在赋值运算符和析构函数中,如果减1后为0,则调用delete释放对象。

18 悬挂指针与野指针有什么区别?

  • 悬挂指针:当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针;
  • 野指针:未初始化的指针被称为野指针。

19 指针和引用的区别

  • 指针有自己的一块空间,而引用只是一个别名;
  • 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  • 可以有const指针,但是没有const引用;(具体解释看评论区)
  • 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
  • 指针可以有多级指针(**p),而引用止于一级;
  • 指针和引用使用++运算符的意义不一样;
  • 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

21 简述队列和栈的异同

队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是 “后进先出”。

「注意」:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

22 结构体struct和类class的区别

struct默认访问权限为public,class默认访问权限为private

23 简述指针常量与常量指针的区别

  • 指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
  • 指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

/*指针常量*/
int a = 0;
int *const b = &a;
/*常量指针*/
const int *p;
int const *p;

30 面向对象的三大特征是哪些?

  • 封装:将客观事物封装成抽象的类,而类可以把自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏。
  • 继承:可以使用现有类的所有功能,并且无需重新编写原来的类即可对功能进行拓展;
  • 多态:一个类实例的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。

31 C++中类成员的访问权限

  • public:公有,类内外都可以访问
  • protected:保护,仅类内可访问
  • private:私有,仅雷内可访问

32 多态的实现有哪几种?

  • 静态多态:通过重载和模板技术实现的,在编译期间确定
  • 动态多态:通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。

35 纯虚函数有什么作用?如何实现?

定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须覆盖该函数。

实现方式是在虚函数声明的结尾加上= 0即可。

37 为什么基类的构造函数不能定义为虚函数?

虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。

38 为什么基类的析构函数需要定义为虚函数?

为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据,所以必须将析构函数定义为虚函数,从而在对象销毁时,调用派生类的析构函数,从而销毁派生类对象中的所有数据。

如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象。
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。

41 多继承存在什么问题?如何消除多继承中的二义性?

  1. 增加程序的复杂度,使得程序的编写和维护比较困难,容易出错;
  2. 在继承时,基类之间或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性;

消除同名二义性的方法:
利用作用域运算符::,用于限定派生类使用的是哪个基类的成员;
在派生类中定义同名成员,覆盖基类中的相关成员;

  1. 当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类的成员时,将产生另一种不确定性,即路径二义性;

消除路径二义性的方法:
消除同名二义性的两种方法都可以;
使用虚继承,使得不同路径继承来的同名成员在内存中只有一份拷贝。

43 重载和重写之间有什么区别?

  • 范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。

  • 参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。

  • virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。

45 对虚函数和多态的理解

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。

虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

52 基类和派生类的构造和析构顺序

继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

即 :父构造 -> 子构造-> 子析构 -> 父析构

53 深拷贝与浅拷贝区别,浅拷贝可能会带来的问题

  • 浅拷贝:简单的赋值拷贝操作

  • 深拷贝:在堆区重新申请空间,进行拷贝操作

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

54 当类中类成员时的构造和析构顺序

当类中成员是其他类对象时,我们称该成员为 对象成员

  1. 对象成员的构造
  2. 本类的构造
  3. 本类的析构
  4. 对象成员的析构

55 简述类的this指针,以及可能的使用场景

  • 在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。函数不占对象空间,所有函数共享一个函数实例

  • this指针概念

    • c++通过提供特殊的对象指针,this指针。解决代码是如何区分那个对象调用自己的问题。this指针指向被调用的成员函数所属的对象,this指向的是对象。
  • this指针的用途

    • 当形参和成员变量同名时,可用this指针来区分
    • 在类的非静态成员函数中返回对象本身,可使用return *this

59 菱形继承是什么,有什么危害,怎么避免

  • 菱形继承概念:
    当两个派生类继承同一个基类,又有某个类同时继承者两个派生类时这种继承被称为菱形继承,或者钻石继承

  • 菱形继承产生问题:是共同的子类继承两份相同的数据,导致资源浪费以及毫无意义

  • 解决方法:利用虚继承可以解决菱形继承问题

60 抽象类的定义和特点

virtual void func() = 0; //纯虚函数
当类中有了纯虚函数,这个类也称为抽象类,如上述代码中的Base类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

62 std::vector 相关

  • 底层原理:

vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。

当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间

  • vector中的reserve和resize的区别

    • vector的reserve增加了vector的capacity,但是它的size没有改变!而resize改变了vector的capacity同时也增加了它的size
    • reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。加入新的元素时,要调用push_back()/insert()函数。
    • resize是改变容器的大小,且在创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。此时再调用push_back()函数,是加在这个新的空间后面的。
    • 两个函数的参数形式也有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。

63 std::list相关

  • list的底层原理
    list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。

list不支持随机存取,适合需要大量的插入和删除,而不关心随即存取的应用场景。

  • list与vector的区别
    • list不支持随机存取;
    • 在list的任何位置执行插入和移除都非常快.插入和删除动作不影响指向其它元素的指针,引用,迭代器,不会造成失效;
    • list不支持随机存取,不提供下标操作符和at()函数;
    • list没有提供容量,空间重新分配等操作函数,每个元素都有自己的内存;
    • list也提供了特殊成员函数,专门用于移动元素

64 std::deque相关

  • 底层原理:
    deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。

  • std::vector,std::list和std::deque的使用场景
    vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
    list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
    需要从首尾两端进行插入或删除操作的时候需要选择deque。

65 std::map、std::set、std::multiset、multimap相关

  • 底层原理
    map 、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。

    红黑树的特性:
    (1)每个结点或是红色或是黑色;
    (2)根结点是黑色;
    (3)每个叶结点是黑的(并且都为NULL);
    (4)如果一个结点是红的,则它的两个儿子均是黑色;
    (5)每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。

    • 为什么要红黑树?
      普通的二叉搜索树在极端情况下可退化成链表,此时的增删查效率都会比较低下。

    • 红黑树能保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍

    对于STL里的map容器,count方法与find方法,都可以用来判断一个key是否出现,mp.count(key) > 0统计的是key出现的次数,因此只能为0/1,而mp.find(key) != mp.end()则表示key存在。

66 std::unordered_map、unordered_set

  • 底层原理:
    unordered_map的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。

  • unordered_map 与map的区别?使用场景?

    • 构造函数:unordered_map 需要hash函数,等于函数;map只需要比较函数(小于函数)。
    • 存储结构:unordered_map 采用hash表存储,map一般采用红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。
    • 总体来说,unordered_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时,如果考虑效率,特别是在元素达到一定数量级时,考虑unordered_map 。但若对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,unordered_map 可能会让你陷入尴尬,特别是当unordered_map 对象特别多时,你就更无法控制了,而且unordered_map 的构造速度较慢。

73 内存泄漏的场景有哪些?

  • malloc和free未成对出现;new/new []和delete/delete []未成对出现;
    • 在堆中创建对象分配内存,但未显式释放内存;比如,通过局部分配的内存,未在调用者函数体内释放;
    • 在构造函数中动态分配内存,但未在析构函数中正确释放内存;
  • 没有将基类的析构函数定义为虚函数。
  • 未定义拷贝构造函数或未重载赋值运算符,从而造成两次释放相同内存的做法;比如,类中包含指针成员变量,在未定义拷贝构造函数或未重载赋值运算符的情况下,编译器会调用默认的拷贝构造函数或赋值运算符,以逐个成员拷贝的方式来复制指针成员变量,使得两个对象包含指向同一内存空间的指针,那么在释放第一个对象时,析构函数释放该指针指向的内存空间,在释放第二个对象时,析构函数就会释放同一内存空间,这样的行为是错误的;

74 堆和栈有什么区别?

  • 分配和管理方式不同:
    • 堆是动态分配的,其空间的分配和释放都由程序员控制;
    • 栈是由编译器自动管理的,其分配方式有两种:静态分配由编译器完成,比如局部变量的分配;动态分配由alloca()函数进行分配,但是会由编译器释放;
  • 产生碎片不同:
    • 对堆来说,频繁使用new/delete或者malloc/free会造成内存空间的不连续,产生大量碎片,是程序效率降低;
    • 对栈来说,不存在碎片问题,因为栈具有先进后出的特性;
  • 生长方向不同:
    • 堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长;
    • 栈是向着内存地址减小的方向增长的,从内存的高地址向低地址方向增长;
  • 申请大小限制不同:
    • 栈顶和栈底是预设好的,大小固定;
    • 堆是不连续的内存区域,其大小可以灵活调整

75 静态内存分配和动态内存分配有什么区别?

  • 静态内存分配是在编译时期完成的,不占用CPU资源;动态内存分配是在运行时期完成的,分配和释放需要占用CPU资源;
  • 静态内存分配是在栈上分配的;动态内存分配是在堆上分配的;
  • 静态内存分配不需要指针或引用类型的支持;动态内存分配需要;
  • 静态内存分配是按计划分配的,在编译前确定内存块的大小;动态内存分配是按需要分配的;
  • 静态内存分配是把内存的控制权交给了编译器;动态内存分配是把内存的控制权给了程序员;
  • 静态内存分配的运行效率比动态内存分配高,动态内存分配不当可能造成内存泄漏。

81 C++传值方式?区别?

值传递:形参即使在函数体内值发生变化,也不会影响实参的值;

引用传递:形参在函数体内值发生变化,会影响实参的值;

指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;

做函数形参时,最好不使用值传递,因为会拷贝一个副本,效率很低,使用引用传递和指针传递更高效,不发生拷贝行为,但是两者间引用传递更加安全。

82 讲一下内存分区

  • 代码区:存放函数体的二进制代码,由操作系统进行管理;
  • 全局区:存放全局变量和静态(全局、局部)变量和字符串常量;
  • 栈区(stack):由编译器自动分配释放, 存放函数的参数值,局部变量等;
  • 堆区(heap):由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

内存分区意义:不同区域存放不同的数据,赋予数据不同的生命周期, 更大限度的灵活编程。

操作系统

2 同步、异步、阻塞、非阻塞的概念

  • 同步:当一个同步调用发出后,调用者要一直等待返回结果。通知后,才能进行后续的执行。

  • 异步:当一个异步过程调用发出后,调用者不能立刻得到返回结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

  • 阻塞:是指调用结果返回前,当前线程会被挂起,即阻塞。

  • 非阻塞:是指即使调用结果没返回,也不会阻塞当前线程。

3 线程与进程区别

  • 进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位
  • 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
  • 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
  • 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。
  • 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。

4 为什么有了进程,还要有线程呢?

进程属于在CPU和系统资源等方面提供的抽象,能够有效提高CPU的利用率。

线程是在进程这个层次上提供的一层并发的抽象:

(1)能够使系统在同一时间能够做多件事情;

(2)当进程遇到阻塞时,例如等待输入,线程能够使不依赖输入数据的工作继续执行

(3)可以有效地利用多处理器和多核计算机,在没有线程之前,多核并不能让一个进程的执行速度提高

6 进程间的通信方式有哪些?

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。

  • 管道PIPE
    • 是半双工的,具有固定的读端和写端;
    • 只能用于父子进程或者兄弟进程之间的进程的通信;
    • 可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
  • 命名管道
    FIFO 可以在无关的进程之间交换数据,与无名管道不同;
    FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
  • 信号量semophere
    信号量(semaphore)是一个计数器。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
    信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;

信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;

每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;

支持信号量组。

  • 共享内存shared memory
    共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
    共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  • 消息队列message queue
    消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;
    消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;

消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;

消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

  • 套接字socket
    可用于不同主机间的进程通信;
  • 信号signal
    比较复杂的通信方式,用于通知进程某个事件已经发生。

7 进程的调度算法有哪些?

调度算法是指:根据系统的资源分配策略所规定的资源分配算法。常用的调度算法有:先来先服务调度算法、时间片轮转调度法、短作业优先调度算法、最短剩余时间优先、高响应比优先调度算法、优先级调度算法等等。

8 什么是死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
img

9 死锁的几种场景

  • 忘记释放锁
  • 重复加锁
  • 多线程多锁,抢占锁资源

11 物理地址、逻辑地址、虚拟内存的概念

1.逻辑地址
是上层程序员可以操作的地址,和段相关的偏移地址部分,也就是变址寄存器中存储的32位偏移地址,而其他寄存器上的地址往往对于上层程序员来说是不可更改甚至是不可见的. 只有在实模式下,逻辑地址才和物理地址一致(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑地址也就是在保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样).应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及.应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。

2.物理地址
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。在实地址模式(因为实模式没有分段或分页机制,Cpu不进行自动地址转换)下,程序员操作的就是物理地址,所谓的物理地址就是物理内存上的32位地址,即物理地址可以直接定位到物理内存上的位置,无论任何操作,最终都必须要得到物理地址才能在物理内存上进行操作。

3.虚拟地址
cpu要访问虚拟内存地址时,需要经过地址翻译成物理地址才能访问。比如cpu要访问虚拟地址4100,需要通过专用的硬件内存管理单元(memory management unit)MMU来翻译成对应的内存物理地址4,然后cpu在内存地址4的位置上取到数据返回。

4.虚拟内存
在运行一个进程的时候,它所需要的内存空间可能大于系统的物理内存容量。通常一个进程会有4G的空间,但是物理内存并没有这么大,所以这些空间都是虚拟内存,它的地址都是逻辑地址,每次在访问的时候都需要映射成物理地址。

当进程访问某个逻辑地址的时候,会去查看页表,如果页表中没有相应的物理地址,说明内存中没有这页的数据,发生缺页异常,这时候进程需要把数据从磁盘拷贝到物理内存中。如果物理内存已经满了,就需要覆盖已有的页,如果这个页曾经被修改过,那么还要把它写回磁盘。

虚拟内存被分为一块块固定的大小,成为虚拟页(Virtual Page)简称VP,对应的物理内存也被分成一块块同样的大小,成为物理页(Physical Page)简称PP。磁盘和内存之间是以页为单位进行数据交换的。

14 谈谈你对动态链接库和静态链接库的理解?

静态链接就是在编译链接时直接将需要的执行代码拷贝到调用处,优点就是在程序发布的时候就不需要的依赖库,也就是不再需要带着库一块发布,程序可以独立执行,但是体积可能会相对大一些。

动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。优点是多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝,缺点是由于是运行时加载,可能会影响程序的前期执行性能

15 外中断和异常有什么区别?

外中断是指由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

而异常是由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。

16 程序执行过程简介

预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。
编译:将预处理后的文件,进行词法分析、语法分析、语义分析及优化转换成汇编语言,生成.s文件
汇编:汇编变为目标代码(机器代码)生成.o的文件
链接:连接目标代码,生成可执行程序

18 僵尸进程、孤儿进程和守护进程

  • 孤儿进程
    如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。
    一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  • 孤儿进程并不会有什么危害。

  • 僵尸进程

    • 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
    • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
    • 僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
  • 守护进程
    指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等

19 共享内存和内存映射的区别

  1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
  2. 共享内存效果更高
  3. 内存
    所有的进程操作的是同一块共享内存。
    内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
  4. 数据安全
    • 进程突然退出
      共享内存还存在
      内存映射区消失
    • 运行进程的电脑死机,宕机了
      数据存在在共享内存中,没有了
      内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
  5. 生命周期
    • 内存映射区:进程退出,内存映射区销毁
    • 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
      如果一个进程退出,会自动和共享内存进行取消关联。

计算机网络

1. OSI 的七层模型分别是?各自的功能是什么?

OSI体系7层

  • 应⽤层,负责给应⽤程序提供统⼀的接⼝;
  • 表示层,负责把数据转换成兼容另⼀个系统能识别的格式;
  • 会话层,负责建⽴、管理和终⽌表示层实体之间的通信会话;
  • 传输层,负责端到端的数据传输;
  • ⽹络层,负责数据的路由、转发、分⽚;
  • 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
  • 物理层,负责在物理⽹络中传输数据帧;

TCP/IP体系4层:

  • 应用层:解决通过应用进程的交互来实现特定网络应用的问题
  • 运输层:解决进程之间基于网络通信的问题
  • 网络层:解决分组在多个网络上传输(路由的问题)
  • 网络接口层:物理层+数据链路层

原理体系5层

  • 应用层:解决通过应用进程的交互来实现特定网络应用的问题
  • 运输层:解决进程之间基于网络通信的问题
  • 网络层:解决分组在多个网络上传输(路由的问题)
  • 数据链路层:解决分组在一个网络(或者一段链路)上传输的问题
  • 物理层:解决使用何种信号来传输比特的问题

2. 为什么需要三次握手?两次不行?

刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后

  1. 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。

  2. 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

  3. 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 established 状态。

  4. 服务器收到 ACK 报文之后,也处于 established 状态,此时,双方以建立起了链接

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

3. 为什么需要四次挥手?三次不行?

刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:

  • 一次挥手:TCP客户端主动发起关闭连接,发送TCP连接释放,发送报文FIN=1 ACK=1 seq=u ack=v,此时TCP客户端进入FIN-WAIT-1终止等待1状态
  • 二次挥手:TCP服务器端接受到报文后,发送TCP普通确认报文ACK=1 seq=v ack=u+1,此时进入CLOSE-WAIT关闭等待状态此时客户端不能向服务器端传送数据,但反之可以,客户端收到确认报文后。,此时看客户端进入FIN-WAIT-2终止等待2状态
  • 三次挥手:TCP服务器进程发送TCP连接释放报文段,并进入最后确认状态,发送报文FIN=1 ACK=1 seq=w ack=u+1,w是因为可能还有发送一些数据。
  • 四次挥手:TCP客户端收到连接释放报文后,进入TIME-WAIT时间等待状态,并且发送确认报文ACK=1 seq=u+1 ack=w+1,此时服务器端接收到确认报文后进入关闭状态。客户端在结果2MSL(2倍最大报文段寿命)

5. HTTP1.0,1.1,2.0 的版本区别

  • HTTP/1.0

为了提高系统的效率,HTTP/1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。

HTTP/1.0中浏览器与服务器只保持短暂的连接,连接无法复用。也就是说每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。

  • HTTP/1.1

持久连接。所谓的持久连接即TCP连接默认不关闭,可以被多个请求复用

客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。或者客户端在最后一个请求时,主动告诉服务端要关闭连接。

HTTP/1.1版还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。

  • HTTP/2

HTTP/2 为了解决HTTP/1.1中仍然存在的效率问题,HTTP/2 采用了多路复用。即在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应。能这样做有一个前提,就是HTTP/2进行了二进制分帧,即 HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码。

9. 在交互过程中如果数据传送完了,还不想断开连接怎么办,怎么维持?

在 HTTP 中响应体的 Connection 字段指定为 keep-alive

11. TCP 如何保证有效传输及拥塞控制原理

tcp 是面向连接的、可靠的、传输层通信协议
可靠体现在:有状态、可控制
1)有状态是指 TCP 会确认发送了哪些报文,接收方受到了哪些报文,哪些没有收到,保证数据包按序到达,不允许有差错
2)可控制的是指,如果出现丢包或者网络状况不佳,则会跳转自己的行为,减少发送的速度或者重发
所以上面能保证数据包的有效传输。

  • 拥塞控制原理

原因是有可能整个网络环境特别差,容易丢包,那么发送端就应该注意了。

主要用三种方法:

慢启动阈值 + 拥塞避免
快速重传
快速恢复

  • 慢启动阈值 + 拥塞避免

对于拥塞控制来说,TCP 主要维护两个核心状态:

  • 拥塞窗口(cwnd)
  • 慢启动阈值(ssthresh)

然后采用一种比较保守的慢启动算法来慢慢适应这个网络,在开始传输的一段时间,发送端和接收端会首先通过三次握手建立连接,确定各自接收窗口大小,然后初始化双方的拥塞窗口,接着每经过一轮 RTT(收发时延),拥塞窗口大小翻倍,直到达到慢启动阈值。

然后开始进行拥塞避免,拥塞避免具体的做法就是之前每一轮 RTT,拥塞窗口翻倍,现在每一轮就加一个。

  • 快速重传

在 TCP 传输过程中,如果发生了丢包,接收端就会发送之前重复 ACK,比如 第 5 个包丢了,6、7 达到,然后接收端会为 5,6,7 都发送第四个包的 ACK,这个时候发送端受到了 3 个重复的 ACK,意识到丢包了,就会马上进行重传,而不用等到 RTO (超时重传的时间)

选择性重传:报文首部可选性中加入 SACK 属性,通过 left edge 和 right edge 标志那些包到了,然后重传没到的包

  • 快速恢复

如果发送端收到了 3 个重复的 ACK,发现了丢包,觉得现在的网络状况已经进入拥塞状态了,那么就会进入快速恢复阶段:

会将拥塞阈值降低为 拥塞窗口的一半
然后拥塞窗口大小变为拥塞阈值
接着 拥塞窗口再进行线性增加,以适应网络状况

16. 讲一下网络五层模型,每一层的职责?

  • 应用层:解决通过应用进程的交互来实现特定网络应用的问题
  • 运输层:解决进程之间基于网络通信的问题
  • 网络层:解决分组在多个网络上传输(路由的问题)
  • 数据链路层:解决分组在一个网络(或者一段链路)上传输的问题
  • 物理层:解决使用何种信号来传输比特的问题

17. 简单说下 HTTPS 和 HTTP 的区别

Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;Https是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的HTTP。二者之间存在如下不同:

1、端口不同:Http与Https使用不同的连接方式,用的端口也不一样,前者是80,后者是443;

2、资源消耗:和HTTP通信相比,Https通信会由于加减密处理消耗更多的CPU和内存资源;

3、开销:Https通信需要证书,而证书一般需要向认证机构购买;
 
Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。

19. 简单说下每一层对应的网络协议有哪些?

应用层:HTTP、SMTP、FTP、DNS、DHCP、SSH、TELNET
传输层:TCP、UDP
网络层:IP、ARP、OSPF、RIP
数据链路层:CSMA/CD(停止等待协议),PPP点对点协议

20. ARP 协议的工作原理?

网络层的 ARP 协议完成了 IP 地址与物理地址的映射。首先,每台主机都会在自己的 ARP 缓冲区中建立一个 ARP 列表,以表示 IP 地址和 MAC 地址的对应关系。当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP 列表中是否存在该 IP 地址对应的 MAC 地址:如果有,就直接将数据包发送到这个 MAC 地址;如果没有,就向本地网段发起一个 ARP 请求的广播包,查询此目的主机对应的 MAC 地址。

此 ARP 请求数据包里包括源主机的 IP 地址、硬件地址、以及目的主机的 IP 地址。网络中所有的主机收到这个 ARP 请求后,会检查数据包中的目的 IP 是否和自己的 IP 地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的 MAC 地址和 IP 地址添加到自己的 ARP 列表中,如果 ARP 表中已经存在该 IP 的信息,则将其覆盖,然后给源主机发送一个 ARP 响应数据包,告诉对方自己是它需要查找的 MAC 地址;源主机收到这个 ARP 响应数据包后,将得到的目的主机的 IP 地址和 MAC 地址添加到自己的 ARP 列表中,并利用此信息开始数据的传输。如果源主机一直没有收到 ARP 响应数据包,表示 ARP 查询失败

21. TCP 的主要特点是什么?

  • TCP 是面向连接的。(就好像打电话一样,通话前需要先拨号建立连接,通话结束后要挂机释放连接);

  • 每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一);

  • TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复、并且按序到达;

  • TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据;

  • 面向字节流。TCP 中的“流”(Stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。

23. TCP 和 UDP 分别对应的常见应用层协议有哪些?

  1. TCP 对应的应用层协议

    • FTP:定义了文件传输协议,使用 21 端口。常说某某计算机开了 FTP 服务便是启动了文件传输服务。下载文件,上传主页,都要用到 FTP 服务。

    • Telnet:它是一种用于远程登陆的协议,用户可以以自己的身份远程连接到计算机上,通过这种端口可以提供一种基于 DOS 模式下的通信服务。如以前的 BBS 是-纯字符界面的,支持 BBS 的服务器将 23 端口打开,对外提供服务。

    • SMTP:定义了简单邮件传送协议,现在很多邮件服务器都用的是这个协议,用于发送邮件。如常见的免费邮件服务中用的就是这个邮件服务端口,所以在电子邮件设置-中常看到有这么 SMTP 端口设置这个栏,服务器开放的是 25 号端口。

    • POP3:它是和 SMTP 对应,POP3 用于接收邮件。通常情况下,POP3 协议所用的是 110 端口。也是说,只要你有相应的使用 POP3 协议的程序(例如 Fo-xmail 或 Outlook),就可以不以 Web 方式登陆进邮箱界面,直接用邮件程序就可以收到邮件(如是163 邮箱就没有必要先进入网易网站,再进入自己的邮-箱来收信)。

    • HTTP:从 Web 服务器传输超文本到本地浏览器的传送协议。

  2. UDP 对应的应用层协议

    • DNS:用于域名解析服务,将域名地址转换为 IP 地址。DNS 用的是 53 号端口。

    • SNMP:简单网络管理协议,使用 161 号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势。

    • TFTP(Trival File Transfer Protocal):简单文件传输协议,该协议在熟知端口 69 上使用 UDP 服务。

24. 为什么 TIME-WAIT 状态必须等待 2MSL 的时间呢?

  • 避免因为客户端确认报文的丢失而造成服务器端TCP进程不能关闭的资源浪费问题
  • 经过2MSL后,可以使得在本次TCP服务传输过程中的产生的所有报文段都从网络中消失,避免了对下次TCP连接的干扰可能性

26. TCP 协议是如何保证可靠传输的?

  1. 数据包校验:目的是检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时 TCP 发送数据端超时后会重发数据;

  2. 对失序数据包重排序:既然 TCP 报文段作为 IP 数据报来传输,而 IP 数据报的到达可能会失序,因此 TCP 报文段的到达也可能会失序。TCP 将对失序数据进行重新排序,然后才交给应用层;

  3. 丢弃重复数据:对于重复数据,能够丢弃重复数据;

  4. 应答机制:当 TCP 收到发自 TCP 连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒;

  5. 超时重发:当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段;

  6. 流量控制:TCP 连接的每一方都有固定大小的缓冲空间。TCP 的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。TCP 使用的流量控制协议是可变大小的滑动窗口协议。

27. 谈谈你对停止等待协议的理解?

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组;在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。主要包括以下几种情况:无差错情况、出现差错情况(超时重传)、确认丢失和确认迟到。

28. 谈谈你对 ARQ 协议的理解?

  • 自动重传请求 ARQ 协议

停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求 ARQ。

  • 连续 ARQ 协议

连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

29. 谈谈你对滑动窗口的了解?

TCP 利用滑动窗口实现流量控制的机制。滑动窗口(Sliding window)是一种流量控制技术。滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但是发送紧急数据和滑动窗口大小的数据可以发送

30. 谈下你对流量控制的理解?

  • 流量控制定义:所谓流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收
  • 流动窗口:利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制。
    1. TCP接收方利用自己的接收窗口的大小来限制发送方发送窗口的大小。
    2. TCP发送方收到接收方的零窗口通知后,应启动持续计时器。持续计时器超时后,向接收方发送零窗口探测报文。

31. 谈下你对 TCP 拥塞控制的理解?使用了哪些算法?

拥塞控制和流量控制不同,前者是一个全局性的过程,而后者指点对点通信量的控制。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。

拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致于过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

为了进行拥塞控制,TCP 发送方要维持一个拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

TCP 的拥塞控制采用了四种算法,即:慢开始、拥塞避免、快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如:主动队列管理 AQM),以减少网络拥塞的发生。

  • 慢开始:
    慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。

  • 拥塞避免:
    拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大,也就是将cwnd值每一个RTT+1。

  • 快重传与快恢复:
    在 TCP/IP 中,快速重传和快恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。

没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。

有了 FRR,就不会因为重传时要求的暂停被耽误。当有单独的数据包丢失时,快速重传和快恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

判断网络拥塞的依据:发生了超时重传

32. 什么是粘包?

在使用 TCP 传输数据时,一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。

接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。拆包和粘包的问题导致接收端在处理的时候会非常困难,因为无法区分一个完整的数据包。

33. TCP 黏包是怎么产生的?

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

34. 怎么解决拆包和粘包?

通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

  1. 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
  2. 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  3. 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

37. 在浏览器中输入 URL 地址到显示主页的过程?

  1. DNS 解析:浏览器查询 DNS,获取域名对应的 IP 地址:具体过程包括浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等。对于向本地 DNS 服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析(此解析具有权威性);如果要查询的域名不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;

  2. TCP 连接:浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起三次握手;

  3. 发送 HTTP 请求:TCP 连接建立起来后,浏览器向服务器发送 HTTP 请求;

  4. 服务器处理请求并返回 HTTP 报文:服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;

  5. 浏览器解析渲染页面:浏览器解析并渲染视图,若遇到对 js 文件、css 文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。

  6. 连接结束。

47. UDP 如何实现可靠传输?

UDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响。

传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。

最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。

1、添加seq/ack机制,确保数据发送到对端
2、添加发送和接收缓冲区,主要是用户超时重传。
3、添加超时重传机制。
详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。

51. TCP与UDP区别

  1. TCP是面向连接的 ;UDP是无连接的
  2. TCP仅支持单播 ;UDP支持单播、多播和广播
  3. TCP是面向字节流的;UDP是面向应用报文的
  4. TCP可靠传输,使用流量控制和拥塞控制;UDP不可靠传输,不使用流量控制和拥塞控制
  5. TCP报文首部最小20字节最大60字节;UDP报文首部8字节

58. IP地址和MAC地址有什么区别?各自的用处?

简单来说,IP 地址主要用来网络寻址用的,就是大致定位你在哪里,而 MAC 地址,则是身份的唯一象征,通过 MAC 来唯一确认这人是不是就是你,MAC 地址不具备寻址的功能。

5 排序算法总结

首先总结表如下:

排序方法 平均时间复杂度 最好情况 最坏情况 空间复杂度 是否稳定 排序方式
冒泡排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 稳定 内部排序
选择排序 \(O(n^2)\) \(O(n^2)\) \(O(n^2)\) \(O(1)\) 不稳定 内部排序
插入排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 稳定 内部排序
希尔排序 \(O(n\log n)\) \(O(n\log ^2 n)\) \(O(n\log ^2 n)\) \(O(1)\) 不稳定 内部排序
快速排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n^2)\) \(O(\log n)\) 不稳定 内部排序
归并排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n\log n)\) \(O(n)\) 稳定 外部排序
堆排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n\log n)\) \(O(1)\) 不稳定 内部排序
桶排序 \(O(n+k)\) \(O(n+k)\) \(O(n^2)\) \(O(k)\) 稳定 外部排序
计数排序 \(O(n+k)\) \(O(n+k)\) \(O(n+k)\) \(O(k)\) 稳定 外部排序

1 冒泡排序

1.1 算法简述

两两比较,选出较大值,每一轮选出单轮最大值到数组末端,这个过程就像泡泡变大的过程,所以交冒泡算法。

代码

void bubbleSort(vector<int>& nums) {
    for (int i = 0; i < nums.size() - 1; i++) {
        //int flag = 0;
        for (int j = 0; j < nums.size() - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
                //flag = 1;
            }
        }
        //if(flag == 0) return;
    }
}

1.2 优化

定义一个flag,只要进行交换就置为1.当某一次内层for循环没有交换过flag为0,那么说明已经排序好了

1.3 最好情况

数组已经是正序排列时,仅有比较,没有交换

1.4 最坏情况

数组是反序排列时,每一轮都需要交换

5 快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

5.1 算法简述

  1. 从数列中挑出一个元素,称为 "基准"(pivot);

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

5.2 代码

// 快速排序
int Paritition(vector<int>& nums, int low, int high) {
    //返回基准pivot的nums的序号
    int pivot = nums[low];
    while (low < high) {
        //将nums分为小于基准和大于基准的两侧
        while (low < high && nums[high] >= pivot) --high;
        nums[low] = nums[high];
        while (low < high && nums[low] <= pivot) ++low;
        nums[high] = nums[low];
    }
    nums[low] = pivot;
    return low;
}

void QuickSort(vector<int>& nums, int low, int high) {
    if (low < high) {
        int pivot = Paritition(nums, low, high);
        QuickSort(nums, low, pivot - 1);//递归小于基准部分
        QuickSort(nums, pivot + 1, high);//递归大于基准部分
    }
}

void QuickSort(vector<int>& nums) {
    QuickSort(nums, 0, nums.size() - 1);
}
posted @ 2024-03-25 16:28  mobbu  阅读(21)  评论(0编辑  收藏  举报