【干货总结】Linux C/C++面试知识点

本文涉及的产品
云原生网关 MSE Higress,422元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: Linux C/C++基础与进阶知识点,不仅用于面试,平时开发也用得上!

目录

本篇参考网上及自身的面试经验,总结一些高频考察的Linux C/C++知识点,方便后续查阅总结。

一、C/C++编程基础

  1. C++多态的实现
    virtual关键字修饰基类的成员函数,派生类中重写此函数,实现多态
  2. C++四种强制类型转换[1]
  • static_cast
    static_cast<type>(expression)
    该运算符把 expression 转换为 type 类型,主要用于基本数据类型之间的转换,如把 uint 转换为 int,把 int 转换为 double 等。
    另外,static_cast 还可用于类层次结构中,基类和派生类之间指针或引用的转换,但也要注意:
    static_cast 进行上行转换是安全的,即把派生类的指针转换为基类的;
    static_cast 进行下行转换是不安全的,即把基类的指针转换为派生类的。
    注:static_cast 没有运行时类型检查来保证转换的安全性,需要程序员来判断转换是否安全。
uint x = 1;
int y = static_cast<int>(x); // 转换正确
int x = 1;
double y = static_cast<double>(x); // 转换正确
// 上行转换,派生类→基类
Derive* d = new Derive();
Base* b = static_cast<Base*>(d);
// 下行转换,基类→派生类
Base* b = new Base();
Derive* d = static_cast<Derive*>(b);
  • const_cast
    const_cast<type>(expression)
    主要是用来去除复合类型中const和volatile属性(没有真正去除)。expression 和 type 的类型需要保持一致。
  • dynamic_cast
    dynamic_cast<type>(expression)主要用于类层次间的上行转换或下行转换。在进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的,但在下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。
  • reinterpret_cast
    reinterpret_cast<type>(expression)
    该运算符可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。
  1. static的作用[2]
  • 限制变量或函数作用域
    被static关键字修饰的全局函数或者变量具有文件作用域,即只在当前文件中可见。
  • 保持变量内容的持久
    被static修饰的变量会被存储在静态存储区,生命周期也为从定义直至程序结束。对于局部变量,即使在函数退出后该静态变量依然存在,然而却也无法访问。此外,static修饰的变量一生只会被初始化一次。
  • 默认初始化为0
    因为被static修饰的变量会被存储在静态存储区,所以才有了这个一条。因为静态存储区的变量会被默认初始化为0。

除此之外,在C++中,static也可以类成员变量和类成员函数。

  • 类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。
  • 静态成员函数不含有this指针,所以可以作为回调函数。但同时为了可以访问类的成员变量可以将对象的this指针当做实参传入回调函数中。
  • 静态成员函数在类定义体外定义时不能加static关键字修饰,因为成员函数本是类作用域,而在类外用static修饰会将其作用于扩大为文件作用域,所以是不合理的。
  • 静态成员变量并不像一般的成员变量在构造函数中初始化,而是在类的实现文件中初始化,即必须在.cpp文件中初始化,否则在程序链接时会出错,重定义,且初始化时无需再使用static关键字修饰。
  • static修饰的const成员变量可以再类中被定义时初始化。
  • 利用static只会被初始化一次的特性,可以实现单例对象。
  1. extern 有什么作用
    extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明既不是定义,也不分配存储空间。
  2. 说一说const关键字[3]
    const关键字告诉了编译器,它修饰的目标值不能被改变,如果代码中发现有类似改变该变量的操作,那么编译器就会捕捉这个错误。修饰函数时,
    好的编程习惯告诫程序员,当不需要改变的变量,最好使用const修饰,例如非头文件定义的常量,最好用const而非define。
  3. sizeof 和 strlen 的区别
  • sizeof会将空字符\0计算在内,而strlen不会将空字符\0计算在内;
  • sizeof会计算到字符串最后一个空字符\0并结束,而strlen如果遇到第一个空字符\0的话就会停止并计算遇到的第一个空字符\0前面的长度。
  1. #define和const的区别[4]
  • #define 只是在预处理阶段简单的做字符替换,其可以实现宏函数和变量等;const是在编译、运行阶段起作用,修饰变量。
  • const 定义的常数是变量也带类型,#define 定义的只是个常数 不带类型。
  • #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
  • #define属于预处理,会占用代码空间;const+类型定义属于变量定义,占用数据段空间。
  1. 一个指针可以是 volatile 吗
    可以。volatile关键词修饰的变量意思为值可能会改变,指针是可以改变的,与const关键词相反。
  2. 定义和声明的区别
    声明是将一个名称引入程序,定义提供了一个实体在程序中的唯一描述。
  3. 什么是野指针
    访问一个已销毁或者访问受限的内存区域的指针。野指针的使用,可能会引起程序崩溃,且无法用NULL检测野指针。
  4. 指针和引用的区别[5]
    指针是一个变量,存储的是一个地址,指向内存的一个存储单元,指针变量占用内存;
    引用是原变量的一个别名,跟原来的变量实质上是同一个东西,引用变量不占用内存。
  5. 简述指针常量与常量指针区别[6]
    指针常量:本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。
    常量指针:又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。
  6. 引用作为函数参数以及返回值的好处[7]
    在内存中不产生被返回值的副本。(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!)
  7. 谈谈C++内存管理[8]
    32位内存管理
  • stack栈区:专门用来实现函数调用-栈结构的内存块。相对空间下(可以设置大小,Linux 一般默认是8M,可通过 ulimit –s 查看),系统自动管理,从高地址往低地址,向下生长。
  • 内存映射区:包括文件映射和匿名内存映射, 应用程序的所依赖的动态库,会在程序执行时候,加载到内存这个区域,一般包括数据(data)和代码(text);通过mmap系统调用,可以把特定的文件映射到内存中,然后在相应的内存区域中操作字节来访问文件内容,实现更高效的IO操作;匿名映射,在glibc中malloc分配大内存的时候会用到匿名映射。这里所谓的“大”表示是超过了MMAP_THRESHOLD 设置的字节数,它的缺省值是 128 kB,可以通过 mallopt() 去调整这个设置值。还可以用于进程间通信IPC(共享内存)。
  • heap堆区:主要用于用户动态内存分配,空间大,使用灵活,但需要用户自己管理,通过brk系统调用控制堆的生长,向高地址生长。
  • BBS段和DATA段:用于存放程序全局数据和静态数据,一般未初始化的放在BSS段(统一初始化为0,不占程序文件的空间),初始化的放在data段,只读数据放在rodata段(常量存储区)。
  • text段:主要存放程序二进制代码。
  1. 谈谈new、delete、malloc、free[9]

共同点:

  • 都是从堆上申请空间,并且需要用户手动释放。

不同点:

  • malloc和free是函数,new和delete是操作符
  • malloc申请的空间不会初始化,new可以初始化
  • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可。
  • malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。
  • malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
  • 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。
  1. delete和delete[]的区别
    delete只会调用一次析构函数,而delete []会调用每一个成员的析构函数。
  2. 简述 strcpy、sprintf 与 memcpy 的区别
    操作对象不同,strcpy 的两个操作对象均为字符串; sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串; memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  3. C++的空类有哪些成员函数
    一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建。所有这些函数都是inline和public的。
  4. 构造函数为什么不能是虚函数[10]
  • 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。
  • 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
  1. 深拷贝和浅拷贝的区别
  • 浅拷贝: 与拷贝对象共享同一片内存。只复制对象的基本类型,对象类型,仍属于原来的引用。
  • 深拷贝: 申请新的内存,并将目标对象复制到新的内存。
  1. 知道STL吗,挑两个你最常用的容器说一说

容器分为两大类: 顺序容器和关联容器

  • 顺序容器
    顺序容器有以下三种:可变长动态数组 vector、双端队列 deque、双向链表 list。
    之所以被称为顺序容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置(尾部、头部或中间某处)插入,元素就会位于什么位置。
    vector:
    vector 实现的是一个动态数组,单向开口的连续性空间,支持内部元素随机访问,能够自动根据需要动态扩容。[11]
    deuque:
    deque是双向开口的连续线性空间,支持内部元素的随机访问。 擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。且支持头部添加和删除元素的成员。[12]
    list:
    双向链表容器,即该容器的底层是以双向链表的形式实现的。非连续空间、通过指针来连接每一个小空间、插入和删除都是O(1)操作,元素访问效率较低等等,不支持随机访问。
  • 关联容器
    关联容器有以下四种:set、multiset、map、multimap。关联容器内的元素是排序的。插入元素时,容器会按一定的排序规则将元素放到适当的位置上,因此插入元素时不能指定位置。
    set:
    含有 Key 类型对象的已排序集。用比较函数 比较 (Compare) 进行排序。搜索、移除和插入拥有对数复杂度。 set 通常以红黑树实现。[13]
    multiset:
    是排序好的集合(元素已经进行了排序),并且允许有相同的元素。[14]
    map:
    是有序键值对容器,它的元素的键是唯一的。[15]
    multimap:multimap也是存储两个元素之间的映射关系的容器,不相同的是,multimap的key值可以重复出现。[16]
  1. STL中的vector的实现,是怎么扩容的
    vector 为空的时候没有预分配空间,每次添加一个元素时,会判断当前是否还有剩余可用空间,如果没有则进行试探性扩容,并且把内存拷贝到新申请的内存空间上,并且释放原先的内存。
  2. C++中vector和list的区别
    vector是动态数组,内部存储是一片连续性的空间,支持通过下标随机访问。
    list是双向链表,内部存储可能是不连续的空间,通过指针链接这些不连续的空间,不支持随机访问。
  3. 怎么确定一个程序是C编译的还是C++编译的
    如果编译器在编译cpp文件那么__cplusplus就会被定义   如果是一个c文件在被编译那么__STDC__就会被定义。
  4. 说一下什么是内存泄漏,如何避免
    是指程序在申请内存后,无法释放已申请的内存空间,称之为内存泄露。无法释放的内存会一直被无效占用,且无法被再次使用,累计下来会导致进程占用内存越来越大,直至无内存资源可用,导致进程崩溃。
  5. C++中内存泄漏的几种情况[17]
  • 在类的构造函数和析构函数中没有匹配的调用new和delete函数
  • 没有正确地清除嵌套的对象指针
  • 在释放对象数组时在delete中没有使用方括号
  • 指向对象的指针数组不等同于对象数组
  • 缺少拷贝构造函数
  • 缺少重载赋值运算符
  • 没有将基类的析构函数定义为虚函数
  1. 一个文件从源码到可执行文件所经历的过程
    ① 预处理,产生.ii文件
    ② 编译,产生汇编文件 (.s文件)
    ③ 汇编,产生目标文件 (.o或.obj文件)
    ④ 链接,产生可执行文件
  2. 聊聊C++11新特性[18]
  3. C++构造函数和析构函数的调用顺序[19]
    构造函数顺序: 基类构造函数、对象成员构造函数、派生类本身的构造函数。
    析构函数顺序: 派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好相反)。
  4. 用 C++设计一个不能被继承的类
    将自身构造函数和析构函数声明为private。
  5. 什么是纯虚函数
    基类中声明的虚函数,仅有声明无实现。要求派生的子类必须定义自身的实现方法,达到多态效果。
  6. 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因[20]
  • 构造函数不能声明为虚函数
    因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
    虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
  • 析构函数最好声明为虚函数
    首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
    如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
  1. 析构函数的作用
    对象消亡时,自动被调用,用来释放对象占用的空间。
  2. 栈和堆的区别,什么时候必须使用堆
  • 栈:为函数分配的一块内存,函数内部声明的所有局部变量都将占用栈内存。函数执行完毕后,占用的栈会被销毁回收,内部定义的变量均会销毁。效率很高,但是分配的内存容量有限。
  • 堆:程序中未使用的内存,在程序运行时可用于动态分配内存。由程序员自己维护申请和回收,若使用完毕未回收会导致内存泄漏。
  1. 栈溢出(stack overflow)的原因以及解决方法[21]
  • 栈溢出原因
    ① 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。
    ② 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
    ③ 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
  • 解决方法
    增大栈空间; 变量过大时,改用动态分配,使用堆(heap)而不是栈(stack)。
  1. 编码实现某一变量某位清 0 或置 1
// 将第index为置1. index: 0~7
unsigned char set_bit(unsigned char value, int index)
{
    return value | (1 << index);
}
//将第index位清0, index: 0~7
unsigned char clear_bit(unsigned char value, int index)
{
    return value & (~(1 << index));
}
  1. 头文件<>和""的区别
  • <>: 表示引用标准库头文件,编译器会从系统默认库环境路径查找。
  • "": 一般表示用户自己定义使用的头文件,编译器默认会优先从当前路径中寻找;若未找到,再从系统默认库环境路径中去查找。

注:linux下C和C++默认库环境路径:/usr/include

  1. 静态绑定和动态绑定的介绍

把一个方法与其所在的类/对象关联起来叫做方法的绑定。[22]

静态类型:对象在声明时采用的类型,在编译期既已确定。

动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的。

静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期。

动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期。

非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)

  1. 引用是否能实现动态绑定,为什么引用可以实现
    可以。引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。
  2. 什么情况下会调用拷贝构造函数(三种情况)
    拷贝构造函数从来不显示调用,而是由编译器隐式地调用。
    ① 用类的一个对象去初始化另一个对象时;
    ② 当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用;
    ③ 当函数的返回值是类的对象或引用时。
  3. extern "C"作用
    extern "C" 的 主要 作用 就 是 为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,编译器会按照C语言语法进行编译。
  4. typedef和define的区别

typedef和define都是替一个对象取一个别名,以此增强程序的可读性。[23]

①作用阶段不同

typedef: 编译阶段有效, 有类型检查的功能。

define: 预处理阶段有效, 编译前, 只进行简单而机械的字符串替换, 不进行任何检查。

② 功能不同

typedef: 用来定义类型(内部或自定义类型)的别名, 起到使类型易于记忆的功能。

define: 不止可以为类型取别名, 还可以定义常量, 变量, 编译开关等。

作用域不同[24]

typedef: 与变量生命期类似。放在函数外,作用域从定义开始到文件尾;若放在函数内,定义域从定义处到该函数结尾。

define 没有作用域的限制,从定义开始到文件结尾。

//源自https://www.cnblogs.com/retry/p/14045305.html
//a.c
 typedef …//此处开始到文件结尾
 #define …//此处开始到文件结尾
 int negate(int num)
 {
     typedef …//此处开始到该函数结束。注意,该函数内,此定义前,也不能用
     #define …//此处开始到文件结尾
 }
 typedef …//此处开始到文件结尾
 #define …//此处开始到文件结尾
 void show()
 {
     typedef …//此处开始到该函数结束。
     #define …//此处开始到文件结尾
 }

④ 对指针的操作:

二者修饰指针类型时, 作用不同。

Typedef int * pint;  
#define PINT int *  
const pint p;//p不可更改,p指向的内容可以更改,相当于 int * const p;  
const PINT p;//p可以更改,p指向的内容不能更改,相当于 const int *p;或 int const *p;  
pint s1, s2; //s1和s2都是int型指针  
PINT s3, s4; //相当于int * s3,s4;只有一个是指针。
  1. 友元函数和友元类[25]
  • 友元函数
    类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
  • 友元类
    在类A中,用friend修饰另一个已经定义的类B,类B就属于类A的友元类,类B中的所有成员函数都是类A的友元函数,类B可以访问类A的所有成员,包括 publicprotectedprivate 属性的。
  1. 提高c++性能,你用过哪些方式去提升[26]
  • 空间足够时,可以将经常需要读取的资源,缓存在内存中。
  • 尽量减少大内存对象的构造与析构,考虑缓存暂时不用的对象,等待后续继续使用。
  • 尽量使用C++11的右值语义,减少临时对象的构造。
  • 简单的功能函数可以使用内联。少用继承,多用组合,尽量减少继承层级。
  • 在循环遍历时,优化判断条件,减少循环次数。
  • 优化线程或进程的同步方式,能用原子操作的就不用锁。能应用层同步的就不用内核对象同步。
  • 优化堆内存的使用,如果有内存频繁的申请与释放,可以考虑内存池。
  • 优化线程的使用,节省系统资源与切换造成的性能损耗,线程使用频繁的可以考虑线程池。
  • 尽量使用事件通知,谨慎使用轮循或者sleep函数。
  • 界面开发中,耗时的业务代码不要放在UI线程中执行,使用单独的线程去异步处理耗时业务,提高界面响应速度。
  • 经常重构、优化代码结构。优化算法或者架构,从设计层面进行性能的优化。

二、计算机网络编程

  1. select、poll、epoll的区别[27]

select、poll、epoll是多路复用主要用到的三种技术,主要区别如下:

① 支持一个进程所能打开的最大连接数

select

  单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll

  poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll

  虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

② FD剧增后带来的IO效率问题

select

  因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll

  同上。

epoll

  因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

③ 消息传递方式

select

  内核需要将消息传递到用户空间,都需要内核拷贝动作。

poll

  同上。

epoll

  epoll通过内核和用户空间共享一块内存来实现的。

  1. TCP和UDP的区别[28]
  • TCP是面向连接的,UDP是无连接的
  • TCP是可靠的,UDP是不可靠的
  • TCP是面向字节流的,UDP是面向数据报文的
  • TCP只支持点对点通信,UDP支持一对一,一对多,多对多
  • TCP报文首部20个字节,UDP首部8个字节
  • TCP有拥塞控制机制,UDP没有
  • TCP协议下双方发送接受缓冲区都有,UDP并无实际意义上的发送缓冲区,但是存在接受缓冲区
  1. TCP三次握手[29]

① 第一次握手

客户端给服务器发送一个SYN段(在 TCP 标头中 SYN 位字段为 1 的 TCP/IP 数据包), 该段中也包含客户端的初始序列号(Sequence number = J)。

② 第二次握手

服务器返回客户端 SYN +ACK 段(在 TCP 标头中SYN和ACK位字段都为 1 的 TCP/IP 数据包), 该段中包含服务器的初始序列号(Sequence number = K);同时使 Acknowledgment number = J + 1来表示确认已收到客户端的 SYN段(Sequence number = J)。

③ 第三次握手

客户端给服务器响应一个ACK段(在 TCP 标头中 ACK 位字段为 1 的 TCP/IP 数据包), 该段中使 Acknowledgment number = K + 1来表示确认已收到服务器的 SYN段(Sequence number = K)。

  1. 三次握手的原因[30]

  为了实现可靠数据传输,TCP 协议的通信双方,都必须维护一个序列号,以标识发送出去的数据包中,哪些是已经被对方收到的。三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤。

  1. TCP四次挥手[31]

① 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。

② 第二次挥手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。

③ 第三次挥手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。

④ 第四次挥手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段后,关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,Client端也关闭连接。

  1. 四次挥手的原因[32]

其实是客户端和服务端的两次挥手,也就是客户端和服务端分别释放连接的过程。客户端在发送完最后一次确认之后,还要等待2MSL的时间。有两个原因: 一是为了让B能够按照正常步骤进入CLOSED状态,二是为了防止已经失效的请求连接报文出现在下次连接中。

① 由于客户端最后一个ACK可能会丢失,这样B就无法正常进入CLOSED状态。于是B会重传请求释放的报文,而此时A如果已经关闭了,那就收不到B的重传请求,就会导致B不能正常释放。而如果A还在等待时间内,就会收到B的重传,然后进行应答,这样B就可以进入CLOSED状态了。

② 在这2MSL等待时间里面,本次连接的所有的报文都已经从网络中消失,从而不会出现在下次连接中。

  1. 编写socket套接字的步骤
  • 服务器端程序的编写步骤
    ① 调用socket()函数创建一个用于通信的套接字。
    ② 第二步:给已经创建的套接字绑定一个端口号,这一般通过设置网络套接口地址和调用bind()函数来实现。
    ③ 调用listen()函数使套接字成为一个监听套接字。
    ④ 调用accept()函数来接受客户端的连接,这是就可以和客户端通信了。
    ⑤ 处理客户端的连接请求。
    ⑥ 终止连接。
  • 客户端程序编写步骤
    ① 调用socket()函数创建一个用于通信的套接字。
    ② 通过设置套接字地址结构,说明客户端与之通信的服务器的IP地址和端口号。
    ③ 调用connect()函数来建立与服务器的连接。
    ④ 调用读写函数发送或者接收数据。
    ⑤ 终止连接。

三、操作系统

  1. 如何不用sizeof()判断系统是16位还是32位?
    可通过内存地址地址长度判断,定义一个指针变量,通过打印其地址即可判断。
  2. 进程和线程间的通信方式
    匿名管道(pipe)、高级管道(popen)、有名管道(fifo)、消息队列、信号量、信号(sinal)、共享内存、套接字(socket)。
  3. 线程安全和线程不安全
    线程安全指支持多线程访问,多线程访问时不会导致共享数据污染。反之,为线程不安全。
  4. 死锁产生的原因和死锁的条件[33]
  • 死锁的定义
      两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
  • 死锁的产生原因
    通常是源于多个进程对资源的争夺,不仅对不可抢占资源进行争夺或引起死锁,而且对可消耗资源进行争夺也会引起死锁。总结如下:
    ① 系统资源不足;
    ② 进程运行推进的顺序不当;
    ③ 资源分配不当;
  • 死锁产生的必要条件
    ① 互斥条件:
    进程在运行中对资源进行排他性使用,即一个资源仅能被一个进程使用,此时其他进程请求资源时,只能等待其释放。
    ② 请求与保持条件:
    某进程已经保持了一个资源,但又请求另一个资源,若该资源被其他进程占有,此时请求阻塞,且对已经占有的资源不释放;
    ③ 不可抢占条件:
    进程获得的资源在未使用完时不可被抢占,只能在进程使用完时自己释放;
    ④ 循环等待条件
    发生死锁时,必然存在这样一个循环,一个进程p1等待p2占有的资源进程p2等待p3占有的资源...进程pn等待p1占有的资源。
  • 死锁的处理方法
    ① 预防死锁:事先预防策略,容易实现,通过实现设置限制,破坏产生死锁的四个条件之一。(如对资源采用按序分配策略)
    ② 避免死锁:事先预防策略,在资源的动态分配过程中,用某些方法防止系统禁图不安全状态。常见的方法有银行家算法。
    ③ 检测死锁:通过检测机构等及时检测出死锁,采取适当措施,把进程从死锁中解脱。
    ④ 解除死锁:检测出死锁后,采取措施解决。比如剥夺资源,撤销进程。
    这四种方法对死锁的防范逐渐减弱,但对应的是资源利用率的提高。
  1. 如何采用单线程处理高并发
    采用非阻塞,异步编程的思想。
    ① IO多路复用技术
    ② 采用事件驱动模型,基于异步回调来处理事件
  2. 线程的状态
    运行期、挂起、死亡、正常退出、和线程阻塞。
  3. 进程的状态
    ① 运行(running)态:进程占有处理器正在运行。
    ② 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行。
    ③ 等待(wait)态:又称为阻塞(blocked)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。
  4. 系统调用brk和mmap[34]

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

  • brk是将数据段(.data)的最高地址指针_edata往高地址推;
  • mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

  1. 进程和线程的区别
    ① 地址空间
    线程共享本进程的地址空间,而进程之间是独立的地址空间。
    ② 资源
    线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
    ③ 健壮性
    多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
    ④ 执行过程
    每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。
    但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
    ⑤ 可并发性
    两者均可并发执行。
    ⑥ 切换时
    进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
    ⑦ 其他
    线程是处理器调度的基本单位,但是进程不是。
  2. 工作常用到的Linux命令
    find、grep、ps、top、ls(比较简单,不展开)
  3. gdb
    参考网上gdb常用的调试手法。
  4. gcc/g++
    C和C++的编译工具。
  5. 什么是虚拟内存
    《操作系统》:虚拟存储技术的基本思想时利用大容量外存来扩充内存,产生一个比有限的实际内存空间大得多的、逻辑的虚拟空间,简称虚存,以便能够有效地支持多道程序系统的实现和大型程序运行的需要,从而增强系统的处理能力。
  6. 常见的内存管理机制[35]

简单的分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,比如块式管理。非连续分配管理方式允许一个程序内存分散,比如页式管理、段式管理和段页式管理。

  • 块式管理
    远古时代的计算机操作系统的内存管理方式,将内存分为几个固定大小的块,每个块只包含一个进程,如果程序运行需要内存,操作系统就给它分配一块,如果程序运行只需要很小的空间,则分配的这块内存很大一部分就浪费了,这些在每个块中未被利用的空间,我们称为碎片。
  • 页式管理
    把主存分为大小相等且固定的一页一页的形式,页比较小,相对于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
  • 段式管理
    页式管理虽然提高了内存利用率,但是其中的页没有任何实际意义,段式管理把主存分为一段一段 ,每一段的空间又比一页的空间小很多。但是段有实际意义,段式管理通过段表对应逻辑地址和物理地址。
  • 段页式管理
    段页式管理机制结合了段式管理和页式管理的优点,就是把主存分为若干段,每个段又分为若干页,也就是说段页式管理机制中段和段之间以及段的内部都是离散的。
  1. 大端和小端,用C++代码怎么确定
  • 指针法
int JudgeSystem(void) {
    int a = 1;
    char *p = (char *)&a;
 
    // 如果是小端则返回 1,如果是大端则返回 0
    return *p;
}
  • 联合体法
int JudgeSystem(void) {
    union {
        char c;
        int i;
    } un; // 匿名联合体 un
 
    un.i = 1;
 
    // 如果是小端则返回 1,如果是大端则返回 0
    return un.c;
}

四、数据结构及算法

  1. 几种常见的排序算法[36]
    ① 冒泡排序
    ② 选择排序
    ③ 插入排序
    ④ 希尔排序
    ⑤ 快速排序
    ⑥ 归并排序
    ⑦ 堆排序
  2. 链表的特点和操作
  • 特点
    ① 采用动态存储分配,不会造成内存浪费和溢出。
    ② 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素。
  • 常见操作
    ① 创建
    ② 插入
    ③ 修改
    ④ 查找
    ⑤ 删除
  1. 常见的查找算法
    ① 顺序查找
    ② 二分查找
    ③ 插值查找
    ④ 斐波那契查找
    ⑤ 树表查找
    ⑥ 分块查找
    ⑦ 哈希查找
  2. 如何判断链表是否为环形[37]

通过快慢指针。慢指针每次走1步,快指针每次走2步。当快慢指针相遇,说明链表为循环链表。

bool IsRingList(SNode* pHead)
{
    bool ret = false;
    SNode *pSlow = pHead, *pFast = pHead;
    while(pFast && pFast->next)
    {
        pSlow = pSlow->next;
        pFast = pFast->next->next;
        if (pSlow == pFast) {
            return true;
        }
    }
    
    return ret;
}
  1. 计算环形链表的长度

通过快慢指针。慢指针每次走1步,快指针每次走2步,两者速度差为1步。 当两指针相遇时,快指针比慢指针多走1圈。

int GetRingListLength(SNode* pHead)
{
    int length = 0;
    SNode *pSlow = pHead, *pFast = pHead;
 
    while(pFast && pFast->next)
    {
        pSlow = pSlow->next;
        pFast = pFast->next->next;
        length++;
        if (pSlow == pFast) {
            break;
        }
    }
    if (pSlow != pFast)
    {
        length = 0;
        printf("It is not ring list.\n");
    }
    return length;
}
  1. 查找单链表倒数第n个节点

通过快慢指针,快指针先走n步,随后慢指针与快指针同步走,当快指针到达链表尾部时,慢指针即为倒数第n个节点。

参考

[1]

C++四种强制类型转换]: https://zhuanlan.zhihu.com/p/258975506

[2]

static的作用: https://www.cnblogs.com/lit10050528/p/3910271.html

[3]

C++ const关键字小结: https://www.runoob.com/w3cnote/cpp-const-keyword.html

[4]

#define和const的区别: https://blog.csdn.net/yi_ming_he/article/details/70405364

[5]

C++中指针与引用的区别: https://zhuanlan.zhihu.com/p/140966943

[6]

C++中指针常量和常量指针的区别: https://www.cnblogs.com/lizhenghn/p/3630405.html

[7]

将“引用”作为函数返回值类型的格式、好处和需要遵守的规则: https://blog.csdn.net/weibo1230123/article/details/82014538

[8]

C++内存管理全景指南|C++硬核指南: https://zhuanlan.zhihu.com/p/368901281

[9]

C++——malloc/free和new/delete的区别: https://blog.csdn.net/WEIYANGBIN/article/details/109699674

[10]

为什么构造函数不能声明为虚函数,析构函数可以: https://www.cnblogs.com/lpxblog/p/5890933.html

[11]

C++打怪 之 vector: https://mp.weixin.qq.com/s?__biz=MzUzNjExNjMyNA==&mid=2247484576&idx=1&sn=c55235b7632bb18c05bcebb02436d07e&chksm=fafa69abcd8de0bd514592592c65730590ab00ad8f48b0de3dc0b7ce809d4ee5a45e1509e1dc&token=1237879194&lang=zh_CN#rd

[12]

从零开始,打造自己的STL(五、deque): https://zhuanlan.zhihu.com/p/35087078

[13]

std::set API参考文档: https://www.apiref.com/cpp-zh/cpp/container/set.html

[14]

C++ multiset,STL multiset详解: http://c.biancheng.net/view/386.html

[15]

std::map API参考文档: https://www.apiref.com/cpp-zh/cpp/container/map.html

[16]

C++ STL multimap容器用法完全攻略(超详细): http://c.biancheng.net/view/7190.html

[17]

C++中内存泄漏的几种情况: https://www.cnblogs.com/liushui-sky/p/7727865.html

[18]

c++11新特性,所有知识点都在这了: https://zhuanlan.zhihu.com/p/139515439

[19]

C++构造函数和析构函数的调用顺序: https://blog.csdn.net/xyzyhs/article/details/82356948

[20]

构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因: https://blog.csdn.net/libaineu2004/article/details/85684224

[21]

栈溢出几种情况及解决方案: https://blog.csdn.net/Beyond_2016/article/details/81286223

[22]

C++中的静态绑定和动态绑定: https://www.cnblogs.com/lizhenghn/p/3657717.html

[23]

define 和 typedef 区别: https://www.jianshu.com/p/53ac91a23979

[24]

#define与typedef作用域: https://www.cnblogs.com/retry/p/14045305.html

[25]

C++友元函数和友元类(C++ friend关键字): http://c.biancheng.net/view/2233.html

[26]

C++程序性能优化指南: https://segmentfault.com/a/1190000039136866

[27]

select、poll、epoll之间的区别(搜狗面试): https://www.cnblogs.com/aspirant/p/9166944.html

[28]

TCP和UDP的区别: https://blog.csdn.net/weixin_43796685/article/details/104558965

[29]

TCP三次握手详解-深入浅出(有图实例演示): https://blog.csdn.net/jun2016425/article/details/81506353

[30]

TCP 为什么三次握手而不是两次握手(正解版): https://blog.csdn.net/lengxiao1993/article/details/82771768

[31]

一文彻底搞懂 TCP三次握手、四次挥手过程及原理: https://www.cnblogs.com/cooffeeli/p/TCP_Establish_Connection_Close_Connection.html

[32]

TCP四次挥手及原因: https://www.cnblogs.com/GuoXinxin/p/11657933.html

[33]

死锁的定义、产生原因、必要条件和处理方法: https://blog.csdn.net/zymx14/article/details/78046830

[34]

操作系统--brk()和mmap()详解: https://blog.csdn.net/shuzishij/article/details/86574927

[35]

操作系统常见的几种内存管理机制: https://blog.csdn.net/qq_38197844/article/details/108691639

[36]

常见的7种排序算法: https://blog.csdn.net/liang_gu/article/details/80627548

[37]

链表常见操作和15道常见面试题: https://zhuanlan.zhihu.com/p/265159396

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
1月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
89 0
|
1月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
73 1
Linux C/C++之IO多路复用(aio)
|
22天前
|
Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
22天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
22天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
25天前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
24 3
|
26天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
23 2
|
26天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
46 1
|
26天前
|
Android开发
Android面试高频知识点(1) 图解 Android 事件分发机制
Android面试高频知识点(1) 图解 Android 事件分发机制
37 1
|
1月前
|
XML 前端开发 Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制