字节跳动|飞书 C++ 客户端开发|(22届春招,两轮技术面)|2021|
匿名用户
10710
2021.03.27
2021.04.06
发布于 未知归属地

字节跳动飞书C++客户端开发(22届春招,两轮技术面)
字节跳动data部门go后端开发(22届春招,三轮技术面)
腾讯天美工作室C++后台开发(22届春招,三轮技术面)

字节跳动C++客户端面经

飞书的客户端,总共两轮技术面+一轮HR面,因为不想做客户端已经拒绝offer。

一面

  1. 自我介绍
  2. 各种线程同步方式(信号量、互斥锁、自旋锁、读写锁等)
    信号量:
int sem_init(sem_t* sem,int pshared,unsigned int value);
// 初始化一个信号量
// pshared表示是否在进程间共享,0表示只在线程间共享,否则进程间共享
// value为设置的初始值
int sem_destroy(sem_t* sem);
// 销毁一个线程
int sem_wait(sem_t* sem);
// P操作,对信号量-1
int sem_post(sem_t* sem);
// V操作,信号量+1
int sem_getvalue(sem_t* sem, int* valp);
// 返回信号量的值到valp

互斥锁:

int pthread_mutex_init(pthread_mutex_t* mutex, const thread_mutexattr_t* mutexattr);
// 初始化一个互斥锁,mutexattr是相关设置参数
int pthread_mutex_lock(pthread_mutex_t* mutex);
// 对互斥锁加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);
// 解锁
int pthread_mutex_trylock(pthread_mutex_t* mutex;
// 非阻塞加锁,如果已经上锁,不会阻塞,避免死锁
int pthread_mutex_destroy(pthread_mutex_t* mutex);
// 用来撤销互斥锁的资源。

读写锁:

int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t* attr);
// 初始化读写锁
int pthread_destroy(pthread_rwlock_t* rwlock);
// 销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
// 加读锁
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
// 加写锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
// 解锁

自旋锁:
自旋锁和互斥锁差不多,区别是自旋锁阻塞方式和互斥锁不同,互斥锁是让线程睡眠来实现阻塞,而自旋锁是通过不断循环让线程忙等待,适用于占用自旋锁时间比较短的情况。

int pthread_spin_init(__pthread_spinlock_t* __lock, int__pshared);
int pthread_spin_destroy(__pthread_spinlock_t* __lock);
int pthread_spin_trylock(__pthread_spinlock_t* __lock);
int pthread_spin_unlock(__pthread_spinlock_t* __lock);
int pthread_spin_lock(__pthread_spinlock_t* __lock);
  1. C++多态,构造函数能是虚函数吗?构造函数内能调用虚函数吗?析构函数能是虚函数吗?
    虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)和指向虚函数表的指针来实现的。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数,实际调用过程是通过对象中保存的虚函数指针来访问这个虚函数表,进而查询对应的虚函数地址来实现调用的。
    在C++中,提倡不在构造函数和析构函数中调用虚函数。构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编。
    析构函数最好是虚函数,因为当基类指针指向派生类的时候,若基类析构函数不声明为虚函数,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致对象的内存被部分释放,从而造成内存泄漏。

  2. 讲讲static。静态成员函数和静态成员函数之间能相互调用吗?
    全局静态变量,内存中的位置:静态存储区,在整个程序运行期间一直存在。初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化); 全局静态变量在声明他的文件之外是不可见的,其生命周期是是从定义之处开始,到文件结尾。
    局部静态变量,在局部变量之前加上关键字static,局部变量就成为一个局部静态变量,它的生命周期变成了整个源文件。内存中的位置:静态存储区。未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)。作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。
    类的静态成员,必须对类静态成员变量进行初始化(并且是在类的外部),初始化时使用作用域运算符来标明他所属类。
    类静态成员函数,类的静态函数是该类的范畴内的全局函数,不能访问类的成员,只能访问类的静态成员,不需要类的实例即可调用;实际上,他就是增加了类的访问权限的全局函数。类的静态成员函数可以继承或者覆盖,但是不能是虚函数。
    全局静态函数,表明这个函数只在该cpp内有效,但是其他的cpp文件不能访问这个变量;如果有两个cpp文件声明了同名的全局静态变量,那么他们实际上是独立的两个变量。
    静态成员函数和静态成员函数之间能相互调用。

  3. 讲讲智能指针。
    智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr、auto_ptr。
    shared_ptr可以多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。创建一个智能指针一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
    unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。
    weak_ptr是为了配合shared_ptr而引入的一种智能指针(解决shared_ptr的循环引用问题),因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。

  4. 什么时候要重写拷贝构造函数?
    凡是包含动态内存分配成员或者指针成员的类都应该重写拷贝构造函数。

  5. 计算机网络的结构?
    一般来说,可以说TCP/IP体系结构,忽略物理层,有:
    应用层(HTTP、FTP、DNS等协议):实现应用到应用之间的通信;
    传输层(TCP、UDP等协议):实现进程到进程之间的通信;
    网络层(IP、ICMP等协议):实现主机到主机之间的通信;
    数据链路层(ARP等协议):实现点到点之间的通信。

  6. TCP为什么可靠?
    TCP是面向连接的全双工协议,提供了三次握手、四次挥手,ACK、超时重传机制、拥塞控制机制等机制来确保其可靠性。

  7. TCP的流量控制(滑动窗口)、拥塞控制(慢启动、快速恢复、拥塞避免)。
    滑动窗口就是窗口内的包可以不用等待前面的包确认就发送出去,故窗口越大流量越大。
    拥塞控制我认为只要掌握好这张图即可:
    TCP拥塞控制.png

  8. 进程间如何共享内存?
    共享内存是在内存中单独开辟的一段内存空间,这段内存空间有自己特有的数据结构,包括访问权限、大小和最近访问的时间等。两个进程在使用此共享内存空间之前,需要在进程地址空间与共享内存空间之间建立联系,即将共享内存空间挂载到进程中,这样就可以实现内存的共享了。

  9. 讲一讲多进程通信?
    有多种通信方式:匿名管道、有名管道、信号、消息队列、信号量、共享内存、套接字。
    1)匿名管道:本质是内核缓冲区,可用于亲缘进程(父子进程,兄弟进程)间通信,半双工,一端读一端写,先进先出。
    2)有名管道:本质就是一个文件,所以可以提供给没有亲缘关系的进程来通信,。
    3)信号:信号可以在任何时候发给某一进程,这是一种异步通信方式。
    4)消息队列:存放于内核的某个消息链表,允许多个进程进行读写。
    5)信号量:信号量是一个计数器,提供原子的P、V操作,用于进程同步。
    6)共享内存:使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。但是共享内存如果中间涉及到写操作,往往需要同步机制进行辅助,比如信号量。
    7)套接字:套接字主要用于不同主机的进程之间的通信。

  10. list和vector的区别?vector的push_back为什么是O(1)?hash_map和map的区别?
    list和vector的区别
    访问:vector支持随机访问,时间复杂度为O(1),list支持顺序访问,时间复杂度为0(1)。
    插入和删除:vector中插入和删除节点会的时间复杂度平均为O(n),而list中为O(1)。
    空间:vector占据了一块连续的内存,而list的内存空间可以是不连续的。
    push_back()复杂度
    vector可以动态扩容,其在末尾插入新元素的的函数push_back()的摊还时间复杂度为O(1),这是由于其每次空余空间不够了,就会重复分配为原来k倍的一块空间,然后讲之前的数据全部拷贝过去,在插入新的元素。如果从0开始,调用n个push_back(),最后的平均时间复杂度会是O(1),可以等比数列计算得出。一般k值取1.5或2,2的好处就是平均时间复杂度的常数项更低,但是坏处是空间利用率低,而且每次都无法有效利用之前已经释放的内存。1.5倍扩容在底层的内存分配上能够利用之前释放的内存。
    hash_map和map的区别
    前者是哈希表实现,是无序容器,支持O(1)的查找、删除、添加。后者是红黑树实现,是有序容器,支持log(n)的查找、删除、添加。

二面

  1. 自我介绍。
  2. 问是如何学习的?
    就大概介绍自己的学习路径,主要是要表达出自己是一个学习上有规划的人,如果像我一样是非科班转行的可以侧重强调一下自己的自学能力。
  3. 觉得看的计算机书籍里面那本书写的最好的是哪本?
    《深入理解计算机系统》。
  4. 问项目细节。
  5. 在一面的问题里找自己感兴趣的问(面试官说一面问的太详细没啥好问的了)。
    个人感觉面试官放水了。
  6. 模板成员函数能是虚函数吗?为什么?模板的实现在编译的哪个阶段?
    模板的虚函数的使用和一般的类和成员函数完全一样。但是模板成员函数不能是虚函数。可以这么理解:模板成员函数在实例化之前不能知道要实例化多少模板成员函数,而实例化的时候,要创建虚函数表,这两者就矛盾了。
    类模板的实例化发生在编译阶段,以每个cpp文件为编译单位,实例化该文件中的函数模板和类模板。链接器在链接每个目标文件时,会检测是否存在相同的实例;有存在相同的实例版本,则删除一个重复的实例,保证模板实例化没有重复存在。
  7. 给个数组找到a[i]-a[j]的最大值,i<j。
    这个很简单,从左到右扫描一遍数组,保存最大差值和左边的最大值,每次更新把左边的最大值和当前值做差,再和最大差值比较,如果更大就更新,如果当前值大于左边最大值,就更新左边的最大值。时间复杂度O(n)。
  8. m*n的矩阵是否存在一个结点,然后有一条路径从这个节点出发到相邻节点,其间通过所有节点?
    是个数学问题,然后推出(m*n%2==0&&m>1&&n>1)||(m*n==1)即可。
  9. 反问环节。
评论 (3)