- 1、对区块链的了解
- 2、对区块链有哪些了解(POW和POS)
- 3、是否了解redis中的某些数据结构(ziplist、Hash表)
- 4、协程与线程的区别与联系(针对计算密集型和数据密集型两种不同的场景来回答)
- 5、数据库和缓存的不一致性问题如何解决(老生常谈了)
- 6、C 中为什么父类要定义虚析构函数(可能看我不太懂C ,问了个奇怪问题)
- 7、C 14、17、20新特性有了解吗
- 8、C 中shared_ptr和unique_ptr的区别与联系(unique_ptr只有一个,shared_ptr可以共享)
- 9、C 如何自己定义shared_ptr类型(移动构造函数、拷贝构造函数、析构函数等)
- 10、auto自动推导类型有哪些机制
- 11、编程题:给定一个链表,反转left到right的部分
1、对区块链的了解
区块链是一种分布式数据库,它由一系列按照时间顺序排列的数据块组成,并采用密码学方式保证不可篡改和不可伪造。区块链技术最初起源于比特币,作为比特币的底层技术,用于去中心化和去信任地维护一个可靠的数据库。相比于传统的网络,区块链具有数据难以篡改和去中心化的两大核心特点,使得区块链所记录的信息更加真实可靠,并能够解决人们互不信任的问题。
区块链技术可以从金融会计的角度看作是一种分布式开放性去中心化的大型网络记账簿,任何人都可以使用相同的技术标准加入自己的信息,持续满足各种需求带来的数据录入需要。在金融领域,区块链技术可以提高交易速度、降低交易成本、增强交易的透明度和安全性,以及提供更加灵活的智能合约等功能。
除了金融领域,区块链技术还可以应用于其他领域。例如,可以利用区块链技术构建去中心化的身份认证系统,保证个人数据的隐私和安全。此外,区块链技术也可以用于物联网、供应链管理、数字版权等领域。
然而,区块链技术也存在一些问题和挑战。例如,区块链技术的可扩展性和效率问题一直是技术社区探索的关键话题。同时,由于区块链技术的匿名性和去中心化特点,也使得监管和合规方面存在一定的难度。
2、对区块链有哪些了解(POW和POS)
区块链技术中,POW和POS是两种重要的共识机制。
POW,也称为工作量证明,是区块链网络中最早的共识机制之一。在POW模式下,网络中的节点需要解决一个复杂的数学难题,这个难题需要大量的计算能力和能源。因此,这种能源密集型的过程已经引起了人们对其环境影响和长期可持续性的关注。在比特币等数字货币中,POW被广泛使用,矿工们需要不断地进行计算以解决复杂的数学问题,从而获得区块的奖励。
POS,也称为股权证明,是一种根据持有数字货币的数量和时间来选择验证者的共识机制。在POS模式下,不需要像POW那样进行大量的计算,因此更加节能和环保。在POS网络中,验证者的选择是基于他们持有的数字货币的数量和时间,因此攻击者需要拥有很大一部分的数字货币才能破坏网络,这使得攻击成本很高,可能性也很小。然而,POS网络可能更容易受到“无利害关系”和“远程”攻击。
3、是否了解redis中的某些数据结构(ziplist、Hash表)
- Ziplist:Ziplist 是一种紧凑的数据结构,用于存储一系列字符串或整数。它是一种双端队列,支持从头部和尾部进行元素的插入和删除操作。Ziplist 的主要优点是它可以在内存中高效地存储和操作一系列字符串或整数。
Ziplist 的主要特点包括:
- 它是一种紧凑的数据结构,通过串联多个字符串或整数来减少内存占用。
- 它支持从头部和尾部进行插入和删除操作,时间复杂度为 O(1)。
- 它适用于存储一系列相关的字符串或整数,例如在哈希表或列表中存储多个键值对。
- 它是一种可变的数据结构,可以在不创建新节点的情况下修改节点的值。
- 哈希表:Redis 中的哈希表是一种键值对的数据结构,它允许使用一个唯一的键来访问存储在哈希表中的值。哈希表在 Redis 中被广泛使用,例如用于存储数据库中的键值对。
哈希表的主要特点包括:
- 它是一种键值对的数据结构,可以通过唯一的键来访问存储的值。
- 它支持插入、删除和查找操作,时间复杂度为 O(1)。
- 它可以动态地增长和收缩,以适应存储的键值对数量的变化。
- 它使用哈希算法将键映射到相应的值,从而实现快速查找。
4、协程与线程的区别与联系(针对计算密集型和数据密集型两种不同的场景来回答)
协程(Coroutine)和线程(Thread)都是用于实现并发编程的重要概念,但它们在执行方式、资源占用和调度控制上存在一些区别。
执行方式:
- 线程是操作系统资源分配的最小单位,由操作系统负责其调度。当一个线程正在执行时,其他线程需要等待,直到该线程执行完毕后才能继续执行。
- 协程是用户态的轻量级线程,由用户程序控制其调度。协程的切换完全由程序控制,发生在用户态上。协程的创建、切换和销毁完全由用户程序来完成,不需要操作系统的介入。
资源占用:
- 线程是操作系统资源分配的最小单位,因此线程的创建、切换和销毁需要保存和恢复较多的上下文信息,包括寄存器、栈等,这会消耗较大的系统资源。
- 协程的创建、切换和销毁完全由用户程序控制,因此只需保存和恢复协程的上下文信息,包括寄存器、栈等,所需资源较小,效率较高。
调度控制:
- 线程的调度由操作系统负责,通常采用抢占式调度方式。线程之间需要利用消息通信实现同步,协调执行顺序。
- 协程的调度由用户程序负责,通常采用协作式调度方式。协程之间通过挂起和恢复来实现协作同步,协调执行顺序。
应用场景:
- 线程适合于需要并发执行、共享内存资源的场景,例如多任务处理、服务器端处理请求等。
- 协程适合于需要控制并发执行顺序的场景,例如生产者-消费者问题、事件驱动的系统等。同时,协程也适用于I/O密集型任务,因为协程的切换开销远远小于线程的切换开销。
协程(Coroutine)和线程(Thread)在计算密集型和数据密集型两种不同的场景下,有各自的优势和适用场景。
对于计算密集型场景:
- 线程:线程是操作系统资源分配的最小单位,对于计算密集型任务,通常使用线程来分配计算任务。线程之间的切换由操作系统完成,这使得线程在处理计算密集型任务时更加高效。
- 协程:协程在计算密集型场景下可能不是最佳选择。由于协程的调度由用户程序控制,其切换开销相对较小,但在计算密集型任务中,大部分时间都用于计算,因此协程的切换开销可能会成为性能瓶颈。
对于数据密集型场景:
- 线程:线程在数据密集型场景中仍然是一种有效的并发编程工具。由于线程可以共享内存和资源,因此对于需要访问共享数据资源的数据密集型任务,使用线程可以避免重复的数据复制和传输。
- 协程:协程在数据密集型场景下可能更具优势。由于协程的切换开销较小,因此在需要频繁进行I/O操作或处理大量数据的数据密集型任务中,使用协程可以避免频繁的线程切换带来的开销,提高程序的响应能力和并发性能。
5、数据库和缓存的不一致性问题如何解决(老生常谈了)
数据库和缓存的不一致性问题可以通过以下几种方式解决:
- 延时双删策略 缓存超时设置:在写库前后都进行删除缓存操作(redis.del(key)),并设定合理的缓存过期时间。具体的步骤是,先删除缓存,再写数据库,休眠一段时间后再次删除缓存。设置缓存过期时间,所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值,然后再回填缓存。
- 异步更新缓存(基于订阅binlog的同步机制):订阅MySQL binlog增量消费 消息队列 增量数据更新到redis。一旦MySQL产生了更新操作(写入、更新、删除),就把binlog记录相关的消息通过消息队列推送至Redis,Redis则根据binlog中的记录,来对Redis缓存进行更新。
- 主动更新缓存:后台点击更新缓存按钮,从DB查找最新数据集合,删除原缓存数据,存储新数据到缓存。但更新过程中删除掉缓存后刚好有业务在查询,那么这个时候返回的数据会是空,会影响用户体验。
- 被动更新缓存:前台获取数据时发现没有缓存数据就会去数据库同步数据到缓存。但当并发请求获取缓存数据不存在的时候,就会产生并发的查询数据的操作。
6、C 中为什么父类要定义虚析构函数(可能看我不太懂C ,问了个奇怪问题)
在C 中,定义虚析构函数(virtual destructor)主要是为了解决多重继承带来的析构问题。
当一个子类被多次继承时,如果在子类的析构函数中没有正确地调用基类的析构函数,就可能导致基类中的资源没有被正确释放,从而引起资源泄漏。而虚析构函数可以确保在子类的析构函数中正确地调用基类的析构函数,从而避免资源泄漏问题。
具体来说,当一个基类被多次继承时,如果在最顶层的子类的析构函数中没有正确地调用基类的析构函数,就可能导致基类中的资源没有被正确释放。而如果基类定义了虚析构函数,则在最顶层的子类的析构函数中会自动调用基类的虚析构函数,从而确保基类中的资源被正确释放。
7、C 14、17、20新特性有了解吗
C 14、C 17和C 20的新特性是C 语言不断发展和完善的结果。下面是一些主要的新特性:
C 14的新特性包括:
- 泛型的Lambda函数:在C 11中,Lambda函数的形式参数需要被声明为具体的类型,但在C 14中,允许Lambda函数的形式参数声明中使用类型说明符auto,这遵循模板参数推导的规则。
- 允许被捕获的成员用任意的表达式初始化:这既允许了capture by value-move,也允许了任意声明lambda的成员,而不需要外层作用域有一个具有相应名字的变量。
C 17的新特性包括:
- 结构化绑定:允许用一个对象的成员或数组的元素去初始化多个变量。例如,可以直接用简单的变量名来访问每个std::map元素的键和值,让代码的可读性更强。
- 带初始化的if和switch语句:允许我们在条件表达式里声明一个初始化语句。这使得在if和switch语句中可以更方便地使用新的变量。
C 20的新特性包括:
- 模块(Modules):长远来看是要替换头文件。使用模块必须明确说明要从模块中导入的内容,例如要导出哪个类、函数、常量、枚举等。有模块接口文件和模块实现文件,因此可将代码分成接口文件和实现文件。对模块来说,只有函数签名是导出内容,即使在模块接口文件中编写了任何函数体,它们也不会被导出。
8、C 中shared_ptr和unique_ptr的区别与联系(unique_ptr只有一个,shared_ptr可以共享)
- 内存管理方式:
- unique_ptr:独占式智能指针,它“独占”所指向的对象,不能与其他智能指针共享对象。当unique_ptr被销毁(例如离开作用域或被删除)时,它所指向的对象也会被自动销毁(释放内存)。因此,unique_ptr确保了对象的正确释放,避免了内存泄漏。
- shared_ptr:共享式智能指针,允许多个指针指向同一个对象。它使用引用计数的方式来管理内存,当指向的对象被多个shared_ptr共享时,只有当所有的shared_ptr都被销毁时,对象才会被自动销毁(释放内存)。这种机制可以有效地避免内存泄漏,但需要注意的是,如果存在循环引用的情况(例如两个对象互相引用),可能会导致内存泄漏。
- 创建方式:
- shared_ptr支持通过复制构造函数和赋值操作符进行复制,因此可以使用shared_ptr来传递和返回对象。
- unique_ptr只能通过移动方式进行传递和返回,不支持复制。
- 性能开销:
- shared_ptr由于需要维护引用计数,因此在某些情况下可能比unique_ptr有更大的性能开销。但是,这种开销在大多数情况下可以忽略不计,除非在极端情况下需要频繁地创建和销毁智能指针。
- 使用场景:
- unique_ptr适用于独占某个资源的情况,例如一个动态分配的内存块只能被一个指针所管理。
- shared_ptr适用于多个指针共享同一个资源的情况,例如多个指针指向同一个动态分配的数组或对象。
- 接口和用法:
- shared_ptr和unique_ptr都提供了类似的接口,包括成员函数use_count()、reset()、operator bool()等。因此在使用上,可以根据实际需求选择合适的智能指针类型。
9、C 如何自己定义shared_ptr类型(移动构造函数、拷贝构造函数、析构函数等)
首先,你需要包含 <memory>
头文件,这是 std::shared_ptr
的定义所在。
然后,你可以定义自己的 MySharedPtr
类型,继承自 std::shared_ptr
,并重写其构造函数和析构函数。
#include <memory>
class MyClass; // 声明你要管理的类
class MySharedPtr : public std::shared_ptr<MyClass> {
public:
// 移动构造函数
MySharedPtr(MySharedPtr&& other) noexcept : std::shared_ptr<MyClass>(std::move(other)) {
// 在移动构造函数中可以做一些额外的处理
}
// 拷贝构造函数
MySharedPtr(const MySharedPtr& other) : std::shared_ptr<MyClass>(other) {
// 在拷贝构造函数中可以做一些额外的处理
}
// 析构函数
~MySharedPtr() {
// 在析构函数中可以做一些额外的处理,例如释放资源等
}
};
MySharedPtr
继承自 std::shared_ptr<MyClass>
,因此它会自动继承 std::shared_ptr
的所有功能,包括对动态内存的管理。然后,你可以根据需要重写移动构造函数、拷贝构造函数和析构函数。
10、auto自动推导类型有哪些机制
在C 中,auto
关键字用于自动推导变量的类型。它遵循以下机制:
- 当声明为指针或引用时,
auto
的推导结果将保持初始化表达式的const属性。例如:
const int x = 10;
auto* a = &x; // a的类型为const int*
auto& b = x; // b的类型为const int&
- 当不声明为指针或引用时,
auto
的推导结果和初始化表达式抛弃引用和const属性限定符后的类型一致。例如:
const int x = 10;
auto d = x; // d的类型为const int
- 在自动推导时,先不考虑额外的修饰,看看推导出的是什么类型,然后再加上修饰符。例如:
auto i = 1; // i的类型为int
auto l = 2L; // l的类型为long
auto ll = 3LL; // ll的类型为long long
auto f = 1.23f; // f的类型为float
auto d = 3.1415926535; // d的类型为double
- 类成员变量初始化时,不允许使用
auto
推导类型。 auto
可以和const
、&这些类型修饰符结合,得到新的类型。例如:
autostr1="hello";// str1的类型为const char*
autostr2="hello";// str2的类型为std::string (需要打开名字空间using namespace std::literals::string_literals)
这些是auto
自动推导类型的主要机制。在编写代码时,合理地使用auto
可以提高代码的可读性和简洁性。
11、编程题:给定一个链表,反转left到right的部分
这是一个常见的编程问题,以下是一个在Java中反转链表中一部分的解决方案。
首先,我们需要创建一个链表节点类,通常称为 ListNode:
代码语言:javascript复制public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
然后,我们可以编写一个函数来反转链表的特定部分:
代码语言:javascript复制public class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
if (head == null) {
return null;
}
ListNode dummy = new ListNode(0); // 创建一个哑节点作为起始点
dummy.next = head;
ListNode pre = dummy; // 用于移动到要反转的部分的前一个节点
for (int i = 1; i < left; i ) {
pre = pre.next;
}
ListNode start = pre.next; // 要反转的部分的起始节点
ListNode then = start.next; // 要反转的部分的起始节点的下一个节点
// 记录要反转的节点,直到right节点,不包括right节点
for (int i = left; i < right; i ) {
then = then.next;
}
// 反转部分,不包括right节点
ListNode prev = pre;
while (then != null) {
ListNode temp = then.next; // 保存要反转的节点的下一个节点
then.next = temp.next; // 将要反转的节点的下一个节点设为null,实现反转操作
prev.next = then; // 将prev.next设为要反转的节点,实现反转操作
then = temp; // 移动到已经反转的节点的下一个节点,准备下一次反转操作
prev = then; // 移动到已经反转的节点的下一个节点,准备下一次反转操作
}
return dummy.next; // 返回反转后的链表,起始点为哑节点的下一个节点(即head)
}
}
- 原文链接:https://github.com/warthecatalyst/What-to-in-Graduate-School/blob/main/秋招的面经/华科计科第二人的秋招报告.md
END
革命尚未成功,同志仍需努力,冲冲冲