自我介绍

C++

i++ ,++i

  1. i++是右值,++i是左值
  2. 如果在一条语句内执行i++, 会先获取i的值,然后执行语句,执行结束后i自增1
  3. 如果在一条语句内执行++i, 会先对i自增1,然后执行语句
  4. 补充:一般来讲,i++ 的执行操作为:使用临时对象保存i的值,然后执行i+1,返回临时对象;++i 直接对i自增,不涉及临时变量

for-loop

  1. for(int i = 0;i<10;i++/++i>): ++i 与i++ 输出i,结果一致吗? 一致

  2. for()的使用

    1. for如果使用之前声明的变量,可以直接以for (;condition;expression) 循环,循环开始时会获取变量的初始值,判断本次执行是否满足条件,满足则执行,执行完循环体,循环变量更新,不满足则结束循环
    2. 也可以使用for(init;condition;expression)

    3. 其他想到的补充:

对象初始化

回答会根据传递的参数,调用不同的重载。也可以根据成员变成,隐式构造;也可以将构造函数声明为explicit的,禁止隐式构造。 面试官问还有吗?不知道问的什么,然后继续问了虚函数。

虚函数

成员函数,通过virtual修饰,子类继承时可以选择是否重载,也可以通过virtual 成员函数=0的方式声明为纯虚函数,子类必须重载。 子类继承父类时,构造子类时需要先构造父类,析构时通过声明父类析构为virtual,先析构子类,后析构父类。 补充:

  1. 纯虚函数的父类为虚基类,无法实例化。
  2. 如果构造函数中包含虚函数,因为子类还没有构造,没有虚函数指针,因此会调用父类的虚函数。
  3. 虚函数,成员函数会存储在代码段,虚函数表存储在rodata
  4. 在析构函数中调用虚函数时:基类析构函数在执行时,派生类部分的对象已经被销毁,虚函数表的指针可能不再有效,导致虚函数调用的行为不可预测。
  5. inline 因为inline会原地展开,没有具体的地址,无法通过指针访问
  6. static函数不能是虚函数,static成员函数是类级别的,无法通过多态调用。
  7. 子类可以访问父类的静态资源,只要子类有访问权限
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
class Base {
public:
  static int x;

public:
  static void staticFunction() {
    std::cout << "Base static function" << std::endl;
  }
};
int Base::x = 30;

class Derived : public Base {
public:
  static int x;

public:
  static void derivedStaticFunction() {
    std::cout << "Derived static function" << std::endl;
  }
};
int Derived::x = 40;

int main() {
  Base::staticFunction(); // 访问父类的静态成员函数
  Derived::staticFunction(); // 子类也可以通过类名访问父类的静态成员函数
  std::cout << Base::x << std::endl; // 访问父类的静态成员变量
  std::cout << Derived::x << std::endl; // 访问子类的静态成员变量,同名资源 优先输出子类的
}

输出

1
2
Base static function
Base static function

虚函数指针

虚函数指针存在对象空间内,只有在调用时,才会通过虚函数指针,访问虚函数表,调用对应的虚函数。

new delete, malloc free

C++可以视作C的超集,C++继承了Cmallocfree

new / delete

  1. new
    1. 动态申请内存,返回对应对象类型的指针,
    2. 调用类的构造函数,并进行初始化,访问了内存,涉及到虚实地址变换
    3. 申请失败会抛出异常std::bad_alloc
  2. delete
    1. 释放通过new申请的空间;
    2. 调用类的析构函数
  3. 补充:可以通过new(nothrow)返回空指针; newdeleteC++运算符可以重载。是否访问内存取决于具体的初始化逻辑,虚实地址变换由操作系统完成。

malloc / free

  1. malloc
    1. 申请内存,返回void类型的指针,
    2. 不进行初始化,因此没有访存
    3. 申请失败,返回NULL
  2. free
    1. 释放通过malloc申请的空间;
  3. 补充:mallocfreeC的函数。不支持重载

内存布局

  1. 栈: 自动管理,速度比较快,超出作用域自定析构
  2. 堆:手动管理,通过裸指针申请时,必须手动析构;也可以通过智能指针管理
  3. 全局区data:初始化的全局变量和静态变量
  4. 代码区text:代码段,存储函数,和类的静态成员函数
  5. bss区:未初始化的全局变量和静态变量,默认为0
  6. rodata区:只读数据,常量

线程 进程区别

  1. 进程
    1. 进程享有自己的地址空间,因此多进程之间是隔离的,一个进程的崩溃往往不会导致其他进程崩溃
    2. 进程通信比较复杂
    3. 进程是资源分配的基本单位
  2. 线程
    1. 多个线程共享一个进程的地址空间,因此一个线程崩溃有可能导致其他线程的崩溃
    2. 线程通信简单,因此需要频繁交互任务可以使用多线程。
  3. 补充:进程通信包括:管道,共享内存,消息队列,socket,信号,信号量;线程通信主要包括:互斥锁,条件变量,信号量,原子操作,互斥量,读写锁。

数据库索引

尴尬,答成引擎了。然后又回答一遍

  1. B+: 多路平衡,高度比较矮,快速查询,叶子节点为链表结构,支持范围查询,非叶子节点存储索引节点,叶子节点存储数据
  2. B:不支持范围查询,非叶子节点也可以存储数据
  3. 单列索引:单个字段创建索引
  4. 联合索引:多个字段创建索引,需要主要最左匹配原则
  5. 补充:哈希索引:快速查找,无法排序,不支持范围查询,

项目

问到有没有测过并发量。回答没有,但是根据之前测试线程池的经验,应该可以支持1w用户。

网络部分

select, poll, epoll

  1. select 使用了线性结构,需要遍历,且有最大文件描述符限制啊
  2. poll 将select的结构修改为链表,依然需要遍历访问,
  3. epoll 使用了红黑树作为底层结构,

补充:

select

  1. select 使用的固定大小的数组fd_set,每一位表示一个文件描述符
  2. 当有事件发生时,select需要遍历访问,判断哪个描述符触发了事件
  3. select的最大描述符是固定的,由内核常量FD_SETSIZE定义,通常为1024
  4. 调用select时,需要将整个文件描述集合从用户态拷贝到内核,处理后,再拷贝回去

poll

  1. poll使用动态链表结构,可以支持任意数量的文件描述符
  2. poll需要遍历访问,判断哪个描述符触发了事件
  3. 没有最大文件描述符的限制,但是上限会受制于系统或环境
  4. 每次调用poll,需要将整个文件描述符集合拷贝到内核

epoll

  1. epoll使用了两种数据结构,红黑树用来管理所有监控的文件描述符,双向链表用来存储就绪(触发事件)的文件描述符
  2. 触发类型包括:水平触发(LT, Level-Triggered) 只要事件未处理会持续通知; 边缘触发(ET, Edge-Triggered) 高效模式,仅在状态发生变化时通知用户
  3. 使用epoll_ctl注册事件,只有一次注册的开销,之后无需遍历
  4. 支持大量链接
  5. epoll的系统调用
  1. epoll_create创建epoll实例,返回文件描述符
  2. epoll_ctl添加监控文件描述符,添加时需要指定触发类型,以及事件类型,返回0表示成功
  3. epoll_wait等待就绪文件描述符,返回就绪文件描述符列表,返回0表示超时,返回-1表示出错

浏览器访问url到渲染页面的具体过程

  1. DNS解析
    1. 有缓存先查缓存
    2. 没有缓存,查询根域名服务器,根域名服务器通过树状结构查询顶级域名服务器,顶级域名查询负责该域名的权威域名服务器
    3. 权威域名服务器返回IP地址
  2. TCP握手
  3. IP
  4. 同一个子网,通过arp寻找对应的mac,不同子网通过默认路由转发,通过路由器自动更新下一跳的mac地址寻找目标mac地址
  5. 封装http消息,请求行,请求头,请求体,
  6. 通过交换机或路由器将信息转为二进制流,发送到目标主机
  7. 服务器收到后通过拆包,协议栈自底向上到达应用层,知道是http消息,取出对应的资源,以响应行:status_code等,响应头,响应体,经协议栈自顶向下发送
  8. 客户端收到消息,通过浏览器渲染得到页面

如何加快http的速度

  1. 选择压缩算法,压缩消息体
  2. 服务器负载均衡,返回最近的最快的服务器
  3. 选择合适的协议,因为TCP很慢,所以http3.0 使用了UDP实现的quic
  4. 将用户访问过的资源,在本地缓存
  5. 补充:其他优化方式:合并多个css、js使用一个http连接发送,减少请求数量;异步加载js;使用高效的图像压缩算法,压缩图像
  6. 补充缓存:
    1. 浏览器缓存:通过设置适当的缓存策略(如Cache-Control、Expires)在浏览器端缓存常用资源,避免每次请求都重新从服务器加载资源,显著提高页面加载速度。
    2. CDN缓存:内容分发网络(CDN)将资源缓存到离用户更近的节点,通过地理位置分发减少网络延迟,提升响应速度。
    3. 服务器端缓存:对于常用的动态资源(如数据库查询结果),可以使用 Redis 或 Memcached 等缓存技术,减少数据库的压力,快速返回常见请求的结果。
    4. 服务工作者缓存:现代浏览器提供服务工作者(Service Workers)来缓存资源并提供离线支持,使得即使没有网络,用户也可以继续使用应用程序。

对如何成为一个优秀的开发者有什么看法

主观题。。。

手撕 二叉树最近公共祖先

ACM模式,写出函数讲解思路

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct TreeNode{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode():val(0),left(nullptr), right(nullptr){}
    TreeNode(int _val):val(_val),left(nullptr), right(nullptr){}
};
TreeNode * commonAncestor(TreeNode *root, TreeNode *p, TreeNode *q){
    if(!root||p==root||q==root)
        return p;
    auto left = commonAncestor(root->left, p, q);
    auto right = commonAncestor(root->right, p, q);
    if(left&&right)
        return root;
    return left?left:right;
}