编程基础和操作系统
1. 编程基础
概念辨析
多态:类的多态和函数的多态
一个实体同时具备多种形式,在特定情况下,表现不同的状态,对应不同的属性和方法。C++中主要体现在运行和编译两个方面。C中通过钩子函数挂接才能达成同样的效果。
C++中主要通过虚函数实现,也可以通过抽象类、覆盖(重写)、模板。(重载和多态无关)
封装:
将抽象的道德数据和行为或功能相结合,形成有机整体“类”,数据和函数都是类的成员。
封装增强了安全性,简化了编程。
重载和重写:
重载overloading:不同的函数,相同的函数名不同的参数个数或者类型。调用的时候根据参数来区别。对权限没有要求。
重写overriding(覆盖):在派生类中重新对基类中的虚函数重新实现。函数名和参数都一样,只是函数的实现体不一样。调用的时候根据对象的类型来区别。被重写的方法不能拥有比父类更严格的权限。
重写是子类和父类之间的关系,是垂直关系,发生在继承中。重载是同一个类中的方法之间的关系,是水平关系,发生在一个类中。
虚函数:
用virtual关键字申明的函数,虚函数一定是类的成员函数。虚函数理解教程
纯虚函数是虚函数再加上=0
纯虚函数:virtual void fun()=0;即抽象类,必须在子类中实现这个函数,即先有名称,没有内容,在派生类实现内容。
继承派生:
闭包、垃圾回收的概念和过程:
java中的概念 不懂
例题:多集成中派生类对象结构
例题:虚函数表构成
存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始位置的虚指针。虚表和类对应,虚表指针和对象对应。
例题:虚函数实现多态的原理
在基类函数前加上virtual关键字,在派生类中重写该函数,运行时根据对象的实际类型来调用相应函数。如果对象类型是派生类,那么就调用派生类的函数,如果对象类型是基类,那么就调用基类的函数。
例题:构造函数可以定义为私有吗?
将构造函数声明为私有,可以确保本类以外的地方都不能实例化这个类 。
标准的单例模式就是构造函数私有。
1 | class 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 | const int *p; //修饰int 即*p指向的内容不变 |
2. 操作系统
进程和线程:一个进程可能包括多个线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。
进程之间通信:
管道:半双工;只用于具有亲缘关系的进程;特殊的文件,可以read、write但只存在内存中;速度慢,容量有限
FIFO:命名管道;任何进程间都能通讯,但速度慢
socket:
消息队列:消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识; 容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
信号量:不能传递复杂消息,只能用来同步
共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
例题:线程如何创建?如何实现互斥?线程池怎么创建?有几个线程?都执行一个函数吗?线程锁?
有两种常见的创建线程的方法,一种是继承Thread类,一种是实现Runnable的接口,Thread类其实也是实现了Runnable接口。但是创建这两种线程在运行结束后都会被虚拟机销毁,如果线程数量多的话,频繁的创建和销毁线程会大大浪费时间和内存,因为线程执行完毕后死亡,线程对象变成垃圾。使用线程池可以让线程运行完后不立即销毁而是重复使用,继续执行其他的任务。参考执棋手的博客
C++中的多线程类Thread。头文件为#include
构造函数 ThreadPool():
开启一定数量的线程,每开启一个线程,让该线程进入死循环,对接下来的操作,先利用互斥锁先加锁,再利用条件锁判断线程任务列表是否为空,若为空即阻塞该线程(就是没人办理业务时进入待机状态)。接下来,从任务列表中取任务,然后解开互斥锁,让其执行完后再重复以上操作。对于开启的每一个线程都是如此。
添加任务函数 enqueue():
先获取任务,加互斥锁,将该任务加入任务列表,解锁,然后唤醒一个任务,让其进行等待。
析构函数 ~ThreadPool():
唤醒所有的工作,让线程一个个的执行。
1 | #include blablabla |
例题:在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动态调整。