1、C++的默认函数都有哪些?
在C++中,默认函数(也称为特殊成员函数)是一组在类定义中不需要显式声明和定义的函数。这些函数在特定的情况下会被编译器自动合成(隐式生成)。以下是C++中的默认函数:
- 默认构造函数(Default Constructor):当没有提供其他构造函数时,编译器会提供一个默认构造函数。它通常不做任何初始化操作或只执行默认的初始化。
class MyClass {
public: MyClass() { // 默认构造函数 }
};
如果没有提供任何构造函数,编译器也会生成一个默认的无参构造函数。
- 拷贝构造函数(Copy Constructor):当用同类的一个对象来初始化另一个对象时,会调用拷贝构造函数。如果没有提供拷贝构造函数,编译器会生成一个默认的拷贝构造函数,执行成员变量的浅拷贝。
class MyClass {
public: MyClass(const MyClass& other) { // 拷贝构造函数 }
};
- 拷贝赋值运算符(Copy Assignment Operator):当用一个对象给另一个同类对象赋值时,会调用拷贝赋值运算符。如果没有提供拷贝赋值运算符,编译器会生成一个默认的拷贝赋值运算符,执行成员变量的浅拷贝。
class MyClass {
public: MyClass& operator=(const MyClass& other) { // 拷贝赋值运算符 return *this; }
};
- 移动构造函数(Move Constructor):C++11引入移动语义后,当用一个临时对象或即将销毁的对象来初始化另一个对象时,会调用移动构造函数。如果没有提供移动构造函数,编译器会生成一个默认的移动构造函数,执行成员变量的移动语义初始化。
class MyClass {
public: MyClass(MyClass&& other) noexcept { // 移动构造函数 }
};
- 移动赋值运算符(Move Assignment Operator):当用一个临时对象或即将销毁的对象给另一个同类对象赋值时,会调用移动赋值运算符。如果没有提供移动赋值运算符,编译器会生成一个默认的移动赋值运算符,执行成员变量的移动语义赋值。
class MyClass {
public: MyClass& operator=(MyClass&& other) noexcept { // 移动赋值运算符 return *this; }
};
- 析构函数(Destructor):当对象生命周期结束时,会调用析构函数。如果没有提供析构函数,编译器会生成一个默认的析构函数,它执行默认的清理操作。
class MyClass {
public: ~MyClass() { // 析构函数 }
};
- 转换构造函数(Converting Constructor):这种函数允许一个类的对象被用作初始化另一个类的对象的构造函数的参数。这种构造函数不是由编译器自动生成的,而是由程序员在类定义中显式声明的。
这些默认函数在大多数情况下都足够使用,但在某些特定场景下,你可能需要自定义这些函数以满足特定的需求,例如执行深拷贝、禁止拷贝、启用移动语义等。此外,C++11之后还引入了= default和= delete语法来显式地要求编译器生成默认实现或删除这些特殊成员函数。
2、C++的虚函数实现机制?
虚函数是实现多态(动态绑定)/接口函数的基础。利用虚表实现。
C++对象的内存布局,对象的前8位(64位系统)为虚表指针(vtpr),指向对象所对应的虚表。虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。
同一个类的不同实例共用同一份虚函数表,他们都通过一个虚函数表指针指向该虚函数表。
C++中的虚函数表实现机制以及用C语言对其进行的模拟实现
题目:
- 讲一下C++的多态? —— 静态多态(函数重载),动态多态(虚函数)
- 讲一下C++的虚函数的实现机制?
- 多继承情况下,基类Base1与Base2都有虚函数,继承自Base1和Base2的子类Derived1有几个虚表?
----- 1个。Derived1的虚函数表仍然是保存到第1个拥有虚函数表的那个基类的后面的。(详情可见于上文博文链接)
3、C++的智能指针
C++11为C++标准库带来了三个智能指针,分别是shared_ptr,unique_ptr与weak_ptr。
C++智能指针的实现原理为引用计数。引用计数无法处理循环引用的情况。
shared_ptr实现原理是同一个内存空间每多一个指针指向就计数加1,如果计数变为0就释放内存空间。当用普通指针初始化的时候,只能使用一次普通指针。它还可以自定义释放函数。
unique_ptr是计数只能为1,没有拷贝构造函数。
weak_ptr只能指向该内存空间而没有所有权。主要用于辅助第一个指针,防止出现互锁。借助weak_ptr类型指针, 我们可以获取shared_ptr指针的一些状态信息,比如有多少指向相同的shared_ptr指针、shared_ptr指针指向的堆内存是否已经被释放等等。在构建weak_ptr指针对象时,可经常利用已有的shared_ptr指针为其初始化。
Modern C++ 智能指针详解
4、互斥锁、可重入锁、读写锁与自旋锁
- mutex 互斥量
mutex是睡眠等待类型的锁,当线程抢互斥锁失败的时候,线程会陷入休眠。优点就是节省CPU资源,缺点就是休眠唤醒会消耗一点时间。
依据同一线程是否能多次加锁,把互斥量又分为如下两类:
是:递归互斥量recursive mutex,也称可重入锁,reentrant lock
否:非递归互斥量non-recursive mutex,也称不可重入锁,non-reentrant mutex
- read-write lock 读写锁
又称“共享-独占锁”,对于临界区区分读和写,读共享,写独占。
读写锁的特性:
当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞。
当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功。
适用于多读少写的场景。
- spinlock 自旋锁
自旋,更通俗的一个词时“忙等待”(busy waiting)。最通俗的一个理解,其实就是死循环。
自旋锁不会引起线程休眠。当共享资源的状态不满足时,自旋锁会不停地循环检测状态(循环检测状态利用了CPU提供的原语Compare&Exchange来保证原子性)。因为不会陷入休眠,而是忙等待的方式也就不需要条件变量。不休眠就不会引起上下文切换,但是会比较浪费CPU。
题目:
- 讲一下可重入锁?
- 讲一下自旋锁?自旋锁循环检测状态的时候如何保证原子性?
3、QT信号与槽的原理?
- Qt信号槽的调用流程
MOC查找头文件中的signal与slots,标记出信号槽。将信号槽信息储存到类静态变量staticMetaObject中,并按照声明的顺序进行存放,建立索引。
connect链接,将信号槽的索引信息放到一个双向链表中,彼此配对。
emit被调用,调用信号函数,且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数。
active函数在双向链表中找到所有与信号对应的槽索引,根据槽索引找到槽函数,执行槽函数。
- 信号槽的实现:元对象编译器MOC
元对象编译器MOC负责解析signals、slot、emit等标准C++不存在的关键字,以及处理Q_OBJECT、Q_PROPERTY、Q_INVOKABLE等相关的宏,生成moc_xxx.cpp的C++文件(使用黑魔法来变现语法糖)。比如信号函数只要声明、不需要自己写实现,就是在这个moc_xxx.cpp文件中自动生成的。
moc的本质就是反射器。
- Qt信号槽的链接方式(connect的第五个参数)
Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。
题目:
- Qt connect的第五个参数(信号槽链接方式)?
- Qt信号槽的调用流程?