说明:
- 面试群,群号: 228447240
- 面试题来源于网络书籍,公司题目以及博主原创或修改(题目大部分来源于各种公司);
- 文中很多题目,或许大家直接编译器写完,1分钟就出结果了。但在这里博主希望每一个题目,大家都要经过认真思考,答案不重要,重要的是通过题目理解所考知识点,好应对题目更多的变化;
- 博主与大家一起学习,一起刷题,共同进步;
- 写文不易,麻烦给个三连!!!
目录
1.一个程序从开始运行到结束的完整过程,你能说出来多少?
答案:
四个过程:
(
1
)预编译
主要处理源代码文件中的以
“#”
开头的预编译指令。处理规则见下
1
、删除所有的
#define
,展开所有的宏定义。
2
、处理所有的条件预编译指令,如
“#if”
、
“#endif”
、
“#ifdef”
、
“#elif”
和
“#else”
。
3
、处理
“#include”
预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4
、删除所有的注释,
“//”
和
“/**/”
。
5
、保留所有的
#pragma
编译器指令,编译器需要用到他们,如:
#pragma once
是为了防止有文件被重 复引用。
6
、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
(
2
)编译
把预编译之后生成的
xxx.i
或
xxx.ii
文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
1
、词法分析:利用类似于
“
有限状态机
”
的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
2
、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
3
、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——
在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
4
、优化:源代码级别的一个优化过程。
5
、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列
——
汇编语言表示。
6
、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
(
3
)汇编
将汇编代码转变成机器可以执行的指令
(
机器码文件
)
。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as
完成。经汇编之后,产生目标文件(
与可执行文件格式几乎一样
)xxx.o(Linux
下
)
、
xxx.obj(Windows
下
)
。
(
4
)链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接。
2.操作系统在对内存进行管理的时候需要做些什么?
答案:
- 操作系统负责内存空间的分配与回收。
- 操作系统需要提供某种技术从逻辑上对内存空间进行扩充。
- 操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址的转换。
- 操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰。
3.说一下你理解中的内存?他有什么作用呢?
答案:
内存是计算机系统中的关键组成部分,用于存储和访问数据和指令。它是临时存储器,用于暂时保存正在执行的程序和数据,以供处理器快速读取和写入。
内存的作用主要有以下几个方面:
-
存储程序和数据:内存用于存储操作系统、应用程序和用户数据。当程序被加载到内存时,处理器可以直接从内存中读取指令并执行,而不需要频繁地从硬盘或其他存储设备读取数据。
-
提供快速访问:与硬盘等外部存储设备相比,内存的读取和写入速度非常快。这使得处理器可以更快地获取所需的数据,提高计算机的响应速度和效率。
-
支持多任务处理:内存使得同时运行多个程序成为可能。每个程序都可以在内存中占据一定的空间,处理器可以在它们之间切换,实现快速的上下文切换,使得多个任务可以并发执行。
-
缓存数据:内存中的缓存用于存储最常用的数据和指令,以提高处理器的访问速度。缓存能够根据处理器的需求快速地提供数据,减少了对主存的访问次数。
4.介绍一下几种典型的锁?
答案:
读写锁
- 多个读者可以同时进行读。
- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)。
- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。
互斥锁
- 一次只能一个线程拥有互斥锁,其他线程只有等待。
-
互斥锁是在抢锁失败的情况下主动放弃 CPU 进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)。
条件变量
互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个 线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足 时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互
斥的机制,条件变量则是同步机制。
自旋锁
如果进线程无法取得锁,进线程不会立刻放弃
CPU
时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU
做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。
5.内存交换是什么?有什么特点?
答案:
交换
(
对换
)
技术的设计思想
:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(
进程在内存与磁盘间动态调度
)
换入:把准备好竞争
CPU
运行的程序从辅存移到内存。
换出:把处于等待状态(或
CPU
调度原则下被剥夺运行权力)的程序从内存移到辅存,把内存空间腾出 来。
6.终端退出,终端运行的进程会怎样
答案:
终端在退出时会发送
SIGHUP
给对应的
bash
进程,
bash
进程收到这个信号后首先将它发给
session
下面的进程,如果程序没有对SIGHUP
信号做特殊处理,那么进程就会随着终端关闭而退出。
7.如何让进程后台运行
答案:
- 命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中了。
- ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程。
- nohup + &,将标准输出和标准错误缺省会被重定向到 nohup.out 文件中,忽略所有挂断 (SIGHUP)信号。
- 运行指令前面 + setsid,使其父进程编程init进程,不受HUP信号的影响。
- 将 命令+ &放在()括号中,也可以是进程不受HUP信号的影响。
8.进程终止的几种方式
答案:
1
、
main
函数的自然返回,
return
2
、调用
exit
函数,属于
c
的函数库
3
、调用
_exit
函数,属于系统调用
4
、调用
abort
函数,异常程序终止,同时发送
SIGABRT
信号给调用进程。
5
、接受能导致进程终止的信号:
ctrl+c (^C)
、
SIGINT(SIGINT
中断进程
)
exit
和
_exit
的区别:
exit 是 C 标准库中的一个函数,它用于正常终止程序并返回到操作系统。当调用 exit
函数时,程序会执行一系列清理工作,例如关闭文件、释放内存等,然后返回到操作系统。
_exit是一个操作系统提供的系统调用,它也可以用于终止程序,但不会执行像 exit
那样的清理工作。调用 _exit
函数会直接终止程序,并立即返回到操作系统,不会执行任何清理操作。
9.从堆和栈上建立对象哪个快?(考察堆和栈的分配效率比较)
答案:
从两方面来考虑:
- 分配和释放,堆在分配和释放时都要调用函数(malloc,free),比如分配时会到堆空间去寻找足够大小的空间(因为多次分配释放后会造成内存碎片),这些都会花费一定的时间,具体可以看看malloc和 free的源代码,函数做了很多额外的工作,而栈却不需要这些。
- 访问时间,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正的数据,而栈只需访问一次。另外,堆的内容被操作系统交换到外存的概率比栈大,栈一般是不会被交换 出去的。
因此,在无特殊需求下,栈的效率更高。
10.常见内存分配方式有哪些?
答案:
(1
) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static
变量。
(2
) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3
) 从堆上分配,亦称动态内存分配。程序在运行的时候用
malloc
或
new
申请任意多少的内存,程序员自己负责在何时用free
或
delete
释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
11.常见内存分配内存错误
答案:
(1)内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL
。如果指针
p
是函数的参数,那么在函数的入口处用
assert(p!=NULL)
进行检查。如果是用malloc
或
new
来申请内存,应该用
if(p==NULL)
或
if(p!=NULL)
进行防错处理。
(2)内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标
“
多
1”
或者
“
少
1”
的操作。特别是在
for
循环语句中,循环次数很容易搞错,导致数组操作越界。
(4)忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然挂掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中malloc
与
free
的使用次数一定要相同,否则肯定有错误(new/delete
同理)。
(5)释放了内存却继续使用它。
- 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新 设计数据结构,从根本上解决对象管理的混乱局面。
- 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束 时被自动销毁。
- 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。