目录

  • 前言
  • 打开源代码
  • 源码追踪解析
  • 总结

前言

  前面讲了那么多,Qt的元对象系统是多么多么厉害,多么多么好,那么Moc是具体是怎么工作的,所谓源码面前,了无秘密,下面让我们一探究竟。

打开源代码

前提时安装qt的时候必须选择 源码。比如我的路径是

/opt/Qt5.9.5/5.9.5/Src/qtbase/src/tools/moc/moc.pro

当然我是以管理员的权限打开QtCreator的

源码追踪解析

打开main.cpp,找到main函数:

int main(int _argc, char **_argv)
{return QT_PREPEND_NAMESPACE(runMoc)(_argc, _argv);
}

调用了 runMoc 函数,先来看一部分:

int runMoc(int argc, char **argv)
{QCoreApplication app(argc, argv);QCoreApplication::setApplicationVersion(QString::fromLatin1(QT_VERSION_STR));bool autoInclude = true;bool defaultInclude = true;Preprocessor pp;Moc moc;pp.macros["Q_MOC_RUN"];pp.macros["__cplusplus"];...

  注意Preprocessor和Moc类,这两个是比较关键的类。Preprocessor做一些预处理的工作,比如找到头文件中的Q_OBJECT、signals和slots等宏。然后Moc类负责分析文件,将结果输出到moc_xxx.cpp文件中去。这也就能解释为什么Q_OBJECT只能生命在头文件,而不能写到cpp文件中了,因为moc根本找不到,它只会扫描头文件。

  再往下是一堆 QCommandLineOption,也就是Qt元对象系统(Meta-Object)(二)、moc的使用一文中写到的moc所支持的一些命令行选项。

// Note that moc isn't translated.
// If you use this code as an example for a translated app, make sure to translate the strings.
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("Qt Meta Object Compiler version %1 (Qt %2)").arg(mocOutputRevision).arg(QString::fromLatin1(QT_VERSION_STR)));
parser.addHelpOption();
parser.addVersionOption();
parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);QCommandLineOption outputOption(QStringLiteral("o"));
outputOption.setDescription(QStringLiteral("Write output to file rather than stdout."));
outputOption.setValueName(QStringLiteral("file"));
outputOption.setFlags(QCommandLineOption::ShortOptionStyle);
parser.addOption(outputOption);QCommandLineOption includePathOption(QStringLiteral("I"));
includePathOption.setDescription(QStringLiteral("Add dir to the include path for header files."));
includePathOption.setValueName(QStringLiteral("dir"));
includePathOption.setFlags(QCommandLineOption::ShortOptionStyle);
parser.addOption(includePathOption);QCommandLineOption macFrameworkOption(QStringLiteral("F"));
macFrameworkOption.setDescription(QStringLiteral("Add Mac framework to the include path for header files."));
macFrameworkOption.setValueName(QStringLiteral("framework"));
macFrameworkOption.setFlags(QCommandLineOption::ShortOptionStyle);
parser.addOption(macFrameworkOption);QCommandLineOption preprocessOption(QStringLiteral("E"));
preprocessOption.setDescription(QStringLiteral("Preprocess only; do not generate meta object code."));
parser.addOption(preprocessOption);
...

再接下来的话会是一些命令选项的解析,比如-D所指定的宏"-DQT_GUI_LIB",或者 -I 指定的头文件目录。
然后我们看关键代码,有一些qDebug语句是我自己加的:

// 1. preprocess
// 包含的所有头文件
const auto includeFiles = parser.values(includeOption);
// 依次扫面解析头文件
for (const QString &includeName : includeFiles) {QByteArray rawName = pp.resolveInclude(QFile::encodeName(includeName), moc.filename);if (rawName.isEmpty()) {fprintf(stderr, "Warning: Failed to resolve include \"%s\" for moc file %s\n",includeName.toLocal8Bit().constData(),moc.filename.isEmpty() ? "<standard input>" : moc.filename.constData());} else {QFile f(QFile::decodeName(rawName));if (f.open(QIODevice::ReadOnly)) {moc.symbols += Symbol(0, MOC_INCLUDE_BEGIN, rawName);/// 关键代码!!!预处理头文件moc.symbols += pp.preprocessed(rawName, &f);moc.symbols += Symbol(0, MOC_INCLUDE_END, rawName);} else {fprintf(stderr, "Warning: Cannot open %s included by moc file %s: %s\n",rawName.constData(),moc.filename.isEmpty() ? "<standard input>" : moc.filename.constData(),f.errorString().toLocal8Bit().constData());}}
}
if (!pp.preprocessOnly) {// 2. parsemoc.parse();
}// 3. and output meta object code
qDebug() << "outputfile: " << output;if (output.size()) { // output file specified
#if defined(_MSC_VER)if (_wfopen_s(&out, reinterpret_cast<const wchar_t *>(output.utf16()), L"w") != 0)
#elseout = fopen(QFile::encodeName(output).constData(), "w"); // create output fileif (!out)
#endif{fprintf(stderr, "moc: Cannot create %s\n", QFile::encodeName(output).constData());return 1;}
} else { // use stdoutout = stdout;
}
qDebug() << "preprocessOnly: " << pp.preprocessOnly;
if (pp.preprocessOnly) {fprintf(out, "%s\n", composePreprocessorOutput(moc.symbols).constData());qDebug() << "outputfile: " << composePreprocessorOutput(moc.symbols).constData();
} else {if (moc.classList.isEmpty())moc.note("No relevant classes found. No output generated.");elsemoc.generate(out);
}if (output.size())fclose(out);
moc.symbols += pp.preprocessed(moc.filename, &in);

Symbols这个类很简单,大家可以自己去看下,主要记录一些符号的信息,比如标记,行号等。pp.preprocessed(rawName, &f) 是很关键的一个函数,我们跳转进去一探究竟:

Symbols Preprocessor::preprocessed(const QByteArray &filename, QFile *file)
{// 读出文件的内容或者将内容映射到内存QByteArray input = readOrMapFile(file);if (input.isEmpty())return symbols;// phase 1: get rid of backslash-newlines// 将\r\n转换为\n// \r转换成\n (os9样式)// 反斜杠-换行换成成换行符input = cleaned(input);// phase 2: tokenize for the preprocessorindex = 0;// 先标记需要预处理的地方,// 比如: #include, signals, slots...symbols = tokenize(input);#if 0for (int j = 0; j < symbols.size(); ++j)fprintf(stderr, "line %d: %s(%s)\n",symbols[j].lineNum,symbols[j].lexem().constData(),tokenTypeName(symbols[j].token));
#endif// phase 3: preprocess conditions and substitute macrosSymbols result;// Preallocate some space to speed up the code below.// The magic value was found by logging the final size// and calculating an average when running moc over FOSS projects.result.reserve(file->size() / 300000);preprocess(filename, result);mergeStringLiterals(&result);#if 0for (int j = 0; j < result.size(); ++j)fprintf(stderr, "line %d: %s(%s)\n",result[j].lineNum,result[j].lexem().constData(),tokenTypeName(result[j].token));
#endifreturn result;
}

其中

symbols = tokenize(input);

的作用是先标记需要预处理的地方比如: #include, signals, slots…,比如有头文件:

#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>class Widget : public QWidget
{Q_OBJECTpublic:explicit Widget(QWidget *parent = 0);~Widget();signals:void Sig();private slots:void onSig();private:
};#endif // WIDGET_H

打印的结果是:

line 12: Widget(IDENTIFIER)
line 12: ((LPAREN)
line 12: )(RPAREN)
line 12: ;(SEMIC)
line 14: signals(SIGNALS)
line 14: :(COLON)
line 15: void(VOID)
line 15: Sig(IDENTIFIER)
line 15: ((LPAREN)
line 15: )(RPAREN)
line 15: ;(SEMIC)
line 17: private(PRIVATE)
line 17: slots(SLOTS)
line 17: :(COLON)
line 18: void(VOID)
line 18: onSig(IDENTIFIER)
line 18: ((LPAREN)
line 18: )(RPAREN)

再往下的话也是一个预处理函数 preprocess(filename, result) ,进一步处理。函数原型:

// 参数preprocessed传递的就是preprocessed函数中找到的一些需要预处理的标记信息等
void preprocess(const QByteArray &filename, Symbols &preprocessed);

依次判断preprocessed的token,

void Preprocessor::preprocess(const QByteArray &filename, Symbols &preprocessed)
{currentFilenames.push(filename);preprocessed.reserve(preprocessed.size() + symbols.size());while (hasNext()) {Token token = next();switch (token) {case PP_INCLUDE:{int lineNum = symbol().lineNum;QByteArray include;bool local = false;if (test(PP_STRING_LITERAL)) {local = lexem().startsWith('\"');include = unquotedLexem();} elsecontinue;until(PP_NEWLINE);...}// 熟悉的信号槽case SIGNALS:case SLOTS: {Symbol sym = symbol();if (macros.contains("QT_NO_KEYWORDS"))sym.token = IDENTIFIER;elsesym.token = (token == SIGNALS ? Q_SIGNALS_TOKEN : Q_SLOTS_TOKEN);preprocessed += sym;} continue;...
}

小结:关于preprocessed函数大概有了个认识,它读取所有需要的头文件,处理保存所有读到的的宏定义信息,比如:#include, public, signals, slots, Q_OBJECT等具体的信息保存到Symbols的一个实例中。

再接下来就是main.cpp中的parse()函数。

if (!pp.preprocessOnly) {// 2. parsemoc.parse();
}

根据读到的宏定义进行解析:

void Moc::parse()
{QVector<NamespaceDef> namespaceList;bool templateClass = false;while (hasNext()) {Token t = next();switch (t) {case NAMESPACE: {int rewind = index;if (test(IDENTIFIER)) {QByteArray nsName = lexem();QByteArrayList nested;while (test(SCOPE)) {next(IDENTIFIER);nested.append(nsName);nsName = lexem();}...}if ((t != CLASS && t != STRUCT)|| currentFilenames.size() > 1)continue;ClassDef def;// 判断是否是一个前置声明if (parseClassHead(&def)) {FunctionDef::Access access = FunctionDef::Private;for (int i = namespaceList.size() - 1; i >= 0; --i)if (inNamespace(&namespaceList.at(i)))def.qualified.prepend(namespaceList.at(i).classname + "::");while (inClass(&def) && hasNext()) {switch ((t = next())) {case PRIVATE:access = FunctionDef::Private;if (test(Q_SIGNALS_TOKEN))error("Signals cannot have access specifier");break;case PROTECTED:access = FunctionDef::Protected;if (test(Q_SIGNALS_TOKEN))error("Signals cannot have access specifier");break;case PUBLIC:access = FunctionDef::Public;if (test(Q_SIGNALS_TOKEN))error("Signals cannot have access specifier");break;case CLASS: {ClassDef nestedDef;if (parseClassHead(&nestedDef)) {while (inClass(&nestedDef) && inClass(&def)) {t = next();if (t >= Q_META_TOKEN_BEGIN && t < Q_META_TOKEN_END)error("Meta object features not supported for nested classes");}}} break;case Q_SIGNALS_TOKEN:parseSignals(&def);break;case Q_SLOTS_TOKEN:switch (lookup(-1)) {case PUBLIC:case PROTECTED:case PRIVATE:parseSlots(&def, access);break;default:error("Missing access specifier for slots");}break;case Q_OBJECT_TOKEN:def.hasQObject = true;if (templateClass)error("Template classes not supported by Q_OBJECT");if (def.classname != "Qt" && def.classname != "QObject" && def.superclassList.isEmpty())error("Class contains Q_OBJECT macro but does not inherit from QObject");break;...
}

ClassDef类中保存了每一个类的具体数据信息,类名,借口id,构造函数列表,信号列表,槽函数列表等:

struct ClassDef : BaseDef {QVector<QPair<QByteArray, FunctionDef::Access> > superclassList;struct Interface{Interface() {} // for QVector, don't useinline explicit Interface(const QByteArray &_className): className(_className) {}QByteArray className;QByteArray interfaceId;};QVector<QVector<Interface> >interfaceList;bool hasQObject = false;bool hasQGadget = false;struct PluginData {QByteArray iid;QMap<QString, QJsonArray> metaArgs;QJsonDocument metaData;} pluginData;QVector<FunctionDef> constructorList;QVector<FunctionDef> signalList, slotList, methodList, publicList;int notifyableProperties = 0;QVector<PropertyDef> propertyList;int revisionedMethods = 0;int revisionedProperties = 0;
};

再看parse函数,因为太多了,我们以信号和槽为例,当读到Q_SIGNALS_TOKEN和Q_SLOTS_TOKEN时分别调用了parseSignals和parseSolts函数。这两个函数大体一致,我们只看看parseSignals函数吧。parseSignals函数进行一些检查和信号函数解析保存的工作。

void Moc::parseSignals(ClassDef *def)
{int defaultRevision = -1;if (test(Q_REVISION_TOKEN)) {next(LPAREN);QByteArray revision = lexemUntil(RPAREN);revision.remove(0, 1);revision.chop(1);bool ok = false;defaultRevision = revision.toInt(&ok);if (!ok || defaultRevision < 0)error("Invalid revision");}next(COLON);while (inClass(def) && hasNext()) {switch (next()) {case PUBLIC:case PROTECTED:case PRIVATE:case Q_SIGNALS_TOKEN:case Q_SLOTS_TOKEN:prev();return;case SEMIC:continue;case FRIEND:until(SEMIC);continue;case USING:error("'using' directive not supported in 'signals' section");default:prev();}FunctionDef funcDef;funcDef.access = FunctionDef::Public;parseFunction(&funcDef);if (funcDef.isVirtual)warning("Signals cannot be declared virtual");if (funcDef.inlineCode)error("Not a signal declaration");if (funcDef.revision > 0) {++def->revisionedMethods;} else if (defaultRevision != -1) {funcDef.revision = defaultRevision;++def->revisionedMethods;}def->signalList += funcDef;while (funcDef.arguments.size() > 0 && funcDef.arguments.constLast().isDefault) {funcDef.wasCloned = true;funcDef.arguments.removeLast();def->signalList += funcDef;}}
}

其中的FunctionDef类与ClassDef的功能类似,FunctionDef保存的时函数的具体信息,比如参数列表,返回值等:

struct FunctionDef
{FunctionDef(): returnTypeIsVolatile(false), access(Private), isConst(false), isVirtual(false), isStatic(false),inlineCode(false), wasCloned(false), isCompat(false), isInvokable(false),isScriptable(false), isSlot(false), isSignal(false), isPrivateSignal(false),isConstructor(false), isDestructor(false), isAbstract(false), revision(0) {}Type type;QByteArray normalizedType;QByteArray tag;QByteArray name;bool returnTypeIsVolatile;QVector<ArgumentDef> arguments;enum Access { Private, Protected, Public };Access access;bool isConst;bool isVirtual;bool isStatic;bool inlineCode;bool wasCloned;QByteArray inPrivateClass;bool isCompat;bool isInvokable;bool isScriptable;bool isSlot;bool isSignal;bool isPrivateSignal;bool isConstructor;bool isDestructor;bool isAbstract;int revision;
};

再接下来就是生成代码了,还是main.cpp中的函数,代码生成主要还是generate函数:

// 3. and output meta object codeif (output.size()) { // output file specified
#if defined(_MSC_VER)if (_wfopen_s(&out, reinterpret_cast<const wchar_t *>(output.utf16()), L"w") != 0)
#elseout = fopen(QFile::encodeName(output).constData(), "w"); // create output fileif (!out)
#endif{fprintf(stderr, "moc: Cannot create %s\n", QFile::encodeName(output).constData());return 1;}} else { // use stdoutout = stdout;}if (pp.preprocessOnly) {fprintf(out, "%s\n", composePreprocessorOutput(moc.symbols).constData());qDebug() << "outputfile: " << composePreprocessorOutput(moc.symbols).constData();} else {if (moc.classList.isEmpty())moc.note("No relevant classes found. No output generated.");elsemoc.generate(out);}

generate函数:

void Moc::generate(FILE *out)
{QByteArray fn = filename;int i = filename.length()-1;while (i > 0 && filename.at(i - 1) != '/' && filename.at(i - 1) != '\\')--i;                                // skip pathif (i >= 0)fn = filename.mid(i);fprintf(out, "/****************************************************************************\n""** Meta object code from reading C++ file '%s'\n**\n" , fn.constData());fprintf(out, "** Created by: The Qt Meta Object Compiler version %d (Qt %s)\n**\n" , mocOutputRevision, QT_VERSION_STR);fprintf(out, "** WARNING! All changes made in this file will be lost!\n""*****************************************************************************/\n\n");if (!noInclude) {if (includePath.size() && !includePath.endsWith('/'))includePath += '/';for (int i = 0; i < includeFiles.size(); ++i) {QByteArray inc = includeFiles.at(i);if (inc.at(0) != '<' && inc.at(0) != '"') {if (includePath.size() && includePath != "./")inc.prepend(includePath);inc = '\"' + inc + '\"';}fprintf(out, "#include %s\n", inc.constData());}}if (classList.size() && classList.constFirst().classname == "Qt")fprintf(out, "#include <QtCore/qobject.h>\n");fprintf(out, "#include <QtCore/qbytearray.h>\n"); // For QByteArrayDatafprintf(out, "#include <QtCore/qmetatype.h>\n");  // For QMetaType::Typeif (mustIncludeQPluginH)fprintf(out, "#include <QtCore/qplugin.h>\n");const auto qtContainers = requiredQtContainers(classList);for (const QByteArray &qtContainer : qtContainers)fprintf(out, "#include <QtCore/%s>\n", qtContainer.constData());fprintf(out, "#if !defined(Q_MOC_OUTPUT_REVISION)\n""#error \"The header file '%s' doesn't include <QObject>.\"\n", fn.constData());fprintf(out, "#elif Q_MOC_OUTPUT_REVISION != %d\n", mocOutputRevision);fprintf(out, "#error \"This file was generated using the moc from %s."" It\"\n#error \"cannot be used with the include files from"" this version of Qt.\"\n#error \"(The moc has changed too"" much.)\"\n", QT_VERSION_STR);fprintf(out, "#endif\n\n");fprintf(out, "QT_BEGIN_MOC_NAMESPACE\n");fprintf(out, "QT_WARNING_PUSH\n");fprintf(out, "QT_WARNING_DISABLE_DEPRECATED\n");fputs("", out);for (i = 0; i < classList.size(); ++i) {Generator generator(&classList[i], metaTypes, knownQObjectClasses, knownGadgets, out);generator.generateCode();}fputs("", out);fprintf(out, "QT_WARNING_POP\n");fprintf(out, "QT_END_MOC_NAMESPACE\n");
}

然后核心代码是由 generator.generateCode() 来生成的。void Generator::generateCode()的代码非常多,我们这里只分析一下moc是如何生成信号的实现的。

void Generator::generateCode()
{...
//
// Generate internal signal functions
//for (int signalindex = 0; signalindex < cdef->signalList.size(); ++signalindex)generateSignal(&cdef->signalList[signalindex], signalindex);...
}
void Generator::generateSignal(FunctionDef *def,int index)
{if (def->wasCloned || def->isAbstract)return;fprintf(out, "\n// SIGNAL %d\n%s %s::%s(",index, def->type.name.constData(), cdef->qualified.constData(), def->name.constData());QByteArray thisPtr = "this";const char *constQualifier = "";if (def->isConst) {thisPtr = "const_cast< " + cdef->qualified + " *>(this)";constQualifier = "const";}Q_ASSERT(!def->normalizedType.isEmpty());if (def->arguments.isEmpty() && def->normalizedType == "void" && !def->isPrivateSignal) {fprintf(out, ")%s\n{\n""    QMetaObject::activate(%s, &staticMetaObject, %d, nullptr);\n""}\n", constQualifier, thisPtr.constData(), index);return;}int offset = 1;for (int j = 0; j < def->arguments.count(); ++j) {const ArgumentDef &a = def->arguments.at(j);if (j)fprintf(out, ", ");fprintf(out, "%s _t%d%s", a.type.name.constData(), offset++, a.rightType.constData());}if (def->isPrivateSignal) {if (!def->arguments.isEmpty())fprintf(out, ", ");fprintf(out, "QPrivateSignal _t%d", offset++);}fprintf(out, ")%s\n{\n", constQualifier);if (def->type.name.size() && def->normalizedType != "void") {QByteArray returnType = noRef(def->normalizedType);fprintf(out, "    %s _t0{};\n", returnType.constData());}fprintf(out, "    void *_a[] = { ");if (def->normalizedType == "void") {fprintf(out, "nullptr");} else {if (def->returnTypeIsVolatile)fprintf(out, "const_cast<void*>(reinterpret_cast<const volatile void*>(&_t0))");elsefprintf(out, "const_cast<void*>(reinterpret_cast<const void*>(&_t0))");}int i;for (i = 1; i < offset; ++i)if (i <= def->arguments.count() && def->arguments.at(i - 1).type.isVolatile)fprintf(out, ", const_cast<void*>(reinterpret_cast<const volatile void*>(&_t%d))", i);elsefprintf(out, ", const_cast<void*>(reinterpret_cast<const void*>(&_t%d))", i);fprintf(out, " };\n");fprintf(out, "    QMetaObject::activate(%s, &staticMetaObject, %d, _a);\n", thisPtr.constData(), index);if (def->normalizedType != "void")fprintf(out, "    return _t0;\n");fprintf(out, "}\n");
}

总结

  到这里基本上所有的主要函数已经分析完了,大致流程是:preprocessed函数读取所有的头文件,解析出来每一个宏和关键字,parse()函数再根据读出来的信息解析保存每一个类,函数等具体信息,然后generate负责将内容写入到文件。分析的比较粗糙,如果想一探究竟还是得自己研究每个细节,Qt的源码可读性也比较好,大家可根据我提到的几个函数自行去研究学习。

Qt元对象系统(Meta-Object)(四)、Moc源代码分析相关推荐

  1. Qt工作笔记-Qt元对象系统解析【2合1】

    博文转载地址: https://blog.csdn.net/spwper/article/details/51332187 说Qt信号与槽是一个很好机制,不如说Qt的元对象系统很强大.这也是大家讲Qt ...

  2. 【QT】QT元对象系统

    QT元对象系统(Meta-Object-System) 元对象系统 元对象系统是一个基于标准C++的扩展,为QT提供了信号与槽机制.实时类型信息.动态属性系统. 元对象系统的三个基本条件:类必须继承自 ...

  3. 1.QT元对象系统、信号槽概述、宏Q_OBJECT

    一.元对象系统(Meta-Object System) Qt添加C++原本不具备的元对象系统,元对象系统提供了信号槽机制,运行时类型信息和动态属性系统. 元对象系统基于三点: 1.元对象系统为以QOb ...

  4. Qt元对象系统:QMetaObject

    一.描述 此类包含有关Qt对象的元信息.Qt为应用程序中使用的每个 QObject 子类创建一个 QMetaObject 实例,该实例存储 QObject 子类的所有元信息. 二.静态成员函数 1.Q ...

  5. 基于Qt5.14.2和mingw的Qt源码学习(三) — 元对象系统简介及moc工具是如何保存类属性和方法的

    基于Qt5.14.2和mingw的Qt源码学习(三) - 元对象系统简介及moc工具是如何保存类属性和方法的 一.什么是元对象系统 1.元对象系统目的 2.实现元对象系统的关键 3.元对象系统的其他一 ...

  6. Qt元对象和属性机制

    Qt元对象和属性机制 1. 元对象 元对象(meta object)意思是描述另一个对象结构的对象,比如获得一个对象有多少成员函数,有哪些属性.在Qt中,我们将要用到的是QMetaObject这个类. ...

  7. linux创建自定义组件qt,QT中的元对象系统:创建自定义的QT类型

    原创文章,转载请注明出处,谢谢! 作者:清林,博客名:飞空静渡 QVariant可以表示QT中的大部分类型,它和pascal中的variant类型或c中的void类型有点相似,不过它的使用和c中的un ...

  8. Qt 如何实现的 Meta Object

    Qt 如何实现的 Meta Object 2009-11-01 11:57 741人阅读 评论(0) 收藏 举报 qtsignalcallbackobjectclasstable (文章转贴自guil ...

  9. 利用Qt元对象技术防止工厂模式下代码臃肿问题,QT 动态创建对象(第2种方法)

    问题的提出: 近来要编写一个仿真液压.电力.机械的软件,如下为液压的: 可以看到液压图中很多液压元器件,这些元器件的id.名称等都是从json配置文件读取的,配置文件格式如下: {"Clas ...

最新文章

  1. Android开发者指南(4) —— Application Fundamentals
  2. 百度面试题:生产者 消费者问题
  3. python变量初始化的位置不当、程序结果可能会出现问题_解决tensorflow由于未初始化变量而导致的错误问题...
  4. MyBatisPlus中进行通用CRUD全局策略配置
  5. Robo 3T SQL
  6. C++11 并发指南一(C++11 多线程初探)
  7. java上传文件功能_Java MemoryMapped文件的功能
  8. 2021数学建模C题题目
  9. win11 32位官方版原版镜像文件v2021.07
  10. SQL SERVER事务处理
  11. docker中使用idea部署运行项目(项目以镜像方式运行)
  12. Eclipse使用:Eclipse安装中文语言包
  13. 画三线格子的高效方法,不用再一个格子一个格子的选中啦
  14. vue 解决跨域问题404问题
  15. 计算机位移指令的作用,移位指令
  16. 用EndNote引用文献出现‘参数错误’解决方式
  17. [BZOJ3895]取石子
  18. 程序员面试金典-刷题笔记
  19. cadence 通孔焊盘_Cadence学习3(通孔类焊盘的建立)(转)
  20. CodeTop 1-20

热门文章

  1. 侦探调查罪案---采用程序计算
  2. CSS 链接与样式介绍
  3. 习惯五 知彼解己---移情沟通的原则
  4. ZYNQ-嵌入式学习(4)
  5. alter在MySQL中是什么意思_MySql之ALTER命令用法详细解读(转)
  6. 重启BarTender打印引擎没响应?你要的故障详解都在这!
  7. Calibre LVS 问题解析
  8. 是什么让双11成为传统电商的流量批发大战?
  9. VUE3 has no exported member ‘toRefs‘
  10. SharedUserData