面试总结


本文分为四个部分。c++面试常问题,计算机网络常问题和操作系统常问题,以及自己找工作的过程。

c++面经

多态

1. 谈谈你对多态的理解?

多态,就是一个对象在不同的时刻体现出来的不同的状态。
在c++中,多态可以分为静态多态和动态多态。静态多态是通过重载和模板实现的,动态多态是通过虚函数实现的。

c++中的动态多态,简单的说,就是在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时,将会根据指针的类型来调用相应的函数。这样就实现了在运行时,根据不同的对象来调用不同的函数。若指针所指的对象是基类,就调用基类中的函数,若指针所指的对象是派生类,就调用派生类中的函数。多态的实现基于虚函数,虚函数的实现基于虚函数表。

实现多态的过程:编译器在发现基类中有virtual关键字时,会为含有虚函数的类生成一份虚函数表,虚函数表是一个一维数组,在虚函数表中保存了虚函数的入口地址。编译器会在每个对象的前四个字节中保存一个虚表指针,指向对象所属类中的虚表,在构造时,根据对象的类型去初始化虚表指针,这样就实现了在运行时,根据不同的对象来调用不同的函数。派生类的构造函数晚于基类的构造函数,在初始化一个派生类的对象时,会先调用基类的构造函数,在此时,编译器将虚表指针先指向基类的虚表,然后再调用派生类的构造函数,此时,虚表指针指向派生类的虚表,这样就实现了在运行时,根据不同的对象来调用不同的函数。

c++的多态是通过动态绑定来实现的,含有虚函数的类都有自己的虚表。当派生类没有对基类中的虚函数进行重写时,派生类中的虚表指针保存的是父类的虚表,当派生类中自己含有虚函数时,在虚表中将此虚函数的地址添加在后边。

2. 什么是虚函数?什么是纯虚函数?为什么有了虚函数还要有纯虚函数?

virtual关键字修饰的类非静态成员函数称为虚函数。只有类的非静态成员函数可以加virtual关键字,普通函数不能加virtual关键字。

纯虚函数是没有函数体的虚函数。纯虚函数在基类中被定义,但是在基类中并不实现,在所有的派生类中必须实现。

为了实现多态,我们常常需要在基类中定义虚拟函数,但是,在很多情况下,基类本身生成对象不合情理,因此,我们需要将基类中的虚函数定义为纯虚函数,这样,基类就不能生成对象了,只能作为接口使用。定义纯虚函数的目的就在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,”你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

3. 什么是虚函数表?虚函数表是怎么实现的?

虚函数表是编译器在处理虚函数时引入的一个新成员。编译器在处理虚函数时,给每个对象都添加了一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这个保存函数地址的数组称为虚函数表,虚函数表中存储了为类成员进行声明的虚函数的地址。

虚函数表在编译器进行编译时创造,且编译器会在构造函数中插入一段代码,用来给虚指针进行赋值。虚函数表放在全局数据区,虚指针放在对象的内存中。

4. 析构函数为什么要声明为虚函数?

如果基类的析构函数不是虚函数,那么在删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这样就会造成派生类中的成员没有被释放,造成内存泄漏。

继承

1. 什么是虚继承?虚继承的实现原理是什么?

虚继承是指在派生类中,将多个基类的同一个成员变量视为一个变量,而不是多个变量。虚继承的实现原理是,编译器在派生类中为虚继承的基类添加一个虚基类指针,指向虚基类的对象。

c++11

智能指针

智能指针出现的原因,是为了帮助程序员管理使用new运算符在堆区开辟的空间,在需要释放的时候及时的释放,避免造成内存泄露。最早,在c++98标准中就已经有了智能指针的雏形auto_ptr。但是,auto_ptr存在一些问题,比如不能拷贝,不能赋值,不能作为函数的返回值等,因此,c++11标准中引入了智能指针的新标准shared_ptrweak_ptrunique_ptr

智能指针的实现原理是,智能指针是一个类,类中有一个指针,指向堆中的对象,当智能指针被销毁时,会调用析构函数,析构函数中会调用delete,从而释放堆中的对象。

要创建智能指针对象,必须在头文件中包含memory头文件,然后使用std::shared_ptr或者std::unique_ptr来创建智能指针对象。

shared_ptr:
shared_ptr是取代auto_ptr的智能指针,它的特点是可以共享对象,即多个shared_ptr可以指向同一个对象,只有当最后一个shared_ptr过期时,对象才会被释放。shared_ptr的实现原理是,shared_ptr中有一个指针,指向堆中的对象,还有一个引用计数,每次创建一个shared_ptr对象,引用计数就加1,每次销毁一个shared_ptr对象,引用计数就减1,当引用计数为0时,这些指针指向的对象就会被释放。
shared_ptr的使用方法如下:

// 创建
std::shared_ptr<int> p1; // 默认初始化,p1为空指针
std::shared_ptr<int> p2(new int(42)); // 用new创建对象
std::shared_ptr<int> p3(p2); // 用p2初始化p3,引用计数加1

// 使用make_shared创建
std::shared_ptr<int> p4 = std::make_shared<int>(42);

// 重置
p1.reset(new int(42)); // 重置p1,引用计数加1 p1指向的对象的引用计数减1
p1 = p2; // 重置p1,引用计数加1 p1指向的对象的引用计数减1

// 自定义删除器 在使用静态数组时,默认的删除器不支持释放静态数组,需要自定义删除器

auto del = [](int* p) {
    std::cout << "delete" << std::endl;
    delete []p;
};
std::shared_ptr<int> p5(new int[10], del);

// 常用函数
p1.use_count(); // 返回引用计数
p1.unique(); // 返回引用计数是否为1
p1.get(); // 返回指针  不建议使用,使用get()返回指针,若提前手动释放了指针,那么p1指向的对象就会被释放两次,造成错误

相较于auto_ptrshared_ptr加上了引用计数的概念。引用计数部分的解决了重复释放可能存在的悬空指针的问题。但是,事实上,还是存在一些问题,比如循环引用的问题,比如下面的代码:

#include <iostream>
#include <memory>
class B;
class A{
public:
    A() {
        std::cout << "A()" << std::endl;
    }
    ~A() {
        std::cout << "~A()" << std::endl;
    }
    std::shared_ptr<B> b;
};

class B {
public:
    B() {
        std::cout << "B()" << std::endl;
    }
    ~B() {
        std::cout << "~B()" << std::endl;
    }
    std::shared_ptr<A> a;
};

int main() {
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->b = b;
    b->a = a;
    return 0;
}

/*
A()
B()
*/

下边这段代码会造成内存泄漏,在main函数退出之前,ab的引用计数都是2,在退出时,首先销毁A对象,此时A对象的引用计数减1,变成1,然后销毁B对象,此时B对象的引用计数减1,变成1。但是,A对象和B对象都没有被释放,因为它们的引用计数都不为0。
为了解决这个问题,我们可以使用weak_ptr:

weak_ptr:
weak_ptr是一种用于解决shared_ptr相互引用时产生死锁问题的智能指针。如果有两个shared_ptr相互引用,那么这两个shared_ptr指针的引用计数永远不会下降为0,资源永远不会释放。weak_ptr是对对象的一种弱引用,它不会增加对象的use_countweak_ptrshared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptrweak_ptr也可以通过调用lock函数来获得shared_ptr
weak_ptr指针通常不单独使用,只能和 shared_ptr 类型指针搭配使用。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
weak_ptr并没有重载operator->operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。

#include <iostream>
#include <memory>
class B;
class A{
public:
    A() {
        std::cout << "A()" << std::endl;
    }
    ~A() {
        std::cout << "~A()" << std::endl;
    }
    std::shared_ptr<B> b;
};

class B {
public:
    B() {
        std::cout << "B()" << std::endl;
    }
    ~B() {
        std::cout << "~B()" << std::endl;
    }
    std::weak_ptr<A> a;
};

int main() {
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->b = b;
    b->a = a;
    return 0;
}
/*
A()
B()
~A()
~B()
*/

除此之外,使用shared_ptr还可能出现的问题是,同一内存被多次引用。

class A{
public:
    A(int a) {
        this->a_ = a;
        std::cout << "A()" << std::endl;
    }
    ~A() {
        std::cout << "~A()" << std::endl;
    }
    void addToGroup(vector<shared_ptr<A>> &group) {
        group.push_back(shared_ptr<A>(this));
    }
private:
    int a_;
};

在每次调用addToGroup函数时,都会创建一个新的shared_ptr对象,指向A对象,然后将这个shared_ptr对象添加到group中。这样,group中就会有多个指向同一个A对象的shared_ptr对象,这样就会造成同一内存被多次引用,最终这个对象会被多次删除,导致运行出错。本质上,这个例子警告我们,不要使用同一个raw pointer创建多个shared_ptr对象。
解决这个问题的方法是,将std::enable_shared_from_this作为基类:

class A: public std::enable_shared_from_this<A> {
public:
    A(int a) {
        this->a_ = a;
        std::cout << "A()" << std::endl;
    }
    ~A() {
        std::cout << "~A()" << std::endl;
    }
    void addToGroup(vector<shared_ptr<A>> &group) {
        group.push_back(shared_from_this());
    }
private:
    int a_;
};

除此之外,shared_ptr并不是线程安全的。shared_ptr的引用计数本身是原子操作,但是数据成员的读写不是,shared_ptr的两个数据成员px(指向对象)和pn(指向引用计数)都是非原子操作,所以在多线程环境下,shared_ptr可能是不安全的。但是这种场景可能很少见:

// 假设三个智能指针
shared_ptr<int> p1; // 线程A的局部变量
shared_ptr<int> p2(new int(10)); // 线程A和线程B共享的变量
shared_ptr<int> p3(new int(10)); // 线程B的局部变量

// 线程A
p1 = p2;
// 线程B
p2 = p3;
// 在执行这条语句时,首先改变ptr的朝向,再修改引用计数,因为现在是多线程,可能出现的问题是,线程B在这两个操作之间修改了p2->px的值,导致p1->px的值不正确。
// 还有可能出现的问题是,线程B在这两个操作之间修改了p2->pn的值,此时,p2->pn = 0,对象被释放,这时p1就会变成悬空指针

// 使用make_shared可以解决这个问题吗
// 不可以,make_shared的实现原理是,先在堆中分配内存,然后再调用构造函数,这两个操作不是原子操作,所以也会出现这个问题。

所以,要在多线程的环境下使用shared_ptr,需要对其进行加锁。

unique_ptr:
unique_ptr是C++11中新增的智能指针,它的特点是,它是独占式的,即一个unique_ptr对象只能指向一个对象,不能指向多个对象。unique_ptr的实现原理是,unique_ptr中有一个指针,指向堆中的对象,当unique_ptr对象被销毁时,他指向的对象也会被销毁。
unique_ptr的使用方法如下:

// 创建
unique_ptr<int> p(); // 空指针
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = make_unique<int>(10);
unique_ptr<int> p3 = p1; // 错误,不能拷贝
unique_ptr<int> p4 = move(p1); // 正确,可以移动

// 常用函数
p1.get(); // 返回指针
p1.release(); // 释放智能指针对当前对象的所有权,返回指针指向的地址   此时若没有其他智能指针指向该对象,该对象不会被释放会出现内存泄漏
p1.reset(p); // 释放智能指针的所有权,指针指向空(若p为空)或者新的地址(p)
p1.swap(p2); // 交换两个智能指针的所有权

lambda表达式

lambda表达式是C++11中新增的特性,它是一个匿名函数,可以作为参数传递给其他函数,也可以作为其他函数的返回值。lambda表达式的语法如下:

[capture](parameters) -> return_type {body}

用的很多很熟,不再赘述。

STL

vector

vector是C++中的一个容器,它是一个动态数组,可以根据需要动态的增加和删除元素。vector的实现原理是,vector中有一个指针,指向堆中的数组,当vector中的元素个数超过数组的大小时,会重新分配一块更大的内存(原内存大小的二倍),将原来的元素拷贝到新的内存中,然后释放原来的内存。

vector的缺点:增删元素时,需要重新分配内存,拷贝元素,效率较低。

vector删除元素时会发生什么?
vector删除元素有四种方式,removeerasepop_backclear。使用remove删除元素时,不改变容器的大小,将删除的目标元素的后边的元素向前移动,然后返回一个指向最后一个元素的迭代器,使用erase删除元素时,会改变容器的大小,将删除的目标元素的后边的元素向前移动,然后释放最后一个元素的内存,使用pop_back删除元素时,会改变容器的大小,释放最后一个元素的内存,使用clear删除元素时,会改变容器的大小,释放所有元素的内存。
所有的删除元素的方式都不会涉及到重新给容器分配空间,容器的capacity始终不变。

sort的底层是什么?
sort的底层是快速排序+堆排序/插入排序,第一种排序方式在递归深度超过排序元素数量的对数值之后转换为堆排序。第二种排序方式在排序元素数量较少时使用,当排序元素数量超过16时,转换为第一种排序方式。

unordered_map

unordered_map的元素是无序的。
unordered_map的底层实现是一个哈希表。
unordered_map的插入,查找时间复杂度都是$O(1)$。

map

map的元素是有序的。
map是STL的一个关联容器,map中的元素是关键字-值的对(key-value):关键字起到索引的作用,值则表示与索引相关联的数据。每个关键字只能在map中出现一次。map的底层实现是一个红黑树,自平衡的二叉搜索树。

语言律师

const和宏定义的区别

const定义的变量在编译阶段进行处理,宏定义的变量在预编译阶段进行处理。

编译器在预编译阶段会对源代码中的宏定义进行展开,提取其中的参数,并将他们替换成对应的值。

typedef的作用

typedef关键字的作用是为已有的类型定义一个新的名字,这个新的名字可以方便地使用,且具有更具描述性的名字。通过typedef,可以让程序更加易读,易于维护。

typedef int INT_8; // 根据编码规范,指明INT_8是一个8位的整型
INT_8 a = 10;
c++编译分为哪些阶段?
  1. 预编译阶段 进行一些简单的文本替换工作,将宏定义 define 和 条件编译指令 ifndef, ifdef, endif 等进行替换,将头文件中的内容替换到源文件中,将注释去掉等。(#.cpp -> #.i)
  2. 编译优化阶段 通过预编译输出的.i文件中,只有常量:数字、字符串、变量的定义,以及关键字:main、if、else、for、while等。这阶段要做的工作主要是,通过语法分析和词法分析,确定所有指令是否符合规则,之后翻译成汇编代码。(#.i -> #.s)
  3. 汇编阶段 汇编过程就是把汇编语言翻译成目标机器指令的过程,生成目标文件(.obj .o等)。目标文件中存放的也就是与源程序等效的目标的机器语言代码。(#.s -> #.o)
  4. 链接过程 链接过程就是把各个目标文件合并成一个可执行文件的过程。(#.o -> #.exe)

计算机网络面经

OSI七层模型

物理层: 底层数据传输,网线,网卡标准。
数据链路层:定义数据的基本格式,如何传输,如何标识。
网络层:定义IP编址,定义路由功能,如不同设备的数据转发。
传输层:端到端传输数据的基本功能,如TCP,UDP。
会话层:控制应用程序之间会话能力,将不同软件的数据分发给不同软件。
表示层:数据格式转换,加密解密,压缩解压缩。
应用层:应用程序,包括Web应用。

传输层的数据称为报文,网络层的数据称为包,数据链路层的数据称为帧,物理层的数据称为比特流。

网络七层模型是一个理论模型,而非实现。网络四层模型是实现模型,是实际的网络协议栈。

HTTP

1. 说一下一个完整的HTTP请求过程包含哪些内容?

建立起客户机与服务器的TCP连接,发送HTTP请求报文,服务器接收请求报文,服务器处理请求报文,服务器发送响应报文,客户端接收响应报文,客户端关闭TCP连接。

2. HTTP长连接和短连接的区别?

长连接:客户端和服务器建立连接后,不关闭连接,可以继续发送请求和接收响应。
短连接:客户端和服务器建立连接后,发送请求,接收响应,然后关闭连接。
长连接常常用于客户端和服务器之间需要频繁通信的场景,且连接数不太多的情况下;短连接常常用于客户端和服务器之间只需要一次通信的场景。

3. HTTP请求方法?

客户端发送的请求报文的第一行为请求行,其中包含了方法字段。
常见的方法有GET,POST,PUT,DELETE,HEAD,OPTIONS,TRACE,CONNECT。

序号 方法 描述
1 GET 请求获取Request-URI所标识的资源
2 HEAD 类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容
5 DELETE 请求服务器删除Request-URI所标识的资源
6 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
7 OPTIONS 允许客户端查看服务器的性能
8 TRACE 回显服务器收到的请求,主要用于测试或诊断
4. GET和POST的区别?

根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。——打开网页

根据 RFC 规范,POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。——给网页评论

GET方法是幂等的,POST方法不是幂等的。GET方法不会改变服务器端的资源,可以被缓存,POST方法会修改服务器端的资源,不可缓存。

5. 什么是HTTP?

HTTP是超文本传输协议,即hypertext transfer protocol,是一种用于分布式、协作式和超媒体信息系统的应用层协议。
HTTP是一个双向协议,浏览器是客户端,网站是服务端,双方约定使用HTTP协议进行通信。
HTTP常见的状态码:

  1. 1xx:提示信息,表示当前是协议处理的中间状态,还需后续的操作
  2. 2xx:成功,报文已经被收到且正确处理
  3. 3xx:重定向,提示资源位置发生变动,需要客户端重新发送请求
  4. 4xx:客户端错误,请求报文有误,服务器无法正常处理
  5. 5xx:服务端有误,服务器在处理请求时发生错误
6. 常见的HTTP字段?

Host字段:客户端发送请求时用来指定服务器的域名
Content—length字段:服务端在返回数据时表明本次回应的数据长度
Connection字段:常用于客户端要求服务器使用HTTP长连接机制,以便其他请求复用
Content-Type/Accept字段:content-type用于服务端在传输数据时告诉客户端,本次传输的是什么格式的数据,accept用于客户端发送请求时声明自己可以接收哪些类型的数据

7. HTTP缓存有哪些实现方式?
  1. 强制缓存
    强制缓存是指,只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动权在于浏览器。可以通过HTTP响应头中的Cache-Control来实现,具体流程如下:

    1. 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;
    2. 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;
    3. 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。
  2. 协商缓存
    协商缓存是指,通过服务器来告诉浏览器可以使用本地缓存的资源。协商缓存可以通过请求头部中的If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。
    除此之外,协商缓存可以通过请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段来实现,这两个字段的意思是:响应头部中 Etag:唯一标识响应资源;请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

协商缓存必须配合强制缓存中的Cache-control来使用,只有在未能命中强制缓存的基础上,才能发起带有协商缓存字段的请求。

协商缓存和强制缓存的工作流程

8. HTTP/1.1特性?

优点:

  1. 简单:HTTP 基本的报文格式就是 header + body,头部信息也是 key-value 简单文本的形式,易于理解,降低了学习和使用的门槛。
  2. 灵活,易于扩展:HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充。
  3. 应用广泛和跨平台

缺点:

  1. 无状态:因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。既然服务器没有记忆能力,它在完成有关联性的操作时会非常麻烦。例如登录->添加购物车->下单->结算->支付,这系列操作都要知道用户的身份才行。但服务器不知道这些请求是有关联的,每次都要问一遍身份信息。可以通过cookie技术来解决。
  2. 明文传输
  3. 不安全: HTTP 的安全问题,可以用 HTTPS 的方式解决,也就是通过引入 SSL/TLS 层,使得在安全上达到了极致。
9. HTTP/1.1的性能?

1.长连接

常常用于客户端和服务器之间需要频繁通信的场景早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无谓的 TCP 连接建立和断开,增加了通信开销。

为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。

持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

2.管道网络传输
HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。

即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。那么,管道机制则是允许浏览器同时发出 A 请求和 B 请求。

3.队头阻塞
请求 - 应答的模式会造成 HTTP 的性能问题。当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」,好比上班的路上塞车。

10. HTTPS和HTTP的区别?
  • HTTP是一种明文传输的协议,存在安全风险,HTTPS是一种加密传输的协议,可以有效的防止信息被窃取。
  • HTTP建立连接相对较为容易,TCP三次握手之后即可建立连接;HTTPS在TCP三次握手之后,还需要进行SSL握手,SSL握手需要消耗更多的时间。
  • HTTP的默认端口号为80,HTTPS的默认端口号为443。
  • HTTPS协议需要向CA申请数字证书,来保证服务器的身份是可信的。

HTTPS在HTTP和TCP层之间加入了SSL/TLS协议层,SSL/TLS协议层主要负责加密和解密,以及身份认证,信息校验等工作。
其中,信息的机密性通过混合加密来保证,信息的完整性通过摘要算法来保证,身份认证工作通过将服务器的公钥放进数字证书中来保证。
1.混合加密
混合加密是指在传输过程中,使用对称加密算法加密数据,使用非对称加密算法加密对称加密算法的密钥,这样就可以保证数据的安全性,又可以保证密钥的安全性。
2.摘要算法+数字签名
摘要算法是一种单向加密算法,也就是说,只能加密,不能解密。它的作用是对数据进行摘要,生成一个固定长度的字符串,这个字符串就是摘要信息。摘要信息的长度是固定的,而且摘要信息是唯一的,即使数据发生了变化,摘要信息也会发生变化。通过这个数字签名,就可以验证数据是否曾经被篡改。
但是,这种方法并不能保证内容+摘要信息不会同时被中间人替换。因此,还需要通过非对称加密的方式,将摘要信息加密,然后再传输。不同的是,这里的加密是使用私钥,解密使用公钥。
3.数字证书
到了这里,我们已经可以保证数据的安全性,但是,还不能保证服务器的身份是可信的。因此,我们需要使用数字证书来保证服务器的身份是可信的。
数字证书是由CA机构颁发的,它包含了服务器的公钥,以及服务器的身份信息。客户端在收到服务器的数字证书之后,会验证数字证书的合法性,如果合法,就会使用数字证书中的公钥来解密摘要信息,然后再使用摘要算法对数据进行摘要,最后比较两个摘要信息是否一致,如果一致,就说明数据没有被篡改过,且来自可信的服务端。

11. HTTPS怎么建立连接?期间交换了什么?

SSL协议基本流程:
1.客户端向服务器端索要并验证服务器端的数字证书。
2.双方协商产生会话密钥。
3.双方使用会话密钥进行加密通信。

SSL/TLS握手阶段:

  1. ClientHello: 首先,客户端会向服务器端发起一个加密通话请求,请求中,包含了客户端支持的TLS协议版本,客户端生成的随机数(用来生成会话密钥),客户端支持的加密算法;
  2. ServerHello: 服务器端收到客户端请求后,向客户端发出响应,响应中,包含了服务器端支持的TLS协议版本,服务器端生成的随机数(用来生成会话密钥),确认要使用的加密算法,服务器端的数字证书;
  3. 客户端回应:客户端收到服务器端的响应后,会验证服务器端的数字证书,如果验证通过,就会生成一个随机数,然后使用服务器端的公钥对随机数进行加密,将加密后的随机数添加到对服务器的回应中,除此之外,还在回应中添加加密通信算法改变通知,客户端握手结束通知,然后将加密后的随机数发送给服务器端;
  4. 服务器的最后回应:服务器端收到客户端的回应后,会使用自己的私钥对客户端发送过来的随机数进行解密,然后使用客户端和服务器端生成的随机数,以及客户端和服务器端协商的加密算法,生成会话密钥,然后使用会话密钥对后续的通信进行加密。最后,服务器将向客户端发送加密算法改变通知和握手结束通知。
12. HTTP/2有哪些优化?

HTTP/2基于HTTPS,因此,安全更有保障。除此之外,还有性能上的提升:
1.头部压缩
HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。
这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
2.二进制格式
HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)。
二进制格式可以使用位运算高效的进行解析,且极大的提升了HTTP传输的效率。
HTTP/2将响应报文分为两类帧,头信息帧和数据帧,头信息帧中包含了响应头信息,数据帧中包含了响应体信息。不同的帧之间可以通过流标识符进行关联,这样就可以将不同的帧组装成一个完整的响应报文。
3.并发传输
HTTP/2引入了 Stream 和 帧 的概念,多个 Stream 复用在一条 TCP 连接上。1 个 TCP 连接包含多个 Stream,Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成。Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体)。
针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应。
比如下图,服务端并行交错地发送了两个响应: Stream 1 和 Stream 3,这两个 Stream 都是跑在一个 TCP 连接上,客户端收到后,会根据相同的 Stream ID 有序组装成 HTTP 消息。

并发传输

13. HTTP/3有哪些优化?

1.基于UDP的传输
2.更快的连接建立
3.连接迁移

DNS

1. DNS是什么?

DNS(domain name system),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。通过DNS,用户只需要输入域名,就可以访问对应的IP地址的服务器。

2. DNS的工作原理?

DNS将主机域名转换为IP地址,属于应用层协议,使用UDP传输。
DNS服务器是一个分布式的数据库,存储着域名和IP地址的对应关系。当用户访问一个域名时,首先会在本地的DNS缓存中查找,如果没有找到,就会向根域名服务器发起请求,根域名服务器会返回顶级域名服务器的地址,然后向顶级域名服务器发起请求,顶级域名服务器会返回权威域名服务器的地址,然后向权威域名服务器发起请求,权威域名服务器会返回域名对应的IP地址,然后用户就可以访问该IP地址的服务器了。

3. 为什么DNS使用UDP而不是TCP?

DNS请求的数据包很小,一般不超过512字节,而且DNS服务器之间的通信是一次性的,不需要保持连接,所以使用UDP比较合适。

4. 为什么区域传送使用TCP协议?

区域传送是指DNS服务器之间的数据传输,区域传送使用TCP协议,因为区域传送需要保持连接,而且数据量比较大,一般在512字节以上,所以使用TCP比较合适。

TCP

1. TCP建立连接三次握手?

三次握手表示的是在TCP连接建立之前,服务器端和用户端之间需要交换三个TCP报文段。
第一次握手之前,客户端处于CLOSED状态,服务器处于LISTEN状态。第一次握手,客户端发送SYN报文,进入SYN_SENT状态。第二次握手,服务器收到SYN报文,发送SYN+ACK报文,进入SYN_RCVD状态。第三次握手,客户端收到SYN+ACK报文,发送ACK报文,进入ESTABLISHED状态。服务器收到ACK报文,进入ESTABLISHED状态。

2. TCP断开连接四次挥手?

四次挥手表示的是在TCP连接断开之前,服务器端和用户端之间需要交换四个TCP报文段。
第一次挥手之前,客户端处于ESTABLISHED状态,服务器处于ESTABLISHED状态。第一次挥手,客户端发送FIN报文,进入FIN_WAIT_1状态。第二次挥手,服务器收到FIN报文,发送ACK报文,进入CLOSE_WAIT状态。第三次挥手,服务器发送FIN报文,进入LAST_ACK状态。第四次挥手,客户端收到FIN报文,发送ACK报文,进入TIME_WAIT状态。服务器收到ACK报文,进入CLOSED状态。客户端等待2MSL后,进入CLOSED状态。

3. 在浏览器中输入一个网址,到页面加载完成,中间发生了什么?
  1. 解析URL,生成发送给web服务器的请求信息。
  2. 查询域名对应的IP地址,如果没有缓存,就向DNS服务器发送请求。
  3. 得到IP地址后,向web服务器发送请求。
  4. 经过三次握手,四次挥手之后,建立起客户机与服务器的TCP连接
  5. 服务器端传输网页内容,本地浏览器进行渲染
4. 一段TCP报文的首部有哪些内容?

TCP首部分为固定首部和扩展首部,固定首部的大小为20字节,扩展首部的大小为0-40字节。固定首部包含了源端口号,目的端口号,序列号,确认号,数据偏移,保留位,标志位,窗口大小,校验和,紧急指针。扩展首部包含了选项和填充。

源端口号:16位,标识发送端的端口号
目的端口号:16位,标识接收端的端口号
序列号:32位,取值范围为$[0, 2^{32} - 1]$,序号增加到最后一个后,下一个序号就变为0。标识发送端发送的数据字节流的第一个字节的序号
确认号:32位,取值范围为$[0, 2^{32} - 1]$,确认号增加到最后一个之后,下一个确认号就又变为0。标识接收端期望收到的下一个TCP报文段的第一个字节的序号,同时也是对之前收到的所有数据的确认。若确认号为n,则代表接收端期望收到的下一个字节的序号为n,同时也代表接收端已经收到了序号为0到n-1的所有字节。
数据偏移:4位,标识TCP首部的长度,单位为4字节,取值范围为$[5, 15]$,即TCP首部的长度为$[20, 60]$字节。
标志位:6位,标识TCP报文段的各种控制信息,包括URG,ACK,PSH,RST,SYN,FIN。
确认标志位ACK: 1位,若为1,则确认号字段有效,若为0,则确认号字段无效。TCP规定,在TCP建立连接后发送的所有TCP报文段都必须把ACK置为1。
同步标志位SYN:在TCP建立连接时用来同步序号。
终止标志位FIN:在TCP释放连接时用来释放序号。
复位标志位RST:用来复位TCP连接。
推送标志位PSH:用来要求接收端尽快将数据交给应用层。
校验和:16位,标识TCP首部和TCP数据的校验和,用于检测TCP报文段在传输过程中是否发生了错误。在计算校验和时,TCP首部和TCP数据的长度必须是16位的整数倍,若不是,则需要填充。

TCP头部格式

5. 什么是TCP的粘包/拆包?发生的原因?

TCP是面向字节流的协议,发送端将数据分割成报文段,然后按序发送给接收端,接收端将报文段按序组装成数据。如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。

解决方案:

  1. 消息定长,例如每个报文的大小固定为200字节,如果不够,就用空格补齐。
  2. 发送端在每个包的末尾都使用固定的分割符,如果发生拆包需等待多个包全部发送过来之后再找到其中的分割符,然后再进行合并。
  3. 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够的长度之后才算是读到了一个完整的消息。
  4. 使用更加复杂的传输协议
6. 重传机制?

超时重传机制是指,当发送端发送一个报文段之后,如果在规定的时间内没有收到接收端的确认报文段,就会重传该报文段。超时重传机制可以保证TCP的可靠性,但是会降低TCP的性能。

超时重传的几种方式:

  1. 固定超时时间:发送端发送报文段之后,固定一个时间,如果在这个时间内没有收到确认报文段,则重传该报文段。
  2. 加权平均往返时间:发送端发送报文段之后,记录下发送报文段的时间,然后等待接收到确认报文段之后,记录下接收到确认报文段的时间,然后计算出往返时间RTT,然后根据RTT计算出超时时间,如果在超时时间内没有收到确认报文段,则重传该报文段。

快速重传机制是指,当发送端发送一个报文段之后,如果接收端收到了报文段,但是发现报文段的序号不是自己期望的序号,就会立即发送一个确认报文段,告诉发送端自己期望的序号,然后发送端就会重传该报文段。快速重传机制可以保证TCP的可靠性,但是也会降低TCP的性能。

选择性确认机制是指,接收端在接收到报文段之后,不会立即发送确认报文段,而是会等到自己期望的序号的报文段都收到之后,才会发送确认报文段。

7. TCP的流量控制?

所谓流量控制,就是让发送方的发送速率不要太快,让接收方来得及接收。利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制。

链路层和IP层

1. 什么是ARP协议?

ARP协议是ARP(Address Resolution Protocol)协议是一种用于将网络层地址(如IPv4地址)映射到数据链路层地址(如MAC地址)的协议。
其工作原理是,主机向自己所在的网络广播一个ARP请求,该请求中包含了目标机器的网络地址。此网络上的其他机器都将收到这个请求,但是只有被请求的目标机器会回应一个ARP响应,其中包含了自己的物理地址。

如果获取不到mac地址会发生什么
当ARP无法获取特定IP地址对应的MAC地址时,它将无法通过局域网发送任何数据包给该IP地址。这可能导致数据包被丢弃或超时。(ChatGPT)

ARP协议主要应用于局域网(LAN)中,其作用是将网络层(IP层)的IP地址和数据链路层(MAC层)的物理地址建立映射关系,这样网络设备就可以通过MAC地址来寻址其他设备,以实现数据的传输。在LAN中,每个设备(如计算机、交换机、路由器等)都有唯一的MAC地址,以及在当前网络中分配的唯一IP地址,ARP协议就是将这两者建立映射关系的协议。在实际应用中,ARP协议主要用于以下两个方面:

  1. 以太网中的ARP协议常用于获取主机或路由器在局域网中的MAC地址。
  2. ARP协议还有一个重要的应用是在网络层(IP层)找到下一跳路由器的MAC地址,以便进行跨子网的通信。这种情况下的ARP通常被称为“无状态ARP”或“动态ARP”。(ChatGPT)

在实际操作中,计算机会在自己的ARP缓存中保存IP地址和MAC地址的映射关系,这样就可以避免每次都要发送ARP请求来获取MAC地址,提高了效率。

2. 什么是ICMP协议?

ICMP(Internet Control Message Protocol)协议是TCP/IP协议族的一个子协议,主要用于在IP主机、路由器之间传递控制消息。ICMP协议是IP协议的一个重要组成部分,它是IP协议的补充,为IP协议提供差错报告、网络统计和诊断等功能。ICMP协议是TCP/IP协议族中最基本的协议之一,它是TCP/IP协议族的一个子协议,主要用于在IP主机、路由器之间传递控制消息。ICMP协议是IP协议的一个重要组成部分,它是IP协议的补充,为IP协议提供差错报告、网络统计和诊断等功能。

3. 什么是IP协议?

IP协议是一种无连接协议,它不会在通信的两端维护一个持久的连接,而是将每个数据包独立看作一个独立的单元进行传输。这个单元包含了目标主机的IP地址以及其他必要的信息,它们能够在传输过程中被路由器进行转发,并最终到达目标主机。

IP协议是Internet中最底层的协议之一,它定义了地址标识和寻址机制,还定义了如何将数据分组并进行路由选择等细节。IP协议对于Internet的实现来说是非常关键的一部分,它能够让不同类型的设备(如路由器和交换机)进行通信,同时也为上层的应用层协议提供了一个可靠的传输机制。

ipv4: 五类IP地址 0-127,128-191,192-223,224-239,240-255。

解决IP地址不够用的问题:无地址分类,不通过地址分类来确定网络号和主机号的位数,而是通过在IP地址后加上一个0-32的数字来指示网络号所占的位数。

4. NAT

NAT 即网络地址转换(Network Address Translation),是一种用于将私有网络中的 IP 地址转换为公网 IP 地址的技术,在互联网出现前就已存在并广泛应用于局域网中。

当局域网中的主机需要与互联网上的其他主机进行通信时,NAT 会将局域网中的私有 IP 地址转换为公网 IP 地址,并在路由器上将数据包转发到公网。另外,在互联网上的主机向局域网中的主机发起连接时,NAT 也会将数据包的目标 IP 地址进行转换,将公网 IP 地址转换为局域网中的私有 IP 地址。

这种转换可以使得私有网络中的主机访问互联网,同时也能够保证私有网络中的主机的安全性,因为私有网络中的主机并不直接暴露在公网上。

5. DHCP协议?

DHCP(Dynamic Host Configuration Protocol)协议是一种用于自动分配IP地址的协议,它可以为网络中的主机自动分配IP地址,从而使得网络中的主机可以方便地进行通信。

操作系统面经

I/O多路复用 select/poll/epoll

1. socket网络模型?

服务端调用:

  1. 调用socket()函数,创建一个制定了网络协议和传输协议的Socket,
  2. 调用bind()函数,给这个Socket绑定一个IP地址和端口号,
  3. 调用listen()函数,将这个Socket设置为监听状态,通过netstat命令,我们可以看到一个端口是否真的在被监听,
  4. 调用accept()函数,等待客户端的连接,

用来监听的Socket和用来进行通信的Socket是不同的Socket。

为了实现并发访问,采用多线程模型。服务器的主进程负责监听客户的连接,accept()函数用于接收客户端的连接请求,当接收到客户端的连接请求之后,主进程就会创建一个新的Socket,然后将新创建的Socket对应的文件描述符交给子线程来处理,主进程继续监听客户端的连接请求。

2. I/O多路复用技术

使用一个进程维护多个socket。类似于一个CPU并发多个进程。
通过select/epoll/poll等系统调用,进程通过一个系统调用函数,从内核中获取多个事件。

3. select/poll
  1. select
    将已连接的所有的Socket放进一个文件描述符的集合中,通过调用select函数实现将文件描述符集合拷贝到内核中,让内核检测是否有网络事件发生。select的底层实现是一个bitsmap,默认最大值是1024。
  2. poll
    相比于select,poll的底层是一个链表,可以设置最大值。
3. epoll

epoll在内核中使用了一个红黑树来跟踪进程中所有等待检测的文件描述字。
epoll使用事件驱动的机制,在内核中维护了一个链表来记录就绪事件,当某个socket有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

epoll有两种触发方式,边缘触发(ET)和水平触发(LT)。
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。

如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK

设计模式

1. 什么是单例模式?

单例模式是指一个类只能创建一个实例,并且提供一个全局的访问点。
为什么要有单例模式 因为有很多类 我们希望它只创建一个对象,比如线程池,缓存,网络请求。

单例的优点:提供了对唯一实例的受控访问 节省了系统的资源

单例实现的关键点:

  1. 构造函数设置为私有的,这样就不能通过new来创建对象了
  2. 提供一个全局的访问点(静态方法或者枚举),通过该访问点来获取唯一的实例
  3. 考虑对象创建时的线程安全问题,确保单例类的对象有且仅有一个
  4. 考虑对象的序列化和反序列化问题,确保单例类的对象在序列化和反序列化之后仍然是同一个对象
  5. 考虑是否支持延迟加载,如果支持延迟加载,需要考虑线程安全问题

单例模式的实现方式:

  1. 饿汉式
class Singleton {
private:
    Singleton() {}
    static Singleton* instance;
public:
    static Singleton* getInstance() {
        return instance;
    }
};
Singleton* Singleton::instance = new Singleton();

在类加载的过程中就已经将对象创建好了,所以是线程安全的,但是如果不使用的话,会造成资源的浪费。

  1. 懒汉式
class Singleton {
private:
    Singleton() {}
    static Singleton* instance;
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;

懒汉式是在第一次调用getInstance方法时才创建对象,所以是延迟加载的,但是如果多个线程同时调用getInstance方法,就会创建多个对象,所以是线程不安全的。

  1. 懒汉式加锁
class Singleton {
private:
    Singleton() {}
    static Singleton* instance;
    static mutex mtx;
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            mtx.lock();
            if (instance == nullptr) {
                instance = new Singleton();
            }
            mtx.unlock();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx;

2. 什么是工厂模式?

工厂模式是一种创建型设计模式,它使用工厂方法来创建对象,而不是通过直接实例化对象来完成。工厂模式有以下一些优点:

  1. 将客户端代码与具体创建对象的代码解耦:客户端只需要知道统一的接口,而不必关心具体对象的创建和实现。

  2. 可以根据需要灵活地添加新产品:如果以后需要添加新的产品,只需要实现相应的产品类和工厂类就可以了,不需要修改客户端代码。

  3. 可以确保创建出来的对象都符合特定的规格、接口或标准:通过工厂方法创建对象,可以确保它们都符合预定的标准。

  4. 对象的创建可以集中管理:所有需要创建对象的地方都可以通过工厂方法获取创建的对象,这样可以更容易地对对象的创建过程进行管理和优化。

总的来说,工厂模式可以提高代码的灵活性、可维护性和可扩展性,更好地符合面向对象设计的封装和解耦原则。

工厂模式的实现方式:

  1. 简单工厂模式
class Product {
public:
    virtual void show() = 0;
};
class ProductA : public Product {
public:
    void show() {
        cout << "ProductA" << endl;
    }
};
class ProductB : public Product {
public:
    void show() {
        cout << "ProductB" << endl;
    }
};
class Factory {
public:
    static Product* createProduct(string type) {
        if (type == "A") {
            return new ProductA();
        } else if (type == "B") {
            return new ProductB();
        }
        return nullptr;
    }
};

简单工厂模式的缺点是,如果需要添加新的产品,就需要修改工厂类的代码,违反了开闭原则。使用了比较多的ifelse语句,不易维护。

  1. 工厂方法模式
class Product {
public:
    virtual void show() = 0;
};
class ProductA : public Product {
public:
    void show() {
        cout << "ProductA" << endl;
    }
};
class ProductB : public Product {
public:
    void show() {
        cout << "ProductB" << endl;
    }
};
class Factory {
public:
    virtual Product* createProduct() = 0;
};
class FactoryA : public Factory {
public:
    Product* createProduct() {
        return new ProductA();
    }
};
class FactoryB : public Factory {
public:
    Product* createProduct() {
        return new ProductB();
    }
};

工厂方法模式的优点:遵循了开闭原则,扩展性极强。比如现在要增加一个产品C,我们只需要增加一个创建C产品的工厂,这个工厂继承自抽象工厂即可,不需要改变原有代码,可维护性高。

工厂方法模式的缺点:增加了类的数量,当有成千上万个类型的产品时,就需要有成千上万个工厂类来生产这些产品。

  1. 抽象工厂模式
class ProductA {
public:
    virtual void show() = 0;
};
class ProductA1 : public ProductA {
public:
    void show() {
        cout << "ProductA1" << endl;
    }
};
class ProductA2 : public ProductA {
public:
    void show() {
        cout << "ProductA2" << endl;
    }
};
class ProductB {
public:
    virtual void show() = 0;
};
class ProductB1 : public ProductB {
public:
    void show() {
        cout << "ProductB1" << endl;
    }
};
class ProductB2 : public ProductB {
public:
    void show() {
        cout << "ProductB2" << endl;
    }
};
class Factory {
public:
    virtual ProductA* createProductA() = 0;
    virtual ProductB* createProductB() = 0;
};
class Factory1 : public Factory {
public:
    ProductA* createProductA() {
        return new ProductA1();
    }
    ProductB* createProductB() {
        return new ProductB1();
    }
};
class Factory2 : public Factory {
public:
    ProductA* createProductA() {
        return new ProductA2();
    }
    ProductB* createProductB() {
        return new ProductB2();
    }
};

抽象工厂模式把产品子类进行分组,同组中的不同产品由同一个工厂子类的不同方法负责创建,从而减少了工厂子类的数量。

迭代器模式

求职过程

实习

  1. 实习简历投递的第一家公司是pdd,开的早,就直接投了。但是时间跟别的事情冲突了没有做笔试,寄。
  2. 第二家是百度,这次做了笔试,无消息,进池子了。
  3. 小红书 泡池子。。。。
  4. 地平线 简历寄
  5. 华为 免笔试了 5.10号面试

    1. 专业面试
      介绍项目 研究生期间主要干啥了
      竞赛怎么做的 怎么分工的
      聊了大概二十多分钟
      手撕代码 最长回文字符串
      八股 线程和进程有什么区别?……
      你平时写的代码中用过并发编程吗?答webserver线程池……
      socket编程?
      vector几种删除方法?(remove,erase,clear,popback),vector的内存管理?可以自己定义吗?
      数据结构 栈的特性 双向链表怎么删除节点 怎么添加节点
      问了解过交换机的原理吗?答不懂硬件只会协议。
      IP协议,mac地址和IP地址转换(arp协议)
      没有反问环节
    2. 主管面
      介绍项目 细聊项目
      最得意的竞赛?竞赛中都是怎么分工的?竞赛中暴露了什么不足?会不会复盘?
      家是哪的 有没有女朋友 怎么看待加班 怎么应对压力
      全程聊天 还挺轻松的
      反问 咋没有手撕 实习的主管面没有手撕 还有什么要补足的 被告知非科班出身还是要再努力努力……

    进池子了,开泡。

  1. 招行信用卡 没做笔试
  2. 荣耀 笔试做了 5.8一面

    项目 问了十五分钟
    五分钟 八股
    c++面向对象的三大特性?封装,继承和多态 分别详细展开说一下?
    父类子类构造函数和析构函数的调用顺序?构造函数先调用父类的,然后调用子类的,析构函数相反
    STL map list和vector的区别
    了解过智能指针吗?了解过 没用过 不问了???
    多线程怎么解决资源冲突的问题? 锁和信号量 接着不问了???
    用过什么设计模式?了解过 没用过 又不问了???
    感觉面试官问完项目之后就不太感兴趣了。。。。

  3. 美团 笔试寄

转博了 此贴终结


文章作者: 李垚
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李垚 !
评论
  目录