C++面试3
一、常用设计模式
https://blog.csdn.net/m0_71530237/article/details/141140118?spm=1001.2014.3001.5501
二、死锁以及解决方式?
死锁:一种常见的并发问题,发生在多个进程或线程因为竞争资源而陷入相互等待的状态,导致这些进程或线程无法继续执行,要理解死锁,通常会涉及以下四个必要条件。
1、互斥条件:资源是不可共享的,即一个资源在同一时间只能被一个进程占用。
2、占有且等待:一个进程已经持有一个资源,同时又在请求其他资源,而此时这些资源正被其他进程占有。
3、不可剥夺:资源不能被强制剥夺,进程只能资源释放资源。
4、循环等待:存在一个进程集,这些进程之间形成一个循环的资源等待链。
死锁的解决方式:
1、死锁预防:确保上诉四个条件中的至少一个不能成立,即可阻止死锁发生
2、死锁避免:使用某些算法来动态的判断资源分配状态,避免进入死锁状态,常见的方法是银行家算法。
银行家算法在进程请求资源时,判断系统是否会进入不安全状态。如果不会,则分配资源;否则拒绝该请求,防止死锁发生。
3、死锁检测与恢复:允许系统进入状态,然后检测和处理它。
4、资源有序分配:通过给资源定义一个全局顺序,进程只能按照顺序请求资源,避免循环等待的产生。
实践中的死锁解决方式:
超时机制:当一个线程请求资源超过一定时间时,认为它可能处于死锁状态,释放已持有资源并重新开始请求。
锁分层:为不同资源定义不同的优先级和层级,线程只能按照优先级从低到高的顺序去请求资源,避免循环等待。
三、堆栈空间?堆和栈的区别?堆栈溢出问题?
1、栈
栈是操作系统为每个线程分配的一块连续内存空间,用于存储局部变量、函数参数、返回地址等。栈是一种后进先出(LIFO)的数据结构,具有自动管理内存的特点。
存储内容:局部变量、函数参数、返回地址、函数调用过程中的临时数据。
分配方式:栈的内存分配由操作系统自动完成,通常由编译器在函数调用时生成指令管理。
内存大小:站的大小是固定的,一般由操作系统在程序启动时确定。
优点:速度快,自动分配内存和释放,不容易产生内存泄漏。
缺点:大小优先,不能存储大数据;递归调用或过深的函数调用栈容易造成栈溢出。
栈的调用流程:
1、参数返回地址等数据会压入栈中。
2、在函数内部的局部变量也会存储在栈上。
3、函数返回时,栈顶数据(函数返回值和局部变量等)会被弹出,释放栈空间
2、堆
堆是操作系统提供的一块较大的动态空间,专门用于存储动态分配的数据,程序运行时可以通过动态内存分配函数(如malloc()、new)来手动管理堆内存。
存储内容:动态分配的内存快(如malloc()、new分配的内存)
分配的方式:由程序员手动分配和释放,灵活且可动态扩展。
内存大小:堆的大小不固定,通常由操作系统的物理内存决定,能够存储较大的数据。
优点:可以动态分配大内存快,灵活方便,数据生命周期可以跨越多个函数调用。
缺点:分配和释放的速度较慢,如果程序员忘记释放内存,容易造成内存泄漏;过多的动态分配也可能导致堆碎片问题,影响内存效率。
堆的调用流程:
1、使用malloc()或new函数分配内存。
2、返回一个指向这块内存的指针。
3、当不在需要时通过free()或delete释放内存,否则会发生内存泄漏。
栈与堆的比较:
特性 | 栈 | 堆 |
管理方式 | 由操作系统自动管理 | 需要程序员手动管理 |
存储内容 | 局部变量、函数参数、返回地址等 | 动态分配的对象和数组 |
分配速度 | 快(内存连续,自动分配和释放) | 慢(需要寻找空闲内存、手动管理) |
内存大小 | 较小(通常几MB) | 较大(取决于系统可用物理内存) |
生命周期 | 随函数调用结束自动释放 | 需程序员手动释放、否则会内存泄漏 |
使用场景 | 局部变量、函数调用 | 动态分配大数据或对象 |
常见问题 | 栈溢出(如递归过深) | 内存泄漏、堆碎片 |
四、内存泄漏?如何避免?
内存泄漏是指程序在动态分配内存后,未能正确释放已不再使用的内存,导致该内存无法被再次分配和使用。随着程序的运行,未释放的内存逐渐增多,最终可能导致系统内存耗尽,引发系统崩溃或系统性能下降和。避免内存泄漏是编写高效、健壮系统的重要环节。
如何避免:
1、手动释放内存
2、避免重复分配内存
3、使用智能指针
五、智能指针的本质是什么?
智能指针本质上是一个类,用来存储指向动态分配对象的指针,负责动态释放动态分配的对象,防止内存泄漏。动态分配资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。借助RAII和类的特点,防止内存泄漏的问题。
六、野指针?
①、定义一个指针变量,没有给他指向有效的空间或置为空时,系统会默认初始化一个随机地址,变成野指针。②、释放一个指针变量指向的空间时,没有置为空,也会变成野指针。
避免野指针:
1、定义指针变量时,要么设为空,要么指向合法的内存
2、向指针变量指向的空间赋值时,一定要开辟内存空间,
3、开辟空间后需要检查是否分配成功
4、分配成功后要进行初始化
5、注意使用时不要越界访问,否则会产生内存泄漏
6、结束时要释放内存空间
7、释放完之后要将指针变量置为空
检测工具:
gcc自带的ASAN或valgrind工具
七、指针和引用的区别?
指针是一个变量,引用是变量的别名;指针可用为空,引用定义时必须初始化;指针在初始化之后可以改变指向,引用在初始化之后不可以改变指向;指针可以有多级,引用最多两级;指针需要动态分配内存空间,引用不需要动态分配;指针引用占8个字节;指针间接访问,效率低、,引用直接访问,效率高;做函数形参时,指针需要考虑传值还是传址,引用不需要考虑,它既可以访问,也可以修改,函数返回值是引用可以做左值;
八、C++三大特性?
封装:
封装是面向对象的核心思想,因为封装解决了代码独立性的问题,从而提高了代码的复用性,为复用性和扩展性提供了基础,封装将成员属性和成员方法绑定在一起,通过选择访问权限实现对外提供接口,对内开放数据从而达到对数据的保护,类内有构造函数和析构函数,不用去考虑释放,提供了一种更安全的机制,提高开发效率。
类封装的本质是在于将数据和行为绑定在一起,然后通过对象来完成操作,封装对私有函数添加get/set方法,全局变量会破坏数据的封装,可以采用静态成员来解决。
继承:
派生类可以使用基类的属性和方法,实现不同类之间的代码复用性,继承使得我们可以基于已有的类创建新的类,无需重新编写实现代码,派生类可以添加新的属性和方法,或重写(override)基类的方法来实现新的功能(通过重写虚函数virtual来实现)
基类提供属性和方法,派生类继承自基类并扩展或修改其行为
单继承 :一个派生类只能继承自一个基类
多继承 :一个派生类可以同时继承自多个基类
多态:
①多态是使用同一接口,传递不同实例对象,执行不同操作(C语言中的共用体就是一种多态)
②当功能不断变化,考虑用多态实现代码的扩展性
③静多态(编译时绑定)的函数重载和模板和动多态(运行时绑定)的虚函数(少用)
④基类中要有虚函数;派生类要重写基类中的虚函数(override);通过基类的指针或引用绑定派生类的对象
⑤优点,多态解决代码扩展性的问题,增加程序的灵活性,减轻系统升级、维护、调试的工作量和复杂度
缺点:多态影响效率,造成空间浪费(引入虚函数表指针)
九、重载、重写和隐藏?
重写:派生类中重新定义基类的虚函数,具有相同的函数签名,实现多态
重载:同一作用域内定义多个同名函数,但是他们的参数不同
隐藏:子类中定义了与父类具有相同名字但不同参数列表的函数,或子类中定义了与父类中非虚函数同名的函数,此时,父类中的同名函数在子类中被隐藏。子类的函数会覆盖父类的同名函数
十、this指针?
this指针是隐藏在对象成员函数内的一种指针,当一个对象被创建后,他的没有个成员函数都含有一个系统自动生成的隐含指针this,用于保存这个对象的地址,也就是说我们没有写上this指针,编译器在编译的时候也会加上。this永远指向当前对象,this指针并不是对象的一部分,不会影响sizeof(对象的大小)的结果。
this指针是C++实现封装的一种机制,他将对象和该对象调用的成员函数连接在一起,在外部看来,每一个对象都拥有自己的成员函数。一般情况下,不写this,而是让系统进行默认设置。
十一、局部变量和全局变量?
特性 | 局部变量 | 全局变量 |
作用范围 | 尽在定义的函数或代码块内可见 | 整个程序范围内可见 |
生命周期 | 进入作用域时创建,离开作用域时销毁 | 程序启动时创建,程序结束时销毁 |
存储位置 | 栈内存 | 全局数据区或静态存储区 |
初始化 | 无默认初始值,使用前需手动初始化 | 自动初始化为0或null |
访问方式 | 只能子啊定义他的函数或块内访问 | 可被程序的任何地方访问(除非有限定) |
冲突问题 | 不会和其他函数中的局部变量冲突 | 可能会与其他模块中的变量或函数名冲突 |
十二、进程间通信和线程间通信?
进程间通信:
传统通信:管道、信号
ipc通信:消息队列、共享内存、信号量
网络通信:socket(套接字)
线程间通信:
全局变量、条件变量、信号量、消息队列、事件、管道和文件
十三、多线程访问同步方式?
互斥锁:用于保护共享资源,确保同一时间只有一个线程能够访问该资源
读写锁:允许多个线程同时读取共享资源,但写操作需要独占资源
条件变量:用于在线程之间传递信号,允许一个线程等待条件满足后继续执行
信号量:一种计数器,用于控制对资源的访问
十四、线程池?
线程池如何实现?
线程池通过安全队列存储执行的任务,并使用多个线程从队列中取出任务执行。线程池通过 submit
方法提交任务,任务被封装成 std::packaged_task
并加入队列中。当任务到来时,通过条件变量唤醒等待的线程,线程执行任务。线程池在析构时会确保所有任务执行完毕,关闭线程并释放资源。
如何确保线程池中的任务能够按照顺序执行?
线程池不保证任务按照提交顺序执行,因为多个线程可能并发执行任务,如果需要严格的顺序控制,可以通过使用额外的同步机制(队列或条件变量)来管理任务的执行顺序
线程池中为什么不直接存储线程对象,而是存储指向线程的指针?
存储指向线程的指针可以避免对象的拷贝和移动操作,保证每个线程对象在创建后都能被正确管理和释放。如果存储线程对象本身,可能会引发不必要的拷贝构造和移动构造操作
线程池的作用是什么?他是如何提高多线程应用的性能?
线程池的主要作用是通过复用以创建的线程来执行任务,避免频繁创建和销毁线程开销,从而提高性能。线程池提前创建·一定数量的线程,当有任务时,线程池中的线程可以直接处理任务,避免了创建新线程的延迟
十五、connect连接方式?
网络编程中用于建立客户端和服务器端之间的连接的系统调用。它主要用于基于 TCP/IP 协议的套接字编程中,用于客户端连接到服务器
十六、TCP与UDP的区别?
十七、简单的数据结构?
https://blog.csdn.net/m0_71530237/article/details/141996354?spm=1001.2014.3001.5501
十八、槽函数的使用以及注意事项?
槽函数本质上是一个普通的成员函数,它可以被信号触发,槽函数的定义与普通函数类似,但是要放在slots:访问控制符之下,或通过Q_OBJECT宏使其成为槽函数。使用connect()函数将信号和槽连接起来。第一个参数:发送信号的对象;第二个参数:信号的名称;第三个参数:接收信号的对象;第四个参数:槽函数的名称,从QT5开始,可以使用lambda表示式,不需要定义一个单独的槽函数
注意事项:信号和槽默认线程安全;使用Q_OBJECT宏
十九、C++新特性
自动类型推导 (auto
): 自动推导变量的类型,减少显式类型声明的繁琐
范围 for
循环: 用于遍历容器或数组,简化代码。
nullptr
: nullptr
取代 NULL
,表示空指针,类型更明确。
lambda
表达式: 允许定义匿名函数,常用于 STL 算法或事件处理。
智能指针 (std::shared_ptr
和 std::unique_ptr
): 引入了自动管理内存的智能指针,减少手动管理内存泄漏的风险
右值引用 (&&
) 和移动语义: 移动语义允许资源的转移,而不是复制,提高了性能,特别是在容器操作中
线程支持 (<thread>
头文件): 引入了标准线程库,简化了多线程编程