类(二)

本节继续讨论封装。

在上一节中,虽然类将数据私有化,但类的公有函数代码同学们依然可以访问(看见..),并没有实现封装的作用。比如,你只想让别人用你的代码写文章,但是不想让合作方知道你如何写的代码。在这里,针对这个问题,继续介绍类的使用,其中主要涉及类的撰写习惯以及一个新的东西:库。

之前我们只是介绍了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进行了包含。对其编译后,会形成一个具有同样功能的库。在目录下,其看起来就是这个样子: Alt text 上面这种目录下一个.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中目前只包含了类的实现。

这段代码,目前就实现了类的真正的封装。在感受类的封装之前,总结一下这段程序的框架:

  • 将代码分为程序和库两部分;
  • 库:
    • 库的.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中的实例。 Alt text 上面这个图是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()函数。

results matching ""

    No results matching ""