2020年中兴C/C++ linux后台开发精选面试题及答案

  1. 堆的维护时间复杂度。
    假如有N个节点,那么高度为H=logN,最后一层每个父节点最多只需要下调1次,倒 数第二层最多只需要下调2次,顶点最多需要下调H次,而最后一层父节点共有2^(H-1) 个,倒数第二层公有2(H-2),顶点只有1(20)个,所以总共的时间复杂度为s = 1 * 2^(H-1) + 2 * 2^(H-2) + … + (H-1) * 2^1 + H * 2^0 将H代入后s= 2N - 2 - log2(N),近似的时间复杂度就是O(N)。

  2. CPU是怎么执行指令的?
    计算机每执行一条指令都可分为三个阶段进行。即取指令-----分析指令-----执行指 令。

取指令的任务是:根据程序计数器PC中的值从程序存储器读出现行指令,送到指令寄 存器。

分析指令阶段的任务是:将指令寄存器中的指令操作码取出后进行译码,分析其指令性 质。如指令要求操作数,则寻找操作数地址。

计算机执行程序的过程实际上就是逐条指令地重复上述操作过程,直至遇到停机指令可 循环等待指令。

一般计算机进行工作时,首先要通过外部设备把程序和数据通过输入接口电路和数据总 线送入到存储器,然后逐条取出执行。但单片机中的程序一般事先我们都已通过写入器 固化在片内或片外程序存储器中。因而一开机即可执行指令。

下面我们将举个实例来说明指令的执行过程:

开机时,程序计算器PC变为0000H。然后单片机在时序电路作用下自动进入执行程序 过程。执行过程实际上就是取出指令(取出存储器中事先存放的指令阶段)和执行指令 (分析和执行指令)的循环过程。

例如执行指令:MOV A,#0E0H,其机器码为“74H E0H”,该指令的功能是把操作数E0H 送入累加器,

0000H单元中已存放74H,0001H单元中已存放E0H。当单片机开始运行时,首先是进 入取指阶段,其次序是:

1 程序计数器的内容(这时是0000H)送到地址寄存器;

2 程序计数器的内容自动加1(变为0001H);

3 地址寄存器的内容(0000H)通过内部地址总线送到存储器,以存储器中地址译码电 跟,使地址为0000H的单元被选中;

4 CPU使读控制线有效;

5 在读命令控制下被选中存储器单元的内容(此时应为74H)送到内部数据总线上,因 为是取指阶段,所以该内容通过数据总线被送到指令寄存器。至此,取指阶段完成,进 入译码分析和执行指令阶段。

由于本次进入指令寄存器中的内容是74H(操作码),以译码器译码后单片机就会知道 该指令是要将一个数送到A累加器,而该数是在这个代码的下一个存储单元。所以,执 行该指令还必须把数据(E0H)从存储器中取出送到CPU,即还要在存储器中取第二个 字节。其过程与取指阶段很相似,只是此时PC已为0001H。指令译码器结合时序部件, 产生74H操作码的微操作系列,使数字E0H从0001H单元取出。因为指令是要求把取 得的数送到A累加器,所以取出的数字经内部数据总线进入A累加器,而不是进入指 令寄存器。至此,一条指令的执行完毕。单片机中PC=“0002H”,PC在CPU每次向存 储器取指或取数时自动加1,单片机又进入下一取指阶段。这一过程一直重复下去,直 至收到暂停指令或循环等待指令暂停。

CPU就是这样一条一条地执行指令,完成所有规定。

  1. 什么函数不能声明为虚函数?
    常见的不能声明为虚函数的有普通函数(非成员函数)、静态成员函数、内联成员函数、 构造函数和友元函数。以下将分别对这几种情况进行分析。

1)普通函数(非成员函数)只能overload(重载),不能被override(覆盖),不能 被声明为虚函数,因此,编译器会在编译时绑定函数。

2)静态成员函数不能是虚函数,因为静态成员函数对于每个类来说只有一份代码,所 有的对象都共享这一份代码,它不归某个对象所有,所以,它也没有动态绑定的必要性。

3)内联成员函数不能是虚函数,因为内联函数本身就是为了在代码中直接展开,减少 函数调用花费的代价而设立的,而虚函数是为了在继承后对象能够准确地执行自己的动 作,这是不可能统一的。再说,inline函数在编译时被展开,虚函数在运行时才能动 态地绑定函数。

4)构造函数之所以不能是虚函数,因为构造函数本来是为了明确初始化对象成员才产 生的,然而虚函数主要是为了在不完全了解细节的情况下也能正确处理对象。另外,虚 函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用虚函数来完 成你想完成的动作?

5)友元函数。C++语言不支持友元函数的继承,对于没有继承特性的函数没有虚函数 的说法。友元函数不属于类的成员函数,不能被继承。所以,友元函数不能是虚函数。

  1. 线上CPU爆高,请问你如何找到问题所在。
    1、top命令:Linux命令。可以查看实时的CPU使用情况。也可以查看最近一段时间 的CPU使用情况。

2、PS命令:Linux命令。强大的进程状态监控命令。可以查看进程以及进程中线程的 当前CPU使用情况。属于当前状态的采样数据。

3、jstack:Java提供的命令。可以查看某个进程的当前线程栈运行情况。根据这个 命令的输出可以定位某个进程的所有线程的当前运行状态、运行代码,以及是否死锁等 等。

4、pstack:Linux命令。可以查看某个进程的当前线程栈运行情况。

  1. 曹操南下攻打刘备,刘备派关羽守锦州,关羽派张飞去守城门。刘备又派诸葛亮去向孙权求援。孙权派兵攻打曹操.请画出UML图.在这里插入图片描述

  2. 红黑树如何插入和删除的?
    插入:

(1)如果父节点为黑色,直接插入不处理

(2)如果父节点为红色,叔叔节点为红色,则父节点和叔叔节点变为黑色,祖先节 点变为红色,将节点操作转换为祖先节点

(3)如果当前节点为父亲节点的右节点,则以父亲结点为中心左旋操作

(4)如果当前节点为父亲节点的左节点,则父亲节点变为黑色,祖先节点变为红色, 以祖先节点为中心右旋操作

删除:

(1)先按照排序二叉树的方法,删除当前节点,如果需要转移即转移到下一个节点

(2)当前节点,必定为这样的情况:没有左子树。

(3)删除为红色节点,不需要处理,直接按照删除二叉树节点一样

(4)如果兄弟节点为黑色,兄弟节点的两个子节点为黑色,则将兄弟节点变为红色, 将着色转移到父亲节点

(5)如果兄弟节点为红色,将兄弟节点设为黑色,父亲结点设为红色节点,对父亲 结点进行左旋操作

(6)如果兄弟节点为黑色,左孩子为红色,右孩子为黑色,对兄弟节点进行右旋操 作

(7)如果兄弟节点为黑色,右孩子为红色,则将父亲节点的颜色赋值给兄弟节点, 将父亲节点设置为黑色,将兄弟节点的右孩子设为黑色,对父亲节点进行左旋

  1. Linux操作系统重要组成部分?
    Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。内核、shell 和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并 使用系统。

1.Linux内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、 共 享库、需求加载、可执行程序和TCP/IP网络功能。Linux内核的模块分为以下几个 部分:存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信、系统的初 始化和系统调用等。

2.Linux shell是系统的用户界面,提供了用户与内核进行交互操作的一种接 口。 它接收用户输入的命令并把它送入内核去执行,是一个命令解释器。另外,shell 编 程语言具有普通编程语言的很多特点,用这种编程语言编写的shell程序与其他应 用程序具有同样的效果。

3.Linux文件系统文件系统是文件存放在磁盘等存储设备上的组织方法。Linux系统 能支持多种目前流行的文件系统,如EXT2、EXT3、FAT、FAT32、VFAT和ISO9660。

4.Linux应用程序标准的Linux系统一般都有一套都有称为应用程序的程序集,它 包括文本编辑器、编程语言、XWindow、办公套件、Internet工具和数据库等。

  1. 如果在构造函数中调用memset(this, 0, sizeof(*this))来初始化内存空间,有什么问题吗?
    对于有虚函数和虚表存在的类,在进行memset后不能调用虚函数和虚基表继承而来的 数据和函数

  2. TCP的nagle算法和延迟ack,还有CORK呢?他们有什么好处?一起用会有什么效果?你觉得可以有什么改进?
    nagle算法:防止网络中存在太多小包而造成网络拥塞

延迟ack:减少ACK包的频繁发送

CORK:将多个包变成一个包发送,提高网络利用率,使载荷率更大

  1. 什么是协程?
    协程是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个 线程也可以拥有多个协程。
    在这里插入图片描述

最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户 态执行)。

这样的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

其中 yield 是python当中的语法。当协程执行到yield关键字时,会暂停在那一行, 等到主线程调用send方法发送了数据,协程才会接到数据继续执行。

但是,yield让协程暂停,和线程的阻塞是有本质区别的。协程的暂停完全由程序控制, 线程的阻塞状态是由操作系统内核来进行切换。

因此,协程的开销远远小于线程的开销。

  1. 编写类String的构造函数、析构函数和赋值函数。
    已知类String的原型为:

class String
{
public:
String(const char *str = NULL); //普通构造函数
String(const String &other); //拷贝构造函数
~ String(void); //析构函数
String & operator =(const String &other); //赋值函数
private:
char *m_String; //私有成员,保存字符串
};

#include
using namespace std;
class String
{
public:
String(const char *str = NULL); //普通构造函数
String(const String &other); //拷贝构造函数
~ String(void); //析构函数
String & operator =(const String &other); //赋值函数
private:
char *m_String; //私有成员,保存字符串
};

String::~String(void)
{
cout << “Destructing”<< endl;
if (m_String != NULL) //如果m_String不为NULL,释放堆内存
{
delete [] m_String;
m_String = NULL; //释放后置为NULL
}
}

String::String(const char *str)
{
cout << “Construcing” << endl;
if(str == NULL) //如果str为NULL,存空字符串""
{
m_String = new char[1]; //分配一个字节
*m_String = ‘\0’; //将之赋值为字符串结束符
}
else
{
//分配空间容纳str内容
m_String = new char[strlen(str) + 1];
//拷贝str到私有成员
strcpy(m_String, str);
}
}

String::String(const String &other)
{
cout << “Constructing Copy” << endl;
//分配空间容纳str内容
m_String = new char[strlen(other.m_String) + 1];
//拷贝str到私有成员
strcpy(m_String, other.m_String);
}

String & String:perator = (const String &other)
{
cout << “Operate = Function” << endl;
if(this == &other) //如果对象与other是同一个对象
{ //直接返回本身
return *this;
}
delete [] m_String; //释放堆内存
m_String = new char[strlen(other.m_String)+1];
strcpy(m_String, other.m_String);
return *this;
}

int main()
{
String a(“hello”); //调用普通构造函数
String b(“world”); //调用普通构造函数
String c(a); //调用拷贝构造函数
c = b; //调用赋值函数

return 0;
}
12. 确保线程安全的几种方法?
确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、过度优化。

1.竞争与原子操作

多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很 多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一 半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的 (Atomic),因为不管怎样,单条指令的执行是不会被打断的。

因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原 子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就 要选用其他的方法了。

2.同步与锁

为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对 同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得 对同一个数据进行访问。

同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或 资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取 锁时,线程会等待,直到锁重新可用。

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一 个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称 信号量)。

3.可重入

一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进 入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。 可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

4.过度优化

在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可 能对代码进行过度的优化以确保线程安全。

我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译 器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作 volatile变量的指令顺序。

在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办 法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指 令交换到barrier之后,反之亦然。

  1. 进程切换发生的原因?处理进程切换的步骤?
    原因:中断发生;更高优先级进程唤醒;进程消耗完了时间片;资源阻塞;

步骤:

(1)保存处理器的上下文

(2)用新状态和其它相关信息更新正在运行进程 的PCB

(3)将原来的进程移到合适的队列中【就绪,阻塞】

选择另外一个执行的 进程,更新被选中进程的PCB,将它加载进CPU

  1. 拥塞控制的方式?具体怎么做的?快重传的时机是什么?
    (1)慢开始

(2)拥塞避免

(3)快重传【收到3个失序分组确认】

(4)快恢复

  1. 如何判断两个结构体是否相等?
    判断两个结构体是否相等:重载操作符"=="

不能用函数memcpy来判断两个结构体是否相等:memcmp函数是逐个字节进行比较的, 而struct存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无 法比较。

#include
using namespace std;

struct s
{
int a;
int b;
bool operator == (const s &rhs);
};

bool s::operator == (const s &rhs)
{
return ((a == rhs.a) && (b == rhs.b));
}

int main()
{
struct s s1, s2;
s1.a = 1;
s1.b = 2;
s2.a = 1;
s2.b = 2;
if (s1 == s2)
cout << “两个结构体相等” << endl;
else
cout << “两个结构体不相等” << endl;
return 0;
}
篇幅有限,今天先分享到这里,需要更多面试题及学习资料的可以加Q裙812855908,免费分享
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_40989769/article/details/105053431