成年技术就业培训机构「成人培训成新战场,文都集团行业先发优势愈发凸显」成人培训成新战场,文都集团行业先发优势愈发凸显

作者:学到教育 任金城

对于大多学习Qt的朋友,心中都有种好奇——那就是Qt最核心的信号与槽是如何实现的,对于小编自己也是一样,当然大家肯定都会去查阅相关资料,但大部分时候也只是一知半解,如果说要自己实现就会又摸不着头脑了;所以小编决定自己亲自用C++实现一个简单版的信号槽,来理解Qt的实现原理。于是小编就在翻阅各牛人朋友的博客和反复研究Qt源码自己重新写了一下以便交流学习。

我们先还是简单的梳理一下Qt信号与槽的实现机理:在Qt中实现信号与槽最重要的就是通过元对象系统(MOS)的元对象编译器(MOC)将我们定义的需要使用到信号与槽的类中的信号及信号调用槽函数的方法进行定义(这一步就会生成与源文件对应的moc_xx.cpp文件),然后通过系统提供的关联方法(connect)将信号与槽建立一一对应关系,当发射信号(其实就是调用信号函数)时就会通过信号与槽的对应关系找到对应槽函数进行调用。这样的好处就是对于使用者而言不必去关心函数指针回调函数这些对于初学者比较不太容易搞清晰的东西,简化了使用者的操作。当然就像我们在享受幸福生活的时候,就一定有人在我们背后默默付出砥砺前行!这里也一样,对于我们使用者简化了操作,那为了实现这样的效果就需要在后台提供更多的支持。接下来我们就通过代码再来梳理一遍。

首先我们使用信号与槽肯定就会有信号的发送者与接收者,所以我们就先去定义这两个类对象:

sender.h

#pragma once

#include "object.h"


class Sender : public Object

{

X_OBJECT

public:

Sender(int n = 0) : m_num(n){

}

void sendSig();

signals:

void holdClass(int n);

int m_num;

};

sender.cpp

#include "sender.h"


void Sender::sendSig()

{

std::cout << "发送信号:holdClass" << std::endl;

emit holdClass(m_num);

}

在Qt中需要使用信号槽的对象都需要直接或间接继承一个类QObject,并且需要添加一个私有宏定义Q_OBJECT,这里就用Object和X_OBJECT代替,signals是Qt中用于声明信号函数的关键字,emit是Qt中用于发送信号定义的关键字,这里我们先假设已经有这些类和宏定义,注意信号函数是不需要我们定义的,他是在MOC预处理生成的moc_xx.cpp中自动生成定义的,所以这里的cpp很简单只有一个普通函数sendSig()的定义。同理我们再自己定义一个信号的接收者对象和其对应的槽函数。

receiver.h

#pragma once

#include "object.h"


class Receiver : public Object

{

X_OBJECT

public:

Receiver() {

}

public slots:

void attendClass(int n);

};

receiver.cpp

#include "receiver.h"


void Receiver::attendClass(int n)

{

std::cout << "执行槽函数attendClass:cur class " << n << std::endl;

}

这里的slots就是Qt中用于标识槽函数声明的关键字,槽函数是需要用户自己定义的。

然后我们就需要再将发送者信号与接收者槽关联起来,我们这就提供一个主函数来模拟关联信号与槽,让发送者产生信号:

main.cpp

#include "sender.h"

#include "receiver.h"


int main()

{

Sender xuedao(9527);

Receiver rjc;

Object::connect(&xuedao, SIGNAL(holdClass(int)), &rjc, SLOT(attendClass(int)));

xuedao.sendSig();


return 0;

}

这里的SIGNAL与SLOT在Qt中就是两个转换字符串的宏定义,connect是QObject的一个静态函数方法。

我们要想这个程序能正常运行起来,接下来我们就需要去定义一个类似QObject的Object类和上面需要用到的关键字与宏定义,以及模拟MOC预处理产生对应的moc_xx.cpp,里面细节的地方为了方便理解我都通过代码注释解释说明了

object.h

#pragma once


#include <iostream>

#include <map>

#include <vector>

#include <string>

#define signals protected

#define slots

#define emit

#define SLOT(slt) "1"#slt // 1用于标识槽函数

#define SIGNAL(sig) "2"#sig //2用于标识信号


class Object;

struct MetaObject

{

//每个对象可能会有多个信号与槽函数,这里就用两个vector分别保存信号与槽函数信息操作起来方便点

std::vector<std::string> sigs;

std::vector<std::string> slts;

//activate的功能是通过信号发送者即信号索引找到关联接收者和方法索引并调用对应方法

static void activate(Object *sender, int idx, void **argv); //void **argv对应信号传递的参数

struct Connection //用于打包信号接收者与方法的索引(对应上面定义的vector中的信号槽的索引)

{

Object *m_receiver;

int method;

};

};

//Q_OBJECT宏中定义的比较多这里只选择了我们需使用的几个

//static MetaObject meta用于保存使用该宏定义对象中的信号与槽信号与槽的相关信息

//getMetaObject()用于返回发送者或接收者对象中的static MetaObject meta对象

#define X_OBJECT static MetaObject meta; \

virtual MetaObject *getMetaObject(); \

virtual void metaCall(int idx, void **argv); //idx为对应槽函数的索引,void**argv用于接收信号传递的参数

class Object //需要使用信号槽对象的公共接口对象

{

X_OBJECT

public:

virtual ~Object() {}

//connect用于建立信号与槽的关联信息

static void connect(Object *sender, const char *s1, Object *receiver, const char *s2);

private:

friend class MetaObject; //用于方便meta对象访问下面的信号槽map

std::multimap<int, MetaObject::Connection> mp; //用于保存信号索引与接收者对象即索引的对应关系

//由于一个信号可以对应多个槽,同样多个信号也可以对应一个槽,所以这里选用了multimap容器做对应关系映射

};

object.cpp

#include "object.h"

#include <string.h> //调用strcmp函数需要包含


void MetaObject::activate(Object *sender, int idx, void **argv)

{

//在信号槽对应关系的mp中找到发送者idx索引信号对应的接收者及关联方法的调用

auto ptr = sender->mp.equal_range(idx);

for(auto it = ptr.first; it != ptr.second; it++) {

MetaObject::Connection con = it->second;

con.m_receiver->metaCall(con.method, argv); //调用接收者与发送者信号关联的方法,并传递需要的参数

}

}


void Object::connect(Object *sender, const char *s1, Object *receiver, const char *s2)

{

int sig_idx = -1, slt_idx = -1;

MetaObject *senderMeta = sender->getMetaObject(); //获取发送者中保存的meta对象

MetaObject *receiverMeta = receiver->getMetaObject(); //获取接收中保存的meta对象

//比对信号名称找到对应的信号索引

for(int i = 0; i < senderMeta->sigs.size(); i++) {

if(0 == strcmp(s1+1, senderMeta->sigs[i].c_str())) {

sig_idx = i;

}

}

//这里确认是槽函数,并找到对应的槽函数索引

//如果有信号与信号关联的情况这里就需要去查找接收者对应的信号索引,这里省略了

if('1' == *s2) {

for(int i = 0; i < receiverMeta->slts.size(); i++) {

if(0 == strcmp(s2+1, receiverMeta->slts[i].c_str())) {

slt_idx = i;

}

}

}

if(-1 == sig_idx || -1 == slt_idx) {

std::cout << "no match sig or slt" << std::endl;

}

//利用multimap建立信号索引与接收者和方法索引的对应关系

MetaObject::Connection con = {receiver, slt_idx};

sender->mp.insert(std::make_pair(sig_idx, con));

}

//下面的主要是预留的方便父类调用子类重写方法的接口这里简单定义即可

void Object::metaCall(int idx, void **ag)

{

}

MetaObject Object::meta;

MetaObject *Object::getMetaObject()

{

return &meta;

}

下面就轮到MOC生成的moc_xx.cpp,这些文件在Qt中是自动生成的不需要我们实现,我这里只能手动模拟简单的实现发送者的moc_sender.cpp与接收者的moc_receiver.cpp最终我们编译程序是需要将这两个文件一起编译才能通过的。

moc_sender.cpp

#include "sender.h"


//根据定义的信号槽顺序将信号与槽函数名称进行保存,Qt中会将函数名称参数分开保存处理,这里简单模拟以下就好

static const char *sigs_name[] = {"holdClass(int)"};

static const char *slts_name[] = {nullptr}; //空表示当前没有定义对应的函数

static std::vector<std::string> sigs(sigs_name, sigs_name+1);

static std::vector<std::string> slts;

MetaObject Sender::meta = {sigs, slts};

//Sender的信号定义

void Sender::holdClass(int n)

{

void *arg[] = {(void *)&n};

//调用MetaObject的静态方法activate传递当前的信号发送者对象、信号索引及参数

MetaObject::activate(this, 0, arg); //0表示当前信号函数在sigs_name[]中的索引

}

MetaObject *Sender::getMetaObject()

{

return &meta; //返回Sender的meta对象

}


void Sender::metaCall(int idx, void **arg)

{

// 我们这里Sender 中没有槽函数所以这里没任何操作

}


moc_receiver.cpp

#include "receiver.h"


static const char *sigs_name[] = {nullptr};

static const char *slts_name[] = {"attendClass(int)"};

static std::vector<std::string> sigs;

static std::vector<std::string> slts(slts_name, slts_name+1);


MetaObject Receiver::meta = {sigs, slts};


MetaObject *Receiver::getMetaObject()

{

return &meta; //返回Receiver的meta对象

}


void Receiver::metaCall(int idx, void **arg)

{

//这里根据slts_name[]中的索引值调用对应的槽函数

if(0 == idx) {

int n = *((int *)arg[0]);

attendClass(n);

}

}

有了上面这些文件最后我们只需要将所有的.cpp文件一起编译运行就可以实现Qt中信号与槽的效果了:

g++ object.cpp sender.cpp receiver.cpp moc_sender.cpp moc_receiver.cpp main.cpp -o xuedao

也可用其他可使用的编译器编译进行编译,这里直接用的g++。

?

随着“双减”政策落地,入局成人培训成为了众多教培机构的热门选择。作为一家有着20余年教培经验的老牌企业,文都致力于为学员提供高水平教学服务,构建完善教研体系,帮助来自全国各地的众多学员实现了自身理想。

“据我们观察,针对不同年龄段教学的机构,策略和做法是不同的。”文都教育集团副总裁汪华阳曾表示,中小学培训和成人培训的理念不同,会导致机构在产品设计、交付,甚至产品服务人员的设置都不同。“仅从基础硬件的角度来看,机构间的差异还是非常大的。”由此不难看出,想要在已趋于成熟的成人培训市场站稳脚跟,哪怕是曾经的K12头部机构也需要狠下一番功夫。

优势积攒并非一日之功

老玩家在行业内深耕多年,在长期发展中也逐步积攒下了不可小觑的企业优势。文都集团自成立至今的20余年间,不仅积累了丰富的行业洞察和市场口碑,也一直在教学、教研、服务等方面积极探索,不断跟进市场需求动态调整。同时,立足强大的品牌影响力,文都集团稳步推进全国化布局,形成了较为强大的国内市场——据统计,文都目前已在全国设立了1000多个教学网点,累计培训学员1000多万人次。

期间,文都集团也基于考生对于授课场景、服务场景、班型等不同层次的需求,按照由浅入深以及整个考试流程设计了完善的课程培训体系,相继推出了涵盖考研、大学英语四六级、医考相关、公考、教资考试、财务会计考试、法律职业资格考试等多种类型的优质课程产品。目前,文都已经形成了线上、线下融合发展的教育培训产品链格局,以多样化的教育产品和渠道途径充分满足学子们多元化、多方面的学习需求。由此可见,对于老玩家来说,打赢这场硬仗的关键还是看实力,实力雄厚者有了这些新入局企业的对照提升,自然能够在竞争中倒逼自身更上一层楼。

教学力才是行业硬通货

随着新玩家的涌入和竞争的加剧,成人培训正向着更加规范化、市场化的方向发展,具备教学教研硬实力的专业机构将迎来前所未有的行业利好。目前,成人培训市场内耳熟能详、市场占有率较高的,均是和文都集团一样资历老、经验足的品牌。这些企业通过多年积累和迭代,构建了具有自身特色的教研体系,能够持续稳定产出高品质的教学产品。

文都集团依托“橄榄型师资结构”努力提高自身教学壁垒,打造优质师资的完善供应链,为教学提供源源不断的师资保障。“橄榄型师资结构”两端分别是大咖教师和教学研究院,中间大结构为优秀教师。文都集团通过建设标准化师资和教研体系,来持续放大大咖教师的影响力,打造“教、学、测、练、评”的全链路教学闭环,确保高水平教学内容持续稳定产出。

文都教育集团师资培养体系

在优秀师资之外,文都集团也十分注重配套教育资源的产出和积累。文都教研院组织编写了与课程相配合使用的精品图书、内部讲义、考前模拟题等专业的辅导材料,辅助课程达到更好的效果。

文都图书展示

激活高质量发展新引擎

作为成人培训领域的专业教育品牌,文都集团具备一定的先发优势。但在越来越多新玩家入局的市场环境下,文都集团也没有故步自封,而是面向学员日益精细化的学习需求,寻找自身业务高质量发展的突破点。

早在几年前,文都教育便开始加码教育科技布局,基于移动互联网、人工智能、大数据、云计算等技术,打造以文都教育大数据、文都智能学习系统、文都教育新生态为核心的文都智慧教育体系。在全新升级的OMO战略中,慧学课程更是进一步放大线上线下融合业务的增长引擎价值,将课程、教师、标准教材等教学环节充分打通。在智能督学系统的辅助下,文都教学研究院的专家老师们基于现有学情分析报告,围绕各个学习场景,深入挖掘学员潜在需求,对各教学环节进行科学指导,提高教学效率,实现因材施教。

帮助学员快速成长的“三位一体”图

与此同时,文都集团也持续利用先进技术赋能教师,大大提高教师的备课效率,将他们从繁冗的重复性工作中抽离出来,从而有更多时间和精力去关注学生高阶思维、方法总结和创新能力的培养,帮助学员科学备考、高效复习。

日期(2023-01-14) 评论(0) 浏览(11)

0 评论

发表评论