FFMPEG录屏(15)---- WGC 捕获桌面(三) WGC(Windows Graphics Capture)采集

news/2024/7/10 22:18:04 标签: ffmpeg, windows, c++

前言

前两篇已经通过官网Demo对WGC采集方式进行了验证和扩展,现在开始正片~

FFMPEG录屏(13)---- WGC 捕获桌面(一) 改造官网Demo
FFMPEG录屏(14)---- WGC 捕获桌面(二) Copy数据到CPU

参考资料

New Ways to do Screen Capture
Windows.UI.Composition-Win32-Samples
WebRtc WGC

限制

WindowsGraphicsCapture APIs first shipped in the Windows 10 April 2018 Update (1803). These APIs were built for developers who depended on screen capture functionality for their modern applications without depending on restricted capabilities. These APIs enable capture of application windows, displays, and environments in a secure, easy to use way with the use of a system picker UI control.

C++/WinRT is an entirely standard modern C++17 language projection for Windows Runtime (WinRT) APIs, implemented as a header-file-based library, and designed to provide you with first-class access to the modern Windows API. With C++/WinRT, you can author and consume Windows Runtime APIs using any standards-compliant C++17 compiler. The Windows SDK includes C++/WinRT; it was introduced in version 10.0.17134.0 (Windows 10, version 1803).

综上想要基于最新的捕获技术WindowsGraphicsCapture进行图像捕获有以下限制

  • 系统版本不低于10.0.17134.0 (Windows 10, version 1803)
  • 相关接口均基于微软的新一代运行时库接口C++/WinRT,而且是最低要求 C++17

目前大多数项目和很多成熟项目中一般C++版本最高也才到C++14,所以我们一般会把WGC功能封装进一个动态库中,在使用时进行动态加载。

导出

  1. export.h中声明宏用以导出函数
#ifdef AMRECORDER_IMPORT
#define AMRECORDER_API extern "C" __declspec(dllimport)
#else
#define AMRECORDER_API extern "C" __declspec(dllexport)
#endif
  1. export.h中定义WGC模块接口类wgc_session
namespace am {

class wgc_session {
public:
  struct wgc_session_frame {
    unsigned int width;
    unsigned int height;
    unsigned int row_pitch;

    const unsigned char *data;
  };

  class wgc_session_observer {
  public:
    virtual ~wgc_session_observer() {}
    virtual void on_frame(const wgc_session_frame &frame) = 0;
  };

public:
  virtual void release() = 0;

  virtual int initialize(HWND hwnd) = 0;
  virtual int initialize(HMONITOR hmonitor) = 0;

  virtual void register_observer(wgc_session_observer *observer) = 0;

  virtual int start() = 0;
  virtual int stop() = 0;

  virtual int pause() = 0;
  virtual int resume() = 0;

protected:
  virtual ~wgc_session(){};
};

} // namespace am
  1. export.h中定义函数用以判断当前系统是否支持使用WGC进行采集
AMRECORDER_API bool wgc_is_supported();
  1. export.h中定义接口函数用以创建wgc_session类实例
AMRECORDER_API am::wgc_session *wgc_create_session();

实现

  1. 在预编译头文件pch.h中引入使用到的头文件
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for
// future builds. This also affects IntelliSense performance, including code
// completion and many code browsing features. However, files listed here are
// ALL re-compiled if any one of them is updated between builds. Do not add
// files here that you will be updating frequently as this negates the
// performance advantage.

#ifndef PCH_H
#define PCH_H

// add headers that you want to pre-compile here
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files

#include <Unknwn.h>
#include <inspectable.h>

// WinRT
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.Graphics.DirectX.h>
#include <winrt/Windows.Graphics.DirectX.Direct3d11.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <Windows.Graphics.Capture.Interop.h>

#include <DispatcherQueue.h>


// STL
#include <atomic>
#include <memory>

// D3D
#include <d3d11_4.h>
#include <dxgi1_6.h>
#include <d2d1_3.h>
#include <wincodec.h>

// windowws

#include <Windows.h>

#include "../Recorder/error_define.h"
#include "export.h"
#include "wgc_session_impl.h"

#endif // PCH_H

  1. export.cpp中实现函数wgc_is_supported()
#include "pch.h"

#include <winrt/Windows.Foundation.Metadata.h>

bool wgc_is_supported() {
  try {
    /* no contract for IGraphicsCaptureItemInterop, verify 10.0.18362.0 */
    return winrt::Windows::Foundation::Metadata::ApiInformation::
        IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 8);
  } catch (const winrt::hresult_error &) {
    return false;
  } catch (...) {
    return false;
  }
}
  1. wgc_session_impl.h中声明派生类 wgc_session_impl
#include <mutex>
#include <thread>

namespace am {
class wgc_session_impl : public wgc_session {
  struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1"))
      IDirect3DDxgiInterfaceAccess : ::IUnknown {
    virtual HRESULT __stdcall GetInterface(GUID const &id, void **object) = 0;
  };

  struct {
    union {
      HWND hwnd;
      HMONITOR hmonitor;
    };
    bool is_window;
  } target_{0};

public:
  wgc_session_impl();
  ~wgc_session_impl();

public:
  void release() override;

  int initialize(HWND hwnd) override;
  int initialize(HMONITOR hmonitor) override;

  void register_observer(wgc_session_observer *observer) override;

  int start() override;
  int stop() override;

  int pause() override;
  int resume() override;

private:
  auto create_d3d11_device();
  auto create_capture_item(HWND hwnd);
  auto create_capture_item(HMONITOR hmonitor);
  template <typename T>
  auto
  get_dxgi_interface(winrt::Windows::Foundation::IInspectable const &object);
  HRESULT create_mapped_texture(winrt::com_ptr<ID3D11Texture2D> src_texture,
                                unsigned int width = 0,
                                unsigned int height = 0);
  void
  on_frame(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const
               &sender,
           winrt::Windows::Foundation::IInspectable const &args);
  void on_closed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const &,
                 winrt::Windows::Foundation::IInspectable const &);

  int initialize();
  void cleanup();

  void message_func();

private:
  std::mutex lock_;
  bool is_initialized_ = false;
  bool is_running_ = false;
  bool is_paused_ = false;

  wgc_session_observer *observer_ = nullptr;

  // wgc
  winrt::Windows::Graphics::Capture::GraphicsCaptureItem capture_item_{nullptr};
  winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session_{
      nullptr};
  winrt::Windows::Graphics::SizeInt32 capture_frame_size_;

  winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice
      d3d11_direct_device_{nullptr};
  winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
  winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};

  std::atomic<bool> cleaned_ = false;
  winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
      capture_framepool_{nullptr};
  winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
      FrameArrived_revoker capture_framepool_trigger_;
  winrt::Windows::Graphics::Capture::GraphicsCaptureItem::Closed_revoker
      capture_close_trigger_;

  // message loop
  std::thread loop_;
  HWND hwnd_ = nullptr;
};

template <typename T>
inline auto wgc_session_impl::get_dxgi_interface(
    winrt::Windows::Foundation::IInspectable const &object) {
  auto access = object.as<IDirect3DDxgiInterfaceAccess>();
  winrt::com_ptr<T> result;
  winrt::check_hresult(
      access->GetInterface(winrt::guid_of<T>(), result.put_void()));
  return result;
}


} // namespace am
  1. 在中实现wgc_session_impl相关逻辑
#include "pch.h"

#include <functional>
#include <memory>

#define CHECK_INIT                                                             \
  if (!is_initialized_)                                                        \
  return AM_ERROR::AE_NEED_INIT

#define CHECK_CLOSED                                                           \
  if (cleaned_.load() == true) {                                               \
    throw winrt::hresult_error(RO_E_CLOSED);                                   \
  }

extern "C" {
HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(
    ::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice);
}

namespace am {

wgc_session_impl::wgc_session_impl() {}

wgc_session_impl::~wgc_session_impl() {
  stop();
  cleanup();
}

void wgc_session_impl::release() { delete this; }

int wgc_session_impl::initialize(HWND hwnd) {
  std::lock_guard locker(lock_);

  target_.hwnd = hwnd;
  target_.is_window = true;
  return initialize();
}

int wgc_session_impl::initialize(HMONITOR hmonitor) {
  std::lock_guard locker(lock_);

  target_.hmonitor = hmonitor;
  target_.is_window = false;
  return initialize();
}

void wgc_session_impl::register_observer(wgc_session_observer *observer) {
  std::lock_guard locker(lock_);
  observer_ = observer;
}

int wgc_session_impl::start() {
  std::lock_guard locker(lock_);

  if (is_running_)
    return AM_ERROR::AE_NO;

  int error = AM_ERROR::AE_WGC_CREATE_CAPTURER_FAILED;

  CHECK_INIT;
  try {
    if (!capture_session_) {
      auto current_size = capture_item_.Size();
      capture_framepool_ =
          winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
              CreateFreeThreaded(d3d11_direct_device_,
                                 winrt::Windows::Graphics::DirectX::
                                     DirectXPixelFormat::B8G8R8A8UIntNormalized,
                                 2, current_size);
      capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
      capture_frame_size_ = current_size;
      capture_framepool_trigger_ = capture_framepool_.FrameArrived(
          winrt::auto_revoke, {this, &wgc_session_impl::on_frame});
      capture_close_trigger_ = capture_item_.Closed(
          winrt::auto_revoke, {this, &wgc_session_impl::on_closed});
    }

    if (!capture_framepool_)
      throw std::exception();

    is_running_ = true;

    // we do not need to crate a thread to enter a message loop coz we use
    // CreateFreeThreaded instead of Create to create a capture frame pool,
    // we need to test the performance later
    // loop_ = std::thread(std::bind(&wgc_session_impl::message_func, this));

    capture_session_.StartCapture();

    error = AM_ERROR::AE_NO;
  } catch (winrt::hresult_error) {
    return AM_ERROR::AE_WGC_CREATE_CAPTURER_FAILED;
  } catch (...) {
    return AM_ERROR::AE_WGC_CREATE_CAPTURER_FAILED;
  }

  return error;
}

int wgc_session_impl::stop() {
  std::lock_guard locker(lock_);

  CHECK_INIT;

  is_running_ = false;

  if (loop_.joinable())
    loop_.join();

  if (capture_framepool_trigger_)
    capture_framepool_trigger_.revoke();

  if (capture_session_) {
    capture_session_.Close();
    capture_session_ = nullptr;
  }

  return AM_ERROR::AE_NO;
}

int wgc_session_impl::pause() {
  std::lock_guard locker(lock_);

  CHECK_INIT;
  return AM_ERROR::AE_NO;
}

int wgc_session_impl::resume() {
  std::lock_guard locker(lock_);

  CHECK_INIT;
  return AM_ERROR::AE_NO;
}

auto wgc_session_impl::create_d3d11_device() {
  auto create_d3d_device = [](D3D_DRIVER_TYPE const type,
                              winrt::com_ptr<ID3D11Device> &device) {
    WINRT_ASSERT(!device);

    UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

    //#ifdef _DEBUG
    //	flags |= D3D11_CREATE_DEVICE_DEBUG;
    //#endif

    return ::D3D11CreateDevice(nullptr, type, nullptr, flags, nullptr, 0,
                               D3D11_SDK_VERSION, device.put(), nullptr,
                               nullptr);
  };
  auto create_d3d_device_wrapper = [&create_d3d_device]() {
    winrt::com_ptr<ID3D11Device> device;
    HRESULT hr = create_d3d_device(D3D_DRIVER_TYPE_HARDWARE, device);

    if (DXGI_ERROR_UNSUPPORTED == hr) {
      hr = create_d3d_device(D3D_DRIVER_TYPE_WARP, device);
    }

    winrt::check_hresult(hr);
    return device;
  };

  auto d3d_device = create_d3d_device_wrapper();
  auto dxgi_device = d3d_device.as<IDXGIDevice>();

  winrt::com_ptr<::IInspectable> d3d11_device;
  winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(
      dxgi_device.get(), d3d11_device.put()));
  return d3d11_device
      .as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
}

auto wgc_session_impl::create_capture_item(HWND hwnd) {
  auto activation_factory = winrt::get_activation_factory<
      winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
  auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
  winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
  interop_factory->CreateForWindow(
      hwnd,
      winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
      reinterpret_cast<void **>(winrt::put_abi(item)));
  return item;
}

auto wgc_session_impl::create_capture_item(HMONITOR hmonitor) {
  auto activation_factory = winrt::get_activation_factory<
      winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
  auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
  winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
  interop_factory->CreateForMonitor(
      hmonitor,
      winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
      reinterpret_cast<void **>(winrt::put_abi(item)));
  return item;
}

HRESULT wgc_session_impl::create_mapped_texture(
    winrt::com_ptr<ID3D11Texture2D> src_texture, unsigned int width,
    unsigned int height) {

  D3D11_TEXTURE2D_DESC src_desc;
  src_texture->GetDesc(&src_desc);
  D3D11_TEXTURE2D_DESC map_desc;
  map_desc.Width = width == 0 ? src_desc.Width : width;
  map_desc.Height = height == 0 ? src_desc.Height : height;
  map_desc.MipLevels = src_desc.MipLevels;
  map_desc.ArraySize = src_desc.ArraySize;
  map_desc.Format = src_desc.Format;
  map_desc.SampleDesc = src_desc.SampleDesc;
  map_desc.Usage = D3D11_USAGE_STAGING;
  map_desc.BindFlags = 0;
  map_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
  map_desc.MiscFlags = 0;

  auto d3dDevice = get_dxgi_interface<ID3D11Device>(d3d11_direct_device_);

  return d3dDevice->CreateTexture2D(&map_desc, nullptr,
                                    d3d11_texture_mapped_.put());
}

void wgc_session_impl::on_frame(
    winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender,
    winrt::Windows::Foundation::IInspectable const &args) {
  std::lock_guard locker(lock_);

  auto is_new_size = false;

  {
    auto frame = sender.TryGetNextFrame();
    auto frame_size = frame.ContentSize();

    if (frame_size.Width != capture_frame_size_.Width ||
        frame_size.Height != capture_frame_size_.Height) {
      // The thing we have been capturing has changed size.
      // We need to resize our swap chain first, then blit the pixels.
      // After we do that, retire the frame and then recreate our frame pool.
      is_new_size = true;
      capture_frame_size_ = frame_size;
    }

    // copy to mapped texture
    {
      auto frame_captured =
          get_dxgi_interface<ID3D11Texture2D>(frame.Surface());

      if (!d3d11_texture_mapped_ || is_new_size)
        create_mapped_texture(frame_captured);

      d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
                                          frame_captured.get());

      D3D11_MAPPED_SUBRESOURCE map_result;
      HRESULT hr = d3d11_device_context_->Map(
          d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
          0 /*coz we use CreateFreeThreaded, so we cant use flags
               D3D11_MAP_FLAG_DO_NOT_WAIT*/
          ,
          &map_result);
      if (FAILED(hr)) {
        OutputDebugStringW(
            (L"map resource failed: " + std::to_wstring(hr)).c_str());
      }

      // copy data from map_result.pData
      if (map_result.pData && observer_) {
        observer_->on_frame(wgc_session_frame{
            static_cast<unsigned int>(frame_size.Width),
            static_cast<unsigned int>(frame_size.Height), map_result.RowPitch,
            const_cast<const unsigned char *>(
                (unsigned char *)map_result.pData)});
      }


      d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
    }
  }

  if (is_new_size) {
    capture_framepool_.Recreate(d3d11_direct_device_,
                                winrt::Windows::Graphics::DirectX::
                                    DirectXPixelFormat::B8G8R8A8UIntNormalized,
                                2, capture_frame_size_);
  }
}

void wgc_session_impl::on_closed(
    winrt::Windows::Graphics::Capture::GraphicsCaptureItem const &,
    winrt::Windows::Foundation::IInspectable const &) {
  OutputDebugStringW(L"wgc_session_impl::on_closed");
}

int wgc_session_impl::initialize() {
  if (is_initialized_)
    return AM_ERROR::AE_NO;

  if (!(d3d11_direct_device_ = create_d3d11_device()))
    return AM_ERROR::AE_D3D_CREATE_DEVICE_FAILED;

  try {
    if (target_.is_window)
      capture_item_ = create_capture_item(target_.hwnd);
    else
      capture_item_ = create_capture_item(target_.hmonitor);

    // Set up
    auto d3d11_device = get_dxgi_interface<ID3D11Device>(d3d11_direct_device_);
    d3d11_device->GetImmediateContext(d3d11_device_context_.put());

  } catch (winrt::hresult_error) {
    return AM_ERROR::AE_WGC_CREATE_CAPTURER_FAILED;
  } catch (...) {
    return AM_ERROR::AE_WGC_CREATE_CAPTURER_FAILED;
  }

  is_initialized_ = true;

  return AM_ERROR::AE_NO;
}

void wgc_session_impl::cleanup() {
  std::lock_guard locker(lock_);

  auto expected = false;
  if (cleaned_.compare_exchange_strong(expected, true)) {
    capture_close_trigger_.revoke();
    capture_framepool_trigger_.revoke();

    if (capture_framepool_)
      capture_framepool_.Close();

    if (capture_session_)
      capture_session_.Close();

    capture_framepool_ = nullptr;
    capture_session_ = nullptr;
    capture_item_ = nullptr;

    is_initialized_ = false;
  }
}

LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
                            LPARAM l_param) {
  return DefWindowProc(window, message, w_param, l_param);
}

void wgc_session_impl::message_func() {
  const std::wstring kClassName = L"am_fake_window";

  WNDCLASS wc = {};

  wc.style = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc = DefWindowProc;
  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
  wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW);
  wc.lpszClassName = kClassName.c_str();

  if (!::RegisterClassW(&wc))
    return;

  hwnd_ = ::CreateWindowW(kClassName.c_str(), nullptr, WS_OVERLAPPEDWINDOW, 0,
                          0, 0, 0, nullptr, nullptr, nullptr, nullptr);
  MSG msg;
  while (is_running_) {
    while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
      if (!is_running_)
        break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    Sleep(10);
  }

  ::CloseWindow(hwnd_);
  ::DestroyWindow(hwnd_);
}

} // namespace am

需要注意的是,在创建frame_pool时,我们使用的是

Direct3D11CaptureFramePool::CreateFreeThreaded

而非官网Demo中使用的

Direct3D11CaptureFramePool::Create

因此并没有启动message_loop的线程,解释可以参考 Direct3D11CaptureFramePool.CreateFreeThreaded Method

  1. export.cpp中实现wgc_create_session
am::wgc_session *wgc_create_session() { return new am::wgc_session_impl(); }

至此我们已经完成了对WGC功能模块的封装,支持采集指定的桌面或窗口。


美中不足

  1. 鼠标支持,自Windows 10, version 2004 (introduced in 10.0.19041.0)才开始支持捕获鼠标。
    windows.graphics.capture.graphicscapturesession.iscursorcaptureenabled?view=winrt-22621">GraphicsCaptureSession.IsCursorCaptureEnabled Property
  2. 黄色边框去除,自Windows 10, version 2104 (introduced in 10.0.20348.0)才开始去除采集目标的黄色边框。
    windows.graphics.capture.graphicscapturesession.isborderrequired?view=winrt-22621">GraphicsCaptureSession.IsBorderRequired

结尾

完整DEMO已经上传,还是老地方 screen-recorder


http://www.niftyadmin.cn/n/318792.html

相关文章

MiniGPT-4 笔记

目录 简介 实现方法 效果及局限 参考资料 简介 MiniGPT-4 是前段时间由KAUST&#xff08;沙特阿卜杜拉国王科技大学&#xff09;开源的多模态大模型&#xff0c;去网站上体验了一下功能&#xff0c;把论文粗略的看了一遍&#xff0c;也做个记录。 论文摘要翻译&#xff1…

数字转汉字人民币

数字转汉字人民币 public class yyy {public static String convert(int num) {String[] units new String[]{"", "十", "百", "千", "万", "十万","百万", "千万", "亿"…

python 字符串的三种定义方式

Python是一种广泛使用的编程语言&#xff0c;特别是在数据分析、机器学习和人工智能领域。在Python中&#xff0c;字符串是一个非常重要的数据类型&#xff0c;可用来存储和操作文本数据。在Python中&#xff0c;有三种定义字符串的方式&#xff0c;本文将分别介绍它们。 1.使…

【ArrayList】| 深度剥析Java SE 源码合集Ⅵ

目录 一. 概述二. 🦁 深度探索1. 类图2. 属性3. 构造方法4. 添加单个元素5. 数组扩容6. 添加多个元素7. 移除单个元素8. 移除多个元素9. 查找单个元素10. 获得指定位置的元素11. 设置指定位置的元素12. 转换成数组13. 求哈希值14. 判断相等15. 清空数组16 序列化数组17. 反序…

网络安全从业人员职业发展和规划

1、为什么做这次分享&#xff1f; 2、人生周期三模型 3、职业生涯阶段划分 4、通用职业发展路径 5、当前安全行业前景如何&#xff1f; 6、安全就业行情如何&#xff1f; 7、安全行业就业市场岗位划分 8、什么是相对比较好的履历&#xff1f; 9、选择甲方还是选择乙方&#xf…

让chatgpt编写一个微信小程序的对话页面,它是这么整的,我懵了

请阅读这份文档https://tdesign.tencent.com/miniprogram/components/cell?tabdemo。使用腾讯的miniprogram tdesign UI库编写一个微信小程序的对话页面&#xff0c;要求消息在一侧&#xff0c;用户头像统一在左侧&#xff0c;每条消息底下有重用和复制按钮&#xff0c;点击可…

【Linux】2.3 编译器—gcc/g++ 项目自动化构建工具—make/Makefile

文章目录 「gcc/g」<预处理><编译><汇编><链接> 「Link?」什么是动态库、静态库 「make/Makefile」「补充&#xff1a;sudo」信任用户 「gcc/g」 vim&#xff1a;editorgcc&#xff1a;compiler &#xff08;C&#xff09;g&#xff1a;compiler &am…

链表详解 - C语言描述

目录 认识链表 链表的分类 链表的实现 单链表的增删查改 增操作 删操作 查操作 改操作 带头双向循环链表 认识链表 链表是一种物理存储上非连续&#xff0c;数据元素的逻辑顺序通过链表中的指针链接次序&#xff0c;实现的一种线性存储结构。链表由一系列节点(结点)组…