面试理论性问题汇总

失业专用……

C/C++

extern C的作用:
由于C++支持函数重载,但是C并不支持,函数在编译在库中的名字和C的不同,所以用extern C来表明这个函数或者变量是用C语言来编译和连接的,这样做可以解决名字匹配问题,实现C++和C的混合变成。

const的作用:
1、想要阻止一个变量被改变,可以用const关键字
2、用于修饰函数的参数和返回值等,被修饰的东西都受到了强制保护,能预防意外的发生,提高程序的健壮性。
3、const既可以修饰指针本身,也可以修饰所指向的数据
4、const常量赋值时必须被初始化。

const和define的区别:
1、const有数据类型,而define没有。
2、const能由编译器进行安全检查,但是define只是单纯的字符串替换,没有安全性。
3、集成化的调试工具能对const常量进行调试,但是不能对宏常量进行调试。

用宏实现min:#define MIN(a,b) ((a) > (b) ? (b) : (a))

在const成员函数中,用mutable修饰成员变量名之后就可以修改类的成员变量了。

sizeof是一个运算符,sizeof(),括号内的内容是被替代类型,如:
int a = 8;sizeof(a),这个a会被替代成int。用来计算一个类型的字节大小。
如果sizeof里面是指针,返回的永远是4(备注:64位系统指针返回的是8)

static的作用:
1、该变量的内存只被分配一次,其值在下次调用时维持上次的值
2、模块内的static全局变量可以被模块内的所有函数访问,但不能被模块外的其他函数访问
3、在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝
4、在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量

内联函数和宏的差别:
1、内联函数在编译的时候可以直接被镶嵌到目标代码里去,宏只是简单的替换。
2、内联函数要做参数安全检查,宏不能。
3、内联函数是以增加空间消耗为代价的。

内联函数的适用:
1、一个函数不断被重复调用。
2、函数简短,且不包含for、while、switch等语句。

指针和引用的区别:
1、非空区别。任何情况下引用都不可能指向空值。
2、合法性区别。由于指针可能为空值,所以需要被测试,而引用则不需要测试合法性。
3、可修改区别。指针可以被赋值指向不同的对象,但是引用则只能指向在初始化时被指定的对象,但是对象的内容是可以改变的。
4、sizeof(引用)返回的是变量的大小

struct和类的区别是struct里面默认的访问控制是public,class里是private。

覆盖和重载:
覆盖是子类重新定义父类虚函数的做法,重载是允许存在多个重名函数,但这些函数的参数表不同。

派生类的三种继承方式:
1、公有继承方式:公有继承时,派生类的对象可以访问基类中的公有成员,派生类的的成员函数可以访问基类中的公有成员和保护成员。
2、私有继承方式:私有继承时,基类的成员只能由直接派生类访问,无法再往下继承。
3、保护继承方式:与私有继承方式相似。

private修饰符表示只能由同类里面的成员访问,protected修饰符表示继承时子类可以对基类拥有完全访问权。

虚函数和多态:
多态简而言之就是用父类型的指针指向子类中的实例,通过父类的指针调用实际子类的中的成员函数
虚函数的创建过程:在编译时,编译器会给每个有虚函数的类创建一个虚函数表,指明了每一个虚函数实际应该调用的函数,同一类的不同实例共用同一份虚函数表
虚析构函数的作用:为了使基类的指针删除派生类的对象时,也能够调用派生类的析构函数

关于虚函数的具体讲解

内存分配方式:
1、栈区:由编译器自动分配释放(即我们程序中用到的int之类的)
2、堆区:由程序员自己控制分配释放,如果不管他,结束的时候可能会由系统自动回收(用malloc或者new申请的东西)
3、全局区(静态区):全局变量和静态变量的存储是放在一起的,在程序结束之后由系统释放
4、文字常量区
5、程序代码区:存放函数体二进制代码的地方

内存相关的常见错误:
1、内存分配可能失败,所以在使用之前要检查指针是否为空
2、内存虽然分配成功了,但是没有初始化就开始使用了
3、操作越过了内存的边界
4、使用完毕之后没有及时释放内存,导致内存泄露
5、内存释放之后还在继续使用

如何限制类只在堆(栈)上分配内存?
在C++中,类的对象建立分为两种,静态建立比如A a;另一种是动态建立比如A* ptr=new A。
1、只能在堆上分配内存的话,首先想到是禁止直接调用构造类,但是这样的话new的时候也不能调用了,在实际当中,编译器会检查构造的对象中的非静态函数,所以可以把类的析构函数设为私有,这样编译器就不会在栈空间上给类对象分配内存了。但是这样不能解决子类继承问题,可以改用protect成员,类外不能访问。其次就是类的使用不方便,可以new缺不能delete,为了统一可以把构造函数也设为protect,然后用一个静态函数来实现构造。

class A  
{  
protected:  
    A(){}  
    ~A(){}  
public:  
    static A* create()  
    {  
        return new A();  
    }  
    void destory()  
    {  
        delete this;  
    }  
};  

2、只能在栈上分配内存的话,重写operator new就好了

class A  
{  
private:  
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的  
    void operator delete(void* ptr){} // 重载了new就需要重载delete  
public:  
    A(){}  
    ~A(){}  
};  

new和malloc的区别:
1、属性:new是c++操作符,只要编译器支持就好了;malloc是库函数,需要头文件
2、参数:new申请内存时不需要制定大小,由编译器自行控制;malloc需要显式地指出需要的内存大小
3、返回类型:内存分配成功时,new返回的是对象类型的指针;malloc返回的是*void,需要自己进行强制类型转换
4、new g[]的时候,先把内存全部申请好,再多次调用构造函数,delete的时候先多次调用析构函数,再一次性释放内存;而malloc和free只能申请和释放动态内存,干不了别的

智能指针:
使用普通指针容易造成内存泄漏等问题,使用智能指针可以更好的管理堆内存。
c++提供了shared_prt类来作为智能指针使用,他使用的是引用计数,每一个shared_ptr的拷贝都指向相同的内存,每使用一次,内部引用计数加一,每析构一次,引用计数减一,当减为0时,自动删除所指向的堆内存。

函数参数的执行顺序:参数入栈时是从右向左入栈的,但是入栈前会把参数列表从右向左算一遍得到表达式的结果,对于一般的操作,参数入栈时的值时直接从变量的地址取的,但是对于a++编译器会开辟一个缓冲区把a存进去,再进行运算,最后入栈的时候时从缓冲区取值的,下面来举个例子,a=10;printf("%d %d %d\n",a++,++a,a,a++)得到的结果就是12,13,13,10,理解一下就会了

线程与进程相关

多线程和多进程的区别?
进程具有独立的地址空间,由操作系统管理,进程与进程之间是逻辑隔离的,一般不会互相影响,优点是隔离性高,稳定,缺点是信息资源共享麻烦;
线程是进程启动的执行单元,优点是创建、销毁、切换比较方便,CPU利用率更高,占用内存少,缺点是需要管控的东西比较多,线程之间互相影响出问题的几率更大,一个线程挂了会导致整个进程都挂掉,所以才有线程安全这个说法

什么是线程安全?
线程安全的代码会通过同步机制保证各个线程都是正常的执行,不会出现数据污染的情况

线程与进程应该怎么选择?
在需要频繁创建和销毁,要处理大量的运算和数据,需要及时响应的,一般都用线程,这类运算会消耗大量的CPU,比如图像处理、算法处理等。一些操作是并发的且会阻塞的话,也推荐使用多线程;例如socket或者磁盘操作这种,使用进程更好,因为他更稳定,且内存隔离,单进程的崩溃不会导致应用的崩溃

线程之间是如何通信的?
一种是使用全局变量进行通信,还有一种是可以通过自定义消息机制进行通信,这是利用了Windows的消息驱动系统,当线程1发送消息后,操作系统会先收到消息,然后再把消息转发给线程2,这里要保证线程2已经建立了消息循环。线程之间通信的目的主要是用于线程同步,这个在下面会写

进程之间是如何通信的?
进程之间通信的方式包括管道,消息队列,共享内存,socket等,如果通信的内容较少,或者是需要信号来出发某些行为的话,用消息机制就好了。但是如果通信的内容比较多或者有需要数据交换则需要使用共享内存或者建立socket等通信方式

名词解释:
管道是一种存在于内存中的特殊文件,有自己的数据结构,他只允许单向通信,只允许有血缘关系的进程之间通信
信号量不以传输数据为主要目的,主要是为了保护共享资源,使得共享资源在同一时刻只有一个进程读写
消息队列由操作系统来实现消息发送和接受之间的同步,他允许任何进程通过共享消息队列来实现进程间的通信,但是由于消息的复制很耗费CPU,所以一般不应用在消息量大或者操作频繁的场合
共享内存通过将共享的内存区附加到进程的虚拟地址空间来实现,他利用内存缓冲区来实现信息交换,交换速度快,允许的信息量大,但是由于他没有任何的同步和互斥机制,所以一般通过信号量来实现共享内存的读写同步

线程同步和线程异步
和普通的意思一样,同步就是当一个线程执行完之后才能执行下一个线程,异步就是不用等待该线程执行完,别的线程就开始执行了

一般同步使用在需要同时访问某一块数据的时候,比如线程A和B都要访问某数据库,那么就需要同步,有一种情况不需要同步,就是原子操作;异步的话比如一场游戏中的背景音乐和游戏图像,就是互不相干的,就可以使用异步

多线程的同步和互斥
临界区:适合一个进程的多个线程访问公共区域或代码段时使用
互斥量:是处于加锁或解锁状态的一个变量,用于管理共享资源,能适用于不同进程内多线程的公共资源访问,如果是某进程内部使用的话,更推荐临界区,速度快,占用资源少
信号量:信号量与互斥量的不同在于允许多个线程同时访问公共资源,相当于操作系统的PV操作,在一开始时会设置好最大的允许线程数,线程占用数满时,别的线程就不能进来了,只有等他们释放了资源之后,别的线程才可以进来
事件:通过通知操作的方式保持线程同步
PS:互斥量,信号量,事件都是在内核中的,可以跨进程使用

同步和互斥的异同:
线程同步是指线程之间有制约关系,一个线程的执行依赖于另一个线程的消息,在没有得到消息时等待,直到消息到达才会唤醒,比如生产者要生产产品去卖给消费者,就在生产者和消费者之间弄一个缓冲区,只有当缓冲区为空时,生产者才会往缓冲区扔产品,而只有缓冲区不为空时,消费者才会从缓冲区买产品,此时就要保持同步
线程互斥可以看作时特殊的线程同步,指对共享资源进行访问时的资源排他性,只允许一个线程去访问该共享资源,比如一个数据库在线程A修改完之前不能给其他线程进行读取等

死锁
进程间进行通信或由于对资源的竞争导致的永久阻塞,若没有外力会永远保持该状态
产生原因:1、系统资源不足。2、系统资源分配不当。3、进程运行顺序和速度不同
产生死锁的必要条件:
1、互斥条件:一个资源只能被一个进程使用
2、请求与保持条件:一个进程在因为请求其他资源而阻塞的同时,不释放自身已经获得的资源
3、不剥夺条件:一个进程在没有主动释放资源之前,不会强行剥夺该资源
4、循环条件:若干进程之间满足头尾相接的资源等待条件

线程池
线程池就是一堆已经创建好的线程,最大数目一定,然后初始后都处于空闲状态,当有新任务进来时就从线程池中取出空闲线程处理任务,任务完成之后又重新放回去,当线程池中的所有线程都在任务时,只能等待有线程结束任务才能继续执行。

数据库

索引特点:
B树或者B+树是数据库索引的数据结构;
避免了对数据库的全表扫描,只需要访问少量的索引页和数据页就可以了,对于非聚集索引,甚至可以不访问数据页;
索引的创建和维护需要额外的空间和时间;
索引要选择那种,不是绝大多数都一样,但又是极少数才相同的数据来建立

聚集索引:在mysql中主键就是索引,一个表只能有一个聚集索引,类似于电话簿按姓名排序
非聚集索引:其逻辑顺序和磁盘上的存储顺序不相同,可以有多个

B+树
他是一颗N叉树,所有的数据都保存在叶子节点中,这些叶子节点本身按照关键字顺序链接成一个链表;
所有的非叶子节点可以看作是索引部分,仅包含关键字的最值,他的高度比红黑树之类的二叉树低,减少了IO次数,所以更适合于数据库和文件系统;
他的插入是自底向上插入的;
每次新建节点时,分配一个页的内存,和磁盘上的存储分配按页对齐,从而实现了访问一个node只需要一次IO;

事务
第一次听到事务这个词是我在听说了多线程访问数据库,数据库会自己加锁的时候,我问大神那外部还要不要加锁,答曰:如果不是原子操作,就弄一个事务操作,commit一下。然后就大概理解什么是事务了,应该就是把一块连续逻辑的数据库操作当成一个整体,防止数据库更改到一半发现无法访问造成的数据污染。他具有以下四个特性:
0、原子性:一个事务中的所有操作,要么全都执行,要么全都不执行,不会结束在中间某个环节,如果执行到一半出错,会回滚已经执行的操作,还原到事务执行前的状态
1、一致性:事务执行前和执行后,数据库的完整性没有被破坏
2、隔离性:多个事务可以并发对同一个数据库中的数据进行读写操作,隔离性可以防止多个事务并发时由于交叉执行导致的数据污染
3、持久性:事务执行完毕后,对数据库的修改是永久的,不会因为故障而丢失

计算机网络

TCP/IP的三次握手四次释放
从网上拿了两张图,能看清不同阶段的状态名字

为什么释放多了一次?
因为当主机B收到主机A的要求断开连接的数据包的时候,可能还在继续向主机A发送数据,所以会先发送确认收到要求的数据包,等数据全部传输完成之后再发送要求断开连接的数据包。

如果三次握手中S端一直收不到C端的ACK包会怎么样?
会每隔x秒再发送一次SYN+ACK包,多次无响应自动关闭连接,此时C端还在往S端写数据,S端将发送RST包(用于强制关闭TCP连接)响应。

为什么TIME_WAIT状态要经过2MSL(最大报文段生存时间)才返回CLOSE状态?
因为如果第一个ACK包丢失,S端会再发送FIN包过来,为了避免这种情况,需要等待两倍的最大报文段生存时间,也就是ACK包和FIN包发送的总和时间,再断开连接比较好。

为什么不能两次握手?
因为如果第二次发送的ACK+SYN包丢失,就会造成C端不知道到底有没有连接成功,一直在等待,而S端发送数据超时后,重复发送数据,造成了死锁现象。

TCP和UDP的区别:
1、一个面向连接,一个面向非连接
2、一个传输速度慢,一个传输速度快
3、TCP的传输可靠性更高
4、UDP的传输速度更快,有更好的实时性
5、TCP只支持点对点,UDP支持一对多,多对一,多对多

UDP是与TCP相对应的协议,是面向非连接的协议,不与对方建立连接,而是直接把数据包发送过去。这样看来UDP的通信效率高,但是TCP的可靠性比较高。UDP适用于一次只传送少量数据。

HTTP的工作过程
1、地址解析
如果浏览器请求http://localhost.com:8080/index.html
从中分析出协议名、主机名、端口、对象路径等部分,上面那个地址的解析结果为:
协议名:http
主机名:localhost.com
端口:8080
对象路径:index.html
在这一步,需要域名系统DNS解析域名,来得到主机的IP地址。
2、封装HTTP请求数据包
把以上部分结合自己本机的信息,封装成一个HTTP请求数据包
3、封装成TCP包,建立TCP连接(此时考三次握手)
4、客户机发送请求命令
5、服务器响应
6、服务器关闭TCP连接

get和Post的区别:
get的请求是在url中传参的,有长度限制,由于参数暴露,所以相比较于post更不安全
post的请求是存在request的body里的,没有长度限制
一般get用于查询之类的操作,post则更适用于提交表单

socket:
为什么服务器端在listen之前要调用bind函数?
通常服务器在启动的时候会绑定一个众所周知的地址,用于提供服务,这样客户端才能方便的通过它来连接服务器,而客户端只要由系统自动分配一个端口号和自身的ip地址结合就好了。

阻塞式和非阻塞式(主要针对服务端):
阻塞调用是指在结果返回之前,当前线程会被挂起(这个状态下CPU不会给该线程分配时间片)。函数只有在得到结果之后才会返回。
非阻塞式指在不能立刻得到结果的时候,该函数不会阻塞该线程,而会立即返回,并不断去查询是否已经得到返回结果。

同步与异步(主要针对客户端):
同步是指在发出一个功能调用之后,没有得到结果之前,该调用是不会返回的。也就是事情必须一件一件做。
异步与之相对,在调用部分完成之后,通过状态、通知和回调来通知调用者。

算法方面

今天面试又考到了排序算法的复杂度啊稳定度啊,没记牢,答得迷迷糊糊,记录一下。

快排在每次只能筛一个出去的时候,就会退化成冒泡排序,所以最差是O(N^2)
归并排序就是两两分组,然后归并成四四,再归并成八八
基数排序是先按照个位数,分别丢到十个桶里去,拿出来之后再按十位数继续丢,直至排序完毕

红黑树
在STL中运用到红黑树的有set,map等
特性:
0、每个节点非红即黑
1、根节点是黑色的
2、空的叶子节点是黑色的
3、一个节点是红色的话,他的子节点必须是黑色的
4、从根节点到任意一个空子节点的路径上的黑色节点数量是一致的
最坏情况下的树的深度为2logn,也就是两倍的全黑路径长度,最坏复杂度为logn

添加操作:首先把红黑树当作是普通二分查找树,插入节点;然后把节点变成红色;进行旋转或着色,使其重新成为一颗红黑树

因为最坏情况下也是logn的复杂度,所以优于BST(二分查找树)
相比较于AVL(完全平衡树)虽然时间复杂度一样,但是统计性能更高,插入新节点后的维护操作还是红黑树比较快

快速排序
快速排序一直用库函数sort,懒得自己去仔细看或者实现一遍,这里简单说一下快排的实现和非递归写法
快排是一种交换排序,排序方式是在当前数组中随机选一个数,把所有比他大的扔到他的后面去,所有比他小的扔到他的前面去,下面扔一下他的交换函数

int gao(vector<int>& nums, int l, int r)
    {
        int loc = l;
        int num = nums[l];
        for(int i = l + 1;i <= r;i++)
        {
            if(num < nums[i])
                swap(nums[i], nums[++loc]);
        }
        swap(nums[loc],nums[l]);
        return loc;
    }

非递归的做法就是用栈来存还需要进行交换的数组的头尾

stack<int>st;
st.push(0);
st.push(nums.size() - 1);
while(!st.empty())
{
    int high = st.top();st.pop();
    int low = st.top();st.pop();
    int mid = gao(nums,low,high);
    if(mid < high - 1)
    {
        st.push(mid+1);
        st.push(high);
    }
    if(mid > low + 1)
    {
        st.push(low);
        st.push(mid-1);
    }
}

也可以自己去写着试一试题目地址

A*算法

先放一个之前看到的很棒的各种寻路算法的展示demo

在我的理解之中,A*算法就是一个优先队列+结构体然后套上搜索的算法,内部的排序方式为G+H,G为当前移动已经消耗的费用,H为预估还会消耗的费用。在我的上一款moba类端游项目中,我参与设计了遥感类移动的寻路算法,所有的A*算法主要的不同就在于对于移动消耗的费用的不同。例如我希望他尽量少的进行拐弯操作,就会增加一下改变方向移动的费用;如果我希望搜索出来的路径尽量不要极限地贴着障碍物,那么在移动的时候如果下一个点附近有障碍物,就对应的根据和障碍物的距离增加一定的消耗,等等。

设计模式

这里只简单的写一些概念,具体实现之类的去runoob看

单例模式
解释:保证了一个类只会被实例化一次,并提供了供全局访问的一个点
使用情况:当一个实例会被频繁的创建和销毁时使用,使用单例模式之后可以控制实例数量,节省系统资源
关键点:构造函数是私有函数,这样就保证其不会在外部被实例化
一般实现方式:在第一次被调用的时候创建该实例
优点:在内存中只有一个实例,减少了内存开销;避免了某实例频繁的创建和销毁;避免对资源的多重占用
缺点:没有接口,不能继承
适用场景:比如一个文件会被多个进程操作,此时应用可以保证文件的处理通过唯一的实例进行;一个医院只能有一个院长;在web页面中的计数器,先用单例存起来,最后再更新到数据库中去

工厂模式
解释:定义了一个创建类的接口,让子类自己决定实例化哪一个工厂类
关键点:创建实例在子类进行
优点:调用者想要创建一个实例只需要知道名字就好了;扩展性高,想要增加一个产品,只要扩展一个工厂类就好了
缺点:每增加一个产品都要增加一个具体的产品类和对象实现工厂,增加了系统的复杂度
适用场景:当一个类的构造比较复杂的时候,使用工厂模式,就不需要关注他的构造过程,直接用就好了,
吐槽:感觉工厂模式就没啥用啊……构造不都是在构造函数里实现的么……理解起来感觉就是简单的封装,数据库上可以用一下

代理模式
解释:为其他对象提供代理以控制对这个对象的访问
解决问题:当直接访问某对象时,由于权限、对象创建开销大、需要进程外的访问等问题,需要在访问此对象时加上对该对象的访问层
优点:扩展性高,智能化
缺点:由于多出了中间环节,所以可能会造成请求的速度变慢

观察者模式
解释:当对象中的一对多关系,一个对象的状态发生改变时,所有依赖于他的对象都会收到通知并被自动更新
解决问题:当一个对象改变需要通知其他对象时,考虑到了易用和降低耦合
缺点:观察者和观察目标之间存在循环依赖关系的话,可能导致崩溃
使用场景:一个对象想通知其他对象,但是并不知道到底要通知谁

Redis

Redis是一种key-value数据库,里面支持string,list,set,hash等数据结构。redis具有持久化的特性,虽然他的所有数据都是保存在内存中的,但是还是会把数据(改动)同步到硬盘中去

Redis的优势:
1、性能极高,io速度快
2、数据类型丰富
3、原子性的操作,对于单次操作要么成功要么失败完全不执行
Redis的缺点:
1、由于物理内存的限制,不能处理海量数据,只能适用于数据不是很多的情况
2、集群中扩容困难,需要运维提前设置好足够多的内存,否则扩容起来相当麻烦

RDB持久化:也称半持久化,在指定的时间间隔内,会把内存中的数据集写入到磁盘中,实际的操作是fork一个子进程,将数据集中写到临时文件中,写入成功后再替换之前的文件。
优点:由于是直接存储数据,所以灾后数据恢复会比较快;由于只是fork了一个子进程,所以性能较高
缺点:由于是间隔性的存储数据,不能保证数据的高可用性;当数据集过大时,可能会引起服务器短暂停止服务(几百毫秒级别)

AOF持久化:也称全持久化,会以日志方式记录数据库的每一个增删操作。
优点:更高的数据安全性;当日志过大时,会将修改数据写入磁盘文件中,同时创建一个新的文件来记录此期间数据库的修改
缺点:由于是存储了对数据库的操作,所以恢复大数据时比较慢;运行效率上略低于半持久化

目前知道的redis适用场景
1、广播订阅:redis内部分好几个频道,用户可以订阅订阅了频道X,那么当有消息发送到频道X的时候,所有订阅了频道X的用户都会收到该消息
2、单纯提高性能用:朋友的公司就是单纯把redis当做数据库的缓存,用来缓解读写压力

额外扩展————如何处理秒杀功能等高并发情况
1、使用redis进行抢占式数据处理
2、分布式处理,将redis配置到集群中
3、做成异步,收到消息后立刻返回,之后再进行数据处理

Django

FBV和CBV的简述:
F和C分别是function和class,一种是在视图函数里使用函数,一种是在视图函数里使用类,用类的话更好一些,因为可以分别写get和post操作的时候的逻辑,看起来简洁一点。

ORM的理解:
中文为对象-关系-映射,他实现了数据库和数据模型的解耦,当你更换数据库的时候,就不需要再去修改里面的东西了,只要修改一下配置文件就好了。

Model的几种继承类型:
抽象继承:和c++的抽象类类似,本身不会生成数据表,用来保存一下你不想重复录入的信息字段
多重表继承:父模型也会生成数据表的抽象继承
代理继承:当你只是想设置某模型的一些方法之类的东西时使用,比如增加一个查询最近录入数据的方法,或者修改他的默认manager,这些修改不会对父模型造成影响

元数据:
用class Mate定义的一些数据,常用的有:
db_table:命名该模型创建的数据库的名字
ordering:设置默认某字段的排序顺序
verbose_name:给类起一个别名
abstract:配置该模型是否为抽象基类

热评文章