C++封装思想之二:友元机制和运算符重载(1W字详解)

news2024/11/15 17:30:40

目录

友元机制和运算符重载

友元机制

友元函数

友元的作用

友元类

前置声明 

友元类的注意事项 

友元成员函数(类的某个成员函数 作为另一个类的友元)

运算符重载

运算符重载的作用

运算符重载的注意事项

运算符重载的实现

成员函数重载

友元函数重载

运算符重载规则 

重载“=”运算符

重载"<"、">"运算符

重载"++"运算符

先自加再使用

先使用后自加

重载"+"运算符

重载"[ ]"运算符

重载"->"运算符

流运算符重载

重载函数调用运算符"()"(了解)

类型转换运算符(详解)

以bool类型转换作为引入

类型转换运算符详解

补充(内置类)

实例(内置类实现C++链表封装)


友元机制和运算符重载

友元机制

友元函数

比如下面这种情况:在func中每次打印num都需要调用get_num函数,函数的调用和销毁浪费了大量的运行时间(即函数不是类的一部分,但又需要频繁地访问类的数据成员)

因此C++提出了友元的概念

什么样的函数为友元函数?

函数不是类的一部分,但又需要频繁地访问类的数据成员

友元的作用

友元的作用:类的非成员函数可以直接访问类的非公有成员变量,省去函数的调用和访回过程。从而提高了程序的运行效率(即减少了类型和安全性检查及调用的时间开销

友元类

前置声明 

前置声明:只是单纯声明有这个类,但是不知道这个类的具体构造;

只可以利用类型名声明指针和引用变量,不能实例并访问类的内部构造;

若需要利用指针或引用调用前置类型的接口,必须按照声明和实现分离的方式进行编码。(后置实现)

注意:注意类声明的顺序;

友元类的注意事项 

 

友元成员函数(类的某个成员函数 作为另一个类的友元)

友元成员函数:即让当前类的某个函数成为另一个类的友元函数,这样就可以在当前类的这个函数中访问另一个类的私有成员

注意事项:通过前置声明使用类的方法必须在前置类定义之后初始化,不能互为友元成员函数

 class Room;//向前声明 只能说明类名称
 class goodGay
 {
     public:
     void visiting01(Room &room);
     void visiting02(Room &room);
 };

 class Room
 {
     friend void goodGay::visiting02(Room &room);
     private:
         string bedRoom;//卧室
     public:
         string setingRoom;//客厅
     public:
     Room(string bedRoom, string setingRoom)
     {
         this‐>bedRoom = bedRoom;
         this‐>setingRoom = setingRoom;
     }
 };
int main(int argc, char *argv[])
 {
     Room room("吴维的卧室","吴维的客厅");
     goodGay ob;
     ob.visiting01(room);
     ob.visiting02(room);
     return 0;
 }

 void goodGay::visiting01(Room &room)
 {
     cout<<"翰文访问了"<<room.setingRoom<<endl;
     //cout<<"翰文访问了"<<room.bedRoom<<endl;
 }

 void goodGay::visiting02(Room &room)
 {
     cout<<"好基友张三访问了"<<room.setingRoom<<endl;
     cout<<"好基友张三访问了"<<room.bedRoom<<endl;
 }

运算符重载

可以通过函数参数的不同可以实现一个运算符任意方法的重载

运算符重载的作用

  • 可以提高程序的可读性
  • 体现了C++的可扩充性
  • 运算符重载仅仅只是语法上的方便,它是另一种函数调用的方式
  • 运算符重载,本质上是函数重载

运算符重载的注意事项

不要滥用重载、因为它只是语法上的方便,所以只有在涉及的代码更容易写、尤其是更易读时才有必要重载

运算符重载的实现

成员函数重载

友元函数重载

运算符重载规则 

  • 运算符重载不允许发明新的运算符
  • 不能改变运算符操作对象的个数
  • 运算符被重载后,其优先级和结合性不会改变
  • 不能重载的运算符

  • 成员函数重载和友元函数重载的选择
    • 一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
    • 以下一些双目运算符不能重载为类的友元函数:=、()、[ ]、->
    • 类型转换运算符只能以成员函数方式重载
    • 流运算符只能以友元的方式重载

重载“=”运算符

MyString &MyString::operator=(MyString ob)
{
    //str2 = str1;
    if(this->str != NULL)
    {
        delete [] this->str;
        this->str = NULL;
    }

    this->size = ob.size;
    this->str = new char[this->size+1];
    memset(this->str, 0, this->size+1);
    strcpy(this->str, ob.str);

    return *this;
}

MyString &MyString::operator=(char *str)
{
    //str2 = str1;
    if(this->str != NULL)
    {
        delete [] this->str;
        this->str = NULL;
    }

    this->size = strlen(str);
    this->str = new char[this->size+1];
    memset(this->str, 0, this->size+1);
    strcpy(this->str, str);

    return *this;
}

 类的赋值运算符 “=” 只能重载为成员函数,而不能把它重载为友元函数,具体原因见:赋值运算符"="的重载https://blog.csdn.net/aaqian1/article/details/86423858

重载"<"、">"运算符

bool MyString::operator>(MyString ob)
{
    if(str==NULL || ob.str == NULL)
    {
        exit(-1);
    }
    if(strcmp(this->str, ob.str) > 0)
    {
        return true;
    }
    return false;
}

bool MyString::operator>(char *str)
{
    if(this->str==NULL || str == NULL)
    {
        exit(-1);
    }
    if(strcmp(this->str, str) > 0)
    {
        return true;
    }
    return false;
}

bool MyString::operator<(char *str)
{
    if(this->str==NULL || str == NULL)
    {
        _Exit(-1);
    }
    if(strcmp(this->str,str)<0)
    {
        return true;
    }
    return false;
}

bool operator<(const MyString &ob1, const MyString &ob2)
{
    if(ob1.str==NULL || ob2.str == NULL)
    {
        _Exit(-1);
    }
    if(strcmp(ob1.str,ob2.str)<0)
    {
        return true;
    }
    return false;
}

运行结果:

重载"++"运算符

实例:Integer类(友元和成员函数重载方式)

先自加再使用

成员函数重载

友元函数重载

先使用后自加

成员函数重载

友元函数重载

重载"+"运算符

MyString operator+(const MyString &ob1,const MyString &ob2)
{
    MyString tmp;
    tmp.size = ob1.size+ob2.size;
    tmp.str = new char[tmp.size+1];
    memset(tmp.str, 0, tmp.size+1);

    strcpy(tmp.str, ob1.str);
    strcat(tmp.str, ob2.str);

    return tmp;
}

MyString MyString::operator+(char *str)
{
    MyString tmp;
    tmp.size = size+strlen(str);
    tmp.str = new char[tmp.size+1];
    memset(tmp.str, 0, tmp.size+1);

    strcpy(tmp.str, this->str);
    strcat(tmp.str, str);

    return tmp;
}

重载"[ ]"运算符

char& MyString::operator[](int pos)
{
    if(pos<0 || pos>=size)
    {
        cout<<"元素位置不合法"<<endl;
        exit(-1);
    }

    return str[pos];
}

重载"->"运算符

->重载实现访问类中内置成员类里的某个成员

如下所示LinkIterator类和Node类同属于Link类的内置类,在main函数中实例化一个Link类对象指针,如果没有重载"->",对象指针将无法访问Node类中的私有成员m_num和next。但是由于Link类中有一个Node类的成员对象指针m_p。因此通过在Link类中重载"->",返回m_p,那么在main函数中的Link类对象指针就可以通过"->"访问到m_p中的m_num和next。

流运算符重载

如果采用成员函数的方法,由于是cout的成员,因此不能当前在类中实现重载。所以只能采用第二种方法。

流运算符>为内部类,不能通过类内成员函数重载的方法来实现,只能通过友元函数的方法来实现。

//全局函数实现 <<重载
ostream& operator<<(ostream &out, MyString ob)
{
    out<<ob.str;
    return out;
}
//全局函数实现 >>重载
istream& operator>>(istream &in, MyString &ob)
{
    char buf[1024]="";
    cin>>buf;

    if(ob.str != NULL)//ob已经有字符串
    {
        delete [] ob.str;
        ob.str = NULL;
    }

    ob.size = strlen(buf);
    ob.str = new char[ob.size+1];
    memset(ob.str, 0,ob.size+1);
    strcpy(ob.str, buf);

    return in;
}

重载函数调用运算符"()"(了解)

 重载()运算符 一般用于 为算法 提供策略。

以重载()用于输出信息为例:

类型转换运算符(详解)

以bool类型转换作为引入

在MyString类中定义了bool类型转换函数,在main函数中实例化MyString对象的时候会隐式地将MyString类型转到bool的类型。

PS:类型的true还是false由bool类型转换构造函数中的具体返回值来确定

如果str1中的str不为空,则打印str中的内容:

输出结果如下:

类型转换运算符详解

类型转换运算符(conversion operator)是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式是:

operator type() const;

        其中,type表示转换目标类型,除了void、数组或函数类型,但允许转换成指针(包括数组指针及函数指针)。类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义为类的成员函数。类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成const成员。

        转换构造函数和类型转换运算符,有时也被称作用户定义的类型转换(use-defined conversions)。

        定义含有类型转换运算符的类

        举一个简单的例子,令其表示0到255之间的一个整数:

class SmallInt
{
public:
    SmallInt(int i = 0) :val(i)
    {
        if (i < 0 || i>255)
        {
            throw out_of_range("Bad SmallInt value");
        }
    }
 
    operator int() const {return val;}
 
private:
    size_t val;
};

这里的SmallInt类既定义了向类类型的转换,也定义了从类类型向其他类型的转换。其中,构造函数将算术类型的值转换成了SmallInt对象,而类型转换运算符将SmallInt对象转换成int:

SmallInt si;
si = 4;          // 首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=
si + 3;          // 首先将si隐式地转换成int,然后执行整数的加法

尽管编译器一次只能执行一次用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。因此,我们可以将任何算术类型传递给SmallInt的构造函数。类似的,我们也能使用类型转换运算符将一个SmallInt对象转换成int,然后再将所得的int转换成任何其他的算术类型:

// 内置类型转换将double实参转换成int
SamllInt si = 3.14;         // 调用SmallInt(int)构造函数
 
// SmallInt的类型转换运算符将si转换成int
si + 3.14;                 // 内置类型转换将所得的int继续转换成double

在实践中,类很少提供类型转换运算符。在大多数情况下,如果类型转换自动发生,用户可能会感觉比较意外,而不是感觉受到了帮助。而这条经验法则存在一种例外的情况:对于类来说,定义向bool类型转换还是比较普遍的现象。

显示的类型转换运算符

C++11新标准引入了显示的类型转换运算符(explicit conversion operator):

class SmallInt
{
public:
    // 编译器不会自动执行这一类型转换
    explicit operator int() const {return val;}
    // 其他成员与之前的版本一致
};

和显式的构造函数一样,编译器通常也不会将一个显式的类型转换运算符用于隐式类型转换:

SmallInt si = 3;             // 正确:SmallInt的构造函数不是显式的
si + 3;                      // 错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3;    // 正确:显式地请求类型转换

类型转换参考如下

C++重载运算与类型转换https://blog.csdn.net/weixin_43918519/article/details/123933954?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~Rate-2-123933954-blog-100067470.235%5Ev38%5Epc_relevant_anti_t3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~Rate-2-123933954-blog-100067470.235%5Ev38%5Epc_relevant_anti_t3&utm_relevant_index=3

关于operator bool () 和bool operator ==()https://blog.csdn.net/znzxc/article/details/80385995

C++中Operator类型强制转换成员函数解析https://blog.csdn.net/zhangzheng_1986/article/details/81080407

补充(内置类)

必须使用内置类的封装才能接管++、--运算符,当传统类型已经不足够进行操作的时候,必须要内置类进行封装

实例(内置类实现C++链表封装)

#include <iostream>

using namespace std;

class Link
{
public:
    class Node
    {
    public:
        Node(int num) : m_num(num), next(nullptr)
        {
        }
        int m_num;
        Node *next;

        friend ostream& operator<<(ostream &out, const Link::Node node);
    };

    class LinkIterator;

    typedef LinkIterator iterator;
    // typedef Node * iterator;

    class LinkIterator
    {
    public:
        LinkIterator(Node *p) : m_p(p)
        {
        }

        LinkIterator operator++()
        {
            m_p = m_p->next;
            return *this;
        }

        LinkIterator operator++(int num)
        {
            LinkIterator temp(this->m_p);
            this->m_p = m_p->next;
            return temp;
        }

        Node *operator->()
        {
            return m_p;
        }

        bool operator!=(const LinkIterator &other)
        {
            return m_p != other.m_p;
        }

        bool operator==(const LinkIterator &other)
        {
            return this->m_p == other.m_p;
        }

        LinkIterator & operator+(int index)
        {
            for(int i = 0; i < index; i++)
            {
                m_p = m_p->next;
            }

            return *this;
        }

        Node operator*()
        {
            return *m_p;
        }

    private:
        Node *m_p;
    };

    iterator begin()
    {
        return LinkIterator(m_head);
    }

    iterator end()
    {
        return LinkIterator(nullptr);
    }

    Link(Node *head = nullptr) : m_head(head)
    {
    }

    ~Link()
    {
        Node *temp = m_head;
        while (m_head != nullptr)
        {
            temp = m_head;
            m_head = m_head->next;
            delete temp;
        }

        m_head = nullptr;
    }

    void insert_head(Node *newnode)
    {
        newnode->next = m_head;
        m_head = newnode;
    }

    void insert_tail(Node *newnode)
    {
        Node *temp = m_head;
        if (temp == nullptr)
        {
            newnode->next = nullptr;
            m_head = newnode;
        }
        else
        {
            while (temp->next != nullptr)
            {
                temp = temp->next;
            }

            temp->next = newnode;
            newnode->next = nullptr;
        }
    }

    bool insert_mid(Node *newnode, int index)
    {
        Node *temp = m_head;

        if (temp == nullptr)
        {
            return false;
        }

        while (temp != nullptr)
        {
            if (temp->m_num == index)
            {
                newnode->next = temp->next;
                temp->next = newnode;
                return true;
            }

            temp = temp->next;
        }

        return false;
    }

    void insert_mid(iterator it, Node *newnode)
    {
        newnode->next = it->next;
        it->next = newnode;
    }

    iterator find(int index)
    {
        for (auto it = this->begin(); it != this->end(); ++it)
        {
            if (it->m_num == index)
            {
                return it;
            }
        }

        return this->end();
    }

    bool delete_node(Node *node)
    {
        Node *temp = m_head;

        if (temp == nullptr)
        {
            return false;
        }

        if (m_head->m_num == node->m_num)
        {
            m_head = m_head->next;
            free(temp);
            temp = nullptr;
            return true;
        }
        else
        {
            Node *p = temp;
            temp = temp->next;
            while (temp != nullptr)
            {
                if (temp->m_num == node->m_num)
                {
                    p->next = temp->next;
                    free(temp);
                    temp = nullptr;
                    return true;
                }

                p = temp;
                temp = temp->next;
            }

            return false;
        }
    }

    void display()
    {
        Node *temp = m_head;
        while (temp != nullptr)
        {
            cout << temp->m_num << " ";
            temp = temp->next;
        }
        cout << "\n";
    }

private:
    Node *m_head;
};

ostream& operator<<(ostream &out, const Link::Node node)
{
    out << node.m_num;
    return out;
}

int main(int argc, char **argv)
{
    Link::Node p(5);

    Link link;

    for (int i = 0; i < 5; i++)
    {
        link.insert_tail(new Link::Node(i));
    }

    link.insert_mid(new Link::Node(5), 3);
    link.insert_head(new Link::Node(6));
    link.delete_node(new Link::Node(4));

   // link.display();

    for (auto it = link.begin(); it != link.end(); ++it)
    {
        //cout << it->m_num << endl;
        cout << *it << endl;
    }

    auto it = link.find(6);
    if (it == link.end())
    {
        cout << "not find" << endl;
    }
    else
    {
        cout << it->m_num << endl;
    }

    it = it + 3;
    cout << it->m_num << endl;
    
    string s1 = "hello world";
    for(auto it = s1.rbegin(); it != s1.rend(); it++)
    {
        cout << *it << endl;
    }
    
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/838804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2021-03-03 Multisim 14.0 电池充电防止反接保护

R2R3当作充电线电阻看,也可设置这2个电阻导线电阻,电阻取值依据充电电流范围确定,由于电池存在电压因此可以用光耦检测,发光二极管当作继电器看,可采用继电器自锁,当下次再次反接的话另一个继电器同样,2个继电器相互控制.本电路可验证极性变化时2路检测的变化,图中S1为模拟电池…

计算机视觉:替换万物Inpaint Anything

目录 1 Inpaint Anything介绍 1.1 为什么我们需要Inpaint Anything 1.2 Inpaint Anything工作原理 1.3 Inpaint Anything的功能是什么 1.4 Segment Anything模型&#xff08;SAM&#xff09; 1.5 Inpaint Anything 1.5.1 移除任何物体 1.5.2 填充任意内容 1.5.3 替换任…

Finalshell连接Linux超时之Connection timed out: connect

目录 &#x1f349;前言 &#x1f33c;报错 &#x1f33c;摸索 &#x1f4aa;解决措施 &#x1f349;前言 &#xff08;1&#xff09;福利&#xff1a;花了2小时才解决的BUG&#xff0c;希望本篇文章能帮你10分钟解决&#xff01; &#xff08;2&#xff09;tips&#xff1…

6.s081/6.1810(Fall 2022)Lab3: page tables

文章目录 前言其他篇章参考链接0. 前置环境1. Speed up system calls (easy)1.1 简单分析1.2 映射1.3 页分配1.4 页释放1.5 测试 2. Print a page table (easy)2.1 简单分析2.2 实现2.3 测试 3. Detect which pages have been accessed (hard)3.1 简单分析3.2 实现3.2.1 获取参…

DBSCAN聚类

一、概述 DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法&#xff0c;簇集的划定完全由样本的聚集程度决定。聚集程度不足以构成簇落的那些样本视为噪声点&#xff0c;因此DBSCAN聚类的方式也可以用于异常点的检测。 二、算法…

一零六七、JVM梳理

JVM&#xff1f; Java虚拟机&#xff0c;可以理解为Java程序的运行环境&#xff0c;可以执行Java字节码&#xff08;Java bytecode&#xff09;并提供了内存管理、垃圾回收、线程管理等功能 java内存区域划分?每块内存中都对应什么? 方法区&#xff1a;类的结构信息、常量池、…

5个顶级的开源有限元分析软件

每当我参加数值分析课程的教学时&#xff0c;都会回顾有限元方法的基础知识&#xff0c;很自然地就会出现使用哪种软件的问题。 以下讨论基于三个基本考虑&#xff1a; 在实际应用中&#xff0c;很少有人从头开始编写 FEM 代码。商业 FEM 软件通常在某些预定义的情况下非常易于…

生命在于学习——Linux安全加固以及基线检查

一、账号管理 1、口令锁定策略 基线检查&#xff1a; 查看文件more /etc/pam.d/password-auth判定条件&#xff1a;是否存在以下内容 auth required pam_tally2.so deny5 onerrfail unlock_time300 even_deny_root5 root_unlock_time600安全加固&#xff1a; &#xff08;1…

MyBatis的输入映射和输出映射

文章目录 前言案例总结resultMap的使用 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; MyBatis的输入映射和输出映射是将Java对象和数据库表的列进行映射&#xff0c;实现数据的自动转换。 输入映射&#xff08;Parameter Mapping&#xff09;&#x…

从小白到大神之路之学习运维第74天-------Docker网络模型详解

第四阶段 时 间&#xff1a;2023年8月4日 参加人&#xff1a;全班人员 内 容&#xff1a; Docker网络模型详解 目录 一、环境配置 &#xff08;一&#xff09;安装docker-ce&#xff08;Linux安装Docker&#xff09; 二、Docker网络基础 &#xff08;一&#xff09;端…

Linux 创建子进程

文章目录 前言一、进程&#xff0c;线程&#xff0c;程序 区分二、创建子进程三、创建多个进程1. 获取进程号2. 循环创建多个进程 四、进程工具。1. ps 查看当前进程.2. kill 进程终止. 总结 前言 在计算机科学中&#xff0c;进程&#xff08;Process&#xff09;、线程&#…

8.5day06 框架基础--反射+注解

文章目录 反射获取类的各种信息获取类的字节码文件 注解元注解 复习redis两道算法题 摆烂了&#xff0c;不想学啦&#xff01;&#xff01;&#xff01; 反射 反射主要用来做框架; 学习内容 获取类的各种信息 第一步 加载类&#xff0c;获取类的字节码文件 第二步 获取类的…

抖音seo矩阵系统源码搭建开发详解

抖音SEO矩阵系统是一个用于提高抖音视频在搜索引擎排名的工具。如果你想开发自己的抖音SEO矩阵系统&#xff0c;以下是详细的步骤&#xff1a; 开发步骤详解&#xff1a; 确定你需要的功能和算法 抖音SEO矩阵系统包含很多功能&#xff0c;比如关键词研究、内容优化、链接建设、…

【ASP.NET MVC】使用动软(一)(9)

一、解决的问题 前文为解决数据库操作设计的 TestMysql 类&#xff0c;仅简单地封装了一个Query函数&#xff0c;代码如下&#xff1a; public class TestMysql{public static string SqlserverConnectStr "server127.0.0.1;charsetutf8;user idroot;persistsecurityin…

PLC4X踩坑记录

plc4x引起的oom 使用Jprofiler查看dump文件 由上可以看出有大量的NioEventLoop对象没有释放 PlcConnection#close 设备断连重连后导致的oom&#xff0c;看源码close方法主要是channel通道关闭。 修改NettyChannelFactory源码 plc4x设计思想是一个设备一个连接&#xff0c;…

剑指OfferII-58.左旋转字符串

剑指OfferII-58.左旋转字符串 目录 剑指OfferII-58.左旋转字符串题目描述解法一&#xff1a;字符数组解法二&#xff1a;原地反转 题目描述 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。 请定义一个函数实现字符串左旋转操作的功能。 比如&#xff0c…

性能测试浅谈

早期的性能测试更关注后端服务的处理能力。 一个用户去访问一个页面的请求过程&#xff0c;如上图。 数据传输时间 当你从浏览器输入网址&#xff0c;敲下回车&#xff0c;开始... 真实的用户场景请不要忽视数据传输时间&#xff0c;想想你给远方的朋友写信&#xff0c;信件…

visio,word添加缺少字体,仿宋_GB2312、楷体_GB2312、方正小标宋简体等字体下载

一. 内容简介 visio,word添加缺少字体,仿宋_GB2312、楷体_GB2312、方正小标宋简体等字体下载 二. 软件环境 2.1 visio 三.主要流程 3.1 下载字体 http://www.downza.cn/ 微软官方给的链接好多字体没有&#xff0c;其他好多字体网站&#xff0c;就是给你看个样式&#xff…

JMeter(二十四)、使用吞吐量控制器实现不同的用户操纵不同的业务

一、需求 需求&#xff1a;博客系统&#xff0c;模拟用户真实行为&#xff0c;80%的用户阅读文章&#xff0c;20%的用户创建文章&#xff0c;创建文章的用户随机的删除或者修改文章。 二、脚本实现 80%的用户查看文章 20%用户创建文章 根据post_id是否能整除2&#xff0c;决…

在线课堂,视频点播,springboo+vue

springbootvue三端&#xff08;管理后台&#xff0c;教师端&#xff0c;用户端&#xff09;端可提供源码&#xff0c;可远程安装&#xff0c;需要的加微信&#xff1a; 体验地址&#xff1a;http://edu.dgrxs.com/ 用户端&#xff1a; 管理端&#xff1a; 教师端&#xff1a;