基于Qt + FFmpeg实现视频片段裁剪功能

 更新时间:2026年06月30日 09:24:26   作者:luoyayun361  
本文介绍基于Qt与FFmpeg的视频片段快速裁剪功能实现,采用Stream Copy模式避免重编码,确保导出速度快、画质无损;核心包括QML交互组件、异步缩略图与音频峰值波形生成、关键帧对齐seek、packet时间戳重写及容器兼容性处理,需要的朋友可以参考下

前言

视频片段裁剪用于从一个视频文件中选择一段时间范围,并导出成新的短视频文件。当前实现的是 快速裁剪 / Stream Copy 路线:

导入视频
  -> VideoTrimPage 自动生成时间轴缩略图
  -> VideoTrimEditor 维护选区、播放头和预览
  -> 可选生成轻量音频峰值波形
  -> MediaAnalyzer 后台调度导出
  -> VideoClipProcessor 使用 FFmpeg stream copy 写出片段

当前版本不做视频帧级重编码,因此导出速度快、画质无损,但起点会按关键帧附近对齐。这是 stream copy 的正常限制,不是时间轴计算错误。

效果图:

一、整体架构

VideoTrimPage.qml
    页面层:当前文件、缩略图刷新、音频峰值生成、导出参数和结果展示
        ↓
VideoTrimEditor.qml
    交互层:视频预览、缩略图时间轴、播放头、选区左右把柄、音频峰值轨
        ↓ selectionStartMs / selectionEndMs
MediaAnalyzer
    QML 门面:文件导入通知、异步缩略图、异步音频峰值、异步导出、状态回写
        ↓
VideoFrameExtractor / AudioDecoder / VideoClipProcessor
    FFmpeg 层:批量抽帧、流式音频峰值聚合、stream copy 裁剪导出

二、页面入口:VideoTrimPage.qml

VideoTrimPage.qml 是裁剪功能入口,负责:

  • 展示当前视频文件;
  • 自动生成或手动刷新时间轴缩略图;
  • 按需生成音频峰值波形;
  • 选择输出封装;
  • 控制是否保留音频、字幕、元数据;
  • 调用 trimCurrentVideoCopy() 执行导出。

2.1 输出封装和提示

页面当前暴露常见视频封装:

property var outputFormats: ["mp4", "mkv", "mov", "webm"]
property string exportModeNotice: "快速导出使用 stream copy,无重编码、画质无损,但起点按关键帧对齐。"

实际能否写入成功仍取决于源流编码和目标容器兼容性。例如部分字幕流不能直接写入 MP4,此时可以取消字幕或改用 MKV。

2.2 缩略图生成触发

页面使用独立缩略图状态,不复用全局 busy

function generateThumbnails() {
    if (!mediaAnalyzer.videoTrimThumbnailBusy && appRoot.hasFile && appRoot.currentFileIsVideo())
        mediaAnalyzer.generateVideoTrimThumbnails(24, 180)
}

这里的参数含义:

参数含义
24默认生成 24 张时间轴缩略图
180单张缩略图目标宽度

页面进入时会启动一个短延迟定时器:

Timer {
    id: autoGenerateThumbnailsTimer
    interval: 50
    repeat: false
    onTriggered: page.generateThumbnails()
}

这个延迟用于等待 openFile() 完成旧数据清理,避免旧缩略图任务或旧列表影响新文件。

2.3 重复导入相同文件

重复导入同一路径时,currentFile 字符串没有变化,currentFileChanged 不会触发。为此 MediaAnalyzer::openFile() 每次成功导入都会发出:

emit fileOpened();

页面同时监听 currentFileChangedfileOpened

Connections {
    target: mediaAnalyzer

    function onCurrentFileChanged() {
        page.refreshDefaultPath()
        autoGenerateThumbnailsTimer.restart()
    }

    function onFileOpened() {
        page.refreshDefaultPath()
        trimEditor.resetForImportedFile()
        autoGenerateThumbnailsTimer.restart()
    }
}

这样即使重复导入同一个视频,也会:

  1. 重置编辑器旧播放状态;
  2. 清空旧选区状态;
  3. 重新触发缩略图生成。

2.4 音频峰值波形

裁剪页不再通过 decodeCurrentToPcm() 解码完整 PCM,而是调用:

mediaAnalyzer.generateVideoTrimAudioPeaks(4096)

4096 表示把整段音频压缩成 4096 个峰值桶。每个桶保存一组 qint16 min/max,数据量约为:

4096 * 2 * sizeof(qint16) = 16KB

这比完整 PCM 更适合大视频文件,避免 1GB 级视频生成波形时给 UI 和内存造成压力。

2.5 编辑器组合

页面把视频、缩略图、缩略图进度、峰值数据传给 VideoTrimEditor

VideoTrimEditor {
    id: trimEditor
    source: mediaAnalyzer.currentFileUrl
    thumbnails: mediaAnalyzer.videoTrimThumbnails
    thumbnailsLoading: mediaAnalyzer.videoTrimThumbnailBusy
    thumbnailProgress: mediaAnalyzer.videoTrimThumbnailProgress
    thumbnailTotal: mediaAnalyzer.videoTrimThumbnailTotal
    audioPeakData: mediaAnalyzer.videoTrimAudioPeakData
    audioPeakCount: mediaAnalyzer.videoTrimAudioPeakCount
    audioPcmData: mediaAnalyzer.videoTrimAudioPeakCount > 0 ? "" : mediaAnalyzer.pcmData
}

当已有轻量峰值数据时,不再把完整 PCM 传入音频轨,避免其它页面生成过的大 PCM 影响裁剪页绘制性能。

2.6 导出调用

点击“快速导出”时,页面把编辑器当前选区传给后端:

mediaAnalyzer.trimCurrentVideoCopy(Math.round(trimEditor.selectionStartMs),
                                   Math.round(trimEditor.selectionEndMs),
                                   outputPathField.text.trim(),
                                   formatBox.currentText,
                                   keepAudioBox.checked,
                                   keepSubtitleBox.checked,
                                   copyMetadataBox.checked)

页面只读取 selectionStartMsselectionEndMs,不直接处理 FFmpeg 或 packet。

三、编辑器交互:VideoTrimEditor.qml

VideoTrimEditor.qml 是裁剪页的主要交互组件。

3.1 选区和播放头状态

编辑器以毫秒为唯一数据源:

property real selectionStartMs: 0
property real selectionEndMs: 0
property real playheadMs: displayPositionMs
property real displayPositionMs: 0
readonly property real durationMs: player.duration > 0 ? player.duration : inferredDuration()
readonly property bool hasSelection: selectionEndMs > selectionStartMs

坐标和时间通过线性函数互转:

function msForX(x) { ... }
function xForMs(ms) { ... }

所有可视元素,包括选区矩形、左右暗区、左右把柄、播放头和音频轨,都绑定这套毫秒状态。

3.2 默认选中整段视频

当前实现中,打开视频后默认选区是整段视频:

selectionStartMs = 0
selectionEndMs = durationMs

缩略图是渐进生成的,durationMs 可能先后来自 MediaPlayer.duration 或缩略图携带的 durationMs。只要用户还没有手动拖动把柄,默认全选会跟随最新时长自动扩展。

3.3 重复导入重置

重复导入同一路径时,MediaPlayer.source 不变化,因此 onSourceChanged 不会触发。编辑器提供:

function resetForImportedFile() {
    playAfterSeekTimer.stop()
    player.stop()
    userPlaybackRequested = false
    userAdjustedSelection = false
    playbackFinished = false
    pendingSeek = false
    pendingSeekMs = 0
    selectionStartMs = 0
    selectionEndMs = durationMs > 0 ? durationMs : 0
    setDisplayPosition(0)
    if (player.seekable)
        player.seek(0)
    selectionChanged(selectionStartMs, selectionEndMs)
}

这个函数由 VideoTrimPage.qmlfileOpened() 信号里调用,用来清理旧播放、旧 seek、旧选区和旧播放完成状态。

3.4 只允许左右把柄调整选区

当前交互规则:

  • 默认选中整段;
  • 不允许在时间轴空白区域拖拽创建新选区;
  • 只能拖动选区左右把柄改变范围;
  • 点击选区外不做任何事;
  • 点击选区内只定位播放头,不改变选区。

左右把柄调用:

editor.editSelectionFromTimeline(newStartMs, editor.selectionEndMs)
editor.editSelectionFromTimeline(editor.selectionStartMs, newEndMs)

setSelection() 会判断当前播放头是否仍在新选区内:

var playheadStillInside = currentPlayhead >= selectionStartMs && currentPlayhead <= selectionEndMs
if (!playheadStillInside)
    requestPlayerSeek(selectionStartMs)

因此拖动选区时:

  • 如果播放头仍被新选区包含,播放头保持不动;
  • 如果播放头被排除到选区外,播放头才回到新选区起点。

3.5 选区内点击定位

时间轴上有一个点击区域:

MouseArea {
    id: timelineClickArea
    anchors.fill: parent
    onPressed: {
        var target = editor.positionFromTimelineClick(mouse.x)
        if (target < 0) {
            mouse.accepted = false
        } else {
            editor.seekTo(target)
            mouse.accepted = true
        }
    }
}

positionFromTimelineClick() 只接受选区内部时间点。选区外点击会被忽略,避免播放头跳出有效导出范围。

3.6 单一播放按钮

当前只保留一个播放按钮:

ActionButton {
    text: (player.playbackState === MediaPlayer.PlayingState || playAfterSeekTimer.running) ? "暂停" : "播放"
    enabled: editor.source !== "" && editor.hasSelection
    onClicked: editor.togglePlayback()
}

播放规则:

场景行为
正在播放点击按钮暂停
延迟播放等待中点击按钮取消播放
播放头在选区内从当前播放头播放
播放头不在选区内从选区起点播放
上一次已播到选区终点再次播放从选区起点开始
播放到选区终点自动停止并把播放头复位到选区起点

播放结束逻辑:

function finishPlayback() {
    playAfterSeekTimer.stop()
    playbackFinished = true
    player.stop()
    requestPlayerSeek(selectionStartMs)
}

这里使用 stop() 清理播放器状态,降低重复播放或重复导入时旧解码管线残留的概率。

3.7 平滑播放头

QtMultimedia 的 player.position 更新频率较低,直接绑定会让时间轴播放头一顿一顿。编辑器使用显示位置 displayPositionMs 做 UI 插值:

Timer {
    id: smoothPlayheadTimer
    interval: 16
    repeat: true
    running: editor.isPlaying
    onTriggered: {
        var elapsed = Date.now() - editor._lastSyncTime
        var interpolated = editor._lastSyncPosition + elapsed
        editor.displayPositionMs = editor.clampMs(interpolated)
    }
}

真实 seek 和导出仍然使用真实毫秒选区;插值只影响 UI 显示。

3.8 pending seek 防抖

Windows/Qt 5 Multimedia 可能在主动 seek 后先回调旧位置,再回调新位置,导致指针乱跳。编辑器用 pendingSeek 固定 UI 指针:

function requestPlayerSeek(ms) {
    pendingSeekMs = clampMs(Number(ms || 0))
    pendingSeek = true
    setDisplayPosition(pendingSeekMs)
    player.seek(pendingSeekMs)
}

在播放器真实位置接近目标后,才解除 pending 状态:

if (Math.abs(player.position - pendingSeekMs) <= 250) {
    pendingSeek = false
    setDisplayPosition(player.position)
}

3.9 视频画面兜底预览

初始预览由缩略图兜底图承担:

Image {
    source: editor.previewImageUrl
    visible: source !== ""
             && !editor.userPlaybackRequested
             && player.playbackState !== MediaPlayer.PlayingState
             && !playAfterSeekTimer.running
}

用户开始播放后,兜底图不再显示,避免覆盖 VideoOutput 造成“只有声音、画面不动”的错觉。

此前用于首帧预热的自动静音短播已移除,因为 Qt 5 Multimedia 在 Windows 上偶发会在自动短播、用户播放、重新导入之间产生状态抢占。

3.10 音频峰值轨

AudioPeakTrack 当前只负责显示,不参与选区编辑:

AudioPeakTrack {
    enabled: false
    peakData: editor.audioPeakData
    peakCount: editor.audioPeakCount
    selectionStartMs: Math.round(editor.selectionStartMs)
    selectionEndMs: Math.round(editor.selectionEndMs)
    positionMs: Math.round(editor.displayPositionMs)
}

禁用鼠标交互的原因是:当前需求要求只能通过缩略图时间轴的左右把柄修改选区。

四、异步时间轴缩略图

缩略图由 MediaAnalyzer::generateVideoTrimThumbnails() 生成。

4.1 独立状态

缩略图不使用全局 busy,而使用独立属性:

videoTrimThumbnailBusy
videoTrimThumbnailProgress
videoTrimThumbnailTotal
videoTrimThumbnails

这样缩略图生成期间,页面仍然可以播放、调整选区或执行其它操作。

4.2 取消旧任务和防止旧结果回写

每次刷新时间轴或切换文件都会取消旧任务:

cancelVideoTrimThumbnailJob();
const int jobId = ++m_videoTrimThumbnailJobId;
QSharedPointer<QAtomicInt> cancelFlag(new QAtomicInt(0));

旧 worker 可能无法被 QtConcurrent 立即强杀,所以使用两层保护:

机制作用
cancelFlagworker 内部尽快停止
jobId旧任务即使稍后完成,也不会回写 QML 状态

4.3 渐进式追加

缩略图生成采用“后台批量抽帧 + 主线程逐张追加”:

extractor.extractTimelineFrames(..., [=](int index, qint64 timestampMs, const QString &imagePath, const QVariantMap &) {
    if (cancelFlag->loadAcquire() != 0)
        return false;

    QVariantMap item;
    item.insert("timestampMs", timestampMs);
    item.insert("imageUrl", QUrl::fromLocalFile(imagePath).toString());
    item.insert("durationMs", durationMs);

    QMetaObject::invokeMethod(this, [this, jobId, item]() {
        if (jobId != m_videoTrimThumbnailJobId)
            return;
        appendVideoTrimThumbnail(item);
        setVideoTrimThumbnailState(m_videoTrimThumbnailBusy,
                                   m_videoTrimThumbnails.size(),
                                   m_videoTrimThumbnailTotal);
    }, Qt::QueuedConnection);
    return true;
}, &errorText);

优点:

  • UI 不必等全部缩略图完成后才显示;
  • 大文件抽帧不会阻塞主线程;
  • 进度条可以按已生成数量推进。

4.4 批量抽帧优化

VideoFrameExtractor::extractTimelineFrames() 只打开一次容器、只创建一次解码器,然后对多个时间点循环 seek 和解码:

avformat_open_input(...)
avformat_find_stream_info(...)
avcodec_open2(...)

for (timestamp in timestampsMs) {
    av_seek_frame(..., AVSEEK_FLAG_BACKWARD)
    avcodec_flush_buffers(codecCtx)
    av_read_frame(...)
    receiveAndSaveFrame(...)
}

这个实现比“每张图单独打开一次视频”快得多,尤其适合大视频文件。

时间轴缩略图不要求精确到目标帧。实现会 seek 到目标时间之前的关键帧,然后取第一张可解码画面,避免为精确缩略图向后解码很长 GOP。

五、异步音频峰值波形

点击“生成音频波形”时,页面调用:

MediaAnalyzer::generateVideoTrimAudioPeaks(4096)

5.1 为什么不用完整 PCM

完整 PCM 对大视频很重。例如 48kHz、双声道、16-bit 音频:

每秒约 48000 * 2 * 2 = 192KB
1 小时约 691MB

如果把完整 PCM 作为 Q_PROPERTY 传给 QML,会造成明显内存和绑定压力。

5.2 峰值桶格式

AudioDecoder::decodeToPeaks() 把音频压缩成固定数量峰值桶:

bool decodeToPeaks(const QString &filePath,
                   int peakCount,
                   QByteArray *peakData,
                   QVariantMap *peakInfo,
                   QString *errorText,
                   const std::function<bool()> &shouldCancel) const;

每个桶保存:

qint16 min
qint16 max

4096 桶约 16KB,足够绘制裁剪页上的波形轮廓。

5.3 任务取消

音频峰值任务也使用 jobId 和 cancelFlag:

cancelVideoTrimAudioPeakJob();
const int jobId = ++m_videoTrimAudioPeakJobId;
QSharedPointer<QAtomicInt> cancelFlag(new QAtomicInt(0));

切换文件或重复导入时,旧解码任务会尽快退出;即使稍后返回,也不会覆盖新文件的峰值数据。

六、导出调度:MediaAnalyzer::trimCurrentVideoCopy()

QML 导出入口:

Q_INVOKABLE bool trimCurrentVideoCopy(qint64 startMs,
                                      qint64 endMs,
                                      const QString &filePath,
                                      const QString &format,
                                      bool keepAudio,
                                      bool keepSubtitle,
                                      bool copyMetadata);

它负责:

  1. 校验当前文件和选区;
  2. 规范化输出路径;
  3. 自动补全输出后缀;
  4. 设置全局 busy
  5. 在后台线程调用 VideoClipProcessor::trimCopy()
  6. 回主线程更新 videoTrimInfostatus

路径处理支持 file:// URL 和普通本地路径:

if (outputPath.startsWith("file:", Qt::CaseInsensitive))
    outputPath = QUrl(outputPath).toLocalFile();
if (outputPath.isEmpty())
    outputPath = defaultVideoTrimPath(format);

七、FFmpeg Stream Copy 裁剪

VideoClipProcessor::trimCopy() 是实际导出实现。

7.1 当前能力边界

当前只实现快速裁剪:

  • 不解码视频帧;
  • 不重新编码音频或视频;
  • 复制 packet;
  • 起点按关键帧附近对齐;
  • 输出 packet 时间戳从 0 附近重新开始。

7.2 流保留策略

当前保留策略:

流类型是否保留
视频始终保留
音频keepAudio 决定
字幕keepSubtitle 决定
附件/data/未知流暂不复制

7.3 关键帧 seek

stream copy 不能从任意 P/B 帧开始复制。导出前会 seek 到目标起点之前的关键帧:

av_seek_frame(inCtx, videoStreamIndex, seekTarget, AVSEEK_FLAG_BACKWARD);

因此实际写入起点可能早于用户选择起点。结果面板会展示请求起点和实际关键帧起点,帮助用户理解差异。

7.4 packet 写入和时间戳重写

导出循环读取输入 packet,跳过未映射流,把保留流写入输出容器:

av_packet_rescale_ts(packet, inStream->time_base, outStream->time_base);
av_interleaved_write_frame(outCtx, packet);

写入前会平移 pts/dts,让新文件从 0 附近开始播放,并尽量保持音视频字幕之间的相对同步。

7.5 容器兼容性

由于不重编码,源编码必须被目标容器接受。常见建议:

  • MP4 适合 H.264/AAC 等常见组合;
  • MKV 更宽容,适合作为失败时的备选;
  • WebM 对编码组合要求更严格;
  • 字幕流失败时可以取消“保留字幕”再导出。

八、稳定性处理

8.1 重复导入相同文件

问题:同一路径重复导入时,currentFileChangedMediaPlayer.sourceChanged 都可能不触发。

解决:

  • MediaAnalyzer::openFile() 每次成功都 emit fileOpened()
  • VideoTrimPage.qml 监听 fileOpened()
  • VideoTrimEditor.resetForImportedFile() 主动清播放器、播放头、pending seek 和选区;
  • 页面重新触发缩略图生成。

8.2 防止旧异步任务污染新文件

缩略图和音频峰值任务都使用:

jobId + QAtomicInt cancelFlag

切换文件时旧任务可以继续在后台自然退出,但回主线程时会因 jobId 不匹配而被丢弃。

8.3 避免 UI 卡顿

耗时操作均放到后台:

操作线程策略
时间轴缩略图QtConcurrent 后台执行,逐张回主线程追加
音频峰值QtConcurrent 后台流式解码,完成后一次回传小数据
视频导出QtConcurrent 后台 stream copy

8.4 避免“只有声音,画面不动”

当前采取了几项处理:

  • 播放时隐藏兜底 Image,避免覆盖 VideoOutput
  • 去掉自动静音短播预热,减少 Qt Multimedia 状态抢占;
  • 播放启动前用 pendingSeek 固定目标位置;
  • 如果播放器真实位置已接近播放头,不重复 seek,减少后端 seek/play 竞态;
  • 播放结束和重复导入时使用 stop() 清理播放器状态。

九、当前限制

限制说明
非精确裁剪stream copy 起点按关键帧附近对齐
不支持空白拖选当前交互只允许左右把柄调整选区
音频轨只显示AudioPeakTrack 在裁剪页禁用鼠标交互
无磁盘缓存缩略图和峰值只服务当前会话,暂不做持久缓存
无导出进度导出只显示 busy/status,未暴露 packet 级进度

十、后续扩展方向

能力推荐方向
精确裁剪新增 trimReencode(),走解码、裁剪、编码、mux 管线
关键帧可视化抽帧或探测阶段额外提取关键帧时间点
缩略图磁盘缓存以文件路径、mtime、大小、时长、参数生成 cache key
导出进度和取消VideoClipProcessor 增加进度回调和取消标志
多片段导出复用 trimCopy(),外层批量调度多个选区
精细音频定位允许音频轨点击定位,但仍不允许其改变选区

十一、小结

当前视频片段裁剪实现的核心特点:

  • 页面打开后默认选中整段视频;
  • 只能通过左右把柄调整选区;
  • 点击选区内只定位播放头;
  • 单一播放按钮控制播放/暂停;
  • 播放头使用 16ms 插值,移动更平滑;
  • 缩略图异步批量抽帧、逐张显示;
  • 音频波形使用轻量 min/max 峰值桶;
  • 导出使用 FFmpeg stream copy,速度快、画质无损;
  • 通过 fileOpened()、jobId、cancelFlag、pendingSeek 和播放器 stop 处理重复导入、旧任务回写和播放状态竞态。

这套实现的定位是稳定、高效的快速裁剪。它明确承认关键帧对齐限制,并把精确裁剪留给后续单独的重编码管线实现。

以上就是基于Qt + FFmpeg实现视频片段裁剪功能的详细内容,更多关于Qt FFmpeg视频片段裁剪的资料请关注脚本之家其它相关文章!

相关文章

  • C++实现神经网络框架SimpleNN的详细过程

    C++实现神经网络框架SimpleNN的详细过程

    本来自己想到用C++实现神经网络主要是想强化一下编码能力并入门深度学习,对C++实现神经网络框架SimpleNN的详细过程感兴趣的朋友一起看看吧
    2021-08-08
  • C++中的按位与&、按位与或|、按位异或^运算符详解

    C++中的按位与&、按位与或|、按位异或^运算符详解

    这篇文章主要介绍了C++中的按位与&、按位与或|、按位异或^运算符,是C++入门学习中的基础知识,需要的朋友可以参考下
    2016-01-01
  • C++ Boost Serialization库超详细奖金额

    C++ Boost Serialization库超详细奖金额

    Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
    2022-12-12
  • C++实现扫雷、排雷小游戏

    C++实现扫雷、排雷小游戏

    这篇文章主要为大家详细介绍了C++实现扫雷、排雷小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • C++基于QWidget和QLabel实现图片缩放,拉伸与拖拽

    C++基于QWidget和QLabel实现图片缩放,拉伸与拖拽

    这篇文章主要为大家详细介绍了C++如何基于QWidget和QLabel实现图片缩放、拉伸与拖拽等功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-02-02
  • C语言程序环境中的预处理详解

    C语言程序环境中的预处理详解

    这篇文章主要为大家详细介绍了C语言程序环境中的预处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-02-02
  • C/C++ 获取自身IP与域名片段的示例代码

    C/C++ 获取自身IP与域名片段的示例代码

    这篇文章主要介绍了C/C++ 获取自身IP与域名片段的示例代码,帮助大家更好的理解和学习C/C++编程,感兴趣的朋友可以了解下
    2020-10-10
  • C++实现查找中位数的O(N)算法和Kmin算法

    C++实现查找中位数的O(N)算法和Kmin算法

    这篇文章主要介绍了C++实现查找中位数的O(N)算法和Kmin算法,对于C++程序算法设计有一定的借鉴价值,需要的朋友可以参考下
    2014-09-09
  • C++数据结构之实现邻接表

    C++数据结构之实现邻接表

    这篇文章主要为大家详细介绍了C++数据结构之实现邻接表,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • 关于C++中0是十进制还是八进制的问题

    关于C++中0是十进制还是八进制的问题

    本篇文章中,小编将为大家介绍关于C++中0是十进制还是八进制的问题,有需要的朋友可以参考一下
    2013-04-04

最新评论