2020年美团秋招C++精选面试题及答案(上)

1. B+树特点

(1)每个结点的关键字个数与孩子个数相等,所有非最下层的内层结点的关键字是对应子树上的最大关键字,最下层内部结点包含了全部关键字。
(2)除根结点以外,每个内部结点有 到m个孩子。 [3]
(3)所有叶结点在树结构的同一层,并且不含任何信息(可看成是外部结点或查找失败的结点),因此,树结构总是树高平衡的。

2.CAS的缺点及解决

CAS的缺点有如ABA问题,自旋锁消耗问题、多变量共享一致性问题.
1.ABA:
问题描述:线程t1将它的值从A变为B,再从B变为A。同时有线程t2要将值从A变为C。但CAS检查的时候会发现没有改变,但是实质上它已经发生了改变 。可能会造成数据的缺失。
解决方法:CAS还是类似于乐观锁,同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference
2.自旋消耗资源:
问题描述:多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。
解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
虽然base和cells都是volatile修饰的,但感觉这个sum操作没有加锁,可能sum的结果不是那么精确。
2.多变量共享一致性问题:
解决方法: CAS操作是针对一个变量的,如果对多个变量操作,

  1. 可以加锁来解决。
  2. 封装成对象类解决。

更多一线互联网大厂面试题vx关注零声学院领取!
在这里插入图片描述

3.已知一个函数rand7()能够生成1-7的随机数,请给出一个函数,该函数能够生成1-10的随机数

该解法基于一种叫做拒绝采样的方法。主要思想是只要产生一个目标范围内的随机数,则直接返回。如果产生的随机数不在目标范围内,则丢弃该值,重新取样。由于目标范围内的数字被选中的概率相等,这样一个均匀的分布生成了。

显然rand7至少需要执行2次,否则产生不了1-10的数字。通过运行rand7两次,可以生成1-49的整数,

在这里插入图片描述

由于49不是10的倍数,所以我们需要丢弃一些值,我们想要的数字范围为1-40,不在此范围则丢弃并重新取样。

代码:

int rand10() {
    
    
 int row, col, idx;
 do {
    
    
 row = rand7();
 col = rand7();
 idx = col + (row-1)*7;
 } while (idx > 40);
 return 1 + (idx-1)%10;
}
 

由于row范围为1-7,col范围为1-7,这样idx值范围为1-49。大于40的值被丢弃,这样剩下1-40范围内的数字,通过取模返回。下面计算一下得到一个满足1-40范围的数需要进行取样的次数的期望值:在这里插入图片描述

4. C++11创建线程的三种方式

  1. 通过函数
    thread:标准库的类
    join:阻塞主线程并等待
// MultiThread.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include<iostream>
#include<vector>
#include<map>
#include<string> 
#include<thread>
 
using namespace std;
void myPrint()
{
    
    
 cout << "线程开始运行" << endl;
 cout << "线程运行结束了" << endl;
}
 
int main()
{
    
    
 std::thread my2Obj(myPrint); // 可调用对象
 my2Obj.join();// 主线程阻塞在这,并等待myPrint()执行完
 cout << "wangtao" << endl;
 return 0;
}
 
detach(): 将主线程和子线程完全分离,子线程会驻留在后台运行,被C++运行时库接管,失去控制
 
void myPrint()
{
    
    
 cout << "线程开始运行1" << endl;
 cout << "线程开始运行2" << endl;
 cout << "线程开始运行3" << endl;
 cout << "线程开始运行4" << endl;
 cout << "线程开始运行5" << endl;
 cout << "线程开始运行6" << endl;
 cout << "线程开始运行7" << endl;
 cout << "线程开始运行8" << endl;
 cout << "线程开始运行9" << endl;
 
}
 
int main()
{
    
    
 std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
 my2Obj.detach();
 cout << "wangtao1" << endl;
 cout << "wangtao2" << endl;
 cout << "wangtao3" << endl;
 cout << "wangtao4" << endl;
 cout << "wangtao5" << endl;
 cout << "wangtao6" << endl;
 cout << "wangtao7" << endl;
 cout << "wangtao8" << endl;
 return 0;
}
 
joinable():判断是否可以成功使用join()或者detach()
 
程序说明:detach后不能在实施join
 
int main()
{
    
    
 std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
 if (my2Obj.joinable()){
    
    
 cout << "1:joinable() == true" << endl;
 }
 else {
    
    
 cout << "1:joinable() == false" << endl;
 }
 my2Obj.detach();
 
 if (my2Obj.joinable()) {
    
    
 cout << "2:joinable() == true" << endl;
 }
 else {
    
    
 cout << "2:joinable() == false" << endl;
 }
 cout << "wangtao1" << endl;
 cout << "wangtao2" << endl;
 cout << "wangtao3" << endl;
 cout << "wangtao4" << endl;
 cout << "wangtao5" << endl;
 cout << "wangtao6" << endl;
 cout << "wangtao7" << endl;
 cout << "wangtao8" << endl;
 return 0;
}
 
int main()
{
    
    
 std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
 if (my2Obj.joinable()){
    
    
 my2Obj.join();
 }
 cout << "wangtao1" << endl;
 cout << "wangtao2" << endl;
 cout << "wangtao3" << endl;
 cout << "wangtao4" << endl;
 cout << "wangtao5" << endl;
 cout << "wangtao6" << endl;
 cout << "wangtao7" << endl;
 cout << "wangtao8" << endl;
 return 0;
}
 
 
2.通过类对象创建线程
class CObject
{
    
    
public:
 void operator ()() {
    
    
 cout << "线程开始运行" << endl;
 cout << "线程结束运行" << endl;
 }
};
 
 
int main()
{
    
    CObject obj;
 std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
 if (my2Obj.joinable()){
    
    
 my2Obj.join();
 }
 cout << "see you " << endl;
 
 return 0;
}
 
 
class CObject
{
    
    
 int& m_obj;
public:
 CObject(int& i) :m_obj(i) {
    
    }
 void operator ()() {
    
     // 不带参数
 cout << "线程开始运行1" << endl;
 cout << "线程开始运行2" << endl;
 cout << "线程开始运行3" << endl;
 cout << "线程开始运行4" << endl;
 cout << "线程开始运行5" << endl;
 }
};
int main()
{
    
    
 int i = 6;
 CObject obj(i);
 std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
 if (my2Obj.joinable()){
    
    
 my2Obj.detach();
 }
 cout << "see you " << endl;
 
 return 0;
}detach() 主线程结束对象即被销毁,那么子线程的成员函数还能调用吗?
这里的的对象会被复制到子线程中,当主线程结束,复制的子线程对象并不会被销毁
只要是没有引用、指针就不会出现问题 
通过复制构造函数和析构函数来验证对象是否复制到了子线程中 
// MultiThread.cpp : Defines the entry point for the console application.
//
 
#include "stdafx.h"
#include<iostream>
#include<vector>
#include<map>
#include<string> 
#include<thread>
using namespace std;
class CObject
{
    
    
 int& m_obj;
public:
 CObject(int& i) :m_obj(i) {
    
    
 cout << "ctor" << endl;
 }
 CObject(const CObject& m) :m_obj(m.m_obj) {
    
    
 cout << "copy ctor" << endl;
 }
 ~CObject(){
    
    
 cout << "dtor" << endl;
 }
 void operator ()() {
    
     // 不带参数
 cout << "线程开始运行1" << endl;
 cout << "线程开始运行2" << endl;
 cout << "线程开始运行3" << endl;
 cout << "线程开始运行4" << endl;
 cout << "线程开始运行5" << endl;
 }
};
int main()
{
    
    
 int i = 6;
 CObject obj(i);
 std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
 if (my2Obj.joinable()){
    
    
 my2Obj.detach();
 }
 cout << "see you " << endl;
 
 return 0;
}

子线程的析构函数在后台执行,所以输出的dtor是主线程的。用join() 结果为:
3.通过lambda表达式创建线程

int main()
{
    
    
 auto myLamThread = [] {
    
    
 cout << "线程开始运行" << endl;
 cout << "线程结束运行" << endl;
 };
 thread cthread(myLamThread);
 cthread.join();
 std::cout << "see you " << endl;
 
 return 0;
}

6. 讲一讲C++的内联函数

内联函数inline:引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
1.在内联函数内不允许使用循环语句和开关语句;
2.内联函数的定义必须出现在内联函数第一次调用之前;
3.类结构中所在的类说明内部定义的函数是内联函数。

7. 宏定义的优缺点

优点:

  1. 提高了程序的可读性,同时也方便进行修改;
  2. 提高程序的运行效率:使用带参的宏定义既可完成函数调用的功能,又能避免函数的出栈与入栈操作,减少系统开销,提高运行效率;
    3.宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能。比如##连接符。

缺点:

  1. 由于是直接嵌入的,所以代码可能相对多一点;
  2. 嵌套定义过多可能会影响程序的可读性,而且很容易出错;
  3. 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。
  4. CPU如果访问内存?
    通过内存管理单元(MMU)
    先看一张简单的CPU访问内存的流程图:
    在这里插入图片描述

TLB:转换lookaside 缓存,有了它可以让虚拟地址到物理地址转换速度大增。
从上图中可以清楚的知道了,CPU,DDR,MMU它们三者之间的关系。CPU在MMU开启的情况下,访问的都是虚拟地址。
首先通过MMU将虚拟地址转换为物理地址,
然后再通过总线上去访问内存(我们都知道内存是挂在总线上的)。
那MMU是怎么将虚拟地址转换为物理地址呢?当然是通过页表的方式。MMU从页表中查出虚拟地址对应的物理地址是什么,然后就去访问物理内存了。

猜你喜欢

转载自blog.csdn.net/lingshengxueyuan/article/details/108602597