C++与QT高频面试问题(不定时更新)
C++面试题
一 智能指针
1. 要解决的问题
传统的c++中,动态内存管理需要使用到(new与delete)来对指针创建与销毁,这就涉及到两个问题
- 内存泄露问题(忘记了delete)
- 悬空指针/非法访问(即提前释放了正在使用的内存,或重复释放同一块内存)
2. 绑定对象的生命周期
构造时获取资源(初始化时指向动态内存)——即创建
析构时自动释放资源(对象离开作用域时自动调用
delete
)——即销毁
这种机制保证了异常安全——即使程序抛出异常,栈展开过程也会调用已构造对象的析构函数,从而确保资源被释放。作者说:底层原理就是,我们不应该在堆内存中创建与销毁指针,而是应该在栈内存中创建与销毁指针,不管是否使用堆内存的指针,都要放到类的构造函数中进行资源的分配,且将对象本身放在栈上,利用c++的栈对象自动析构的机制释放,这样就适应了类的整个生命周期,从而避免了以上的两个问题。
3. 智能指针的创分类
C++11在<memory>头文件中提供了三种主要的智能指针,它们通过管理所指对象的所有权来工作。
1. std::unique_ptr
(独占所有权指针)
即同一时刻只能有一个指针指向同一个对象,不可拷贝与赋值(避免了悬空指针问题)
1. 两种创建(c++14之后,使用make——unique分配内存)
std::unique_ptr<T> p1= std::make_unique<T>(77);
std::unique_ptr<T> p2(new int (22));
2. 使用(通过*来解引用,与指针使用方法一致)
cout << *p1<< endl;
3.三个函数(release与delete配合使用)
p1.release();
- 指针仍然存在,置空所管理的对象
delete p1.get();
- 代表释放p1
p2.reset();
- p2.reset(nullptr); //代表释放当前资源并置空
- p2.reset(new int(20)); //转移所有权到新资源
4. 经典陷阱题
p1.reset(p2.get); ——move()解决。
5. 性能高,在经过编译器优化之后,性能仅次于new与delete。
2. std::shared_ptr
(共享所有权指针)()
同一时刻可以有多个指针指向同一个对象(共享对象与资源)
1. 引入计数机制(计数也是指针)
确保该对象在最后一个对象销毁时,自动释放资源(避免资源泄露与悬挂指针)
1. 创建(c++14之后,使用make分配内存)
p1= std::make_shared<T>(77);
2. 多个线程来copy
shared_ptr
是被允许的,但是一块来修改他管理的对象(不安全,需使用mutex互斥量,进行保护)
3. std::weak_ptr
(弱引用指针)
主要用于观察用shared_ptr管理的对象(不参与对象的生命周期管理与引用计数)
1. 因为是观察shared_ptr管理的对象,所以,即使存在有多个weak_ptr指向对象,当shared_ptr被销毁,该对象仍然被释放
2. 由于是弱引用,没有对象的管理权,所以不能被解引用,可以使用lock函数,临时提升为shared_ptr,
4. 总结与对比
特性 | std::unique_ptr | std::shared_ptr | std::weak_ptr |
---|---|---|---|
所有权 | 独占 | 共享 | 无(弱引用) |
拷贝 | 不允许(仅移动) | 允许(增加引用计数) | 允许(不增加引用计数) |
性能开销 | 极小(几乎无额外开销) | 较大(维护引用计数,原子操作) | 与shared_ptr 类似 |
主要用途 | 独占资源的默认选择 | 需要共享所有权的复杂场景 | 打破shared_ptr 循环引用 |
推荐创建方式 | std::make_unique | std::make_shared | 从shared_ptr 构造 |
5. 核心原则:
首选
std::unique_ptr
:除非明确需要共享所有权,否则应使用它。它是管理动态资源最安全、最轻量的方式。其次考虑
std::shared_ptr
:当确实需要多个实体共享对象生命周期时使用。使用
std::weak_ptr
作为shared_ptr
的辅助:用于打破循环引用或提供非拥有性访问。优先使用
make_*
函数:std::make_unique
和std::make_shared
在异常安全性和性能上通常优于直接使用new
。避免混合使用智能指针和原始指针:不要用同一个原始指针初始化多个智能指针,也不要手动删除
get()
返回的原始指针。
二 多线程
1. 要解决的问题
1. 解决主线程堵塞的问题(大文件加载导致的页面卡顿无响应的等等)
2. 利用性能,提升效率
1. 实现多线程的方式
1. Thread类
2.
三 进程线程间的通信(重点锁)
其实这个需要重点关注锁机制,即线程与通信的,如何保证线程安全。
1. 互斥锁(Mutex)(阻塞锁)
- 互斥锁是最基本的锁类型,用于确保同一时间只有一个线程可以访问共享资源。
适用场景
保护简单的临界区
需要确保只有一个线程访问共享资源的情况
缺点
睡眠和唤醒操作涉及上下文切换,有一定开销
使用不当可能导致死锁
只能提供基本的互斥功能,无法满足复杂同步需求
创建方式
- 引入头文件<mutex>
- 对某个对象进行加解锁,然后引导线程去访问
实际操作的问题
- 如果你使用的是window 32位的 mingw64 会创建线程与互斥锁发生未定义的情况,需要使用posix版本的或者引入<windows.h>的头文件。
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; int shared_data = 0;void increment() {mtx.lock();shared_data++;mtx.unlock(); }int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Shared data: " << shared_data << std::endl;return 0; }
2. 读写锁(Read-Write Lock)(阻塞锁)
- 读写锁允许多个读线程同时访问共享资源,但写线程独占资源。读锁是共享的,写锁是排他的。
- 写锁有最高优先性
实现原理
当没有写锁时,多个读锁可以同时获取
当有写锁时,所有读锁和写锁都会被阻塞
写锁请求会优先于读锁(避免写线程饥饿)
优点
读操作并发性好,适合读多写少的场景
提高了系统吞吐量
缺点
实现相对复杂
如果写操作频繁,读操作可能会被阻塞
可能导致写线程饥饿
3. 自旋锁(Spinlock) (非阻塞锁)
即访问不成功,忙等待
4. 条件变量 (Condition Variable)(配合互斥锁使用)
- 条件变量用于线程间的等待和通知机制,通常与互斥锁配合使用。它允许线程在某个条件不满足时等待,当条件满足时被唤醒。
优点
避免了忙等待,节省CPU资源
支持复杂的同步模式
5. 递归锁 (Recursive Mutex)
- 递归锁允许同一个线程多次获取同一个锁,而不会导致死锁。每次获取锁后必须释放相同次数。
6. 屏障 (Barrier)
- 屏障用于同步多个线程,要求所有线程都到达屏障点后才能继续执行。(即线程到达阈值,则可以进行访问读写)
7. 总结比较
锁机制 优点 缺点 适用场景 互斥锁 简单,节省CPU 上下文切换开销 通用临界区保护 读写锁 读并发性好 写操作可能饥饿 读多写少 自旋锁 无上下文切换 忙等待消耗CPU 短临界区,多核 递归锁 可重入 可能隐藏设计问题 递归或可重入函数 条件变量 避免忙等待 使用复杂,虚假唤醒 条件等待 信号量 控制访问数量 可能死锁 资源池,生产者消费者 屏障 线程同步简单 必须等待所有线程 并行计算阶段同步 无锁编程 高性能,无死锁 实现复杂,活锁风险 高性能并发数据结构
四 STL库(Standard Template Library 标准模板库)
- 提供了一系列通用的模板类和函数,实现了常用的数据结构和算法。STL 的核心思想是泛型编程,通过模板技术实现代码的通用性和复用性。