后端开发面经系列--快手音视频C++开发
快手音视频C++开发
公众号:阿Q技术站
https://www.nowcoder.com/discuss/528703603104632832
1、看到有iOS项目,问为什么用Moya,相比于NSURLSession好在哪里?
这里讲讲Moya与NSURLSession的区别。
- Moya:
- 抽象网络层:Moya 是一个基于 Alamofire 的抽象层,它简化了网络请求的编写,并提供了一个更易于维护和测试的网络层抽象。
- 基于协议的设计:Moya 的设计采用了基于协议的方式,通过定义抽象的 TargetType 协议来描述每个网络请求的细节,包括 URL、请求方法、参数等,从而实现了网络请求的高度抽象和模块化。
- 易于测试:由于 Moya 的网络请求部分被抽象到了协议中,因此可以很容易地进行单元测试,而不需要真正发起网络请求。
- NSURLSession:
- 底层 API:NSURLSession 是苹果提供的底层网络请求 API,它提供了对网络请求的底层控制和定制能力,可以满足更加复杂的网络请求需求。
- 更多的灵活性:相比于 Moya,NSURLSession 提供了更多的灵活性和自由度,可以根据具体需求定制各种网络请求的细节,但也因此需要更多的代码来处理网络请求。
2、C++ malloc 和 new的区别?
- 返回类型:
malloc
返回的是void*
类型的指针,需要显式地进行类型转换。new
返回的是分配类型的指针,不需要进行显式的类型转换。
- 内存分配的大小:
malloc
需要手动指定要分配的内存大小,以字节为单位。new
在分配内存时会根据类型自动计算所需的内存大小。
- 构造函数的调用:
malloc
只分配内存空间,不会调用对象的构造函数。new
在分配内存后会调用对象的构造函数进行初始化。
- 返回空指针处理:
malloc
在分配失败时返回NULL
,需要手动检查分配是否成功。new
在分配失败时会抛出std::bad_alloc
异常,可以使用try-catch
块来处理异常。
- 动态数组分配:
malloc
不支持动态数组的分配,需要手动计算数组大小并分配内存。new
可以直接用于动态数组的分配,如int* arr = new int[10];
。
3、i++是原子性的吗?
在大多数情况下,i++
操作并不是原子的。原子操作是指在执行过程中不会被中断的操作,它要么完全执行成功,要么完全不执行,没有中间状态。i++
操作包含读取变量的当前值、增加这个值,然后将新值写回变量。在多线程环境下,如果多个线程同时对同一个变量进行 i++
操作,就可能出现竞态条件(Race Condition),导致最终结果不确定。
要使 i++
操作变为原子操作,可以使用互斥锁(mutex)或原子操作(atomic operation)来保护这个操作,确保同一时间只有一个线程能够执行这个操作。在 C++11 标准引入了 <atomic>
头文件,提供了一系列的原子操作类型,比如 std::atomic<int>
,它可以保证 i++
操作的原子性。
例如,使用 std::atomic<int>
来声明 i
变量,可以确保 i++
操作的原子性:
#include <atomic>
std::atomic<int> i{0};
// 在多线程环境中对 i 进行递增操作
i++;
这样做能够确保在多线程环境中对 i
进行递增操作时不会出现竞态条件,保证了操作的原子性。
4、SSL的过程?
SSL(Secure Sockets Layer)是一种用于在网络上安全传输数据的协议。它最初由网景公司开发,后来发展为TLS(Transport Layer Security)协议。TLS是SSL的继任者,目前主要使用的是TLS 1.2 和 TLS 1.3。
TLS/SSL 协议的基本过程如下:
-
握手阶段(Handshake):
- Client Hello:客户端向服务器发送一个消息,其中包含客户端支持的加密算法和其他选项。
- Server Hello:服务器选择一个加密算法,并向客户端发送一个消息,该消息包含服务器选择的加密算法和其他选项。
- Certificate:服务器将自己的证书发送给客户端,证书包含服务器的公钥以及相关的信息。
- Server Key Exchange:如果服务器需要,它会向客户端发送用于协商密钥的额外信息。
- Certificate Request:如果服务器需要客户端提供证书,它会发送一个请求。
- Server Hello Done:服务器告诉客户端握手过程结束。
-
密钥协商阶段(Key Exchange):
- 客户端使用服务器的公钥加密一个对称密钥,并将其发送给服务器。
- 服务器使用自己的私钥解密客户端发送的密钥,并使用该密钥与客户端进行进一步的通信。
-
身份验证(Authentication):
- 客户端验证服务器的证书是否合法(例如,是否由受信任的证书颁发机构颁发)。
- 可选地,服务器可以验证客户端的证书。
-
加密通信(Secure Communication):
- 客户端和服务器使用协商好的对称密钥进行通信,这个对称密钥是在握手阶段协商好的。
- 所有传输的数据都使用这个对称密钥进行加密和解密。
-
握手完成(Handshake Complete):
客户端和服务器都确认握手成功完成,之后开始正常的数据传输。
5、介绍下死锁?
死锁是指在多线程或多进程的环境中,两个或多个进程或线程由于竞争资源而无法继续执行的状态。这种状态发生在每个进程都在等待另一个进程所持有的资源时,导致所有进程都无法继续执行,即陷入了死循环,无法解决自己的问题。
死锁发生的必要条件包括:
- 互斥条件:资源不能被共享,一次只能被一个进程或线程占用。
- 请求与保持条件:一个进程或线程在持有一些资源的同时又请求另一些资源。
- 不剥夺条件:资源只能在进程使用完之后由进程自己释放,不能被其他进程强行剥夺。
- 循环等待条件:存在一个进程或线程的资源申请序列,使得每个进程或线程都在等待下一个进程或线程所持有的资源。
死锁的解决方法包括:
- 预防死锁:通过破坏死锁的四个必要条件之一来预防死锁的发生。例如,破坏循环等待条件,可以规定所有进程对资源的申请顺序,使得资源的申请按照某种顺序进行,从而避免循环等待。
- 避免死锁:在资源分配的过程中,动态地避免死锁的发生。例如,使用银行家算法来判断一个资源分配是否会导致死锁,如果会,就不进行该资源的分配。
- 检测和解除死锁:当死锁已经发生时,通过检测死锁并采取措施解除死锁。例如,通过资源分配图来检测死锁,并通过撤销进程的资源分配来解除死锁。
6、介绍下C++智能指针
- std::unique_ptr:
- 一种独占所有权的智能指针,它确保在其生命周期内只有一个指针可以指向给定的资源。
- 当 std::unique_ptr 被销毁时,它所管理的对象也会被销毁。
- 使用 std::move 可以将所有权从一个 unique_ptr 转移到另一个 unique_ptr。
- std::shared_ptr:
- 一种共享所有权的智能指针,它允许多个指针共享对同一对象的所有权。
- std::shared_ptr 使用引用计数来跟踪有多少个指针共享对对象的所有权,当引用计数变为零时,对象会被销毁。
- std::make_shared 可以创建一个指向动态分配的对象的 shared_ptr。
- std::weak_ptr:
- std::weak_ptr 是 std::shared_ptr 的一种观察者,它不增加引用计数,因此不会影响对象的生命周期。
- std::weak_ptr 可以用来解决 std::shared_ptr 的循环引用问题,因为它不会造成循环引用导致的内存泄漏。
- std::auto_ptr(已在 C++11 中被弃用):
- std::auto_ptr 是 C++98 中引入的智能指针,用于管理动态分配的对象。
- std::auto_ptr 具有所有权转移的语义,即通过赋值或复制 std::auto_ptr 时,所有权会从一个指针转移到另一个指针。
- 由于其所有权转移的特性容易导致错误,C++11 中引入了更安全和更灵活的 std::unique_ptr 和 std::shared_ptr,std::auto_ptr 已经被弃用。
7、怎么样来防止类的循环引用?
在 C++ 中,类的循环引用是指两个或多个对象相互引用对方,导致它们的引用计数永远不会变为 0,从而导致内存泄漏。
-
使用弱引用(std::weak_ptr):
- 当一个对象需要引用另一个对象,但又不希望造成循环引用时,可以使用 std::weak_ptr 来持有对对象的弱引用。
- std::weak_ptr 不会增加对象的引用计数,因此不会影响对象的生命周期,同时也不会导致循环引用。
-
破坏循环引用:
- 如果发现类之间存在循环引用,可以考虑重新设计类的结构,尝试破坏循环引用的关系。
- 可以通过使用回调或观察者模式来代替直接引用对方。
-
使用裸指针:
在某些情况下,可以使用裸指针来避免循环引用,但需要注意裸指针可能会带来内存泄漏和悬挂指针的问题。
-
使用 std::enable_shared_from_this:
- 如果一个类需要返回一个指向自身的 shared_ptr,可以继承自 std::enable_shared_from_this,它提供了一个成员函数 shared_from_this(),可以返回一个指向自身的 shared_ptr。
- 在使用 std::enable_shared_from_this 时,需要注意只能在已经存在 shared_ptr 对象的成员函数中使用 shared_from_this(),否则会导致未定义行为。
-
使用局部变量:
尽量避免在类的构造函数或析构函数中使用 shared_ptr,因为在这些函数中 shared_ptr 的引用计数可能会因为循环引用而无法正确减少到 0。
8、shared_ptr是否是线程安全的?
std::shared_ptr
是线程安全的,但它的引用计数的增加和减少操作并不是原子的。这意味着在多线程环境下,对同一个 std::shared_ptr
的引用计数进行增加或减少可能会导致竞争条件(race condition)。因此,在多线程环境下使用 std::shared_ptr
时需要采取一些额外的措施来确保线程安全性,比如使用互斥锁或其他同步机制。
为了在多线程环境下安全地使用 std::shared_ptr
,可以考虑以下几点:
-
使用互斥锁保护共享资源:
在对
std::shared_ptr
进行引用计数的操作(增加或减少)时,使用互斥锁来保护共享资源,确保同时只有一个线程可以修改引用计数,从而避免竞争条件。 -
使用原子操作:
C++11 提供了一些原子操作的函数(比如
std::atomic
类型),可以用来对std::shared_ptr
的引用计数进行原子操作,从而避免竞争条件。 -
避免跨线程共享:
尽量避免在多个线程之间共享同一个
std::shared_ptr
对象,特别是在对象的生命周期结束后仍然可能被访问的情况下,这样会增加线程安全性的复杂度。 -
使用线程安全的容器:
如果需要在多线程环境下管理一组共享指针,可以考虑使用线程安全的容器(比如
std::shared_mutex
、std::atomic_shared_ptr
等),这些容器提供了更高级别的线程安全性。
9、C++类型转换,static_cast
和dynamic_cast
?
-
static_cast
:-
static_cast
是一种静态类型转换,它在编译时进行类型检查和转换。它可以用于显式地将一个类型转换为另一个类型,比如将整数类型转换为浮点类型,或者将指针类型转换为另一个指针类型。static_cast
主要用于一般的类型转换,比如数值类型之间的转换、指针之间的转换(包括基类指针向派生类指针的转换),以及将空指针转换为目标类型的指针。 -
例如:
int i = 10; double d = static_cast<double>(i); // 将整数类型转换为浮点类型
-
-
dynamic_cast
:-
dynamic_cast
是一种动态类型转换,它主要用于在运行时进行类型转换和检查。dynamic_cast
主要用于处理多态类的指针或引用的类型转换,它会在运行时检查是否可以安全地将一个指向基类对象的指针或引用转换为指向派生类对象的指针或引用。如果转换是安全的,dynamic_cast
返回指向目标类型的指针或引用;如果转换不安全,dynamic_cast
返回空指针(对于指针)或抛出std::bad_cast
异常(对于引用)。 -
例如:
class Base { virtual void foo() {} }; class Derived : public Base {}; Base* basePtr = new Derived(); Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全的派生类指针转换
-
需要注意的是,dynamic_cast
只能用于具有虚函数的类,因为它需要在运行时检查对象的类型信息。另外,dynamic_cast
只能用于指针或引用类型的转换,不能用于将基本数据类型转换为类类型或者将类类型转换为基本数据类型。
10、C++怎么实现多态的?
-
虚函数(virtual function):
-
虚函数是在基类中声明为虚函数的函数。通过在基类中声明虚函数,在派生类中可以重写(override)这些函数,并且在运行时根据对象的实际类型来调用相应的函数。在 C++ 中,通过在函数声明前加上
virtual
关键字来声明一个虚函数。 -
例如:
class Shape { public: virtual void draw() const { // 虚函数的默认实现 } }; class Circle : public Shape { public: void draw() const override { // 重写虚函数 } };
-
-
虚函数表(vtable):
- 虚函数表是 C++ 实现多态的一种机制,它是一个存储类中所有虚函数地址的表格。每个含有虚函数的类都有一个虚函数表,其中存储着该类的虚函数的地址。当一个类含有虚函数时,编译器会在该类的对象中插入一个指向虚函数表的指针,这个指针在对象创建时被初始化,并且指向该类的虚函数表。
-
虚函数的调用:
-
当使用基类指针或引用指向派生类对象时,通过该指针或引用调用虚函数时会根据对象的实际类型来决定调用哪个版本的函数。这个过程称为动态绑定(dynamic binding)或运行时多态。编译器会根据对象的实际类型,在运行时查找对应的虚函数表,并调用相应的函数。
-
例如:
Shape* shapePtr = new Circle(); shapePtr->draw(); // 调用派生类的 draw 函数
-
通过使用虚函数和虚函数表,C++ 实现了运行时多态性,使得程序可以更灵活地处理不同类型的对象,提高了代码的可扩展性和可维护性。
11、map与unordered_map的区别?
- 内部实现:
std::map
是基于红黑树实现的有序关联容器,它将键值对按照键的大小顺序进行排序存储,因此在std::map
中查找、插入和删除操作的时间复杂度为 O(log n)。std::unordered_map
是基于哈希表实现的无序关联容器,它使用哈希函数将键映射到存储桶中,因此它的元素存储顺序是不确定的。在std::unordered_map
中查找、插入和删除操作的平均时间复杂度为 O(1),但在最坏情况下可能达到 O(n)。
- 排序性:
std::map
中的元素是按照键的大小顺序进行排序存储的,因此它适用于需要按照键进行有序访问的场景。std::unordered_map
中的元素没有特定的顺序,因此它适用于不需要按照键顺序访问的场景。
- 哈希函数要求:
- 由于
std::unordered_map
是基于哈希表实现的,因此它要求键类型必须提供哈希函数,并且要支持相等比较运算符。如果键类型没有提供哈希函数,可以通过自定义哈希函数或者提供std::hash
的特化版本来满足要求。
- 由于
- 内存使用:
- 由于
std::map
使用红黑树实现,它需要额外的内存来存储树的结构,因此在存储大量元素时可能占用更多的内存。 std::unordered_map
使用哈希表实现,其内存占用通常比std::map
更少,但在哈希冲突较多时可能会导致性能下降。
- 由于
- 迭代器稳定性:
- 在
std::map
中,迭代器在插入或删除元素后仍然有效,因为它们不受树结构的改变影响。 - 在
std::unordered_map
中,插入或删除元素可能导致哈希表的重新哈希,这可能会使得之前获取的迭代器失效。
- 在
12、TCP拥塞控制的流程?
- 慢启动(Slow Start):
- 在开始时,TCP 的拥塞窗口大小为一个最大报文段大小(Maximum Segment Size,MSS)。
- 当发送方发送的数据包得到确认时,拥塞窗口大小加倍。
- 这个过程会一直持续,直到拥塞窗口大小达到一个阈值(慢启动阈值)。
- 拥塞避免(Congestion Avoidance):
- 一旦拥塞窗口大小达到了慢启动阈值,TCP 就进入拥塞避免阶段。
- 在拥塞避免阶段,拥塞窗口大小每经过一个往返时间(RTT),就会增加 1 个 MSS。
- 这样做是为了逐渐增加发送速率,同时防止过快地发送导致网络拥塞。
- 快速重传和快速恢复(Fast Retransmit and Fast Recovery):
- 如果发送方连续收到三个重复的确认报文(Duplicate Acknowledgment),就会触发快速重传。
- 快速重传会立即重传丢失的报文段,而不是等待超时重传。
- 在快速重传后,TCP 进入快速恢复状态,将拥塞窗口的大小减半,然后逐渐增加,以便重新找到网络的最佳传输速率。
- 超时重传(Timeout Retransmission):
- 如果发送方在超时时间内没有收到确认报文,就会触发超时重传。
- 超时重传会导致拥塞窗口大小直接设置为 1,并重新开始慢启动过程。
TCP 拥塞控制通过动态调整拥塞窗口大小,根据网络的拥塞情况动态调整发送速率,从而有效地控制网络拥塞,保证网络的稳定性和公平性。
13、操作系统虚拟地址到物理地址的转换流程?
操作系统中,虚拟地址到物理地址的转换是通过内存管理单元(MMU)来实现的,主要分为两个阶段:地址转换和访问权限检查。
- 地址转换:
- 当 CPU 发出一个内存访问请求时,例如读取数据或指令,首先会将虚拟地址发送给 MMU。
- MMU 中的地址转换单元会根据页表(Page Table)来将虚拟地址转换成物理地址。
- 页表是一个数据结构,用于存储虚拟页号和物理页号的映射关系。
- 访问权限检查:
- 在进行地址转换的同时,MMU 会检查访问权限,例如读、写、执行等权限。
- 如果虚拟地址对应的物理页没有相应的权限,或者访问的地址超出了进程的地址空间,则会产生异常,操作系统会捕获并处理这些异常。
- TLB 缓存:
- 为了加快地址转换的速度,MMU 中通常还会有一个 TLB(Translation Lookaside Buffer)缓存。
- TLB 缓存保存了一部分页表的映射关系,可以加速常用页面的地址转换。
- 页错误处理:
- 如果在地址转换过程中发现虚拟地址对应的物理页不在内存中(即发生了缺页),操作系统会触发一个页错误异常。
- 操作系统会根据页错误的类型,可能会将缺失的页从磁盘加载到内存中,然后重新执行引起缺页异常的指令。
14、手撕的复原IP地址。
问题描述:
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
例如:"0.1.2.201"
和 "192.168.1.1"
是 有效 IP 地址,但是 "0.011.255.245"
、"192.168.1.312"
和 "192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
思路:
- 定义函数和数据结构:定义一个名为
restoreIpAddresses
的函数,它接受一个字符串s
作为输入,并返回一个字符串向量,表示所有可能的有效 IP 地址。再定义一个辅助函数restoreIpAddressesDFS
,它用于执行深度优先搜索(DFS)。 - 初始化和终止条件:在
restoreIpAddresses
函数中,首先创建一个空的结果向量result
和一个空字符串ip
。然后调用restoreIpAddressesDFS
函数开始搜索有效的 IP 地址。DFS 函数中,需要考虑以下终止条件:- 如果已经遍历完整个字符串,并且已经找到了 4 个整数,则将结果加入到结果集中。
- 如果剩余字符数太多或太少,无法凑成一个 IP 地址,则不再继续搜索。
- 深度优先搜索:在
restoreIpAddressesDFS
函数中,从start
索引开始,尝试将连续的 1 到 3 个字符解释为一个整数。使用一个循环来尝试不同长度的整数,同时确保整数的值在 0 到 255 之间。在每次尝试后,我们将得到的整数加入到ip
字符串中,并递归地调用restoreIpAddressesDFS
函数,尝试生成下一个整数。在递归调用后,我们需要恢复ip
字符串的状态,以便尝试其他可能的解。 - 去除前导零:在循环中,需要特别处理前导零的情况。例如,如果以 0 开头的整数只能是 0,不能是 01 或 001 等。
- 结果返回:当递归搜索完成后,将得到的有效 IP 地址加入到结果向量
result
中,并最终返回结果。
这个算法的时间复杂度是 O(1),因为 IP 地址的长度是固定的。
参考代码:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector<string> restoreIpAddresses(string s) {
vector<string> result;
string ip;
restoreIpAddressesDFS(s, 0, 0, ip, result);
return result;
}
void restoreIpAddressesDFS(const string& s, int start, int step, string ip, vector<string>& result) {
if (start == s.length() && step == 4) { // 如果已经遍历完整个字符串,并且已经找到了4个整数,则将结果加入到结果集中
result.push_back(ip);
return;
}
if (s.length() - start > (4 - step) * 3) return; // 剩余字符数太多,无法凑成一个IP地址
if (s.length() - start < (4 - step)) return; // 剩余字符数太少,无法凑成一个IP地址
int num = 0;
for (int i = start; i < start + 3; ++i) {
num = num * 10 + (s[i] - '0');
if (num <= 255) {
ip += s[i];
restoreIpAddressesDFS(s, i + 1, step + 1, ip + (step == 3 ? "" : "."), result);
}
if (num == 0) break; // 避免前导0的情况
}
}
int main() {
string s = "25525511135";
vector<string> result = restoreIpAddresses(s);
for (const auto& ip : result) {
cout << ip << endl;
}
return 0;
}
但是ACM模式,紧张了。原本之前第一次做这题的时候自己就撕出来了,现在再做反而只把代码框架撕出来,甚至编译报错。