c++基础必备

  1. 编译语言和解释语言的区别?

    编译型语言:
    • 优点:编译器一般会有预编译的过程对代码进行优化。因为编译只做一次,运行时不需要编译,所以编译型语言的程序执行效率高。可以脱离语言环境独立运行。
    • 缺点:编译之后如果需要修改就需要整个模块重新编译。编译的时候根据对应的运行环境生成机器码,不同的操作系统之间移植就会有问题,需要根据运行的操作系统环境编译不同的可执行文件。
    解释型语言
    • 优点:有良好的平台兼容性,在任何环境中都可以运行,前提是安装了解释器(虚拟机)。灵活,修改代码的时候直接修改就可以,可以快速部署,不用停机维护。
    • 缺点:每次运行的时候都要解释一遍,性能上不如编译型语言。
  2. doule 和float二进制存储,double 1.5 和 float 1.5 的大小

    无论是单精度还是双进度在存储中分为三个部分:

    1.符号位(Sign):0代表正,1代表负

    2.指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储

    3.尾数部分(Mantissa):尾数部分

    其中 float 的存储方式如下图所示:
      在这里插入图片描述

  3. 智能指针shared_ptr和weak_ptr,循环引用的情况的处理

    shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。共享指针对象重载了 operator* 和 operator-> , 所以你可以像通常的指针一样使用它。std::shared_ptr 的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。

    为了解决循化引用问题,我们又引入了weak_ptr弱指针,用来辅助shared_ptr。注意weak_ptr不能单独使用,必须辅助shared_ptr才能使用。weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被销毁,即使有weak_ptr指向对象,对象还是会被释放。

    //智能指针的循环引用
    struct Node
    {
        int _data;
        shared_ptr<Node> _next;
        shared_ptr<Node> _prev;
    }
    shared_ptr<Node> sp1(new Node);
    shared_ptr<Node> sp2(new Node);
    //现在将这两个指针互相连接出现循环引用:
    sp1->_next=sp2;
    sp2->prev=sp1;
    //我们可以将指针域的智能指针声明为弱指针。
    struct Node
    {
        int _data;
        weak_ptr<Node> _next;
        weak_ptr<Node> _prev;
    }
    
  4. dynamic_cast的作用,4种类型转换?static_cast和dynamic_cast的区别,为什么dynamic_cast能够向上向下转?

    1)、 static_cast (expression)。最常用的应该还是基本数据类型之间的转换
    该运算符把 expression 转换为 type-id 类型,但没有运行时类型检查来保证转换的安全性。主要用法如下:
    (1)用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
    (2)用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
    (3)把空指针转换成目标类型的空指针。
    (4)把任何类型的表达式转换成void类型。

    2)、const_cast<指针或引用>: 里边的内容必须是引用或者指针。常用于去除const类对象的只读属性,且强制转换的类型必须是指针*或引用&。

    3)、reinterpret_cast,支持任何转换,但仅仅是如它的名字所描述的那样“重解释”而已,不会对指针的值进行任何调整,用它完全可以做到“指鹿为马”,但很明显,它是最不安全的转换.

    4)、dynamic_cast (expression) 其他三种都是编译时完成的,dynamic_cast 是运行时处理的,运行时要进行类型检查。

    4.1)不能用于内置的基本数据类型的强制转换
    

    4.2)dynamic_cast 要求 <> 内所描述的目标类型必须为指针或引用。dynamic_cast 转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回 nullptr。
     4.3)在类的转换时,在类层次间进行上行转换(子类指针指向父类指针)时,dynamic_cast 和 static_cast 的效果是一样的。在进行下行转换(父类指针转化为子类指针)时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。 向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。
      4.4)使用 dynamic_cast 进行转换的,基类中一定要有虚函数,否则编译不通过(类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义)。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表

  5. C++ 内存分配原理和内存泄漏原因及如何避免
    内存分配原理: 内存碎片:当程序同时处理一系列内存需求,其中包括大块内存,小块内存,大小混合内存分配和释放时,有可能有大量的小块内存持续的分配和释放。这样很容易产生内存碎片,也就是说请求一个相对较大的内存时,空闲内存总和足够分配,但是不能分配一个连续的大块内存。为了解决内存碎片的问题,SGI 设计了两级allocator:

  • 当请求内存较大(大于128bytes)时,采用第一级allocator

  • 小于128bytes时,采用第二级allocator,采用内存池的方法管理小内存

  • 第一级和第二级allocator主要区别在于处理不同大小的内存申请。内存申请大于128bytes时,第一级allocator直接调用malloc和free函数。第二级allocator则复杂很多,具体实现包括:维护16个自由链表,负责16种不同大小的内存空间分配。自由链表的内存由内存池分配,而内存池则由malloc函数分配内存。如果内存不足或请求内存大于128bytes则转调第一级allocator。

       **内存泄漏原因:**new和delete没有正确的匹配使用。在Linux平台上 有valgrind可以非常方便的帮助我们定位内存泄漏。使用智能指针和良好的编程习惯解决内存泄漏。
    
  1. 野指针是什么

    野指针不同于空指针,空指针是指一个指针的值为null,而野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放,所以在实际使用的过程中,我们并不能通过指针判空去识别一个指针是否为野指针。避免野指针只能靠我们自己养成良好的编程习惯。

    • 指针变量没有初始化: 任何指针变量在刚创建时不会自动赋值为NULL指针(我们知道int整形变量没有初始化前也是随机的一个数,指针说简单点也是一个整形值,那自然它在为人为的初始化之前,自然也是一个随机值。某些编译器在debug模式下会赋值为NULL,但release下不会)。在VC会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd。
    • 指针delete或者free之后没有设置值为空: 对于堆内存的操作,我们分配了一些内存空间(使用new或者malloc方法申请一片内存),使用完成之后进行了释放(使用delete 或者 free),但是并没有将指针置为空,再次使用的时候就会出现崩溃现象。使用if(p==NULL)这种判断方式是不行的。
    • 指针超过了变量的作用范围: 即在变量的作用范围之外使用了指向变量地址的指针。这一般发生在将调用函数中的局部变量的地址传出来引起的。这个是很多在开始写C++代码时经常犯的错误
    • 解决方法: 静态代码分析可以解决 缓冲区溢出内存泄露野指针部分代码规范 等典型问题,代码分析工具可以解决的指针未初始化以及释放忘记delete的问题。 常用的代码检查工具有 pc-lintcppcheckTScanCodeAstree 等。另外某些工具也可以解决野指针问题如 vld 的windows下的内存泄露工具,以及linux下的 valgrind 工具等。野指针最好的解决办法就是通过智能指针来代替普通指针的实现(智能指针被封装为一个栈对象,当生命周期退出的时候就根据引用计数判断是否需要释放堆上的内存)。
  2. C++是单继承还是多继承,菱形继承的问题,为什么虚继承能解决

    如果在多重继承中Class A 和Class B存在同名数据成员,则对Class C而言这个同名的数据成员容易产生二义性问题。这里的二义性是指无法直接通过变量名进行读取,需要通过域(::)成员运算符进行区分。菱形继承会导致数据的二义性,这里的二义性是由于他们间接都有相同的基类导致的。 这种菱形继承除了带来二义性之外,还会浪费内存空间。为了解决上述菱形继承带来的问题,C++中引入了虚基类,其作用是 在间接继承共同基类时只保留一份基类成员。虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式声明的。虚继承是声明类时的一种继承方式,在继承属性前面添加virtual关键字。

  3. const和define的区别,哪种更好
    区别:(1)就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用。
    (2)就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
    (3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
    (4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。

    const的优点:
    (1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
    (2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
    (3)const可节省空间,避免不必要的内存分配,提高效率

  4. 指针和数组的区别

  • 数组:数组是用于储存多个相同类型数据的集合。
  • 指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。

指针和数组还是在本质上是不一样的。指针就是指针,指针变量在32位的系统下面是4Byte,而在64位系统下面是8Byte,其值为某一个内存的地址。而数组就是数组,其大小与元素的类型和个数有关,定义数组时必须制定其元素的类型和个数,数组可以存放任何类型的数据,但是不能存放函数。数组名可以当做数组第一个元素的访问指针。

  1. 全局变量和局部变量的区别,操作系统和编译器是怎么知道的

    操作系统和编译器通过内存分配的位置来知道的全局变量分配在全局数据段,并且在程序被运行的时候就被加载。编译器通过语法词法的分析,判断出是全局变量还是局部变量。如果是全局变量的话,编译器在将源代码翻译成二进制代码时就为全局变量分配好一个虚拟地址 (windows下0x00400000以上的地址,也就是所说的全局区),所以程序在对全局变量的操作时是对一个硬编码的地址操做。 局部变量的话,编译时不分配空间,而是以相对于ebp或esp的偏移来表示局部变量的地址,所以局部变量内存是在局部变量所在的函数被调用时才真正分配。 以汇编的角度来看:函数执行时,局部变量在栈中分配,函数调用完毕释放局部变量对应的内存,另外局部变量可以直接分配在寄存器中。

  2. STL中map和unordered_map的区别,两种map的底层实现

    内部实现机理不同map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
    unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。

  3. RTTI的底层实现原理,如何动态获得类型,怎么判断指针对象的继承关系

    运行时类型识别(RTTI)的引入有三个作用:

1)配合typeid操作符的实现;

2)实现异常处理中catch的匹配过程;

3)实现动态类型转换dynamic_cast编译器会在虚函数表 vftable 的开头插入一个指针,指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。虽然这么做会消耗资源,但也是不得已而为之。这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type Identification,RTTI)。在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。

  1. c++对象的生命周期

    • 从静态存储区分配:此时的内存在程序编译的时候已经分配好,并且在程序的整个运行期间都存在。全局变量,static变量等在此存储
    • 在栈区分配:相关代码执行时创建,执行结束时被自动释放。局部变量在此存储。栈内存分配运算内置于处理器的指令集中,效率高,但容量有限
    • 在堆区分配:动态分配内存。用new/malloc时开辟,delete/free时释放。生存期由用户指定,灵活。但有内存泄露等问题.
  2. static关键字

    static关键字的作用:

    1、函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此,其值在下次调用的时候仍然维持原始值。静态变量只初始化一次,未初始化的静态变量会默认初始化为0。

    2、在模块内的static全局变量可以被模块内的所有函数访问,但是不能被模块外的其他函数访问。

    3、在模块内的static函数只可以被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内。

    4、在类中的static成员变量属于整个类所有,对类的所有对象只有一份拷贝。

    5、在类中的static成员函数属于整个类所有,这个函数不接受this指针,因而只能访问类的static成员变量。

    const关键字的作用:

    1、想要阻止一个变量被改变,可以使用const关键字。在定义该const关键字是,通常要对它进行初始化,因为以后再也没有机会去改变它。

    2、对于指针来说,可以指定指针本省为const,也可以指定指针所指向的数据为const,或者二者同时指定为const。

    3、在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值。

    4、对于类的成员函数,若指定为const,则表明其实一个常函数,不能修改类的成员变量。

    5、对于类的成员函数,有时候必须制定其返回值为const,以使得其返回值不能为左值。

  3. 移动语义和右值引用

    移动语义:C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值**。**如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能。移动语义就是交换对象的使用权。C++在用临时对象或函数返回值给左值对象赋值时的深度拷贝(deep copy)一直受到诟病。考虑到临时对象的生命期仅在表达式中持续,如果把临时对象的内容直接移动(move)给被赋值的左值对象,效率改善将是显著的。这就是移动语义的来源。这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。

    image

    右值引用: 我们通过&&来获得右值引用。右值引用一个重要的特性就是只能绑定到将要销毁的对象。std::move是个模板函数,把输入的左值或右值转换为右值引用类型的临终值。其核心是强制类型转换static_cast()语句。将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。

    void swap_A(A &a1, A &a2){
          
          
        A tmp(std::move(a1)); // a1 转为右值,移动构造函数调用,低成本
        a1 = std::move(a2);   // a2 转为右值,移动赋值函数调用,低成本
        a2 = std::move(tmp);  // tmp 转为右值移动给a2
    }
    

    完美转发: 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。那如何实现完美转发呢,答案是使用std::forward()。

    引用折叠: X& &、X& &&、X&& &都折叠成X& ; X&& &&折叠为X&&。当将一个左值传递给一个参数是右值引用的函数,且此右值引用指向模板类型参数(T&&)时,编译器推断模板参数类型为实参的左值引用.

  4. vector的扩容机制
    简介: 往vector中添加元素时,如果空间不够将会导致扩容。vector有两个属性:size和capacity。size表示已经使用的数据容量,capacity表示数组的实际容量,包含已使用的和未使用的。
    vector扩容规则: 当数组大小不够容纳新增元素时,开辟更大的内存空间,把旧空间上的数据复制过来,然后在新空间中继续增加。新的更大的内存空间,一般是当前空间的1.5倍或者2倍,这个1.5或者2被称为扩容因子,不同系统实现扩容因子也不同。在VS2017中,vector的扩容因子是1.5。在GCC的实现中,vector扩容是2倍扩容的。

  5. 位拷贝和值拷贝
    位拷贝: 拷贝的是地址(也叫浅拷贝),创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
    深拷贝: 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。而值拷贝则拷贝的是内容(深拷贝)。
    区别: 深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
    拷贝构造: 在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。区别是在有指针成员的情况下深拷贝是拷贝指针指向的对象的内容,浅拷贝是拷贝的对象的地址。
    在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。
    1). 一个对象以值传递的方式传入函数体
    2). 一个对象以值传递的方式从函数返回
    3). 一个对象需要通过另外一个对象进行初始化
    以上的情况需要拷贝构造函数的调用。如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作赋共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。 拷贝构造函数不可以改变它所引用的对象,其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环。

  6. C++ malloc的底层实现
    1)当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
    2)当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
    malloc实现原理: Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
    当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
    1、空闲存储空间以空闲链表的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。( 因为程序中的某些地方可能不通过malloc调用申请,因此malloc管理的空间不一定连续。)
    2、当有申请请求时,malloc会扫描空闲链表,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc时并不是花费了完全相同的时间)。
    3、如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此malloc分配的是一块连续的内存。
    4、释放时,首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。

  7. reactor模式

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

  8. select和epoll

//select函数
 #include <sys/select.h>
 //select接口,调用时会堵塞
 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

而select()的本质就是通过设置和检查fd的三个fd_set来进行下一步处理。

select大体执行步骤:

  1. 向内核注册监视的文件描述符信息,从用户空间拷贝fd_set到内核空间

  2. 内核遍历[0,nfds)范围内的每个fd,调用fd所对应的设备的驱动poll函数,poll函数可以检测fd的可用流(读流、写流、异常流)。

  3. 检查是否有流发生,如果有发生,把流设置对应的类别,并执行4,如果没有流发生,执行5。或者timeout=0,执行4。

  4. select返回,遍历fd_set数组中值仍为1的文件描述符。

5)select阻塞进程,等待被流对应的设备唤醒时执行2,或timeout到期,执行4。

select的局限性:

1)维护一个存放大量描述符的数组:每次调用select()都需要将fd_set从用户态拷贝到内核态,然后由内核遍历fd_set,如果fd_set很大,那么拷贝和遍历的开销会很大,为了减少性能损坏,内核对fd_set的大小做了限制,并通过宏定义控制,无法改变(1024)。

2)单进程监听的描述符数量有限制:单进程能打开的最大连接数。

3)轮询遍历数组,效率低下:select机制只知道有IO发生,但是不知道是哪几个描述符,每次唤醒,都需要遍历一次,复杂度为O(n)。

epoll: epoll能够支持百万级别的句柄监听.
1)、调用epoll_create: linux内核会在epoll文件系统创建一个file节点,同时创建一个eventpoll结构体,结构体中有两个重要的成员:rbr是一棵红黑树,用于存放epoll_ctl注册的socket和事件;rdllist是一条双向链表,用于存放准备就绪的事件供epoll_wait调用。

/*建一个epoll句柄*/
int epoll_create(int size);

2)、调用epoll_ctl: 会检测rbr中是否已经存在节点,有就返回,没有则新增,同时会向内核注册回调函数ep_poll_callback,当有事件中断来临时,调用回调函数向rdllist中插入数据,epoll_ctl也可以增删改事件。

   /*向epoll句柄中添加需要监听的fd和时间event*/
	    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

​ 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback, 它会将发生的事件添加到rdlist双链表中。

   struct eventpoll{
    
    
	            /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
	            struct rb_root  rbr;
	            /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
	            struct list_head rdlist;
	            ....
	     };

3)调用epoll_wait: 返回或者判断rdllist中的数据即可。

 /*返回发生事件的队列*/
	    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

epoll总结: 不重复传递socket句柄给内核,红黑树及双向链表都在内核cache中,避免拷贝开销。内核使用红黑树而不是数组存放描述符和事件,增删改查非常高效,轻易可处理大量并发连接。采用回调机制,事件的发生只需关注rdllist双向链表即可。

  1. 边缘触发和条件触发

LT: 只要文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作。

ET: 检测到有IO事件时,通过epoll_wait调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,必须将该文件描述符一直读到空,让errno返回EAGAIN为止,否则下次的epoll_wait不会返回余下的数据,会丢掉事件。

ET比LT更加高效,因为ET只通知一次,而LT会通知多次,LT可能会充斥大量不关心的就绪文件描述符。

  1. 进程池 线程池 内存池

  2. 默认构造函数和拷贝构造函数的生成情况
    默认构造函数的4中场景:
    1) 包含一个对象类型的成员,而这个类有一个无参的构造函数。:因为成员中有一个类对象需要调用默认的构造方法,而这个默认构造方法需要在这个类中的构造方法中调用。
    2)父类有一个无参构造函数:子类需要先调用父类的构造方法,所以编译器必须生成一个默认构造函数。
    3)类中含有虚函数:如果一个类中有虚函数,那个这个类需要构造一个虚函数表,还需要在类对象中创建一个虚函数指针,然后把虚函数表的地址赋值给虚函数指针。
    4)类中有虚继承:如果类中有虚继承,那么编译器会生成虚基类表,并且改变类对象的数据布局。
    可以看出,以上的情况均属于,编译器需要执行分配内存和数据初始化以外的工作,就必须构造出默认构造函数,不然无法完成对象的构造。
    默认拷贝构造函数:如果不是必要的情况下,编译器可能不会生成默认的拷贝构造函数,而是执行逐位拷贝。必要的情况和构造函数的情况几乎是一样的
    1):含有一个含有拷贝构造函数的类成员对象。
    2):父类含有拷贝构造函数。
    3):含有虚函数或继承了虚函数。
    4):有虚继承。

  3. 信号量和线程池限流的区别:
    ​ 信号量Semaphore是一个并发工具类,用来控制可同时并发的线程数,其内部维护了一组虚拟许可,通过构造器指定许可的数量,每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其它线程释放许可。
      线程池用来控制实际工作的线程数量,通过线程复用的方式来减小内存开销。线程池可同时工作的线程数量是一定的,超过该数量的线程需进入线程队列等待,直到有可用的工作线程来执行任务。
      使用Seamphore,你创建了多少线程,实际就会有多少线程进行执行,只是可同时执行的线程数量会受到限制。但使用线程池,你创建的线程只是作为任务提交给线程池执行,实际工作的线程由线程池创建,并且实际工作的线程数量由线程池自己管理。
      简单来说,线程池实际工作的线程是work线程,不是你自己创建的,是由线程池创建的,并由线程池自动控制实际并发的work线程数量。而Seamphore相当于一个信号灯,作用是对线程做限流,Seamphore可以对你自己创建的的线程做限流(也可以对线程池的work线程做限流),Seamphore的限流必须通过手动acquire和release来实现。
      区别就是两点:
    1、实际工作的线程是谁创建的?
    使用线程池,实际工作线程由线程池创建;使用Seamphore,实际工作的线程由你自己创建。
    2、限流是否自动实现?
    线程池自动,Seamphore手动。

25.多进程和多线程的选取
进程是分配资源的基本单位;线程是系统调度和分派的基本单位。属于同一进程的线程,堆是共享的,栈是私有的。属于同一进程的所有线程都具有相同的地址空间。
多进程的优点:
①编程相对容易;通常不需要考虑锁和同步资源的问题。
②更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
③有内核保证的隔离:数据和错误隔离。 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)。
多线程的优点:
①创建速度快,方便高效的数据共享
共享数据:多线程间可以共享同一虚拟地址空间;多进程间的数据共享就需要用到共享内存、信号量等IPC技术。
②较轻的上下文切换开销 - 不用切换地址空间,不用更改寄存器,不用刷新TLB。
③提供非均质的服务。如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率。
选取场景:
①需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大)
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

②需要进行大量计算的优先使用线程(CPU频繁切换)
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。

③强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

④可能要扩展到多机分布的用进程,多核分布的用线程。

  1. 管道、消息队列、共享内存哪个开销最小?
    共享内存是所有IPC手段中最快的一种。它之所以快是因为共享内存一旦映射到进程的地址空间,进程之间数据的传递就不须要涉及内核了。管道、FIFO和消息队列,任意两个进程之间想要交换信息,都必须通过内核,内核在其中发挥了中转站的作用:
  • 发送信息的一方,通过系统调用(write或msgsnd)将信息从用户层拷贝到内核层,由内核暂存这部分信息。
  • 提取信息的一方,通过系统调用(read或msgrcv)将信息从内核层提取到应用层。
    一个通信周期内,上述过程至少牵扯到两次内存拷贝(从用户拷贝到内核空间和从内核空间拷贝到用户空间)和两次系统调用,这其中的开销不容小觑。用户层的体验固然不佳,所以产生了共享内存的通信方式,共享内存是在用户空间不经过内核。
  1. 进程中某个线程崩溃,是否会对其他线程造成影响?
    一般来说,每个线程都是独立执行的单位,每个线程都有自己的上下文堆栈,一个线程的的崩溃不会对其他线程造成影响。但是通常情况下,一个线程崩溃会产生一个进程内的错误,例如,在 Linux 操作系统中,可能会产生一个 Segment Fault 错误,这个错误会产生一个信号,操作系统默认对这个信号的处理就是结束进程,整个进程都被销毁了,这样的话这个进程中存在的其他线程自然也就不存在了。

  2. Meyers Singleton
    Meyers Singleton的实现方式基于"static variables with block scope"的自动线程安全特性,非常简单易懂。

class MeyersSingleton{
    
    
public:
  static MySingleton& getInstance(){
    
    
    static MySingleton instance;
    // volatile int dummy{};
    return instance;
  }
private:
  MySingleton()= default;
  ~MySingleton()= default;
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;
};
  1. TCP相关
    a. 慢启动算法的思路:主机开发发送数据报时,如果立即将大量的数据注入到网络中,可能会出现网络的拥塞。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次文段都能正确的接受确认报文段。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小。
    b.TCP采用随机序列号,主要是基于如下两个原因:防止接受网络上粘滞的TCP包,如果都从0开始的话,极其容易接受之前断开连接发送的粘滞包。虽然可以采用每次TCP会话都使用一个UUID作为标记,但是考虑到每次都要携带UUID,比较浪费流量,所以就采用随机序列号的方法。防止Hack猜测序列号,然后伪装TCP报文,当然这种防御其实很弱。

猜你喜欢

转载自blog.csdn.net/u014618114/article/details/107993106