321. 对操作系统的理解
操作系统是管理计算机硬件资源的软件,它提供了应用程序运行的环境和接口。操作系统负责调度任务、管理内存、处理输入输出、文件系统管理等。他作为用户和计算机硬件之间的桥梁,使得用户可以更方便的使用计算机资源。
322. 怎么理解信号?
信号是操作系统发送给程序的一种简单信息,用于通知程序发生了某种事件,如非法操作、外部中断等。程序可以预设处理函数来响应特定信号,实现对这些事件的自定义处理或恢复操作。
323. 信号的底层实现
信号的底层实现涉及操作系统内核的中断处理机制。当系统发生特定事件时,内核会中断当前进程,将控制权转移到内核中的信号处理例程。该例程确定信号的类型,并根据进程注册的处理函数或默认行为来处理信号,然后将控制权返回给进程,继续执行或执行信号处理函数。
324. 和QT的信号与槽是类似的概念嘛
操作系统中的信号是关于进程间的通信和异常处理机制,是由操作系统内核管理的;而QT的信号与槽是一种应用层的编程模式,主要用于对象之间的事件通信,是QT框架提供的一种机制来简化事件驱动编程。
325. 对设计模式的理解
设计模式是软件工程中经过验证的解决特定问题的可复用方案。
326. 用过哪些设计模式?
创建型模式:
- 工厂方法
- 抽象工厂
- 建造者
- 原型
- 单例
结构型模式:
- 适配器
- 桥接
- 组合
- 装饰器
- 外观
- 享元
- 代理
行为型模式:
- 责任链
- 命令
- 解释器
- 迭代器
- 中介者
- 备忘录
- 观察者
- 状态
- 策略
- 模板方法
- 访问者
解释:
设计模式是软件工程中经过验证的解决特定问题的可复用方案。你提到的这些设计模式可以分为三大类:创建型模式、结构型模式和行为型模式。
创建型模式
- 工厂方法(Factory Method): 定义一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
- 抽象工厂(Abstract Factory): 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
- 建造者(Builder): 将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示。
- 原型(Prototype): 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
- 单例(Singleton): 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
结构型模式
- 适配器(Adapter): 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
- 桥接(Bridge): 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 组合(Composite): 将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户对单个对象和组合对象的使用具有一致性。
- 装饰器(Decorator): 动态地给一个对象添加一些额外的职责。装饰器模式在功能扩展方面比生成子类更灵活。
- 外观(Facade): 为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口,这一接口使得这一子系统更加容易使用。
- 享元(Flyweight): 运用共享技术有效地支持大量细粒度的对象。
- 代理(Proxy): 为其他对象提供一种代理以控制对这个对象的访问。
行为型模式
- 责任链(Chain of Responsibility): 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有对象处理它为止。
- 命令(Command): 将请求封装成对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
- 解释器(Interpreter): 给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
- 迭代器(Iterator): 提供一种方法顺序访问一个聚合对象中的各个元素,而又不需暴露该对象的内部表示。
- 中介者(Mediator): 用一个中介对象来封装一系列对象之间的交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 备忘录(Memento): 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复对象到以前的状态。
- 观察者(Observer): 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 状态(State): 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
- 策略(Strategy): 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
- 模板方法(Template Method): 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 访问者(Visitor): 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
这些设计模式在不同的情况下有不同的应用场景,可以帮助开发人员编写更加灵活、可维护和可扩展的代码。
327. 详细讲一种熟悉的设计模式
责任链模式是一种行为设计模式,允许多个对象处理一个请求,将这些对象连成一条链,并沿着这条链传递请求,直到其中一个对象处理它为止。在C++中使用责任链模式可以增加新的处理类而不影响现有的代码逻辑,实现请求处理对象的解耦。
328. 默认的拷贝构造函数是深拷贝还是浅拷贝?为什么?
默认的拷贝构造函数执行的是浅拷贝,因为他只复制对象的各个成员的值,对于指针成员,仅复制指针的值而不复制指针所指向的数据。如果成员包含指向动态分配内存的指针,那么多个对象可能会指向相同的资源。
329. 怎么实现深拷贝?
- 定义拷贝构造函数:在拷贝构造函数中,为对象中的每个需要深拷贝的成员分配新的内存,并复制原对象相应成员的内容。
- 定义赋值运算符:和拷贝构造函数类似,首先释放现有对象中需要深拷贝成员的内存(如果有的话),然后为这些成员分配新的内存,并复制复制对象对应成员的内容。
- (可选)定义析构函数:确保在对象生命周期结束时,为深拷贝分配的动态内存得到释放,以避免内存泄漏。
330. 虚函数、虚表、虚指针解释一下,分别在哪个时间段创建的
虚函数:类中通过virtual关键字声明的函数,允许在派生类中被重写,实现多态。
虚表:一个存储类的虚函数地址的表,在编译时期被创建,每个含有虚函数的类都有一个虚表。
虚指针:一个指针,指向对象的虚表,在对象实例化时(运行时)被创建,并初始化指向类的虚表。
331. select、poll、epoll区别解释一下:
- select:它的文件描述符集合大小有限(通常受FD_SETSIZE宏限制),每次调用都需要复制整个文件描述符集,且在返回后需要遍历整个集合来找出活跃的文件描述符,这让他在处理大量文件描述符时效率较低。
- poll:类似于select,但他不受文件描述符数量的限制,因为它使用动态数组(而不是固定大小的位数组)。同样,在每次调用时需要复制整个数组,并在返回后遍历数组以找出活跃的文件描述符。
- epoll:仅在Linux系统上可用,它使用了一个事件列表,所以不需要像select或poll那样复制和遍历整个文件描述符集。epoll在首次监视某个文件描述符时向内核注册文件描述符,之后不需要再次注册,且当文件描述符就绪时,他只返回那些状态发生改变的文件描述符,这大大提高了效率,尤其适合处理大量文件描述符的场景。
332. 内存池的基本原理解释一下
内存池的基本原理是预先在内存中分配一大块连续空间,并将这块空间划分为大小相等或不等的小块,用于满足程序运行时的内存分配请求。当程序请求分配内存时,内存池会从这些预分配的小块中找到一个合适的块返回,而不是直接向操作系统请求。同样,当内存被释放,他会返回到内存池中,而不是归还给操作系统。这种方式可以减少频繁向操作系统申请和释放内存所造成的开销和碎片化问题,从而提高内存分配和回收的效率。
333. 左值引用和右值引用的区别解释一下,为什么需要右值?
左值引用:是对可寻址(即可以取地址)且非临时的对象的引用,例如变量。左值引用用&
表示。
右值引用:是对临时对象(即将被销毁、不能取地址的对象)或可移动对象的引用,它扩展了对象的生命周期。右值引用用&&
表示。
为什么需要右值:右值引用主要用于实现移动语义和完美转发。
- 移动语义允许资源(如动态内存)从一个对象转移到另一个对象,减少不必要的临时对象复制,优化性能。
- 完美转发允许模板函数将其接收到的参数以原来的值类别(左值或右值)转发到其他函数,这在模板编程和函数重载解析中很有用。
334. 智能指针的基本原理,引用计数是线程安全的么?智能指针是线程安全的嘛?
智能指针的基本原理是通过封装一个原始指针,在对象的生命周期管理上提供自动化处理。
引用计数在std::shared_ptr中默认是线程安全的,即多个线程同时创建或销毁同一个shared_ptr实例是安全的,因为修改引用计数的操作是原子的。
智能指针的线程安全性取决于其操作。对于智能指针本身的管理(如赋值和析构),std::shared_ptr是线程安全的。但是,多个线程访问智能指针管理的对象不一定是线程安全的,需要额外的同步机制来保护被管理的对象。
335. 多线程访问单例,你要怎么办?
双重检查锁定模式(Double-Check Locking)。步骤如下:
- 在访问单例对象之前,首先检查对象是否已经被创建,以避免锁定开销。
- 若单例对象未被创建,则进入同步块。同步块内部再次检查对象是否已被创建,以防止多个线程同时通过第一次检查。
- 如果确认单例对象未被创建,此时创建单例实例。
336. std::vector数据存在哪里?为什么不在栈上?
std::vector的数据实际存储在堆上。这是因为std::vector需要能够动态地增长或缩小其大小,而堆内存提供了动态内存分配的能力。栈上的空间是有限的且固定大小的,适合存储大小在编译时期就已经确定的局部变量,不适合存储大小可变的std::vector。
337. 为什么栈比堆快?
栈比堆快主要是因为其内存分配方式和访问速度。
栈内存由操作系统自动管理,分配和释放速度非常快,因为它使用连续的内存块,并以后进先出的顺序进行访问。相反,堆内存分配更加灵活但也更加复杂,需要程序员手动分配和释放,且可能产生内存碎片,这会影响访问速度。此外,栈内存访问通常涉及更少的指令和间接性。
338. unordered_map底层是什么?
unordered_map底层实现是一个哈希表。他通过使用哈希函数来把键转换为哈希值,然后根据这个哈希值把元素存储在相应的桶中。在碰到哈希冲突时,会通过链接法解决,即在同一个桶中用链表链接存储所有哈希值相同的元素。
339. C++入口函数是什么?main函数之前执行的是什么函数?
C++入口函数是main函数。在main函数执行之前,C++运行时会执行一些环境准备工作,包括初始化静态存储持续期内的对象、执行非局部对象的构造函数等。这些通常会涉及到全局对象的构建、静态对象的初始化,以及C++标准库和运行时库的初始化。不同的编译器和平台可能会有不同的启动例程,比如GCC的_start函数,在调用main函数之前设置程序运行的环境。
340. windows和linux如何创建线程
在windows和Linux上创建线程的方法如下:
windows:使用CreateThread函数创建线程。
#include <windows.h>
DWORD WINAPI ThreadFunc(LPVOID param){
//线程函数
return 0;
}
int main(){
HANDLE thread = CreateThread(NULL , 0 , ThreadFunc , NULL , 0 , NULL);
//等待线程结束
WaitForSingleObject(thread , INFINITE);
CloseHandle(thread);
}
Linux:使用pthread_create函数创建线程。
#include <pthread.h>
void* ThreadFunc(void* arg){
//线程函数
return NULL;
}
int main(){
pthread_t thread'
pthread_create(&thread , NULL , ThreadFunc , NULL);
//等待线程结束
pthread_join(thread , NULL);
}
在Windows中,使用Windows API(如CreateThread)创建线程;在Linux中,通常使用POSIX线程库(pthread)创建线程。
350. 线程和进程的区别,线程占空间么?
线程和进程的主要区别在于资源管理和执行环境:
- 进程:是操作系统进行资源分配和调度的一个独立单位,拥有独立的地址空间和系统资源(如文件句柄和设备)。
- 线程:是进程内的执行流,是CPU调度和分派的基本单位,线程共享其所属的进程的地址空间和资源,但拥有自己的执行堆栈、程序计数器和一系列寄存器。
线程占用空间:线程确实占用空间,主要包括线程栈(用于存储局部变量和调用历史)、线程本地存储区和一些必要的管理信息,但相比进程,线程所需的资源和空间通常较少。
351. 线程栈实现原理,线程栈是怎么存储数据的?
线程栈的实现原理基于以下机制:
- 栈的分配:操作系统为每个线程分配一块连续的内存区域作为栈,用于存储执行流程中的数据。这块内存的大小通常是预设的,可以通过系统设置或程序指定调整。
- 数据存储:线程栈按照后进先出(LIFO)的原则存储数据。每当函数被调用时,函数的局部变量、函数参数和返回地址被推入栈顶;当函数返回时,这些信息被从栈中弹出,回到调用者环境。
- 栈帧:每个函数调用在栈中占据一个栈帧。栈帧包含函数的局部变量、函数参数、返回地址和某些书keeping信息,如基指针(EBP)等。
- 指针管理:栈指针(SP)用于追踪栈顶的位置,确保数据正确地入栈和出栈。在函数调用过程中,基指针(BP)用于稳定地向当前栈帧的开始,便于访问函数参数和局部变量。
352. new和malloc的区别
- 用途:new是C++中用于分配内存的运算符,同时调用构造函数初始化对象;malloc是C中的库函数,仅分配内存。
- 返回类型:new返回具体类型的指针,无需类型转换;malloc返回void* 类型,需要显式转换为目标类型的指针。
- 内存初始化:new分配内存后会自动调用构造函数初始化对象;malloc仅分配内存,不进行初始化。
- 配对操作:new与delete配合使用,malloc和free配对使用。
- 错误处理:new在无法分配内存时会抛出异常,而malloc则返回NULL。
- 重载:new和delete可以被重载,malloc不可以被重载。
353. C++ int占几个字节?指针呢?
在C++中,int的大小通常是4个字节。指针的大小通常是4个字节(32位架构)或8个字节(64位架构)。
354. 一般vs程序崩溃是什么原因造成的?
Visual Studio程序崩溃通常由以下原因造成:
- 内存访问违规:试图访问无效内存地址。
- 资源泄露:大量消耗系统资源而未释放。
- 指针误用:空指针解引用或野指针操作。
- 异常未捕获:抛出异常且未被捕获处理。
- 堆栈溢出:无限递归或大量局部变量占用。
- 并发错误:多线程访问共享资源未同步。
- 第三方库错误:依赖库中的错误或不兼容。
- 硬件故障:如内存损坏等硬件问题。
- 调试器问题:VS本身的bug或插件造成的问题。