C++在VS下创建、调用dll
1.dll的优点
代码复用是提高软件开发效率的重要途径。一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块并在之后的项目中重复使用。比较常见的例子是各种应用程序框架,ATL、MFC等,它们都以源代码的形式发布。由于这种复用是“源码级别”的,源代码完全暴露给了程序员,因而称之为“白盒复用”。“白盒复用”的缺点比较多,总结起来有4点。
暴露了源代码;多份拷贝,造成存储浪费;
容易与程序员的“普通”代码发生命名冲突;
更新功能模块比较困难,不利于问题的模块化实现;
实际上,以上4点概括起来就是“暴露的源代码”造成“代码严重耦合”。为了弥补这些不足,就提出了“二进制级别”的代码复用。使用二进制级别的代码复用一定程度上隐藏了源代码,对于缓解代码耦合现象起到了一定的作用。这样的复用被称为“黑盒复用”。
说明:实现“黑盒复用”的途径不只dll一种,静态链接库甚至更高级的COM组件都是。
2.dll的创建
参考程序原文:http://msdn.microsoft.com/zh-cn/library/ms235636.aspx
新建“Win32项目”,选择应用程序类型为"DLL”,其他默认。添加头文件testdll.h
//testdll.h #ifdef TESTDLL_EXPORTS #define TESTDLL_API __declspec(dllexport) #else #define TESTDLL_API __declspec(dllimport) #endif namespace MathFuncs { // This class is exported from the testdll.dll class MyMathFuncs { public: // Returns a + b static TESTDLL_API double Add(double a, double b); // Returns a - b static TESTDLL_API double Subtract(double a, double b); // Returns a * b static TESTDLL_API double Multiply(double a, double b); // Returns a / b // Throws const std::invalid_argument& if b is 0 static TESTDLL_API double Divide(double a, double b); }; }
当定义了符号TESTDLL_EXPORTS,TESTDLL_API被设置为 __declspec(dllexport) 修饰符,This modifier enables the function to be exported by the DLL so that it can be used by other applications。若未定义则TESTDLL_API被设置为__declspec(dllimport),This modifier enables the compiler to optimize the importing of the function from the DLL for use in other applications。当DLL项目生成时,TESTDLL_EXPORTS默认是定义的,所以默认设置的是__declspec(dllexport) 修饰符。
添加cpp文件
// testdll.cpp : 定义 DLL 应用程序的导出函数。 #include "stdafx.h" #include "testdll.h" #include <stdexcept> using namespace std; namespace MathFuncs { double MyMathFuncs::Add(double a, double b) { return a + b; } double MyMathFuncs::Subtract(double a, double b) { return a - b; } double MyMathFuncs::Multiply(double a, double b) { return a * b; } double MyMathFuncs::Divide(double a, double b) { if (b == 0) { throw invalid_argument("b cannot be zero!"); } return a / b; } }
编译就会生成对应的dll文件,同时也会生成对应的lib文件。
注意:a.DLL中导出函数的声明有两种方式:在函数声明中加上__declspec(dllexport);采用模块定义(.def)文件声明。详见:http://www.cnblogs.com/enterBeijingThreetimes/archive/2010/08/04/1792099.html
b.对于C文件创建dll时或者想使用C编译器创建dll时,建议使用 extern “C” 标志,参见extern "C"的简单解析
3.dll的调用
应用程序使用DLL可以采用两种方式:一种是隐式链接(调用),另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息。VS在VC\bin目录下提供了一个名为Dumpbin.exe的小程序,用它可以查看DLL文件中的函数结构。两种的对比详见:http://blog.sina.com.cn/s/blog_53004b4901009h3b.html
隐式链接采用静态加载的方式,比较简单,需要.h、.lib、.dll三件套。新建“控制台应用程序”或“空项目”。配置如下:
项目->属性->配置属性->VC++ 目录-> 在“包含目录”里添加头文件testdll.h所在的目录
项目->属性->配置属性->VC++ 目录-> 在“库目录”里添加头文件testdll.lib所在的目录
项目->属性->配置属性->链接器->输入-> 在“附加依赖项”里添加“testdll.lib”(若有多个 lib 则以空格隔开)
添加cpp文件
//mydll.cpp #include <iostream> #include "testdll.h" using namespace std; int main() { double a = 7.4; int b = 99; cout << "a + b = " << MathFuncs::MyMathFuncs::Add(a, b) << endl; cout << "a - b = " << MathFuncs::MyMathFuncs::Subtract(a, b) << endl; cout << "a * b = " << MathFuncs::MyMathFuncs::Multiply(a, b) << endl; cout << "a / b = " << MathFuncs::MyMathFuncs::Divide(a, b) << endl; try { cout << "a / 0 = " << MathFuncs::MyMathFuncs::Divide(a, 0) << endl; } catch (const invalid_argument &e) { cout << "Caught exception: " << e.what() << endl; } return 0; }
现在可以编译通过了,但是程序运行就报错,还需要将testdll.dll复制到当前项目生成的可执行文件所在的目录。
显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。
新建项目,不需要特殊配置,添加cpp文件
/* *作者:侯凯 *说明:显式调用DLL *日期:2013-6-5 */ #include<Windows.h> //加载的头文件 #include<iostream> using namespace std; int main() { typedef double (*pAdd)(double a, double b); typedef double (*pSubtract)(double a, double b); HMODULE hDLL = LoadLibrary("testdll.dll"); //加载dll文件 if(hDLL != NULL) { pAdd fp1 = pAdd(GetProcAddress(hDLL, MAKEINTRESOURCE(1))); //得到dll中的第一个函数 if(fp1 != NULL) { cout<<fp1(2.5, 5.5)<<endl; } else { cout<<"Cannot Find Function "<<"add"<<endl; } pSubtract fp2 = pSubtract(GetProcAddress(hDLL, "?Subtract@MyMathFuncs@MathFuncs@@SANNN@Z")); //得到dll中标示为"?..."的函数,C++编译器考虑了函数的参数 if(fp2 != NULL) { cout<<fp2(5.5, 2.5)<<endl; } else { cout<<"Cannot Find Function "<<"Subtract"<<endl; } FreeLibrary(hDLL); } else { std::cout<<"Cannot Find "<<"testdll"<<std::endl; } return 1; }
显式调用的问题:在DLL文件中,dll工程中函数名称在编译生成DLL的过程中发生了变化(C++编译器),在DLL文件中称变化后的字符为“name标示”。GetProcAddress中第二个参数可以由DLL文件中函数的顺序获得,或者直接使用DLL文件中的”name标示”,这个标示可以通过Dumpbin.exe小程序查看。如果C++编译器下,想让函数名更规范(和原来工程中一样),具体方法详见:http://blog.csdn.net/btwsmile/article/details/6676802。
当然,为了让函数名更规范,最常用的方式是:创建dll过程中使用C编译器来编译函数,这样DLL文件中的函数名和原dll工程中的函数名就一致了。
4.更一般的显式调用
为了解决上部分最后的问题,可以使用 extern “C” 为dll工程中的函数建立C连接,简单的示例工程如下。
在DLL创建的工程中,添加cpp文件
/* *作者:侯凯 *说明:创建dll,使用C接口——C编译器生成的dll中函数的"name标示"仍为addfun *日期:2013-6-5 */ // cdll.cpp : 定义 DLL 应用程序的导出函数。 // #include "stdafx.h" #ifdef __cplusplus // if used by C++ code extern "C" { // we need to export the C interface #endif __declspec(dllexport) int addfun(int a, int b) { return a+b; } #ifdef __cplusplus } #endif
编译即可生成DLL文件。在dll调用工程中,添加cpp文件
/* *作者:侯凯 *说明:显式调用dll *日期:2013-6-5 */ #include <windows.h> #include <iostream> using namespace std; void main() { typedef int(*FUNA)(int,int); HMODULE hMod = LoadLibrary("cdll.dll");//dll路径 if (hMod) { FUNA addfun = (FUNA)GetProcAddress(hMod, TEXT("addfun"));//直接使用原工程函数名 if (addfun != NULL) { cout<<addfun(5, 4)<<endl; } else { cout<<"ERROR on GetProcAddress"<<endl; } FreeLibrary(hMod); } else cout<<"ERROR on LoadLibrary"<<endl; }
运行,这样便可以调用dll的函数了。再进一步,上述dll文件如果通过隐式调用,利用.dll、.lib文件,调用函数应为
//隐式链接 #include <iostream> #pragma comment(lib,"cdll.lib") using namespace std; extern "C" _declspec(dllimport) int addfun(int a,int b); //载入addfun函数,这里起到了.h文件的作用 //dll中使用C编译器 故这里需要extern "C" 如果dll中无extern "C" //此处为:_declspec(dllimport) int addfun(int a,int b); void main() { cout<<addfun(5,4)<<endl; }
String关键源码解析
根据java官网文档的描述,String类代表字符串,是常量,他们的值在创建之后是不可变的,究竟String类型是怎么实现这些的呢?
final关键字
在探讨String类型的原理之前,我们应该先弄清楚关于final关键字的使用:
1> 如果final修饰的是类的话,那么这个类是不能被继承的
2> 如果final修饰的是方法的话,那么这个方法是不能被重写的
3> 如果final修饰的是变量的话,那么这个变量的值在运行期间是不能被修改的
当然,关于具体的赋值等操作,可以参考《对象与内存管理》中的最后一点,这里就不再重复了。
String类与final的不解之缘
现在,我们开始探讨String类吧,下面只是String类的部分源代码:
1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence 3 { 4 /** The value is used for character storage. */ 5 private final char value[]; //用来存储字符串转换而来的字符数组 6 7 /** The offset is the first index of the storage that is used. */ 8 private final int offset; //字符串起始字符在字符数组的位置 9 10 /** The count is the number of characters in the String. */ 11 private final int count; //字符串分解成字符数组后字符的数目 12 }
从上面代码,我们知道:
1> String类是被final修饰的,从安全角度来说,通过final修饰后的String类是不能被其他类继承的,在最大程度上的保护了该类,从效率上来说,提高了该类的效率,因为final修饰后会比没有用final修饰的快。
2> value[], offet, count也是被final修饰的,这时候三个变量的值必须在编译期间就被确定下来,并且在运行期间不能再被修改了。因此,每次我们每次进行字符串修改、拼接的时候,并不能直接修改当前String对象的值,只能重新创建一个新的对象。
3>我们创建String对象的时候,String对象还使用字符数组(char[])来存储我们的字符串的。
String类常用的构造方法
其实呢,一般情况下,我们使用String类的时候很少通过构造方法来创建String对象的,因为这是不推荐的,但是不知道大家知不知道,
1 String str = "abc";
这种通过直接量创建String对象其实就等效于下面使用了通过字符串构造方法创建对象的。
1 char data[] = {'a', 'b', 'c'}; 2 String str = new String(data);
但是一般情况下使用第二这种方式太麻烦了,所以我们都推荐使用第一种方式创建String对象。
下面我们开始讲解一下String类常用的构造方法吧。
1>无参数的构造方法,这个创建的String对象的值为"",注意是是"",这个就等效于我们String str = "";具体关于参数的相信不用讲大家都应该知道了吧,不记得了的朋友可以看回前面final中列举出的代码注释。
1 /** 2 * Initializes a newly created {@code String} object so that it represents 3 * an empty character sequence. Note that use of this constructor is 4 * unnecessary since Strings are immutable. 5 */ 6 public String() { 7 this.offset = 0; 8 this.count = 0; 9 this.value = new char[0]; 10 }
2>使用Stirng对象作为构造方法的参数,需要注意的是,通过该构造方法创建String对象将会产生2个字符串对象,所以不推荐使用(具体为什么是两个,可以参考博文《小学徒进阶系列—JVM对String的处理》)
1 public String(String original) { 2 int size = original.count; //获取源字符串的字符数量 3 char[] originalValue = original.value; //获取源字符串的字符数组 4 char[] v; //用于新字符串对象存储字符数组 5 if (originalValue.length > size) { 6 int off = original.offset; //获取源字符串起始字符在字符数组的位置 7 v = Arrays.copyOfRange(originalValue, off, off+size); //返回将源字符数组复制到新的字符数组中 8 } else { 9 // The array representing the String is the same 10 // size as the String, so no point in making a copy. 11 v = originalValue; 12 } 13 this.offset = 0; 14 this.count = size; 15 this.value = v; 16 }
其实在构造方法中的各行代码里,我想大家在看这行代码的时候,最想知道的应该是Arrays.copyOfRange(char[] original,int from,int to)中各个参数的含义吧,官网中的解释是这样子的:
public static char[] copyOfRange(char[] original, int from, int to) 将指定数组的指定范围复制到一个新数组。该范围的初始索引 (from) 必须位于 0 和 original.length(包括)之间。original[from] 处的值放入副本的初始元素中(除非 from == original.length 或 from == to)。原数组中后续元素的值放入副本的后续元素。该范围的最后索引 (to)(必须大于等于 from)可以大于 original.length,在这种情况下,'\\u000' 被放入索引大于等于 original.length - from 的副本的所有元素中。返回数组的长度为 to - from。
参数: original - 将要从其复制一个范围的数组 from - 要复制的范围的初始索引(包括) to - 要复制的范围的最后索引(不包括)。(此索引可以位于数组范围之外)。
返回: 包含取自原数组指定范围的新数组,截取或用 0 填充以获得所需长度
抛出: ArrayIndexOutOfBoundsException - 如果 from < 0 或 from > original.length() IllegalArgumentException - 如果 from > to NullPointerException - 如果 original 为 null
3>使用字符数组作为String构造方法的参数,前面你我们已经知道了,使用String str = "abc",相当于使用该构造方法创建对象
1 public String(char value[]) { 2 int size = value.length; 3 this.offset = 0; 4 this.count = size; 5 this.value = Arrays.copyOf(value, size); 6 }
4>同样使用字符数组作为String构造方法的参数,但是并不是全部都是用来构造字符串对象的,而是只使用从offerset起的count个字符作为String对象的值。
1 public String(char value[], int offset, int count) { 2 if (offset < 0) { 3 throw new StringIndexOutOfBoundsException(offset); 4 } 5 if (count < 0) { 6 throw new StringIndexOutOfBoundsException(count); 7 } 8 // Note: offset or count might be near -1>>>1. 9 if (offset > value.length - count) { 10 throw new StringIndexOutOfBoundsException(offset + count); 11 } 12 this.offset = 0; 13 this.count = count; 14 this.value = Arrays.copyOfRange(value, offset, offset+count); 15 }
String类常用的方法
在这里,我重点讲解关于String类常用的方法,同时分析它的源代码,具体使用方法和执行结果,读者可以自行尝试哦,我就不累赘的写出来啦,而且我把这些代码缩起来了,避免整篇博文都是源代码,看的辛苦,大家需要看哪个方法的源代码,就直接展开哪个方法就行了,好啦,言归正传,我们开始分析吧。
1> public char charAt(int index)
返回指定索引处的 char 值。索引范围为从 0 到 length() - 1。序列的第一个 char 值位于索引 0 处,第二个位于索引 1 处,依此类推,这类似于数组索引。下面是该方法的源码解析:
2> public String concat(String str)
将指定字符串连接到此字符串的结尾。如果参数字符串的长度为 0,则返回此 String 对象。
否则,创建一个新的 String 对象,用来表示由此 String 对象表示的字符序列和参数字符串表示的字符序列连接而成的字符序列。(从上面的注释中,我们已经知道,因为String类型是常量,一旦创建值是不可以改变的,所以只能通过创建新的字符串并返回新字符串的引用,确保了字符串不可变及可共享)
3> public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
将字符从此字符串复制到目标字符数组。
要复制的第一个字符位于索引 srcBegin 处;要复制的最后一个字符位于索引 srcEnd-1 处(因此要复制的字符总数是 srcEnd-srcBegin)。要复制到 dst 子数组的字符从索引 dstBegin 处开始,并结束于索引: dstbegin + (srcEnd-srcBegin) - 1
参数:
srcBegin - 字符串中要复制的第一个字符的索引。
srcEnd - 字符串中要复制的最后一个字符之后的索引。
dst - 目标数组。
dstBegin - 目标数组中的起始偏移量。
4> indexOf(..)
这方法常用的有几个,比如public int indexOf(String str) 和 public int indexOf(String str, int fromIndex),他们都是返回指定子字符串在此字符串中第一次出现处的索引。只是前者是搜索整个字符串,而后者是从指定位置开始搜索。
public int indexOf(String str)
public int indexOf(String str, int fromIndex)
通过查看上面两个方法的源代码我们可以发现,这两个方法内部都是使用String类中的一个使用默认权限的indexOf()方法进行实现的,我们一起来进行详细的分析。
5> public boolean contains(CharSequence s)
当且仅当此字符串包含指定的 char 值序列时,返回 true。
从源代码我们也可以知道,这个方法利用的是前面说的indexOf(String str)方法进行实现的,具体就不再细说了。
6> public boolean isEmpty()
判断字符串是否为空。
7> startsWith(String prefix)、endsWith(String suffix)
前者是判断字符串是否以prefix为开头,而后者是判断字符串是否以suffix为结尾。
public boolean startsWith(String prefix)
public boolean endsWith(String suffix)
从上述代码我们可以看到,这两个方法同样调用的是一个方法来进行实现的,下面我们也来分析一下这个方法
8> copyValueOf(char data[])
其实,准确的说,这个方式是将data数组转换成字符串对象。
同样,这个方法调用的是下面这第9个方法进行实现的
9> copyValueOf(char data[], int offset, int count)
通过源代码我们可以知道, 这个方法通过调用String类型的构造方法进行创建并且返回的字符串对象
10> toCharArray()
将字符串对象的字符数组复制到一个新的数组中并返回这个新创建的数组。
11> trim()
这个方法用于去除当前字符串对象中的首部和尾部的空白,不会去除中间的空白。
这段代码很简单,我就不再详细介绍吧,大家自己看就行啦,呵呵,原谅我偷下懒吧。
String类的equals()和==
本来呢,equals也是String中的一个常用的方法,可是为什么要单独放出来讲呢?因为他太重要了,很多初学者都很容易把他和==给混淆了。下面我们讲解下String用这两个进行对象判断时两者的区别吧。
== 判断的是字符串对象引用地址是否相同
equals判断的主要是两个字符串对象中的内容是否相同。
我们举个代码作为例子吧,相信大家运行一次肯定就能够懂的了。