该动态库是为第3方系统提供数据导入导出功能(就是实现几个函数)。项目要求用C++进行封装,由于本人没有C++方面的开发经验,只能临时学习和琢磨了。下面简要说明一下项目的开发过程。
一、开发环境选择
我是windows环境下的开发者,选择的环境是Visual Studio 2010,环境安装就不再说了(这个应该难不倒开发人员的啊)。
二、创建项目
在vs窗口文件菜单->新建项目,在已安装的模板中选择Visual C++ 语言,然后再右侧选择win32项目,在下面输入项目名称,点击确定按钮,进入win32应用程序向导,点击“下一步”,在应用程序设置窗口,在应用程序类型中选择 DLL,在附加选项中选择导出符号,如下图:
选择好了后,点击“完成”,项目创建完成。项目结构如下图
三、定义导出函数
在头文件文件夹中打开MyDll.h头文件,在这里定义你需要的函数,我的项目中定义了如下函数:
MYDLL_API intrdcompany(void);
MYDLL_API int DBFGet(wchar_t* pFilePath,wchar_t* pFileName);
MYDLL_API int DBFUp(wchar_t* pFilePath,wchar_t*pFileName);
四、实现函数
在源文件文件夹中打开MyDll.cpp文件,发现头文件已被引入,下面的工作就是实现函数了,实现的过程我就不再详细说了,下面我详细谈谈在整个过程中用到的知识点和遇到的问题及解决方法。
五、dbf文件的读写
由于项目中需要读写中间文件,并且中间文件要求使用dbf文件。Dbf是一种数据库文件,可以使用odbc之类的数据访问引擎来访问,这需要依赖其他环境还有复杂的配置。我在网上搜了下dbf文件的格式,希望自己来实现dbf文件的读写,搜到文件格式定义后,发现也很复杂,又在网上搜了一下,看看有没有现成的代码可用,经过努力,竟然找到了。代码实现的很简单,只有两个文件(dbfAPI.h和dbfAPI.cpp),添加到项目中,测试,发现可以使用(很兴奋啊)。
先看一下头文件定义
整个还是比较简单的。
六、字符串的处理
说实在的,C++对字符处理太麻烦了,就是两个简单的字符串的拼接,都叫你头大(相对于C#、Java等)。不管怎么样还都做啊,下面就静下心慢慢就研究啦。
下面就说第1个问题,字符串拼接问题,经过查阅资料,找到了wcscpy和wcscat两个函数(也可以用CString类来实现),这两个函数的用法我就不说了,可以到网上搜啊。直接说说我的实现吧,要看实现,看代码最直接了,
1. wchar_t
2. wcscpy(tempPath,(wchar_t*)pFilePath);
3. wcscat(tempPath,L"\\");
4. wcscat(tempPath,(wchar_t*)pFileName);
这4行代码的功能就是拼接一个文件的绝对路径。解释一下这4行代码,第1行,定义一个wchar_t的定长数组(由于项目使用Unicode字符集),给需要处理的字符串分配所需要的内存空间。
第2-3行的是依次将字符串写入数组中。
七、判断文件是否存在
由于要读取中间文件,要在读取前判断文件是否存在和可以访问,实现方法很多,我的实现方法如下:
int IsExit(wchar_t* fileName)
{
int
HANDLEhandle = CreateFile(fileName,GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ,NULL,
OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL);
if(INVALID_HANDLE_VALUE == handle)
{
return
}
CloseHandle(handle);
return
}
八、动态数组
我的实现中需要用到中间数据,并且类型不同,因此,采用了Vector动态模板,关于Verctor简单介绍一下。
vector是C++标准模板库中的部分内容,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库。vector之所以被认为是一个容器,是因为它能够像容器一样存放各种类型的对象,简单地说,vector是一个能够存放任意类型的动态数组,能够增加和压缩数据。
使用方法很简单,定义自己的数据类型,可以是类、结构。
为了可以使用vector,必须在你的头文件中包含下面的代码:
|
vector属于std命名域的,因此需要通过命名限定,如下完成你的代码:
|
或者连在一起,使用全名:
|
建议使用全局的命名域方式:
|
Vector成员函数
函数 | 表述 |
c.assign(beg,end) c.assign(n,elem) | 将[beg; end)区间中的数据赋值给c。 将n个elem的拷贝赋值给c。 |
c.at(idx) | 传回索引idx所指的数据,如果idx越界,抛出out_of_range。 |
c.back() | 传回最后一个数据,不检查这个数据是否存在。 |
c.begin() | 传回迭代器重的可一个数据。 |
c.capacity() | 返回容器中数据个数。 |
c.clear() | 移除容器中所有数据。 |
c.empty() | 判断容器是否为空。 |
c.end() | 指向迭代器中的最后一个数据地址。 |
c.erase(pos) c.erase(beg,end) | 删除pos位置的数据,传回下一个数据的位置。 删除[beg,end)区间的数据,传回下一个数据的位置。 |
c.front() | 传回第一个数据。 |
get_allocator | 使用构造函数返回一个拷贝。 |
c.insert(pos,elem) c.insert(pos,n,elem) c.insert(pos,beg,end) | 在pos位置插入一个elem拷贝,传回新数据位置。 在pos位置插入n个elem数据。无返回值。 在pos位置插入在[beg,end)区间的数据。无返回值。 |
c.max_size() | 返回容器中最大数据的数量。 |
c.pop_back() | 删除最后一个数据。 |
c.push_back(elem) | 在尾部加入一个数据。 |
c.rbegin() | 传回一个逆向队列的第一个数据。 |
c.rend() | 传回一个逆向队列的最后一个数据的下一个位置。 |
c.resize(num) | 重新指定队列的长度。 |
c.reserve() | 保留适当的容量。 |
c.size() | 返回容器中实际数据的个数。 |
c1.swap(c2) swap(c1,c2) | 将c1和c2元素互换。 同上操作。 |
vector<Elem> c vector <Elem> c1(c2) vector <Elem> c(n) vector <Elem> c(n, elem) vector <Elem> c(beg,end) c.~ vector <Elem>() | 创建一个空的vector。 复制一个vector。 创建一个vector,含有n个数据,数据均已缺省构造产生。 创建一个含有n个elem拷贝的vector。 创建一个以[beg;end)区间的vector。 销毁所有数据,释放内存。 |
Vector操作
函数 | 描述 |
operator[] | 返回容器中指定位置的一个引用。 |
如下创建集合的代码:
vector<User> users; //创建集合
User user; //创建对象
memset(&user,0,sizeof(user));//为对象分配空间
strcpy(user.id,it->id.c_str());
strcpy(user.userid,it->userid.c_str());
strcpy(user.name,it->username.c_str());
…
users.push_back(user); //将数据对象添加到集合
如下代码演示读取数据集中的数据:
通过operator[]操作返回指定位置的对象,代码如下:
void WriteFileRowToTxt(wchar_t*pTxtFileName,vector<FileRow> fileRows)
{
std::ofstreamfout(pTxtFileName,std::ios::out);
int
char
char
int
for(index=0;index<iLength-1;++index)
{
sizeof(rowStr));
"%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\r\n",
fileRows[index].id.c_str(),
fileRows[index].district.c_str(),
fileRows[index].building.c_str(),
fileRows[index].unit.c_str(),
fileRows[index].floor.c_str(),
fileRows[index].direction.c_str(),
fileRows[index].userid.c_str(),
fileRows[index].username.c_str(),
fileRows[index].useraddress.c_str(),
fileRows[index].meterid.c_str(),
fileRows[index].metersum.c_str(),
fileRows[index].meterrdsn.c_str(),
fileRows[index].usertype.c_str(),
fileRows[index].bookid.c_str());
fout.write(rowStr,strlen(rowStr));
}
fout.close();
}
九、配置调试环境
由于开发的DLL动态库,程序没法直接执行,需要创建一个驱动模块,驱动模块可以使用任何语言,我这里采用。Net C#语言建立驱动模块。
用vs2010创建一个C#的项目,可以是windows form程序,也可以是控制台程序。我在这个项目中创建了windows form程序。项目创建好了后,就需要配置一下调试环境了。
第1步:将DLL动态库项目和windowsForm项目编译输出路径设置为相同的目录,建议在跟项目路径中建立一个编译目录,比如:编译目录
第2步:设置好windowsForm项目执行程序名称,如:MyTest
第3步:设置DLL调试环境,方法如下:先打开dll项目的属性页,找到配置属性下的调试,在对应的命令行输入驱动程序的路径,如下图:
第4步:在驱动模块中导入要调试的函数,变编写调用代码,然后启动调试。这时可以在dll中的函数上设置断点了。
Dll函数导入代码如下:
[DllImport("你的dll.dll", CharSet =CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public staticexternint DBFGet(stringfilePath,string
[DllImport("你的dll.dll", CharSet =CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public staticexternint DBFUp(stringfilePath,string
调用代码:
string filePath = Application.StartupPath;
string fileName = "下载.dbf";
int
MessageBox.Show(result.ToString());
十、一个奇葩的问题
在开发调试即将结束时,遇到了一个问题,在VB6中调用时,传入的字符串(文件路径和文件名称)变成了乱码,在vb6中刚开始我函数导入是这样写的:
Private Declare Function DBFGetLib "MyDll.dll" (ByVal filePath As String, ByVal fileName As String)As Integer
我传入字符串,VB应该会按找指针的方式传入,程序运行后发现,DLL中获取的参数值并不是我传入的实现值。
进过分析和思考,在想是不是用byVal 值传入方式不对,于是修改为byRef传入方式,经测试这种办法依然不行。
进过查阅相关资料,使用Unicode编码的C++是双字节,而vb6传入是ANSI编码,这样就卡在这里了。
后进过思考,尝试用指针方式传入,查Vb6资料,发现vb6中有个strPtr函数可以获取字符串起始地址,由于地址是个整型数,所有修改导入方式,如下:
Private Declare Function DBFGetLib " MyDll.dll" (ByVal filePath As Long, ByVal fileName As Long) AsInteger
Vb的调用代码修改为:
Dim filePath As String
Dim fileName As String
filePath = App.Path
fileName = "下载.dbf"
Dim iResult As Integer
iResult =DBFGet(StrPtr(filePath), StrPtr(fileName))
编译,执行,结果一切正常,问题终于解决。