声网Linux流媒体C++开发
https://www.nowcoder.com/feed/main/detail/38a87df1514f48b187cc7e2234656b73
音视频
1、flv报文分析。具体到某个字段在哪存?
FLV(Flash Video)是一种用于在网络上传输和流式传输视频和音频的格式。FLV 文件通常由一个包含视频和音频数据的标签流组成。每个标签都包含一个时间戳和实际的音视频数据。
在 FLV 文件中,不同类型的数据以不同的标签类型存储。常见的标签类型包括音频标签(8)、视频标签(9)、脚本数据标签(18)等。
以视频标签(9)为例,它包含了视频的关键帧、非关键帧等信息。具体到某个字段在哪存取取决于这个标签的结构。下面是一个简化的视频标签结构示例:
| Field Name | Field Size (bytes) | Description |
|------------------|--------------------|--------------------------------------------|
| Tag Type | 1 | 标签类型,视频标签为 9 |
| Data Size | 3 | 数据大小,表示这个标签的总大小(不包括 Tag Type 和 Data Size 字段) |
| Timestamp | 4 | 时间戳,表示这个标签的时间戳 |
| Stream ID | 3 | 流 ID,一般为 0 |
| Video Data | Data Size - 11 | 视频数据,实际的视频数据 |2、rtmp交互流程?rtmp直播推流经过哪些环节?
RTMP 交互流程:
- 握手阶段:
- 客户端向服务器发送一个 C0+C1 的握手请求,其中 C0 包含了 RTMP 版本号(一般为 3),C1 包含了一个时间戳和随机数。
- 服务器收到握手请求后,返回 S0+S1+S2 的握手响应,其中 S0 包含了 RTMP 版本号,S1 包含了客户端发送的时间戳和随机数,S2 包含了服务器端的时间戳和随机数。
- 建立网络连接:
- 客户端向服务器发送一个 Connect 命令,请求建立 RTMP 连接。
- 服务器收到 Connect 命令后,返回一个 NetConnection.Connect.Success 消息,表示连接建立成功。
- 建立流:
- 客户端向服务器发送一个 CreateStream 命令,请求创建一个流(Stream)用于传输音视频数据。
- 服务器收到 CreateStream 命令后,返回一个 NetStream.Create.Success 消息,表示流创建成功。
- 播放流:
- 客户端向服务器发送一个 Play 命令,请求播放流。
- 服务器收到 Play 命令后,开始向客户端发送音视频数据,客户端开始播放音视频流。
RTMP 直播推流环节:
摄像头采集:
视频直播通常从摄像头采集视频数据,可以使用摄像头设备或者软件模拟摄像头。
音频编码:
采集到的音频数据经过编码器(如AAC编码器)进行编码,将原始音频数据转换为压缩后的音频流数据。
视频编码:
采集到的视频数据经过编码器(如H.264编码器)进行编码,将原始视频数据转换为压缩后的视频流数据。
FLV 封装:
编码后的音频和视频数据被封装成 FLV(Flash Video)格式的文件或者数据流,FLV 文件中包含了音频和视频的时间戳、帧类型等信息。
RTMP 封装:
封装后的 FLV 数据流被封装成 RTMP 数据包,RTMP 数据包中包含了 FLV 数据以及与服务器进行交互的控制信息。
推流到服务器:
客户端通过 RTMP 协议将封装好的音视频数据流推送到流媒体服务器上,服务器会接收这些数据并进行相应的处理,如存储、转发等。
3、rtmp的握手过程?
- 客户端发送 C0+C1 握手请求:
- 客户端首先向服务器发送一个 C0+C1 的握手请求,其中 C0 是一个字节,表示 RTMP 协议的版本号,一般为 3。
- C1 是一个 1536 字节的结构体,其中包含了客户端当前时间戳(4 字节)、客户端随机数(1528 字节)、客户端支持的加密方式等信息。
- 服务器发送 S0+S1+S2 握手响应:
- 服务器收到客户端的握手请求后,会返回一个 S0+S1+S2 的握手响应。
- S0 是一个字节,表示 RTMP 协议的版本号,一般也为 3。
- S1 是一个 1536 字节的结构体,其中包含了服务器当前时间戳(4 字节)、客户端发送的时间戳(4 字节)、服务器随机数(1528 字节)、服务器支持的加密方式等信息。
- S2 是一个 1536 字节的结构体,其中包含了客户端发送的时间戳、服务器发送的时间戳和客户端随机数,用于验证客户端的身份和确认连接。
- 客户端发送 C2 握手确认:
- 客户端收到服务器的握手响应后,会根据 S1 中的信息生成一个 C2 结构体作为握手确认。
- C2 结构体与 S1 结构体中的信息相对应,包含了客户端发送的时间戳、服务器发送的时间戳和服务器随机数,用于验证服务器的身份和确认连接。
- 握手完成:
- 一旦客户端发送了 C2 握手确认,握手过程就完成了。
- 服务器和客户端都会根据握手中的信息进行一些验证和确认操作,确保双方的身份和连接信息正确无误。
4、rtmp停止推流发送什么指令?
- FCUnpublish 指令
- 当客户端需要停止推流时,会发送一个
FCUnpublish指令给服务器。 FCUnpublish指令告诉服务器取消发布(停止推送)指定的流。
- 当客户端需要停止推流时,会发送一个
停止推流的过程如下:
客户端发送 FCUnpublish 指令:
客户端在需要停止推流时,向服务器发送一个
FCUnpublish指令,其中包含了要停止推流的流名。服务器处理 FCUnpublish 指令:
- 服务器收到
FCUnpublish指令后,会停止接收来自客户端的指定流的数据。 - 服务器可能会根据具体的业务逻辑进行一些清理工作,比如释放资源、更新状态等。
- 服务器收到
服务器响应客户端:
- 如果停止推流成功,服务器会向客户端发送一个成功响应消息,表示停止推流操作已经完成。
- 客户端可以根据这个响应消息来确认停止推流是否成功,并且进行一些后续的操作。
5、h.264帧类型?并详细说明
H.264 是一种常用的视频编码标准,定义了一系列用于压缩视频的算法和技术。在 H.264 中,视频帧被分为三种类型:I帧、P帧和B帧。这些帧类型在视频编码和解码过程中起着不同的作用。
- I帧:
- I帧是视频序列中的关键帧(Keyframe),也称为独立帧(I-frame)。
- 每个I帧都是完整的图像帧,不依赖于其他帧的信息,可以独立地解码和显示。
- I帧通常用于视频的起始点或者场景切换处,是解码器恢复视频画面的重要参考。
- P帧:
- P帧是根据之前的一个或多个参考帧进行预测编码而得到的帧。
- P帧只包含了图像变化的部分,通过参考帧进行预测和补偿,可以有效地压缩视频数据。
- P帧需要依赖于之前的I帧或者P帧进行解码,因此在解码时需要先解码出参考帧,然后再进行预测和补偿。
- B帧:
- B帧是根据之前和之后的参考帧进行双向预测编码而得到的帧。
- B帧可以更好地利用视频序列中的时间和空间相关性,进一步提高视频的压缩率。
- B帧在解码时需要参考前后的帧进行解码,因此对于编码和解码的复杂度要求较高,但可以达到更高的压缩效率。
6、I帧和IDR帧区别?
- I帧:
- I帧是视频序列中的关键帧(Keyframe),也称为独立帧(I-frame)。
- 每个I帧都是完整的图像帧,不依赖于其他帧的信息,可以独立地解码和显示。
- I帧通常用于视频的起始点或者场景切换处,是解码器恢复视频画面的重要参考。
- IDR帧:
- IDR 帧是 H.264 中的一种特殊的 I 帧,也是关键帧(Keyframe)。
- 与普通的 I 帧相比,IDR 帧在编码器中会对编码状态进行重置,使得解码器可以从这一帧开始解码,而不需要依赖之前的任何帧。
- IDR 帧通常用于视频的随机访问点,比如视频流的开始或者用户跳转到视频的某个位置,可以直接从 IDR 帧开始解码,而无需解码之前的帧。
7、GOP开放模式和封闭模式的使用场景及如何选择?
在视频编码中,GOP(Group of Pictures)指的是一组连续的视频帧,其中包含了一个I帧(关键帧)和若干个P帧(预测帧)和B帧(双向预测帧)。GOP 的开放模式和封闭模式是指在编码过程中 GOP 的组成方式。
- GOP 开放模式:
- 在 GOP 开放模式下,每个 GOP 都是相互独立的,即每个 GOP 的第一个帧都是一个 I 帧。
- 这种模式适用于视频内容变化频繁的情况,因为每个 GOP 都是独立的,一旦某个 GOP 中的帧出现损坏或丢失,不会影响到其他 GOP,只会影响到当前 GOP 内的帧。
- 开放模式的 GOP 结构使得视频能够更容易地进行随机访问,例如快速定位到视频的某个位置。
- GOP 封闭模式:
- 在 GOP 封闭模式下,多个 GOP 之间存在关联,一个 GOP 的最后一个帧通常是一个 I 帧,下一个 GOP 的第一个帧是上一个 GOP 的最后一个帧。
- 这种模式适用于视频内容变化较少的情况,因为 GOP 之间的关联性使得视频在解码过程中可以更好地利用帧间预测,从而获得更好的压缩效率。
- 封闭模式的 GOP 结构使得视频的整体压缩效率更高,但是在视频出现丢失或损坏时,可能会影响到多个 GOP,导致整体恢复的难度增加。
在选择 GOP 开放模式和封闭模式时,需要考虑视频内容的特点、对压缩效率和容错性的要求以及应用场景等因素:
- 如果视频内容变化频繁,对于实时性要求高,或者需要支持快速随机访问,可以选择 GOP 开放模式。
- 如果视频内容变化较少,对于整体压缩效率要求高,可以选择 GOP 封闭模式。
8、h.264的码率控制模式(只知道可编码率和固定码率),讲一讲区别以及使用场景
可变比特率(VBR,Variable Bit Rate):
- VBR 模式下,视频编码器根据视频内容的复杂度和变化程度动态调整输出的比特率。
- 当视频内容复杂或者变化较多时,编码器会分配更多的比特率来保留更多细节和运动信息,从而保证视频质量。
- 当视频内容简单或者变化较少时,编码器会分配较少的比特率,以节省带宽和存储空间。
适用场景:
- VBR 模式适用于对视频质量要求较高的场景,如电影、视频会议等,因为它可以根据视频内容的需要分配更多的比特率,从而保证视频质量。
固定比特率(CBR,Constant Bit Rate):
- CBR 模式下,视频编码器输出的比特率是固定的,不会根据视频内容的复杂度和变化而变化。
- 即使视频内容变化较多或者复杂度较高,编码器仍然会保持输出的比特率不变。
- 这种模式可以稳定地控制输出的码率,适用于对码率要求严格的场景,如视频直播、在线视频等。
适用场景:
- CBR 模式适用于对网络带宽有严格要求的场景,因为它可以稳定地控制输出的码率,避免因视频内容变化导致的码率波动,从而保证视频的稳定传输。
选择 VBR 还是 CBR 主要取决于对视频质量和码率稳定性的要求:
- 如果对视频质量要求较高,而且可以接受一定的码率波动,可以选择 VBR 模式。
- 如果对视频的码率要求严格,需要稳定的码率输出,可以选择 CBR 模式。
9、yuv420P在内存中的排列方式?
YUV420P 是一种常见的视频像素格式,用于存储和表示彩色视频。它将像素数据分为亮度(Y)和色度(UV)两部分,并采用了色度子采样的方式来减少色度分量的存储量。
排列方式:
- Y 分量(亮度):
- Y 分量存储了图像的亮度信息,对应于灰度图像。它占据了图像数据中的大部分空间,因为它包含了图像的主要信息。
- Y 分量的存储方式是按照从左到右、从上到下的顺序依次存储每个像素的亮度值。
- U 和 V 分量(色度):
- U 和 V 分量存储了图像的色度信息,用于表示像素的颜色。
- 在 YUV420P 格式中,色度分量采用了 2:1 的水平和垂直色度子采样,即每 4 个像素共用一个色度分量。
- U 和 V 分量的存储方式是按照从左到右、从上到下的顺序依次存储每个 2x2 区域内的色度值,每个 2x2 区域共用一个 U 或 V 分量。
综合来说,YUV420P 的内存排列方式可以描述为:
- 首先按照从左到右、从上到下的顺序存储 Y 分量的像素值。
- 然后按照相同的顺序存储 U 分量的像素值(每 4 个像素共用一个 U 分量)。
- 最后按照相同的顺序存储 V 分量的像素值(每 4 个像素共用一个 V 分量)。
10、yuv420p和nv12的区别?
- YUV420P:
- YUV420P 是一种 Planar(平面)格式,它将像素数据分为亮度(Y)和色度(UV)两个平面。
- Y 分量(亮度)存储了图像的亮度信息,对应于灰度图像,它的存储方式是按照从左到右、从上到下的顺序依次存储每个像素的亮度值。
- UV 分量(色度)采用了 2:1 的水平和垂直色度子采样,即每 4 个像素共用一个色度分量,其中 U 和 V 分量分别存储每个 2x2 区域内的色度值。
- NV12:
- NV12 也是一种 Planar(平面)格式,它将像素数据分为亮度(Y)和两个色度(UV)平面。
- Y 分量的存储方式与 YUV420P 相同,按照从左到右、从上到下的顺序依次存储每个像素的亮度值。
- UV 分量的存储方式略有不同,它将 U 和 V 交替地存储在一个平面中,即先存储一个 U 值,然后存储一个 V 值,再存储下一个 U 值,以此类推。
区别:
- 主要的区别在于 UV 分量的存储方式不同。在 YUV420P 中,UV 分量分别存储在不同的平面中,而在 NV12 中,UV 分量交替存储在同一个平面中。
- 这样的存储方式差异导致了在数据访问和处理时的不同方式,因此在使用时需要根据具体的需求和应用场景选择合适的格式。
11、h.264裸流分析?(vcl层和nal层分析)
在 H.264 裸流中,可以进行 VCL(Video Coding Layer)层和 NAL(Network Abstraction Layer)层的分析。这两个层次的分析可以帮助我们了解视频编码的具体细节和结构。
- NAL 层分析:
- NAL 单元是 H.264 中的基本数据单元,它负责将视频编码后的数据进行分割和打包。
- NAL 单元以起始码(start code)为起始标志,通常为 0x00 00 01 或 0x00 00 00 01。
- NAL 单元可以分为两类:VCL NAL 单元和非 VCL NAL 单元。
- VCL NAL 单元包含了视频编码的具体内容,如 I 帧、P 帧、B 帧等;非 VCL NAL 单元则包含了编码器的控制信息,如 SPS、PPS、SEI 等。
- 在 NAL 层分析中,我们可以逐个解析 NAL 单元,识别其类型(VCL 或非 VCL)以及具体的内容,如帧类型、帧大小等。
- VCL 层分析:
- VCL 层是 H.264 中的视频编码层,负责对视频帧进行压缩编码。
- VCL 层的主要内容包括帧间预测、变换、量化、熵编码等过程。
- 在 VCL 层分析中,我们可以关注每个 VCL NAL 单元的具体内容,了解其中的编
12、两个h.264包,是怎么识别是两个包的?
在 H.264 流中,视频帧被分割成一个个的 NAL 单元(Network Abstraction Layer units)进行传输。这些 NAL 单元之间通过起始码(start code)或者长度前缀来进行分割,因此可以通过检测起始码或者长度前缀来识别不同的 NAL 单元,进而识别出不同的 H.264 包。
- 起始码(start code)分割:
- 在 H.264 中,每个 NAL 单元的开始处通常包含一个起始码,它是一个固定的字节序列,通常为 0x00 00 01 或 0x00 00 00 01。
- 这个起始码的存在可以用来判断两个 H.264 包的分割位置。当检测到起始码时,就可以确定前一个 NAL 单元的结束位置和后一个 NAL 单元的开始位置,从而识别出两个 H.264 包之间的分割点。
- 长度前缀(length prefix)分割:
- 另一种分割 NAL 单元的方式是使用长度前缀。在这种方式下,每个 NAL 单元的开始处会有一个固定长度的字段,用来指示该 NAL 单元的长度。
- 通过读取长度前缀字段的值,就可以确定 NAL 单元的结束位置和下一个 NAL 单元的开始位置,从而识别出两个 H.264 包之间的分割点。
13、SPS和PPS是干什么的?
- SPS(Sequence Parameter Set):
- SPS 包含了描述视频序列的信息,如分辨率、帧率、像素采样格式(如 YUV420、YUV422)、编码器配置等。
- SPS 是一种全局参数集,通常情况下一个视频序列只包含一个 SPS。
- SPS 中的信息对整个视频序列都是通用的,因此在解码视频时,只需要在视频序列开始时读取一次 SPS,然后就可以根据其中的信息来正确解码整个视频序列。
- PPS(Picture Parameter Set):
- PPS 包含了描述图像帧的信息,如帧类型(I、P、B 帧)、量化参数、运动矢量预测方式等。
- PPS 是一种局部参数集,每个图像帧都需要包含一个对应的 PPS。
- PPS 中的信息与具体的图像帧相关,因此在解码每个图像帧时都需要读取对应的 PPS,以获取该帧的解码参数。
SPS 和 PPS 的作用在于:
- 提供了视频解码所需的关键参数,使得解码器能够正确地解析和解码 H.264 视频流。
- SPS 中的信息是全局的,对整个视频序列都适用;而 PPS 中的信息是针对具体的图像帧的,每个图像帧都需要包含对应的 PPS。
- SPS 和 PPS 的存在使得 H.264 能够更加灵活地描述和编码各种类型的视频内容,同时也方便了解码器对视频流进行解析和处理。
C++:
1、c++11新特性有哪些?
自动类型推导(auto):
使用 auto 关键字可以让编译器根据初始化表达式的类型推导出变量的类型。
c++auto i = 42; // i 的类型被推导为 int auto f = 3.14; // f 的类型被推导为 double范围-based for 循环:
使用范围-based for 循环可以方便地遍历容器中的元素。
c++std::vector<int> vec = {1, 2, 3, 4, 5}; for (auto& element : vec) { std::cout << element << " "; }nullptr 关键字:
- nullptr 用于表示空指针常量,取代了传统的 NULL 宏。
c++int* ptr = nullptr; // ptr 是空指针Lambda 表达式:
- Lambda 表达式可以定义匿名函数,简化了函数对象的定义和使用。
c++auto sum = [](int a, int b) { return a + b; }; std::cout << sum(1, 2); // 输出 3智能指针(std::shared_ptr、std::unique_ptr):
- 智能指针用于管理动态分配的内存,避免了手动管理内存可能出现的问题。
c++std::shared_ptr<int> ptr = std::make_shared<int>(42);右值引用和移动语义:
- 右值引用和移动语义允许将资源的所有权从一个对象转移到另一个对象,提高了资源的利用效率。
c++std::vector<int> source = {1, 2, 3}; std::vector<int> destination = std::move(source); // source 被移动到 destination初始化列表:
- 初始化列表可以用于对数组、容器和类的成员进行初始化。
c++std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用初始化列表初始化 vectorconstexpr 关键字:
- constexpr 关键字用于声明常量表达式,可以在编译时求值。
c++constexpr int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); }
2、介绍一下智能指针有哪些?shared_ptr底层数据结构如何实现的?shared_ptr是线程安全的吗?shared_ptr和weak_ptr来构造有什么区别?
- std::unique_ptr:
std::unique_ptr是一种独占式(exclusive ownership)的智能指针,即同一时间只能有一个std::unique_ptr拥有一个资源。- 它通过移动语义来传递所有权,因此不能进行复制,但可以进行移动。
- 由于其独占性质,
std::unique_ptr在管理资源的所有权上非常高效,适用于大多数情况下需要动态分配内存并且只有一个所有者的场景。
- std::shared_ptr:
std::shared_ptr是一种共享式(shared ownership)的智能指针,可以有多个std::shared_ptr共享同一个资源。- 它使用引用计数来管理资源的生命周期,当最后一个
std::shared_ptr被销毁时,它会自动释放资源。 - 由于引用计数的维护,
std::shared_ptr的内部需要分配额外的内存来存储计数器,可能会导致一些额外的开销。
- std::weak_ptr:
std::weak_ptr也是一种共享式的智能指针,但它不会增加资源的引用计数。std::weak_ptr可以从std::shared_ptr或std::weak_ptr构造,用于解决std::shared_ptr的循环引用问题,避免内存泄漏。std::weak_ptr通常用于缓存、观察者模式等场景,不直接管理资源的生命周期。
std::shared_ptr 的底层数据结构,通常是一个包含两个指针的结构体:一个指向管理的对象,另一个指向引用计数。引用计数通常是原子操作,因此 std::shared_ptr 在多线程环境下是线程安全的。
std::shared_ptr 和 std::weak_ptr 的区别在于:std::shared_ptr 共享所有权,当所有 std::shared_ptr 被销毁时才释放资源;而 std::weak_ptr 不增加引用计数,只用于协助 std::shared_ptr,当资源被释放后,相应的 std::weak_ptr 会自动失效,不再指向任何对象。
3、什么是左值引用?int &&a=10;和int &&a=b;(int b=10)的区别?
- 左值引用:
- 左值引用是对左值的引用,通常用于引用具名对象。
- 左值引用使用
&符号声明,如int& ref = some_variable;。 - 左值引用可以绑定到左值,但不能绑定到右值。
- 右值引用:
- 右值引用是对右值的引用,通常用于移动语义和完美转发。
- 右值引用使用
&&符号声明,如int&& rref = 10;。 - 右值引用可以绑定到临时对象(如字面常量、函数返回的临时对象等),也可以绑定到具名对象(在某些情况下)。
int &&a=10; 和 int &&a=b;(int b=10;)的区别:
int &&a=10;:这里a是一个右值引用,绑定到字面常量10,因为10是一个右值。int &&a=b;:这里a是一个右值引用,但它尝试绑定到左值b,这在语法上是合法的,但这样的引用并不会产生移动语义,实际上会产生左值引用,即a成为了b的别名。
4、lambda表达的的捕获方式有哪几种?值捕获能否修改捕获的值?
值捕获(Value Capture):
通过值捕获,Lambda 表达式会复制外部变量的值,并在 Lambda 内部使用这个副本。
c++int x = 5; auto lambda = [x]() { return x; };这里 Lambda 表达式
[x]中的x是通过值捕获的,Lambda 内部使用的是x的副本。引用捕获(Reference Capture):
通过引用捕获,Lambda 表达式会捕获外部变量的引用,而不是复制其值。
c++int x = 5; auto lambda = [&x]() { return x; };这里 Lambda 表达式
[&x]中的&x是通过引用捕获的,Lambda 内部使用的是x的引用。隐式捕获(Implicit Capture):
通过隐式捕获,Lambda 表达式可以自动捕获其所在作用域中的变量。
c++int x = 5; auto lambda = [&]() { return x; };这里 Lambda 表达式
[&]中的&表示通过引用捕获,同时使用了隐式捕获,捕获了x的引用。混合捕获(Mixed Capture):
Lambda 表达式还可以同时使用值捕获和引用捕获。
c++int x = 5; auto lambda = [x, &y]() { return x + y; };这里 Lambda 表达式
[x, &y]中的x是通过值捕获,&y是通过引用捕获。
如果需要在 Lambda 内部修改外部变量的值,可以考虑使用引用捕获(Reference Capture)或者 mutable 关键字。通过引用捕获可以在 Lambda 内部修改外部变量的值,而 mutable 关键字则用于声明 Lambda 表达式的函数调用运算符为一个非 const 成员函数,使得 Lambda 内部可以修改捕获的值。
5、lambda和std::function的区别?
- Lambda 表达式:
- Lambda 表达式是 C++11 引入的,用于创建匿名函数对象的一种便捷语法。
- Lambda 表达式可以在需要函数对象的地方直接定义和使用,通常用于简单的函数对象的定义,避免了传统函数对象类的繁琐定义。
- Lambda 表达式的语法形式为
[capture](parameters) -> return_type { body },其中capture是捕获列表,用于捕获外部变量;parameters是参数列表;return_type是返回类型;body是函数体。
- std::function:
std::function是 C++11 中的模板类,用于包装可调用对象(函数、函数指针、Lambda 表达式等),并提供统一的调用接口。std::function可以像函数指针一样被赋值、传递和调用,可以作为参数传递给函数,也可以作为函数的返回值。std::function的模板参数指定了可调用对象的类型,例如std::function<int(int)>表示可以接受一个int类型参数并返回一个int类型结果的可调用对象。std::function提供了与函数指针类似的功能,但比函数指针更加灵活,可以包装更多类型的可调用对象。
主要区别和适用场景:
- Lambda 表达式通常用于创建简单的函数对象,特别是在需要局部定义一个简单的函数对象时非常方便。
std::function用于包装可调用对象,可以在需要传递函数对象的地方使用,提供了一种通用的函数对象封装方式,可以与模板和泛型编程结合使用,使得代码更加灵活。
6、手撕翻转链表
思路:
- 如果链表为空或者只有一个节点,直接返回头结点head。
- 初始化 pre 为 nullptr,cur 为头结点 head,node 为 cur 的下一个节点。
- 在循环中,不断更新 pre、cur 和 node 的值,使得 cur 的 next 指向 pre,然后将 pre、cur 和 node 分别向后移动一位。
- 当 cur 移动到链表末尾时,pre 就是反转后的新头结点。
参考代码:
#include <iostream>
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head; // 如果链表为空或者只有一个节点,直接返回头结点
}
ListNode* pre = nullptr; // 初始化 pre 为 nullptr
ListNode* cur = head; // 初始化 cur 为头结点
ListNode* node = nullptr; // 初始化 node 为 nullptr
while (cur != nullptr) {
node = cur->next; // 保存当前节点的下一个节点
cur->next = pre; // 当前节点的 next 指向 pre,完成反转
pre = cur; // 更新 pre
cur = node; // 更新 cur
}
return pre; // pre 就是反转后的新头结点
}
};
int main() {
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
Solution solution;
ListNode* newHead = solution.reverseList(head);
while (newHead != nullptr) {
std::cout << newHead->val << " ";
newHead = newHead->next;
}
return 0;
}