吉比特面试准备
算法知识
1、快速排序
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
2、堆排序
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
hash冲突解决办法
1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
2.再哈希法
3.链地址法(Java hashmap就是这么做的,为了好的查询,可以用红黑树)
c++知识
引用
一般例子
int a = 1;
int &b = a;//引用
b = 2;
cout << a;
引用的一些错误
int &a;//引用是已经存在对象的别名,所以右边必须有一个对象
int &b = 1;//同上,引用的初始值必须是一个对象
double c = 1.1;
int &d = c;//类型应该一致
指针
指针的一般用法
int ival = 42;
int *p = &ival;//
cout << *p;
引用与指针的区别
1:指针本书就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象.
2:指针无须在定义时赋初值.和其他内置类型一样,在块作用域内定义的指针如果没有被初始化.也将拥有一个不确定的值
3:指针和引用都能提供对其他对象的间接访问,然而在具体实现细节是二者有很大的不同,其中最重要的一点就是引用本身并非一个对象.一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问他最初绑定的对象.指针和存放的地址之间就没有这种限制.给指针赋值就是令它存放一个新的地址,从而指向一个新的对象
4:指针需要申请一段内存空间,引用不需要.
const 限定符
const其值定义好就不能改变
const 函数
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。const 成员函数也称为常成员函数。(放在函数的后面)
int getage() const;
修饰的是返回值,表示返回的是指针所指向值是常量。(放在函数前面)
const int * GetPosition();
static 局部静态变量
static修饰的变量,在程序第一次定义的时候初始化,并且直到程序结束的时候才被销毁,在此期间即使对象所在函数结束执行也不会对他造成影响
例子
int count_size()
{
static int siz = 0;
return ++siz;
}
int main()
{
for(int i=1;i<=10;i++)
{
cout << count_size() << '
';
}
return 0;
}
/*
输出结果
1
2
3
4
5
*/
智能指针
内存泄露:动态内存分配的时候,忘记释放内存
智能指针:share_ptr(允许多个指针访问一个对象),unique_ptr(独占所指向的对象),weak_ptr
shared_ptr
例子
shared_ptr<int>p1 = make_shared<int>(42);//初始化
auto q(p1);//拷贝
// auto q = p1;//赋值
如何实现自动销毁所管理的对象
我们可以认为每一个share_ptr都有一个关联的计数器,无论我们何时拷贝一个一个share_ptr,计算器都会递增,当我们给share_ptr赋一个新值或者share_ptr被销毁,计算器就会递减.一旦一个计算器变成0,他就会自动释放所管理的对象.
unique_ptr
一个unique_ptr"拥有"它所指的对象",与share_ptr不同.某个时刻只能有一个unique_ptr指向一个给定对象.当unique_ptr被销毁时,它所指向的对象也被销毁
例子
unique_ptr<int>p1 (new int(42));//初始化
unique_ptr<int>p2(p1);//错误,不支持拷贝
unique_ptr<int>p3;
p3 = p2; //错误,不支持赋值
如何实现unique_ptr拷贝
我们不能拷贝或者赋值unique_ptr,但可以通过调用release和reset将指针的所以权从一个(非const)unique_ptr转移到另一个unique
u.release():u放弃了指针的控制权,返回指针并将u职位空
u.reset():释放u指向的对象
unique_ptr<int>p1 (new int(42));//初始化
unique_ptr<int>p2(p1.release());// p1.release()返回指针,并将p1置为空.
unique_ptr<int>p3(new int(20));
p2.reset(p3.release());//p2释放了原来指向p1的对象
new和malloc的区别
1、属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
2、参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
3、返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4、分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
5、自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
6、重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
7、内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态(.说了静态--函数重载,动态--虚函数)。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
虚函数
继承的时候,对于某些函数,基类希望它的派生类自定义适应自己的版本
深拷贝与浅拷贝
【浅拷贝】是增加了一个指针,指向原来已经存在的内存。而【深拷贝】是增加了一个指针,并新开辟了一块空间
构造和析构
拷贝构造函数
如果应该构造函数的第一个参数是自身的引用,且任何额外参数都有默认值,则此函数的拷贝构造函数
析构函数
class Foo{
public:
~Foo();
};
为什么析构函数要用虚函数
如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。若是将析构函数声明为虚函数,则可以解决此问题。
当一个类有子类时,该类的析构函数必须是虚函数,原因:会有资源释放不完全的情况
操作系统
协程和线程的区别
先简要说下结论:
协同程序(coroutine)与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针(IP,instruction pointer),但与其它协同程序共享全局变量等很多信息。
协程(协同程序): 同一时间只能执行某个协程。开辟多个协程开销不大。协程适合对某任务进行分时处理。
线程: 同一时间可以同时执行多个线程。开辟多条线程开销很大。线程适合多任务同时处理。
1.协程,即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程实际上是在一个线程中,只不过每个协程对CUP进行分时,协程可以访问和使用unity的所有方法和component
2.线程,多线程是阻塞式的,每个IO都必须开启一个新的线程,但是对于多CPU的系统应该使用thread,尤其是有大量数据运算的时刻,但是IO密集型就不适合;而且thread中不能操作unity的很多方法和component
线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只在必要时才会被挂起。
进程通信的方式
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
设计模式
单例模式
在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式有 3 个特点:
单例类只有一个实例对象;
该单例对象必须由单例类自行创建;
单例类对外提供一个访问该单例的全局访问点;