C++开发工程师面经总结

C++开发工程师面经总结

C++语言基础

  1. C++语言特点及与C语言的区别

    C++在C语言基础上引入面向对象的特性,并兼容c语言;

    c++代码质量好,执行效率高;

    c++增加许多高级特性,使c++使用更加便捷安全,例如引用,4类cast转换,类与对象,模板,智能指针等,stl模板库等。

    c++是面向对象的,c语言主要是面向过程的语言。

  2. c++中struct与class的区别

    struct一般用于一个数据结构的集合,class一般用于对象数据的封装;

    struct默认访问权限是public,class默认是private;

    struct与class均可以做继承,但是struct默认公有继承可修改,class默认私有继承;

    class关键字能用于定义模板,struct不可以。

  3. c++中什么是模板

    template

    模板是一种泛型编程,能独立与各种类型绑定编码;

    模板通常有函数模板与类模板;

    函数模板可以隐式调用或者显示调用,即自动绑定类型或者直接尖括号显示指定类型。

    函数模板会有两次编译,一个在声明处编译,一个在调用处对替换类型后的代码再编译。

    类模板有高复用性,编译时检查数据类型比较安全,然后移植性高。

  4. include头文件的顺序以及双引号“”和尖括号<>的区别

    尖括号的头文件是系统文件,双引号的头文件是自定义文件

    使用尖括号的头文件查找路径是:编译器设置的头文件路径->系统变量

    使用双引号的头文件查找路径是:当前头文件目录->编译器设置的头文件路径->系统变量

  5. 数组与指针的区别

    数组是存储多个相同类型数据的集合,数组名是首元素的地址

    指针是一个变量,其本身也有地址,变量值为其他变量在内存中的地址

    同类型指针变量可以相互赋值,数组不行,只能一个一个元素赋值拷贝

    求占用内存大小时:32位平台下指针为4个字节,64位为8个字节,数组大小sizeof(数组名)/sizeof(数组类型)

  6. 模板类的实现

    模板类是C++中的一种强大的泛型编程工具,可以处理多种类型的数据,并且在编译时自动生成特定类型的代码。

    优点:

    \1. 代码复用

    \2. 减少代码冗余

    1. 类型安全

    模板类比传统的宏或者泛型类更安全,因为模板类在编译期进行类型检查,任何类型不匹配问题在编译时就会暴露出来。

    4.灵活性:能够处理自定义类型

    模板类的局限性

    1. 编译时间增加:因为模板类在编译时生成特定类型的代码,所以使用大量模板可能会增加编译时间。
    2. 代码膨胀:对于每个不同的类型实例,编译器会生成不同的代码,可能导致代码膨胀,增加可执行文件的大小。
    3. 复杂的错误信息:模板编译时的错误信息有时比较难以理解,特别是模板嵌套时,错误信息会非常冗长。

    模板类隐式实例化:在编译过程中编译器决定使用什么类型实例化一个模板,className<int> P; 在尖括号中直接写入类型名去实例化一个类对象。

    模板类显式实例化:程序员直接代码中明确模板类使用的类型,template class className<int>; 直接在程序中尖括号类显式明确类采用的模板类型,可以提前生成模板实例,避免重复实例化。(因为编译期间会创建对应的模版代码,隐式实例化可能会每个地方都创建一次,重复实例化代码,增加编译时间和代码体积)这在需要优化编译时间或者需要确保某些实例化确实存在时非常有用

    **模板具体化:**当模板使用某种类型生成的类或者函数不能满足需要,可以通过模板具体化时修改类中定义,template<> class className<int> {}; 花括号体内可以直接重新定义类的某些函数等,相当于对通用模板类进行了重写。

    // 通用模板类
    template <typename T>
    class Stack{}
    
    // 针对特定类型 char* 的完全具体化
    template <>
    class Stack<char*>{}
    

    类什么时候实例化通常看是否需要分配内存空间,例如声明不需要,引用是操作同一块空间则也不需要实例化,指针使用一块固定地址的话则也不需要。

  7. 程序是怎么执行的

    预处理:宏定义删除并展开宏,引入头文件代码,删除注释,添加行号和文件名标识等,预处理后依然是.cpp源文件;

    编译:对预处理的源文件进行代码分析与优化,(词法分析:关键字标识符识别,语法分析:,语义分析:无类型错误、未定义的变量、类型不匹配等问题)生成汇编程序.s文件,也是ASCII文件;

    汇编:对汇编程序进行转换为二进制.o文件,链接才能执行。

    链接:将所有二进制文件与链接库包括动态链接库与静态链接库链接在一起,生成exe可执行文件(window),linux下为.out文件。

    静态链接在链接时将函数与过程链接到可执行文件中,就算删除链接库也不影响程序;生成的静态链接库,Windows下以.lib,Linux下以.a。

    动态链接是执行中再找链接的方法与过程,可执行文件中只有链接库的定位信息,window下是dll,linux下是.so。

  8. static关键字作用

    全局静态变量:作用域为该文件中,其它文件不可见,存储在全局静态变量静态存储区

    局部静态变量:在静态存储区初始化,作用域依然为局部作用域,但离开作用域不会被销毁,依然在内存中,下次调用函数时直接取出该变量,不用再分配内存。

    静态函数:只在声明其的文件中可见,外部文件不可见,函数实现需要static修饰,不会与同名函数冲突。

    类静态成员:可以实现多个对象的数据共享,并保证了数据的隐藏性,类外定义无需加static,可以被普通成员函数或静态成员函数使用,注意静态类成员变量要在类外初始化。

    类静态成员函数:与类绑定而非对象,实现中可使用静态成员,但不能直接引用非静态成员,使用非静态成员时需要采用对象来引用,使用类静态成员函数直接通过类名引用或者对象使用。类静态成员函数定义可在类中也可在类外,类外定义不可加static。

  9. extern用法

    extern可以用于变量和函数前面,用来说明该变量或者函数的定义在其它地方,若声明与定义处有不符,则会导致运行报错。

  10. c++四种cast强制转换

static_cast:可以用于执行类相关类型转换,例如基类向派生类,派生类向基类的转换,不会执行安全检查,主要执行非多态的类型转换;还有一般类型转换,如基本数据类型转换,void*与其它类型指针的转换。

const_cast:一般用于const属性的转化,例如增加const属性或者删除const属性,唯一一个操作const操作符的转换。

dynamic_cast:专门用于类之间的转换,即转换类型必须是类指针、类引用或者void*,派生类转换为基类与隐式转换相同,基类转换为派生类仅当基类指向对象为派生类时才可转换成功,也即基类最开始的指向是转换目标的完整有效对象才可转换成功,转换成功返回转换类型,失败则返回空指针;另外也可用将任何类型指针转化为void指针。

reinterpret_cast:几乎可以做任何类型转换,如int转指针,但是不检查指向内容,也不检查指针类型自身,容易出问题。

  1. 函数指针与指针函数

    函数指针:即指向函数的指针变量,存放着函数的入口地址,通常用于函数回调,即别人的库函数里调用我们的函数即回调,如sort排序可以自定义排序规则函数。

    指针函数:一个函数,返回值是指针。

  2. nullptr能否调用成员函数

    如果是编译则可以通过,因为编译期间绑定地址与是否为空无关,但是调用时若解引用了这个空指针则报错。

  3. 野指针怎么产生和避免

    野指针就是指向位置不可知不正确的指针,产生原因通常有:指针指向释放后指针未置nullptr、指针指向变量在指针之前被销毁(如指针指向一个函数局部变量)、局部指针变量未初始化(局部变量初始化随机)等等。

    避免野指针方法:定义时初始化为空、释放之后置空、申请内存后判定是否为空、使用智能指针等等。

  4. 堆和栈的区别

    空间分配不同:栈由系统自动分配释放,存放局部变量以及函数参数值等;堆由程序员分配释放。

    缓存方式不同:栈使用一级缓存,调用完立即释放,堆处于二级缓存,速度更慢一点。

    数据结构不同:栈先进后出,内存地址连续,堆为树结构,内存不连续

  5. 智能指针

    头文件memory,智能指针是一个类,在构造函数中传入一个普通指针,析构函数中释放传入的指针,智能指针的类是栈上的对象,函数结束或者程序结束时后自动释放。

    unique_ptr:是一种独占型指针,在c11中用来代替c98里的auto_ptr,以避免auto_ptr可能存在的内存崩溃问题;unique_ptr保证了同一时间只有一个智能指针可以指向某个对象,无法直接对其复制,在编译器就会进行检查。

    unique_ptr<string> p3 (new string ("auto"));   //#4
    unique_ptr<string> p4;                       //#5
    p4 = p3;//此时会报错!!在c98中则可以,因此两个智能指针指向同一内存可能会内存崩溃
    unique_ptr<string> pu3; 
    pu3 = unique_ptr<string>(new string ("You"));   // #2 对于临时的右值对象则可以复制
    

    shared_ptr:是共享型指针,可以使用多个shared_ptr指向一块内存空间,其使用引用计数的方式管理内存,计数为0时自动释放空间。但是当两个对象使用share_ptr指向对方且对象内又有对方的share_ptr时,可能导致循环引用导致死锁,引用计数可能会失效,从而导致内存泄漏。多个线程可以同时读,但是要写操作的话需要加锁。

    use_count 返回引用计数的个数
    unique 返回是否是独占所有权( use_count 为 1)
    swap 交换两个shared_ptr 对象(即交换所拥有的对象)
    reset 指针置空,放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
    get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.
    

    weak_ptr:是一种不控制对象声明周期的指针,即其指向一个share_ptr管理的对象,就是协助share_ptr进行对象管理,可以用来放置share_ptr出现循环引用导致内存泄漏的风险;weak_ptr是一种对对象的弱引用,即它不会增加share_ptr的引用计数,并且可以与share_ptr互相转化,share_ptr可以直接赋值给weak_ptr,同时可以对weak_ptr调用lock函数来获取到share_ptr。

    注意weak_ptr不能直接访问其指向对象的方法,需要先通过lock函数转化为share_ptr再进行访问。

  6. 内联函数与宏函数的区别

    宏定义本质不是函数,预处理时复制宏代码无函数压栈退栈过程,提高效率;

    宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性

    内联函数则是一个函数,编译时在调用处将函数体展开,省了调用开销,提高了效率。

    宏定义无类型检查,内联函数则满足函数提醒,也有类型检查,通常用于比较短使用频繁的函数,一般定义也在头文件,如果内联函数太冗长,则导致代码量过大消耗过多内存空间,并且也没有意义了。

    内联函数定义在头文件中是为了让编译器在每个包含头文件的源文件中都能看到该函数的定义,从而实现内联展开,如果其他源文件使用该内联函数,而看不到定义可能出现链接问题

  7. ++i与i++

    均是做i的加1操作,前者先加再赋值,后者先赋值再加,后者效率更慢;i++不能做左值,++i可以,两者均不是原子操作。

  8. new与malloc的区别

    new为操作符,而malloc为函数,new调用时会先分配内存,再调用构造函数,释放时调用析构函数,malloc无构造与析构;

    new无需指定内存大小,也不用进行指针类型转换,malloc需要指定分配内存大小,返回的指针也需要强转。

    new发生错误会抛出异常,malloc会返回null,new会更加安全。

    new操作符可以重载,但是malloc不可以重载。

  9. const与define的区别

    两者均可以定义常量,但const生效于编译阶段,define在预处理阶段生效;

    const常量需要额外内存空间,define常量是直接作为操作数使用,无需内存空间;

    const常量有类型,而define没有类型,不利于做类型检查。

  10. c++几种传值方式

    值传递:值传递会拷贝对象,效率低,形参变化也不会影响实参的值。

    引用传递:直接绑定对象,效率高,形参变化会影响实参。

    指针传递:同理与引用但是操作指针不如操作引用更安全。

  11. 指针常量与常量指针

    int const *a:常量指针,内容为常量,地址可以改变,相当int const (*a)

    int *const a:指针常量,内容可以变,但是指向地址不变,相当于int * (const a)。

  12. 内存分配的方式

    c++中内存主要分为5个区。从上到下在内存中是从高到低

    常量存储区:通常存放常量,不允许修改。

    栈:通常存放函数局部变量,函数执行结束自动释放内存;

    堆:通常是程序员分配的空间,如new,malloc等,需要由程序员自己释放内存;

    全局/静态存储区:通常存放全局变量和静态变量。通常会分为数据段(存放已初始化的全局变量与静态变量)与BSS段(存放未初始化或者初始化为0的全局变量和静态变量)。

    程序代码区:用于存放二进制代码。

    内存的分配注意判断以及初始化为nullptr,堆区内存要注意释放并及时置空,以免产生野指针。

  13. 什么是内存对齐?作用是什么

    内存通常按照字节划分,让各种类型数据按照一定规则存放在某个位置,使对其的访问更加高效即内存对齐,如一个变量内存中存放的地址正好是其长度的整数倍,则称为自然对齐。

    如一个int型数据存放地址正好是4个字节的整数倍,则可以一次取出数据,如果地址为奇数则可能会通过一次char查询一次int查询才取出数据。通常64位机器中char为1个字节,short为2个字节,int为4个字节,float为4个字节,double为8个字节。通过内存对齐使数据的访问更加高效,在某些机器上如果没有内存对齐甚至可能会出错。

  14. 面向对象与面向过程区别

    面向对象是一种编程思想,即将一切事物看做一个对象,一种种类,他们均有各自的属性以及行为方法,将这些属性与方法封装可以成为一个类。

    面向过程是根据业务逻辑从上到下写代码。

    面向对象是进行类的封装,提高代码复用性,提高开发效率。

  15. 面向对象的特征与继承方式

    封装:将类的属性与方法封装起来,隐藏对象的一些属性与实现细节,仅提供对外接口与外界对象交互,提高程序安全性。对象只能访问类的public成员,protected与private成员均不可访问。

    继承:通过对基类进行拓展,在能使用现有功能前提下,无需重新编写原有功能进行新的拓展。

    对于基类的各个成员

    public继承:private不可见,protected仍为protected成员,public仍为public成员,派生类对象只能访问基类的public成员。

    protected继承:private不可见,protected仍为protected成员,public变为protected成员,派生类对象对于基类成员均不可访问。

    private继承:private不可见,protected变成private成员,public变成private成员,派生类对象对于基类成员均不可访问。

    多态:即不同对象对于同一行为会有多种状态,如父类指针可以根据指向的子类对象,相同的函数会有不同的状态。实现方法通常为重写和重载。

  16. c++中重写和重载

    重写:指派生类中重新定义的函数,其函数名,参数列表,返回值类型均与基类中的一致,只有函数体会不一样,派生类对象调用时会调用派生类中的重写函数,基类的被重写函数不会调用,并且基类中的被重写函数必须有virtual修饰。(如果其它一样,但是返回值类型不同,那就是函数隐藏,基类中的同名函数被隐藏,不会被调用)

    重载:一些函数的功能相似但有细节不同,如数值类型不同,这时通过使用相同的函数名,而参数类型、顺序、个数有区别(返回值类型不同不可以构成重载),使两个函数有区分,实现了函数重载。

  17. 多态原理与使用

    原理:

    1、当类中存在虚函数时,编译器会在类中自动生成一个虚函数表
    2、虚函数表是一个存储类成员函数指针的数据结构
    3、虚函数表由编译器自动生成和维护
    4、virtual 修饰的成员函数会被编译器放入虚函数表中
    5、存在虚函数时,编译器会为对象自动生成一个指向虚函数表的指针(通常称之为 vptr 指针)

    继承了父类虚函数的子类,编译器会自动为重写函数添加一个virtual关键字,编译器会找到指针指向对象的vptr指针,然后找到其对应虚函数表中的虚函数调用,实现了多态,如果父类指针指向子类对象,则编译器会找到子类对象中的虚函数表里的虚函数并调用。

    以上是动态多态(函数调用在运行时绑定),静态多态指的是重载(函数调用在编译器决定)

  18. c++构造函数类型

    默认构造函数:编译器自动生成的构造函数,函数体为空。

    普通构造函数:可以有多个,但是参数数目或类型需要不同,也能通过该构造函数进行类成员初始化。

    拷贝构造函数:函数参数为对象的引用,用于根据一个已经存在的对象复制出一个新的对象,没有显式写拷贝构造时候系统会自动生成一个拷贝构造函数,默认拷贝是浅拷贝,浅拷贝通过值传递两个变量同时操作一个内存,可能导致内存泄漏风险,深拷贝需要额外开辟一块内存空间,导致效率降低。

    移动构造函数:参数类型为右值引用(&&)的拷贝构造函数,避免了深拷贝与浅拷贝的问题,无需新开辟内存避免了深拷贝问题,如果传入对象时临时值,则会选择使用移动拷贝构造,将临时变量的资源做浅拷贝就避免了左值引用浅拷贝的问题,move函数是强制将左值对象转换为右值。

  19. 一个类默认生成哪些函数,多重继承时类的初始化顺序及析构顺序

    默认生成:无参构造函数,拷贝构造函数,移动构造函数,拷贝赋值运算符重载(用于将一个对象的内容拷贝到另一个对象中。),移动赋值运算符重置(用于将一个对象的内容移动到另一个对象中,以避免不必要的拷贝),析构函数。 ·

    创建派生类对象时,优先调用基类构造函数;

    如果类中有成员类,则成员类的构造函数也优先调用;

    父类构造函数->成员类对象构造函数->自身构造函数;

    派生类Derive(): public base1, virtual public base2, public base3, virtual public base4{

    	Obj1 obj1;
    
    	Obj2 obj2;
    

    };

    派生类对象内包含多个子类成员,同时继承了多个基类,基类中加virtual的则为虚继承,该基类为虚基类,目的是让继承虚基类的派生类能共享虚基类中命名变量,即防止多次继承同一个基类的派生类会产生命名冲突,通过虚继承使派生类对同一命名的虚基类成员只持有一个,不会同时继承多个同命名变量。

    此时构造函数顺序为:base2, base4, base1, base3, obj1, obj2, derive;

    即优先构造虚基类,顺序以继承顺序排序,其次构造普通继承的基类,顺序以继承顺序,再构造子类构造,顺序以初始化顺序,最后构造自身构造函数。

    析构函数的顺序则与构造函数的相反。

  20. 虚析构和虚构造

    虚析构:将可能被继承的基类的析构函数设置为虚函数,当使用基类指针指向子类对象时,释放基类指针也能释放掉子类的空间,能防止内存泄漏,如果基类析构函数不是虚函数,则可能导致子类空间无法被析构。默认情况下析构函数不是虚函数,因为虚函数需要虚函数表与虚表指针,需要额外空间。

    不能虚构造:虚函数需要在虚函数表里调用,但是如果有虚构造函数的话,对象没有实例化,没内存空间分配,那就无法调用虚函数表,自相矛盾了,另外虚构造函数也没有什么实际的意义。

  21. c++类中定义引用数据成员

    1.不能使用默认构造函数去初始化,要提供构造函数去初始化引用数据成员,否则发生未初始化错误。

    2.初始化引用成员时构造函数的形参也必须是引用类型。

    3.不能在构造函数内初始化,必须在初始化列表中进行初始化。

    初始化列表一般用于对那些定义时必须赋初始值的变量进行初始化,如引用,const类型变量。采用初始化列表方式进行变量初始化效率更高,因为采用构造函数内初始化是先定义,再初始化,初始化列表是定义与初始化同时进行,同时构造函数内初始化可能再类实例化时会先进行默认初始化构造,再进入自定义的构造函数中进行变量初始化。

  22. 类中常函数是什么

    类的成员函数的最后面加上一个const关键字,表明这个函数不会对类的非静态成员变量做修改(可以修改类静态成员变量),只能对数据成员进行读取,另外const修饰的常量对象可以调用常函数,不可调用无const修饰的函数(可以调用静态成员函数)。

  23. 虚继承以及作用以及虚基类

    虚继承一般用于解决类的多重继承中基类的成员存在二义性的问题以及存储空间浪费的问题,一般出现原因也是菱形继承导致。

    在继承基类时在继承类型前加上virtual关键字可以实现虚继承,被继承的类也叫做虚基类,在派生类中对于同一份基类成员只会存在一份拷贝,也不会出现二义性的问题。

  24. 虚函数与纯虚函数

    虚函数主要是实现多态的机制,虚函数必须是基类的非静态成员函数,通过基类指针指向子类对象,调用虚函数的重写函数时,不同的子类对象会展现不同的调用效果,虚函数绑定是在运行时选择,即动态多态,虚函数是通过虚函数表来进行调用,实例化对象时,会生成一个虚表指针来找到虚函数表,虚函数表中存储着指向该类中各个虚函数的指针。

    纯虚函数是在基类中声明的虚函数,即在虚函数声明后加上"= 0",通过声明纯虚函数使基类成为一个抽象类,对于纯虚函数,子类必须要进行重写以实现多态性,抽象类同时也不可以进行实例化。一般纯虚函数是为了给子类提供一个函数接口,同时必须要进行重写实现,但自身无定义也不可调用,抽象类也可通过创建基类指针或引用来实现多态。

    虚函数与纯虚函数均不可使用static标识,因为static修饰的函数是在编译期间绑定,但是虚函数是动态绑定,并且两者修饰的函数生命周期也不同。

    构造函数可以调用虚函数,但是一般没什么意义,虚函数一般用于多态,基类中构造函数调用虚函数和多态没什么关联。

  25. 拷贝构造函数的传参类型

    传参必须是引用传递。

    如果是值传递的话会再次调用该类的拷贝构造函数,从而无穷无尽地递归调用拷贝构造。

    注意指针方式也是传值,如果传指针则是普通构造。

  26. 拷贝赋值与移动赋值区别

    拷贝赋值是通过拷贝构造函数来赋值,通过同一类创建过的对象来初始化新的对象。

    移动赋值是通过移动构造函数来赋值,二者区别在于:

    拷贝构造函数形参是一个左值引用,移动构造的形参一般是右值引用;

    左值是指那些有持久内存地址的对象;

    右值是指那些没有持久内存地址的值,通常是临时对象

    std::move:C++标准库提供了 std::move 函数,用于将一个左值显式地转换为右值引用,从而触发移动语义。

     // 移动构造函数
        MyClass(MyClass&& other) noexcept : size(other.size), data(other.data) {
            other.data = nullptr;
            other.size = 0;
            std::cout << "Move constructor called" << std::endl;
        }
        在移动构造函数内部,资源(data 指针)从 other 对象中“移动”到新创建的对象中,并且将 other 对象的资源指针设为 nullptr,即使析构函数被调用,仍然不会导致重复释放内存的问题。
        重新赋值:你可以通过赋予一个新的值(调用构造函数或通过赋值运算符)来重用被移动的对象。
    
    避免直接使用:避免直接使用被移动后的对象的成员变量,因为它们可能处于“无效”状态。
    

    拷贝构造一般是对整个对象或者变量的拷贝,移动构造生成一个指针指向源对象的地址,接管其内存,效率高也无浅拷贝的风险。

  27. 仿函数以及作用

    仿函数又称函数对象,是一个能行使函数功能的类。仿函数的类中必须重载operator()运算符。

    仿函数比一般函数更加灵活,能够行使类的功能同时又能行使函数功能,例如在一些回调函数中如sort排序里进行一些特殊的排序筛选操作,第三个参数一般传入一个函数,如果需要在这个函数内加上一个阈值变量就不方便提前设定,而仿函数可以通过类的特性提前初始化一个类成员变量,通过通过函数一样的调用代替一般函数,实现特殊要求的操作。

    class ShorterThan {
    public:
         explicit ShorterThan(int maxLength) : length(maxLength) {}
         bool operator() (const string& str) const {
             return str.length() < length;
         }
     private:
         const int length;
     };
     
     shorterthan s(10);		//仿函数初始化
     bool flag = s("abc");		//类似于函数的调用
    
  28. STL中map, hashTable, deque, list的实现原理

    map:map实现原理是红黑树(结点时红色或者黑色,叶子结点都是黑色空结点,是平衡的二叉搜索树,即左右子树高度相差不高于1,同时结点值左小右大),所以map中元素有序,对map的操作都是对红黑树的一系列操作。

    hashTable:原理是采用函数映射思想将存储位置与记录关键字记录下来,能快速定位查找的内容。

    deque:实现原理是双向队列。元素在内存中连续存放,所有适用于vector的操作都适用于deque,随机存储元素是常数时间,速度仅次于vector。

    list:实现原理是双向链表。元素在内存中不连续排列,任何位置增删都是常数时间,不支持随机存取。

  29. STL的空间配置器allocator介绍

    allocator封装了stl容器在内存管理的底层细节,头文件为memory,allocator一般使用的函数有:

    allocate:分配内存(底层为malloc函数);construct:调用已分配对象的构造函数;destory:调用析构函数;deallocate:用于释放内存(底层为free函数)。

    allocator底层采用malloc分配内存可能存在内存碎片问题,所以其采用了双层级配置器。第一级对于分配较大的内存则采用malloc与free进行分配与释放;当配置内存大小小于128bytes时使用第二级配置器,其内存申请与释放通过一个内存链表来维护内存池,该内存链表通过union结构(联合体,类似struct结构,但是union内所有成员共用一块内存,也即只能使用一个变量,对一个变量修改则就只能使用这个变量,union内存大小取决于union中最大的一个变量所占内存)实现,空闲内存相互挂接在一起,被使用的内存则被链表剔除。

  30. STL中容器及各个时间复杂度

    vector:采用一维数组实现,内存连续,插入(ON),查看(O1),删除(ON)。

    deque:采用双端队列实现,内存连续,插入(ON),查看(O1),删除(ON)。

    list:采用双向链表实现,元素存放在堆中,内存一般不连续,插入(O1),查看(ON),删除(O1)。

    map, set, multimap, multiset:均采用红黑树实现,插入(OlogN),查看(OlogN),删除(OlonN)

    map:关联容器,提供1对1的哈希,采用红黑树实现所以元素排列有序,元素都是pair,但是建立红黑树导致内存消耗较大,对于有顺序要求的问题可使用map,键不能重复且不能修改,map无法插入相同键的键值对。

    multimap:与map区别在于提供了1对多的哈希,可以插入相同键的键值对。

    set:集合,元素不会重复,同时是有序的。

    multiset:与set区别在于允许元素重复。

    unordered_map, unordered_set, unordered_multimap, unordered_multiset: 均采用哈希表(也即散列表)实现,时间复杂度为:插入(O1, 最坏情况为ON,因为要先查找到尾部再插入), 查看(O1, 最坏情况为ON), 删除(O1, 最坏情况为ON)。哈希表通过散列函数来对key值进行空间映射,最简单的方法是余数法,将key与一个数组长度取余,余数作为数组下标,数组存放某个链表头指针,用链表去存放值,通过链表也可以依次存放哈希冲突后的值。所以最坏情况下,所有键值均冲突,此时哈希表变成一个普通链表,插入,查看,删除均要依次遍历,所以复杂度为ON。

    原理采用哈希表实现的结构查询更快,查询问题可以哈希表,但是注意哈希表的建立比较耗时。

  31. STL各个容器使用场景

    vector:随机访问与存取时,随机插入删除比较少。

    list:随机插入删除较多,随机访问存取较少。

    deque:类似排队场景时,例如可能对队列头部或者尾部进行插入删除,可以使用双端队列这种。

    set:主要负责查找不重复元素,并且有顺序要求,插入删除效率高。

    map:对于大量数据中的元素查询,实现高速查询,插入删除效率高。

  32. STL中迭代器作用以及与指针区别

    用于指向顺序容器和关联容器之中的元素,并进行访问与修改。

    迭代器不是指针,是一个模板类,其内部封装了指针,并且模拟了指针的一些功能,例如一些指向,自增自减运算符,并且迭代器隐藏了容器元素的内部结构,只提供了对外接口,符合隐藏原则,迭代器返回的是对象的引用而不是对象的值,所以对值的访问需要对迭代器进行解引用。

  33. STL中迭代器怎么删除元素

    对于序列容器如vector,deque来说,使用erase删除后,后面的元素迭代器全部失效,每个元素往前移动一位,并返回下一个有效的迭代器。

    对于关联容器map,set来说,使用erase后,当前元素迭代器失效,由于内部为红黑树,则不会影响后面的迭代器,也会返回下一个有效迭代器,或者需要在使用erase前记录下下一个元素迭代器。

    list的earse也会返回下一个有效迭代器。

    stack,queue,priority_queue均不支持迭代器。

  34. STL中resize与reserve的区别

    capacity:该值在容器初始化时赋值,表示容器容纳的最大元素数量,但创建时并未创建元素对象。

    size:表示容器当前实际元素数目,可以通过下标访问,因为此时已经有元素对象了。

    resize既分配了空间也创建了对象;reserve只增加了容器预留空间,但是没有创建对象,需要使用insert或者push等操作加入对象。

    resize既修改了size大小也修改了capacity的大小,但是reserve只修改了capacity大小。

    两者形参个数不同,resize有两个参数分别是修改后容器大小以及元素初始值(默认为0),reserve参数只有修改后容器预留的大小。

    注意resize中修改容器大小的参数如果小于当前大小,则会删除多余的元素,保留前面部分。

    (vector虽然是动态增加元素,但是直接reserve分配足够的容量,可以避免vector反复进行内存分配与拷贝元素,提高性能,减少反复分配导致的内存碎片)

  35. push_back与emplace_back的区别

    push_back一般是先构造对象,然后将该对象拷贝到容器末尾,emplace_back直接在容器的末尾构造对象,省了拷贝过程,效率更高。

  36. auto与decltype的区别

    两者均是关键字,decltype用法更像函数,两者功能类似,都是在编译时期进行自动类型推导。

    进行auto推导是通过右值来推导变量类型,所以必须要初始化,同时auto推导的是值类型,对于引用类型(const)会自动去掉引用标识符。

    decltype是根据括号内的变量或者表达式类型来推导出类型,所以无需进行初始化,而且decltype可以推导出引用(const),无需再次添加引用标识符,适用于变量声明、函数返回类型等。

    decltype(auto)这种写法就必须进行初始化,先根据右值类型推导替换掉auto,然后用decltype推导出该类型。一般用于函数返回类型推断和模板编程和泛型编程。

    const int x = 42;
    
    // 返回 `const int&`
    decltype(auto) getXRef() {
        return (x);  // 返回的是 `x` 的引用,且 `x` 是 `const` 的
    }
    
    // 返回 `const int`
    decltype(auto) getXVal() {
        return x;  // 返回的是 `x` 的值,且 `x` 是 `const` 的
    }
    
    

    decltype(x1) x;

    auto 的实现依赖于编译器的类型推导机制。在代码编译时,编译器根据变量的初始化表达式推导出变量的具体类型。其实现大致流程如下:

    1. 编译器遇到 auto 关键字:当编译器遇到 auto 声明时,暂时不确定变量的类型,等待初始化表达式。
    2. 推导变量类型:编译器查看 auto 右边的初始化表达式,分析表达式的类型,并将这个类型赋予 auto
    3. 生成代码:推导完成后,编译器生成带有具体类型的变量声明和相应的代码。

    在 C++14 中,函数返回类型也可以使用 auto。编译器根据函数体中的返回值推导出返回类型。

    auto add(int a, int b) {
        return a + b; // 返回值类型是 int
    }
    
  37. null与nullptr的区别

    在c++中nullptr是一个空指针类型,主要能用来解决null二义性的问题,比如null在c++中表示为0,在一些重载的隐式转化中可能被绑定为int类型而不是void*类型,而nullptr可以很好避免这种问题。因为 NULL0,所以它既可以被解释为整数 0,也可以作为指针使用。这在某些情况下会引起类型不匹配的歧义,特别是在函数重载和模板编程时。

  38. 正则表达式贪婪模式与非贪婪模式

    贪婪匹配:默认是贪婪模式,正则表达式一般趋向于最大长度匹配,也就是所谓的贪婪匹配。如上面使用模式p匹配字符串str,结果就是匹配到。

    非贪婪匹配:在量词后面直接加上一个问号?就是非贪婪模式,就是匹配到结果就好,就少的匹配字符。如上面使用模式p匹配字符串str,结果就是匹配到。

  39. lambda表达式

    就是一种没有名称的函数,语法格式是:

    [capture](parameters)mutable throw() ->ret{body}
    

    capture表示作用域内的捕获列表,[]表示什么也不捕获,函数体无法使用任何作用域变量;[=]表示按值捕获所有变量包括this指针;[&]按引用方式捕获所有变量包括this指针;[=, &a,&b]表示变量a,b按引用捕获,其余全按值捕获;[&, a,b,c]表示a,b,c按值捕获,其余全按引用捕获;[a, &b]表示a按值捕获,b按引用捕获;[this]表示只捕获this指针。

    parameters表示lambad形参列表,可为空,一般情况括号也可以省略。

    mutable是修饰符,是可选的,如果写上则表示取消lambda的常量性,默认lambda是常函数,即不可修改作用域内的值,写上的时候必须要有参数列表的括号。

    throw()是异常说明,也是可选的,用来指示lambda不会引发异常,该修饰符尽量不用。

    ->ret表示返回类型,一般情况下lambda会自动推导返回类型,是可选的。

    body表示函数体,可以访问的变量有:捕获变量,形参变量,局部声明的变量,类数据成员(this被捕获时),具有静态存储持续时间的任何变量,如全局变量。

    lambda短小精悍,让代码紧凑,但是难以实现函数复用。

    编译器会将lambda生成一个匿名类对象,并在其中重载operator()运算符,将lambda函数体中代码复现到operator方法里。

    auto fun1 = [](int a, int b){return a+b;}
    auto p = fun1(1,2);
    
  40. 运算符重载

    返回值类型 operator 运算符名称 (形参表列){
    //TODO:
    }

    本质就是函数重载,通过operator关键字实现,注意不能改变原运算符优先级,不能创建新的运算符,不能重载作用域运算符、四类类型转换、sizeof、条件运算符等等。

    当运算符作为类成员函数时,形参参数一般为一个(后置单目运算符除外),也就是操作符的右操作数,这是因为类的this隐式访问了对象本身,默认充当了左操作数。

    当运算符作为全局函数时,两个操作数均由形参传递,另外在类中会将全局运算符重载声明为friend友元函数,因为可能会访问类中的private变量。

  41. 流运算符的重载

    流运算符重载一般不成为类的成员函数,这是因为类的成员运算符重载一般左操作数是类本身,但是流运算符重载要求第一个操作数是流对象。

    所以针对流运算符重载一般放在类外重载,然后在类中声明为友元函数。

    void operator<<(ostream& out,const Date& d)
    {
    	//流对象需要在操作符左边,然后在对应的类中使用友元
    	out << d._year << "-" << d._month << "-" << d._day << endl;
    }
    
  42. atomoic操作

    atomic是一种原子操作的模板类,在头文件中,原子操作不可被中断,是线程安全的。

    提供了不同的内存顺序选项,用于控制多线程操作中内存可见性和排序。

    std::atomic<int> atomicInt(0); // 定义并初始化一个原子整数
    int value = atomicInt.load(); // 读取原子变量的值
    atomicInt.store(10);          // 设置原子变量的值
    int oldValue = atomicInt.exchange(5); // 将原子变量的值设置为5,并返回旧值
    
  43. 构造函数中调用虚函数

    语法上来说是合理的,但是一般情况下是不建议的,在构造函数调用的虚函数,一般都属于那个构造函数所属于的类,这样可能导致出现一些语义的迷惑性。

  44. STL三大组成
    1. 容器(Containers)

      容器是用来存储和管理数据的集合

    2. 算法(Algorithms)

      STL提供了大量的算法,用于操作容器中的数据,如排序、查找、遍历、拷贝等

    3. 迭代器(Iterators)

      迭代器是用于遍历容器元素的工具,迭代器可以看作是指向容器元素的指针。

  45. C++完美转发(优质引用)

    C++中的完美转发(perfect forwarding)是指在模板函数中,将参数原封不动地传递给另一个函数,既保留其左值/右值属性,也保留其类型(如constvolatile修饰符)。完美转发通常与C++11引入的**右值引用(rvalue reference)以及std::forward**一起使用,保证了参数传递时的高效性和正确性。

    完美转发的实现步骤:

    1. 使用模板参数的转发引用(T&&)
      函数参数应定义为转发引用,也就是T&&。这种形式可以同时接收左值和右值。
    2. 使用std::forward进行条件转发
      在函数内部,使用std::forward<T>对传入的参数进行条件转发。std::forward能够保留参数的左值或右值属性。
    #include <iostream>
    #include <utility>  // for std::forward
    
    // 定义接收任何类型参数的模板函数
    template <typename T>
    void wrapper(T&& arg) {
        // 使用std::forward进行完美转发
        process(std::forward<T>(arg));
    }
    
    // 被转发到的目标函数
    void process(int& x) {
        std::cout << "左值引用被调用: " << x << std::endl;
    }
    
    void process(int&& x) {
        std::cout << "右值引用被调用: " << x << std::endl;
    }
    
    int main() {
        int a = 10;
        
        wrapper(a);          // 传入左值,会调用左值引用的process
        wrapper(20);         // 传入右值,会调用右值引用的process
        
        return 0;
    }
    
    

    优点:

    1. 避免不必要的拷贝或移动

    完美转发在提高性能、减少不必要的拷贝和提高代码通用性方面具有明显的优势,但它也带来了代码复杂性、可读性下降和调试困难等问题。在高性能需求的场景下,完美转发是一项非常有用的技术,

    采用引用的方式能避免拷贝,但是不能接受右值传入。

    volatile的主要功能是防止编译器对变量的读写进行优化,使得程序每次访问该变量时都能获得最新的值。它在硬件编程信号处理中很常见,但在多线程编程中并不是一个适当的同步手段。变量可能被外部的硬件或其他线程修改,编译器无法预测这个修改行为,所以它不会缓存flag的值,而是每次都会去内存中重新读取flag的值。

  46. C++空类占用内存多少,有函数呢

    空类通常占用1字节的内存空间。这个1字节的内存主要是为了保证不同的对象实例有唯一的地址,以满足C++标准中的“对象必须有不同地址”的要求。

    如果在空类中添加成员函数(包括普通成员函数、虚函数或静态函数),空类的大小不会发生变化,依然是1字节。这是因为成员函数并不存储在对象的内存布局中,而是与对象实例无关的代码段。

    当类中包含虚函数时,类的对象会存储一个虚函数表指针(vptr),指向虚函数表(vtable)。这时,空类的大小通常会变大,因为虚函数表指针需要额外的内存空间。

    在64位系统上,虚函数表指针通常占用8字节

  47. 虚函数表是子类和基类都有吗?

    基类有虚函数表,并且虚表中包含基类中虚函数的地址。

    派生类的虚函数表

    • 如果派生类重写了某些基类的虚函数,则派生类有一个新的虚函数表,包含派生类重写的虚函数的地址。
    • 如果派生类没有重写基类的虚函数,它会继承基类的虚函数表。

    每个对象都有一个虚表指针(vptr),指向该类的虚函数表。派生类对象的虚表指针可能指向派生类的虚函数表,也可能指向基类的虚函数表,取决于派生类是否重写了虚函数。

    如果子类重写了某些虚函数,子类会有自己的虚函数表,但它也会包含未重写的基类虚函数的地址。

  48. vector的内存扩展

    std::vector 是一个动态数组容器,它的内存管理具有自动扩展的特性。当向 vector 中添加元素,超过其当前容量时,vector 会进行内存扩展(也称为容量扩展)。

    vector 的容量和大小

    • 大小(size):表示当前 vector 中已经存储的元素的个数。
    • 容量(capacity):表示当前 vector 已经分配的内存空间可以容纳的元素个数。

    内存扩展的原理

    vector 的大小超出当前容量时,发生以下步骤:

    1. 分配更大的内存vector 会分配一块比当前容量更大的连续内存空间。通常,vector 会以几何增长的方式扩展内存(例如,通常扩展为当前容量的两倍)。增长策略有助于减少频繁的内存分配,提高效率。
    2. 移动或复制旧元素:所有现有的元素会被复制或移动到新分配的内存区域。对于复杂对象,vector 会调用对象的复制构造函数或移动构造函数来完成这一过程。
    3. 释放旧内存:旧的内存空间会被释放,新的内存空间开始承载新的元素。

    内存扩展的影响

    • 性能开销:内存扩展时,需要重新分配内存并移动或复制所有元素,这会带来较大的性能开销。尤其在元素数量庞大时,内存扩展的成本显著。因此,频繁的内存扩展应尽量避免。
    • 迭代器失效:当 vector 进行内存扩展后,原有的内存地址不再有效,指向旧内存区域的迭代器、指针、引用会失效,必须重新获取新的迭代器。

    为了避免频繁的内存扩展,可以使用 vectorreserve() 函数提前分配足够的内存。reserve() 设置 vector 的容量,但不会改变其大小,适合在事先知道需要存储的元素数量时使用。

    std::vector 对象本身的内存大小主要包括一个指向数据的指针、当前大小(size_t)和容量(size_t)三个成员变量。

    这些成员变量的大小是固定的,并不随 vector 存储的数据量变化。

    vector 内部存储的数据(即实际元素)是动态分配的,位于堆上,其大小取决于 vector 的当前容量。

  49. 怎么快速知道一个单向链表是否成环

    要快速判断一个单向链表是否成环(即是否存在环),可以使用快慢指针算法(也称为弗洛伊德环检测算法)。

    快慢指针算法

    1. 初始化

      • 设置两个指针,slowfast,都指向链表的头节点。
    2. 遍历

      • 移动 slow 指针一步。
      • 移动 fast 指针两步。
      • 如果 fast 指针遇到 NULLfast->nextNULL,链表没有环。
      • 如果 slow 指针和 fast 指针相遇,链表有环。

操作系统

  1. 常用Linux命令

    linux常用命令

  2. 怎么以root权限运行某个程序
    sudo chown root filename
    sudo chmod a+s filename
    修改文件权限
    
  3. 软链接与硬链接的区别

    软链接:就是符号链接,链接文件中就包含了目标文件的路径名,可以是任意文件或者目录。

    硬链接:就是一个文件的多个文件名,通常不能用于目录,相当于两个指针指向同一块内存,可以用多个文件名与同一文件进行链接,并可以存在同一目录或者不同目录。

    软链接可以对不存在的目录或者文件进行链接,可以交叉文件系统;硬链接只能对存在的文件进行链接,不能对目录进行创建,不能交叉文件系统创建硬链接。

    删除一个硬链接不影响其它相同硬链接的文件除非全删,删除软链接也不影响被指向的文件,删除文件后软链接变悬空了访问会错误。

  4. 进程调度算法有哪些

    先来先服务调度:每次调度都从后备进程队列里选择一个或者多个前面的进程,然后分配内存与资源,放入就绪队列里。属于非抢占式调度,优点是公平,实现简单;缺点是不利于短作业。

    短作业优先调度:从后备队列中选择一个或者多个估计运行时间最短的进程,进行调度运行。降低作业的平均等待时间,提高系统吞吐量。对长作业不利;未考虑作业的紧迫程度;对进程估计执行时间难以预测。

    高优先级优先调度:在后备队列中优先级最高的进行优先分配资源进行创建。常被用于批处理系统中,还可用于实时系统中。

    时间片轮转:在后备队列中按照时间片轮转进行调度,一个时间片可能几个ms到几百ms,时间片结束后计时器发送中断请求,系统停止当前进程并且放入就绪队列列尾,然后资源再分给下个进程。优点是兼顾长短作业;缺点是平均等待时间较长,上下文切换较费时。适用于分时系统。

    多级反馈队列调度算法:有多个进程队列,每个队列优先级不同,在队列中采用时间片轮转调度,优先级越高的队列则时间片越短,当该时间片运行完进程未结束则该进程进入次优先级队列末尾,最低优先级队列的时间片比较长,所以大作业也能执行完。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。

    调度算法可设计为抢占式和非抢占式,其中非抢占式(Nonpreemptive):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统。 抢占式(Preemptive):允许将逻辑上可继续运行的在运行过程暂停的调度方式,可防止单一进程长时间独占,CPU系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)。

    先来先服务是非抢占式的,优先级调度两种都可以设计,时间片轮转是抢占式的。

  5. linux系统态和用户态

    内核态与用户态是操作系统两种运行级别,内核态有最高权限,可以访问所有系统指令;用户态只能访问一部分指令。

    进入内核态的三种方式:1.系统调用;2.异常;3.设备中断。其中系统调用是主动的,另外两种是被动的。

    cpu所有指令中一些指令可能导致一些危险,如内存清理,时钟设置等核心操作不可让用户程序随意进行。

  6. LRU算法(最近最少使用页面置换算法)

    一般用于缓存淘汰,将缓存中最近最少使用的对象删除掉。

    实现方式一般用链表和hashmap

    在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

  7. 什么是页表,其作用是什么

    页表是虚拟内存的概念。操作系统虚拟内存到物理内存的映射表,即页表。

    虚拟内存的每个字节的映射非常多,通过分页能够减少虚拟内存页到物理内存页的映射表大小。

    从虚拟地址到物理地址转换方式有:

    分页,分段,段页式管理(结合前两者优点),哈希页表(适用于地址空间很大情况)

  8. 什么是虚拟内存

    是一种内存管理机制,通过在硬盘上创建一个交换空间(swap space)或页面文件(page file),使计算机能够运行需要比实际物理内存更多内存的应用程序,可以通过页表寻址完成虚拟地址到物理地址的转换。

    物理地址(Physical Address)是指实际存在于计算机硬件中的内存地址。它直接对应于计算机的物理内存(RAM)中的位置。

    虚拟地址(Virtual Address)是指应用程序看到的地址。每个进程都有自己独立的虚拟地址空间,这样一个进程不能直接访问另一个进程的内存。这种隔离提高了系统的安全性和稳定性。

    对大进程通过虚拟内存调度仍然能让其执行;对进程对物理地址的访问进行控制,扩大地址空间,提高了内存安全性;能实现内存共享,方便进程通信,可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。

    但是虚拟内存创建页表占用效率与空间,地址转换也会耗时。

  9. 进程,线程,协程区别

    进程是系统资源分配的最小单元;

    线程是CPU调度的最小单元,一个进程里包含多个线程并发执行任务;

    协程是一种轻量型线程,由程序控制,即在用户态进行,直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,切换开销比线程更小,多个协程属于一个进程,不存在同时写变量冲突,无需多线程的锁机制。

    进程与线程均可并发执行。

    线程是非抢占式的,协程是抢占式的。协程能够保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用状态,协程也无需锁机制,因为相当于是串型执行。

  10. 有了进程为什么需要线程

    线程切换的开销比进程切换要小,因为线程之间共享大部分资源。

    线程共享同一进程的内存空间和资源,如文件描述符和内存。这使得线程间的通信和数据共享变得更高效。

    进程创建和管理较为复杂,可能会导致程序响应速度变慢,尤其是在高并发场景下。

    线程和进程通常会结合使用,以达到最优的性能和可靠性。

    当一个程序需要高可靠性且不能容忍部分崩溃影响整个应用时,使用进程可以提供更好的隔离。例如,浏览器使用多个进程来隔离不同的标签页和插件。资源隔离:不同进程可以拥有独立的资源和内存空间

  11. 进程通信方式与同步方式

    进程通信包括:管道,信号量,共享内存,消息队列,信号,文件,套接字socket。

    **匿名管道:**通过一个管道文件描述符进行数据传输,一个进程写入数据,另一个进程读取数据,适合有亲缘关系的进程,比如父子进程。简单轻量适合简单数据流传送。

    **命名管道:**可以进行无亲缘关系进程通信,使用一个特殊文件表示管道,持久化。

    **消息队列:**是一个操作系统维护的缓冲区,可以存储多个消息,支持消息的优先级和排序。消息队列可以实现消息的随机查询,可以按照消息的类型读取。

    **共享内存:**多个进程通过映射访问同一块内存读写数据。速度快,需要同步机制比如信号量或者互斥锁防止竞争条件和数据一致性问题。

    **信号量:**一般用于同步进程之间的操作,避免竞争条件,保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。信号量可以是计数信号量(用于计数资源)或二值信号量(用于实现互斥锁)。通常与共享内存结合使用,用于同步和协调进程。

    **信号:**用于通知接收进程某个事件的发生。

    **文件:**使用文件作为进程间交换数据的媒介。简单且易于实现,但效率较低。文件操作涉及磁盘I/O,可能会有性能开销。

    **套接字:**支持网络通信和进程间通信(IPC)

    进程同步方式:信号量,管道,消息队列,互斥锁,信号,读写锁。

    **互斥锁:**确保一次只有一个进程(或线程)访问临界区,适用于单个资源的保护。不能解决多个资源的同步问题。可能导致死锁。

    管道:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。

  12. 互斥锁和读写锁

    **互斥锁:**互斥锁是独占锁,用于保护临界区,确保一次只有一个线程能够进入临界区,任何时刻只有一个线程可以持有锁。

    优点:

    • 简单易用,开销较小。
    • 有效防止数据竞争。

    缺点

    • 可能导致死锁。线程A已经持有锁1,并且在持有锁1的同时,尝试获取锁2;

      线程B已经持有锁2,并且在持有锁2的同时,尝试获取锁1;两个线程相互等待对方释放资源,从而形成循环等待,导致死锁。(尽量避免在一个线程中同时持有多个锁。如果必须持有多个锁,应该遵循固定的顺序获取锁。给所有的锁分配一个全局顺序,并确保所有线程按照相同的顺序获取锁。)

    • 无法提升多读少写场景下的并发性。

    **读写锁:**读写锁允许多个线程同时读取共享资源,但在有线程进行写操作时,所有的读操作和其他写操作都会被阻塞。读操作可并发进行,写操作是独占的。适用于读操作多于写操作的场景,可以提高并发性。

    优点

    • 读操作可以并发进行,提高了多读场景下的性能。
    • 适合多读少写的应用,如缓存读写。

    缺点

    • 写操作会阻塞所有读操作和其他写操作。
    • 实现复杂度高于互斥锁。
  13. 死锁和怎么解决死锁

    两个或多个进程或线程因相互等待对方释放资源而无法继续执行。

    解决死锁的方法:

    银行家算法

    • 避免死锁:在每次资源申请时,通过预先判断当前资源分配是否会导致系统进入不安全状态来避免死锁。如果资源分配会导致不安全状态,则拒绝该资源申请。

    超时法

    • 避免和解除死锁:为资源申请设置超时时间,如果进程在等待资源超过了这个时间,则自动放弃申请并释放已占有的资源。这种方法可以减少死锁发生的可能性,但不能完全避免。

    资源有序分配法

    • 预防死锁:给资源分配一个全局的顺序(序号),所有进程或线程按照这个顺序申请资源。一个进程在持有较大序号的资源时不能申请较小序号的资源,所有进程都按照序号顺序请求资源,所以不存在循环等待的问题。
  14. 线程之间通信方式

    线程间的通信方式包括临界区、互斥锁、信号量、条件变量、读写锁

    1. 临界区:每个线程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只准许一个线程进入临界区,进入后不允许其他线程进入。不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。
    2. 条件变量:通过条件变量通知操作的方式来保持多线程同步。
  15. 线程之间同步方式

    互斥锁、信号量、条件变量、读写锁

  16. Sleep和Wait的区别

    (1)sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。

    (2)wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用,进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。

  17. 线程池的使用

    线程池是一组预先创建的线程集合,这些线程在池中等待执行任务。当有新任务提交时,线程池中的一个线程会被分配来执行这个任务,任务完成后,线程会返回到线程池中,等待下一个任务。

    作用:

    提高性能

    • 减少线程创建和销毁开销:线程池通过复用线程来避免频繁创建和销毁线程的开销,提高了性能。
    • 减少延迟:由于线程已经存在于池中,提交任务的延迟时间减少。

    控制并发

    • 限制最大线程数:线程池允许控制并发线程的数量,防止系统资源过度使用,避免线程过多导致的资源竞争和系统性能下降。

    提高资源利用率

    • 线程复用:线程池中的线程在任务完成后不会被销毁,而是被复用,减少了资源的浪费。

    任务管理

    • 任务排队和调度:线程池可以对提交的任务进行排队,并按照一定的策略调度任务执行,例如先来先服务、优先级调度等。

    实现步骤:

    (1)设置一个生产者消费者队列,作为临界资源。

    (2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行

    (3)当任务队列为空时,所有线程阻塞(否则线程会忙等待,会消耗系统资源)。

    (4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。

    线程数量和哪些因素有关:CPU,IO、并行、并发

    CPU核心数决定了可以并行执行的任务数。

    如果是CPU密集型应用,则线程池大小设置为:CPU数目+1 如果是IO密集型应用,则线程池大小设置为:2*CPU数目+1 最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
    

    I/O密集型任务(线程等待时间占比较高):

    • 需要更多线程:因为线程在等待I/O时不会占用CPU,更多的线程可以保持CPU的活跃利用,提高系统的处理能力。

    CPU密集型任务(线程CPU时间占比较高):

    • 需要较少线程:因为线程几乎完全占用CPU,过多的线程会导致频繁的上下文切换和资源竞争,从而降低性能。
  18. 解释同步和异步,阻塞和非阻塞
    1. 同步与异步的区别

      同步:是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。

      异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。

    2. 阻塞与非阻塞的区别

      阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。

      非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。

  19. GDB调试命令
    quit   //退出
    break 7   //第七行设置断点
    break get_sum    //以函数名设置断点
    next	//继续执行
    step   	//继续执行,会跟踪进入函数
    continue   //继续执行
    
  20. 大端与小端

    小端模式:低位字节存储在低位地址,如常见X86结构

    大端模式:高位字节存储在低位地址

    我们可以根据联合体来判断系统是大端还是小端。因为联合体变量总是从低地址存储。

    int fun1(){  
        union test{   
            char c;   
            int i; 
        };  
        test t; t.i = 1;  
        //如果是大端,则t.c为0x00,则t.c != 1,反之是小端  
        return (t.c == 1);  
    } 
    
  21. 操作系统怎么申请和管理内存

    物理内存有四个层次:寄存器,高速缓存,主存,磁盘

    操作系统有一个部分为内存管理器,主要记录哪些内存正在使用,在进程需要时分配内存以及在进程完成时回收内存。

    操作系统进程分配内存有两种方式,分别由两个系统调用完成:*brk和mmap

  22. 一个线程占用多大内存

    一个Linux线程大概占用8M内存

  23. malloc的实现原理

    **malloc底层实现:**当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。

  24. 32位系统能访问4GB以上的内存吗?

    正常情况下是不可以的。原因是计算机使用二进制,每位数只有0或1两个状态,32位正好是2的32次方,正好是4G,所以大于4G就没办法表示了,而在32位的系统中,因其它原因还需要占用一部分空间,所以内存只能识别3G多。要使用4G以上就只能换64位的操作系统了。

    但是使用PAE技术就可以实现 32位系统能访问4GB以上的内存。

  25. 请你说说并发和并行
    1. 并发:对于单个CPU,在一个时刻只有一个进程在运行,但是线程的切换时间则减少到纳秒数量级,多个任务不停来回快速切换。
    2. 并行:对于多个CPU,多个进程同时运行。
  26. Linux中fork的作用

    fork函数用来创建一个子进程。

    • 子进程是父进程的几乎完全的拷贝,包括代码、数据、堆栈等。
    • 子进程和父进程从 fork 返回后的下一行代码开始执行。
    • 因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,

    对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。

  27. 请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程

    **孤儿进程:**该进程的父进程已经终止,但是它自身还在运行的进程。

    孤儿进程会被操作系统中的"Init"进程(通常PID为1)收养,成为这些孤儿进程的新父进程,并等待这些孤儿进程能够正常终止不会成为僵尸进程。

    **僵尸进程:**指已经终止但是其父进程未读取其终止状态的进程。比如指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。

    当一个进程终止时,它的所有资源都会被释放,但仍然会保留一个记录,其中包含了进程的退出状态等信息。

    这个记录会一直保留,直到父进程读取它

    **如何解决僵尸进程:**僵尸进程的存在并不消耗系统资源,但如果系统中存在大量僵尸进程,可能会导致进程表被占满,从而阻碍系统创建新的进程。

    方法1:父进程应当调用 waitwaitpid 函数来读取子进程的终止状态。这是处理僵尸进程的最常见方法。

    方法2:父进程可以通过处理 SIGCHLD 信号来自动读取子进程的终止状态。SIGCHLD 信号会在子进程终止时发送给父进程。父进程可以通过定义一个信号处理函数并使用 sigactionsignal 函数来捕捉 SIGCHLD 信号。

    Unix 信号是一种进程间通信的机制,可以在进程之间传递简单的消息。

    SIGINT:表示终端的中断信号,通常由用户按 Ctrl+C 产生。

    SIGTERM:表示终止信号,通常用于请求程序正常终止。

    SIGKILL:表示强制终止信号,不能被捕捉或忽略。

    SIGCHLD:表示子进程状态改变的信号,通常在子进程终止时发送给父进程。

    方法3:使用kill命令

    ps aux | grep Z 
    

    ​ 会列出进程表中所有僵尸进程的详细内容。

    ​ 然后输入命令:

     kill -s SIGCHLD pid(父进程pid)
    

    终止僵尸进程的父进程会使孤儿进程被 init 进程收养,并由其清理僵尸进程。

  28. 什么是守护进程,如何去实现

    守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,脱离任何控制终端,这样即使用户注销,进程也不会终止。不直接与用户交互。通常用于执行系统服务或其他长期运行的任务。它们在系统引导时启动,并在系统关闭时终止。

    实现守护进程:

    1. 创建子进程第一次 fork:创建一个子进程,然后终止父进程。这是为了使进程在后台运行。

    2. 创建新会话:子进程调用 setsid 创建一个新的会话,并成为会话的领导者和进程组的领导者,从而脱离控制终端和其他进程组。

    3. signal(SIGHUP, SIG_IGN):忽略 SIGHUP挂起信号,以防止子进程在控制终端关闭时收到该信号。

    4. 第二次 fork:创建另一个子进程,然后终止第一个子进程。会话领导者有可能通过打开一个终端设备重新获得控制终端。通过第二次 fork,新的子进程(孙进程)不再是会话领导者,这就确保了它不能重新获得控制终端。

    5. 更改工作目录chdir(“/”):更改工作目录为根目录,以避免阻止文件系统的卸载。

    6. umask(0):重设文件权限掩码,以确保进程创建的文件有适当的权限。

      关闭文件描述符:关闭从父进程继承的所有文件描述符,因为守护进程不需要标准输入、输出和错误输出;还可以避免这些描述符保持打开而导致的资源泄漏。

      重定向文件描述符:因为守护进程不需要标准输入、输出和错误输出,所以将标准输入、输出和错误输出重定向到 /dev/null 或指定的日志文件,可以避免它们对守护进程的操作产生干扰,防止守护进程的输出(例如错误信息)可能会意外地显示在控制台或写入不希望的文件中。

数据库

  1. MySQL语句操作

    MySQL连接

    mysql -u username -p        //连接MySql, -u为用户名, -P为密码
    SHOW DATABASES;				//显示所有数据库
    USE your_database;			//使用指定数据库
    SHOW TABLES;				//显示该数据库所有表
    EXIT;						//退出MySql
    

    数据库操作

    CREATE DATABASE mydatabase;			//创建数据库
    DROP DATABASE [IF EXISTS] <database_name>;				//删除数据库,IF EXISTS 是一个可选的子句,表示如果数据库存在才执行删除操作,避免因为数据库不存在而引发错误
    

    数据表创建

    CREATE TABLE IF NOT EXISTS `runoob_tbl`(		
       `runoob_id` INT UNSIGNED AUTO_INCREMENT,		
       `runoob_title` VARCHAR(100) NOT NULL,
       `runoob_author` VARCHAR(40) NOT NULL,
       `submission_date` DATE,
       PRIMARY KEY ( `runoob_id` )
    )ENGINE=InnoDB DEFAULT CHARSET=utf8;
    //不存在则创建表
    //UNSIGNED表示为非负整数AUTO_INCREMENT 表示该列值自动递增
    //VARCHAR为可变长度字符,长度最大100, NOT NULL表示该列不能为空,即插入数据时必须插入值
    //PRIMARY KEY定义了主键
    
    DROP TABLE [IF EXISTS] table_name;  -- 会检查是否存在,如果存在则删除
    

    插入数据

    INSERT INTO table_name (username, email, birthdate, is_active)
    VALUES
        ('test1', 'test1@runoob.com', '1985-07-10', true),
        ('test2', 'test2@runoob.com', '1988-11-25', false),
        ('test3', 'test3@runoob.com', '1993-05-03', true);
    

    查询数据

    SELECT column1, column2, ...
    FROM table_name
    [WHERE condition]
    [ORDER BY column_name [ASC | DESC]]
    [LIMIT number];
    //Where过滤条件
    //ORDER BY指定结果排序方式
    ASC为默认的升序
    LIMIT表示指定返回的行数
    
    SELECT id, name, position
    FROM employees
    WHERE position LIKE 'Manager%';
    //LIKE为字符串匹配关键字(不能作为正则表达式匹配)
    
    SELECT column1, column2, ...
    FROM table1
    WHERE condition1
    UNION
    SELECT column1, column2, ...
    FROM table2
    WHERE condition2
    [ORDER BY column1, column2, ...];
    //UNION 操作符用于将两个或多个 SELECT 语句的结果合并为一个结果集。默认情况下,UNION 会去除重复的行。
    
    SELECT column1, aggregate_function(column2)
    FROM table_name
    WHERE condition
    GROUP BY column1;
    //GROUP BY为分组查询结果,将非聚合列column1和聚合列一起分组返回
    
    SELECT department, AVG(salary) AS average_salary
    FROM employees
    GROUP BY department
    HAVING AVG(salary) > 70000;
    //HAVING作用类似于 WHERE 子句,但 WHERE 是在数据分组之前进行过滤,而 HAVING 是在数据分组之后进行过滤的。
    
    SELECT column1, column2, ...
    FROM table_name
    WHERE column_name IN (SELECT column_name FROM another_table WHERE condition);
    //嵌套查询
    

    操作数据

    UPDATE table_name
    SET column1 = value1, column2 = value2, ...
    WHERE condition;
    //更新数据,SET为关键字,指定要更新的列
    
    ALTER TABLE employees
    ADD hire_date DATE;
    //添加列
    
    ALTER TABLE employees
    DROP COLUMN hire_date;
    //删除列
    
    ALTER TABLE employees
    MODIFY COLUMN salary DECIMAL(10, 2);
    //修改列数据类型
    
    //ALTER:改变表的定义和结构,不影响表中的数据内容。UPDATE:改变表中的现有数据记录。
    
    DELETE FROM table_name
    WHERE condition;
    //删除指定数据
    

    聚合函数

    COUNT()函数统计数据表中包含的记录行的总数
    AVG()函数通过计算返回的行数和每一行数据的和,求得指定列数据的平均值
    SUM()是一个求总和的函数,返回指定列值的总和。
    MAX()返回指定列中的最大值。
    MIN()返回查询列中的最小值。
    
  2. 表和表之间怎么连接的

    连接(JOIN)是用来将两个或多个表的数据关联起来的操作

    **内连接(INNER JOIN):**返回两个表中满足连接条件的记录。它只返回在两个表中都存在的匹配行。如果某一表中的某一行在另一表中没有匹配项,则该行不会出现在结果集中。

    SELECT columns
    FROM table1
    INNER JOIN table2
    ON table1.column = table2.column;
    
    //ON 关键字主要用于 JOIN 语句中,定义连接操作的条件,指定如何将两个或多个表的行匹配在一起。
    //IN 关键字用于 过滤查询结果,它允许你检查一个列的值是否在一个指定的列表或子查询的结果集中。(可以用于非JOIN语句)
    

    **外连接(OUTER JOIN):**返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

    **左连接(LEFT JOIN):**返回左表中的所有记录和右表中满足连接条件的记录。

    **右连接(RIGHT JOIN):**返回右表中的所有记录和左表中满足连接条件的记录。

    **全外连接(FULL JOIN):**返回左表和右表中的所有记录,包括在一个表中存在但在另一个表中不存在的记录。如果没有匹配项,则结果集中的另一表列将包含 NULL 值。

  3. 事务是什么

    事务可以被定义为一个执行单元,通常由多个数据库操作(如插入、更新、删除)组成,是一组操作的集合。

    1. 原子性:事务中的所有操作要么全部成功,要么全部失败。任何一步操作失败,事务将会回滚(撤销)。
    2. 一致性:一致性指事务将数据库从一种状态转变为另一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
    3. 隔离性:要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,这通常使用锁来实现。
    4. 持久性:事务一旦提交,其结果就是永久性的,它对数据库的更改是永久性的,即使系统崩溃或发生故障,这些更改也不会丢失。持久性保证的是事务系统的高可靠性,而不是高可用性。
    -- 开始事务
    START TRANSACTION;
    
    -- 从账户1中扣除金额
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    
    -- 向账户2中添加金额
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    
    -- 提交事务
    COMMIT;
    
    
    ROLLBACK;   //回滚当前事务,撤销事务期间所做的所有更改。这使得数据库恢复到事务开始之前的状态。
    SAVEPOINT savepoint_name;  	//在事务中设置一个保存点,允许在该保存点之后的操作失败时,只回滚到该保存点
    ROLLBACK TO SAVEPOINT savepoint_name;		//回滚到保存点
    RELEASE SAVEPOINT savepoint_name;		//释放保存点
    
  4. 数据库怎么优化

    MySQL数据库优化是多方面的,原则是减少系统的瓶颈,减少资源的占用,增加系统的反应速度。例如,通过优化文件系统,提高磁盘I\O的读写速度;通过优化操作系统调度策略,提高MySQL在高负荷情况下的负载能力;优化表结构、索引、查询语句等使查询响应更快。

    针对查询,我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。

    针对插入,我们可以通过禁用索引、禁用检查等方式来提高插入速度,在插入之后再启用索引和检查。

    针对数据库结构,我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。

  5. 数据库三大范式

    完全依赖:给定主键的值,非主键列的值是唯一确定的

    传递依赖:如果非主键列 A 依赖于非主键列 B,而非主键列 B 又依赖于主键列,那么列 A 就对主键列存在传递依赖。

    一般来说,数据库只需满足第三范式(3NF)就行了

    第一范式(1NF):确保表的每个列都是原子性的,即不可再分的。

    第二范式(2NF):在符合1NF的基础上,消除部分依赖,确保每个非主键列完全依赖于主键。

    第三范式(3NF):在符合2NF的基础上,消除传递依赖,确保每个非主键列直接依赖于主键。

  6. MySQL主从同步是如何实现的?
    1. 主服务器(master)把数据更改记录到二进制日志(binlog)中。
    2. 从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。
    3. 从服务器重做中继日志中的日志(从服务器的 SQL 线程会从中继日志中读取日志事件,并将这些事件应用到从服务器的数据库中。这一过程被称为“重做中继日志”。),把更改应用到自己的数据库上,以达到数据的最终一致性。

    “异步实时”在 MySQL 主从复制中,指的是数据更新的同步过程虽然是异步的(即主服务器的操作不会因为从服务器的同步状态而被阻塞),但复制过程仍然尽量在短时间内完成,以确保数据的快速同步。

    主从复制是异步实时,数据同步是有延迟的。

  7. SqLite和MySql区别

    都是关系型数据库

    **SqLite:**嵌入式数据库,数据库引擎和数据文件集成在一个本地文件中(如 .sqlite.db)。适用于轻量级应用,支持单用户或低并发环境。可以在离线环境开发

    **MySql:**客户端-服务器数据库,数据库服务器和客户端可以分开,通常需要网络连接。需要安装和配置数据库服务器,涉及更复杂的设置和管理。设计用于高并发和大规模数据处理,支持多用户和复杂的查询。中大型网站、企业应用、需要远程访问和高并发的场景。

  8. Redis数据库

    非关系型数据库(NoSQL);可以离线进行本地存储,也可以客户端-服务端网络通信存储。

    键值存储

    • 数据以键值对的形式存储,每个键(Key)对应一个值(Value)。这些值可以是各种数据结构,如字符串、列表、集合等。、
    • 一般数据存储在内存中,这使得读写速度非常快,也可以存到磁盘。
    • 非常适合需要快速访问和高吞吐量的应用场景。
    • Redis 提供了两种主要的持久化方式:
      • RDB(快照):定期将内存中的数据快照保存到磁盘上。
      • AOF(追加文件):记录所有写操作到日志文件中,以便在重启时重做操作。
    //字符串操作
    SET mykey "Hello"
    GET mykey
    
    //集合操作
    SADD:向集合添加一个或多个成员
    SMEMBERS key    //返回集合所有成员
    
    DEL:删除一个或多个键
    EXISTS:检查键是否存在
    
  9. C++中数据库使用

    MySQL:使用 mysql_driver.h和mysql_connection.h进行连接和操作。

    SQLite:使用 sqlite3.h进行本地数据操作。

    Redis:使用 Hiredis.h 库与 Redis 服务器进行交互。

计算机网络

  1. TCP三次握手与四次挥手

    第一次握手:建立连接时,客户端向服务器发送一个SYN(请求连接)数据包,请求连接,等待确认;

    第二次握手:服务端收到SYN包,回复一个确认字符ack,同时发送一个SYN数据包给客户端;

    第三次握手:客户端收到SYN+ack包,再回复一个ack表示自己已经收到,然后连接建立开发发送数据。

    第一次挥手:客户端发送一个FIN(关闭连接)数据包,表示数据发送完毕,请求关闭连接,然后客户端不再发送数据但是能接受数据;

    第二次挥手:服务端发送一个ack表示自己收到了fin包,此时等待剩余数据传输完毕;

    第三次挥手:服务端等待数据传输完毕,向客户端发送一个fin包,表示可以断开连接;

    第四次挥手:客户端收到fin包后回复一个ack表示确认,等待一段时间后确保服务端没数据发送过来后断开连接。

  2. 为什么TCP握手要3次,2次不可以

    一方面TCP连接是双向的,只有经过3次握手才能确保双方都接收到了对方的数据,两次握手只能保证客户端知道服务端收到了自己的数据了。

    另一方面,能防止已经失效的请求连接报文又发送给服务端导致又再次连接,例如客户端第一次请求连接滞留在网络中,然后重发一次给服务端建立起了连接,等关闭连接后这时失效报文重新送到了服务端,如果两次握手则服务端收到后直接给客户端发送一条数据就直接单方面建立连接了导致资源浪费,如果三次握手,服务端向客户端发送确认报文时客户端是不会回应的,因为客户端已经断开等待了,服务端超时没收到回复也会停止等待。确保服务器只有在收到客户端的 ACK 确认后才正式建立连接并分配资源,从而避免资源浪费。

  3. TCP和UDP的区别,它们头部结构如何

    TCP面向连接,即数据传送之前需要经过3次握手建立连接,会话结束后也要结束连接。UDP无需连接可以发送数据。

    TCP保证数据按序发送按序到达,提供超时重传保证可靠,UDP是不可靠的传输协议,虽然是按序发送,但是不保证按序到达也不保证能到达。

    TCP数据首部需要20个字节,UDP只需要8个字节。

    TCP有流量控制与拥塞控制,UDP没有,网络的拥堵也不会影响发送端的发送速率。

    TCP是一对一的连接,而UDP是支持1对1,多对1,多对多的通信。

    TCP面向的是字节流的服务,UDP面向的是报文的服务。面向报文指应用层交付UDP多长的报文,UDP就照样发送,既不合并,也不拆分。面向字节流指应用层与TCP的交互是一次一个数据段,TCP可以对数据段进行划分与合并。

  4. Socket网络编程需要用到哪些函数

    服务端:

    1. Socket创建套接字
    2. bind绑定本地地址和端口Ip和Port
    3. listen监听
    4. accept等待客户端连接
    5. write和read发送接收数据
    6. close关闭连接

    客户端:

    1. 创建Socket
    2. connect连接服务器
    3. write和read发送接收数据,或者send和recv
    4. close关闭连接
  5. 简述网络五层模型

    **物理层:**负责数据的物理传输,如电缆、光纤

    **数据链路层:**提供在物理层上可靠的数据帧传输。以太网、Wi-Fi、PPP协议(点对点协议)

    **网络层:**负责数据包的路由和转发。IP协议

    **传输层:**提供端到端的通信服务。确保数据的完整性、顺序控制、流量控制和错误检测。TCP(传输控制协议)、UDP(用户数据报协议)。

    **应用层:**提供网络服务和应用程序接口。HTTP、FTP(文件传输)、SMTP(邮件传输)、DNS(域名解析为IP地址)。

数据结构与算法

1. 请你来说一说红黑树和AVL树的定义,特点,以及二者区别
  1. **平衡二叉树(AVL树):**左右子树都是平衡二叉树,左右子树高度之差不超过1。由于平衡性比较严格,所以插入和删除操作可能要通过旋转二叉树来重新平衡。对于包含 n个节点的树,其高度为 O(log⁡n)

    插入,删除,查找时间复杂度:o(logn)

    频繁的插入删除操作会导致多次旋转,最多需要O(logn)次旋转,会导致插入删除效率下降。

  2. **红黑树:**是一种自平衡二叉搜索树,每个节点增加一个存储位表示节点的颜色(红色或者黑色),通过颜色规则,从根到叶子路径最长不超过最短的两倍,使得近似保持平衡。

    根节点是黑色;叶子节点都是黑色;如果一个节点是红色,那么其两个子节点都是黑色(即红色节点不能有红色子节点);从任意节点到每个叶子节点(叶子节点指的是最底层的空节点)所有路径包括相同数量的黑色节点。

    插入,删除,查找时间复杂度:o(logn)

    AVL由于其平衡严格性,所以高度一般比红黑树低,所以查找操作上遍历层数可能更少,所以查找效率更高;红黑树允许一定程度不平衡,插入和删除操作上需要旋转调整次数可能更少,通常为常数次,所以插入删除更快,通常比较稳定,所以作为map,set的底层结构。

2.请你回答一下map和unordered_map优点和缺点
  1. map底层是红黑树实现:具有有序性;查找删除插入操作时间复杂度稳定,都是Logn
  2. unordered_map底层是哈希表,插入删除查找时间复杂度是常数级别,但是可能不稳定,取决于哈希函数,极端情况下位oN,并且空间占用率上要哈希表存储键值对,并且在哈希冲突时需要其它链表之类的数据结构存储冲突的数据,并且哈希表一般有些空间会预留以减少冲突,所以空间占用也会通常比较大点。

堆与栈

1.请说一说你理解的stack overflow,并举个简单例子导致栈溢出
  1. 栈溢出一般是程序向栈中某个变量写入字节数超过了这个变量本身申请的字节数,导致栈中与其相邻的变量值被改变。

  2. 栈溢出原因:

    1. 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。局部变量是存储在栈中的
    2. 递归调用过深,每次调用都在栈上分配新空间,可能会导致栈空间耗尽而溢出。
    3. 无限递归,同理,递归函数没正确的终止条件导致不断调用自身。
    4. 指针或数组越界。
    5. 一些语言和运行环境可能用栈管理异常处理,如果异常处理机制使用太多栈空间也可能溢出。

    一般可避免深度递归,或者用迭代方法代替递归;对于大型数据结构使用堆结构动态分配而不是栈;可以在运行环境中配置比较大的栈空间满足需求。

2.请你回答一下栈和堆的区别,以及为什么栈要快
  1. 堆和栈的区别:

    堆是由低地址向高地址扩展;栈是由高地址向低地址扩展;

    堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存

    堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片

    堆的分配效率较低,而栈的分配效率较高

    栈的大小通常较小,适合处理小规模数据和局部变量。堆的大小通常较大,由操作系统动态管理,适合存储大量数据和对象。

  2. 栈效率高的原因:

    栈是操作系统提供的数据结构,计算机底层分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行,每次栈帧的推入和弹出都是O(1)时间复杂度操作。;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。

3.堆的数据结构以及小根堆和大根堆
  1. 堆(Heap) 是一种特殊的完全二叉树数据结构,主要用于实现优先队列(默认大根堆)从小到大排序。

    堆是一种完全二叉树,意味着除了最后一层,其他层都是满的,如果一共有h层,那么1~h-1层均满,在h层可能会连续缺失若干个右叶子,且最后一层的节点从左到右排列,不能中途缺失。

  2. **小根堆:**任何节点的值都不大于其子节点的值。即,堆顶(根节点)是堆中最小的元素。

  3. **大根堆:**任何节点的值都不小于其子节点的值。即,堆顶(根节点)是堆中最大的元素。

数组与链表

1.请你回答一下Array&List, 数组和链表的区别
  1. **数组:**元素在内存中连续存放,下标访问元素,查找效率高,插入删除需要移动后面元素导致效率很低,在栈上分配内存的静态数组,一般需要提前申请内存大小,可能浪费空间,不利于扩展,空间不够时需要重新定义数组,或者使用动态数组,在堆上分配空间,可以动态调整大小
  2. **链表:**内存不连续,通过存储下一个元素的地址指针来访问,插入删除效率高,查找效率低,不用指定大小扩展方便。
  3. 对于动态数组作为局部变量时候,如vector对象本身实在栈上分配,但是存储的元素实在堆上分配,函数结束后vector能自动释放在堆上分配的内存。

排序

1.介绍各类排序算法以及时间复杂度

排序的稳定性要看排序中元素之间的相对位置是否改变。

冒泡排序:稳定的排序算法,平均时间复杂度O(n2),最优为O(n),空间复杂度O(1);对于n个元素的数组,首先遍历第一和第二个元素,若为逆序则交换两者位置;然后比较第二和第三个元素,双重循环重复n次,每次将当前n-i个元素中最大者放在n-i的位置上,n次遍历后完成排序。

选择排序:不稳定的排序算法,时间复杂度O(n2),空间复杂度O(1);每次循环,在无序数组中找到最小值,然后与无序数组中第一个值交换位置,然后在剩下的无序数组中重复该操作,n次遍历后完成排序。

插入排序:稳定的排序算法,平均时间复杂度O(n2),最优为O(n),空间复杂度O(1);首先认定第一个元素为已排序过了,然后对第二个元素开始在已排序数组中遍历一遍,插入到正确位置,然后重复操作。

希尔排序:不稳定的排序算法,平均时间复杂度为O(nlogn),最坏为O(n2),最优为O(n),空间复杂度为O(1);类似于插入排序,首先设定一个初始变量k,通常可以为数组长度的一半,然后将数组中间隔为k的元素划分为一组,对每组进行插入排序,然后k减少例如再减半,然后重复间隔为k的元素分一组进行插入排序,重复操作使数组基本有序,最后一次k一定为1,这时再做一次插入排序,使数组有序,多数情况每次插入都是微调。

归并排序:稳定的排序算法,时间复杂度为O(nlogn),空间复杂度为O(n);归并排序采用了分治的思想,通过空间换取时间,首先将数组划分为n/2个长度为2或者1的子序列,每个子序列自身进行基本排序,然后子序列两两进行归并,归并也类似双指针,两个指针均从起点开始比较,顺序排列,重复两两归并直到全部合并。

堆排序:不稳定的排序算法,时间复杂度为O(nlogn),空间复杂度为O(1);采用完全二叉树的结构实现,大顶堆指根结点以及子树的根结点均大于等于左右结点,小顶堆指根结点以及子树的根结点均小于等于左右结点。首先建立一个堆,然后交换堆顶元素与最后一个元素再重新调整堆结构,直到删除堆中全部元素。

快速排序:不稳定的排序算法,平均时间复杂度为O(nlogn),最坏为O(n2),最优为O(logn),空间复杂度为O(nlogn)。采用分治的思想,一般取出数组第一位作为基准数,然后双指针从左往右找一个比基准数大的,从右往左找一个比基准数小的,然后交换这两位,当双指针在某个元素位置相遇,将基准数与该元素交换,使基准数左边都比基准数小,右边都比他大;然后对左序列和右序列做同样的操作,直到数组升序排序成功。

哈希

1.解决哈希冲突的办法
  1. 当哈希表关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,这样的现象称为哈希冲突。
  2. 链地址法:将所有哈希值相同的Key通过链表存储。key按顺序插入到链表中
  3. 再哈希法:当发生哈希冲突时使用另一个哈希函数计算地址值,直到冲突不再发生。这种方法不易产生聚集,但是增加计算时间,同时需要准备许多哈希函数。
2.哈希表的实现
  1. hash表的实现主要包括构造哈希和处理哈希冲突两个方面:

    对于构造哈希来说,主要包括直接地址法、平方取中法、除留余数

    对于处理哈希冲突来说,最常用的处理冲突的方法有开放定址法、再哈希法、链地址法、建立公共溢出区等方法。

算法

1.深度优先遍历和广度优先遍历

​ 深度优先遍历(DFS)和广度优先遍历(BFS)是图和树数据结构中常用的遍历算法。

  1. **深度优先遍历(DFS):**从起始节点开始,沿着一条路径深入图的每一个节点,直到不能再深入为止。然后回溯到上一个节点,继续沿另一条路径进行遍历。

    使用递归:递归方式实现较为简单,利用函数调用栈来保存状态。

    适合需要遍历所有路径的场景,例如解决连通性问题、寻找路径等。

  2. **广度优先遍历(BFS):**从起始节点开始,首先访问该节点,然后访问所有直接邻居节点,接着访问这些邻居节点的邻居,依此类推,逐层访问图中的所有节点。

    使用队列:逐层访问,显式地使用队列来管理待访问的节点。

    适合寻找最短路径、树层次遍历等场景,例如解决最短路径问题、图的层次遍历等。

  3. 复杂度都是:

    时间复杂度:O(V + E),其中 V 是图中的节点数,E 是边数。

    空间复杂度:O(V),主要取决于队列的大小

设计模式

创建型模式(Creational Patterns)

目的:处理对象创建的复杂性,提供对象创建的方式,简化对象创建过程。

结构型模式(Structural Patterns)

目的:处理类和对象的组合,以便它们可以更好地协同工作。

行为型模式(Behavioral Patterns)

目的:处理对象之间的交互和责任分配,定义了如何分配责任,如何在对象间进行交互。

1.单例模式

属于创建型模式,该类不能被复制,一个类中一般只有一个实例对象,并提供唯一一个全局访问接口,该实例被所有程序模块共享,一般可以用于写日志时创建唯一一个日志实例。该类的构造函数无法被公开调用(私有成员),实例对象一般被定义为静态类成员。

主要用于节省资源提高效率,因为无需频繁创建对象,只需要创建一次对象即可。

这个类不可被复制;这个类不可被公开创造。

一般的实现比较简单,但遇到多线程开发时对于单例模式就需要考虑线程安全的问题,一般可通过在单例类中进行加解锁来提高线程安全。

实现方式一般有懒汉模式和饿汉模式

懒汉模式:

实现方式1:静态指针+第一次使用时初始化;

第一次用到类实例时才会实例化,也是最简单的实现。遇到多线程时为了提高线程安全则可以通过加解锁方式,但是频繁加解锁可能带来性能损耗,通常可以双重判断实例是否为空,不为空再加锁再判断是否为空是否创建,锁开销较大。

实现方式2:局部静态变量;

也可以使用把类实例创建为静态局部变量返回,C++11 引入了对静态局部变量的线程安全初始化的保证标准,如果多个线程同时试图初始化一个静态局部变量,那么只有一个线程会进行初始化,而其他线程会等待初始化完成。这就确保了静态局部变量的初始化是线程安全的。如果实在担心可以在主线程一开始就调用创建静态局部变量的方法。

饿汉模式:单例类定义的时候就进行实例化,没有多线程问题。但是可能影响程序启动效率,可能浪费内存和计算资源,尤其是在实例可能永远不会被使用的情况下。

懒汉模式适合在对象创建开销较大且对象不一定会被使用的场景,但需要处理线程安全问题。

饿汉模式实现简单且线程安全,但可能会浪费资源,如果实例的创建开销较大且不一定会被使用,可能不是最佳选择。

单例模式

控制实例数目:确保一个类只有一个实例,避免了多次创建对象带来的资源浪费和系统开销。节省资源:避免了频繁创建和销毁对象,适合资源开销较大的对象(如数据库连接、文件操作等)。依赖性强:全局访问点可能导致代码之间的隐式依赖,增加耦合度,使得代码难以维护和扩展。

并发问题:在多线程环境中,可能需要额外的同步机制来保证线程安全。如果处理不当,可能导致性能问题或不一致的状态。

2.工厂模式

创建型模式,定义一个创建对象的接口,让子类决定实例化哪个类,对象的创建统一让工程类去管理。一般工厂方法返回一个基类(或接口)指针,实际的对象是由具体工厂类创建的子类对象,有比较好的封装性。

简单工厂模式:实现比较简单,一般是在工程类中做判断,然后创建相应对象。当需要增加新的产品对象时,就需要修改工程类。优点是实现简单,能降低系统耦合性。缺点是违背了开放封闭原则:软件实体可以拓展,但是不可修改,因为需要增加新产品时就需要修改工程类。

使用场景:当创建对象的逻辑复杂或需要根据不同条件创建不同的对象时。

适用项目:创建对象种类繁多或对象创建过程复杂的系统,如图形界面中的按钮、文本框等组件。

例子:在一个跨平台应用中,工厂模式可以用于根据操作系统的不同,创建适配不同系统的UI组件。

#include <iostream>

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Rectangle" << std::endl;
    }
};

class ShapeFactory {
public:
	//通过一个静态方法根据传入的参数决定创建哪种具体产品类的实例。
    static Shape* createShape(const std::string& type) {
        if (type == "circle") {
            return new Circle();
        } else if (type == "rectangle") {
            return new Rectangle();
        }
        return nullptr;
    }
};

工厂方法模式:定义一个用于创建对象的接口,一般在工厂父类中创建一个创建对象的纯虚函数,然后让子类来决定实例化哪个类。优点是拓展性好,符合开放封闭原则,新增加一个新产品对象时,只需要增加产品类和工程子类即可。缺点是每增加一个新产品就需要增加一个产品类和工厂子类,实现时需要更多的类定义。

#include <iostream>

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Rectangle" << std::endl;
    }
};

class ShapeFactory {
public:
    virtual Shape* createShape() = 0;
    virtual ~ShapeFactory() {}
};

class CircleFactory : public ShapeFactory {
public:
    Shape* createShape() override {
        return new Circle();
    }
};

class RectangleFactory : public ShapeFactory {
public:
    Shape* createShape() override {
        return new Rectangle();
    }
};

int main() {
    ShapeFactory* factory = new CircleFactory();
    Shape* shape = factory->createShape();
    shape->draw();
    delete shape;
    delete factory;

    factory = new RectangleFactory();
    shape = factory->createShape();
    shape->draw();
    delete shape;
    delete factory;

    return 0;
}

抽象工厂模式:与工厂方法模式主要区别在于工厂方法模式只有一个抽象产品类来派生多个产品类,而抽象工厂模式有多个抽象产品类来派生多个产品类,例如原来只有一个生产水杯的抽象类,现在抽象工厂模式新增了一个生产水壶的抽象类,工厂方法模式中具体工厂类只能创建一种产品对象,而抽象工厂模式可以创建多种,例如生产水杯同时也能生产水壶。

提供一个创建一系列相关或互相依赖对象的接口,而无需指定具体类。

一个工厂可以创建多个相关的产品。

//程序实例(抽象工厂模式)
//单核    
class SingleCore     
{    
public:    
    virtual void Show() = 0;  
};    
class SingleCoreA: public SingleCore      
{    
public:    
    void Show() { cout<<"Single Core A"<<endl; }    
};    
class SingleCoreB :public SingleCore    
{    
public:    
    void Show() { cout<<"Single Core B"<<endl; }    
};    
//多核    
class MultiCore      
{    
public:    
    virtual void Show() = 0;  
};    
class MultiCoreA : public MultiCore      
{    
public:    
    void Show() { cout<<"Multi Core A"<<endl; }    
    
};    
class MultiCoreB : public MultiCore      
{    
public:    
    void Show() { cout<<"Multi Core B"<<endl; }    
};    
//工厂    
class CoreFactory      
{    
public:    
    virtual SingleCore* CreateSingleCore() = 0;  
    virtual MultiCore* CreateMultiCore() = 0;  
};    
//工厂A,专门用来生产A型号的处理器    
class FactoryA :public CoreFactory    
{    
public:    
    SingleCore* CreateSingleCore() { return new SingleCoreA(); }    
    MultiCore* CreateMultiCore() { return new MultiCoreA(); }    
};    
//工厂B,专门用来生产B型号的处理器    
class FactoryB : public CoreFactory    
{    
public:    
    SingleCore* CreateSingleCore() { return new SingleCoreB(); }    
    MultiCore* CreateMultiCore() { return new MultiCoreB(); }    
};   

优点: 工厂抽象类创建了多个类型的产品,当有需求时,可以创建相关产品子类和子工厂类来获取。

缺点: 扩展新种类产品时困难。抽象工厂模式需要我们在工厂抽象类中提前确定了可能需要的产品种类,以满足不同型号的多种产品的需求。但是如果我们需要的产品种类并没有在工厂抽象类中提前确定,那我们就需要去修改工厂抽象类了,而一旦修改了工厂抽象类,那么所有的工厂子类也需要修改,这样显然扩展不方便。

3.装饰器模式

装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许在不修改现有类的情况下,动态地向对象添加职责(功能)。装饰器模式通过创建一个装饰器类来包装原始类,从而提供新的行为或功能。

#include <iostream>
#include <string>

// 组件接口
class Component {
public:
    virtual ~Component() {}
    virtual std::string operation() const = 0;
};

// 具体组件类
class ConcreteComponent : public Component {
public:
    std::string operation() const override {
        return "ConcreteComponent";
    }
};

// 装饰器基类
class Decorator : public Component {
protected:
    Component* component;
public:
    Decorator(Component* comp) : component(comp) {}
    std::string operation() const override {
        return component->operation();
    }
};

// 具体装饰器类A
class ConcreteDecoratorA : public Decorator {
public:
    ConcreteDecoratorA(Component* comp) : Decorator(comp) {}
    std::string operation() const override {
        return "ConcreteDecoratorA(" + Decorator::operation() + ")";
    }
};

// 具体装饰器类B
class ConcreteDecoratorB : public Decorator {
public:
    ConcreteDecoratorB(Component* comp) : Decorator(comp) {}
    std::string operation() const override {
        return "ConcreteDecoratorB(" + Decorator::operation() + ")";
    }
};

int main() {
	// 一个子类组件的对象
    Component* simple = new ConcreteComponent();
    std::cout << "Client: I've got a simple component: " << simple->operation() << std::endl;
	
	// 可以将不同实现的子类传入装饰器类,在该基础上装饰增加功能
    Component* decorator1 = new ConcreteDecoratorA(simple);
    std::cout << "Client: Now I've got a decorated component: " << decorator1->operation() << std::endl;
	
	// 可以将不同装饰器对象传入新的装饰器中,这样可以组合不同装饰器的功能
    Component* decorator2 = new ConcreteDecoratorB(decorator1);
    std::cout << "Client: Now I've got a doubly decorated component: " << decorator2->operation() << std::endl;

    delete simple;
    delete decorator1;
    delete decorator2;

    return 0;
}

体现装饰器模式的关键点

  1. 装饰器基类(Decorator)
    • 继承与基类组件,并实现了组件接口,并持有一个组件对象的引用,这个引用是个组件基类指针,这样可以接收组件子类对象的指针,并能够实现子类组件的功能,在不同子类实现的基础上增加装饰功能,比较灵活。
    • 其主要作用是为具体装饰器类提供一个与组件接口相同的接口,同时通过组合持有被装饰的组件对象。
  2. 具体装饰器类(ConcreteDecoratorA 和 ConcreteDecoratorB)
    • 继承自装饰器基类,并在其基础上增加新的功能。
    • 调用装饰器基类的操作,并在此基础上扩展或修改其行为。

应用场景

  1. 功能扩展:需要为类添加功能,但不希望通过继承来实现,避免类爆炸。
  2. 动态行为:需要在运行时动态地为对象添加功能,可以随时启用或禁用这些功能。
  3. 替代继承:避免子类继承的局限性和复杂性,通过组合实现功能扩展。
  4. 职责分离:将多个职责分散到不同的类中,而不是在一个类中实现所有功能。

相比类继承的方式,装饰器模式的优点

通过继承增加功能的局限性

  1. 类爆炸问题
    • 当需要为基类添加多个功能时,每个功能都需要一个子类,如果多个功能需要组合,则需要每种组合都创建一个新的子类。这会导致类的数量急剧增加,管理和维护变得困难。
    • 例如,假设有一个基类 Component,有两个功能 FeatureAFeatureB,如果使用继承,则需要创建 ComponentWithFeatureAComponentWithFeatureBComponentWithFeatureAAndB 等多个子类。
  2. 灵活性差
    • 继承在编译时就决定了类的功能,无法在运行时动态地添加或删除功能。而装饰器模式可以在运行时动态地组合和更改对象的行为。
    • 继承增加功能时,如果需要动态地添加或删除某个功能,只能通过创建新的子类或修改现有子类来实现,不够灵活。
  3. 违反单一职责原则
    • 通过继承增加功能的类可能会承担过多的职责,违反了单一职责原则,难以维护和扩展。
    • 例如,一个子类可能同时实现了多个功能,导致类的职责变得不单一。

缺点

  1. 增加了代码量:每个装饰器类都需要持有一个基类引用,增加了一些代码量。
  2. 内存开销:由于装饰器模式需要持有基类引用,每层装饰器都会增加一些内存开销。

优点:

组合灵活:可以在运行时动态地组合对象的行为,根据需要随时添加或删除功能,而继承方式编写类的功能都是编译时期决定,比如两个装饰器功能的组合,继承方式必须实现这个类,这个功能是静态实现的,而装饰器模式可以在运行时候动态增加组合这个对象来实现,可扩展性强。

遵循开闭原则:可以通过添加新的装饰器类来扩展对象的功能,而不需要修改现有代码。

职责单一:每个装饰器类只负责一个功能的添加,职责单一,代码更加易于理解和维护。

使用场景:当需要在不改变现有类的情况下动态地添加或更改对象的行为时。

适用项目:需要对功能进行组合和扩展的场景,如输入/输出流处理、图形界面中的组件装饰。

例子:在一个文本编辑器中,通过装饰器模式为文本框添加滚动条、边框、阴影等装饰功能。

4.观察者模式

观察者模式是一种行为设计模式,它定义了对象间的一对多依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都会收到通知并自动更新。

关键角色

  1. Subject(主题):持有对观察者对象的引用,可以增加、删除和通知观察者对象。
  2. Observer(观察者):定义一个更新接口,用于接收主题的更新通知。
  3. ConcreteSubject(具体主题):实现主题接口,维护具体的观察者列表,并在状态改变时通知所有观察者。
  4. ConcreteObserver(具体观察者):实现观察者接口,在接收到更新通知时进行相应的处理。

优点

  1. 解耦:观察者模式将观察者和主题解耦,使得它们可以独立变化。(也即观察者和主题的类都可以独立修改自己的功能不需要变更另一方)
  2. 灵活性:可以在运行时动态增加和删除观察者。(即不需要的观察者可以随时删除,而不需要修改类的功能)
  3. 一致性:保证所有观察者在主题状态改变时得到通知并更新。

缺点

  1. 复杂性增加:由于存在多个观察者和主题对象,管理它们之间的关系可能会变得复杂。
  2. 性能开销:如果观察者很多,通知的过程可能会带来性能开销。(可以通过,异步线程通知、观察者分配优先级如观察者列表引入优先队列来通知解决)
  3. **循环依赖:**如果观察者和被观察者之间存在循环依赖关系,可能会导致无限循环的通知,从而引发系统崩溃。在某些复杂的系统中,可能存在多个观察者和被观察者相互依赖的情况。例如,观察者 A 更新后通知被观察者 B,而被观察者 B 更新后又通知观察者 A,可能会导致无限循环。

适用项目:事件驱动的系统、发布-订阅系统,如用户界面事件处理、通知系统。

例子:在一个社交网络应用中,用户发布动态时,所有关注他的用户都会收到通知。

//观察者
class Observer  
{
public:
    Observer() {}
    virtual ~Observer() {}
    virtual void Update() {} 
};
//博客主题
class Blog  
{
public:
    Blog() {}
    virtual ~Blog() {}
    void Attach(Observer *observer) { m_observers.push_back(observer); }     //添加观察者
    void Remove(Observer *observer) { m_observers.remove(observer); }        //移除观察者
    void Notify() //通知观察者
    {
        list<Observer*>::iterator iter = m_observers.begin();
        for(; iter != m_observers.end(); iter++)
            (*iter)->Update();
    }
    virtual void SetStatus(string s) { m_status = s; } //设置状态
    virtual string GetStatus() { return m_status; }    //获得状态
private:
    list<Observer* > m_observers; //观察者链表
protected:
    string m_status; //状态
};
//具体博客类
class Blog优快云 : public Blog
{
private:
    string m_name; //博主名称
public:
    Blog优快云(string name): m_name(name) {}
    ~Blog优快云() {}
    void SetStatus(string s) { m_status = "优快云通知 : " + m_name + s; } //具体设置状态信息
    string GetStatus() { return m_status; }
};
//具体观察者
class ObserverBlog : public Observer   
{
private:
    string m_name;  //观察者名称
    Blog *m_blog;   //观察的博客,当然以链表形式更好,就可以观察多个博客
public: 
    ObserverBlog(string name,Blog *blog): m_name(name), m_blog(blog) {}
    ~ObserverBlog() {}
    void Update()  //获得更新状态
    { 
        string status = m_blog->GetStatus();
        cout<<m_name<<"-------"<<status<<endl;
    }
};
//测试案例
int main()
{
    Blog *blog = new Blog优快云("wuzhekai1985");
    Observer *observer1 = new ObserverBlog("tutupig", blog);
    blog->Attach(observer1);  //添加观察者,运行时可以随时添加删除观察者
    blog->SetStatus("发表设计模式C++实现(15)——观察者模式");
    blog->Notify();		//主题状态改变通知观察者
    delete blog; delete observer1;
    return 0;
}

5.策略模式

**策略模式(Strategy Pattern)**是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使得它们可以互相替换。策略模式使得算法可以在不影响客户端的情况下发生变化,也就是说,客户端可以动态地选择不同的算法来完成相同的任务。

策略模式的组成部分

  1. 策略接口(Strategy Interface):这是一个抽象接口,定义了所有支持的算法的通用方法。
  2. 具体策略类(Concrete Strategy Classes):每个具体策略类实现了策略接口,并提供了特定的算法实现。
  3. 上下文类(Context Class):上下文类持有一个策略对象的引用,并且可以在运行时根据需要替换策略对象。上下文类通过策略接口调用算法,而不需要了解具体的实现细节。

使用场景:当有多种算法可以使用,并且需要在运行时动态地选择其中一种时。

适用项目:需要在不同情况下使用不同算法或行为的系统,如支付系统中的不同支付方式选择、游戏中的不同AI策略。

例子:在一个电子商务网站中,用户可以选择不同的支付方式(如信用卡、PayPal),策略模式可以让系统根据用户的选择调用不同的支付处理逻辑。

#include <iostream>
#include <memory>

// 策略接口,定义支付方法
class PaymentStrategy {
public:
    virtual void pay(int amount) = 0;
    virtual ~PaymentStrategy() = default;
};

// 具体策略类:信用卡支付
class CreditCardPayment : public PaymentStrategy {
public:
    void pay(int amount) override {
        std::cout << "Paying " << amount << " using Credit Card." << std::endl;
    }
};

// 具体策略类:PayPal支付
class PayPalPayment : public PaymentStrategy {
public:
    void pay(int amount) override {
        std::cout << "Paying " << amount << " using PayPal." << std::endl;
    }
};

// 具体策略类:比特币支付
class BitcoinPayment : public PaymentStrategy {
public:
    void pay(int amount) override {
        std::cout << "Paying " << amount << " using Bitcoin." << std::endl;
    }
};

// 上下文类:持有一个策略对象的引用
class PaymentContext {
private:
    std::unique_ptr<PaymentStrategy> strategy;
public:
    // 通过构造函数或 setter 方法设置策略
    void setStrategy(std::unique_ptr<PaymentStrategy> newStrategy) {
        strategy = std::move(newStrategy);
    }

    void pay(int amount) {
        if (strategy) {
            strategy->pay(amount);
        } else {
            std::cout << "Payment strategy is not set." << std::endl;
        }
    }
};

// 客户端代码示例
int main() {
    PaymentContext context;

    context.setStrategy(std::make_unique<CreditCardPayment>());
    context.pay(100);

    context.setStrategy(std::make_unique<PayPalPayment>());
    context.pay(200);

    context.setStrategy(std::make_unique<BitcoinPayment>());
    context.pay(300);

    return 0;
}

策略模式的优点

  1. 算法的可扩展性:可以轻松地添加新策略,而不影响现有代码。
  2. 避免条件判断:通过策略类替代复杂的条件语句,简化代码结构。
  3. 提高代码的可维护性:将算法和业务逻辑分离,增强代码的清晰度和灵活性。

策略模式的缺点

  1. 增加类的数量:每个策略都需要一个独立的类,可能导致类数量增加。
  2. 客户端需要了解策略:客户端必须知道不同的策略,才能选择合适的算法。
  3. 策略间不能共享状态:策略类是独立的,无法直接共享数据或状态。

策略模式就是在上下文类中持有一个指向策略基类的指针,通过传入不同策略子类的指针,在运行时通过多态机制来执行不同的算法或行为。

6.命令模式

**命令模式(Command Pattern)**是一种行为设计模式,它将请求或操作封装为一个对象,

命令模式的组成部分

  1. 命令接口(Command Interface):声明执行命令的方法,通常是 execute()
  2. 具体命令类(Concrete Command Classes):实现命令接口,封装具体的操作,通常包含对接收者(Receiver)对象的引用。
  3. 接收者(Receiver):实际执行命令的类,命令对象会调用接收者的相关方法。
  4. 调用者(Invoker):持有命令对象并在需要时调用命令的 execute() 方法。
  5. 客户端(Client):创建命令对象并设置其接收者

使用场景:当需要将操作封装为对象,以便进行参数化、队列执行、记录日志或支持撤销/重做时。

适用项目:需要执行一系列操作、支持撤销/重做、操作记录的系统,如编辑器中的命令历史、任务队列处理系统。

例子:在一个图像编辑器中,用户可以执行多步操作,命令模式可以帮助记录这些操作,以便用户撤销或重做。

#include <iostream>
#include <memory>

// 命令接口
class Command {
public:
    virtual void execute() = 0;
    virtual ~Command() = default;
};

// 接收者类:灯
class Light {
public:
    void on() {
        std::cout << "Light is ON" << std::endl;
    }

    void off() {
        std::cout << "Light is OFF" << std::endl;
    }
};

// 具体命令类:打开灯
class LightOnCommand : public Command {
private:
    Light* light;
public:
    LightOnCommand(Light* light) : light(light) {}

    void execute() override {
        light->on();
    }
};

// 具体命令类:关闭灯
class LightOffCommand : public Command {
private:
    Light* light;
public:
    LightOffCommand(Light* light) : light(light) {}

    void execute() override {
        light->off();
    }
};

// 调用者类:遥控器
class RemoteControl {
private:
    std::unique_ptr<Command> command;
public:
    void setCommand(std::unique_ptr<Command> newCommand) {
        command = std::move(newCommand);
    }

    void pressButton() {
        if (command) {
            command->execute();
        }
    }
};

// 客户端代码
int main() {
    Light light;

    RemoteControl remote;

    remote.setCommand(std::make_unique<LightOnCommand>(&light));
    remote.pressButton();

    remote.setCommand(std::make_unique<LightOffCommand>(&light));
    remote.pressButton();

    return 0;
}

说白了就是在调用接口中持有命令基类指针,然后在调用对象里传入不同的命令的子类指针,在运行时通过多态来执行不同的命令

优点

  1. 解耦:调用者不需要知道具体操作的细节,只需调用命令。
  2. 可扩展:新增操作很方便,只需添加新的命令类。
  3. 支持撤销/重做:轻松实现操作的撤销和重做。
  4. 组合命令:可以把多个命令组合在一起执行。在多线程环境中,可以将命令放入队列,由专门的线程来依次处理。

缺点

  1. 类数量增加:每个操作都需要一个命令类,可能导致类的数量增加。
  2. 复杂性:对简单操作来说,可能显得过度设计。

QT使用基础

1.QT信号槽

在C++的Qt框架中,信号与槽(Signals and Slots)是实现对象间通信的核心机制之一。它们提供了一种解耦的方式,允许一个对象通知另一个对象某些事件发生,而无需直接依赖彼此。

信号与槽QObject提供了信号与槽机制,用于对象间的通信。

信号与槽的特点

  1. 类型安全:Qt会在编译时检查信号和槽的参数是否匹配,确保调用时不会发生类型错误。
  2. 松耦合:信号与槽机制允许对象之间松散耦合,信号的发出者无需知道接收者是谁。
  3. 异步调用:在多线程环境中,信号与槽可以实现异步调用,信号会在线程边界上排队,槽在目标线程中执行。
  • 工作原理

    1. 信号发出:当一个线程中的对象发出信号时,Qt会检查连接到该信号的槽函数所在的线程。
    2. 事件队列:如果槽函数与信号发出者在同一线程中,槽函数将立即被执行。如果它们位于不同线程,Qt会将槽函数的调用请求放入目标线程的事件队列中。
    3. 槽函数执行:目标线程(即槽函数所在的线程)会从事件队列中取出任务并执行槽函数。由于任务是在目标线程中执行的,因此不会产生线程不安全问题。

    优点

    • 线程安全:通过这种方式,避免了线程之间直接调用导致的竞争条件和数据不一致问题,因为槽函数总是在与它关联的对象所在的线程中执行。
    • 异步处理:信号发出线程可以继续执行而不会被阻塞,接收线程会在合适的时间处理信号,大大提高了程序的响应速度和并发性。
    信号槽连接Connect接口的第五个参数
    connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::AutoConnection);
    
    1. AutoConnection
    • 自动连接。这是默认值,如果你不指定第五个参数,Qt会自动选择合适的连接方式。
    • 如果信号和槽在同一个线程中,则使用 直接连接Qt::DirectConnection)。
    • 如果信号和槽在不同的线程中,则使用 队列连接Qt::QueuedConnection)。
    2. DirectConnection

    直接连接。信号发出后,槽函数立即在信号发出线程中被调用。这种连接方式适合信号和槽都在同一线程中执行的场景。

    使用这种方式可能导致槽函数在与其对象不属于同一线程的情况下被调用,可能引发线程安全问题。

    Qt::DirectConnection 方式下,槽函数会在发出信号的线程中同步执行。这意味着当信号被发出时,信号的发出者会立即调用槽函数,并在槽函数执行完成之前不会继续执行后续的代码。因此,在这种连接方式下,槽函数的执行确实会阻塞信号发出者的后续操作。

    使用 Qt::QueuedConnection:如果希望避免阻塞,特别是发出信号的线程是主线程时,使用 Qt::QueuedConnection 可以将槽函数的执行推迟到事件循环中,从而避免阻塞发出信号的线程。

    3. QueuedConnection

    队列连接。信号发出时,槽函数的调用被放入接收对象所在线程的事件队列中,槽函数会在该线程的事件循环中执行。

    这种方式通常用于跨线程通信,确保槽函数在与其对象所属的线程中执行,避免线程安全问题。

    如果是同一线程,也会放入该线程的事件队列中,不会同步执行

    4. BlockingQueuedConnection

    阻塞队列连接。与 Qt::QueuedConnection 类似,但信号发出后,发出信号的线程会阻塞,直到槽函数执行完成。

    仅当信号发出者和接收者处于不同的线程中时有效,并且使用这种连接方式时需要小心,因为可能会导致死锁。

    死锁通常发生在以下场景中:

    场景一:相互依赖的等待
    1. 线程A 发出信号,通过 Qt::BlockingQueuedConnection 连接到 线程B 的槽函数。
    2. 线程A 阻塞,等待 线程B 完成槽函数的执行。
    3. 线程B 在执行槽函数时,需要某种资源或操作,而这部分操作可能依赖 线程A 完成。
    4. 线程A 正在等待 线程B,但 线程B 也在等待 线程A,因此产生了相互依赖,导致死锁。

    避免相互依赖:在设计跨线程通信时,避免产生相互依赖的条件。确保槽函数不依赖发出信号线程中的资源或操作。

    时间限制:在一些特殊情况下,你可以给阻塞操作添加时间限制,使用 QTimer 或其他超时机制来防止长时间阻塞。

    5. UniqueConnection

    唯一连接。如果已经存在相同的信号与槽连接,则不会建立新的连接。这种方式用于避免重复连接。这个标志通常与其他连接类型组合使用(如果只使用 Qt::UniqueConnection,则默认连接类型是 Qt::AutoConnection。)。无论是在单线程还是多线程环境中,Qt::UniqueConnection 的作用都是确保信号与槽之间不会建立重复的连接。

    “相同的连接”是指连接中涉及的 信号发出信号的对象(Sender)、以及 接收信号的对象(Receiver) 都是完全相同的。(信号或者槽函数有重载就不算相同连接)

2.QT如何进行多线程编程

  1. 使用 QThread

    创建一个继承自 QThread 的类,并重写 run() 函数,将需要在新线程中执行的代码放在 run() 方法中。

  2. 使用 QObjectQThread

    另一种方法是将工作对象和线程分开,将 QObject 派生的工作类移到线程中,线程负责运行这个工作类的事件循环。

    即创建一个继承自 QObject 的工作类,创建一个 QThread 对象,并将工作对象移动到这个线程

    class Worker : public QObject {
        Q_OBJECT
    
    public slots:
        void doWork() {
            // 执行具体任务
        }
    };
    
    QThread* thread = new QThread;
    Worker* worker = new Worker;
    worker->moveToThread(thread);		//工作对象移动到该线程
    
    // 连接信号与槽
    QObject::connect(thread, &QThread::started, worker, &Worker::doWork);
    QObject::connect(worker, &Worker::finished, thread, &QThread::quit);
    QObject::connect(worker, &Worker::finished, worker, &Worker::deleteLater);
    QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
    
    // 启动线程
    thread->start();
    
    线程安全
    • 共享资源:在多线程环境中操作共享资源时,需要使用互斥锁(QMutex)或其他同步机制来避免数据竞争和不一致性。
    • 信号与槽:信号与槽机制在跨线程通信时是线程安全的,但你仍需确保槽函数中的操作是线程安全的。
    事件循环
    • 事件循环:确保线程中有事件循环(通过调用 QThread::exec()),否则线程将无法处理信号和槽。

    • QThread 子类中:重写 run() 方法,并在其中调用 exec(),以启动线程的事件循环。

      QObject 对象中:确保线程中有事件循环运行,通常通过将对象移到线程中并启动线程来实现。

      QObject 对象移到线程中:你不需要直接调用 exec()。线程事件循环由 QThreadrun() 方法中的 exec() 启动。

    线程结束
    • 清理:线程结束时应确保所有资源都已释放,并使用 QThread::quit()QThread::wait() 来正确关闭线程。

    在工作任务结束之后释放信号给线程执行quit,然后quit执行后线程会释放finished信号,通过这个信号触发工作任务对象释放资源

    quit() 方法会请求线程退出其事件循环。这个方法不会立即终止线程,而是将一个退出事件放入事件循环中,等待线程处理完当前事件后退出。

    wait() 方法会阻塞调用线程,直到目标线程完成其执行。这是一个同步操作,确保目标线程完全退出后才会继续执行调用 wait() 的线程。在希望主线程或其他线程等待子线程完成后再继续执行时,使用 wait() 方法。

    使用QObject和MoveToThread方法的多线程编程优点:

    集中管理:多个 QObject 对象可以共享同一个线程的事件循环,简化了线程管理。你不需要为每个对象创建独立的线程,减少了线程创建和销毁的开销。

    资源共享:由于多个工作对象在同一个线程中运行,它们可以共享线程内的资源,避免了跨线程资源访问的问题。

    灵活性:可以通过信号与槽机制方便地实现对象间的通信。你可以在同一个线程中处理多个对象之间的信号与槽连接,确保它们在同一个线程的上下文中执行。

3.Qt的事件处理机制

Qt的事件处理机制基于事件循环和事件队列:

  • 事件队列:Qt将发生的事件(如用户输入、定时器超时)存储在事件队列中。
  • 事件循环:通过QCoreApplication::exec()(主线程的事件循环调用接口)启动事件循环。事件循环不断从事件队列中取出事件,并将其分发到相应的对象进行处理。
  • 事件处理:对象的event()方法负责处理传递给它的事件。根据事件类型,event()方法会调用相应的处理函数(如QMouseEventQKeyEvent等)。

4.QWidget 和 QML 的区别

QWidget:适用于需要稳定、传统桌面应用程序的场景。

QML:是Qt Quick的声明式UI语言,适用于开发动态和响应式的用户界面。QML通过声明式语法简化UI设计,支持动画和高级视觉效果,适合于触摸屏设备和现代用户界面。适用于需要动态和高度交互式界面的现代应用程序,特别是移动设备和嵌入式设备。

QMLHTML 的声明式结构定义、CSS 的样式和布局管理、以及 JavaScript 的逻辑和交互功能综合在一起,提供了一个强大而灵活的 UI 构建工具。

5.自定义控件

创建自定义控件包括以下步骤:

选择合适的基类继承,例如 QWidget,或者继承现有的 Qt 控件(如 QPushButton),以便在其基础上进行扩展。

  1. 继承自QWidget
  2. 重写paintEvent()方法来实现自定义绘制

6.Qt内存管理与性能

Qt内存管理

  • 父子关系:Qt通过父子对象机制自动管理内存,当父对象被销毁时,所有子对象也会被销毁。
  • 智能指针:Qt提供了QSharedPointerQScopedPointer来管理对象的生命周期。
性能优化

问题:如何优化Qt应用的性能?比如如何避免不必要的绘制操作和事件处理?
回答

  • 避免不必要的绘制:使用QRegion来只更新需要重绘的区域,避免全局重绘。
  • 减少事件处理开销:避免在paintEvent中进行复杂的计算,使用缓存来提高性能。使用多线程,减少不必要的事件处理,优化事件处理函数,使用事件过滤器管理事件。

7.Qt国际化翻译

QTranslator:加载翻译文件(.qm 文件)并应用到应用程序。

tr() 函数:用于标记需要翻译的文本。

翻译文件以.ts结尾。

8.paintEvent和paint

paintEvent()

  • 用于处理控件的绘制。
  • 由 Qt 自动调用。
  • 接受 QPaintEvent 对象,提供了需要更新区域的信息。
  • 适合处理控件的重绘操作。

paint()

  • 基本的绘图接口,通常不直接使用。
  • 提供低级别的绘图功能。
  • 需要手动调用,适合用于自定义绘图设备。

在 Qt 开发中,paintEvent() 是绘图的标准方法,适用于大多数控件和应用程序的绘制需求。paint() 更适合于特定的绘图设备或底层绘图需求。如果你在处理控件的绘制,应该优先使用 paintEvent()

通常情况下,在 Qt 中你只需要重写 paintEvent(),而不是 paint()

9.QGraphView使用,配合谁使用

QGraphView 通常与 QGraphicsSceneQGraphicsItem 一起使用。它们共同组成了Qt的图形视图框架,可以方便地管理和显示复杂的二维图形对象。

QGraphicsSceneQGraphicsScene 是一个场景管理器,负责存储和管理所有的图形项(QGraphicsItem),如形状、文本、图片等。它会将这些项放置在一个坐标系中,允许这些项相互交互。

QGraphicsItemQGraphicsItem 是图形项的基类,你可以通过继承它来创建自定义图形项。这些项可以添加到 QGraphicsScene 中,并通过 QGraphicsView 来显示。

QGraphicsViewQGraphicsView 是用于显示 QGraphicsScene 的视图窗口。它允许你通过平移、缩放、旋转等方式查看和操作场景中的图形项。

10.QGraphView内的坐标关系

QGraphicsView 中的坐标关系涉及多个不同的坐标系,它们之间可以相互转换。这些坐标系主要包括:

场景坐标系 (Scene Coordinates):这是 QGraphicsScene 的坐标系,所有的 QGraphicsItem 对象都在这个坐标系中定义。场景坐标系是整个场景的全局坐标系,通常使用笛卡尔坐标,原点通常在左上角,坐标值随着向右和向下增加。

视图坐标系 (View Coordinates):这是 QGraphicsView 的坐标系。它描述的是视图窗口的显示区域。视图坐标系的原点在视图窗口的左上角,单位是像素。当你在 QGraphicsView 中平移、缩放或旋转视图时,视图坐标系的显示区域会变化,但不会影响场景坐标系。

项坐标系 (Item Coordinates):每个 QGraphicsItem 都有自己的局部坐标系,用来定义图形项的外形和位置。项的原点通常在其局部空间内,位置、旋转、缩放等属性相对于这个局部坐标系进行。

Qt 提供了几种方法来进行不同坐标系之间的转换:

  1. 场景到视图
    • mapFromScene():将场景坐标转换为视图坐标。
    • mapToScene():将视图坐标转换为场景坐标。
  2. 项到场景
    • mapFromItem():将某个 QGraphicsItem 的局部坐标转换为场景坐标。
    • mapToItem():将场景坐标转换为某个 QGraphicsItem 的局部坐标。
  3. 视图到项
    • mapFromView():将视图坐标转换为 QGraphicsItem 的局部坐标。
    • mapToView():将 QGraphicsItem 的局部坐标转换为视图坐标。

11.Qt样式表qss怎么设置只设置对象,不设置其属性

在 Qt 样式表(QSS)中,可以通过对象名称或者对象的类名来指定样式规则。如果你想仅仅针对某个对象设置样式,而不改变它的子控件或属性,你可以使用 QWidgetobjectName 属性来实现。这样可以确保只应用于特定的对象,而不影响其他对象或该对象的子属性。

方法 1:通过 objectName 指定对象

你可以使用对象的 objectName 来设置样式,而不改变其他对象的属性。假设你的对象名称是 myButton,你可以这样设置 QSS 样式:

QPushButton* button = new QPushButton(this);
button->setObjectName("myButton");

#myButton {
    background-color: red;
    color: white;
}
方法 2:使用类选择器

你还可以通过对象的类名来设置样式,仅针对某个类对象,而不影响其子控件。比如,只为 QPushButton 设置样式:

QPushButton {
    background-color: blue;
    color: yellow;
}

//阻止样式继承子控件
如果你不希望某个控件的样式影响其子控件,可以通过设置 QSS 的子类选择器,避免样式继承:
QPushButton {
    background-color: green;
    color: white;
}

QPushButton QPushButton {
    background-color: none; /* 避免子控件受影响 */
}

#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    // 创建一个 QPushButton 对象
    QPushButton *button1 = new QPushButton("Button 1");
    button1->setObjectName("myButton"); // 设置对象名称

    QPushButton *button2 = new QPushButton("Button 2"); // 没有设置对象名称

    // 通过 QSS 设置样式,只针对对象名称为 "myButton" 的按钮
    QString styleSheet = 
        "#myButton {"
        "    background-color: red;"
        "    color: white;"
        "    border-radius: 5px;"
        "}";

    // 应用样式表到整个应用程序或特定窗口
    app.setStyleSheet(styleSheet);

    // 显示按钮
    button1->show();
    button2->show();

    return app.exec();
}

在上述代码中,通过 button1->setObjectName("myButton");button1 设置了对象名称为 "myButton"。然后在 QSS 中使用 #myButton 来选择这个按钮,并为其设置样式,而 button2(没有设置对象名称的按钮)不会受到影响。

app.setStyleSheet(styleSheet); 将样式表应用到整个应用程序。在这个例子中,只有 button1(对象名称为 "myButton")的样式被修改,而其他按钮不会受到影响。

如果你有一个复杂的控件,比如带有子控件的 QWidget,并且你只想修改这个控件本身而不影响它的子控件,你可以明确指定样式只应用于父控件。例如:

QWidget *widget = new QWidget;
widget->setObjectName("myWidget");

QString styleSheet = 
    "#myWidget {"
    "    background-color: lightblue;"
    "}"
    "#myWidget QPushButton {"
    "    background-color: none;"  // 子控件不受影响
    "}";

app.setStyleSheet(styleSheet);

12.Qt的事件循环机制,界面点击之后会发生什么?

  1. 用户点击按钮 -> 生成点击事件。

    事件分发 -> 事件被添加到事件队列。

    事件循环 -> 从队列中取出事件,分发给按钮。

    事件处理 -> 按钮处理点击事件,调用槽函数。

    UI 更新 -> 执行槽函数,显示对话框。

13.Qt树是什么?

“树”通常指的是 QTreeWidgetQTreeView 这两个类,它们用于展示和管理层次化的数据结构。它们常被用于显示树状结构的文件系统、组织结构、菜单等。尽管它们都用于树形数据的展示和管理,但它们的实现和用途略有不同。

QTreeWidget 是一个方便的封装,适合简单的树形结构展示和操作,适合对模型的操作不复杂的情况。

QTreeView 提供了更大的灵活性和扩展性,适合需要自定义数据模型和复杂交互的场景。

14.Qt锁和C++锁的使用?

  1. C++中的锁

    std::mutex:标准的互斥锁,用于保护共享资源。

    std::recursive_mutex:递归锁,允许同一个线程多次锁定,但需要同样次数的解锁。

    std::unique_lock:提供了灵活的锁管理方式,比如延迟锁定、提前解锁等功能。

    std::lock_guard:简单易用的RAII风格锁,作用域结束时自动释放锁。异常安全:在出现异常或提前退出时,RAII对象依然会在离开作用域时调用析构函数,确保资源被正确释放,避免死锁。

    #include <iostream>
    #include <thread>
    #include <mutex>
    
    std::mutex mtx;
    
    void print_data(int i) {
        std::lock_guard<std::mutex> lock(mtx);  // 使用RAII,自动管理锁,这样在函数结束,lock_guard的生命周期结束会自动解锁,这样就可以自动管理
        // 临界区:保护共享资源
        std::cout << "Thread " << i << " is working\n";
        //lock.unlock()   如果不想自动管理解锁,也可以提前进行手动解锁
    }
    
    int main() {
        std::thread t1(print_data, 1);
        std::thread t2(print_data, 2);
    
        t1.join();
        t2.join();
        return 0;
    }
    
    

    std::lock_guard对象lock被创建时,mtx.lock()会被调用,锁住互斥量。

    print_data函数结束或作用域结束时,std::lock_guard对象lock被销毁,mtx.unlock()会自动被调用,释放锁。

    std::mutex mtx;
    //也可以自己进行手动加解锁,会增加代码复杂性
    void print_data(int i) {
        mtx.lock();  // 手动加锁
        std::cout << "Thread " << i << " is working\n";
        mtx.unlock();  // 手动解锁
    }
    
  2. Qt中的锁

    QMutex:Qt中的互斥锁,类似于C++的std::mutex

    QReadWriteLock:读写锁,允许多个读操作并发进行,但写操作是独占的。

    QMutexLocker:类似于C++的std::lock_guard,用于自动管理锁的生命周期。

    #include <QThread>
    #include <QMutex>
    #include <QDebug>
    
    QMutex mutex;
    
    class Worker : public QThread {
    public:
        Worker(int id) : id(id) {}
    
        void run() override {
            QMutexLocker locker(&mutex);  // 自动管理锁的生命周期
            qDebug() << "Thread" << id << "is working";
            shared_data.push_back(value);
            //函数结束,locker生命周期到达自动解锁
            //locker.unlock();  // 手动解锁,如果需要提前解锁的话
        }
    
    private:
        int id;
    };
    
    int main() {
        Worker thread1(1);
        Worker thread2(2);
    
        thread1.start();
        thread2.start();
    
        thread1.wait();
        thread2.wait();
    
        return 0;
    }
    
    

    QMutexLocker对象locker在构造时会自动加锁,调用mutex.lock()

    run()函数结束或作用域结束时,QMutexLocker会自动解锁,调用mutex.unlock()

    QMutex mutex;
    std::vector<int> shared_data;
    //不想使用自动加解锁机制,也可以自己手动加解锁
    class Worker : public QThread {
    public:
        Worker(int id) : id(id) {}
    
        void run() override {
            mutex.lock();  // 手动加锁
            qDebug() << "Thread" << id << "is working";
            mutex.unlock();  // 手动解锁
        }
    
    private:
        int id;
    };
    
    

    假设我们有一个共享资源shared_data,我们希望通过锁来保护对它的访问。我们可以将操作封装到一个专门的函数中。锁的管理将由函数内部负责,调用者不需要关注锁的细节:锁的范围明确:锁的作用范围限制在临界区内部,不会锁住不必要的代码,减少锁的开销。

    QT读写锁

    QReadWriteLock允许多个线程同时读取数据,但写入操作是独占的,与C++标准库中的std::shared_mutex类似。

    读锁(QReadWriteLock::lockForRead():允许多个线程同时获取,适用于读取数据的操作。多个线程可以同时持有读锁。

    写锁(QReadWriteLock::lockForWrite():独占锁,只有一个线程可以获取,适用于写入数据的操作。持有写锁时,其他读锁和写锁的请求会被阻塞。

    #include <QReadWriteLock>
    #include <QThread>
    #include <QDebug>
    
    // 共享资源
    int shared_data = 0;
    QReadWriteLock rw_lock;  // 读写锁
    
    void read_shared_data(int thread_id) {
        rw_lock.lockForRead();  // 获取读锁
        qDebug() << "Thread" << thread_id << "reads shared_data:" << shared_data;
        rw_lock.unlock();  // 释放读锁
    }
    
    void write_shared_data(int thread_id, int value) {
        rw_lock.lockForWrite();  // 获取写锁
        shared_data = value;
        qDebug() << "Thread" << thread_id << "writes shared_data:" << shared_data;
        rw_lock.unlock();  // 释放写锁
    }
    
    int main() {
        QList<QThread*> threads;
    
        // 创建一些读线程
        for (int i = 0; i < 5; ++i) {
            QThread* t = QThread::create(read_shared_data, i);
            threads.append(t);
            t->start();
        }
    
        // 创建一个写线程
        QThread* writer = QThread::create(write_shared_data, 5, 42);
        threads.append(writer);
        writer->start();
    
        // 再创建一些读线程
        for (int i = 6; i < 10; ++i) {
            QThread* t = QThread::create(read_shared_data, i);
            threads.append(t);
            t->start();
        }
    
        // 等待所有线程完成
        for (QThread* t : threads) {
            t->wait();
            delete t;
        }
    
        return 0;
    }
    
    

    多个线程可以同时获取读锁并访问共享资源。这就是读写锁的主要优点之一:当只有读操作时,多个线程可以并发执行,而不会互相阻塞。只有当有写操作时,写线程才会阻塞其他读线程和写线程。

    自动管理锁:QReadLockerQWriteLocker

    Qt提供了QReadLockerQWriteLocker来自动管理锁,类似于QMutexLocker,它们通过RAII模式管理锁的获取和释放,减少手动调用lock()unlock()的可能出错情况。

    #include <QReadWriteLock>
    #include <QReadLocker>
    #include <QThread>
    #include <QDebug>
    
    int shared_data = 0;
    QReadWriteLock rw_lock;
    
    void read_shared_data(int thread_id) {
        QReadLocker locker(&rw_lock);  // 自动获取读锁
        qDebug() << "Thread" << thread_id << "reads shared_data:" << shared_data;
        // 离开作用域时,自动解锁
        //locker.unlock();  // 提前解锁
    }
    
    
    #include <QReadWriteLock>
    #include <QWriteLocker>
    #include <QThread>
    #include <QDebug>
    
    int shared_data = 0;
    QReadWriteLock rw_lock;
    
    void write_shared_data(int thread_id, int value) {
        QWriteLocker locker(&rw_lock);  // 自动获取写锁
        shared_data = value;
        qDebug() << "Thread" << thread_id << "writes shared_data:" << shared_data;
        //locker.unlock();  // 提前解锁
        // 离开作用域时,自动解锁
    }
    
    

    QReadLockerQWriteLocker:通过RAII风格自动管理锁的生命周期,减少手动解锁的风险。

    QMutex 互斥锁

    • 独占性QMutex 是一个独占锁。当一个线程获得了 QMutex,其他线程必须等待,直到这个线程释放锁。
    • 读写操作:无论是读操作还是写操作,持有 QMutex 的线程都会阻塞其他线程对该资源的访问。没有区分读和写,所有操作都是互斥的。

    写多读少的场景下,QReadWriteLockQMutex 的性能差异不大,因为写操作会使得读锁机制失去作用。使用 QReadWriteLock 的复杂性和开销可能不必要,即使在写多的场景中,它的读锁机制仍然存在,可能会增加代码的复杂性。而使用 QMutex 更简单直接。

    join和wait

    std::thread::join()QThread::wait() 都用于等待线程完成,它们在不同的使用场景下提供线程同步功能。

    std::thread::join()QThread::wait() 的确是阻塞调用它们的线程,直到目标线程完成。

    #include <iostream>
    #include <thread>
    
    void threadFunction() {
        std::cout << "Thread is running." << std::endl;
    }
    
    int main() {
        std::thread t(threadFunction);  // 创建并启动一个新线程
        t.join();  // 阻塞主线程,直到线程 t 完成
        std::cout << "Thread has finished." << std::endl;
        return 0;
    }
    
    
    #include <QThread>
    #include <QDebug>
    
    class MyThread : public QThread {
        void run() override {
            qDebug() << "Thread is running.";
        }
    };
    
    int main() {
        MyThread thread;
        thread.start();
        thread.wait();  // 阻塞主线程,直到线程 thread 完成
        qDebug() << "Thread has finished.";
        return 0;
    }
    
    

    调用处线程:阻塞的是调用这些方法的线程。如果在主线程中调用这些方法,主线程会被阻塞,直到目标线程完成。

    这种机制在需要确保所有线程的任务完成后再继续执行后续代码时非常有用,保证了多线程程序的同步和正确性。

    QThread::wait() 可以指定一个超时时间,适用于需要限制等待时间的场景。如果超时,wait() 返回 false

    如果超时发生(即目标线程没有在指定时间内完成),wait() 方法会返回 false。此时,调用线程会停止阻塞并继续执行。

    #include <QThread>
    #include <QDebug>
    
    class MyThread : public QThread {
    public:
        void run() override {
            QThread::sleep(10);  // 模拟长时间运行的任务
            qDebug() << "Thread is running.";
        }
    };
    
    int main() {
        MyThread thread;
        thread.start();
        if (thread.wait(5000)) {  // 等待最多 5 秒钟
            qDebug() << "Thread completed.";
        } else {
            qDebug() << "Thread timeout.";
        }
        return 0;
    }
    
    

    在 Qt 中,如果你使用信号和槽机制进行线程间通信,可以使用 QThread::wait() 来确保线程任务完成后再进行下一步操作。

    #include <QThread>
    #include <QDebug>
    #include <QObject>
    
    class Worker : public QObject {
        Q_OBJECT
    public slots:
        void doWork() {
            qDebug() << "Worker thread is doing work.";
        }
    };
    
    int main() {
        QThread workerThread;
        Worker worker;
        worker.moveToThread(&workerThread);
        QObject::connect(&workerThread, &QThread::started, &worker, &Worker::doWork);
        QObject::connect(&workerThread, &QThread::finished, []() {
            qDebug() << "Thread finished.";
        });
    
        workerThread.start();
        workerThread.wait();  // 等待线程完成
        return 0;
    }
    
    

    std::thread::join()

    • 适用场景:用于标准 C++ 项目中的线程管理。
    • 功能:阻塞当前线程直到目标线程完成。
    • 超时:不支持超时功能。

    QThread::wait()

    • 适用场景:用于 Qt 项目中的线程管理。
    • 功能:阻塞当前线程直到目标线程完成,可以指定超时时间。
    • 超时:支持超时功能(指定最大等待时间)。

    detach() 的作用

    • detach() 明确表示线程将独立运行,不会阻塞主线程。它将线程与 std::thread 对象解除绑定,确保线程在后台继续执行。

    如果你选择不显式地管理线程的生命周期(即不调用 join()detach()),主线程将不会等待或阻塞,线程会在后台独立运行。这种方法可能导致线程资源管理不当,如果主线程退出时,未完成的线程可能会被强制终止。后台线程可能会被终止,导致未定义行为或资源泄漏。

15.Qt怎么不让信号发送

  1. 动态地修改信号和槽连接
    #include <QCoreApplication>
    #include <QObject>
    #include <QDebug>
    
    class MyObject : public QObject {
        Q_OBJECT
    
    signals:
        void mySignal();
    
    public slots:
        void mySlot() {
            qDebug() << "Slot triggered.";
        }
    };
    
    int main(int argc, char *argv[]) {
        QCoreApplication a(argc, argv);
    
        MyObject obj;
        QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot);
    
        emit obj.mySignal();  // 触发槽函数
    
        QObject::disconnect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot);
        emit obj.mySignal();  // 不会触发槽函数
    
        QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot);
        emit obj.mySignal();  // 触发槽函数
    
        return a.exec();
    }
    
    #include "main.moc"
    
    

    通过 QObject::disconnect() 临时断开了信号和槽的连接,然后恢复连接。

  2. 使用 QObject::blockSignals()
    #include <QCoreApplication>
    #include <QObject>
    #include <QDebug>
    
    class MyObject : public QObject {
        Q_OBJECT
    
    signals:
        void mySignal();
    
    public slots:
        void mySlot() {
            qDebug() << "Slot triggered.";
        }
    };
    
    int main(int argc, char *argv[]) {
        QCoreApplication a(argc, argv);
    
        MyObject obj;
        QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot);
    
        obj.blockSignals(true);  // 阻止所有信号的发送
        emit obj.mySignal();     // 这个信号不会触发槽函数
    
        obj.blockSignals(false); // 恢复信号的发送
        emit obj.mySignal();     // 这个信号会触发槽函数
    
        return a.exec();
    }
    
    #include "main.moc"
    
    

    obj.blockSignals(true) 阻止了 obj 对象的所有信号的发送,obj.blockSignals(false) 恢复了信号的发送。

16.qmake之后会发生什么,和cmake有什么区别?

qmake 是 Qt 提供的构建工具,主要用于生成 Qt 项目的 Makefile。qmake 根据 .pro 文件(Qt 项目的配置文件)生成适合于不同平台和编译器的 Makefile。

qmake 的工作流程:

  1. 解析 .pro 文件
    • qmake 读取项目文件(.pro),其中包含了项目的配置、源文件、头文件、库依赖等信息。
  2. 生成 Makefile
    • qmake 根据 .pro 文件的内容,生成适合当前平台和编译器的 Makefile。这些 Makefile 包含了编译和链接项目所需的指令。
  3. 运行 make
    • 使用生成的 Makefile,运行 make 或类似的构建工具来编译和链接项目。

CMake 的工作流程:

  1. 解析 CMakeLists.txt 文件
    • CMake 读取 CMakeLists.txt 文件,其中定义了项目的配置、源文件、编译选项、依赖关系等。
  2. 生成构建系统文件
    • CMake 根据 CMakeLists.txt 的内容生成适合当前平台和编译器的构建系统文件,如 Makefile、Visual Studio 项目文件、Ninja 文件等。
  3. 运行构建系统
    • 使用生成的构建系统文件,运行相应的构建工具(如 makeninjamsbuild 等)来编译和链接项目。
qmakeCMake 的主要区别
  1. 设计目标
    • qmake:主要为 Qt 项目设计,专注于简化 Qt 项目的构建。它对 Qt 的支持非常强大,但对于非 Qt 项目的支持有限。
    • CMake:是一个通用的构建系统工具,支持多种编译器和平台。它不仅支持 Qt 项目,还支持 CMake 用于各种语言和构建系统的项目。
  2. 配置文件
    • qmake:使用 .pro 文件。
    • CMake:使用 CMakeLists.txt 文件。

CMakeLists.txtCMake 构建系统的配置文件,用于描述如何生成构建文件(如 Makefile 或 Visual Studio 项目文件)。

内容:包含了项目的配置、源文件、编译选项、依赖关系等。CMake 使用这些信息来生成适合于不同平台和编译器的构建系统文件。

Makefile 是构建系统的实际执行文件,定义了如何进行编译和链接。

CMakeLists.txtCMake 的输入文件,描述项目的构建配置,CMake 使用它来生成适合平台的构建文件(如 Makefile)。

17.linux下怎么GDB调试正在运行的软件?

在调试进程之前,需要找到目标程序的进程 ID (PID)。可以使用以下命令找到进程的 PID:

ps aux | grep <process_name>

这会显示系统上正在运行的进程,找到你需要调试的程序的 PID。

例如,如果你正在调试名为 myapp 的程序,执行以下命令:

ps aux | grep myapp

GDB 提供了一个 attach 命令,可以附加到正在运行的进程。假设你已经知道了目标程序的 PID 是 12345,可以使用以下命令启动 GDB 并附加到该进程:

gdb -p 12345
调试流程

附加到进程后,你可以像调试从头启动的程序一样,使用 GDB 进行调试。常用的调试命令包括:

  • 查看代码:使用 list 查看源代码。

    (gdb) list
    

    查看函数调用栈:使用 backtracebt 查看当前的函数调用栈。

    (gdb) bt
    

    设置断点:可以在特定函数或行号处设置断点。

    (gdb) break myFunction
    (gdb) break 42  # 在第42行处设置断点
    
    

    继续运行:使用 continue 让进程继续执行。

    (gdb) continue
    

    单步执行:使用 stepnext 进行单步调试。

    (gdb) step  # 进入函数
    (gdb) next  # 不进入函数
    

    查看变量:使用 print 命令查看变量值。

    (gdb) print myVar
    

    查看线程:如果目标进程是多线程程序,可以使用 info threads 命令查看所有线程。

    (gdb) info threads
    

    停止调试并退出 GDB

    (gdb) quit
    

18 .Qt的多线程通信实现方式及C++的多线程通信

C++ 多线程通信方式
  1. 共享内存 + 锁机制

    • 描述:线程通过共享的内存区域传递数据,使用锁来同步访问。常见的同步机制有互斥锁 (std::mutex)、读写锁等。
    • 用法:
      • 共享的数据(如全局变量、对象)被多个线程访问。
      • 使用 std::mutex 确保线程间互斥,避免数据竞争。
  2. 原子操作 (std::atomic)

    • 描述:提供无需加锁的线程安全操作,适合对单个变量的轻量级操作(如计数器)。
    • 优点:避免锁竞争带来的性能开销。
    std::atomic<int> counter(0);
    
    void threadFunction() {
        counter.fetch_add(1);
    }
    
  3. 条件变量 (std::condition_variable)

    • 描述:条件变量与锁一起使用,线程可以在特定条件下等待,直到其他线程通知该条件已满足。
    • 适用场景:适合需要等待某个条件(如生产者-消费者模式)的场景。
    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;
    
    void worker() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return ready; });
        // 继续执行
    }
    
    void notify() {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
        cv.notify_all();
    }
    
    
Qt 多线程通信方式
  1. 信号和槽机制 (SignalSlot)

    • 描述:这是 Qt 的核心通信机制。信号可以在线程之间传递,槽函数会响应信号的发射。这种机制可以保证线程间数据的安全传递,而不需要手动管理锁。
    • 适用场景:适合简单的跨线程通信、异步任务通知等。
  2. 事件和事件循环

    • 描述:Qt 的事件系统可以用于线程之间的通信。通过 QCoreApplication::postEvent(),线程可以向其他线程发送事件,目标线程可以通过事件循环处理这些事件。
    • 适用场景:适合复杂的异步任务调度和事件传递。
    // 向对象发送自定义事件
    QCoreApplication::postEvent(receiver, new QEvent(QEvent::User));
    
    // 接收线程处理事件
    void MyObject::customEvent(QEvent *event) {
        if (event->type() == QEvent::User) {
            // 处理事件
        }
    }
    
    
  3. 共享数据 + 互斥锁 (QMutex)

    • 描述:类似于标准 C++ 中的 std::mutex,Qt 提供 QMutex 来保护共享资源的访问。
    • 适用场景:适合需要手动控制共享资源访问的场景。
  4. 条件变量 (QWaitCondition)

    • 描述QWaitCondition 提供了类似于标准 C++ std::condition_variable 的功能,允许线程等待某个条件满足。
    • 适用场景:适合需要在某个条件下进行同步操作的场景。
    QMutex mutex;
    QWaitCondition condition;
    bool ready = false;
    
    void worker() {
        QMutexLocker locker(&mutex);
        condition.wait(&mutex, [&] { return ready; });
        // 继续执行
    }
    
    void notify() {
        QMutexLocker locker(&mutex);
        ready = true;
        condition.wakeAll();
    }
    
    
  5. 原子变量

    QAtomicInteger

    QAtomicInteger 用于对整型变量进行原子操作。它支持各种基本的操作,如加减、交换、比较并交换等。

    #include <QAtomicInt>
    
    QAtomicInteger<int> atomicCounter(0);
    
    void incrementCounter() {
        atomicCounter.fetchAndAddRelaxed(1);  // 原子加 1
    }
    
    void resetCounter() {
        atomicCounter.store(0);  // 设置新值,非原子赋值
    }
    
    
    //fetchAndAddRelaxed(int value):原子地增加 value,返回之前的值。
    //store(int newValue):设置新值,非原子操作。
    //load():返回当前值。
    

    QAtomicPointer

    QAtomicPointer 用于对指针进行原子操作。适合在多线程中安全地操作指针,防止数据竞争。

    #include <QAtomicPointer>
    
    struct MyData {
        int value;
    };
    
    QAtomicPointer<MyData> atomicPtr;
    
    void setPointer(MyData* newPtr) {
        atomicPtr.store(newPtr);  // 设置指针
    }
    
    MyData* getPointer() {
        return atomicPtr.load();  // 获取指针
    }
    
    

19.Qt的UDP/TCP通信

UDP 通信QUdpSocket

TCP 通信QTcpSocket(用于客户端)和 QTcpServer(用于服务器)

1. UDP 通信
发送端
  1. 创建 QUdpSocket 实例
  2. 发送数据:使用 writeDatagram() 发送数据包。
udpSocket->writeDatagram(data, QHostAddress::LocalHost, 1234);

writeDatagram:用于发送 UDP 数据包,指定目标地址和端口。

readDatagram:用于接收 UDP 数据包,获取数据、发送者地址和端口。

接收端
  1. 创建 QUdpSocket 实例
  2. 绑定端口:使用 bind() 绑定到特定端口,以便接收数据包。
udpSocket->bind(QHostAddress::Any, 1234);

QHostAddress::Any:监听所有网卡上的数据。

1234:监听的端口号。

接收数据:在收到数据时,使用 readDatagram() 来获取数据包。

udpSocket->readDatagram(buffer.data(), buffer.size(), &sender, &senderPort);
2. TCP 通信

TCP(Transmission Control Protocol)是面向连接、可靠的传输协议,适用于数据传输可靠性要求较高的场景。Qt 使用 QTcpSocket 类来进行 TCP 通信,服务器端使用 QTcpServer 来接受客户端的连接。

TCP 客户端
  1. 创建 QTcpSocket 实例
  2. 连接到服务器:使用 connectToHost() 连接到服务器。
tcpSocket->connectToHost(QHostAddress::LocalHost, 1234);

等待连接:使用 waitForConnected() 等待连接成功。

if (tcpSocket->waitForConnected(5000)) { ... }

发送数据:使用 write() 发送数据到服务器。

tcpSocket->write("Hello, Server");

接收数据:使用 readAll() 读取服务器发送回来的数据。

QByteArray data = tcpSocket->readAll();
TCP 服务器
  1. 创建 QTcpServer 实例

  2. 监听端口:使用 listen() 开始监听客户端的连接请求。

    server->listen(QHostAddress::Any, 1234);
    
  3.  // 开启监听,当有新的连接时,会触发 newConnection 信号
    connect(this, &QTcpServer::newConnection, this, &MyTcpServer::onNewConnection);
    
  4. 处理新连接:当有客户端连接时,使用 nextPendingConnection() 获取客户端的 QTcpSocket

    QTcpSocket *clientSocket = server->nextPendingConnection();
    
  5. 接收数据:使用 readAll() 从客户端读取数据。

    QByteArray data = clientSocket->readAll();
    
  6. 发送数据:使用 write() 给客户端发送响应数据。

    clientSocket->write("Hello, Client");
    

20.Qt柱状图实现方式

柱状图可以通过 Qt Charts 模块来实现。Qt Charts 提供了一套绘制各种图表(包括柱状图、折线图、饼图等)的类。要实现柱状图,可以使用 QBarSeriesQBarSetQChartQChartView 等类。

1. 添加 Qt Charts 模块

在项目的 .pro 文件中添加以下代码以启用 Qt Charts 模块:

2. 创建柱状图

以下是一个使用 Qt Charts 创建简单柱状图的完整示例:

21.2D/3D地图应用

2D 地图应用
1.1 使用 QGraphicsView 实现 2D 地图

QGraphicsView 是 Qt 提供的 2D 图形框架,用来绘制和操作二维场景。你可以将地图瓦片或图形添加到 QGraphicsScene 中进行显示和操作。

代码示例:使用 QGraphicsView 实现简单 2D 地图
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QPixmap>

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    // 创建场景
    QGraphicsScene scene;
    
    // 加载地图瓦片(假设已下载的 2D 地图图片)
    QPixmap mapTile(":/resources/map_tile.png");
    
    // 创建地图图元并添加到场景
    QGraphicsPixmapItem *mapItem = new QGraphicsPixmapItem(mapTile);
    scene.addItem(mapItem);

    // 创建视图来显示场景
    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    
    // 设置视图的缩放、平移等功能
    view.setDragMode(QGraphicsView::ScrollHandDrag);
    view.setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
    view.scale(1.5, 1.5); // 设置初始缩放
    
    view.show();
    return a.exec();
}

  • 使用 QGraphicsScene 管理场景中的所有地图元素(如瓦片、图标等)。
  • 使用 QGraphicsView 展示场景,并支持缩放、拖动等交互操作。
  • 可以使用瓦片加载的方式显示整个地图,或动态加载瓦片。
优点:
  • 易于实现平移、缩放功能。
  • 对简单的 2D 地图场景管理比较方便。
局限:
  • 不适用于复杂的地图交互和大量瓦片加载。
  • 对于大规模地图数据,需要引入缓存和更复杂的加载策略。
3D 地图应用
2.1 使用 QOpenGLWidget 实现 3D 地图

QOpenGLWidget 可以用来结合 OpenGL 绘制 3D 地图,并且可以通过加载高程数据、地形数据、模型等实现 3D 效果。

代码示例:使用 QOpenGLWidget 加载 3D 地图
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QApplication>

class My3DMapWidget : public QOpenGLWidget, protected QOpenGLFunctions {
    Q_OBJECT
public:
    My3DMapWidget(QWidget *parent = nullptr) : QOpenGLWidget(parent) {}

protected:
    void initializeGL() override {
        initializeOpenGLFunctions();
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);  // 设置背景色
    }

    void resizeGL(int w, int h) override {
        glViewport(0, 0, w, h);  // 设置视口
    }

    void paintGL() override {
        glClear(GL_COLOR_BUFFER_BIT);
        
        // 这里可以加载并渲染 3D 地形或其他地图数据
        // 例如使用 OpenGL 渲染地图、模型、标记等
    }
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    My3DMapWidget widget;
    widget.resize(800, 600);
    widget.show();

    return a.exec();
}

  • 通过 QOpenGLWidget,你可以使用 OpenGL API 来渲染 3D 地形、建筑等。
  • initializeGL() 初始化 OpenGL 环境,paintGL() 进行渲染操作。
  • 可以加载高程数据来构建 3D 地形模型。
局限:
  • 使用 OpenGL 需要对 3D 图形和渲染技术有一定的了解。
  • 需要手动管理所有地图元素的加载、显示和交互。

22.QObject对象树

QObject 是一个重要的基类,它提供了信号与槽机制、事件处理、对象树结构等功能。QObject 对象树是由 QObject 类及其子类对象组成的层次结构,每个对象可以拥有父对象和子对象,从而形成一个树形结构。

对象树的主要特性
  1. 父子关系

    • QObject 的每个对象都可以指定一个父对象,通过构造函数传入父对象指针或调用 setParent() 方法来设置父对象。
    • 设置了父对象后,父对象会自动管理其子对象的生命周期。这意味着当父对象被销毁时,所有子对象也会被自动销毁,不需要手动删除。
  2. 生命周期管理

    • QObject 对象树的父子关系用于管理内存。子对象的内存由父对象负责清理,因此父对象销毁时会递归销毁所有子对象,防止内存泄漏。
    • 这种设计简化了动态内存管理,尤其是在复杂的 UI 开发中。
  3. 事件传播

    • 事件可以从子对象向上传递,直到父对象或根对象处理事件。这使得可以集中处理一些通用事件。
  4. 查找对象

    • 可以使用 findChild()findChildren() 方法查找特定的子对象或所有符合条件的子对象。例如:

      QObject* button = parent->findChild<QPushButton*>("myButton");
      
QObject对象树的常见用途
  • 内存管理:简化复杂界面中组件的创建和销毁。
  • 事件过滤:通过设置父子关系,实现集中处理某类事件。
  • 查找子对象:便于在父对象中快速找到特定子对象。

23.鼠标事件

在Qt中,鼠标事件主要通过继承自QWidget的类来处理。常见的鼠标事件包括点击、移动、按下和释放等。Qt 提供了相应的事件处理机制,开发者可以通过重载鼠标事件处理函数来响应这些事件。

常见的鼠标事件类型
  1. 鼠标按下事件 (mousePressEvent): 当鼠标按键被按下时,Qt 会触发该事件。
  2. 鼠标释放事件 (mouseReleaseEvent): 当鼠标按键被释放时,触发该事件。
  3. 鼠标双击事件 (mouseDoubleClickEvent): 当鼠标按键被快速双击时,触发双击事件。
  4. 鼠标移动事件 (mouseMoveEvent): 当鼠标在窗口内移动时,触发该事件。
事件处理函数

Qt 提供了一组虚函数,供开发者重载以响应鼠标事件:

  • void mousePressEvent(QMouseEvent *event)
  • void mouseReleaseEvent(QMouseEvent *event)
  • void mouseDoubleClickEvent(QMouseEvent *event)
  • void mouseMoveEvent(QMouseEvent *event)
#include <QApplication>
#include <QWidget>
#include <QMouseEvent>
#include <QLabel>

class MyWidget : public QWidget {
    QLabel *label;

public:
    MyWidget() {
        label = new QLabel(this);
        label->setText("Move the mouse or click.");
        label->resize(200, 20);
    }

protected:
    // 鼠标按下事件
    void mousePressEvent(QMouseEvent *event) override {
        if (event->button() == Qt::LeftButton) {
            label->setText("Left button pressed.");
        } else if (event->button() == Qt::RightButton) {
            label->setText("Right button pressed.");
        }
    }

    // 鼠标移动事件
    void mouseMoveEvent(QMouseEvent *event) override {
        label->setText(QString("Mouse moved: (%1, %2)")
                           .arg(event->pos().x())
                           .arg(event->pos().y()));
    }

    // 鼠标释放事件
    void mouseReleaseEvent(QMouseEvent *event) override {
        label->setText("Mouse button released.");
    }

    // 鼠标双击事件
    void mouseDoubleClickEvent(QMouseEvent *event) override {
        label->setText("Mouse double-clicked.");
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    MyWidget widget;
    widget.resize(300, 200);
    widget.show();
    
    return app.exec();
}

鼠标事件处理详解
  1. QMouseEvent类

    • QMouseEvent 是所有鼠标事件的基础类,它继承自 QInputEvent,包含了有关鼠标点击、移动等事件的详细信息。
    • 常用方法和属性:
      • pos():返回鼠标事件发生时的相对坐标。QMouseEvent::pos() 返回的是鼠标事件发生时,鼠标相对于当前窗口或控件的坐标位置(即相对坐标),如果你在一个按钮上按下鼠标左键,pos() 返回的就是鼠标在按钮区域中的位置,而不是鼠标在整个屏幕上的位置。Qt中的坐标系默认左上角为原点 (0, 0),向右为 X 轴正方向,向下为 Y 轴正方向。
      • globalPos():返回全局屏幕坐标。
      • button():返回按下的鼠标按键,如 Qt::LeftButtonQt::RightButton
      • buttons():返回当前鼠标按键的状态,支持同时按下多个按键。
  2. 鼠标按键Qt::MouseButton 枚举定义了鼠标按键类型:

    • Qt::LeftButton:左键
    • Qt::RightButton:右键
    • Qt::MiddleButton:中键

    捕获鼠标事件: 如果要在窗口外捕获鼠标事件,可以使用 setMouseTracking(true) 启用鼠标跟踪,这样即使不按下鼠标按钮,也能捕获移动事件。

24.QT文件操作

1. 打开文件资源管理器并获取文件路径

在Qt中,使用QFileDialog类可以打开文件选择对话框,让用户选择文件并获取文件路径。

#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QDebug>

// 打开文件资源管理器,获取文件路径并读取文件
void openFileDialog() {
    QString fileName = QFileDialog::getOpenFileName(nullptr, "打开文件", "", "文本文件 (*.txt);;所有文件 (*.*)");
    
    if (!fileName.isEmpty()) {
        qDebug() << "Selected file:" << fileName;
        
        // 打开文件
        QFile file(fileName);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString content = in.readAll();
            qDebug() << "File content:" << content;
            file.close();
        } else {
            qDebug() << "Failed to open file";
        }
    } else {
        qDebug() << "No file selected";
    }
}

2. 拖动文件到窗口获取路径

为了支持拖动文件到窗口获取文件路径,需要:

  1. 启用拖放功能:调用 setAcceptDrops(true)
  2. 重载dragEnterEvent() 来处理拖入事件,检查拖动的数据是否是文件。
  3. 重载dropEvent() 来处理文件放下事件,获取文件路径。
#include <QWidget>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QDebug>

class DragDropWidget : public QWidget {
    Q_OBJECT

public:
    explicit DragDropWidget(QWidget *parent = nullptr) {
        setAcceptDrops(true);  // 启用拖放功能
    }

protected:
    // 拖动进入事件
    void dragEnterEvent(QDragEnterEvent *event) override {
        if (event->mimeData()->hasUrls()) {
            event->acceptProposedAction();  // 接受拖动
        }
    }

    // 文件放下事件
    void dropEvent(QDropEvent *event) override {
        const QMimeData *mimeData = event->mimeData();
        if (mimeData->hasUrls()) {
            QList<QUrl> urlList = mimeData->urls();
            QString filePath = urlList.at(0).toLocalFile();  // 获取第一个文件路径
            qDebug() << "Dropped file path:" << filePath;
        }
    }
};

3. 拖动文件并自动打开文件

基于拖动文件到窗口的功能,进一步处理拖动放下的文件并打开它,可以在 dropEvent() 中直接处理文件内容。

#include <QFile>
#include <QTextStream>
#include <QDebug>

class DragDropWidget : public QWidget {
    Q_OBJECT

public:
    explicit DragDropWidget(QWidget *parent = nullptr) {
        setAcceptDrops(true);  // 启用拖放功能
    }

protected:
    // 拖动进入事件
    void dragEnterEvent(QDragEnterEvent *event) override {
        if (event->mimeData()->hasUrls()) {
            event->acceptProposedAction();  // 接受拖动
        }
    }

    // 文件放下事件
    void dropEvent(QDropEvent *event) override {
        const QMimeData *mimeData = event->mimeData();
        if (mimeData->hasUrls()) {
            QList<QUrl> urlList = mimeData->urls();
            QString filePath = urlList.at(0).toLocalFile();  // 获取第一个文件路径
            qDebug() << "Dropped file path:" << filePath;
            
            // 打开文件并读取内容
            QFile file(filePath);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                QTextStream in(&file);
                QString content = in.readAll();
                qDebug() << "File content:" << content;  // 输出文件内容
                file.close();
            }
        }
    }
};

QFileDialog 本身是一个专用的对话框,主要用于文件选择和打开,默认情况下不支持拖放文件到其界面中。但是你可以通过继承 QFileDialog 并重写其事件处理方法来实现自定义的拖放行为。

实现步骤
  1. 继承 QFileDialog:创建一个自定义的 QFileDialog 类。
  2. 启用拖放功能:重载 dragEnterEvent()dropEvent() 方法来处理拖放事件。
  3. 处理拖放事件:在 dropEvent() 中处理拖放的文件,进行文件的打开等操作。
#include <QFileDialog>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QFile>
#include <QTextStream>
#include <QDebug>

class CustomFileDialog : public QFileDialog {
    Q_OBJECT

public:
    explicit CustomFileDialog(QWidget *parent = nullptr) : QFileDialog(parent) {
        setAcceptDrops(true);  // 启用拖放功能
    }

protected:
    // 拖动进入事件
    void dragEnterEvent(QDragEnterEvent *event) override {
        if (event->mimeData()->hasUrls()) {
            event->acceptProposedAction();  // 接受拖动
        }
    }

    // 文件放下事件
    void dropEvent(QDropEvent *event) override {
        const QMimeData *mimeData = event->mimeData();
        if (mimeData->hasUrls()) {
            QList<QUrl> urlList = mimeData->urls();
            QString filePath = urlList.at(0).toLocalFile();  // 获取第一个文件路径
            qDebug() << "Dropped file path:" << filePath;
            
            // 处理文件(例如,读取文件内容)
            openFile(filePath);
        }
    }

private:
    void openFile(const QString &filePath) {
        QFile file(filePath);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString content = in.readAll();
            qDebug() << "File content:" << content;  // 输出文件内容
            file.close();
        } else {
            qDebug() << "Failed to open file";
        }
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    CustomFileDialog dialog;
    dialog.setWindowTitle("Custom File Dialog");
    dialog.exec();  // 打开自定义对话框

    return app.exec();
}

#include "main.moc"

继承 QFileDialogCustomFileDialog 继承自 QFileDialog 并在构造函数中启用拖放功能。

重载事件处理方法

  • dragEnterEvent(QDragEnterEvent \*event):接受拖入的文件。
  • dropEvent(QDropEvent \*event):处理文件的放下事件,获取文件路径并进行处理。

文件处理:在 dropEvent() 中调用 openFile() 方法来读取文件内容。

25.界面大批量信息显示的渲染速度问题

在Qt中处理界面的大批量信息显示时,渲染速度和性能可能会受到影响。特别是当需要显示大量数据或者频繁更新界面时,优化渲染性能变得尤为重要。以下是一些提高界面大批量信息显示渲染速度的建议:

1. 使用 QGraphicsViewQGraphicsScene

QGraphicsViewQGraphicsScene 提供了一个高效的方式来处理和显示大量图形项。这种方法适用于需要高效渲染大量自定义图形或信息的情况。

#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsTextItem>
#include <QApplication>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    
    // 添加大量图形项到场景中
    for (int i = 0; i < 10000; ++i) {
        QGraphicsTextItem *item = new QGraphicsTextItem(QString::number(i));
        item->setPos(i % 100 * 20, i / 100 * 20);
        scene.addItem(item);
    }

    view.show();
    return app.exec();
}

2. 使用 QTableViewQListViewQAbstractTableModel / QAbstractListModel

如果你的数据是以表格形式或者列表形式呈现的,可以使用 QTableViewQListView,结合 QAbstractTableModelQAbstractListModel,这可以帮助提高大数据量的显示效率。

#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QTableView view;
    QStandardItemModel model(10000, 10);  // 10000 行 10 列

    // 填充数据
    for (int row = 0; row < 10000; ++row) {
        for (int col = 0; col < 10; ++col) {
            model.setItem(row, col, new QStandardItem(QString("Item %1,%2").arg(row).arg(col)));
        }
    }

    view.setModel(&model);
    view.show();

    return app.exec();
}

3. 减少界面更新频率

对于需要频繁更新的数据,尽量减少界面的刷新频率。例如,如果需要更新图形或文本,考虑将更新操作合并到一个批量操作中,而不是每次更改后立即更新界面。

1. 批量更新

当你需要更新多个图形项或文本时,可以将这些更新操作合并在一起,而不是逐个进行更新。例如,假设你需要更新一个表格中的多个单元格,最好一次性更新整个表格,而不是每次更改一个单元格时都触发更新。

性能提升:每次更新界面时,Qt 都需要重新绘制和渲染内容。频繁的更新会增加CPU和GPU的负担,导致界面卡顿或不流畅。

避免闪烁:频繁更新界面可能会导致闪烁现象,使得用户体验变差。减少更新频率可以减少闪烁问题。

减少资源消耗:每次界面更新都消耗系统资源。减少更新次数可以减少资源消耗,尤其是在处理大量数据时。

void updateTableData(const QVector<QString> &data) {
    tableView->setUpdatesEnabled(false);  // 禁用更新

    for (int i = 0; i < data.size(); ++i) {
        // 更新表格中的数据
        model->setData(model->index(i, 0), data[i]);
    }

    tableView->setUpdatesEnabled(true);   // 启用更新
    tableView->viewport()->update();      // 刷新视图
}

2. 使用定时器

如果数据需要周期性更新,可以使用 QTimer 定时器来控制更新频率,而不是在每次数据变化时立即更新。例如,设置一个定时器每隔一定时间更新一次界面,而不是在数据变化时立即更新。

3. 条件更新

仅在数据变化时才进行更新。例如,可以比较新旧数据,如果数据没有变化,则跳过更新。

4. 使用延迟更新

在处理多个数据更新时,可以将更新操作放入事件队列中,延迟执行。Qt 提供了 QTimer::singleShot() 来延迟执行更新操作。

QTimer::singleShot(int msec, const QObject *receiver, const char *member);

msec:延迟的时间,单位是毫秒。

receiver:槽函数的接收者对象,即要调用槽函数的对象。

member:槽函数的成员函数指针,表示要调用的函数。

void delayedAction() {
    qDebug() << "Action executed after delay";
}

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QPushButton button("Click me");
    QObject::connect(&button, &QPushButton::clicked, []() {
        // 延迟1秒钟执行 delayedAction()
        QTimer::singleShot(1000, &delayedAction);
    });

    button.show();
    return app.exec();
}

26.Qt表格使用

QTableViewQTableWidget 是用于显示表格数据的常用控件。它们各有特点和适用场景。以下是它们的使用方法以及它们之间的主要区别。

1. QTableView

QTableView 是一个视图类,它依赖于数据模型来展示表格数据。通常与 QAbstractTableModel 或其子类一起使用,适用于需要自定义表格数据和行为的场景。

使用步骤
  1. 定义数据模型:创建一个自定义的模型类,继承自 QAbstractTableModel
  2. 创建视图:使用 QTableView 来显示模型的数据。
  3. 连接模型和视图:将模型设置到视图中。

在模型中实现排序功能。你可以重写 QAbstractTableModel::sort() 方法来实现自定义排序逻辑。

#include <QApplication>
#include <QTableView>
#include <QAbstractTableModel>
#include <QStandardItemModel>

class MyTableModel : public QAbstractTableModel {
    Q_OBJECT

public:
    MyTableModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {}

    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        return 10;  // 10 行
    }

    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        return 5;  // 5 列
    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        if (role == Qt::DisplayRole) {
            return QString("Item %1,%2").arg(index.row()).arg(index.column());
        }
        return QVariant();
    }
    
    //排序函数重写
    void sort(int column, Qt::SortOrder order) override {
        // Sort data_ based on the specified column and order
        std::sort(data_.begin(), data_.end(), [column, order](const QVector<QVariant> &a, const QVector<QVariant> &b) {
            if (order == Qt::AscendingOrder) {
                return a.at(column) < b.at(column);
            } else {
                return a.at(column) > b.at(column);
            }
        });
        emit layoutChanged();  // Notify the view about the changes
    }
    
private:
    QVector<QVector<QVariant>> data_;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QTableView view;
    MyTableModel model;
    view.setModel(&model);
    view.show();

    return app.exec();
}

我们重写了 sort() 方法来实现自定义排序逻辑,并在排序后调用 emit layoutChanged() 来通知视图更新。

2. QTableWidget

QTableWidgetQTableView 的一个便捷封装,直接提供了表格数据的管理功能。适用于较简单的表格数据展示,不需要复杂的数据模型时使用。

使用步骤
  1. 创建 QTableWidget:实例化 QTableWidget 并设置行和列。
  2. 填充数据:使用 setItem() 方法设置每个单元格的内容。
#include <QApplication>
#include <QTableWidget>
#include <QTableWidgetItem>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QTableWidget tableWidget(10, 5);  // 10 行 5 列
    tableWidget.setWindowTitle("QTableWidget Example");

    for (int row = 0; row < 10; ++row) {
        for (int col = 0; col < 5; ++col) {
            QTableWidgetItem *item = new QTableWidgetItem(QString("Item %1,%2").arg(row).arg(col));
            tableWidget.setItem(row, col, item);
        }
    }
    
	tableWidget.setSortingEnabled(true);  // 启用排序功能
    tableWidget.show();
    return app.exec();
}

setSortingEnabled(true) 启用排序功能。这样,当用户点击表头时,表格会自动按该列进行排序。

默认排序QTableWidget 的默认排序功能对 QTableWidgetItem 对象的文本进行排序。如果单元格中的数据是文本类型,则排序会按字母顺序进行。

自定义排序:如果需要对其他类型的数据进行排序(例如数字或日期),你可能需要自定义 QTableWidgetItem 的排序行为。可以通过继承 QTableWidgetItem 并重写 operator< 来实现自定义排序。

可以获取表头,然后获取点击信号排序

QObject::connect(m_twList.horizontalHeader(), SIGNAL(sectionClicked(int)), this, SLOT(SlotClickedTableHeaderToSort(int)), Qt::QueuedConnection);

SlotClickedTableHeaderToSort中可以自定义排序,然后重新AddItem然后setItem。

调用 QTableWidget::setItem() 方法确实会导致表格的更新和刷新。每次你用 setItem() 设置或替换单元格中的 QTableWidgetItem 时,QTableWidget 会自动刷新显示,以确保界面上显示的是最新的内容。

如果你在排序或修改大量数据时,频繁调用 setItem() 可能会影响性能,尤其是当表格非常大时。




tableWidget.setUpdatesEnabled(false);  // 禁用更新
// 执行大量的 setItem() 操作
tableWidget.setUpdatesEnabled(true);   // 启用更新

使用 QAbstractTableModelQTableView:如果表格数据非常大且需要高效的处理,考虑使用 QTableView 和自定义 QAbstractTableModel。这样可以更细粒度地控制数据更新和视图刷新。


QTableView

  • 优点:灵活性高,支持复杂的数据模型,适合需要动态和自定义表格数据的场景。
  • 适用场景:大规模数据,复杂数据操作,性能要求高的场景。

QTableWidget

  • 优点:简单易用,适合静态或简单表格数据。
  • 适用场景:简单表格展示,快速开发,数据量不大时。

其它

C语言的数据结构经主要包括C语言基础、操作系统、计算机网络、数据结构与算法、设计模式等内容。 在C语言经中,C++的增强特性也是常见的一部分,比如引用、类与对象、模板、智能指针等,还有STL模板库等。这些特性使得C++的使用更加便捷和安全。 此外,C++11引入了一些新特性,比如std::enable_if和SFINAE。SFINAE是Substitution failure is not an error的缩写,意思是匹配失败不是错误。简单来说,当调用模板函数时,编译器会根据传入参数推导最合适的模板函数,在这个推导过程中,如果某些模板函数的推导结果是编译无法通过的,只要有一个可以正确推导出来的模板函数,那些推导结果可能引发编译错误的模板函数并不会导致整个编译错误。 在试中,还会涉及到指向数据成员的指针。C++中可以使用指向类成员函数的指针来调用函数,也可以使用指向类数据成员的指针来访问数据成员。 多线程也是试中常见的一个话题,关于多线程的问题,需要考虑线程安全性。在C++中,需要采取一些措施来保证线程安全,比如使用互斥锁、条件变量等。 综上所述,C语言的数据结构经主要围绕C语言基础、操作系统、计算机网络、数据结构与算法、设计模式等内容展开,并且会涉及到C++的一些增强特性,以及指向数据成员的指针和多线程的相关知识。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [C++开发工程师经总结](https://blog.youkuaiyun.com/Arcofcosmos/article/details/127156504)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [2020秋招_C++基础、数据结构基础经记录](https://blog.youkuaiyun.com/XindaBlack/article/details/107120742)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值