C/C++笔记

一、cmake与交叉编译

0.编译相关

0.1 编译的过程

编译的过程实质上是把高级语言翻译成机器语言(二进制文件)的过程。预处理、编译、汇编和链接四个过程。

0.2 硬链接和软连接的实质

硬链接是通过文件系统中的inode来实现的。当创建一个硬链接时,操作系统会为目标文件创建一个新的目录项,该目录项指向相同的inode,因此,原始文件和硬链接文件都共享相同的inode和数据块。硬链接是对文件的直接引用,它们在文件系统中没有区别,可以独立地访问和操作。

软链接的本质是一个特殊类型的文件,它包含了指向目标文件的路径。软链接文件中存储的是目标文件的路径信息,而不是目标文件本身的数据。当访问软链接时,操作系统会通过路径信息找到目标文件,并提供对目标文件的访问。软链接本质上是一个符号,它指向目标文件。

  • 软链接可以跨文件系统创建,因为它们只是一个指向目标文件的路径,而不需要共享相同的inode。
  • 软链接的目标文件可以是一个不存在的文件或目录,这使得软链接更加灵活。但是如果目标文件被删除或移动,软链接将失效。
  • 软链接占用磁盘空间更多,因为它们需要保存目标文件的路径信息。

1. 什么是 CMake?它的作用是什么?

CMake是一个开源的跨平台构建工具,用于管理C++的构建过程,生成Makefile文件。

2. CMAKE常用函数

函数用途
add_executable()添加源文件
set()定义变量
link_directories()添加库
include_directories()添加头文件
add_compile_options()添加参数
add_subdirectory()将目录下所有源文件<br/>存入变量
CMAKE_C_FLAGS
CMAKE_CXX_FLAGS
设置C和C++标志位
CMAKE_C_COMPILER<br/>CMAKE_CXX_COMPILER指定编译器
find_package查找第三方库

3.什么是交叉编译?

二、C++

2.1 C++11新特性

  • lambda表达式 - 是一种局部函数
  • 智能指针与nullptr
  • 继承和多态中final和overide关键词

    • final修饰虚函数,表示该虚函数不能再被重写,类不能被继承,如果子类继承后重写了该虚函数则编译报错。
    • override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错。
  • 类型推导

    • auto和decltype关键字
  • 右值引用

    • 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
    • 什么是右值:右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。
    • 左值引用,就是绑定到左值的引用,通过&来获得左值引用
    • 右值引用,就是绑定到右值的引用,通过&&来获得右值引用
    • 右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:

      • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
      • 能够更简洁明确地定义泛型函数。
  • 线程相关的库,例如thread、mutex、std::atomic、条件变量等

2.1.1 智能指针

指针用法
std::shared_ptr可以记录指向同一对象的多个指针的个数,最后一个指针被销毁时释放空间
std::unique_ptr独占指针,直至生命周期结束,但是可转让对象
std::weak_ptr用于管理shared_ptr,解决智能指针成环问题(互相指向)没有计数器,
没有引用了就释放。
  • 简介

    • 智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的管理
    • 智能指针重载了(*运算符)和(->)运算符
    • 智能指针离开作用域
  • 为什么要使用智能指针

    • 使用auto_ptr时,拷贝构造后,编译器无法报错程序员误操作被剥夺资源对象的智能指针;
    • 避免野指针出现;
    • 无需手动释放内存,可以规避内存泄漏问题。
  • unique_ptr的简介:

    • 不能使用赋值和拷贝操作初始化,只能使用引用操作初始化
    std::unique_ptr<int> p1(new int(5));//正确
    std::unique_ptr<int> p2 = new int(5);//错误
    • 可以使用make_unique进行构造,但是属于C++14
    • 如果需要所有权转移,则需要std::move()函数进行所有权转移
    • 可以使用release(返回对象指针)和reset(释放对象空间)将所有权转移
    • 离开作用域后就会被释放
  • shared_ptr-共享指针简介:

    • 多个指针可以共享同一个对象所有权,使用计数器来维护指向对象数量。等到计数器变为0时会释放最后一个对象。
  • weak_ptr-弱指针:

    • 是一种不控制对象生命周期的智能指针,指向一个shared_ptr管理的对象,weak_ptr不修改shared_ptr的计数器,只是提供一个访问shared_ptr的对象的手段
    • weak_ptr和shared_ptr之间可以通过lock进行转换。
  • 智能指针的原理
  • 智能指针的误用

    • 对于unique_ptr,使用move进行所有权转移后,依旧使用原来的内容。

2.1.2 auto、decltype和decltype(auto)的用法

auto推断类型,deltype推断类型(只想用类型)

2.1.3 final和overide关键词

2.1.4

2.2 内存管理

2.2.1 内存的分区:

内容说明
栈区局部变量
堆区new、malloc分配
全局/静态存储区static和全局变量
常量存储区常量字符串(虚函数表)
代码区存函数的(虚函数)
  • 堆区和栈区的区别

    • 管理方式不同,栈区操作系统自动分配释放,而堆区需要自己手动分配和释放
    • 空间大小不同,栈区小于堆区
    • 分配方式不同,栈区都是动态分配,栈区有动态有静态。
    • 分配效率不同,栈存储效率高,堆区手动分配,效率比较低。
  • 内存中的顺序
  • 地址从小至大:栈区->代码区

2.2.2 new、delete、malloc、free关系?

相同点

  • 都可用于内存的动态申请和释放

不同点

  • 前者是C++运算符,后者是C/C++语言标准库函数
  • new自动计算要分配的空间大小,malloc需要手工计算
  • new是类型安全的,malloc不是。例如:
  • new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
  • new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

2.3 C++的容器STL

C++ STL从广义来讲包括了三类:算法,容器和迭代器。(容器分序列式容器和关联式容器(底层是二叉树)(键值对))

  • 算法包括排序,复制等常用算法,以及不同容器特定的算法。
  • 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
  • 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

容器包括:vector(底层应该是动态空间,空间不够了就进行复制和动态扩展),deque双端数组,stack栈,queue队列,list链表,set和map(红黑树)

2.3.1 STL组件

  • 容器
  • 算法
  • 迭代器
  • 仿函数:函数对象,是一种特殊的类,可以作为函数的替代品,提供了额外的参数和功能。重载了函数调用运算符 operator() 的对象。它可以像函数一样被调用,用于实现函数对象的封装和行为定制。
  • 适配器:是一种把容器和算法结合起来使用的方法,实现了算法和容器的通用接口
  • 分配器:用于管理容器内存的组件,提供了内存分配、回收和管理的方法。

2.3.2 常见容器

  • 序列式容器

    1. std::vector - 动态数组,支持快速随机访问和动态增长。
    2. std::list - 双向链表,支持快速插入和删除。
    3. std::deque - 双端队列,支持快速在两端插入和删除、占用内存比较多,
  • 有序关联式容器

    1. std::set - 有序集合,不允许重复的键。
    2. std::multiset - 有序多重集合,允许重复的键。
    3. std::map - 有序映射,将键映射到值,不允许重复的键。
    4. std::有序映射,将键映射到值,不允许重复的键。 - 有序多重映射,将键映射到值,允许重复的键。
  • 无序关联式容器

    1. std::unordered_set - 无序集合,不允许重复的键。
    2. std::unordered_multiset - 无序多重集合,允许重复的键。
    3. std::unordered_map - 无序映射,将键映射到值,不允许重复的键。
    4. std::unordered_multimap - 无序多重映射,将键映射到值,允许重复的键。
  • 容器适配器

    1. std::stack - 栈(基于 std::dequestd::list 实现)。
    2. std::queue - 队列(基于 std::dequestd::list 实现)。
    3. std::priority_queue - 优先队列、底层是大(小)根堆(二叉树)(通常基于 std::vector 实现)。

2.3.4 vector容器特辑

  • 优点

    1. 使用连续的存储空间,访问速度快。
    2. 支持随机访问
    3. 尾插效率比较高
    4. 储存大小自动管理
  • 缺点

    1. 随机插入效率比较低
    2. 只能尾部插入和删除
    3. 添加数据占用内存超过vector预先分配的大小时,需要重新分配、拷贝与释放。
  • vector扩容

    • windows平台下是1.5倍
    • linux平台下是2倍
    • 优化:①创建容器对象后,直接通过reserve(修改容量大小)resize(同时修改容量大小和实际数据内容的大小)分配空间大小;②shrink_to_fit函数进行容量缩减
    • reserve与resize的区别

      1. reserve只响应扩容操作,不能进行容量缩减
      2. resize即对容量大小进行分配,也对实际size数据多少进行操作,当待分配数据小于当前数据的数量时,删除数据,容量不变;当待分配数据大于当前数据的数量时,即进行扩容,并且创建指定数量的新对象,并赋初值(按第二参数)
    • vector容量缩减

      • 先用clear,再shrink_to_fit
  • vector迭代器失效原因

    1. 删除与插入,当前位置到末尾的迭代器都会失效,需要更新迭代器
    2. 扩容,在其他位置重新开辟了一块内存,所有位置的迭代器都会失效
  • 线程安全性

    1. vector容器基本都是线程不安全的
    2. 可以使用固定容量来避免动态扩容,避免lock-free
    3. std::shared_mutex共享独占锁C++17

2.3.3 哈希表

通过键 key 和一个映射函数 Hash(key) 计算出对应的值 value,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。

2.3.7 hashtable中解决冲突有哪些方法?

线性探测:使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位

开链:每个表格维护一个list,如果hash函数计算出的格子相同,则按顺序在这个list中

再散列:发生冲突时使用另一种hash函数再计算一个地址,直到不冲突

二次探测:使用hash函数计算出来的元素占用了,按照各自平方的步长依次寻找。

公共溢出区:一旦hash函数计算的结果相同,就再放入公共溢出区。

2.3.8 哈希函数

  • 直接定址法

    • 取关键字本身 / 关键字的某个线性函数值 作为哈希地址。
  • 除留余数法

    • 假设哈希表的表长为 m,取一个不大于 m 但接近或等于 m 的质数 p,利用取模运算,将关键字转换为哈希地址。
  • 平方取中法

    • 先通过求关键字平方值的方式扩大相近数之间的差别,然后根据表长度取关键字平方值的中间几位数为哈希地址。
    • 比如:Hash(key) = (key * key) // 100 % 1000,先计算平方,去除末尾的 2 位数,再取中间 3 位数作为哈希地址
  • 基数转换法

    • 将关键字看成另一种进制的数再转换成原来进制的数,然后选其中几位作为哈希地址。
    • 比如,将关键字看做是 13 进制的数,再将其转变为 10 进制的数,将其作为哈希地址。

2.4 C++多态/继承

2.4.1 什么是多态?

多态是C++的三大特性之一,直白的说,就是“一种接口,多种实现”。具体表现是调用同一种函数名时,函数的实现不同。

2.4.2 什么是编译时多态和运行时多态

C++中实现多态有两种方式,编译时多态和运行时多态。编译时多态是通过函数重载,运算符重载和模板实现的,在编译阶段就能决定调用哪个函数。运行时多态是通过虚函数实现的,使用方法就是基类的指针或者引用指向和引用派生类的对象时,调用时派生类的虚函数,在运行时才能决定调用哪个虚函数。

2.4.3 为什么运行时多态在运行时才能决定要调用哪个函数?

当通过基类指针调用虚函数时,在编译阶段并不能确定这个指针指向的是基类的对象或者是某个派生类的对象。只有在运行时,用户指定了这个对象的类型,才能确定调用这个对象的虚函数。

2.4.4 虚函数的实现原理?【重要】

虚函数的特性是通过虚函数表来实现的,每个有虚函数的类都会包含一张虚函数表,实际上是个函数指针数组,保存的是虚函数的地址。类声明的对象中会保存这个表的指针。C++编译器会在对象占用内存空间的初始地方保存这个指针,这样就可以通过对象的地址得到虚函数表,通过遍历虚函数表得到虚函数地址。

当子类继承父类时,也会继承这张虚函数表。子类对基类的虚函数没有重写时,子类的虚表指针指向基类的虚表;当子类对基类的虚函数重写时,子的虚表指针指向自身的虚表;当子类中有自己的虚函数时,将此虚函数地址添加在虚函数表后面。

在C++多继承中,子类继承了几个基类就会有几个虚表。如果类c继承了类a和类b,则c对象有两个虚函数表指针指向两个虚函数表。c中覆盖的虚函数会替换掉两个表中同名的虚函数指针位置。新添加的虚函数的地址会附加到第一个虚函数表后面。

2.4.5 虚函数类型

构造函数、静态函数,友元函数,内联函数都不可以是虚函数,但是析构函数最好是虚函数。

为什么基类析构函数最好是虚函数:如果创建基函数的指针指向派生类对象,在delete的时候会只析构基类不析构派生类。

2.4.6 什么是纯虚函数

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。包含纯虚函数的类称为抽象类抽象类无法创建对象。

例如动物,它本身就不能是对象。但是猫可以是。

2.5 C++模板是什么,你知道底层怎么实现的吗?

  1. 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
  2. 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。

2.6 什么是继承?继承有哪几种?

  • 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性(父类或者基类)的基础上进行扩展。

2.6.1 继承分哪几种?

  • 公有继承

    • ·派生类可以直接访问
  • 保护继承

    • 公有和保护成员作为它自己的保护成员,但不继承基类的私有成员。
    • 公有成员和保护成员在派生类中变成了保护的,只能在派生类内部或者派生类的友元函数中访问。
  • 私有继承

    • 公有和保护成员作为它自己的私有成员,但不继承基类的私有成员。
    • 基类的所有成员在派生类中都变成了私有的,只能在派生类内部访问,不能在外部访问。

2.6.2 静态成员如何?

  • 静态成员是共享的成员,不仅仅基类的多个对象共用这个成员,派生类的对象也和基类的对象一起共用这个成员

2.6.3 多继承?隐患?如何避免

  • 多继承下的隐患–数据冗余与二义性

    • 数据冗余,两个基类成员都被继承了。
    • 二义性,两个基类成员名称相同。
    • 避免函数相同;应当指明基类;使用虚继承(virtual关键字)

2.6.4 虚继承?

主要是解决菱形继承的二义性和数据冗余问题,使用virtual关键字,可以使共同基类只被继承一次。

解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。解决了二义性问题,也节省了内存,避免了数据不一致的问题。

2.6.4 什么是友元函数?友元是不被继承的?

2.7 构造函数、拷贝构造函数和赋值操作符的区别

构造函数

对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数

拷贝构造函数

对象不存在,但是使用别的已经存在的对象来进行初始化

赋值运算符

对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的

2.8 内存对齐

所谓的内存对齐,就是为了让内存存取更加有效率而采取的一种优化手段,对齐的结果是使得内存中数据的首地址是CPU单次获取数据大小的整数倍

只要在代码前添加关键字 #pragma pack(n) 即可

2.9 什么是大端小端

大端模式,是指数据的高字节保存在内存的低地址中;小端模式,是指数据的高字节保存在内存的高地址中。

2.10 深拷贝与浅拷贝

在拷贝构造时,只拷贝了指针,未拷贝指针对应的对象,这样若原有拷贝对象被释放,再使用指针操作原有对象时会导致内存泄漏。

二、设计模式

三、操作系统

3.1 进程间通信的方式是什么?

进程之间的通信方式一般有管道、FIFO、消息队列、信号量和共享内存五种。

管道指的是无名管道,是UNIX 系统IPC最古老的形式。其特点如下:

(1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

(2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

(3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

FIFO是命名管道,是一种文件类型。其特点如下:

(1)FIFO可以在无关的进程之间交换数据,与无名管道不同。

(2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

消息队列是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。其特点如下:

(1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

(2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

(3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

信号量是一个计数器,用于实现进程间的互斥和同步,其特点如下:

(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

(2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

(3)每次对信号量的 PV 操作不仅限于对信号量值加1或减1,而且可以加减任意正整数。

(4)支持信号量组。

共享内存指的是两个或多个进程共享一个给定的存储区,其特点如下:

(1)共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

(2)因为多个进程可以同时操作,所以需要进行同步。

(3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

3.2 线程之间的通信方式是什么?

3.3 进程调度算法你了解多少?

  • 先来先服务(FCFS)
  • 短作业优先(SJF)
  • 最短时间优先(SRTN)
  • 时间片轮转
  • 优先级调度
  • 多级反馈队列

3.4 进程与线程的区别

  • 进程是资源分配的最小单位、线程是程序执行的最小单位
  • 线程的启动速度比线程快
  • 同一线程共享的有堆、全局变量、静态变量、指针,引用、文件等,而独自占有栈

3.5 线程安全性的体现

  • 原子性

    • 互斥访问,同时只能有一个线程来对它进行操作,如Aotmic包、互斥锁mutex
    • 锁的类型:
  • 可见性

    • 一个线程对主内存的修改可以及时的被其他线程观察的到
    • volatile修饰共享变量,共享变量在修改后会立即被更新到内存中。
  • 有序性

    • 一个线程观察其他线程中指令执行顺序,由于指令重排序存在,观察结果一般杂乱无序

3.6 内存泄漏

3.6.1 什么是内存泄漏

内存泄漏,是指疏忽或错误造成了程序未能释放掉不再使用的内存的情况,失去了对该内存的控制,从而造成了内存的浪费或者性能不良的情况。

3.6.2 内存泄漏有哪些情况

  • 堆内存泄漏:主要是new,malloc进行分配的内存,在操作完成后没有使用free和delete释放掉
  • 系统资源泄露:主要是系统分配给程序的资源没有释放(bitmap)
  • 多态的基类未释放子类对象:基类指针指向子类对象,基类析构函数非虚函数,则只会调用基类析构,不调用子类析构。
  • 数组对象未使用delete [],而使用了delete
  • 拷贝对象时进行了浅拷贝,未拷贝指针对象,拷贝完成后使用指针时,原对象已经被释放。

四、计算机网络

4.1 OSI的七层模型分别是什么

  • 物理层(网线)
  • 数据层(mac地址)
  • 网络层(IP,路由等)
  • 传输层(TCP/IP)
  • 会话层
  • 表示层
  • 应用层

4.2 http请求过程

域名解析->TCP的3次握手->发起http请求

4.3 拆包与粘包

完整的业务会被拆分成多个包进行发送,也可能把多个小的包封装成一个大的数据包。

原因:

1.应用程序写入数据的字节大小大于套接字发送区的大小

2.paload大于MTU进行IP分片

4.4 HTTP缓存策略

  • 强制缓存和协商缓存

1.强制缓存的优先级高于协商缓存

2.强制缓存是服务器告诉浏览器一个时间,时间内读取本地缓存,时间外进行协商缓存策略

3.协商缓存是让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified字段通过请求发送给服务器,由服务器校验。如果资源一致则返回 304 并继续使用浏览器缓存,反之返回 200 和最新的资源。

4.5 三次握手与四次分手

三次握手

1.客户端->SYN(连接请求报文)->发送等待状态

2.服务器端->ACK+SYN(确认连接)->客户端

3.客户端->ACK->表示收到确认连接请求

四次挥手

1.客户端—>FIN(断开连接请求)

2.服务器端—>FIN+ACK(表示收到断开连接请求)

3.服务器端传输剩余数据后

4.服务器端->FIN(断开连接请求)

5.客户端收到后->ACK(表示收到服务器端的断开连接请求).

6.最后再等待2个最大报文时长,保证最后一个ACK发送完毕

4.6 TCP与UDP的区别

  • TCP是面向连接的(三次握手、四次挥手),而UDP是面向无连接的
  • TCP可靠性强:无差错(CRC16)、不丢失、不重复、有序到达(序列号)
  • 传输效率:TCP传输效率相对较低,UDP效率较高
  • TCP支持一对一(端口到端口),UDP支持广播

4.7 如何创建UDP和TCP连接

五、数据结构

5.1 二叉树

5.1.1 BFS和DFS的区别是什么?

BFS是广度优先搜索,其特点是:

(1)BFS从根节点开始搜索,并根据树级别模式探索所有邻居根。

(2)它使用队列数据结构来记住下一个节点访问。

(3)BFS比DFS需要更多的内存。

(4)它是使用FIFO列表应用的。

(5)寻找最短路径的理想选择。

(6)该算法用于查找两个节点之间的最短路径,发现图中的所有连接组件,分析图是否为二部图等。

DFS是深度优先搜索,其特点是:

(1)DFS从根节点开始搜索,并从根节点尽可能远地探索这些节点。

(2)使用堆栈数据结构来记住下一个节点访问。

(3)DFS所需的内存少于BFS所需的内存。

(4)它是通过LIFO列表应用的。

(5)寻找最短距离的理想选择。

(6)该算法用于解决问题,拓扑排序,需要对图进行回溯,识别图中的循环以及发现两个节点之间的路径等。

5.1.2 什么是完全二叉树,什么是满二叉树

完全二叉树:除了最后一层的节点,其余节点的左右节点都有数据,最后一层的节点无数据的节点都在右侧。(口语化)

满二叉树:二叉树的每一层节点度都达到最大值,最后一层数据是满的。

5.1.2 什么是平衡二叉树

它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

5.1.3 什么是搜索二叉树

二叉搜索树(Binary Search Tree)也叫二叉查找树,他是具有下列性质的一种二叉树。

  • 若左子树不空,则左子树上所有节点的值都小于根节点的值;
  • 若右子树不空,则右子树上所有节点的值都大于根节点的值;
  • 任意节点的子树也都是二叉搜索树;
  • 经典搜索二叉树是没有重复值的,重复值都会被放在同一个节点。

使用中序遍历,

六、嵌入式相关

6.1 通信协议

6.1.1 RS485与RS232、RS422的区别

I2CSPIRS232RS485/RS422
模式 四种模式
电平 +/-3~+-15V差分信号
设备数量2^7=128(经验为8个)由片选线决定一主一从32-1(RTU)
通信速率10k-1.2MHz
通信距离
通信方式半双工全双工全双工半双工/全双工(422)

6.1.2 Modbus协议相关

6.1.3 IIC相关

6.1.3.1 IIC能挂载多少个从设备

IIC总线最多可以挂多少个设备由IIC地址决定,8位地址,减去1位广播地址,是7位地址,$$2^7=128$$,但是地址0x00不用,那就是127个地址,所以理论上可以挂127个从器件。

IIC的从机地址由三种类型,7bit,8bit,10bit。

但是 I2C 协议规定,总线上的电容不可以超过400pF。管脚都是有输入电容的,PCB上也会有寄生电容,所以会有一个限制。

实际设计中经验值大概是不超过8个器件。

6.1.3.2 SDA和SCL的引脚
  • 为什么配置成开漏输出,不能是推挽输出

    • 防止短路
  • 为什么一定要配置上拉电阻

    • 确保数据空闲时为高电平
    • 开漏输出无法输出高电平,此时需要外部上拉来提供高电平
6.1.4.2 起止标志
  • 起始:SCL高电平,SDA,高->低
  • 终止:SCL高电平,SCL,低->高
6.1.4.3 读/写区别

在7位寻址时,低1bit位为读写位,0表示写,1表示读

6.1.4.4 应答信号

主机发送完一个8位数据后,会等待从机的回答;一个ACK信号,SDA将会拉低。

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

从机通过拉低SDA表示发出应答信号,表示通信成功,否则表示通信失败。

如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据。

6.1.4.5 NACK与ACK
  • ACK:发送者在ACK时钟脉冲期间释放SDA线,接收者可以将SDA拉低并在时钟信号为高时保持低电平。
  • NACK:当在第9个时钟脉冲的时候SDA线保持高电平,就被定义为NACK信号。Master要么产生STOP条件来放弃这次传输,或者重复START条件来发起一个新的开始。

6.2 MQTT面试相关

6.2.1 请介绍一下mqtt

MQTT协议是一种轻量级的基于发布/订阅模式的“轻量级”即时通讯协议,该协议构建于TCP/IP协议上,广泛应用于物联网和移动设备等资源有限的场景中。其特点包括:

  • 小巧灵活、易于实现、低开销、低带宽占用
  • 支持QoS级别
  • 支持订阅/发布模式
  • 支持SSL/TLS加密等

6.2.2 mqtt的QOS级别

MQTT中的QoS级别包括:0、1和2三个级别。

  • QoS 0表示最多发送一次,不进行确认;
  • QoS 1表示至少发送一次,确保消息至少被接收一次;
  • QoS 2表示恰好发送一次,确保消息仅被接收一次。不同的QoS级别会对消息的可靠性和效率产生影响,越高的QoS级别意味着更高的可靠性但也会降低效率。

6.2.3 MQTT与HTTP协议比较

  • MQTT是一种轻量级的消息传输协议,而HTTP是一种应用层协议;
  • MQTT应用于实时通信和物联网场景,HTTP主要用于Web应用;
  • MQTT采用长连接保持通信,HTTP每次请求需要建立新的连接。它们各自适用于不同的场景,例如,MQTT适用于需要实时通信、资源有限的场景,而HTTP适用于需要通过浏览器访问Web服务的场景。

6.2.4 MQTT Broker的作用是什么?它有哪些常见的开源实现?

  • MQTT Broker是MQTT协议的核心组件,作为消息的发布/订阅中心,负责接收和转发客户端的消息。常见的开源MQTT Broker包括:Eclipse Mosquitto、Apache ActiveMQ Artemis、EMQ X等。

6.2.5 MQTT客户端连接到Broker时需要提供哪些参数?它们有什么作用?

MQTT客户端连接到Broker时需要提供以下参数:

  • Broker地址、Port端口号
  • Client ID客户端标识符;Username用户名、Password(密码)等
  • 其中Client ID是必需的,且需要确保唯一性,其他参数根据需要进行配置。

6.2.6 如果一个MQTT客户端想要订阅多个主题,应该如何实现?

如果一个MQTT客户端想要订阅多个主题,可以将多个主题名称用“/”(斜杠)分隔开,例如:“topic1/topic2”。这样订阅后,客户端就能接收所有以“topic1”或“topic2”作为前缀的消息。

七、QT相关

7.1 信号槽与槽函数

Qt信号与槽机制是一种基于事件机制的编程模型,用于对象之间的通信。信号是由发送方对象发射的事件,而槽是接收方对象用于处理这些事件的函数。在Qt中,我们可以使用QObject类中的信号和槽机制来实现对象间的通信。通过定义信号和槽函数,在信号发射时,会自动调用对应的槽函数进行处理。

优缺点

1)QT信号槽机制的引用精简了程序员的代码量 (不用写回调函数)
2)QT的信号可以对应多个槽(但他们的调用顺序随机),也可以多个槽映射一个信号
3)QT的信号槽的建立和解除绑定十分自由
4)信号槽同真正的回调函数比起来时间的耗损还是很大的,所有在嵌入式实时系统中应当慎用
5)信号槽的参数限定很多例如不能携带模板类参数,不能出现宏定义等等

7.2 如何进行Qt的性能优化

减少内存使用:使用智能指针、减少不必要的拷贝、避免频繁的new和delete操作等。如使用QVector代替QList,在需要大量存储数据时能够提高性能。

减少绘制次数:使用QPainter的缓存绘制功能,对于需要频繁绘制的控件,将绘制结果缓存起来,只在需要更新时才进行重绘。

使用多线程:在需要大量计算的场景中,将计算放到后台线程中,避免阻塞UI线程,提高响应速度。

避免频繁的信号和槽连接:频繁的信号和槽连接会带来额外的开销,可以将一些信号槽的连接放到初始化阶段,避免重复连接。

合理使用QML:对于需要频繁更新的UI组件,使用QML实现,能够减少UI线程的工作量,提高UI性能。

八、算法

排序算法

Last modification:October 26th, 2023 at 05:32 pm