1. 信号与槽
优点:类型安全;松散耦合
缺点:同回调函数相比,运行速度较慢
优点:
- 类型安全:需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,则编译器会报错。
- 松散耦合:信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的那个槽接收它发出的信号,它只需要在适当的时间发送适当的信号即可,而不需要关心是否被接收和哪个对象接收了。Qt保证适当的槽得到了调用,即使关联的对象在运行时删除,程序也不会崩溃。
- 灵活性:一个信号可以关联多个槽,多个信号也可以关联同一个槽
缺点:
- 速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍左右。
- 原因:
需要定位接收信号的对象。
安全地遍历所有槽。
编组,解组传递参数。
多线程的时候,信号需要排队等候。(然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损失,对于实时应用程序是可以忽略的。)
回调函数(callback):
1.定义
- 对于普通函数,就是按照实现设定的逻辑和顺序执行。
- 对于回调函数,假设Program A和Program B分别有两个人独立开发。回调函数Fun A2它是由Program A定义,但是由Program B调用。Program B只负责取调用Fun A2,但是不管Fun A2函数具体的功能实现。
2. 由来
因为有这样的使用场景,Fun A2只有在 Fun B1调用时才能执行,有点像中断函数的概念。那可能会有人问,在Program A中不断去查询Fun B1的状态,一旦它被执行了,就让Program A自己去执行Fun A2不行吗?如果你有这样的疑问,说明你已经入门了。
答案是“可以”,但是这样实现的方案不好。因为整个过程中Program A一直都在查询状态,非常耗资源,查询频率高了费CPU,查询频率低了实时性保证不了,Fun B1都执行好久了你才反应过来,Fun A2的执行就会明显晚于Fun B1了。
正因为如此,回调函数才登上了舞台。
3. 如何实现
函数的回调并不复杂,把 Fun A2的函数的地址/指针告诉Program B就可以了。
普通函数和类的静态成员函数都分配有确定的函数地址,但是类的普通函数是类共用的,并不会给类的每一个实例都分配一个独立的成员函数,这样就太浪费存储资源了。
参考:https://blog.csdn.net/zhoupian/article/details/119495949
常用:std::funtion和std::bind的使用
std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等[1]。
std::bind()函数的意义就像它的函数名一样,是用来绑定函数调用的某些参数的[2]。
2. static和const的使用
- static:静态变量声明,分为局部静态变量,全局静态变量,类静态成员变量。也可修饰类成员函数。
(1)局部静态变量:存储在静态存储区,程序运行期间只被初始化一次,作用域仍然为局部作用域,在变量定义的函数或语句块中有效,程序结束时由操作系统回收资源。
(2)全局静态变量:存储在静态存储区,静态存储区中的资源在程序运行期间会一直存在,直到程序结束由系统回收。未初始化的变量会默认为0,作用域在声明他的文件中有效。
(3)类静态成员变量:被类的所有对象共享,包括子对象。必须在类外初始化,不可以在构造函数内进行初始化。
(4)类静态成员函数:所有对象共享该函数,不含this指针,不可使用类中非静态成员。
- const:常量声明,类常成员函数声明。 const和static不可同时修饰类成员函数,const修饰成员函数表示不能修改对象的状态,static修饰成员函数表示该函数属于类,不属于对象,二者相互矛盾。const修饰变量时表示变量不可修改,修饰成员函数表示不可修改任意成员变量。
3. 指针常量、常量指针,常指针常量
int main()
{
int x = 10;
int y = 20;
// 指针常量(常指针):数据类型 * const 指针 = 变量
// 指向的位置不能发生改边
int* const p1 = &x; // 指针常量,p1不可更改
// p1 = &y; 错误
*p1 = 30; // 正确,变量的值可以更改
cout << "x: " << x << endl; // x: 30
// 常量指针:const 数据类型 * 指针 = 变量
// 或:数据类型 cosnt * 指针 = 变量
// 指向的值不能改变
const int* p2 = &x; // 常量指针,*p2不可变
// *p2 = y; 错误
p2 = p1; // 正确,p2可变
// 常指针常量:const 数据类型 * cosnt 指针 = 变量
const int * const p3 = &x; // p3和*p3均不可更改
// p3 = p2; 错误
// *p3 = 20; 错误
}
4. 指针和引用的异同
指针:是一个变量,但是这个变量存储的是另一个变量的地址,我们可以通过访问这个地址来修改变量。
引用:是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行的操作。
相同点:二者都可以对变量进行修改。
不同点:指针可以不必须初始化,引用必须初始化。
指针可以有多级,但是引用只有一级(int&& a不合法, int** p合法)。
指针在初始化后可以改变,引用不能进行改变,即无法再对另一个同类型对象进行引用。
sizeof指针可以得到指针本身大小,sizeof引用得到的是变量本身大小。
指针传参还是值传递,引用传参传的是变量本身。
注:
值传递:形参是实参的拷贝,改变形参的值并不会影响外部实参的值。
引用传递:形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
5. 多态ref:(75条消息) C++---静态多态与动态多态_Jammm的博客-CSDN博客https://blog.csdn.net/qq_37934101/article/details/81365449
(1)静态多态
称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
静态多态有两种实现方式:
- 函数重载:包括普通函数的重载和成员函数的重载
重载函数的关键是函数参数列表——也称函数特征标。包括:函数的参数数目和类型,以及参数的排列顺序。所以,重载函数与返回值,参数名无关。
- 函数模板的使用
函数模板是通用的函数描述,也就是说,使用泛型来定义函数,其中泛型可用具体的类型(int 、double等)替换。通过将类型作为参数,传递给模板,可使编译器生成该类型的函数。
// 交换两个值,但是不清楚是int 还是 double,如果不使用模板,则要写两份代码
// 使用函数模板,将类型作为参数传递
template<class T>
class Swa(T a,T b)
{
T temp;
temp = a;
a = b;
b = temp;
};
(2)动态多态
动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
动态绑定:
1.通过基类类型的引用或者指针调用虚函数
2.必须是虚函数(派生类一定要重写基类中的虚函数)
3.纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
虚函数表:对于有虚函数的类,编译器都会维护一张虚函数表(虚表),对象的前四个字节就是指向虚表的指针(虚表指针)。该类的每个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),需指针指向虚函数表。
注意:对象不包含虚函数表,只有需指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
6. 常用数据结构
vector:向量,连续存储,可随机访问。
deque:双向队列,连续存储,随机访问。
list:链表,内存不连续,不支持随机访问。
stack:栈,不可随机访问,只允许再开头增加/删除元素。
queue:单向队列,尾部增加,开头删除。
set:集合,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
map:图,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
hash_set:哈希表,随机访问。查找、插入、删除时间复杂读为O(1)。
7.struct 和class 有什么区别?
- struct的成员默认是public属性的,class的成员默认是private属性的
- struct继承默认是public属性的,class继承默认是private属性的
- struct不可以使用模板,class可
8. 虚函数
为什么要虚继承?
非虚继承时,显然D会继承两次A ,内部就会存储两份A的数据,浪费空间,而且还会有二义性,D调用A的方法时,由于有两个A,究竟调用哪个A的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚继承下,只有一个共享的基类对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚基类,在虚继承下,基类对象的复制以及由此引起的二义性被消除了。
如果虚函数是有效的,那为什么不把所有函数设为虚函数?
虚函数是有代价的,由于每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候会产生一定的系统开销,这是没有必要的。另外,虚函数的调用相对于普通函数要更慢一些,因此每次都要查找虚函数表,有一定的时间开销。