C++面试题
文章目录
- 1、变量的声明和定义
- 2、sizeof和strlen的区别
- 3、volatile关键字
- 4、数组和链表的区别
- 5、引用和指针的区别
- 6、static关键字
- 7、const关键字
- 8、内存四区
- 9、struct和union的区别
- 10、#define和const的区别
- 11、为什么有#define了还要用const
- 12、重载,覆盖,隐藏
- 13、new、delete、malloc、free之间的关系
- 14、delete 和 delete[]
- 15、虚函数、纯虚函数
- 16、STL库
- 17、C++文件编译与执行的四个阶段
- 18、STL常用容器实现原理
- 19、STL中unordered_map和map的区别
- 20、构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因
- 21、拷贝构造函数
- 22、野指针
- 23、vector和list的区别
- 24、友元
- 25、struct 和 class 区别
- 26、include头文件的“”和<>的区别
- 27、智能指针
- 28、智能指针的性能如何
- 29、C++11新特性
- 30、TCP和UDP的区别及使用场景
- 31、单例模式
- 32、工厂模式
- 33、什么函数不能声明为虚函数
- 34、析构函数可以是纯虚的吗
- 35、C++中为什么用模板类
- 36、三次握手
- 37、四次挥手
- 38、extern “C”
- 39、拷贝构造函数在哪几种情况下会被调用
- 40、Get/Post的区别
- 41、http和https分别是什么?他们的区别是什么
- 42、锁的实现方式
- 43、线程与进程的区别和联系
- 44、树
- 45、Twap算法
- 46、红黑树和AVL树的定义,特点,以及二者区别
- 47、Socket通信中的粘包和拆包问题是什么
- 48、函数指针,指针函数
- 49、指针数组,数组指针
- 50、C++为什么用模板类
- 51、如何定义一个只能在堆上创建的类
- 52、如何定义一个只能在栈上创建的类
- 53、内联函数有什么优点?内联函数和宏定义的区别
- 54、常见的消息队列
- 55、RabbitMQ
1、变量的声明和定义
- 声明
- 告诉编译器变量的存在,不分配内存或初始化
- 它通常发生在头文件或函数参数列表中,以便不同部分的代码可以知道这个变量的存在
- 定义
- 为变量分配存储空间并可以赋初值,它在程序中创建了变量的实体,使其可以在运行时存储数据
- 变量只需要在一个地方定义一次,但可以在多个地方声明
2、sizeof和strlen的区别
- sizeof
- 是一个运算符,它返回一个变量或数据类型的字节数
- strlen
- 是一个函数,用于计算字符串中的字符数
3、volatile关键字
用于告诉编译器不要对标记为 volatile
的变量进行优化,以确保其值在程序执行期间不会被缓存或重新排序
volatile还可以告诉编译器不要将变量的值缓存在寄存器中,而是每次都从内存中读取。这对于与硬件通信或多线程编程等情况非常重要
volatile还可以提醒其他程序员或维护人员,这个变量是特殊的,可能会在不同的地方更改,因此要特别小心处理
4、数组和链表的区别
- 数组
- 数组是一种静态数据结构,其元素在内存中是连续存储的,具有固定的大小,因此它们对于随机访问和遍历元素非常高效。然而,数组的大小在创建时就确定,难以动态扩展或缩小,这限制了其灵活性。
- 链表
- 链表是一种动态数据结构,由节点组成,每个节点包含数据和指向下一个节点的指针。链表的元素在内存中不一定是连续存储的,这使得插入和删除元素非常高效,因为只需调整指针,而不需要移动大量数据。链表的大小可以动态增长或缩小,因此更适合用于动态的数据结构。
5、引用和指针的区别
- 语法差异: 引用使用
&
符号声明,而指针使用*
符号。引用在声明时需要初始化,而指针可以稍后初始化或者指向不同的对象。 - 空值: 指针可以为空,即指向空(null)地址,而引用必须始终引用某个有效的对象。
- 别名 vs 变量: 引用是变量的别名,操作引用等同于操作原始变量。指针是一个独立的变量,它存储另一个变量的地址,需要通过解引用运算符
*
来访问目标变量。 - 多次赋值: 引用一旦初始化,无法重新绑定到另一个对象。指针可以在需要时重新指向不同的对象。
- 传递方式: 通常,引用用于传递函数参数,使函数能够修改原始数据。指针也可以用于这一目的,但需要更多的注意来避免悬空指针或内存泄漏。
6、static关键字
- 静态成员变量: 静态成员变量属于类而不是类的实例。它们被共享并存储在类的单一内存位置中,可用于跟踪整个类的信息。
- 静态全局变量: 静态全局变量在整个程序的生命周期内存在,但它们仅在定义它们的文件中可见。它们可用于在单个文件内共享数据,而不暴露给其他文件。
- 静态局部变量: 静态局部变量在函数内部定义,但仅初始化一次,然后保持其值。它们通常用于避免重复初始化和在多次函数调用之间保持状态。
- 静态成员函数: 静态成员函数是属于类而不是类的实例的函数,它可以直接通过类名调用,无需创建类的对象。这些函数通常用于执行与类相关但不需要特定对象实例的任务,如全局配置或实用函数。
7、const关键字
- 常量声明: 通过在变量前加上
const
关键字,可以声明一个常量,其值在初始化后不可更改,有助于提高代码的可读性和安全性。 - 成员函数: 在类中,
const
可用于声明成员函数,表示该函数不会修改类的成员变量。这有助于确保在调用函数时不会意外地修改对象的状态。 - 函数参数: 在函数参数中使用
const
表示该参数是只读的,不能在函数内部修改。这有助于传递参数时保护其内容。 - 返回值: 在函数声明中,
const
可以表示该函数返回一个常量值,不可用于赋值操作。
总之,const
在C++中用于创建常量、指定不可修改的数据、保护函数参数和防止不必要的数据更改,有助于提高代码的可靠性和维护性。
const int *a; *//等于 int const \*a;*
int * const a;
int const * const a; *//等于 const int\* const a;*
以*为分界点,
当const在*的左边时,实际物体值不变
当const在*的右边时,指针不变
当const在*的两边时,指针和指向的数都不可变
即, 左物右指
这样来记比较方便!!
8、内存四区
- 栈区(Stack): 栈区用于存储局部变量和函数调用的上下文信息。它是一个自动分配和释放内存的区域,通常包含函数参数、局部变量和函数调用的返回地址。栈上的数据在函数调用结束后会自动释放,因此具有较短的生命周期。
- 堆区(Heap): 堆区用于存储动态分配的内存,通常由程序员手动分配和释放。堆上的数据的生命周期可以很长,需要程序员负责管理内存的分配和释放,以避免内存泄漏。
- 全局/静态区(Global/Static): 全局区用于存储全局变量和静态变量。这些变量在程序启动时创建,直到程序结束时才销毁。全局区中的数据在整个程序执行期间都是可用的。
- 常量区(Constant): 常量区用于存储常量数据,如字符串常量。这些数据通常存储在只读内存区域,不能被修改。常量区中的数据对于所有程序实例都是相同的。
9、struct和union的区别
struct(结构体)
- struct是一种用户自定义的数据结构,每个成员变量在内存中占据独立的空间,结构体的大小等于所有成员变量大小之和。
- 可以同时访问结构体的所有成员,每个成员都有自己的地址。
- 每个成员可以独立初始化。
union(联合)
- union也是一种用户自定义的数据结构,联合的内存大小等于最大成员的大小,因为只能存储一个成员的值。
- 只能同时访问一个成员,因为它们共享相同的内存,访问一个成员可能会影响其他成员的值。
- 只能初始化一个成员,因为只能存储一个成员的值。
10、#define和const的区别
#define
- #define创建的常量没有特定类型,它们只是文本替换
- #define常量在预处理阶段进行替换,可能导致多个重复的值
- 不能使用指针指向#define常量,因为它们没有明确的内存位置
- #define可以用于定义简单的宏函数。
const
- const常量有明确定义的类型,可以提供更好的类型安全性
- const常量在编译时确定其值,只有一个实例,不会有多个拷贝
- 可以使用指针指向const常量,因为它们在内存中有确定的位置
- const不用于定义函数,它是用于创建常量变量的关键字
11、为什么有#define了还要用const
- 类型安全性:
const
创建的常量具有明确定义的数据类型,这提供了类型安全性。如果使用错误的类型,编译器将发出错误。而#define
创建的常量只是文本替换,没有类型信息,可能导致类型错误。 - 调试和可读性:
const
常量在编译后会保留类型信息,这对调试和代码可读性非常有帮助。#define
常量只是文本替换,不会在调试信息中显示。 - 作用域控制:
const
常量可以具有作用域,可以限定在特定的代码块中,而#define
常量在整个文件中都有效。 - 指针关联:你可以使用指针指向
const
常量的地址,这对于处理常量数据很有帮助。使用#define
常量时,它们没有确定的内存位置,无法使用指针。
总之,虽然 #define
和 const
都可以用于定义常量,但 const
在类型安全性、调试可读性、作用域控制和指针关联方面提供了更多的优势。因此,现代C++编程中通常更倾向于使用 const
来定义常量。
12、重载,覆盖,隐藏
- 重载(Overload):在同一作用域内定义多个具有相同名称但不同参数列表的函数,编译器根据参数来选择正确的函数。
- 覆盖(Override):在派生类中重新定义基类中的虚函数,以提供新的实现。覆盖的函数在基类和派生类中必须具有相同的函数原型,包括函数名称、参数类型和返回类型。
- 隐藏(Hide):在派生类中定义一个与基类中函数名称相同但参数列表不同的函数,导致基类函数在派生类中不可见。隐藏的函数在派生类中是一个全新的函数,不要求是虚函数。
13、new、delete、malloc、free之间的关系
new
和delete
(C++中的运算符):new
是C++中的运算符,用于在堆上动态分配内存,并返回分配内存的指针。它还会调用对象的构造函数。delete
是C++中的运算符,用于释放通过new
分配的内存,并调用对象的析构函数。它与new
配对使用,确保资源的正确释放。
malloc
和free
(C语言中的函数):malloc
是C语言中的库函数,用于在堆上动态分配内存,返回一个指向分配内存的指针。它不会调用对象的构造函数。free
是C语言中的库函数,用于释放通过malloc
分配的内存。它与malloc
配对使用,确保资源的正确释放。
关键区别:
new
和delete
是C++特有的,与类和对象相关。它们处理内存和对象的生命周期。malloc
和free
是C语言中的标准库函数,用于处理一般的内存分配和释放,不处理对象构造和析构。- 在C++中,可以混合使用
new
和delete
以及malloc
和free
,但要小心避免混淆和内存泄漏。
在现代C++中,推荐使用 new
和 delete
来进行内存管理,因为它们更安全,能够正确处理对象的构造和析构。
14、delete 和 delete[]
-
delete
用于释放单个对象的内存,而delete[]
用于释放数组对象的内存。 -
如果使用
new
分配了单个对象的内存,应使用delete
进行释放;如果使用new[]
分配了数组对象的内存,应使用delete[]
进行释放。
15、虚函数、纯虚函数
- 虚函数(Virtual Function):
- 虚函数是在基类中声明为虚函数的函数,其特征是在派生类中可以被覆盖(重写)。
- 虚函数用
virtual
关键字进行声明,例如:virtual void myFunction() { /* 函数实现 */ }
。 - 虚函数允许运行时多态性(动态绑定),即可以使用基类指针或引用来调用派生类的实现。
- 基类中的虚函数可以有默认实现,但它们通常在派生类中被重写以提供具体的实现。
- 纯虚函数(Pure Virtual Function):
- 纯虚函数是在基类中声明为纯虚函数的函数,它没有默认实现,只有函数原型。
- 纯虚函数用
virtual
关键字和= 0
进行声明,例如:virtual void myFunction() = 0;
。 - 派生类必须提供对纯虚函数的具体实现,否则派生类也将成为抽象类。
- 纯虚函数用于定义一个接口,以确保派生类提供必要的实现。
16、STL库
STL(Standard Template Library)是C++标准库中的一个重要组成部分,提供了各种通用数据结构和算法,以及用于处理这些数据结构的迭代器等工具。
-
容器(Containers):
- 序列式容器(Sequence Containers):这些容器按照元素的线性顺序存储数据,可以在其中插入、删除和访问元素。常见的序列容器包括:
- vector:动态数组
- list:双向链表
- array:固定大小的数组
- …
- 关联式容器(Associative Containers):这些容器按照键(key)来组织元素,用于高效的查找和检索。常见的关联容器包括:
- set:有序不重复元素集合
- multiset:有序可重复元素集合
- map:有序键-值对映射
- …
- 序列式容器(Sequence Containers):这些容器按照元素的线性顺序存储数据,可以在其中插入、删除和访问元素。常见的序列容器包括:
-
算法(Algorithms):
sort
:对序列进行排序。find
:在序列中查找指定元素。for_each
:对序列中的每个元素执行指定操作。count
:计算序列中指定元素的数量。remove
:删除容器中的指定元素。
-
迭代器(Iterators): 用于遍历容器中的元素,提供了统一的接口。
-
函数对象(Functors): 可以像函数一样使用的对象,用于自定义算法的行为。
-
适配器(Adapters): 提供了容器和迭代器之间的适配器,如栈适配器(stack adapter)和队列适配器(queue adapter)。
17、C++文件编译与执行的四个阶段
- 预处理:根据文件中的预处理指令来修改源文件的内容
- 编译:编译成汇编代码
- 汇编:把汇编代码翻译成目标机器指令
- 链接:链接目标代码生成可执行程序
18、STL常用容器实现原理
- vector:
vector
是基于动态数组实现的。它在内存中分配一块连续的内存区域,当元素数量达到容量上限时,会重新分配更大的内存块,并将元素复制过去。 - list:
list
是双向链表的实现。每个元素都包含一个指向前一个元素和后一个元素的指针。 - set 和 map:
set
和map
通常使用平衡二叉搜索树(如红黑树)来实现,以保持元素的有序性和快速查找。 - unordered_set 和 unordered_map:
unordered_set
和unordered_map
使用哈希表作为底层数据结构,以提供快速的查找和插入操作。
19、STL中unordered_map和map的区别
- map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN),存在重复值。
- unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的,不存在重复值。
20、构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因
-
为什么构造函数不定义
- 构造函数的目的是创建对象并初始化其状态,包括分配资源、设置成员变量等。
- 在对象创建的过程中,虚函数表还没有建立,因此虚函数的机制不适用于构造函数。
-
为什么析构函数定义
- 析构函数的目的是在销毁对象时释放资源和清理状态。
- 当使用基类指针指向派生类对象时,如果基类的析构函数不是虚函数,那么在销毁对象时只会调用基类的析构函数,而不会调用派生类的析构函数。
- 这可能导致资源泄漏和不正确的清理。通过将析构函数声明为虚函数,可以确保在销毁对象时正确调用派生类的析构函数,以便释放资源和执行清理操作。
21、拷贝构造函数
- 拷贝构造函数是一种特殊的构造函数,用于创建一个对象并使其与另一个对象具有相同的数据状态。
- 它通常用于对象之间的赋值操作和传递对象作为函数参数时,确保新对象与源对象具有相同的数据。
- 编译器默认生成的拷贝构造函数通常执行浅拷贝,自定义拷贝构造函数可以用于执行深拷贝,以确保对象之间不共享相同的资源。拷贝构造函数对于维护对象独立性和避免资源泄漏非常重要。
22、野指针
野指针不是NULL指针,是未初始化或者未清零的指针
- 释放后未置空: 当你使用
delete
或free
释放动态分配的内存后,如果不将指针置为nullptr
(或NULL
),它可能仍然包含之前的内存地址,从而成为野指针。 - 超出作用域: 当指针指向的对象超出了其作用域(例如,局部变量在函数结束后),指针将成为野指针。
- 指针悬空: 当指针指向的对象已经被销毁或释放,但指针仍然在使用,导致它成为野指针。
23、vector和list的区别
- vector
vector
使用动态数组(array)作为底层数据结构,因此元素在内存中是连续存储的。这使得对元素的随机访问非常高效,但在插入和删除元素时可能涉及数据的移动。- 由于
vector
的元素是连续存储的,它通常会占用更少的内存,因为不需要额外的指针。
- list
list
使用双向链表作为底层数据结构,每个元素都包含了指向前一个和后一个元素的指针。这使得插入和删除元素非常高效,但随机访问需要遍历链表,效率相对较低。list
的元素需要额外的指针来维护链表结构,因此通常占用更多内存。
24、友元
- 友元函数
- 友元函数是一个不属于类的函数,被声明为某个类的友元,允许访问该类的私有成员。
- 友元类
- 友元类是一个类,被声明为另一个类的友元,允许友元类的成员函数访问该类的私有成员。
这些机制提供了更灵活的访问控制,但需要谨慎使用,以避免破坏类的封装性和安全性。
25、struct 和 class 区别
在C++中,struct
和class
都是用于定义用户自定义数据类型的关键字,它们有一些区别,主要涉及到成员的默认访问权限
- 默认访问权限:
struct
:成员的默认访问权限是public
,即结构体中的成员可以被外部访问。class
:成员的默认访问权限是private
,即类中的成员默认是私有的,无法被外部直接访问。
- 用途:
struct
:通常用于表示一组相关的数据,类似于记录,其成员通常是公有的,用于数据聚合。class
:通常用于表示一个抽象的数据类型,其成员可以包含数据和成员函数,用于封装数据和行为。
- 继承:
struct
:支持继承,可以派生自其他结构体或类。class
:同样支持继承,可以派生自其他类。
26、include头文件的“”和<>的区别
-
对于双引号包含的头文件,查找头文件默认为当前头文件目录下查找。
-
而尖括号查找头文件默认是编译器设置的头文件路径下查找
27、智能指针
智能指针是C++中的类模板,用于管理动态分配的内存资源。它们自动处理内存的分配和释放,从而减少内存泄漏的风险。
C++11引入了std::shared_ptr
和std::unique_ptr
std::shared_ptr
允许多个智能指针共享同一个对象,维护一个引用计数。当引用计数为零时,对象将被销毁。std::unique_ptr
代表独占所有权,只有一个智能指针可以拥有对象。它更轻量,因为不需要维护引用计数。
28、智能指针的性能如何
智能指针通常比原始指针稍微慢一些,因为它们需要额外的开销来管理引用计数等信息。但在现代C++中,这种性能差异通常可以忽略不计。
29、C++11新特性
- 范围
for
循环: 简化了遍历容器和数组的操作。 - Lambda表达式: 允许创建匿名函数,提供更灵活的函数定义方式。
- 智能指针:
std::shared_ptr
和std::unique_ptr
等用于更安全地管理动态内存。 - 移动语义和右值引用: 提高了对象的资源管理效率,减少了不必要的拷贝操作。
- 右值引用: 右值引用是一种新的引用类型,用双引号(&&)表示。它主要用于绑定到临时对象或可以被"移动"的对象,即将要销毁的对象。右值引用允许修改、移动或获取临时对象的内容。
- 移动语义: 移动语义是一种编程范式,它利用右值引用来实现更有效的资源管理。它允许在对象之间转移资源,而不是复制资源。例如,将一个临时对象的内容(右值)转移到另一个对象,而不是执行深拷贝操作。
- 标准库中的容器,如
std::vector
,已经实现了移动语义,使得容器的操作更加高效。
- 标准库中的容器,如
- …
30、TCP和UDP的区别及使用场景
TCP:
- 面向连接:需要建立连接,数据传输完成后关闭连接。
- 可靠性:保证数据不丢失、不乱序,提供数据完整性。
- 流量控制:控制数据发送速率,适应接收端处理能力。
- 面向字节流:数据被视为字节流。
- 适用于需要可靠传输和顺序传输的应用。
UDP:
- 无连接:无需建立连接,直接发送数据包。
- 不可靠性:不保证数据可靠性,数据可能丢失或乱序。
- 面向消息:以消息为单位发送和接收数据。
- 适用于实时性要求高、数据量较小、容忍数据丢失的应用。
TCP适用于需要可靠传输的应用,而UDP适用于实时性要求高、可以容忍数据丢失的应用。
31、单例模式
- 什么是单例模式?
- 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供全局访问点。
- 如何实现单例模式?
- 可以通过将构造函数私有化(private),并提供一个静态方法来获取唯一的实例。
- 什么是懒汉式和饿汉式单例?
- 懒汉式是在首次访问时创建单例实例,而饿汉式是在类加载时立即创建实例。
- 如何处理多线程下的单例?
- 可以使用互斥锁(mutex)来保证线程安全,或者使用双重检查锁(Double-Checked Locking)。
32、工厂模式
- 什么是工厂模式?
- 工厂模式是一种创建型设计模式,它提供了一种创建对象的接口,但允许子类决定实例化哪个类。
- 工厂模式的不同类型是什么?
- 主要有简单工厂、工厂方法和抽象工厂。简单工厂是通过一个工厂类创建不同类型的对象。工厂方法是将对象创建延迟到子类中。抽象工厂提供一个接口来创建一系列相关对象。
- 工厂模式和单例模式之间的关系?
- 工厂模式通常用于创建多个对象实例,而单例模式用于确保只有一个对象实例。
- 举例说明工厂模式的应用场景。
- 工厂模式常用于创建不同类型的数据库连接、日志记录器、UI控件等。
33、什么函数不能声明为虚函数
- 构造函数和析构函数: 构造函数(包括拷贝构造函数)不能声明为虚函数。虚函数的调用是在对象构造完成后才发生的,而构造函数的执行顺序是固定的,不允许虚函数的动态绑定。
- 静态成员函数: 静态成员函数属于类而不是对象,因此它们不能被声明为虚函数。虚函数是与对象实例相关的,而静态成员函数与类关联。
- 普通(非成员)函数: 普通的全局函数或类外的非成员函数不能声明为虚函数。虚函数必须属于类,通过类的成员函数来实现。
34、析构函数可以是纯虚的吗
- 析构函数可以声明为虚函数,以支持多态性和动态绑定,但不可以声明为纯虚函数。
35、C++中为什么用模板类
- 模板类允许编写通用的代码,适用于多种数据类型,提高代码的重用性和通用性。通过参数化类型,可以在编译时实现类型安全。这样可以减少冗余代码,支持STL,自定义数据类型,提高C++代码的灵活性和效率。
36、三次握手
三次握手是TCP协议中用于建立连接的过程
- 客户端向服务器发送一个连接请求(SYN)。
- 服务器收到请求后,回应一个确认(ACK)和自己的连接请求(SYN)。
- 客户端再次确认服务器的响应,向服务器发送确认(ACK)。
37、四次挥手
四次挥手是TCP协议中用于关闭连接的过程
- 客户端向服务器发送连接终止请求(FIN)。
- 服务器收到请求后,回应一个确认(ACK),表示已经准备好关闭连接,但仍可向客户端发送数据。
- 服务器完成数据发送后,向客户端发送连接终止请求(FIN)。
- 客户端收到请求后,回应一个确认(ACK),表示也准备好关闭连接。
38、extern “C”
extern "C"
是C++中用于与C语言代码进行交互的关键字,它告诉C++编译器按照C语言的规则处理函数名,以确保正确的链接和符号匹配。这通常用于混合编程,以确保C++代码能够与C代码协同工作。
39、拷贝构造函数在哪几种情况下会被调用
- 对象初始化,如用一个对象去初始化另一个对象。
- 对象作为函数参数传递给函数。
- 对象作为函数的返回值。
- 创建临时对象,例如在表达式中。
- 值传递,当对象通过值传递给函数时。
40、Get/Post的区别
- GET请求将数据附加到URL的查询字符串中,数据暴露在URL中,不适合传输敏感数据。POST请求将数据放在请求体中,数据不可见于URL,相对安全,适合传输敏感数据。
- GET请求对数据长度有限制,通常受浏览器或服务器的限制。POST请求通常没有固定的数据长度限制。
- GET请求可以被浏览器缓存,可以提高性能。POST请求通常不被缓存。
- GET用于浏览页面、请求资源、搜索等无副作用的操作。POST用于提交表单数据、上传文件、进行数据更新等有副作用的操作。
- …
41、http和https分别是什么?他们的区别是什么
HTTP和HTTPS都是用于在网络上传输数据的协议
- HTTP(Hypertext Transfer Protocol):
- 不安全,数据以明文传输,容易被窃听或篡改。
- 默认端口号是80。
- 性能较快,适用于一般网站。
- …
- HTTPS(Hypertext Transfer Protocol Secure):
- 安全,数据加密传输,难以被窃听或篡改。
- 默认端口号是443。
- 性能较慢,但随着硬件和加密算法的改进,影响逐渐减小。
- …
42、锁的实现方式
- 互斥锁(Mutex):互斥锁是最常见的锁,用于保护共享资源,确保在任何时刻只有一个线程可以进入临界区。它提供了线程安全的访问机制,但可能会引入线程切换的开销。
- std::mutex:
std::mutex
是C++标准库中的互斥锁,最常见的锁之一。它用于实现基本的线程互斥,确保同时只有一个线程可以访问临界区。你可以使用std::mutex
来保护共享资源,以确保线程安全。
- std::mutex:
- 读写锁(Read-Write Lock):读写锁允许多个线程并发读取共享资源,但在写操作时需要排他性访问。这提高了读取性能,适用于读多写少的场景。
- 自旋锁(Spinlock):自旋锁不会将线程挂起,而是会循环等待资源的可用性,适用于短暂的临界区。自旋锁避免了线程切换的开销,但需要小心避免忙等待时间过长,以免影响性能。
43、线程与进程的区别和联系
- 定义:进程是独立的执行实体,拥有独立的内存空间。线程是进程中的一个执行单元,共享相同的内存空间。
- 资源开销:进程之间的切换开销较大,线程之间的切换开销较小。
- 通信和同步:进程之间的通信需要特殊机制,如消息队列。线程之间可以共享内存,更容易实现通信和同步。
- 稳定性:进程之间的崩溃不影响其他进程,线程之间的崩溃可能影响整个进程。
44、树
1. 二叉树(Binary Tree):
- 每个节点最多有两个子节点,通常称为左子树和右子树。
2. 二叉搜索树(Binary Search Tree,BST):
- 对于每个节点,其左子树中的所有节点的值都小于它的值。
- 对于每个节点,其右子树中的所有节点的值都大于或等于它的值。
3. 平衡二叉树:
- 平衡二叉树是一种二叉搜索树,它确保左子树和右子树的高度差不超过1。
- AVL树是一种常见的平衡二叉树,它通过旋转操作来维护平衡。
4. B树和B+树:
- B树是一种自平衡的多路搜索树,B树的节点可以包含多个子节点,而不仅限于两个。
- B+树也是一种自平衡的多路搜索树,B+树中的非叶子节点不存储数据,只存储键值范围,数据只存储在叶子节点。
45、Twap算法
- TWAP算法是一种交易算法,用于执行大额交易订单,以平均价格的方式逐渐完成订单,以减少对市场价格的冲击。算法的核心思想是将整个订单分散在一段时间内均匀执行,以获得平均价格。这有助于降低对市场造成的影响,特别是在处理大额订单时。
46、红黑树和AVL树的定义,特点,以及二者区别
- 平衡要求: AVL树要求更加严格的平衡,确保左子树和右子树的高度差不超过1。而红黑树的平衡要求相对宽松一些,只要满足一些基本平衡性质即可。
- 节点结构: 红黑树的节点比AVL树的节点多一个颜色属性,通常用红色或黑色表示。这个颜色属性用于维护树的平衡性质。
- 旋转操作: 在维护平衡时,AVL树使用更多的旋转操作来保持平衡,而红黑树则使用颜色属性和少量的旋转操作。
- 性能: 由于AVL树的平衡要求更严格,在插入和删除操作时需要更多的旋转,因此在某些情况下性能可能略逊于红黑树。红黑树则在插入和删除操作上相对高效。
总的来说,AVL树提供了更严格的平衡性,但因此可能在某些操作上更慢。红黑树在平衡和性能之间取得了一种平衡,适用于广泛的应用场景。它们都是平衡二叉树。
47、Socket通信中的粘包和拆包问题是什么
- 粘包问题:粘包问题发生时,多个小消息被粘在一起发送,形成一个大消息。接收端需要额外的逻辑来分离和识别这些消息。例如,发送端发送"Hello"和"How are you?“,接收端可能会收到"HelloHow are you?”。接收端必须分解这个消息以获取原来的两个消息。
- 拆包问题:拆包问题是相反的情况。发送端发送一个大消息,但接收端以小块的方式接收它,这可能导致接收端无法正确重建原始消息。例如,发送端发送"HelloHow are you?“,接收端可能首先收到"Hello”,然后在稍后收到"How are you?"。接收端必须将这些碎片重组为原始消息。
48、函数指针,指针函数
- 函数指针是指向函数的指针,它可以用来动态选择并调用不同的函数。
- 指针函数是一个函数,其返回类型是指针。指针函数返回一个指针,该指针可以指向某个数据或对象。
49、指针数组,数组指针
-
数组指针是一个指向数组的指针。它指向整个数组,而不是数组中的单个元素。
-
指针数组是一个数组,其中的元素都是指针。
50、C++为什么用模板类
- 模板类在C++中用于实现泛型编程,提高了代码的重用性、类型安全性和灵活性。通过模板类,可以编写一次代码,适用于多种不同数据类型,提高了代码的灵活性和性能优化。
51、如何定义一个只能在堆上创建的类
- 提供一个静态工厂方法,返回值处提供一个类的指针,通过new关键字在new出一个对象就可以在堆上分配内存了
52、如何定义一个只能在栈上创建的类
- 可以通过删除类的
new
和delete
运算符或将它们设置为私有来禁用堆上的动态内存分配。此外,在函数中声明和使用该类的对象,以限制其生命周期仅在函数范围内,从而强制将其分配到栈上。这样可以确保对象不会在堆上创建。
53、内联函数有什么优点?内联函数和宏定义的区别
- 内联函数:
- 内联函数是由C++编译器处理的,提供了一种更安全、类型检查更严格的代码重用方法。
- 内联函数的代码会被嵌入到调用它的地方,避免了函数调用的开销。
- 内联函数通常由
inline
关键字定义 - …
- 宏定义:
- 宏定义是一种在预处理阶段进行文本替换的机制,不由编译器处理。
- 宏定义只是简单地将文本替换为宏定义的内容,没有类型安全和错误检查。
- 宏定义通常由
#define
指令定义。 - …
总之,内联函数提供了更安全、更可读且更易维护的代码重用方式,而宏定义提供了更灵活但更容易出错的文本替换机制。在C++中,应尽量使用内联函数以提高代码的可维护性和安全性。
54、常见的消息队列
- RabbitMQ: RabbitMQ是一个开源的消息代理软件,实现了高级消息排队、可靠性、弹性和可扩展性等特性。它支持多种协议,包括AMQP,STOMP,MQTT等,适用于多种应用场景。
- Kafka: Kafka是一个分布式流处理平台和消息代理,旨在处理高吞吐量、持久性和实时数据流。它常用于日志和事件流处理。
- **ActiveMQ:**ActiveMQ是一个基于JMS(Java消息服务)规范的消息代理,支持多种通信协议,包括OpenWire、AMQP、STOMP等。
- **RocketMQ:**RocketMQ是一个开源的分布式消息队列系统,特别适用于大规模的数据处理和实时分析。
55、RabbitMQ
RabbitMQ是一种消息队列中间件,它用于在分布式系统中实现异步通信和解耦组件之间的通信。
- 消息生产者和消费者: 不同的模块需要进行异步通信。使用RabbitMQ来实现消息的发布和订阅机制。生产者将消息发送到队列,而消费者从队列中接收和处理这些消息。
- 可靠性和持久性: RabbitMQ支持消息的持久性,这意味着即使在消息代理重启后,消息也不会丢失。确保了消息的可靠性传递,并配置了队列和交换机以确保消息的持久性。
- 消息路由和交换机: 使用不同类型的交换机,如直接交换机、主题交换机和扇出交换机,来定义消息的路由规则。这使我们能够将消息发送到多个队列或选择性地让消费者订阅消息。
- 集群和负载均衡: 为了确保高可用性和负载均衡,我们设置了RabbitMQ的集群。这确保了即使某个节点出现故障,系统也能继续工作。
- 消息序列化: 我们使用了不同的消息序列化格式,如JSON和Protocol Buffers,以满足不同消息的需求。