面试问题Q&A
现阶段问题还没有归类,等待更新
C++语言基础
Q C和C++两者优缺点,适合情况
C 语言特点:
优点:简洁、高效、可移植性强、面向过程、底层控制、资源利用高。
适用情况:嵌入式系统、操作系统、编译器、硬件驱动开发、对性能要求高的系统。
C++ 语言特点:
优点:面向对象、支持泛型编程、丰富的标准库、更高的抽象性、代码重用性高。
适用情况:大型项目开发、复杂软件系统、图形界面应用、游戏开发、科学计算、应用程序框架。
Q C++空类,编译器自动生成哪些函数?
当定义一个空类(无成员变量和成员函数)时,C++ 编译器可以自动生成特殊成员函数,有默认构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数和析构函数。这些函数被称为“合成函数”或“隐式函数”。
默认构造函数(Default Constructor):
当没有定义构造函数时,编译器会自动生成一个默认构造函数。它没有参数,并执行默认初始化。
拷贝构造函数(Copy Constructor):
编译器生成的拷贝构造函数用于按值传递、返回对象以及初始化新对象时。它会复制对象的成员变量值。
拷贝赋值函数(Copy Assignment Operator):
编译器生成的拷贝赋值函数用于对象之间的赋值操作。它会复制对象的成员变量值。
移动构造函数(Move Constructor):
如果没有显式定义移动构造函数,编译器也会生成默认的移动构造函数。它会将资源从一个对象“窃取”到另一个对象,通常用于支持移动语义。
移动赋值函数(Move Assignment Operator):
如果没有显式定义移动赋值函数,编译器也会生成默认的移动赋值函数。它会将资源从一个对象“窃取”到另一个对象,通常用于支持移动语义。
析构函数(Destructor):
如果没有定义析构函数,编译器会生成一个默认的析构函数,用于对象被销毁时清理资源。
示例:
#include <iostream>
class EmptyClass {
// 没有定义任何成员变量和成员函数
};
int main() {
// 实例化空类对象
EmptyClass obj1;
// 默认构造函数示例
EmptyClass obj2 = EmptyClass(); // 调用默认构造函数
// 拷贝构造函数示例
EmptyClass obj3 = obj1; // 调用拷贝构造函数
// 拷贝赋值函数示例
EmptyClass obj4;
obj4 = obj3; // 调用拷贝赋值函数
// 移动构造函数示例
EmptyClass obj5 = std::move(obj4); // 调用移动构造函数
// 移动赋值函数示例
EmptyClass obj6;
obj6 = std::move(obj5); // 调用移动赋值函数
return 0;
}
EmptyClass没有任何成员变量或成员函数,编译器生成默认构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数和析构函数会在示例中对对象进行实例化、赋值和销毁时自动调用。
Q 对指针,字符数组求sizeof
指针的 sizeof:
int* ptr;
std::cout << "Size of pointer: " << sizeof(ptr) << " bytes" << std::endl;
对于指针,sizeof 返回的是指针本身的大小,通常是存储地址的字节数。在 32 位系统上是 4 字节;在 64 位上是 8 字节。
字符数组的 sizeof:
char arr[] = "Hello";
std::cout << "Size of char array: " << sizeof(arr) << " bytes" << std::endl;
对于字符数组,sizeof 返回的是整个数组的大小,包括末尾的空字符 \0。在这个例子中,数组 "Hello" 共有 6 个字符(包括末尾的空字符 \0),因此 sizeof(arr) 的结果通常是 6 字节。
Q vector内存扩容机制
-
内存分配与扩容原理:
动态内存分配:vector 使用动态内存分配来存储元素,默认情况下会分配一块初始大小的内存(例如 8 个元素大小)来存储元素。
扩容:vector 中的元素数量超过当前内存容量时,需要进行内存扩容。它通常会分配一块更大的内存空间,将原有的元素复制到新的内存区域中。
-
内存扩容策略:
vector 有两个重要的概念:capacity(容量)和 size(大小)。capacity 表示当前已分配的内存空间大小,size 表示当前 vector 中实际存储的元素个数。
成倍增长:vector 在进行内存扩容时采用成倍增长的策略,即每次扩容分配的内存大小是原来容量的两倍(这是一种常见的实现方式,但并非所有编译器都采用相同的策略)。
内存重新分配:vector 需要扩容时,会申请一块新的内存空间,将原有元素复制到新的内存空间中,然后释放原有的内存空间。
-
实现示例:
#include <iostream> #include <vector> int main() { std::vector<int> vec; std::cout << "Initial capacity: " << vec.capacity() << std::endl; // 打印初始容量 for (int i = 0; i < 10; ++i) { vec.push_back(i); // 添加元素 std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl; // 打印当前大小和容量 } return 0; }
在这个示例中,vec 是一个 std::vector 对象。随着元素的不断添加,size() 函数可以获得当前 vector 中元素的个数,而 capacity() 函数可以获取当前 vector 的内存容量。你会发现 capacity 在 size 达到 capacity 时进行了扩容操作,并且容量通常是成倍增长的。
Q 实现vector的push_back()和remove()方法
创建一个动态数组,自己管理内存来模拟 vector 的函数:
#include <iostream>
template <typename T>
class MyVector {
private:
T* arr;
size_t capacity;
size_t size;
public:
MyVector() : arr(nullptr), capacity(0), size(0) {}
~MyVector() {
delete[] arr;
}
void push_back(const T& value) {
if (size >= capacity) {
if (capacity == 0) {
capacity = 1;
} else {
capacity *= 2;
}
T* new_arr = new T[capacity];
for (size_t i = 0; i < size; ++i) {
new_arr[i] = arr[i];
}
delete[] arr;
arr = new_arr;
}
arr[size++] = value;
}
void remove(const T& value) {
size_t index = 0;
for (; index < size; ++index) {
if (arr[index] == value) {
break;
}
}
if (index == size) {
std::cout << "Element not found in vector." << std::endl;
return;
}
for (size_t i = index; i < size - 1; ++i) {
arr[i] = arr[i + 1];
}
size--;
}
void print() const {
std::cout << "Vector elements: ";
for (size_t i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
};
int main() {
MyVector<int> vec;
vec.push_back(5);
vec.push_back(10);
vec.push_back(15);
vec.push_back(20);
vec.print();
vec.remove(10);
vec.print();
vec.remove(100); // Element not found in vector.
return 0;
}
Q 智能指针解决内存泄漏
智能指针用于管理动态分配的内存,并帮助防止内存泄漏和悬挂指针(dangling pointers)等问题。
智能指针的主要原理包括以下几个方面:
自动内存管理: 智能指针通过包装原始指针,将内存资源的所有权和释放责任交给智能指针对象。当智能指针对象超出范围或不再需要时,其析构函数会被调用,自动释放指向的内存资源。
引用计数(Reference Counting): 某些智能指针(如 std::shared_ptr)使用引用计数来跟踪指向某个对象的指针数量。每次创建智能指针时,引用计数增加;当智能指针被销毁时,引用计数减少。当引用计数为零时,代表没有任何指针指向该对象,智能指针会自动释放对象所占用的内存。
所有权转移: 智能指针可以通过移动语义(move semantics)或复制语义(copy semantics)实现所有权的转移。例如,使用 std::move() 可以将一个对象的所有权从一个智能指针转移到另一个智能指针,避免多个指针管理同一块内存导致的问题。
避免悬挂指针: 智能指针在对象被释放后,会将指针设置为 null 或者空指针,防止悬挂指针的产生。
智能指针通过封装原始指针,实现了自动内存管理和所有权管理,提高了内存安全性,避免了内存泄漏和悬挂指针等问题。常见的智能指针包括 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。
Q unique_ptr智能指针独占
-
std::unique_ptr 是 C++ 标准库提供的一种智能指针,具有独占式所有权(exclusive ownership)的特性,确保在任何时刻只有一个 unique_ptr 实例可以指向所管理的对象。
-
这种独占性使得 unique_ptr 在管理动态分配的资源时非常有用,可以避免资源的多重所有权和悬挂指针等问题。
-
std::unique_ptr 通过禁止复制操作、支持移动语义和析构函数释放资源的机制,保证了对动态分配资源的独占所有权,确保在任何时刻只有一个 unique_ptr 可以指向所管理的对象。
Q using关键字
在 C++ 中,using 关键字有多种用途和语法形式,主要用于命名空间的别名、类型别名和引入特定范围内的成员。
1.命名空间的别名:
// 命名空间的别名
namespace ns = some_namespace;
使用 using namespace 或 namespace ns = ... 可以创建一个新的命名空间别名,用于简化长命名空间的使用或提高代码的可读性。
2.类型别名:
// 类型别名
using new_name = existing_type;
using 可以用于创建类型别名,将已存在的类型命名为新的别名,提高代码的可读性和可维护性。
3.引入特定范围内的成员:
// 引入特定范围内的成员
using std::cout;
using std::endl;
using 可以用于引入特定范围内的成员,以便直接使用而无需在代码中指定完整的命名空间。
4.模板别名:
// 模板别名
template <typename T>
using my_container = std::vector<T>;
using 也可以用于创建模板别名,将模板实例化成新的别名,方便使用模板类。
Q move函数
在 C++11 及之后的标准中,std::move() 是一个位于 utility 头文件中的函数,用于将对象转移(移动)为右值引用,常用于移动语义的实现。
std::move() 的作用是将传入的对象转换为右值引用,表明对该对象的所有权可以被移动到其他对象中,而非复制。它并不实际搬迁数据,而是通过静态转换告诉编译器可以使用移动语义。移动语义允许在对象所有权转移时,避免深层拷贝,提高效率。
示例:
#include <utility>
#include <iostream>
#include <string>
int main() {
std::string str1 = "Hello, ";
std::string str2 = "world!";
// 使用 move 将 str2 转移给 str1
str1 = std::move(str2);
std::cout << "str1 after move: " << str1 << std::endl;
std::cout << "str2 after move: " << str2 << std::endl; // 注意:此处输出可能是空字符串或者未定义行为
return 0;
}
例中,std::move() 使得 str2 的所有权转移到 str1,因此在输出 str2 之后可能会得到一个空字符串或者未定义的行为,因为其数据已经被转移。
使用 std::move() 并不意味着对象的内容发生了移动,而是告诉编译器可以使用移动语义,以便更高效地处理对象的所有权转移。此函数主要用于支持移动语义,例如在容器元素的移动、资源管理类中实现移动构造函数和移动赋值操作符等场景。
Q gcc工作原理
GCC(GNU Compiler Collection)是一套由 GNU 开发的开源编译器集合,支持多种编程语言,包括 C、C++、Objective-C、Fortran、Ada 等。
预处理(Preprocessing): 首先,源代码经过预处理器处理,处理内容包括去除注释、宏展开、头文件包含等,生成一个经过预处理后的文件。
编译(Compilation): 预处理后的文件被送到编译器阶段,编译器将其翻译成汇编代码。在这个阶段,源代码被转换为特定硬件架构的汇编语言代码。
汇编(Assembly): 汇编器将汇编代码转换成机器可执行的二进制指令,即目标文件(Object File)。目标文件是二进制格式的文件,包含了编译后的代码、数据和符号表信息。
链接(Linking): 最后,链接器将目标文件与其他库文件(如标准库)合并,解析符号引用,生成最终的可执行文件或库文件。链接器会处理符号表,将不同目标文件中的函数、变量等符号引用解析为统一的地址,生成最终的可执行文件。
GCC 实现了多种优化、调试信息生成、不同平台的代码生成等功能,编译过程中会执行多个优化阶段,尝试提高生成代码的执行效率和性能。
Q 内存动态分配 i++和++i的区别
i++(后缀递增):
i++ 是一种后缀递增操作符。它先将 i 的当前值返回,然后再将 i 的值加一。
具体执行流程是,首先使用 i 的当前值进行表达式运算,然后将 i 的值加一。
int i = 5;
int a = i++; // a = 5, i = 6
++i(前缀递增):
++i 是一种前缀递增操作符。它先将 i 的值加一,然后再将新值返回。
具体执行流程是,首先将 i 的值加一,然后使用新值进行表达式运算。
int i = 5;
int a = ++i; // a = 6, i = 6
在实际编程中,这两者的主要区别在于返回值和递增顺序的不同。i++ 返回的是递增前的值,而 ++i 返回的是递增后的值。
Q 值传递与引用传递
值传递是将参数的实际值复制给函数的形式参数,函数内的操作不会影响原始值。
引用传递是将参数的地址传递给函数的形式参数,允许函数修改原始值。
值传递创建参数的副本,引用传递使用原始数据的引用,允许对原始数据进行更改。引用传递通常更高效,但可能意外地修改原始值。
Q const、define、static的区别
const:
const 是用来声明常量的关键字,指示变量的数值在程序执行期间不能被修改。
通过 const 关键字声明的变量一旦初始化后就不能再修改其值,可用于修饰变量、函数返回值、成员函数、指针等。
#define:
#define 是 C/C++ 预处理指令,用于创建宏定义,实现简单的文本替换。
通过 #define 可以创建常量、函数宏和条件编译指令等,它不是在编译器执行,而是在预处理阶段进行文本替换。
static:
static 是 C++ 中用于指定变量、函数或类成员的关键字,具有不同的用法。
对于全局变量,static 限制了其作用域,使其只在声明它的文件内可见,避免了与其他文件中相同名称的全局变量产生冲突。
对于局部变量,static 使得局部变量的生命周期延长至整个程序运行期间,而不是在函数调用时创建和销毁。
对于类成员,static 使得成员变量或成员函数属于整个类,而不是属于类的每个实例。
简单总结:
const 用于声明常量,防止变量被修改。
#define 用于创建宏定义,进行文本替换。
static 用于控制变量的作用域和生命周期,对于全局变量、局部变量和类成员有不同的含义和作用。
Q 简述几种软件设计模式
-
单例模式(Singleton Pattern):
确保一个类只有一个实例,并提供全局访问点。通过私有化构造函数,控制对象创建,并在需要时返回同一个实例,常用于需要唯一全局对象的场景。 -
观察者模式(Observer Pattern):
建立一种一对多的依赖关系,当一个对象状态发生变化时,所有依赖它的对象都会得到通知并自动更新。通过订阅/发布机制实现对象之间的解耦。 -
工厂模式(Factory Pattern):
在不暴露对象创建逻辑的情况下,创建对象的实例。通过定义一个共同的接口或基类,工厂模式根据客户端需求动态返回对应类的实例,提供更灵活的对象创建方式。
Q c++11特性解释
-
智能指针(Smart Pointers):
智能指针(如 std::shared_ptr 和 std::unique_ptr)管理动态分配的内存。它们提供自动内存管理,避免内存泄漏,确保资源安全释放。 -
Lambda 表达式(Lambda Expressions):
Lambda 表达式允许在函数内部定义匿名函数,简化代码和函数对象的编写,可用于实现更灵活的函数式编程。 -
auto 关键字(Type Inference):
使用 auto 关键字可以让编译器推断变量类型,减少类型重复和增加代码可读性,提高编码效率。 -
移动语义(Move Semantics):
通过 std::move() 实现资源的所有权转移,减少不必要的复制操作,提高性能和效率。 -
范围-based for 循环(Range-based for loop):
提供一种简洁方式来遍历容器、数组和其他类似序列的对象,使代码更加清晰易读。
Q vector、list、map、unordered_map使用场景
vector(动态数组):
场景:适用于需要随机访问、尾部插入或删除元素的场景。适合元素数量变化不频繁且需要快速访问的情况。
原理:连续的内存存储,支持随机访问,通过指针偏移来快速访问元素。
list(双向链表):
场景:适用于频繁插入和删除元素的场景。不支持随机访问,但在任意位置插入和删除元素效率高。
原理:由多个节点组成的链表结构,每个节点包含数据和指向前后节点的指针。
map(有序键值对容器):
场景:适用于需要键值对,并保持有序的场景。通过键快速查找值的应用场景。
原理:基于红黑树实现,保持元素有序,并提供快速的查找、插入和删除操作。
unordered_map(无序键值对容器):
场景:适用于不需要有序键值对的场景,但需要快速查找、插入和删除元素的情况。
原理:基于哈希表实现,通过哈希函数计算键的索引位置,提供快速的查找、插入和删除操作。
Q 深拷贝,浅拷贝
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在编程中用于复制对象或数据的两种不同方式。
区别:
-
浅拷贝(Shallow Copy):
浅拷贝只复制对象的地址,而不是复制对象本身。新对象和原对象共享同一块内存地址。
修改一个对象的内容会影响到另一个对象,因为它们指向相同的内存地址。class ShallowCopy { public: int* data; ShallowCopy(const ShallowCopy& source) { // 浅拷贝只复制指针,不分配新内存 data = source.data; } };
-
深拷贝(Deep Copy):
深拷贝会完全复制对象的内容,包括对象的值和指向的资源。新对象有自己独立的内存空间。
修改一个对象的内容不会影响到另一个对象,因为它们分别拥有独立的内存地址。class DeepCopy { public: int* data; DeepCopy(const DeepCopy& source) { // 深拷贝分配新的内存,并复制数据 data = new int; *data = *(source.data); } };
示例:
int main() {
int* arr = new int(5); // 创建动态分配的整数
ShallowCopy obj1;
obj1.data = arr;
ShallowCopy obj2 = obj1; // 浅拷贝
// 修改 obj1 的数据会影响到 obj2
*(obj1.data) = 10;
std::cout << *(obj2.data); // 输出结果为 10
// 释放内存
delete arr;
delete obj2.data;
return 0;
}
Q c++结构体内存对齐
成员对齐:
结构体的成员按照其自身的大小进行对齐,例如 char 通常是按照1字节对齐,int 可能是4字节对齐。
成员变量之间的间隔(填充字节)是为了保证每个成员变量的对齐。
对齐规则:
通常遵循最大成员对齐的原则,即结构体的对齐值等于其最大成员的大小。
如果结构体的大小不是最大成员大小的倍数,则会在最后一个成员之后填充字节以对齐整个结构体的大小。
struct MyStruct {
char a; // 大小为1字节
int b; // 大小为4字节
double c; // 大小为8字节
};
在上述示例中,MyStruct 结构体可能的对齐方式可能是:
char a; 按照1字节对齐,不需要填充字节。
int b; 通常按照4字节对齐,可能需要3字节填充字节。
double c; 通常按照8字节对齐,不需要填充字节。
因此,整个 MyStruct 结构体的大小可能是 16 字节(1 + 3(填充) + 4 + 8)。
结构体的内存对齐有助于提高程序运行效率,确保数据的正确对齐,避免因为对齐不当而导致的性能损失和内存访问错误。但不同的编译器和平台可能对结构体的内存对齐方式有所不同,可以使用编译器提供的特定的对齐指令进行控制。
Q 继承多态的理解
继承:
功能: 继承是面向对象编程中一种类与类之间的关系,允许一个类(子类/派生类)获取另一个类(父类/基类)的属性和方法。
特性:
子类可以继承父类的属性和方法,通过继承可以实现代码的重用和扩展。
子类可以通过覆盖(重写)父类的方法或添加新方法来改变或增强父类的行为。
提供了层次化的类结构,可以通过派生出新的类来组织和管理代码,增强了代码的可维护性和扩展性。
多态:
功能: 多态是指同一类方法在不同情况下具有不同的行为,是面向对象编程的一个重要特性。
特性:
允许不同类的对象对相同的消息(方法调用)做出不同的响应。
实现方式有静态多态(函数重载、运算符重载)和动态多态(基于继承的虚函数和接口)两种。
动态多态通过虚函数和继承实现,允许父类的指针或引用指向其子类的对象,在运行时根据对象的实际类型调用相应的方法。
Q const和static,inline和define
const 和 static 是 C++ 中的关键字,而 inline 和 #define 是用于定义宏和函数的预处理指令。
const 和 static 的区别:
const: 用于定义常量,表示一个变量在程序执行期间不可被修改。
声明常量:const int MAX_VALUE = 100;
在函数参数中表示不可修改:void func(const int x) {}
static: 用于修改变量和函数的作用域。
修改变量的生命周期:static int count = 0;,静态变量存储在静态存储区,生命周期延长到整个程序运行期间。
修改函数的作用域:static void myFunction() {},静态函数只在声明它的文件内可见,无法通过链接器进行外部链接。
inline 和 define 的区别:
inline: 用于定义内联函数,建议编译器在调用处展开函数代码。
内联函数:inline int add(int a, int b) { return a + b; }
编译器会尝试将函数调用处直接替换为函数体,节省了函数调用的开销,但并非一定会内联展开。
#define: 用于创建预处理宏,简单地进行文本替换。
宏定义:#define PI 3.14159
在预处理阶段直接将定义的宏名称替换为相应的文本,可能会导致代码可读性差和潜在错误,不进行类型检查。
主要区别:
const 和 static 是关键字,分别用于定义常量和修改变量/函数的作用域和生命周期。
inline 和 #define 是预处理指令,inline 建议编译器内联展开函数,而 #define 创建简单的文本替换宏定义。inline 保留了类型检查和作用域,而 #define 只是简单的文本替换,没有类型检查,可能引起潜在错误。
Q C++调试工具使用,gdb的命令
在 C++ 开发中,常用的调试工具包括 GDB(GNU Debugger)、LLDB、Visual Studio Debugger(Windows)、Xcode Debugger(macOS)等。
用法:
启动 GDB:
在命令行中输入 gdb 后加上可执行文件的路径:gdb ./executable_file。
设置断点:
break 或 b:设置断点。例如,break filename.cpp:line_number 在指定文件和行设置断点。
delete:删除断点。delete 后可以跟断点编号或 delete 所有断点。
运行程序:
run 或 r:运行程序。
程序执行控制:
continue 或 c:继续执行程序。
next 或 n:执行下一行代码,不进入函数内部。
step 或 s:执行下一行代码,如果是函数则进入函数内部。
查看变量和调用栈:
print 或 p:打印变量值。例如,print variable_name。
info locals:查看当前作用域的局部变量。
backtrace 或 bt:查看调用栈。
查看源代码:
list 或 l:显示源代码。例如,list function_name。
frame 或 f:切换帧,显示当前执行位置的源代码。
观察内存:
x:检查内存内容。例如,x/num_format address。
设置 Watchpoint:
watch:设置监视点,当指定变量被修改时中断程序执行。
退出 GDB:
quit 或 q:退出 GDB。
Q MFC/QT
MFC(Microsoft Foundation Class)和 Qt 是两种不同的 C++ 应用程序框架,用于图形用户界面(GUI)开发。它们有以下主要区别:
平台和开源性:
MFC 是微软开发的,主要用于 Windows 平台,是 Windows 平台下的主要 GUI 应用程序开发框架。
Qt 是由挪威的 Digia 公司开发并开源,是跨平台的,支持 Windows、Linux、macOS 等多个操作系统,可用于开发跨平台的应用程序。
语言:
MFC 主要基于 C++,但在 C++ 的基础上添加了一些 Microsoft 特有的扩展和类库。
Qt 使用标准 C++,但提供了自己的一套类库和工具,用于实现跨平台的 GUI 应用程序。
功能和组件:
MFC 提供了许多 Windows 平台的原生控件和功能,但跨平台能力有限。
Qt 提供了丰富的控件库和功能,包括 GUI 控件、网络、多线程、XML 解析等,具有更广泛的跨平台能力和功能性。
网络编程
Q c++网络编程socket通信
-
Socket 是一种抽象层,提供了对网络通信的接口,通常位于传输层。它允许应用程序通过网络发送和接收数据。Socket 可以在不同层次的网络协议栈上实现,但通常在传输层上进行操作。
-
在 C++ 中,使用 socket 进行网络编程意味着你可以创建一个套接字(socket),然后将其绑定到特定的端口上以便监听和处理传入的连接请求,或者将其连接到远程主机的特定端口以进行通信。
-
Socket 本身不直接与 HTTP 通信,因为 HTTP 是一个应用层协议,而 socket 位于更低的传输层。但是,可以使用 socket 在传输层上建立连接,然后在连接上发送 HTTP 请求和接收 HTTP 响应。在实际编程中,常见的是使用类库(如 libcurl、Boost.Asio 等)来简化基于 socket 的网络编程,这些库提供了更高层次的抽象和更易用的 API。
-
接收数据的层次通常仍然是通过 socket 来实现,通过读取来自网络的数据流。在使用 socket 进行网络通信时,接收数据的过程通常涉及使用 socket API 中的 recv() 或类似函数来接收数据。数据到达时,它会被接收到 socket 缓冲区,应用程序可以从缓冲区中读取数据进行处理。
操作系统
Q reactor和proactor区别
Reactor 和 Proactor 都是用于处理异步 I/O 的设计模式,通常用于构建高性能的并发网络应用程序,在处理异步 I/O 时有一些不同之处:
-
Reactor 模式:
Reactor 模式基于事件驱动,其核心思想是一个事件循环(event loop)。在这个模式下,应用程序将 I/O 操作(如读取、写入等)交给一个中央事件处理器(称为 Reactor)处理。Reactor 负责监视所有的 I/O 事件,并分发这些事件到合适的处理程序(又称为事件处理器或回调)进行处理。
在 Reactor 模式中,当一个 I/O 操作完成时,应用程序必须主动询问操作是否已经完成,然后才能继续进行下一步操作。 -
Proactor 模式:
Proactor 模式也是一种事件驱动模式,但它更进一步地封装了异步操作。在 Proactor 模式中,应用程序发出一个异步操作请求(如读取或写入),然后继续进行其他工作。操作完成后,系统会通知应用程序,告知操作已完成,可以处理结果了。
这意味着 Proactor 模式下,应用程序发起了一个异步操作后,可以继续执行其他任务,操作完成后会接收到通知,不需要显式地等待操作完成。
Reactor 和 Proactor 都是处理异步 I/O 操作的模式,但它们的主要区别在于异步操作的处理方式:Reactor 模式需要应用程序主动查询操作是否完成,而 Proactor 模式则是在操作完成后异步通知应用程序。
Q 为什么要有主线程/从线程
原因:
-
并发处理: 多线程允许程序同时执行多个任务,这样可以充分利用多核处理器的性能优势,提高程序的执行效率。主线程通常是程序的起点,而子线程则是由主线程创建的辅助线程,用于执行一些独立的任务。
-
任务分解: 主线程可以将任务分配给多个子线程来同时执行,从而提高整体的处理能力。这样可以将复杂的任务分解成多个小的子任务,每个子线程负责执行其中一个子任务。
-
提高响应性: 在一些需要同时进行多项工作的情况下,使用多线程可以保持程序的响应性。例如,在图形用户界面(GUI)应用中,主线程用于处理用户交互事件,而子线程可以用于执行耗时的计算或I/O操作,以避免阻塞主线程导致界面卡顿。
-
并发控制: 多线程也为并发控制提供了一种方式。通过使用线程同步机制(如互斥锁、信号量、条件变量等),可以确保多个线程安全地访问共享资源,避免出现竞态条件(Race Condition)等并发问题。
-
任务分配和负载均衡: 在一些需要大量计算的场景下,可以将任务分配到多个子线程上,以实现负载均衡,从而更好地利用系统资源。
主线程和子线程的存在可以提高程序的并发性、响应性和效率,允许程序同时执行多个任务,从而更好地利用计算机系统的资源。
Q 有限状态机
有限状态机(Finite State Machine,FSM)是一种抽象的计算模型,用于描述对象或系统在不同状态之间的转换和行为。它可以用来建模和解决许多问题,从硬件设计到软件开发都有广泛的应用。
描述:
有限状态机由一组状态、事件和转换组成:
状态(State): 系统或对象可以处于的某个状态。例如,对于一个自动售货机,可能有“待命”、“选择商品”、“投币”、“出售商品”等状态。
事件(Event): 引起状态转换的触发器。例如,用户投入硬币、选择商品等。
转换(Transition): 由事件触发引起的状态变化。从一个状态到另一个状态的跃迁。当某个事件发生时,有限状态机会根据当前状态执行相应的动作,并可能转移到另一个状态。
原理:
有限状态机可以通过不同的表示方式实现,包括状态图(State Diagram)、状态表(State Table)、状态转换图等。其中,状态图通常用图形化形式展示状态之间的转换关系,以状态节点和转换箭头的形式展示。
有限状态机可以分为两种主要类型:
确定性有限状态机(Deterministic Finite Automaton,DFA): 对于给定的输入,只有一种可能的转换路径。在任何给定时间,DFA只处于一个状态。
非确定性有限状态机(Nondeterministic Finite Automaton,NFA): 对于给定的输入,可能有多种转换路径。在任何给定时间,NFA可以处于多个状态。
有限状态机有多种应用:
-
模型建模: 用于描述对象或系统的行为和状态转换,例如计算机网络通信协议、编译器的词法分析和语法分析、控制系统等。
-
软件开发: 在软件开发中,有限状态机可以用于实现状态驱动的逻辑,例如游戏开发中的角色状态、自动机器人、工作流程等。
-
硬件设计: 在数字电路设计中,有限状态机被广泛用于描述电路的控制逻辑。
-
行为设计: 用于描述和实现系统的特定行为模式,例如状态模式、行为树等。
有限状态机可以帮助理解和描述系统的行为,提供了清晰且有效的方式来管理和控制复杂系统的状态和转换。
Q select,poll,epoll区别
select、poll 和 epoll 都是用于 I/O 多路复用的机制,允许在单个线程中处理多个 I/O 事件:
select:
select 是最古老的多路复用机制,适用于所有平台。
有文件描述符数量限制,一般为 1024。
每次调用 select 都需要将文件描述符集合从用户态拷贝到内核态,效率可能较低。
对文件描述符进行线性扫描,当文件描述符数量较多时性能下降明显。
poll:
poll 也是一种多路复用机制,不同于 select,没有文件描述符数量限制。
每次调用 poll 需要将文件描述符集合拷贝到内核态,效率与文件描述符数量成线性关系。
epoll:
epoll 是 Linux 特有的高性能多路复用机制。
使用事件通知的方式避免了 select 和 poll 中的线性扫描问题,可以监听大量文件描述符。
epoll 使用基于事件的就绪通知,只返回已就绪的文件描述符,效率更高。
支持水平触发(LT)和边缘触发(ET)两种模式。
epoll 在处理大量文件描述符时性能更好,避免了 select 和 poll 中的性能瓶颈问题,特别适用于高并发的网络应用。但epoll 只能在 Linux 环境下使用。select 和 poll 有更广泛的跨平台兼容性。
Q 进程与线程,进程状态以及转换
进程与线程的区别:
进程(Process):
独立执行的程序实例,有独立的地址空间,包含代码、数据和资源。
拥有独立的堆和栈,进程间通信开销大。
资源分配由操作系统负责,进程间相互独立。
线程(Thread):
进程的一部分,与同一进程的其他线程共享地址空间和资源。
共享相同的堆和栈,线程间通信开销小。
线程被视为轻量级进程,创建销毁速度快,切换开销小。
进程的状态及转换:
在典型的操作系统中,进程通常经历以下几种状态:
创建(Created): 进程正在创建中,尚未被调度执行。
就绪(Ready): 进程已准备好执行,等待分配 CPU 时间片。
运行(Running): 进程正在 CPU 上执行。
阻塞(Blocked): 进程暂时无法执行,等待某种事件发生,如等待 I/O 完成或资源可用。
终止(Terminated): 进程执行完毕或被手动终止。
进程在这些状态之间转换,通常的转换关系为:
创建状态 -> 就绪状态:进程被创建后,准备好执行。
就绪状态 -> 运行状态:进程被分配 CPU 时间片并执行。
运行状态 -> 就绪状态:时间片用完或等待事件发生。
运行状态 -> 阻塞状态:等待某些事件的完成或资源的释放。
阻塞状态 -> 就绪状态:等待的事件发生或资源可用。
Q 孤儿进程,僵尸进程,守护进程
孤儿进程(Orphan Process):
当父进程结束或者意外终止时,子进程可能成为孤儿进程。孤儿进程将由操作系统的进程管理模块接管,它们的父进程会被系统指定为 init 进程(PID 为1)。
孤儿进程不再有父进程,因此由操作系统负责收养和管理。
僵尸进程(Zombie Process):
当一个进程执行完成后,其父进程却没有调用 wait() 或 waitpid() 等系统调用来获取子进程的退出状态,子进程的进程描述符依然保留在系统中,此时子进程成为僵尸进程。
僵尸进程不占用内存资源,但会占用进程号和部分系统资源,应当由父进程调用 wait() 等来释放僵尸进程。
守护进程(Daemon Process):
在 UNIX 或类 UNIX 系统中运行的一种特殊进程,通常在后台运行,没有控制终端。
守护进程通常被设计用来执行特定的系统任务,例如监听服务请求、周期性任务等。它们通常在系统启动时启动,并一直运行直到系统关闭。
Q 进程间通信方式
进程间通信(Inter-Process Communication,IPC)是指不同进程之间交换数据或信息的机制。在操作系统中,有多种方式实现进程间通信:
管道(Pipes):
单向通信管道,通常用于具有亲缘关系的父子进程间通信。
包括匿名管道(只能在相关进程间使用)和命名管道(允许无关的进程之间通信)。
消息队列(Message Queues):
通过消息队列传递数据,允许进程按照一定顺序收发消息。
提供了一种异步通信方式,不同进程之间通过消息传递进行通信。
共享内存(Shared Memory):
允许多个进程访问同一块物理内存,因此进程间的数据共享效率高。
需要额外的同步机制来避免数据冲突和一致性问题。
信号量(Semaphores):
用于进程间同步和互斥的机制,可用于控制多个进程对共享资源的访问。
通过对信号量进行操作来实现对共享资源的访问控制。
套接字(Sockets):
在网络编程中常用,也可以在本地进程间进行通信。
提供了一种灵活、通用的进程间通信机制,允许通过网络协议或本地接口进行数据交换。
Q 死锁
死锁(Deadlock)是指在多个进程或线程中,每个进程/线程都在等待其他进程/线程释放资源或者完成某个动作,导致所有进程/线程都无法继续执行,从而形成一种僵持状态的现象。
死锁产生的条件通常包括以下四个要素,也称为死锁的必要条件:
互斥条件(Mutual Exclusion): 至少有一个资源是独占的,一次只能被一个进程/线程占用。
不可抢占条件(Hold and Wait): 进程/线程持有至少一个资源,并且在等待获取其他进程/线程持有的资源。
循环等待条件(Circular Wait): 进程/线程之间存在一种循环等待资源的关系,即进程/线程之间形成一个环路,每个进程/线程都在等待下一个进程/线程所持有的资源。
资源不足条件(Resource Depletion): 资源无法无限地分配,可能会出现多个进程/线程因为无法获取所需的资源而等待,从而形成死锁。
死锁的产生是因为进程/线程在资源竞争时出现相互等待的情况,每个进程/线程都在等待其他进程/线程释放资源,导致所有进程/线程都无法继续执行下去。
为了预防死锁,可以采用以下方法:
破坏死锁产生的必要条件:
确保资源的互斥性不会导致死锁。
避免进程持有一个资源时继续等待其他资源。
尽量避免循环等待。
死锁检测和解除:
检测系统中的死锁情况,并采取相应的措施解除死锁。
采用超时、资源剥夺等方式解除死锁。
资源分配策略:
预先分配资源,避免在执行过程中产生死锁。
使用银行家算法等资源分配算法进行资源管理。
Q 乐观锁/悲观锁实现
悲观锁是一种并发控制的机制,它假设在并发访问时会发生冲突,因此在进行读写操作前,会直接对共享资源进行加锁,以防止其他线程同时访问,保证数据的一致性。常见的实现方式包括:
互斥锁(Mutex):
使用互斥锁来实现悲观锁,通过锁定共享资源来确保同一时间只有一个线程可以访问该资源。
在读写操作前,使用互斥锁进行加锁操作,访问结束后释放锁。
#include <mutex>
std::mutex mtx;
void criticalSection() {
mtx.lock(); // 加锁
// 访问共享资源的代码
mtx.unlock(); // 解锁
}
读写锁(Read-Write Lock):
读写锁允许多个线程同时读取共享资源,但当有写操作时需要互斥独占。
读操作时使用读锁,写操作时使用写锁,读锁与写锁之间互斥。
#include <shared_mutex>
std::shared_mutex rwMutex;
void readOperation() {
std::shared_lock<std::shared_mutex> lock(rwMutex); // 读锁
// 读取共享资源的代码
}
void writeOperation() {
std::unique_lock<std::shared_mutex> lock(rwMutex); // 写锁
// 写入共享资源的代码
}
悲观锁在操作共享资源时会频繁加锁和解锁,可能导致线程阻塞等待锁释放,降低系统的并发性能。因此,在使用悲观锁时需要注意锁的粒度和加锁时间,尽量减少锁的持有时间,避免长时间阻塞其他线程的访问。
悲观锁适用于对数据更新频繁、对数据一致性要求高的场景,能够确保数据在任何时刻都是正确的,但也会带来一定的性能开销。
乐观锁是一种并发控制的机制,它假设在大多数情况下,不会发生并发冲突,因此在进行并发操作时,不会立即对共享资源进行加锁,而是在更新操作前进行一次检查。常见的实现方式包括以下几种:
版本号或时间戳:
给数据行增加一个版本号或时间戳字段,每次更新数据时增加版本号或更新时间戳。
在进行更新操作前,先检查当前数据行的版本号或时间戳是否与自己持有的相同,若相同则允许更新,否则认为数据已经被其他进程修改,需要回滚或进行其他处理。
CAS(Compare and Swap)操作:
使用原子操作进行比较并替换的方式。例如,在多线程环境下,使用 compare_exchange_weak() 或 compare_exchange_strong() 等原子操作来比较当前值和预期值,若相等则替换为新值。
在乐观锁中,先读取当前值并保存,然后尝试使用 CAS 操作进行更新,如果比较并替换的操作失败,表示其他线程已经修改了数据,需要进行相应的处理。
乐观锁通常适用于读操作远远多于写操作的情况,因为它不会立即加锁,可以减少锁的争用,提高并发性能。但是,乐观锁在更新操作时需要做额外的版本号或时间戳检查或CAS操作,如果并发冲突较为频繁,可能导致不断的重试,增加了系统的开销。
悲观锁假设会有并发冲突,直接对共享资源进行加锁,以确保数据的一致性,适用于写操作频繁的场景。乐观锁则更适用于读操作较多、并发冲突较少的场景。
Q 并发/并行
"并发"和"并行"是与多任务处理相关的概念,但有着不同的含义:
并发(Concurrency):
指的是系统同时处理多个任务的能力,这些任务可能在一段时间内交替执行,共享系统资源。并发并不一定意味着同时执行,而是在一段时间内有多个任务在执行。
示例:多个程序同时运行,操作系统轮流分配 CPU 时间片给它们,看起来好像是同时运行的。
并行(Parallelism):
指的是系统同时执行多个任务的能力,这些任务真正同时进行,每个任务在不同的处理器核心或计算单元上执行。
示例:多个线程同时在多个处理器核心上执行不同的任务,每个任务都在同一时刻进行。
"并发"强调的是任务的交替执行,多个任务在同一时间段内交替执行,而"并行"强调的是同时执行,多个任务在同一时刻同时进行。
在实际应用中,通过并发和并行可以提高系统的性能和响应能力,但并发和并行的实现需要考虑到系统的硬件架构、资源管理、线程同步和通信等问题。
数据库
Q 数据库四大特性,四种隔离安全机制
数据库的四大特性是:
原子性(Atomicity): 原子性指的是事务中的所有操作要么全部成功执行,要么全部失败回滚,不存在部分执行的情况。在事务执行过程中,如果发生错误或者异常,会回滚到事务开始前的状态,保证数据的一致性。
一致性(Consistency): 一致性指的是事务执行前后,数据库从一个一致性状态转变到另一个一致性状态,不会破坏数据库的完整性约束和业务规则。即使在发生错误或异常时,数据库也能保持数据的完整性。
隔离性(Isolation): 隔离性指的是多个事务并发执行时,每个事务的操作应该与其他事务隔离,互不干扰。事务之间的执行应该相互独立,避免互相影响,以确保数据的完整性和一致性。
持久性(Durability): 持久性指的是一旦事务被提交,其对数据库中数据的修改就是永久性的,即使系统发生故障或重启,修改的数据也不会丢失。
四种隔离安全机制(隔离级别)是:
读未提交(Read Uncommitted): 允许一个事务读取另一个未提交事务的数据。这种隔离级别存在脏读(Dirty Read)问题,可能导致读取到未提交事务的数据,影响数据的一致性。
读提交(Read Committed): 允许事务只能读取到已经提交的数据。解决了脏读问题,但依然存在不可重复读(Non-Repeatable Read)问题,即在一个事务内,多次读取同一数据可能得到不同结果。
可重复读(Repeatable Read): 在一个事务内多次读取同一数据时,保证多次读取结果一致。解决了不可重复读问题,但依然存在幻读(Phantom Read)问题,即在一个事务内多次读取同一范围的数据可能出现新增或删除的情况。
串行化(Serializable): 最高的隔离级别,确保每个事务都独立执行,彼此之间没有交叉。避免了脏读、不可重复读和幻读的问题,但效率较低,因为串行化执行会导致并发性能下降。
Q MySQL引擎/数据库分类
MySQL 提供了多种存储引擎(Storage Engine),不同的存储引擎拥有各自的特性和适用场景。常见的 MySQL 存储引擎包括:
InnoDB:
默认的 MySQL 存储引擎,支持事务和行级锁定,提供了较高的数据完整性和并发性能。
适用于大多数应用场景,特别是需要事务支持和较好的数据一致性的应用。
MyISAM:
不支持事务和行级锁定,但提供了较高的性能和压缩性。
适用于读操作频繁、写操作相对较少的应用场景,如数据仓库和只读数据。
MEMORY:
将表存储在内存中,提供了快速的读写速度,但数据不是持久化的。
适用于需要快速访问和临时存储数据的应用,但不适用于大量数据或长期存储。
NDB Cluster:
适用于集群环境,提供了高可用性、高扩展性和分布式存储能力。
适用于大型数据的高可用集群架构,例如需要高性能和大容量的数据存储。
数据库通常可以根据其应用和功能进行分类:
关系型数据库(RDBMS):
数据以表格的形式组织,采用 SQL(Structured Query Language)作为查询语言,例如 MySQL、PostgreSQL、Oracle 等。
非关系型数据库(NoSQL):
数据不以关系模型(表格)存储,而是以键值对、文档、图形或列族等形式存储,例如 MongoDB、Redis、Cassandra 等。
面向对象数据库(OODBMS):
数据以对象的形式进行存储和操作,直接支持面向对象的概念,例如 ObjectDB、db4o 等。
计算机网络
Q TCP/UDP的选择
- 选择 TCP:对于需要可靠数据传输和确保数据顺序的应用,如文件传输、网页访问。TCP提供错误检测、流量控制和重传机制。
- 选择 UDP:对于实时性要求高、对数据传输延迟要求较低的应用,如音视频流、在线游戏。UDP传输速度快且没有连接的开销,但不保证数据可靠性和顺序性。
Q 3次握手,4次挥手
3 次握手(建立连接):
客户端发送 SYN: 客户端向服务器发送一个标志位为 SYN(同步序列编号)的数据包,表示请求建立连接。客户端将自己的初始序列号(sequence number)设定为一个随机数 A。
服务器回应 SYN + ACK: 服务器收到 SYN 数据包后,如果同意建立连接,则会发送一个带有 SYN/ACK 标志的数据包作为响应,表示确认客户端的请求,并表示自己也准备好了。服务器会将自己的初始序列号设定为另一个随机数 B,并确认收到了客户端发来的 SYN(确认号为 A+1)。
客户端发送 ACK: 客户端收到服务器的 SYN/ACK 数据包后,向服务器发送一个确认报文,确认收到服务器的响应。这个确认报文的序列号设定为 A+1,确认号为 B+1。连接建立完成,双方可以开始进行数据传输。
4 次挥手(终止连接):
客户端发送 FIN: 当客户端想要关闭连接时,它发送一个 FIN(终止连接)标志的数据包给服务器,表示客户端不再发送数据。客户端进入 FIN_WAIT_1 状态。
服务器回应 ACK: 服务器收到客户端的 FIN 后,会发送一个 ACK 确认报文,表示收到了客户端的请求,但仍然允许数据传输。服务器进入 CLOSE_WAIT 状态。
服务器发送 FIN: 当服务器确定数据传输结束后,会发送一个 FIN 给客户端,表示服务器也准备关闭连接。服务器进入 LAST_ACK 状态。
客户端回应 ACK: 客户端收到服务器的 FIN 后,发送一个确认报文,表示已经收到了服务器的关闭请求。客户端进入 TIME_WAIT 状态,等待一段时间后才会关闭连接。服务器收到这个 ACK 后,关闭连接,进入 CLOSED 状态。
-
避免重复建立连接: 三次握手确保客户端和服务器之间的通信双方都愿意建立连接,并且防止因网络延迟或数据包丢失等原因导致的重复连接建立。
-
避免半开连接的产生: 三次握手确保双方的序列号和确认号同步,确保连接的可靠建立。四次挥手中的最后一个 ACK 确认了双方都已经完成了数据传输,避免了半开连接状态的出现。
-
确保数据的可靠传输: TCP 的三次握手确保了双方都已经同步了初始序列号和确认号,建立了可靠的连接后再进行数据传输,保证数据的可靠性和完整性。
-
允许双方有足够时间准备和关闭连接: 四次挥手中的 ACK 和 FIN 的交换允许双方在关闭连接前完成数据的传输和接收,并通知对方自己已经准备好关闭连接,避免了数据丢失和不完整的情况。
Q 计算机网络TCP分包和解包
TCP 分包过程:
数据块划分: 应用层的数据被划分为适当大小的数据块(段),以便在网络上传输。
添加首部信息: 每个数据块被加上 TCP 首部信息(包括序列号、确认号、控制标志等),形成 TCP 段。TCP 首部信息中包含了控制信息,用于管理传输过程和数据重组。
发送数据: 段被发送到网络传输层(TCP 层),通过网络传输到目的地。
TCP 解包过程:
接收数据: 接收端收到传输过来的 TCP 段。
重新组装数据: 接收端的 TCP 层根据序列号和确认号等信息,将接收到的 TCP 段按序重新组装成完整的数据块。
传递给应用层: 当 TCP 接收到所有的数据块并按顺序重新组装后,数据被传递给接收端的应用层处理。
Q http/https的区别
HTTP(Hypertext Transfer Protocol)和 HTTPS(Hypertext Transfer Protocol Secure)
安全性:
HTTP 不加密数据传输,信息以明文形式传输,容易被窃听和篡改。
HTTPS 则通过 SSL/TLS 协议对数据进行加密传输,确保数据传输的安全性和隐私性。
加密方式:
HTTP 使用 TCP(端口号80)作为传输协议,数据传输是明文的。
HTTPS 在 HTTP 的基础上使用了 SSL/TLS 协议(通常使用端口443),通过公钥加密传输,保证了数据的机密性和完整性。
证书认证:
HTTPS 使用了数字证书对通信双方进行认证,验证服务器的身份,防止中间人攻击,确保通信安全。
HTTP 不涉及数字证书的验证,无法验证通信双方的真实身份,存在安全风险。
连接方式:
HTTP 是无状态的协议,每个请求和响应之间相互独立,不保留状态信息。
HTTPS 与 HTTP 相同,也是无状态的,每个请求和响应之间相互独立。
HTTPS 是在 HTTP 的基础上加入了 SSL/TLS 加密机制的安全版本。HTTPS 可以保护数据的隐私性和完整性,防止窃听和篡改,适用于对数据传输安全性有要求的场景,如网上支付、登录、个人隐私信息传输等。
Q 七层网络参考模型
七层网络参考模型指的是 OSI(Open Systems Interconnection)参考模型,它将计算机网络体系结构划分为七个抽象层,每个层都有特定的功能和责任,用于描述网络中数据的传输和处理过程。这些层从底层到顶层依次为:
物理层(Physical Layer):
最底层的层级,负责在物理介质上传输原始比特流,定义了数据传输所使用的物理媒介和接口标准,如电压、光信号等。
数据链路层(Data Link Layer):
负责点对点之间的可靠数据传输,处理帧的传输、错误检测和纠错,定义了数据的帧格式和链路控制协议。
网络层(Network Layer):
提供了数据包在不同网络间的路由和转发功能,负责网络间的数据传输,处理数据包的寻址和路由选择。
传输层(Transport Layer):
提供端到端的通信服务,负责数据的可靠传输和错误恢复,处理数据流的分段、重组和流量控制,例如 TCP 和 UDP 协议。
会话层(Session Layer):
管理不同应用程序之间的对话和通信会话,确保数据传输的安全和一致性。
表示层(Presentation Layer):
负责数据的格式化、编码和加密,确保数据格式在不同系统之间的兼容性和可识别性。
应用层(Application Layer):
最高层的层级,提供各种应用程序和用户接口,向用户提供特定的网络服务,如 HTTP、FTP、SMTP 等。
数据结构与算法
Q c++优先队列实现
C++ 标准库中的 std::priority_queue 是一个基于堆(Heap)数据结构实现的优先队列。
默认情况下,std::priority_queue 使用 std::vector 作为底层容器,并使用默认的 std::less 比较器来实现最大堆(Max Heap)。最大堆的特性是根节点的值大于其子节点的值,因此在最大堆中,队首元素(优先级最高)是整个队列中最大的元素。
优先队列的实现基于堆的性质,支持插入和弹出操作,并保持队列中的元素是按照优先级顺序进行排列的。
标签:std,函数,c++,面试,开发,线程,内存,进程,指针 From: https://www.cnblogs.com/stuBoo/p/17963380未完待续