STL容器-- list的模拟实现(附源码)
STL容器-- list的模拟实现(附源码)
List的实现主要考察我们对list这一容器的理解,和代码的编写能力,通过上节对list容器的使用,我们对list容器已经有了一些基本的了解·,接下来就让我们来实现一些list容器常见的功能函数来加深我们对c++工程的理解:
一、基本框架的搭建
首先我们需要将基本的框架搭建好, 从之前我们学数据结构时,对list的一些认识并结合c++ stl标准可以得出:
- 定义节点的构造
- 明确指针的指向
- list的封装
根据c++中的标准我们可以得知,list时一个双向迭代器, 所以对应的, 我们需要定义的双向循环链表:
1.1节点的构造
template <class T>
struct ListNode
{T _data;ListNode<T>* _next;ListNode<T>* _prev;// 节点的构造函数ListNode(const T& val):_data(val),_next(nullptr),_prev(nullptr){}
};
在定义一个节点的时候,我们先让这个节点的_next and _prev
指针为空
1.2 list的封装
template <class T>
struct ListNode
{T _data;ListNode<T>* _next;ListNode<T>* _prev;// 节点的构造函数ListNode(const T& val = T()):_data(val),_next(nullptr),_prev(nullptr){}
};template<class T>
class list
{
public:typedef ListNode<T> node;void empty_init(){// zdl :: 在初始化一个空链表时, 先定义一个哨兵位_head = new node();_head->_next = _head;_head->_prev = _head;}list(){empty_init();}// functon define:}private:node* _head;
};
同时为了防止访问冲突,我们可以将我们自己实现的类,放在我们自己定义的命名域中:
namespace zdl
{template <class T>struct ListNode{T _data;ListNode<T>* _next;ListNode<T>* _prev;// 节点的构造函数ListNode(const T& val):_data(val),_next(nullptr),_prev(nullptr){}};template <class T>struct ListNode{T _data;ListNode<T>* _next;ListNode<T>* _prev;// 节点的构造函数ListNode(const T& val = T()):_data(val),_next(nullptr),_prev(nullptr){}};template<class T>class list{public:typedef ListNode<T> node;void empty_init(){// zdl :: 在初始化一个空链表时, 先定义一个哨兵位_head = new node();_head->_next = _head;_head->_prev = _head;}list(){empty_init();}// functon define:}private:node* _head;};
}
这样list的基本框架就搭建好了!!
二、节点的尾插和头插
2.1 节点的头插 (push_back)
通过前面数据结构的学习。我们已经清楚了链表的结构,在进行数据尾插时, 就只是在改变指针的指向罢了:
void push_back(const T& val){node* creat = new node(val);node* tail = _head->_prev;tail->_next = creat;creat->_prev = tail;creat->_next = _head;_head->_prev = creat; }
2.2 节点的头插(push_front)
节点的头插和尾插十分的相似,
这里我们就直接展示一下代码:
void push_front(const T& val){node* fnode = new node(val);fnode->_next = _head->_next;fnode->_prev = _head;_head->_next->_prev = fnode;_head->_next = fnode;}
2.3 数据插入验证
为了方便起见, 我这里再再这个类里面定义一个打印链表的函数:
void Print_list(){node* cur = _head->_next;while (cur != _head){cout << cur->_data << " ";cur = cur->_next;}cout << endl;}
完成了list容器的头插和尾插操作接下来我们可以来验证一下,我们的函数实现是否有问题:
#include"list.h"int main()
{zdl::list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_front(3);l1.Print_list();return 0;
}
通过运行可知, 这里的函数没有问题:
三、list迭代器的实现
list迭代器的功能和vetor的功能有很多相似的地方,但是二者在底层实现时,使用的不同的方法:
基于,list迭代器的特殊性质,我们会采用类封装的方式来满足list_iterator的特殊性质:
3.1迭代器基本功能的实现和封装处理
template<class T>struct list_iterator{typedef ListNode<T> node;typedef list_iterator<T> self;node* _nd;list_iterator(node* nd):_nd(nd){}// zdl:: 实现迭代器的基本功能:// 解引用数值T& operator*(){return _nd->_data;}// zdl:: 前置++ / --self& operator++(){_nd = _nd->_next;return *this;}self& operator--(){_nd = _nd->_prev;return *this;}// zdl:: 后置 ++ / --self operator++(int){self tmp = _nd;_nd = _nd->_next;return tmp;}self operator--(int){self tmp = _nd;_nd = _nd->_prev;return tmp;}// zdl:: != 运算bool operator!=(const self& s){return _nd != s._nd;}};
实现了list_iterator的功能后,我们就只需要将他封装道list类中就可以正常使用了:
基本的逻辑都是十分的简单的接下来我们来简单的验证一代码是否可行:
3.2 对结构体(类)的解引用
对
->
运算符的重载
大家现在可以想象一下这样的场景,假设我现在在llist
中存储的不是基础类型的元素,而是类等较为复杂的对象时,我们应当怎么正常的使用这个类对象的成员呢?
这时,我们就可以考虑对->
运算符重载, 通过返回对象的地址,来访问成员变量和成员函数等!
下面我们就来举个例子:
假设我现在我要在list
中存储pos
类:
struct pos
{int row;int col;//构造函数pos(const int& x = 0, const int& y = 0):row(x),col(y){}//成员函数void pos_show(){printf("(%d, %d) ",row, col);}
};
我们就只需要重载->
运算符:
T* operator->()
{return &_nd->_data;
}
运行后可以得到:
但是大家可能会觉得很奇怪,通过->
重载,返回的只是_data
的地址,为什么能直接访问到元素呢?不应该使用it->->
解引用两次才行啊?
这个其实就只是c++
便准下为我们提供的特殊语法, 通过这样的规定使我们能够直接访问到元素, 当然c++
也是支持这样的写法的:
但是不支持这样的写法:
3.3const
迭代器的实现与模板参数的巧用
前面我们已经实现了,可读可写的迭代器,现在我们就来实现一下只读迭代器:
const iterator
,
其实从功能上看,这个迭代器与原来的迭代器十分的相似,只是不能对指向对象的值进行修改,因此我们就只需要对* 和 ->
运算符重载的时候稍加修改就可以得到我们想要的结果,即再创建一个类:
其他的共同功能粗需要改动,只需要改动下面两个重载函数:
const T& operator*()
{return _nd->_data;
}
const T* operator->()
{return &_nd->_data;
}
紧接着我们还需要在llist
类中实现const对象
专用的begin()、end()
接下来我们来验证一下效果:
现在consr iterator
也实现好了,但是这里依然还存在问题,这样我们将相当于为迭代器实现了两个类,这两个类的攻击能还高度重合,这并没有,体现出模板函数的简洁性,因此我们还可以通过其他的方式来优化我们现在的代码。
通过对源码的分析,参照我们或许可以从中得到一些启发, 从源码中可知我们可以得知,通过对模板参数的巧用就可以的实现代码的简化:
接下来我们就只需要将代码稍加改动就可以实现我们的目的:
接下来,我们再次运行看看是否可以达到简化代码的效果:
可以发现现在的代码依然有效,代码简化成功!!
四、丰富构造函数、list增删查改的实现
现在我们已经完成了list
的简单构造函数,现在我们就可以参照 c++ library
完成其他的构造函数:
4.1 list(size_t n, const T& x = T())
我们要实现的这个函数和标准库中的一样,并且直接复用我峨嵋你已经实现的函数就好了:
list(size_t n, const T& x = T())
{for (int i = 0; i < n; i++){push_back(x);}
}
直接来演示一下的效果:
4.2 拷贝构造
我们就只需要将被拷贝链表的元素一个一个的拷贝进链表就行了:
list(const list<T>& l1)
{empty_init();for (auto& e : l1) push_back(e);
}
我们来测试一下效果:
4.3 插入函数的实现(insert)
想要在这个双向链表中插入节点,我们就需要
-
待插入的值
-
待插入位置
所以,insert函数定义为: void insert(iterator pos, const T& val = T())
iterator insert(iterator pos, const T& val = T())
{node* cur = pos._nd;node* prev = pos._nd->_prev;node* insrt = new node(val);prev->_next = insrt;insrt->_prev = prev;insrt->_next = cur;cur->_prev = insrt;return iterator(cur->_prev);}
这个函数也十分的简单如果,有的uu还没有接触过链表,或者是已经忘了链表的增删查改,可以移步去看看我之前的博客:链表的介绍
4.4链表的删除(earse)
关于erase和函数的定义,我们就只需要拿到需要删除的位置就可以了, 所以定义为:void erase(iterator pos)
iterator erase(iterator pos)
{assert(pos != end()); //! 注意不能够将哨兵位删除!!node* cur = pos._nd;node* prev = cur->_prev;prev->_next = cur->_next;cur->_next->_prev = prev;delete cur;return iterator(prev->_next);}
现在我们就直接来测试一下这个函数是否可以满足我们的要求:
由此可知我们实现的都没有问题。
4.5链表元素的查找(find)
定义find函数时,我们就只需要给函数一个特定的需要查找到的值,然后使这个函数返回这个元素的位置(迭代器)
iterator find(const T& val)
{auto it = begin();while (it != end() && *it != val){it++;}if (*it == val) return it;return nullptr;}
4.6 头删和尾删操作
前面我们已经实现了earse
现在我们就只需要对这个和拿书尽心你给复用就好了:
void pop_front()
{erase(++end());
}
void pop_back()
{erase(--end());
}
接下来我们就直接来演示这个函数是否有用:
五、析构函数与clear函数
最后我们就来实现一下list
的析构函数,我们还是继续函数的复用:
// zdl:: 析构类函数的实现:~list(){// ! 不仅要将所有的数值删除还需要将哨兵位也清除!clear();delete _head;_head = nullptr; }void clear(){// !! 不能删除哨兵位,就只是将所有的数值清空。auto it = begin();while (it != end()) it = erase(it);}
现在我们就将已有的链表清空试试:
六、代码展示
list.h
#pragma once #include<iostream>
#include<cassert>
using namespace std;
namespace zdl
{template <class T>struct ListNode{T _data;ListNode<T>* _next;ListNode<T>* _prev;// 节点的构造函数ListNode(const T& val = T()):_data(val),_next(nullptr),_prev(nullptr){}};template<class T, class Ref, class Ptr>struct list_iterator{typedef ListNode<T> node;typedef list_iterator<T, Ref, Ptr> self;node* _nd;list_iterator(node* nd):_nd(nd){}// zdl:: 实现迭代器的基本功能:// 解引用数值Ref operator*(){return _nd->_data;}Ptr operator->(){return &_nd->_data;}// zdl:: 前置++ / --self& operator++(){_nd = _nd->_next;return *this;}self& operator--(){_nd = _nd->_prev;return *this;}self& operator+(size_t n){while (n--){_nd = _nd->_next;}return *this;}self& operator-(size_t n){while (n--){_nd = _nd->_prev;}return *this;}// zdl:: 后置 ++ / --self operator++(int){self tmp = _nd;_nd = _nd->_next;return tmp;}self operator--(int){self tmp = _nd;_nd = _nd->_prev;return tmp;}// zdl:: != 运算bool operator!=(const self& s){return _nd != s._nd;}};template<class T>class list{public:typedef ListNode<T> node;typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;void empty_init(){// zdl :: 在初始化一个空链表时, 先定义一个哨兵位_head = new node();_head->_next = _head;_head->_prev = _head;}list(){empty_init();}list(const list<T>& l1){empty_init();for (auto& e : l1) push_back(e);}list(size_t n, const T& x = T()){empty_init();for (size_t i = 0; i < n; i++){push_back(x);}}void push_back(const T& val){node* creat = new node(val);node* tail = _head->_prev;tail->_next = creat;creat->_prev = tail;creat->_next = _head;_head->_prev = creat; }void push_front(const T& val){node* fnode = new node(val);fnode->_next = _head->_next;fnode->_prev = _head;_head->_next->_prev = fnode;_head->_next = fnode;}void Print_list(){node* cur = _head->_next;while (cur != _head){cout << cur->_data << " ";cur = cur->_next;}cout << endl;}// zdl:: 增删查改的实现iterator insert(iterator pos, const T& val = T()){node* cur = pos._nd;node* prev = pos._nd->_prev;node* insrt = new node(val);prev->_next = insrt;insrt->_prev = prev;insrt->_next = cur;cur->_prev = insrt;return iterator(cur->_prev);}iterator erase(iterator pos){assert(pos != end()); //! 注意不能够将哨兵位删除!!node* cur = pos._nd;node* prev = cur->_prev;prev->_next = cur->_next;cur->_next->_prev = prev;delete cur;return iterator(prev->_next);}iterator find(const T& val){auto it = begin();while (it != end() && *it != val){it++;}if (*it == val) return it;return nullptr;}void pop_front(){erase(begin());}void pop_back(){erase(--end());}// zdl:: 析构类函数的实现:~list(){// ! 不仅要将所有的数值删除还需要将哨兵位也清除!clear();delete _head;_head = nullptr; }void clear(){// !! 不能删除哨兵位,就只是将所有的数值清空。auto it = begin();while (it != end()) it = erase(it);}
// zdl:: 使用类来模拟迭代器的行为iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}private:node* _head;};
}
test.cpp
#include"list.h"
#include<list>
struct pos
{int row;int col;//构造函数pos(const int& x = 0, const int& y = 0):row(x),col(y){}//成员函数void pos_show(){printf("(%d, %d) ",row, col);}
};
int main()
{// zdl::list<pos> l1;// pos p[] = {{1, 2}, {3, 4}, {5, 6}};// for (int i = 0; i < 3; i++) l1.push_back(p[i]);// auto it = l1.begin();// while (it != l1.end())// {// it->pos_show();// it++;// cout << endl;// }// cout << endl;// zdl::list<int> l2(5, 4);// for (auto&i : l2) cout << i << " ";// cout << endl;// zdl::list<int> l4(3, 3);// l4.Print_list();// l4.insert(l4.begin() + 2, 2);// l4.Print_list();// l4.erase(l4.begin() + 2);// l4.Print_list();// zdl::list<int>l5;// for (int i = 1; i <= 10; i++) l5.push_back(i);// cout << "原数组:" << endl;// l5.Print_list();// // zdl:: 现在我们要借助 find + erase函数来删除元素:5// l5.erase(l5.find(5));// cout << "删除后:" << endl;// l5.Print_list();// zdl:: 进行头删和尾删// cout << "进行尾删和头删后:" << endl;// l5.pop_back();// l5.pop_front();// l5.Print_list();// cout << "现在将所有的元素都删除!" << endl;// l5.clear();// l5.Print_list();zdl::list<int> l1(10, 10);cout <<"l1:" << endl;l1.Print_list();zdl::list<int> l2(l1);cout << "l2:" << endl;l2.Print_list();return 0;
}
好,常用的接口我们就实现了,list学习到此告一段落,再见!!
相关文章:

STL容器-- list的模拟实现(附源码)
STL容器-- list的模拟实现(附源码) List的实现主要考察我们对list这一容器的理解,和代码的编写能力,通过上节对list容器的使用,我们对list容器已经有了一些基本的了解,接下来就让我们来实现一些list容器常见…...
python——句柄
一、概念 句柄指的是操作系统为了标识和访问对象而提供的一个标识符,在操作系统中,每个对象都有一个唯一的句柄,通过句柄可以访问对象的属性和方法。例如文件、进程、窗口等都有句柄。在编程中,可以通过句柄来操作这些对象&#x…...
KubeSphere 与 Pig 微服务平台的整合与优化:全流程容器化部署实践
一、前言 近年来,为了满足越来越复杂的业务需求,我们从传统单体架构系统升级为微服务架构,就是把一个大型应用程序分割成可以独立部署的小型服务,每个服务之间都是松耦合的,通过 RPC 或者是 Rest 协议来进行通信,可以按照业务领域来划分成独立的单元。但是微服务系统相对…...

ESP8266-01S、手机、STM32连接
1、ESP8266-01S的工作原理 1.1、AP和STA ESP8266-01S为WIFI的透传模块,主要模式如下图: 上节说到,我们需要用到AT固件进行局域网应用(ESP8266连接的STM32和手机进行连接)。 ESP8266为一个WiFi透传模块,和…...

Web开发 -前端部分-CSS-2
一 长度单位 代码实现: <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document<…...

【QT用户登录与界面跳转】
【QT用户登录与界面跳转】 1.前言2. 项目设置3.设计登录界面3.1 login.pro参数3.2 界面设置3.2.1 登录界面3.2.2 串口主界面 4. 实现登录逻辑5.串口界面6.测试功能7.总结 1.前言 在Qt应用程序开发中,实现用户登录及界面跳转功能是构建交互式应用的重要步骤之一。下…...

记录一次关于spring映射postgresql的jsonb类型的转化器事故,并使用hutool的JSONArray完成映射
事件的起因是这样的,那次事故发生的起因是因为WebFlux和postgreSQL去重新做鱼皮的鱼图图项目(鱼图图作业)。 在做到picture表的时候,发现postgreSQL中有个jsonb的类型可以更好的支持json数组。 出于锻炼新技术的目的,…...

基于 HTML5 Canvas 制作一个精美的 2048 小游戏--day2
为了使 2048 游戏的设计更加美观和用户友好,我们可以进行以下几项优化: 改善颜色方案:使用更温馨的颜色组合。添加动画效果:为方块的移动和合并添加渐变效果。优化分数显示:在分数增加时使用动画效果。 以下是改进后…...

Django框架:python web开发
1.环境搭建: (a)开发环境:pycharm (b)虚拟环境(可有可无,优点:使用虚拟环境可以把使用的包自动生成一个文件,其他人需要使用时可以直接选择导入包ÿ…...

MySQL、HBase、ES的特点和区别
MySQL:关系型数据库,主要面向OLTP,支持事务,支持二级索引,支持sql,支持主从、Group Replication架构模型(本文全部以Innodb为例,不涉及别的存储引擎)。 HBase࿱…...

联发科MTK6762/MT6762安卓核心板_4G智能模块应用
MT6762安卓核心板是一款工业级高性能、可运行 android9.0 操作系统的 4G智能模块。MT6762平台打造具备 AI 体验、先进双摄像头拍摄效果且具备丰富连接功能的智能手机主板。 MT6762安卓核心板 是一款髙性能低功耗的 4G 全网通安卓智能模块。此模块支持 2G/3G/4G 移动,…...
Windows7系统下载安装Source Code Pro字库
Source Code Pro字库介绍 Source Code Pro是由Adobe推出的一款专为代码展示和编写设计的开源等宽字体。它不仅在编程社区中广受好评,还被广泛应用于各种编辑器环境中,以提升代码的可读性和编程体验。 Source Code Pro的设计充分考虑了编程符号的呈…...
Navicat 17 功能简介 | 商业智能 BI
Navicat 17 功能简介 | 商业智能BI 随着 17 版本的发布,Navicat 也带来了众多的新特性,包括兼容更多数据库、全新的模型设计、可视化智能 BI、智能数据分析、可视化查询解释、高质量数据字典、增强用户体验、扩展 MongoDB 功能、轻松固定查询结果、便捷U…...

C# winodw TableLayoutPanel 料盒生产状态UI自动生成
料盒生产状态UI自动生成,效果如下 以前公司项目的这些都是手动拖控件做的。每个设备的料盒数量不一样,层数不一样时都要发好几个小时去改相关细节和代码。上次改了一次。这个又来了。上次就有想法做成根据参数自动生成。但项目时间有限有没有去深入思路和…...

提示词的艺术----AI Prompt撰写指南(个人用)
提示词的艺术 写在前面 制定提示词就像是和朋友聊天一样,要求我们能够清楚地表达问题。通过这个过程,一方面要不断练习提高自己地表达能力,另一方面还要锻炼自己使用更准确精炼的语言提出问题的能力。 什么样的提示词有用? 有…...
哪些前端打印插件可以实现监听用户选择了打印还是取消
在前端实现监听用户是否选择了打印还是取消的功能,确实是一个挑战,因为浏览器的打印行为是通过原生对话框处理的,而这些对话框的行为无法直接被 JavaScript 控制或监听。不过,有一些插件和方法可以帮助你更接近这个目标࿱…...
【PyCharm】连接Jupyter Notebook
【PyCharm】相关链接 【PyCharm】连接 Git【PyCharm】连接Jupyter Notebook【PyCharm】快捷键使用【PyCharm】远程连接Linux服务器【PyCharm】设置为中文界面 【PyCharm】连接Jupyter Notebook PyCharm连接Jupyter Notebook的过程可以根据不同的需求分为 本地连接 和 远程连…...

【Linux系统编程】—— 深入理解Linux中的环境变量与程序地址空间
文章目录 环境变量常见的环境变量查看环境变量环境变量的修改与使用环境变量的组织⽅式环境变量的命令通过代码如何获取环境变量环境变量的继承 前言:在Linux系统中,环境变量和程序地址空间是系统管理和进程运行的重要组成部分。本文将详细探讨环境变量的…...

Spark常见面试题-部分待更新
1. 简述hadoop 和 spark 的不同点(为什么spark更快) Hadoop是一个分布式管理、存储、计算的生态系统,包括HDFS(分布式文件系统)、MapReduce(计算引擎)和YARN(资源调度器)…...
Android BitmapShader实现狙击瞄具十字交叉线准星,Kotlin
Android BitmapShader实现狙击瞄具十字交叉线准星,Kotlin <?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...

如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
Java编程之桥接模式
定义 桥接模式(Bridge Pattern)属于结构型设计模式,它的核心意图是将抽象部分与实现部分分离,使它们可以独立地变化。这种模式通过组合关系来替代继承关系,从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...
WebRTC从入门到实践 - 零基础教程
WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC? WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音…...