【C/C++开发】面试宝典+面试题

C++开发面试

  • C++
  • 算法与数据结构
            • 红黑树和AVL树的定义,特点,以及区别
            • B+树
            • map和unordered_map的底层实现,以及各自的优缺点
  • 操作系统
  • 数据库
  • 计算机网络
  • 面经
            • 1、 一个C++源文件从文本到可执行文件经历的过程
            • 2、#include 的顺序以及尖叫括号和双引号的区别
            • 3、进程和线程,为什么要有线程?
            • 4、C++11有哪些新特性?
            • 5、malloc的原理,brk系统调用干什么的,mmap呢?
            • 6、C++的内存管理方式,STL的allocator,最新版本默认使用的分配器
            • 7、hash表的实现,包括STL中的哈希桶长度常数。
            • 8、hash表如何rehash,怎么处理其中保存的资源?
            • 9、手撕代码:
            • 1、redis的主从复制怎么做的
            • 2、写代码,去掉字符串中的空格空格
            • 3、如何把一个文件快速下发到100w个服务器
            • 4、如何判断一个图是否连同?
            • 5、ubuntu开机的时候系统做了什么
  • 设计模式
    • 单例模式

  • 参考
  1. 牛客网:精华专题 > c++校招面试题目合集
  2. PDD和Tencent招聘C++开发工程师岗位时的面试题(附答案)

C++

算法与数据结构

红黑树和AVL树的定义,特点,以及区别

平衡二叉树(AVL树)

平衡二叉树。又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。一句话说就是以树中所有结点为根的树的左右子树高度之差的绝对值不超过1。将二叉树上的结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1,0,1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

红黑树

红黑树是一种二叉查找树,但在每个节点增加一个存储位表示结点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。

红黑树性质:

  1. 每个节点非红即黑;
  2. 根节点是黑的;
  3. 每个叶结点(叶结点即树尾端NULL指针或NULL结点)都是黑的;
  4. 如果一个结点是红色的,则它的子节点必须是黑色的;
  5. 对于任何结点而言,其到叶子结点(树NULL指针)的每条路径都包含相同数目的黑结点。

区别

AVL树,是高度平衡的。频繁的插入和删除,会引起频繁的rebalance,导致效率下降。
红黑树,不是高度平衡的,算是一种折中。插入最多两次旋转,删除最多三次旋转。

map底层为什么用红黑树实现

由两者区别可知,红黑树在查找,插入,删除的性能都是O(log n),且性能稳定。因此,STL里面很多结构包括map,其底层实现都是使用的红黑树。

B+树

B+树,是一种多路搜索树,主要为磁盘或其他直接存取辅助设备而设计的一种平衡查找树。在B+树中,每个结点可以有多个孩子,并且按照关键字大小有序排列。所有记录结点都是按照键值的大小顺序存放在同一层的叶结点中。

相比B树,其具有以下特点:

  1. 每个结点 的指针上限为2d而不是2d+1(d为结点的出度)
  2. 内结点 不存储data,只存储key。
  3. 叶子结点 不存储指针。
map和unordered_map的底层实现,以及各自的优缺点

底层实现

map底层是基于红黑树实现的,因此map内部元素排列是有序的。
而unordered_map底层是基于哈希表实现的,因此其元素的排列顺序是杂乱无序的。

优缺点

map,其底层是基于红黑树实现的。
优点:

  • 有序性。这是map最大的优点,其元素的有序性在很多应用中都会简化很多操作
  • map的查找,删除,增加等一系列操作,时间复杂度稳定,都为O(log n)

缺点:

  • 查找,删除,增加等一系列操作,平均时间复杂度与n相关。

unordered_map,其底层是一个哈希表。
优点:

  • 查找,删除,添加的速度快,时间复杂度为常数级O(c)

缺点:

  • 空间占用率高。因为unordered_map内部基于哈希表,以(key,value)对的形式存储。
  • unordered_map的查找,删除,添加的时间复杂度不稳定。平均情况下为O©,取决于哈希函数,极端情况下可能为O(n)

操作系统

数据库

计算机网络

面经

(一)pdd服务端(部分)

1、 一个C++源文件从文本到可执行文件经历的过程

对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

  1. 预处理,产生.ii文件
  2. 编译,产生汇编文件(.s文件)
  3. 汇编,产生目标文件(.o或.obj文件)
  4. 链接,产生可执行文件(.out或.exe文件)
2、#include 的顺序以及尖叫括号和双引号的区别
  1. #include的顺序的区别:
    头文件的引用顺序对于程序的编译还是有一定影响的。如果要在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误,也就是常见的某行少个“;”符号。
  2. #include尖括号和双引号的区别:
    1)#include <> ,认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方式因编译器的不同而差别迥异。
    2)#include “”,认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。查找范围大于<>。
3、进程和线程,为什么要有线程?

1、和进程相比,它是一种非常"节俭"的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。(资源)
2、运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。(切换效率)
3、线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。(通信)

除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:

1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。(CPU设计保证)
2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。(代码易维护)

4、C++11有哪些新特性?

1)关键字及新语法:auto、nullptr、for
2)STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
3)多线程:std::thread、std::atomic、std::condition_variable
4)智能指针内存管理:std::shared_ptr、std::weak_ptr
5)其他:std::function、std::bind和lamda表达式

5、malloc的原理,brk系统调用干什么的,mmap呢?

malloc的实现方案:
1)malloc函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。
2)调用malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。
3)调用 free 函数时,它将用户释放的内存块连接到空闲链表上。
4)到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

brk和mmap:
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

6、C++的内存管理方式,STL的allocator,最新版本默认使用的分配器

C++的内存管理方式:
在c++中内存主要分为5个存储区:

栈(Stack):局部变量,函数参数等存储在该区,由编译器自动分配和释放.栈属于计算机系统的数据结构,进栈出栈有相应的计算机指令支持,而且分配专门的寄存器存储栈的地址,效率分高,内存空间是连续的,但栈的内存空间有限。

堆(Heap):程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。

全局/静态存储区:全局变量,静态变量分配到该区,到程序结束时自动释放,包括DATA段(全局初始化区)与BSS段(全局未初始化段)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和静态变量存放在BSS段。BSS段特点:在程序执行前BSS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0.

文字常量区:存放常量,而且不允许修改。程序结束后由系统释放。

程序代码区:存放程序的二进制代码

SGI 版本STL的默认配置器std::alloc

参见:《STL源码剖析》(此书为华科出版的,以前给大家推荐过,强烈建议学习C++的都去读一下)

1)考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层配置器。第一级配置器直接使用malloc()和free();第二级则视情况采取不同的策略:当配置区块超过128bytes时,视为“足够大”,便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用memory pool(内存池)整理方式,而不在求助于第一级配置器。

2)内存池的核心:内存池和16个自由链表(各自管理8,16,…,128bytes的小额区块)。在分配一个小区块时,首先在所属自由链表中寻找,如果找到,直接抽出分配;若所属自由链表为空,则请求内存池为所属自由链表分配空间;默认情况下,为该自由链表分配20个区块,若内存池剩余容量不足,则分配可分配的最大容量;若内存池连一个区块都无法分配,则调用chunk_alloc为内存池分配一大块区块;若内存不足,则尝试调用malloc分配,否则返回bad_alloc异常。

7、hash表的实现,包括STL中的哈希桶长度常数。

hash表的实现主要涉及两个问题:散列函数碰撞处理

1)hash function (散列函数)。最常见的散列函数:f(x) = x % TableSize .

2)碰撞问题(不同元素的散列值相同)。解决碰撞问题的方法有许多种,包括线性探测、二次探测、开链等做法。SGL版本使用开链法,使用一个链表保持相同散列值的元素。

虽然开链法并不要求表格大小必须为质数,但SGI STL仍然以质数来设计表格大小,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。

8、hash表如何rehash,怎么处理其中保存的资源?

先想想为什么需要rehash:

因为,当loadFactor(负载因子)<=1时,hash表查找的期望复杂度为O(1). 因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。

模仿C++的vector扩容方式,Hash表中每次发现loadFactor==1时,就开辟一个原来桶数组的两倍空间(称为新桶数组),然后把原来的桶数组中元素全部转移过来到新的桶数组中。注意这里转移是需要元素一个个重新哈希到新桶中的。

9、手撕代码:

1)给定一个数字数组,返回哈夫曼树的头指针。

2)最长公共连续子序列。


(二)tencent二面(部分)

1、redis的主从复制怎么做的

Redis旧版复制功能只有同步和命令传播。新版复制功能加入了部分同步的功能。

1)同步:

2)命令传播:

当主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。

3)部分同步:(断线后重复制)

复制偏移量:通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。

复制积压缓冲区:主服务保存最近的写命令到复制积压缓冲区,是一个先进先出队列

服务器运行ID:从服务器记录上次同步的主服务器的Id。

2、写代码,去掉字符串中的空格空格
 #include 
 using namespace std;
 int main()
 {
 	char str[40] = " abc 123 456 ";
 	int num = 0;
 	int i;
 	for(i = 0; str[i] != '\0'; ++i)
 	{
 		if(str[i] == ' ')
 			++num;
 		else
 			str[i-num] = str[i];
 	}
 	str[i-num] = '\0';
 	printf("%s\n",str);
}
3、如何把一个文件快速下发到100w个服务器

gossip算法?Gossip有众多的别名“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。

4、如何判断一个图是否连同?

DFS、BFS、并查集

5、ubuntu开机的时候系统做了什么

1)加载BIOS

BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做”硬件自检”。硬件自检完成后,BIOS把控制权转交给下一阶段的启动程序。

2)读取MBR

计算机读取该设备的第一个扇区,也就是读取最前面的512个字节。如果这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给”启动顺序”中的下一个设备。

3)Bootloader

在这种情况下,计算机读取”主引导记录”前面446字节的机器码之后,不再把控制权转交给某一个分区,而是运行事先安装的”启动管理器”(boot loader),由用户选择启动哪一个操作系统。

Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。

Boot Loader有若干种,其中Grub、Lilo和spfdisk是常见的Loader。Linux环境中,目前最流行的启动管理器是Grub。

4)加载内核

内核的加载,内核加载后,接开始操作系统初始化,根据进程的优先级启动进程。

设计模式

单例模式

 #include 

/** 单例模式:一个类只允许有一个实例。
 * 
 * 参考链接:
 * https://www.cnblogs.com/qiaoconglovelife/p/5851163.html
 * https://www.cnblogs.com/myd620/p/6133420.html
*/

 /**
  * 简单单例模式
  * 
  * 概述:
  * 构造函数声明为private或protected,防止被外部函数实例化,
  * 内部保存一个private static的类指针保存唯一的实例,
  * 实例的动作由一个public类方法代劳,该方法也返回单例类唯一的实现。
 */
class singleton
{
protected:
    singleton(){}
private:
    singleton(const singleton&)();  // 禁止拷贝
    singleton& operator=(const singleton&){};   // 禁止赋值
    static singleton* p;    // 实例指针
public:
    static singleton* instace();    // 获取实例
};
singleton* singleton::p = nullptr;  // 指针先置为空
singleton* singleton::instace()
{
    if(p == nullptr)
        p = new singleton();
    return p;
}
/**
 * 总结:该方法没有考虑线程安全,
 * 由于两个线程同时调用instance方法且同时检测到p是nullptr值,则两个线程会同时构造一个实例给p。
*/

/**
 * 懒汉与饿汉模式
 * 
 * 实现方法:
 * 
 *   懒汉:不到万不得已就不会去实例化类:第一次用到类实例的时候才会去实例化。上面的方法为懒汉实现
 *   饿汉:饥不择食:单例类定义时就进行实例化
 * 
 * 特点与选择:
 *   进程同步时,访问量比较大,或者访问的线程比较多时,采用饿汉实现,实现更好的性能(空间换时间)
 *   访问量较小时,采用懒汉实现(时间换空间)
*/

/**
 * 线程安全的懒汉实现(1. 加锁)
 * 
 * pthread_mutex_t      :   代表互斥锁的结构体
 * pthread_mutex_init   :   互斥锁的初始化
 * pthread_mutex_load   :  加锁
 * pthread_mutex_unlock :  解锁
*/
class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
private:
    static singleton* p;
public:
    static pthread_mutex_t mutex;   // 定义互斥锁(结构体)
    static singleton* initance();
};
pthread_mutex_t singleton::mutex;
singleton* singleton::p = nullptr;
singleton* singleton::initance()
{
    if (p == nullptr)   // 实例指针非空,无需操作(“双检锁”机制)
    {
        pthread_mutex_load(&mutex);
        if(p == nullptr)
            p = new singleton();
        pthread_mutex_unlock(&mutex);
    }
    return p;
}

/**
 * 线程安全的懒汉实现(2. 内部静态变量)
 * 
 * instance()定义一个静态实例,也可保证拥有唯一实例,在返回时只需要返回其指针。推荐!
*/
class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
public:
    static pthread_mutex_t mutex;
    static singleton* initance();
    int a;
};
pthread_mutex_t singleton::mutex;
singleton* singleton::instance()
{
    pthread_mutex_lock(&mutex);
    static singleton obj;
    pthread_mutex_unlock(&mutex);
    return &obj;
}

/**
 * 饿汉实现
 * 
 * 饿汉本身就是线程安全,不用加锁。
*/
class singleton
{
protected:
    singleton(){};
private:
    singleton(const singleton&)();  // 禁止拷贝
    singleton& operator=(const singleton&){};   // 禁止赋值
    static singleton* p;
public:
    static singleton* initance();
};
singleton* singleton::p = new singleton;    // 可以不加括号
singleton* singleton::initance()
{
    return p;
}
/**
 * new 类名与new 类名():有括号的,就调用对应的构造函数。
 * 没有加括号,如果有自定义的不带参数的构造函数,则调用该构造函数;
 * 如果没有定义构造函数,就调用默认的不带参数的构造函数。
*/

你可能感兴趣的:(程序猿修炼之路)