面试问题总结
c++常见面试题总结
1、c++基础
虚析构和多态
多态就是利用继承基类重写虚函数virtual和动态绑定来实现调用基类函数,在运行期根据绑定的不同类型的子类,去调用子类的接口。这里就衍生出一个虚析构问题,假如基类的析构函数没有声明virtual,那么释放基类指针的时候也不会调用派生类的析构函数。对于需要在派生类中释放资源的类就会出现内存泄漏的问题,所以如果基类中存在虚函数,那么一般是会去继承重写虚函数的,这时候把基类的析构函数也顺便定义成virtual。
深拷贝和浅拷贝
简单理解就是深拷贝重新分配内存然后将原始内容拷贝过来,而浅拷贝没有重新分配内存,只是从引用了已经存在的资源。简单内置类型比如int型一般不用考虑这些问题,对于复杂类或结构体,如果存在需要释放的资源并且没有提供自定义的拷贝构造函数,编译器会提供默认的拷贝构造函数,那么一旦发生拷贝,默认拷贝提供的是浅拷贝。所以这两个对象都将共用一份资源,一旦释放其中一个的资源,而另外一个对象再去访问共用的的资源就会触发异常。
std容器时间复杂度
访问 插入 删除 是否连续内存 vector O(1) 尾部O(1),其他位置O(n) 尾部O(1),其他位置O(n) 是 deque O(1) 头尾部O(1),中间O(n) 头尾部O(1),中间O(n) 是 list O(n) 任意位置O(1) 任意位置O(1) 否 map O(logn)红黑树查询删除插入时间都一样 O(logn) O(logn) 否 set O(logn) O(logn) O(logn) 否 unordered_map 哈希表实现O(1),最坏情况O(n) O(1),最坏情况O(n) O(1),最坏情况O(n) 否 unordered_set 哈希表实现O(1),最坏情况O(n) O(1),最坏情况O(n) O(1),最坏情况O(n) 否 优缺点对比
优点 缺点 适用场景 vector 支持随机访问,不需要维护额外的数据所以内存占用小,尾部插入或删除速度快。 插入和删除如果在中间,需要往后或往前整体移动插入或删除的位置后面的内存,数据量大的话消耗就大。所以在已知数据大小的情况下可以调用reserver预先分配内存 需要经常随机访问,插入删除等操作少 deque 双向队列,兼有vector随机访问,同时又可以像list一样可以快速在两端pop,push 中间位置插入删除比较耗时,需要移动元素 频繁随机访问,在两端插入删除数据 list 任意位置插入删除效率高,头尾访问快 内存不连续所以无法随机访问 频繁任意位置插入删除数据 forward_list 单向链表所以比list内存占用小,随机插入效率高 不支持随机访问 不需要双向迭代数据 set 红黑树(平衡二叉树)实现,元素自动排序并唯一 插入元素需要重新排序,降低效率 需要自动排序并保持数据唯一 map 红黑树(平衡二叉树)实现,元素自动排序并且键值对映射关系唯一 插入元素需要重新排序,降低效率 需要排序且数据为字典形式 unordered_set 哈希表实现无序排序,查找速度快,大量数据下插入删除均比set快,因为很难遇到最坏情况 哈希表额外内存占用 不需要自动排序并保持数据唯一 unordered_map 哈希表实现不会排序,查找速度快,大量数据下插入删除均比map快 哈希表额外内存占用 不需要排序且数据为字典形式
priority_queue(优先级队列)与队列(queue)
优先级队列是在队列先进先出的基础上,增加优先级的规则,可以分为最大优先级和最小优先级。本质是在插入数据的时候按照优先级排序,可以有相同优先级。实现原理是堆。
静态局部对象、静态全局对象、全局对象区别
内存位置 作用域 静态局部对象 静态区,程序结束释放 作用于特定类或者函数 静态全局对象 静态区,程序结束释放 作用于整个编译单元(cpp),对其他编译单元无效。 全局对象 静态区,程序结束释放 作用于所有编译单元,只需要包含声明的头文件。在.h中使用extern声明,在.cpp初始化。 进程和线程的区别
进程是操作系统分配资源的基本单位,线程是处理器调度和执行任务的基本单位。
同一进程的线程共享进程的地址空间和资源,不同进程之间是独立的地址空间和资源。
进程一般包含多个线程,当一个线程崩溃会导致整个进程崩溃。
2、Qt基础
connect第五个参数的用法
默认参数Qt::AutoConnection,当receiver和sender在同一个线程时,使用Qt::DirectConnection,当不在一个线程时,会切换成Qt::QueuedConnection。从字面意思就能发现这两者的区别,Qt::DirectConnection表示直接连接,当信号发送的时候会立即触发槽函数,所以是同步。而Qt::QueuedConnection由于两个对象不在同一个线程,槽函数不会立即触发,会进入事件循环,所以是异步执行。当没有receiver的时候,槽函数会在sender线程执行。
Qt::BlockingQueuedConnection是Qt::QueuedConnection的同步版,除了会在receiver线程执行槽函数以外,sender线程会一直卡住等待槽函数执行完成。所以使用这个连接类型,一定要确认sender和receiver在不同线程,如果在同一个线程会导致死锁。
Qt::UniqueConnection是一个辅助标志位,和其他连接类型一起使用,用|(逻辑OR)拼接,用来确保同一组对象和槽函数不会多次连接。
QObject对象和线程关系
继承QObject的对象都是不能跨线程调用的,QObject在哪个线程创建它就属于哪个线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class Thread: public QThread
{
Q_OBJECTMeta-Object Compiler
public:
Thread()
{
m_pObject = new QObject();
qDebug() << QThread::currentThread();
qDebug() << m_pObject->thread();
}
~Thread()
{
if (m_pObject) {
m_pObject->deleteLater();
}
if (m_pObject1) {
m_pObject1->deleteLater();
}
}
protected:
void run() override
{
m_pObject1 = new QObject();
qDebug() << QThread::currentThread();
qDebug() << m_pObject1->thread();
}
private:
QObject *m_pObject;
QObject *m_pObject1;
};这个例子就能发现QObject对象的所属线程和当前创建时线程一致。在Thread的构造函数里面创建时还没有启动线程,这时m_pObject的所属线程和Thread类的对象是同一个。run函数由于是在新线程中执行,所以m_pObject1也属于新线程。当出现跨线程警告时,不妨输出一下当前线程和对象所属线程比较一下,就能排查出问题。
信号槽原理
信号槽的实现基础在于moc,全称是Meta-Object Compiler(元对象编译器),在使用信号槽时,需要在类的定义中加一个Q_OBJECT宏定义。这个宏定义展开如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#define Q_OBJECT \
public: \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")moc工具就是自动帮我们实现宏定义展开的函数,所以在编译目录下会存在一个moc_xxx.cpp。我们自定义的信号其实也是个函数,实现也在这个moc生成的cpp中。
这是关于信号在moc的cpp中的实现:
1
2
3
4
5
6
7
8
9
10
11
12// SIGNAL 0
void MainWindow::test()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
// SIGNAL 1
void MainWindow::test1(int _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 1, _a);
}信号槽利用观察者模式,在connect的时候将接收者添加到通知列表中,发送信号就是执行QMetaObject::activate,然后通过回调执行接收者的槽函数。
3、Win32原生绘制界面流程
RegisterClassEx首先是注册窗口类型,这里注册的是真正的窗口。里面lpfnWndProc参数用于传入自定义的事件循环处理函数,lpszClassName用于声明窗口的类型名称。
CreateWindowEx创建窗口,会用到上一步中的lpszClassName。
WM_PAINT是回调函数中消息的一种,在这里使用GDI接口在窗口的DC(设备上下文,我一般理解为画板)画图。比如常见的LineTo等函数。GDI基于c语言,GDI+基于cpp开发,是GDI的扩展,简化了GDI的操作并升级了它的功能。
4、计算机基础
4.1、七层网络模型和四层网络模型
OSI七层模型
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
TCP/IP协议族
应用层(http、https、ftp、telnet等上层协议)
传输层(tcp、udp两种传输协议)
网络层(处理ip包和icmp包等基础最小传输单位)
网络接口层(和硬件的驱动和操作系统底层接口交互)
TCP/IP是一个协议族包含很多协议,这个四层模型和OSI模型范围不同,没有包含物理层的,但是具有一定的对应关系。