常见面试问题2

编程基础和操作系统

1. 编程基础

概念辨析

多态:类的多态和函数的多态

一个实体同时具备多种形式,在特定情况下,表现不同的状态,对应不同的属性和方法。C++中主要体现在运行和编译两个方面。C中通过钩子函数挂接才能达成同样的效果。

C++中主要通过虚函数实现,也可以通过抽象类、覆盖(重写)、模板。(重载和多态无关)

封装:

将抽象的道德数据和行为或功能相结合,形成有机整体“类”,数据和函数都是类的成员。

封装增强了安全性,简化了编程。

重载和重写:

重载overloading:不同的函数,相同的函数名不同的参数个数或者类型。调用的时候根据参数来区别。对权限没有要求。

重写overriding(覆盖):在派生类中重新对基类中的虚函数重新实现。函数名和参数都一样,只是函数的实现体不一样。调用的时候根据对象的类型来区别。被重写的方法不能拥有比父类更严格的权限。

重写是子类和父类之间的关系,是垂直关系,发生在继承中。重载是同一个类中的方法之间的关系,是水平关系,发生在一个类中。

虚函数:

用virtual关键字申明的函数,虚函数一定是类的成员函数。虚函数理解教程

纯虚函数是虚函数再加上=0

纯虚函数:virtual void fun()=0;即抽象类,必须在子类中实现这个函数,即先有名称,没有内容,在派生类实现内容。

继承派生:
闭包、垃圾回收的概念和过程:

java中的概念 不懂

例题:多集成中派生类对象结构
例题:虚函数表构成

存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始位置的虚指针。虚表和类对应,虚表指针和对象对应。

例题:虚函数实现多态的原理

在基类函数前加上virtual关键字,在派生类中重写该函数,运行时根据对象的实际类型来调用相应函数。如果对象类型是派生类,那么就调用派生类的函数,如果对象类型是基类,那么就调用基类的函数。

例题:构造函数可以定义为私有吗?

将构造函数声明为私有,可以确保本类以外的地方都不能实例化这个类 。

标准的单例模式就是构造函数私有。

1
2
3
4
5
6
7
 class A{
private:
A() {}
static A a;
public:
static A& Instance() { return a; }
};

编程语言的区别

JAVA/C/C++/SCALA

C是面向过程的。JAVA和C++是面向对象的。SCALA比较新。

面向对象编程与函数式编程

面向对象编程:封装、继承、多态;相对于面向过程而言的

基于对象编程:支持类但不支持多态,不提供抽象、继承和重载等有关面向对象语言的功能。

例题: C++弱指针,智能指针weak_ptr 防止内存泄漏

STL提供的智能指针一共有四种:auto_ptr(已经淘汰)、unique_ptr、shared_ptr和weak_ptr。

unique_ptr:

shared_ptr:shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。

weak_ptr:是一个引用计数型智能指针,不增加对象的引用计数,即弱引用。弱引用并不修改该对象的引用计数,这意味着弱引用并不对对象的内存进行管理,只要把循环引用的乙方使用弱引用即可解除循环引用。

弱引用的智能指针weak_ptr是用来监视shared_ptr的,不会使引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。weak_ptr没有重载运算符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中关连的资源是否存在。 weak_ptr还可以用来返回this指针和解决循环引用的问题。

strong_ptr是强引用,只要有一个指向对象的shared_ptr存在,该对象就不会被析构,直到指向对象的最后一个shared_ptr析构或是reset()时才会被销毁。利用weak_ptr可以解决常见的空悬指针问题以及循环引用问题。

例题:C++的内存和memory leak,编译过程中的内存分配

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

(1)堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap
Leak.
(2)系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

例题: const的使用(指针数组和数组指针辨析)

1
2
3
4
5
6
7
8
const int *p;  //修饰int 即*p指向的内容不变
int const *p; //修饰int 即*p指向的内容不变
int* const p; //修饰int* 即*p不能改变指向的地址
const int* const p; //都不能变

const int a = 10;
const int &b = a;
const int &c = 20;

2. 操作系统

进程和线程:一个进程可能包括多个线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。

进程之间通信:

管道:半双工;只用于具有亲缘关系的进程;特殊的文件,可以read、write但只存在内存中;速度慢,容量有限

FIFO:命名管道;任何进程间都能通讯,但速度慢

socket:

消息队列:消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识; 容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

信号量:不能传递复杂消息,只能用来同步

共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

例题:线程如何创建?如何实现互斥?线程池怎么创建?有几个线程?都执行一个函数吗?线程锁?

有两种常见的创建线程的方法,一种是继承Thread类,一种是实现Runnable的接口,Thread类其实也是实现了Runnable接口。但是创建这两种线程在运行结束后都会被虚拟机销毁,如果线程数量多的话,频繁的创建和销毁线程会大大浪费时间和内存,因为线程执行完毕后死亡,线程对象变成垃圾。使用线程池可以让线程运行完后不立即销毁而是重复使用,继续执行其他的任务。参考执棋手的博客

C++中的多线程类Thread。头文件为#include,通过std::thread应用。

构造函数 ThreadPool():

开启一定数量的线程,每开启一个线程,让该线程进入死循环,对接下来的操作,先利用互斥锁先加锁,再利用条件锁判断线程任务列表是否为空,若为空即阻塞该线程(就是没人办理业务时进入待机状态)。接下来,从任务列表中取任务,然后解开互斥锁,让其执行完后再重复以上操作。对于开启的每一个线程都是如此。

添加任务函数 enqueue():

先获取任务,加互斥锁,将该任务加入任务列表,解锁,然后唤醒一个任务,让其进行等待。

析构函数 ~ThreadPool():

唤醒所有的工作,让线程一个个的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include blablabla

typedef struct {
int first;
int last;
int result;
} MY_ARGS;

void* myfun(void* args){
int i = 0;
MY_ARGS* my_args = (MY_ARGS*) args;
int first = my_args -> first;
int last = my_args -> last;
pthread_mutex_lock(&lock);
for (i = 0; i < 10000; i++){
s = s + arr[i];
}
my_args -> result = s;
pthread_mutex_unlock(&lock);
return NULL;
}

int main (){
int i;
pthread_t th1;
pthread_t th2;
pthread_mutex_init(&lock, NULL);
MY_ARGS args1 = {0, 2500, 0};
MY_ARGS args2 = {2500, 5000, 0};

pthread_create(&th1, NULL, myfunc, &args1);
pthread_create(&th2, NULL, myfunc, &args2);
pthread_join(th1, NULL);
pthread_join(th2, NULL);

return 0;
}
例题:在Windows的一个磁盘下新建文件夹的系统操作过程
例题:在Linux系统中如果已知一个进程名,怎么输出PID号? (ps和top命令?

ps -ef |grep “name” | grep -v grep

ps -ef | awk ‘/[n]ame/{print $2}’

ps -x | awk ‘/[n]ame/{print $2}’

pgrep -f name

例题:Linux的进程调度

在处理器资源有限的系统中,所有进程都以轮流占用处理器的方式交叉运行。为使每个进程都有运行的机会,调度器为每个进程分配了一个占用处理器的时间额度,这个额度叫做进程的“时间片”,其初值就存放在进程控制块的counter域中。进程每占用处理器一次,系统就将这次所占用时间从counter中扣除,因为counter反映了进程时间片的剩余情况,所以叫做剩余时间片。

Linux调度的主要思想为:调度器大致以所有进程时间片的总和为一个调度周期;在每个调度周期内可以发生若干次调度,每次调度时,所有进程都以counter为资本竞争处理器控制权,counter值大者胜出,优先运行;凡是已耗尽时间片(即counter=0)的,则立即退出本周期的竞争;当所有未被阻塞进程的时间片都耗尽,那就不等了。然后,由调度器重新为进程分配时间片,开始下一个调度周期。

目前,标准Linux系统支持非实时(普通)和实时两种进程。与此相对应的,Linux有两种进程调度策略:普通进程调度和实时进程调度。因此,在每个进程的进程控制块中都有一个域policy,用来指明该进程为何种进程,应该使用何种调度策略。

Linux调度的总体思想是:实时进程优先于普通进程,实时进程以进程的紧急程度为优先顺序,并为实时进程赋予固定的优先级;普通进程则以保证所有进程能平均占用处理器时间为原则。所以其具体做法就是:

  • 对于实时进程来说,总的思想是为实时进程赋予远大于普通进程的固定权重参数weight,以确保实时进程的优先级。在此基础上,还分为两种做法:一种与时间片无关,另一种与时间片有关;
  • 对于普通进程来说,原则上以相等的weight作为所有进程的初始权重值,即nice=0,然后在每次进行进程调度时,根据剩余时间片对weight动态调整。
例题:日志分析用到的shell命令

CSDN上的一个总结博客

例题:Linux地址管理,分段,分页

例题:进程内存总大小,虚拟内存和物理内存,砖石继承

# 杂谈

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×