用Qt做过开发的朋友,不知道是否曾为下面这些问题疑惑过:
我们知道Qt是基于C++的,Qt写的代码最终还是要由C++编译器来编译,但是我们的Qt代码中有很多C++里没有的关键字,比如slots\signals\Q_OBJECT等,为什么C++编译器会放过他们呢?
Qt的槽函数跟普通的成员函数有何区别?一个信号发出后,与之关联的槽函数是什么时候由谁来调用的?
Qt的信号定义跟函数定义有相同的形式,那信号跟普通函数有何区别?一个信号被emit时我们的程序到底在干什么?
信号和槽的连接是怎么建立的?又是如何断开的?
本篇博文里对这些问题作了详尽的解释,初学Qt或者用了很长时间Qt却很少思考过这些问题的朋友,可以在下面的段落中找到答案。

下文出自- original:http://woboq.com/blog/how-qt-signals-slots-work.html

译:NewThinker_Jiwey NewThinker_wei @CSDN



【译】Qt的信号-槽机制是如何工作的
(译注:如果阅读过程中有感觉疑惑的地方,可以参考一下本文的原文-译文对照版:http://blog.csdn.net/newthinker_wei/article/details/22701695)


很多人都知道Qt的信号和槽机制,但它究竟是如何工作的呢?在本篇博文中,我们会探索QObject 和 QMetaObject 内部的一些具体实现,揭开信号-槽的神秘面纱。

在本篇博文中,我会贴出部分Qt5的源码,有时候会根据需要改变一下格式并做一些简化。

首先,我们看一个官方例程,借此回顾一下信号和槽的使用方法。

头文件是这样的:

***********************************************

class Counter :publicQObject
{
    Q_OBJECT
    int m_value;
public:
    int value()const {returnm_value; }
public slots:
    void setValue(intvalue);
signals:
    void valueChanged(intnewValue);
};

***********************************************


在源文件的某个地方,可以找到setValue()函数的实现:

***********************************************

voidCounter::setValue(intvalue)

{

   if (value !=m_value){

       m_value =value; emitvalueChanged(value);

    }

}

***********************************************



然后开发者可以像下面这样使用Counter的对象:

***********************************************

Counter a, b;
  QObject::connect(&a,SIGNAL(valueChanged(int)),
                   &b, SLOT(setValue(int)));

a.setValue(12); // a.value() == 12, b.value() == 12

***********************************************

这是从1992年Qt最初阶段开始就沿用下来而几乎没有变过的原始语法。

虽然基本的API并没有怎么变过,但它的实现方式却变了几次。很多新特性被添加了进来,底层的实现也发生了很多变化。不过这里面并没有什么神奇的难以理解的东西,本篇博文会向你展示这究竟是如何工作的,

MOC,元对象编译器


Qt的信号/槽和属性系统都基于其能在运行时刻对对象进行实时考察的功能。实时考察意味着即使在运行过程中也可以列出一个对象有哪些方法(成员函数)和属性,以及关于它们的各种信息(比如参数类型)。如果没有实时考察这个功能,QtScript 和 QML 就基本不可能实现了。

C++本身不提供对实时考察的支持,所以Qt就推出了一个工具来提供这个支持。这个工具就是MOC。注意,它是一个代码生成器,而不是很多人说的“预编译器”。

MOC会解析头文件并为每一个含有Q_OBJECT宏的头文件生成一个对应的C++文件(这个文件会跟工程中的其他代码一块参与编译)。这个生成的C++文件包含了实时考察功能所需的全部信息(文件一般被命名为moc_HeaderName。cpp)。

因为这个额外的代码生成器,Qt有时对语言会有很严格的要求。 这里我就让这篇Qt文档来解释这个严格的要求。代码生成器没有什么错误,MOC起到了很大的作用。

几个神奇的宏

你能看出这几个关键字并不是标准C++的关键字吗?signals, slots, Q_OBJECT, emit, SIGNAL, SLOT. 这些都是Qt对C++的扩展。这几个关键字其实都是很简单的宏定义而已,在qobjectdefs.h 头文件中可以找到他们的定义。

#define signals public
#define slots /* nothing */

没错,信号和槽都是很简单的功能:编译器会将他们与其他任何宏一样处理。不过这些宏还有一个特殊的作用:MOC会发现他们。

Signals在Qt4之前都是protected类型的,他们在Qt5中变为了public,这样就可以使用一些新的语法了。

#define Q_OBJECT \
public: \static const QMetaObject staticMetaObject; \virtual const QMetaObject *metaObject() const; \virtual void *qt_metacast(const char *); \virtual int qt_metacall(QMetaObject::Call, int, void **); \QT_TR_FUNCTIONS /* translations helper */ \
private: \Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);Q_OBJECT defines a bunch of functions and a static QMetaObject Those functions are implemented in the file generated by MOC.
#define emit /* nothing */

emit是个空的宏定义,而且MOC也不会对它进行解析。也就是,emit完全是可有可无的,他没有任何意义(除了对开发者有提示作用)。

Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a)     "1"#a
# define SIGNAL(a)   "2"#a
#endif

(译注:对于#define的一些高级用法,参见我整理的一片文章:http://blog.csdn.net/newthinker_wei/article/details/8893407)

上面这些宏,会利用预编译器将一些参数转化成字符串,并且在前面添加上编码。

在调试模式中,如果singnal的连接出现问题,我们提示警告信息的时候还会注明对应的文件位置。这是在Qt4.5之后以兼容方式添加进来的功能。为了知道代码对应的行信息,我们可以用qFlagLocation ,它会将对应代码的地址信息注册到一个有两个入口的表里。

MOC生成的代码

我们现在就来看看Qt5的moc生成的部分代码。

The QMetaObject

***********************************************

const QMetaObject Counter::staticMetaObject = {
    { &QObject::staticMetaObject,qt_meta_stringdata_Counter.data,
      qt_meta_data_Counter, qt_static_metacall,0,0}
};

const QMetaObject *Counter::metaObject()const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

***********************************************

我们在这里可以看到Counter::metaObject()和 Counter::staticMetaObject 的实现。他们都是在Q_OBJECT宏中被声明的。

QObject::d_ptr->metaObject 只被用于动态元对象(QML对象),随意通常虚函数metaObject() 只是返回类的staticMetaObject 。

staticMetaObject被创建为只读数据。qobjectdefs.h 文件中QMetaObject的定义如下:

struct QMetaObject
{/* ... Skiped all the public functions ... */enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };struct { // private dataconst QMetaObject *superdata;const QByteArrayData *stringdata;const uint *data;typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);StaticMetacallFunction static_metacall;const QMetaObject **relatedMetaObjects;void *extradata; //reserved for future use} d;
};

代码中用的d是为了表明那些数据都本应为私有的。然而他们并没有成为私有的是为了保持它为POD和允许静态初始化。(译注:在C++中,我们把传统的C风格的struct叫做POD(Plain Old Data),字面意思古老的普通的结构体)。

这里会用父类对象的元对象(此处就是指QObject::staticMetaObject )来初始化QMetaObject的superdata,而它的stringdata 和 data 这两个成员则会用之后要讲到的数据初始化。static_metacall是一个被初始化为 Counter::qt_static_metacall的函数指针。

实时考察功能用到的数据表

首先,我们来分析 QMetaObject的这个整型数组:

***********************************************

static const uint qt_meta_data_Counter[] = {

// content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       2,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount

// signals: name, argc, parameters, tag, flags
       1,    1,   24,    2, 0x05,

// slots: name, argc, parameters, tag, flags
       4,    1,   27,    2, 0x0a,

// signals: parameters
    QMetaType::Void,QMetaType::Int,   3,

// slots: parameters
    QMetaType::Void,QMetaType::Int,   5,

0        // eod
};

***********************************************

开头的13个整型数组成了结构体的头信息。对于有两列的那些数据,第一列表示某一类项目的个数,第二列表示这一类项目的描述信息开始于这个数组中的哪个位置(索引值)。

这里,我们的Counter类有两个方法,并且关于方法的描述信息开始于第14个int数据。

每个方法的描述信息由5个int型数据组成。第一个整型数代表方法名,它的值是该方法名(译注:方法名就是个字符串)在字符串表中的索引位置(之后会介绍字符串表)。第二个整数表示该方法所需参数的个数,后面紧跟的第三个数就是关于参数的描述(译注:它表示与参数相关的描述信息开始于本数组中的哪个位置,也是个索引)。我们现在先忽略掉tag和flags。对于每个函数,Moc还会保存它的返回类型、每个参数的类型、以及参数的名称。

字符串表

***********************************************

struct qt_meta_stringdata_Counter_t {
    QByteArrayData data[6];
    char stringdata[47];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
offsetof(qt_meta_stringdata_Counter_t, stringdata) + ofs \
- idx * sizeof(QByteArrayData) \
)

static const qt_meta_stringdata_Counter_tqt_meta_stringdata_Counter = {
    {
QT_MOC_LITERAL(0,0,7),
QT_MOC_LITERAL(1,8,12),
QT_MOC_LITERAL(2,21,0),
QT_MOC_LITERAL(3,22,8),
QT_MOC_LITERAL(4,31,8),
QT_MOC_LITERAL(5,40,5)
    },
    "Counter\0valueChanged\0\0newValue\0setValue\0"
    "value\0"
};
#undef QT_MOC_LITERAL

***********************************************

这主要就是一个QByteArray的静态数组。QT_MOC_LITERAL 这个宏可以创建一个静态的QByteArray ,其数据就是参考的在它下面的对应索引处的字符串。

信号

MOC还实现了信号signals(译注:根据前面的介绍我们已经知道signals其实就是publib,因此所有被定义的信号其实都必须有具体的函数定义,这样才能C++编译器编译,而我们在开发中并不曾写过信号的定义,这是因为这些都由MOC来完成)。所有的信号都是很简单的函数而已,他们只是为参数创建一个指针数组并传递给QMetaObject::activate函数。指针数组的第一个元素是属于返回值的。在我们的例子中将它设置为了0,这是因为我们的返回类型是void。
传递给activate 函数的第3个参数是信号的索引(在这里,该索引为0)。
***********************************************
// SIGNAL 0
void
Counter::valueChanged(int_t1)
{
    void *_a[] = { 0,const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject,0,_a);
}
***********************************************
调用槽
通过一个槽的索引来调用一个槽函数也是行得通的,这要借助qt_static_metacall这个函数:
***********************************************
void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call_c,int_id,void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Counter *_t =static_cast<Counter *>(_o);
        switch (_id) {
        case 0: _t->valueChanged((*reinterpret_cast<int(*)>(_a[1])));break;
        case 1: _t->setValue((*reinterpret_cast<int(*)>(_a[1])));break;
        default: ;
        }
***********************************************
函数中的指针数组与在上面介绍signal时的那个函数中的指针数组格式相同。只不过这里没有用到_a[0],因为这里所有的函数都是返回void。
关于索引需要注意的地方
在每个 QMetaObject中,对象的槽、信号和其他一些被涉及到的方法都被分配了一个索引,这些索引值从0开始。他们是按照“先信号、再槽、最后其他方法”的顺序排列。这样得到的索引被称为相对索引(relative index),相对索引与该类的父类(就是基类)和祖宗类中的方法数量无关。
但是通常,我们不是想知道相对索引,而是想知道在包含了从父类和祖宗类中继承来的所有方法后的绝对索引。为了得到这个索引,我们只需要在相关索引(relative index)上加上一个偏移量就可以得到绝对索引absolute index了。这个绝对索引就是在Qt的API中使用的, 像QMetaObject::indexOf{Signal,Slot,Method}这样的函数返回的就是绝对索引。
另外,在信号槽的连接机制中还要用到一个关于信号的向量索引。这样的索引表中如果把槽也包含进来的话槽会造成向量的浪费,而一般槽的数量又要比信号多。所以从Qt4.6开始,Qt内部又多出了一个专门的信号索引signal index ,它是一个只包含了信号的索引表。
在用Qt开发的时候,我们只需要关心绝对索引就行。不过在浏览Qt源码的时候,要留意这三种索引的不同。
连接是如何进行的

在进行信号和槽的连接时Qt做的一件事就是找出要连接的信号和槽的索引。Qt会在meta object的字符串表中查找对应的索引。

然后会创建一个QObjectPrivate::Connection对象并添加到内部的链表中。

一个 connection 中需要存储哪些数据呢?我们需要一种能根据信号索引signal index快速访问到对应的connection的方法。因为可能会同时有不止一个槽连接到同一个信号上,所以每一个信号都要有一个槽列表;每个connection必须包含接收对象的指针以及被连接的槽的索引;我们还想让一个connection能在与之对应的接收者被销毁时自动取消连接,所以每个接收者对象也需要知道谁与他建立了连接这样才能在析构时将connection清理掉。

下面是定义在 qobject_p.h 中的QObjectPrivate::Connection :

(译注:只认识QObject不认识QObjectPrivate?看这里:http://blog.csdn.net/newthinker_wei/article/details/22789885)

***********************************************

struct QObjectPrivate::Connection
{
    QObject *sender;
    QObject *receiver;
    union {
        StaticMetaCallFunction callFunction;
        QtPrivate::QSlotObjectBase *slotObj;
    };
    // The next pointer for the singly-linked ConnectionList
    Connection *nextConnectionList;
    //senders linked list
    Connection *next;
    Connection **prev;
    QAtomicPointer<const int> argumentTypes;
    QAtomicInt ref_;
    ushort method_offset;
    ushort method_relative;
    uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
    ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
    ushort isSlotObject : 1;
    ushort ownArgumentTypes : 1;
    Connection() : nextConnectionList(0), ref_(2), ownArgumentTypes(true) {
        //ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
    }
    ~Connection();
    int method() const { return method_offset + method_relative; }
    void ref() { ref_.ref(); }
    void deref() {
        if (!ref_.deref()) {
            Q_ASSERT(!receiver);
            delete this;
        }
    }
};

***********************************************

每一个对象有一个connection vector:每一个信号有一个 QObjectPrivate::Connection的链表,这个vector就是与这些链表相关联的。
每一个对象还有一个反向链表,它包含了这个对象被连接到的所有的 connection,这样可以实现连接的自动清除。而且这个反向链表是一个双重链表。

使用链表是因为它们可以快速地添加和移除对象,它们靠保存在QObjectPrivate::Connection中的next/previous前后节点的指针来实现这些操作。

注意senderList的prev指针是一个“指针的指针”。这是因为我们不是真的要指向前一个节点,而是要指向一个指向前节点的指针。这个“指针的指针”只有在销毁连接时才用到,而且不要用它重复往回迭代。这样设计可以不用对链表的首结点做特殊处理。

(译注:对连接的建立如果还有疑惑,请参考:http://blog.csdn.net/newthinker_wei/article/details/22791617)

信号的发送

我们已经知道当调用一个信号的时候,最终调用的是MOC生成的代码中的QMetaObject::activate函数。

这里是qobject.cpp中这个函数的实现代码,这里贴出来的只是一个注解版本。

***********************************************

void QMetaObject::activate(QObject *sender, const QMetaObject *m,intlocal_signal_index,
                           void **argv)
{
    activate(sender,QMetaObjectPrivate::signalOffset(m),local_signal_index,argv);
    /* We just forward to the next function here. We pass the signal offset of
* the meta object rather than the QMetaObject itself
* It is split into two functions because QML internals will call the later. */

}

void QMetaObject::activate(QObject *sender, intsignalOffset,intlocal_signal_index,void **argv)
{
    int signal_index =signalOffset +local_signal_index;

/* The first thing we do is quickly check a bit-mask of 64 bits. If it is 0,
* we are sure there is nothing connected to this signal, and we can return
* quickly, which means emitting a signal connected to no slot is extremely
* fast. */

    if (!sender->d_func()->isSignalConnected(signal_index))
        return; // nothing connected to these signals, and no spy

......

......


/*译注:获得当前正在运行的线程的ID*/

Qt::HANDLE currentThreadId = QThread::currentThreadId();


    /* ... Skipped some debugging and QML hooks, and some sanity check ... */

/*跳过一些调试代码和完整性检测*/
    /* We lock a mutex because all operations in the connectionLists are thread safe */

/*这里用一个互斥量锁住,因为在ConnectionList里的所有操作都应是线程安全的*/
    QMutexLocker locker(signalSlotLock(sender));

/* Get the ConnectionList for this signal. I simplified a bit here. The real code
* also refcount the list and do sanity checks */

/*得到这个信号的ConnectionList。这里我做了一些简化。原来的代码还有完整性检测等*/
    QObjectConnectionListVector *connectionLists =sender->d_func()->connectionLists;
    const QObjectPrivate::ConnectionList *list =
        &connectionLists->at(signal_index);

QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // We need to check against last here to ensure that signals added
// during the signal emission are not emitted in this emission.

    QObjectPrivate::Connection *last = list->last;

/* Now iterates, for each slot */
    do {
        if (!c->receiver)
            continue;

QObject * const receiver = c->receiver;

/*译注:比较当前正在运行的线程的ID与receiver所属的线程的ID是否相同*/
        const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;

// determine if this connection should be sent immediately or
// put into the event queue

//译注:注意下面这一段,从这里可以看出对于跨线程的连接,信号发出

//后槽函数不会立即在当前线程中执行。其执行要等到槽函数所在的线程被

//激活后。有时间了再研究一下queued_activate这个函数。
        if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
            || (c->connectionType == Qt::QueuedConnection)) {
            /* Will basically copy the argument and post an event */
            queued_activate(sender,signal_index,c,argv);
            continue;
        } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            /* ... Skipped ... */
            continue;
        }

/* Helper struct that sets the sender() (and reset it backs when it
* goes out of scope */

        QConnectionSenderSwitcher sw;
        if (receiverInSameThread)
            sw.switchSender(receiver,sender,signal_index);

const QObjectPrivate::StaticMetaCallFunctioncallFunction =c->callFunction;
        const int method_relative = c->method_relative;
        if (c->isSlotObject) {
            /* ... Skipped.... Qt5-style connection to function pointer */
        } else if (callFunction &&c->method_offset <=receiver->metaObject()->methodOffset()) {
            /* If we have a callFunction (a pointer to the qt_static_metacall
* generated by moc) we will call it. We also need to check the
* saved metodOffset is still valid (we could be called from the
* destructor) */

            locker.unlock();// We must not keep the lock while calling use code
            callFunction(receiver,QMetaObject::InvokeMetaMethod,method_relative,argv);
            locker.relock();
        } else {
            /* Fallback for dynamic objects */
            const int
method =method_relative +c->method_offset;
            locker.unlock();
            metacall(receiver,QMetaObject::InvokeMetaMethod,method,argv);
            locker.relock();
        }

// Check if the object was not deleted by the slot
        if (connectionLists->orphaned)break;
    } while (c != last && (c = c->nextConnectionList) !=0);
}
***********************************************







Qt的信号和槽是如何工作的相关推荐

  1. QT 的信号与槽机制介绍

    QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象.易于扩展.真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT ...

  2. 【Qt】Qt中信号与槽

    00. 目录 文章目录 00. 目录 01. 信号与槽 02. 介绍 03. 信号与槽 04. 信号 05. 槽 06. 一个小例子 07. 一个真实的例子 08. 信号和槽使用默认参数 09. 信号 ...

  3. C++框架_之Qt的信号和槽的详解

    目录 C++_之Qt的信号和槽的详解 1.概述 1.1对象树(子对象动态分配空间不需要释放) 2.信号和槽 Qt5 的书写方式:(推荐的使用)★★★★★ Qt4 的书写方式: 3.自定义信号槽 3.1 ...

  4. 【qt】QT 的信号与槽机制

    QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象.易于扩展.真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT ...

  5. 【转载】QT 的信号与槽机制介绍

    QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象.易于扩展.真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT ...

  6. Qt Designer信号和槽

    2019独角兽企业重金招聘Python工程师标准>>> 先入一些信号槽的基本介绍: 信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包 ...

  7. QT下信号与槽不在同一个线程中如何connect

    QT下信号与槽不在同一个线程中如何connect

  8. QT 读取csv文件-QT根据显示器大小设置窗口大小-QT绑定信号与槽

    QT 读取csv文件 废话不多说先上代码, 需要引入的头文件: 函数是自己写的一个小项目截取的一段,基本上思路就是这个.读取csv格式的和读取txt文件类似. #include <QFile&g ...

  9. 【Qt专栏】Qt 中信号与槽的概念和实现机制

    目录 一,信号和槽概念 1.元对象系统 2.信号和槽 3.底层实现机制 二,什么是观察者设计模式 三,观察者设计模式的编程套路 四,纯 C++ 实现信号与槽机制 1.槽函数模板类 2.信号模板类 3. ...

最新文章

  1. 如何在游标里控制条件_热处理精密渗碳里的碳势如何控制
  2. JAVA实现旋转数组的最小数字问题(《剑指offer》)
  3. can't find a register in class `GENERAL_REGS' while reloading `asm'
  4. linux docker升级,CentOS上面升级Docker到最新版本
  5. StreamBox Ripper 将rm转mp3时候出现g2支持的问题
  6. 分区助手扩大c盘后自动修复_磁盘分区工具,这个好用;无论调整C盘还是系统迁移...
  7. mysql查询各类课程的总学分_基于jsp+mysql的JSP学生选课信息管理系统
  8. 实现iframe_面试官:来说说单点登录的三种实现方式
  9. linux 文件格式压缩
  10. mysql replication 互为主从的安装及配置,以及数据同步
  11. 绘制Linux/Android设备的内存动态变化趋势图
  12. C语言getchar()=='\n'的使用,对输入的任意个字符操作
  13. 最详细的JavaScript教程(高级篇),深入学习JavaScript
  14. 中粤拼音在线转换_在Word中给汉字标注拼音、声调(二)
  15. cmd命令快速切换电源模式-平衡模式和卓越模式
  16. codeforces1467D. Sum of Paths
  17. android 手机存储位置设置,如何将红米手机外置SD卡设定为默认存储
  18. LeetCode 263 Ugly Number(丑数)
  19. 为什么要学习西方文化
  20. [http]http转义和加解密方法

热门文章

  1. android p 牛轧糖_如何启用Android牛轧糖的猫收集复活节彩蛋
  2. win10+ubuntu 18.04双系统(联想小新13pro)——我终于安好了!
  3. java 函数 作为参数_如何在Java中将函数作为参数传递?
  4. 高中音乐教学计算机,浅析多媒体计算机技术在高中音乐课的应用
  5. 函数图像变换的规律,以一元函数和二元函数为例来说明,对多元函数同样适用。...
  6. 【重新定义matlab强大系列三】MATLAB清洗离群数据(查找、填充或删除离群值)
  7. 【Matlab】删除cell中所有的空白cell
  8. 计算两个日期之间的工作日(去掉周末和节假日)
  9. 行业实战 | 5G+边缘计算+“自由视角” 让体育赛事更畅快
  10. 一文读懂Kubernetes Scheduler扩展功能