在这篇博客博主想记录一下此次结队编程作业中与ui组对接的心得。在这里我也想表达一下对涂涵越同学的敬佩,他遇到困难时孜孜不倦求解的毅力着实让我佩服,我们在dll的生成上遇到了很大的困难,要不是他的坚持我们可能就换用类接口,或者两方都碰壁了。能够遇到这样一位治学态度这么好、很负责任的搭档真的是非常幸运啊。接下来我主要从两方面讲一下自己的心得体会。
1. DLL封装
(1)stl接口的实现:在dll接口中传递stl(标准模板库)类型和容器时非常麻烦的事情,不管是生成还是使用起来都有很多陷阱。一开始我们在头文件中输入函数声明时,返回类型是string、string*、 map的地方都报了错,我们的程序中有两个比较复杂的自定义类,其中一个还使用了友元函数和重构运算符,如果dll真如网上大多数博客所说,只支持C接口,那问题就变得相当棘手了。幸运的是,dll并不是不支持stl的,但是写stl的接口方法比较复杂。微软官方给出的解释是:
Most classes in the Standard C++ Libraries use static data members directly or indirectly. Since these classes are generated through template instantiation, each executable image (usually with DLL or EXE file name extensions) will contain its own copy of the static data member for a given class. When a method of the class that requires the static data member is executed, it uses the static data member in the executable image in which the method code resides. Since the static data members in the executable images are not in sync, this action could result in an access violation or data may appear to be lost or corrupted.
(我的翻译:标准库中的大部分类都直接或间接地使用静态的数据成员。 由于这些类是通过模板实例化产生的,每个可执行的像文件(一般是.dll或.exe文件)会复制每一个类中的静态数据成员。当需要类中需要用到这些静态数据成员的方法即将被执行时,这个方法会使用方法代码所在的或所对应的像文件中的静态数据成员。由于像文件中的这些静态数据成员不同步(应该是指不和生成它的初始文件同步), 调用这个方法可能会导致访问失败,或者系统会提示数据丢失或被损坏。)
根据这个提示,我们将类的定义,包括其数据成员的定义及初始化、成员函数的声明写在头文件中,然后在cpp文件include该头文件, 编写函数体(需加上“类名::”),并直接引用该自定义类。点击生成解决方案,就可以成功生成lib文件和dll文件。
以我们用于生成题目的arithmetic类为例,在头文件中,我们定义数据成员和成员函数(方法)如下:
class ArithmeticAPI arithmetic {
private:
typedef struct expNode {
string exp;
string ans;
}expNode;
expNode *p;
int expNum;
int expType; //0 for integers, 1 for decimals, 2 for fractions
int oprNum;
int oprType[5]; //0 for +-, 1 for +-x/, 2 for +-x/^
int min, max;
int accuracy; //the accuracy of decimals
public:
arithmetic() {
expNum = 1;
expType = 0;
oprNum = 1;
oprType[0] = 1;
oprType[1] = 1;
oprType[2] = 0;
oprType[3] = 0;
oprType[4] = 0;
min = 1;
max = 10;
accuracy = 2;
}
void setExpNum(int n);
void setExpType(int n);
void setOprNum(int n);
void setOprAdd(int n);
void setOprSub(int n);
void setOprMul(int n);
void setOprDiv(int n);
void setOprPow(int n);
void setOprAll(int a, int s, int m, int d, int p);
void setOprByStr(string s);
void setBounds(int min, int max);
void setAccuracy(int n);
int getExpNum();
string* getExpSet();
string* getAnsSet();
void generate();
};
在cpp文件中则完成函数体的编写:
#include"7_ArithmeticDll.h"
/* Member functions of arithmetic class */ void arithmetic::setExpNum(int n) { try { expNum = n; if (expNum < 0) { throw "The number of expressions must be an integer and at least one."; } } catch (const char* msg) { cout << msg << endl; setExpNum(0); } } void arithmetic::setExpType(int n) { try { expType = n; if (expType > 2 || expType < 0) { throw "Type of expressions must be an integer among 0, 1 and 2."; } } catch (const char* msg) { cout << msg << endl; expType = 0; setExpNum(0); } }
(2)宏定义API接口
#ifndef ArithmeticDLL #define ArithmeticAPI __declspec(dllexport) #else #define ArithmeticAPI __declspec(dllimport) #endif
在cpp文件中#define ArithmeticDLL ,这样的话在头文件中引用ArithmeticAPI,就是__declspec(dllexport)(输出)。调用方在不#define ArithmeticDLL, 则其引用 ArithmeticAPI的话,对应的就是__declspec(dllimport)(输入)。
(3)DLL工程:不同版本的Visual Studio支持的dll工程或可以生成dll的工程使用方法不同,有一些工程需要手动添加模块定义文件(带.def扩展名)才能生成lib文件。我之前使用的是VS2015,把网上推荐的方法都试了一遍,也没有正确生成dll和lib文件,我的搭档用的是VS2017,直接建立一个DLL工程,经过以上步骤就可以了。以下是VS2017的新建工程界面:
(4)dll的使用:摘抄自我们的API文档
· 将7_ArithmeticDll.dll,7_ArithmeticDll.lib以及7_ArithmeticDll.h 三个文件 复制到存放即将调用core的.cpp的文件夹中
· 在头文件中添加 现有项 "7_ArithmeticDll.h"
· 在资源文件中 添加 现有项 "7_ArithmeticDll.lib"
· 在用来调用core的.cpp中添加 #include"7_ArithmeticDll.h"
· release/debug、x64/x86不能混用
2.参数的传递
为了设置题目生成器,需要传递许多参数,从上面arithmetic类的定义可以看到,需要设置的参数有
int expNum;
int expType; //0 for integers, 1 for decimals, 2 for fractions
int oprNum;
int oprType[5];
int min, max;
int accuracy; //the accuracy of decimals
我们觉得一个子程序(函数)传递这么多参数不易于使用,出错率比较高,所以将各个参数的设置分散到各个set函数中(在上面代码中可以看到),同时为各个参数赋予了比较合理的默认值。
生成题目后,我们将题目存在string数组中并返回string数组的指针(string*类型),后来我们才知道用Qt框架开发UI的同学需要将string类转为Qstring类, 这一点说明我们在接口通用性上考虑还是欠妥当。另外,关于运算符类型的设置,各组core各执一词,有一些组可以任意选择,而我们是选择向下兼容,并且提供了不止一个设置方式,以满足ui的需要。
经过这次和UI组的对接,我觉得接口提供方和调用方一定要保持良好的沟通,不然会导致双方都付出额外的时间。