类(二)
本节继续讨论封装。
在上一节中,虽然类将数据私有化,但类的公有函数代码同学们依然可以访问(看见..),并没有实现封装的作用。比如,你只想让别人用你的代码写文章,但是不想让合作方知道你如何写的代码。在这里,针对这个问题,继续介绍类的使用,其中主要涉及类的撰写习惯以及一个新的东西:库。
库
之前我们只是介绍了C++最基本的内容:函数和程序。我们在一个.C文件里,定义一个主程序,同时定义若干的类型以及函数,所有这些代码都在一个.C文件中。对其进行编译,即可生成我们的程序。执行这个程序即可进行运算。这是最基本最基本的C++程序,虽然包含了一些C++类的使用。
但是要在一个大型程序中,将所有的代码写在一个.C文件中是绝对不可能的。
同时,要充分发挥C++的性能,要尽可能的实现代码复用:重复的代码不需要定义多次。比如看CFD中的湍流模型,同学们完全可以定义一个kEpsilon
湍流模型函数,然后在不同的CFD求解器中调用这个函数。这样同学们就不用在每个CFD求解器中定义kEpsilon
湍流模型了。
尽可能的实现代码复用也是代码架构师的任务之一。
实现代码复用,可以把kEpsilon
湍流模型编写成其他求解器都能调用的代码,一种方法是将其编译为库 (动态库),然后在其他的CFD求解器中,调用此库。OpenFOAM代码中大量的代码被编译成库的形式。OpenFOAM安装目录下src文件夹中所有的代码,都被编译成了库。然后这些库可供不同的OpenFOAM程序来调用。OpenFOAM中库之庞大,甚至可以说OpenFOAM就是一个开源计算流体力学工具库。同学们完全可以在其他平台上调用OpenFOAM这个CFD工具库进行整合计算。目前很多CAE公司确实在做这个事情。
一个不是很好但很容易理解的例子
下面用一个小例子来编写一个库,我们把上一节的代码复制到一个名为myLib.C的文件中(位于class文件夹中),去除掉main()
函数部分,有:
#include <iostream>
using namespace std;
class myInt
{
private:
int a_;
int b_;
int c_ = 99;
public:
void assignA(int a)
{
a_ = a;
}
void assignB(int b)
{
b_ = b;
}
void sum()
{
c_ = a_ + b_;
}
void output()
{
cout << c_ << endl;
}
};
上面的代码只存在一个类声明,类的公有函数在声明内已经被定义。如果要将其编译为库,需要在编译路径的Make文件夹下的file文件中填入:
myLib.C
LIB = $(FOAM_USER_LIBBIN)/libmyLib
其中myLib.C
为需要编译的库程序名称(myLib可更改),后面的LIB
表示编译为库(不可更改),$(FOAM_USER_LIBBIN)/
表示编译地址(可更改),libmyLib
表示编译名称(可更改)。Make文件夹下的options文件不需要填入任何内容,因为myLib库不调用任何其他的库。随后键入wmake,会输出:
wmakeLnInclude: linking include files to ./lnInclude
Making dependency list for source file myLib.C
g++ -std=c++11 -m64 -Dlinux64 -DWM_ARCH_OPTION=64 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -O3 -DNoRepository -ftemplate-depth-100 -IlnInclude -I. -I/home/dyfluid/OpenFOAM/OpenFOAM-5.x/src/OpenFOAM/lnInclude -I/home/dyfluid/OpenFOAM/OpenFOAM-5.x/src/OSspecific/POSIX/lnInclude -fPIC -c myLib.C -o Make/linux64GccDPInt32Opt/myLib.o
g++ -std=c++11 -m64 -Dlinux64 -DWM_ARCH_OPTION=64 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -O3 -DNoRepository -ftemplate-depth-100 -IlnInclude -I. -I/home/dyfluid/OpenFOAM/OpenFOAM-5.x/src/OpenFOAM/lnInclude -I/home/dyfluid/OpenFOAM/OpenFOAM-5.x/src/OSspecific/POSIX/lnInclude -fPIC -shared -Xlinker --add-needed -Xlinker --no-as-needed Make/linux64GccDPInt32Opt/myLib.o -L/home/dyfluid/OpenFOAM/OpenFOAM-5.x/platforms/linux64GccDPInt32Opt/lib \
-o /home/dyfluid/OpenFOAM/dyfluid-5.x/platforms/linux64GccDPInt32Opt/lib/libmyLib.so
表示编译成功,最终的文件名为libmyLib.so
。现在这个库就可以供C++程序嵌入调用了。在这里可以下载上面的范例,直接运行wmake即可编译好libmyLib.so库。
在编译好库文件之后,需要在程序中嵌入调用。参考上一章介绍的OpenFOAM编译环境搭建,我们创立一个application文件夹,内部创建一个文件名为myLibTest.C的文件,将下面的代码输入进去:
#include <iostream>
using namespace std;
int main()
{
myInt myClass;
myClass.assignA(4);
myClass.assignB(10);
myClass.output();
myClass.sum();
myClass.output();
return 0;
}
在编译这个程序的时候,需要将刚才我们编译的库嵌入进入。在OpenFOAM中可以这样实现:在Make文件夹中的options文件中输入:
EXE_INC = -I../class/lnInclude
EXE_LIBS = -L$(FOAM_USER_LIBBIN) \
-lmyLib
options文件中信息的含义在这里已经介绍,下面增加一些内容。
第一行EXE_INC = -I../class/lnInclude
表示调用库文件所在的路径(不再赘述)。下一行LIB_LIBS
后面包含的信息表示调用库的文件路径和文件名。其中文件路径为$(FOAM_USER_LIBBIN)
,库文件名为myLib.so
。如果需要换行表示,需要在行尾添加\
符号。
下面键入wmake进行编译:
dyfluid@dyfluid:~/Solvers_DYFLUID/tutorials/1/application$ wmake
Making dependency list for source file myLibTest.C
g++ -std=c++11 -m64 -Dlinux64 -DWM_ARCH_OPTION=64 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -O3 -DNoRepository -ftemplate-depth-100 -I../class/lnInclude -IlnInclude -I. -I/home/dyfluid/OpenFOAM/OpenFOAM-5.x/src/OpenFOAM/lnInclude -I/home/dyfluid/OpenFOAM/OpenFOAM-5.x/src/OSspecific/POSIX/lnInclude -fPIC -c myLibTest.C -o Make/linux64GccDPInt32Opt/myLibTest.o
myLibTest.C: In function ‘int main()’:
myLibTest.C:8:5: error: ‘myInt’ was not declared in this scope
myInt myClass;
^
myLibTest.C:10:5: error: ‘myClass’ was not declared in this scope
myClass.assignA(4);
^
/home/dyfluid/OpenFOAM/OpenFOAM-5.x/wmake/rules/General/transform:25: recipe for target 'Make/linux64GccDPInt32Opt/myLibTest.o' failed
make: *** [Make/linux64GccDPInt32Opt/myLibTest.o] Error 1
dyfluid@dyfluid:~/Solvers_DYFLUID/tutorials/1/application$
其提示错误‘myInt’ was not declared in this scope
。这是一个很重要的提示,这表明,上面的配置虽然将库挂载到了求解器中,但是求解器编译的时候依然需要对自定义的类型myInt
进行声明。
这个错误可以这样来解决,在myLibTest.C文件中将库文件的源代码进行包含:
#include <iostream>
#include "myLib.C"
...
这样即可编译通过。同学们可以在这里下载相应的源代码。
在这个例子中,同学们了解了如何将自定义的类型编译为库,同时在程序中进行嵌入和调用。但上面的范例并不是一个好的范例。其具有以下缺陷:
- 需要在程序中对库文件的源代码进行包含,这并不能实现封装;
- 若通过在.C文件中将库的源代码进行包含,本质上不需要嵌入库;
- 在对主程序进行编译的时候,需要对包含的
"myLib.C"
文件进行编译比较耗时;
所以,这个例子并不是一个很好的例子,但可以让同学们理解库的概念。
真正的封装
做到完美的封装(同学们将代码拷贝给别人,别人不知道怎么计算的,但是能算出结果),关键是类的成员函数代码不能泄露。要做到这一点很简单,可以将类声明和类的实现(函数代码)分离。通常的做法是将类声明放在.H文件,类实现(机密算法)放在.C文件中。例如我们创建一个名为myLib.H的文件,键入(代码取自上一节):
//类声明
class myInt
{
private:
int a_;
int b_;
int c_ = 99;
public:
void assignA(int a);
void assignB(int b);
void sum();
void output();
};
在这个.H文件中,我们仅仅做了类的声明。同时,创建一个名为myLib.C的文件,键入(代码取自上一节):
#include <iostream>
#include "myLib.H"
using namespace std;
void myInt::assignA(int a)
{
a_ = a;
}
void myInt::assignB(int b)
{
b_ = b;
}
void myInt::sum()
{
c_ = a_ + b_;
}
void myInt::output()
{
cout << c_ << endl;
}
这个.C文件中不包含类的声明,只包含类的实现。并且在文件最开始,通过这一行#include "myLib.C"
将类的实现代码myLib.H进行了包含。对其编译后,会形成一个具有同样功能的库。在目录下,其看起来就是这个样子: 上面这种目录下一个.C文件(类实现),一个.H文件(类声明)以及一个Make文件夹(编译配置文件),像极了OpenFOAM的架构。确实,OpenFOAM大量的类通过这种方式被编译成了库。
同时注意,上面的代码每一行的函数前都添加了myInt::
,这是表明这个函数是myInt
类的函数。如果同学们还记得前面的名称空间,其也具有类似的用法,并且可以混合使用。比如如果自定义了一个名词空间为myName
,同时定义了一个类型test
,则可以在.H文件中这样写:
namespace myName
{
class test
{
void fun();
};
}
同时,在.C文件中的函数实现中,首先添加名称空间,再添加类的名字,如:
void myName::test::fun()
{
...
}
其表示fun()
属于名称空间myName
下的test
类型。
上文讨论的这种.H和.C文件的分开定义非常有利于实现类的封装。在后文中同学们会发现,即使别人想要你的程序,.C文件并不需要拷贝复制给其他人,也可以进行计算。
接下来,我们在myLibTest.C程序中调用这个库。在这里只需要做简单的一个改动:
- 将
#include "myLib.C"
变为#include "myLib.H"
,这是因为类的声明被定义在myLib.H文件中而不是myLib.C中,myLib.C中目前只包含了类的实现; - 下一步参考编译库的方法,在Make文件夹下的options中将编译的库
libmyLib.so
进行包含;
上述两步就实现了类的真正的封装。在感受类的封装之前,总结一下这段程序的框架:
- 将代码分为程序和库两部分;
- 库:
- 库的.H文件仅仅包含类的声明;
- 库的.C文件包含类实现(函数)的定义;
- 库需要编译为.so文件,OpenFOAM通常以libxxx.so命名,如libtest.so,libwhat.so;
- 程序:
- 程序调用库需要两个步骤。1)需要在程序.C文件中对库的.H文件进行包含;
- 2)程序在编译配置文件中(options文件)需要挂载库文件;
接下来我们实现类的封装的最后一步。在之前的过程中,我们把类的声明放在了.H文件中,类的实现放在了.C文件中,对这个.C文件进行编译,可以形成一个库(后缀为.so)。同时,在主程序内,将类的.H文件进行包含,同时在options文件中嵌入库文件,即可将程序成功编译。如果同学们想把自己的库函数封装,需要进行下面的步骤:
在自己的电脑上编写类的.H文件和.C文件;
编译出库文件;
拷贝.H文件和库文件给其他用户,其他用户的程序文件中即可调用这个库,并调用其中的库函数(类接口);
需要注意的是,你并不需要把.C文件拷贝给其他人,其他人即可调用你的库(你的函数实现)。这,就是非常典型的类封装过程。因为你的.C文件包含了所有函数实现,且没有被外传。
很多软件都是进行的类似的操作,很有可能同学们下载了一款软件,其中包含了大量的.so文件(在windows下后缀为.dll的文件),然后包含若干的文件头。很明显,这就是封装后的代码。
同时提醒的是,OpenFOAM为一款开源CFD求解器,并不支持闭源行为。
OpenFOAM实例
现结合上面的程序框架,看一下OpenFOAM中的实例。 上面这个图是OpenFOAM中BirdCarreau模型的架构,可见,其被分为一个.H文件一个.C文件。同学们在这里就应该知道.H文件是类声明部分,.C文件是类函数部分。打开.H文件,代码以及相应的注释如下:
#ifndef BirdCarreau_H
#define BirdCarreau_H
#include "transportModel.H"
#include "dimensionedScalar.H"
#include "volFields.H"
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
namespace Foam //名称空间Foam
{
namespace transportModels //名称空间transportModels
{
/*---------------------------------------------------------------------------*\
Class BirdCarreau Declaration
\*---------------------------------------------------------------------------*/
//类声明,类名字为BirdCarreau
class BirdCarreau
:
public transportModel // 类继承,暂且忽略
{
// Private data
// 私有数据
dictionary BirdCarreauCoeffs_;
dimensionedScalar nu0_;// 私有数据
dimensionedScalar nuInf_;// 私有数据
dimensionedScalar k_;// 私有数据
dimensionedScalar n_;// 私有数据
volScalarField nu_;// 私有数据
// Private Member Functions
//- Calculate and return the laminar viscosity
tmp<volScalarField> calcNu() const;// 私有数据
public:
//- Runtime type information
TypeName("BirdCarreau");
// Constructors
//- construct from components
BirdCarreau
(
const volVectorField& U,
const surfaceScalarField& phi,
const word& phaseName = ""
);
// Destructor
~BirdCarreau()
{}
// Member Functions
//- 成员函数
const volScalarField& nu() const
{
return nu_;
}
//- 成员函数
void correct()
{
nu_ = calcNu();
}
//- 成员函数
bool read();
};
} // End namespace transportModels
} // End namespace Foam
#endif
在BirdCaurreau.C文件中,也对成员函数进行了定义:
// * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * * //
//- 部分代码,成员函数的实现细节
Foam::tmp<Foam::volScalarField> Foam::BirdCarreau::calcNu() const
{
return
nuInf_
+ (nu0_ - nuInf_)
*pow(1.0 + sqr(k_*strainRate()), (n_ - 1.0)/2.0);
}
上面一部分是关键的代码,若要实现封装,因为同学们不需要将.C文件拷贝给其他人,因此上面的代码只有同学们知道。
同时,同学们再次看到这种Foam::tmp<Foam::volScalarField> Foam::BirdCarreau::calcNu() const
特别长的代码应该知道Foam::tmp
表示Foam
名称空间下的tmp
类型,Foam::BirdCarreau::calcNu()
表示Foam
名称空间下的BirdCarreau
类型的calcNu()
函数。