c++面试高频题-速记版

2021/4/25 12:25:18

本文主要是介绍c++面试高频题-速记版,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

内存地址对齐

// 64位系统
struct T{
    int a;  //0-3
    char ch;    //4
    int b;  // 8-11
    long c; // 16-23
    char chs[12];    //24-35
};
// 总共占据40bytes

Linux的虚拟内存、物理内存。

-虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。它有4个重要的能力:

  • 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在主存和磁盘之间来回传送数据,通过这种方式,它高效地使用了主存。
  • 他为每个进程提供了一致的地址空间,它高效地使用了主存。
  • 它保护了每个进程的地址空间不被其他进程破坏。
  • 他能够提供超出当前计算机本身物理内存的内存,有效应对了软件大小增速过快的问题。一般来说,虚拟内存中主要通过分页机制来实现,每个进程会有一个属于自己的页表,然后虚拟地址由虚拟页号和页内偏移量构成,通过虚拟页号在页表中查询到虚拟页的首地址,然后通过页内偏移量,我们可以找到目标地址。这个还可能会涉及到缓存不命中下的交换技术、多级页表、和分段技术,视情况决定是否要说。

LT模式和ET模式,ET模式下accept()为什么一些连接会接收不了?

LT模式:LT是epoll默认的工作方式,支持阻塞和非阻塞两种机制。LT模式下内核会持续通知你文件描述符就绪了,然后你可以对这个就绪的fd进行I/O操作。如果不做任何操作,内核还是会继续通知你的。
ET模式:ET模式相对LT模式更加高效,只支持非阻塞模式。在这个模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不再为那个文件描述符发生更多的就绪通知。直到你做了某些操作导致那个文件描述符不再为就绪状态了。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式时,必须使用非阻塞套接口,以避免一个文件句柄的阻塞导致把其他文件描述符饿死。
ET模式下,只会触发一次读事件,如果不循环读取,除非新的连接到来,其它未被读入的链接不会触发IO事件。

hash_table底层实现是什么?

unordered_map和unordered_set的底层数据结构讲一下。
底层为开链表实现,用一个数组存储桶,每个桶都是hash值相同的键的集合,用链表实现。通过hash函数找到对应的桶,然后在这个桶的链表上完成查找、删除、添加的操作。
hash_table的扩容机制,底层维护一个负载因子,表示当前元素个数/桶的个数,一般默认超过0.75就扩容,底层数据结构维护了扩容的规模变化。

STL了解吗?说一下空间配置器,一级空间配置器、二级空间配置器。

空间配置器是STL用来管理和分配内存的类型。STL有一级空间配置器和二级空间配置器。一级空间配置器就是对new和delete做了个简单的包装。二级空间配置器是利用的池的思想,用一个free-list维护很多不同大小的字节块(8-128bytes),然后每次就分配这样大小的内存块(就近取8的整数倍),如果超过128byte就直接用malloc分配。然后释放内存也放回free-list。如果free-list中没有对应的内存块了,就像内存池中申请内存(内存是一大块连续内存),然后返回用户需要的内存,其余插入到free-list中作为补充。如果内存池用完了,就调用malloc获取大块的连续内存。

数据库范式:

这个比较多,一般只说前三个范式即可。
1NF的定义为:符合1NF的关系中的每个属性都不可再分。表1所示的情况,就不符合1NF的要求。1NF是所有关系型数据库的最基本要求。
第二范式(2NF)在1NF的基础之上,消除了非主属性对于码的部分函数依赖。其定义是不存在非主属性对码存在部分函数依赖关系。
第三范式(3NF)在3NF的基础上,消除了非主属性对码的传递函数依赖。其定义是不存在非主属性对码存在传递函数依赖关系。

TCP通信的异常情况及对应的解决方案。

1. 试图与一个不存在的端口建立连接:服务器端口还没有监听,我们的客户端就调用connect,视图与其建立连接。这时会发生什么呢?这符合触发RST分节的条件,目的为某端口的SYN分节到达,而端口没有监听,那么内核会立即响应一个RST,表示出错。客户端TCP收到这个RST之后则放弃这次连接的建立,并且返回给应用程序一个错误。正如上面所说,建立连接的过程对应用程序来说是不可见的,这是操作系统帮我们来完成的,所以即使进程没有启动,也可以响应客户端。
2 试图与一个不存在的主机上面的某个端口建立连接:这也是一种比较常见的情况,当某台服务器主机宕机了,而客户端并不知道,仍然尝试去与其建立连接。这个时候由于宕机,操作系统帮不上忙,服务器处于一种完全没有响应的状态。那么此时客户端的TCP会怎么办呢?客户端不会收到任何响应,那么等待6s之后再发一个SYN,若无响应则等待24s之后再发一个,若总共等待了75s后仍未收到响应就会返回ETIMEDOUT错误。这是TCP建立连接自己的一个保护机制,但是我们要等待75s才能知道这个连接无法建立,对于我们所有服务来说都太长了。更好的做法是在代码中给connect设置一个超时时间。
3 Server进程被阻塞:由于某些情况,服务器端进程无法响应任何请求,比如所在主机的硬盘满了,导致进程处于完全阻塞,通常我们测试时会用gdb模拟这种情况。上面提到过,建立连接的过程对应用程序是不可见的,那么,这时连接可以正常建立。当然,客户端进程也可以通过这个连接给服务器端发送请求,服务器端TCP会应答ACK表示已经收到这个分节(这里的收到指的是数据已经在内核的缓冲区里准备好,由于进程被阻塞,无法将数据从内核的缓冲区复制到应用程序的缓冲区),但永远不会返回结果。
4 我们杀死了server:这是线上最常见的操作,当一个模块上线时,OP同学总是会先把旧的进程杀死,然后再启动新的进程。那么在这个过程中TCP连接发生了什么呢。在进程正常退出时会自动调用close函数来关闭它所打开的文件描述符,这相当于服务器端来主动关闭连接——会发送一个FIN分节给客户端TCP;客户端要做的就是配合对端关闭连接,TCP会自动响应一个ACK,然后再由客户端应用程序调用close函数,也就是我们上面所描述的关闭连接的4次挥手过程。接下来,客户端还需要定时去重连,以便当服务器端进程重新启动好时客户端能够继续与之通信。
5 Server进程所在的主机宕机:客户端向服务器端发送分节,由于服务器端宕机,不会有任何响应,客户端持续重传,然而服务器始终不能应答,重传数次之后,大约4~10分钟才停止,之后返回一个ETIMEDOUT错误。

各种线程同步方式(信号量、互斥锁、自旋锁、读写锁等)

信号量

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);

讲讲智能指针。

智能指针在C++11版本之后提供,包含在头文件中,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工作,像旁观者那样观测资源的使用情况。(不会增加引用计数)

什么时候要重写拷贝构造函数?

凡是包含动态内存分配成员或者指针成员的类都应该重写拷贝构造函数。

计算机网络的结构?

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

讲一讲多进程通信?

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



这篇关于c++面试高频题-速记版的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程