1 Star 0 Fork 63

张泰威 / openFoamUserManual

forked from poplee / openFoamUserManual 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
56.Understanding_some_C_and_C++.md 15.24 KB
一键复制 编辑 原始数据 按行查看 历史
luofq 提交于 2020-09-14 11:29 . add 56.Understanding_some_C_and_C++.md.

第VIII部分

源代码和编程

56 关于C和C++的一些知识

本章主要讨论C++语言的一些特征。

56.1 定义和声明

在C和C++中,变量的声明和定义具有明显的区别。简而言之,变量的声明仅仅是告知编译器这个变量的存在及其对应的类型,它不会具体描述这个变量的实际含义。 变量的定义同时会告知编译器这个变量的实际含义,但并不意味着这个变量一定分配了具体值。 这方面的具体信息可以看参考文献[57, 37] 或者查阅: http://www.scprogramming.com/declare_vs_define.html.

56.1.1 一个精妙的例子

列表371定义了一个类(class phaseInterface),即告诉编译器这个类是什么(数据成员、方法等)。在这个类phaseInterface的内部,如果想使用一个名叫phaseModel的类,但class phaseModel已经在别处定义,因此不需要重复定义。重新自定义class phaseModel是无效和愚蠢的。 为了使用已经存在的类,我们需要事先告知编译器这个类的存在。列表371第4行起到了这一作用——告诉编译器已经有一个class phaseModel,并且这时编译器也只需要知道这些信息。这种处理也被称为预先声明。 当对类进行编译时,需要确保预先声明类(这里是class phaseModel)的定义已被包含在内,比如链接到包含phaseModel类的库文件中。

列表371:类的声明和定义

namespace Foam
 {

  class phaseModel ;

  class phaseInterface
  {
  // lots of C++ code
  };

  }

56.2 命名空间NameSpace

命名空间是C++的特征之一,主要是为了在程序内部支持逻辑结构。简单来说,命名空间背后的基本理念是:当变量和函数需要被看见时能够确保可见。命名空间不是不可或缺的,但通过它可以维持代码的整洁和干净。然而,这里草率地引用OpenFOAM创始人之一的Jasak教授的一句名言:OpenFOAM只是一种精巧应用C++的技术。因此,在OpenFOAM中需要对命名空间有一定的认识。 关于命名空间NameSpace概念的一般性知识请参阅:

http://www.cplusplus.com/doc/tutorial/namespaces/

http://www.cprogramming.com/tutorial/namespaces.html

http://www.learncpp.com/cpp-tutorial/711-namespaces/ 在57.2章讨论了OpenFOAM对于命名空间的特殊使用。

56.3 const的正确性

const关键词有多种使用方式,同时使用const也有不同的隐含意义。

56.3.1 常量

这是最简单的部分,任何变量都可以通过const关键字声明为常量const keyword。这里有两种声明方式,一是const关键字放在数据类型前面,二是const关键字放在变量名的前面。列表372的两行代码都是正确的声明方式。

列表372: 常量

const int limit = 5;
int const answer = 42;
56.3.2 常量和指针
指向常量

指针可以指向一个常量,但是指针本身不是固定的,它可以变化。当声明一个指向常量的指针时,必须加上const关键字。但是,一个指向常量的指针也可以指向非常量。

列表373: 指向常量

int const constVar1 = 42;
const int constVar2 = 13;
int variable = 11;
const int * pointer = & constVar1 ;
std :: cout << " The pointer points to " << * pointer << std :: endl ;
// change the pointer
pointer = & constVar2 ;
std :: cout << " The pointer points to " << * pointer << std :: endl ;
// point to a non - constant
pointer = & variable ;
std :: cout << " The pointer points to " << * pointer << std :: endl ;

列表374: 列表373的输出

The pointer points to 42
The pointer points to 13
The pointer points to 11
const指针

指针也可以是constant的,无论它所指向的变量类型是什么。这里所说的constant是指针存储的地址不变,也可以说它永远指向同一个变量。但是,该变量本身是可以变化的。

列表375: const指针的使用

int variable = 11;
int * const constPointer1 = & variable ;
std :: cout << " The constant pointer points to " << * constPointer1 << std :: endl ;
variable = 79;
std :: cout << " The constant pointer points to " << * constPointer1 << std :: endl ;

列表376: 列表375的输出

The constant pointer points to 11
The constant pointer points to 79
指向常量的cosnt指针

同时也有指向常变量的常指针。 列表377的最后一行看起来不符合逻辑,但实际上是允许的。为了正确理解这一行,需要遵循从右到左的顺序阅读等号左边的内容。首先,constpointer4是新指针变量的名字。其次,int* const告诉编译器这个新变量是一个指向整型的常指针,这个指针本身或者说指针指向的地址是不能改变的,但是变量variable自身不是常量,它可以随时改变。列表377的最后一行不会改变variable的自然属性,但它限制了该指针只能进行读取操作。所以variable可以改变,但是无法通过constPointer4指针方式赋值改变。

列表377: 指向常量的常指针

int const constVar1 = 42;
int variable = 11;
const int * const constPointer2 = & constVar1 ;
const int * const constPointer4 = & variable ;

56.4 内联函数

动机

只包含少量操作的函数在效率上并不高,原因是调用函数所需的时间可能比运行该函数所有操作的时间还长。尤其是当该函数需要被经常调用时,这个程序会运行地很艰难。但是,函数本身是一种比较干净的编程方式。 首先,借助函数使得程序员能以符合逻辑的方式划分代码。有一些代码是为了实现特殊任务而编写的,将他们包装成在名字上有意义的函数,极大地提升了代码的可读性和可维护性。 另一方面,恰当地编写函数可以避免代码冗余。需要重复进行的任务最适合写入函数中。因此,这些代码只需要编写一次,之后在所有需要的地方调用函数即可。

Inline声明

这一冲突的解决方案是内联函数。inline 声明允许编译器用整个函数代码代替函数的调用,也就是说直接由函数体执行操作。因此,程序员不需要花费太多时间在耗时的函数调用中,同时还能保持代码的干净整洁。 列表378展示了内联函数的定义,该函数体只包含两个逻辑操作,inline声明在函数的返回类型之前。这样来看,内联函数和常规函数在函数编写方面也没有太大差异。

列表378 内联函数的定义

inline bool Foam :: pimpleControl :: finalIter () const
{
return converged_ || ( corr_ == nCorrPIMPLE_ );
}

但是,使用内联声明不能保证编译器一定会用函数体替换函数调用,它还取决于编译器自身以及编译器的配置。

OpenFOAM细节

OpenFOAM的代码风格要求程序员区分内联函数和非内联函数。 在单独的classNameI.H文件中适当的位置使用内联函数。 列表379展示了pimpleControl文件夹的内容,整个程序或模块的代码分成后缀为*.C和*.H的文件。这是区分声明文件和其他文件的通用方式,而后缀为*.dep文件是在编译过程中产生的。这里文件夹中的第四个文件是为了满足Code Style Guide需求的第二个头文件。列表378是pimpleControlI.H文件的一部分。

列表379:pimpleControl文件夹的内容

pimpleControl .C pimpleControl . dep pimpleControl .H pimpleControlI .H

56.5 构造函数和析构函数。

在面向对象编程中一切都是对象,所有对象都是通过构造函数产生的,并且在需要的时候被析构函数删除。

56.5.1 一般语法

构造函数是一种类成员函数,它与一般函数或成员函数相似,但是构造函数有一些固定的准则:  • 构造函数的名字和它所属类的类名相同;  • 构造函数没有返回值; 列表380展示了一个二维空间域内描述点的类,它有两个构造函数。第一个不需要接收输入参数,并将成员变量初始化为零。第二个接收两个整型变量作为输入参数,并将这两个变量作为xPos和yPos这两个成员变量的初始值。 C++支持函数重载,因此编写两个或以上的构造函数是可行的。也就是说,可能存在多个同名函数,但他们拥有不同的输入参数。

列表380:一个关于二维点的类

1 class Point
2 {
3 int xPos ;
4 int yPos ;
5 
6 public :
7 Point ()
8 {
9 /* constructor code */
10 xPos = 0;
11 yPos = 0;
12 }
13 Point (int x, int y)
14 {
15 xPos = x;
16 yPos = y;
17 }
18 };

列表381演示了如何创造Point类的新变量,第一行创建了一个名为p1的Point变量,由于没有输入参数,列表380的第一个构造函数被编译器调用。 第二行同样创建了一个点,括号里的数字将被传到构造函数中,因此列表380的第二个构造函数被调用,并且类的成员变量将被这些输入参数初始化。

列表381:二维点类的使用

1 Point p1;
2 Point p2 (3, 8);
56.5.2 复制构造函数

复制构造函数是用来复制一个对象。当程序没有定义复制构造函数时,C++编译器会自动创建一个默认的复制构造函数,但默认复制构造函数在处理复杂的类时存在一些限制。

列表382:二维点类的复制构造函数

1 Point :: Point ( Point & p)
2 {
3 /* copy constructor code */
4 xPos = p. xPos ;
5 yPos = p. yPos ;
6 }
隐藏复制构造函数

复制构造函数可以被隐藏,结果是复制对象的操作被禁止。为了实现这一目标,复制构造函数必须使用private关键字进行定义。 列表383展示了一个简单的复制构造函数是如何被声明为private的。这表示该复制构造函数只能在类内部被调用,即只能在类Point内部被调用。 列表384展示了一个OpenFOAM源代码的例子,可以看到turbulenceModel类的复制构造函数被声明为private,从而实现了隐藏。

列表383:隐藏复制构造函数

1 class Point
2 {
3 private :
4 Point ( Point & p);
5 };

列表384:隐藏复制构造函数

1 class turbulenceModel
2 :
3 public regIOobject
4 {
5 private :
6 // Private Member Functions
7 
8 // - Disallow default bitwise copy construct
9 turbulenceModel ( const turbulenceModel &);
10
11 /* code continues */
56.5.3初始化列表

C++的类可以拥有任意类型的成员变量。对于复杂的类,需要用到一些初始化操作来确保所有变量都拥有预先定义的类型形式。当一个类的对象被构造函数创造,初始化列表涵盖了所有类成员变量的初始化声明。 列表385展示了一个简单的构造函数初始化列表。61.2.2章的列表488展示了OpenFOAM源代码中的一个初始化列表使用例子。

列表385 :带列表初始化的构造函数

1 class Rectangle
2 {
3 Point topLeft ;
4 Point bottomRight ;
5 
6 public :
7 Rectangle ()
8 {
9 topLeft = Point ();
10 bottomRight = Point ();
11 }
12
13 Rectangle ( Point a, Point b)
14 :
15 topLeft (a),
16 bottomRight (b)
17 {
18 /* constructor code */
19 }
20 }

56.6 面向对象

56.6.1 抽象类

OpenFOAM中关于湍流模型类的植入参见57.10章节。在湍流模型类中大量使用了抽象类和继承。

56.7 模板

OpenFOAM使用了大量的模板。模板是C++的语言特征,它允许泛型编程。说明模板类使用的一个例子是容器类的植入,也就是链表。如果没有模板,容器的多样性要求大量具体类的植入,比如分别存储节点、面和单元的节点链表、面链表、单元链表。 借助多重继承的特性,上述问题得以轻松解决。现在只需要植入一个列表基类,具体类的实现是通过继承列表基类和所需内容类。但是这种方式也有一些缺点,随着复杂程度的增加,多重继承的继承路径注定会成为一个自身的难题,而不是缓解或解决原来的问题。 模板提供了一种告知类的方式:使用类型T来指代所有编译器允许的类型。因此,首先创建了一个模板的容器类。然后,当需要创建一系列具体的节点、面、单元列表时,告诉编译器用具体的类型代替T,之后编译器再对应产生合适的代码。这个过程由编译器进行类型检查,可以确保对有效模板类的具体化顺利进行。 列表386展示了模板的使用。首先植入了一个一般类列表,之后对这个列表具体化为节点、面、单元的类型。其中的typedef指示词允许用一个容易理解的名字来定义类型。一旦这个名字被定义了,我们甚至很难意识到正在使用模板化类。

列表386:模板化的列表

template <class T>
class list
{
// define a list of type T
}
typedef list <node > nodeList ;
typedef list <face > faceList ;
typedef list <cell > cellList ;

从顶层的代码中可以预测到,OpenFOAM遵循了一个类似的策略,volScalarField实际上是一种带有三个模板参数的模板,具体参见列表反映:volScalarField。除了可以用一个更易理解的名字外,这样一个更短的名字也节省了输写时间。使用类型定义typedef语句不仅仅是为了方便。使用完全专门化的GeometricField而不是volScalarField会转化为硬编码。如果OpenFOAM的开发人员在某个时候决定将volScalarField的基类从scalar改变成smartScalar,也不需要修改大量的代码,所需修改的只是一行代码。因此,typedefs的使用大大地提升了代码的可读性和可维护性。

列表387:使用typedef定义volScalarField

typedef GeometricField < scalar , fvPatchField , volMesh > volScalarField ;
56.7.1 OpenFOAM对模板的使用

鉴于本文档不是针对某特定主题的书,因此一些主题的记载显得比较混乱。模板这一主题在很多章节都有讨论,主要是在特定代码例子中介绍模板的使用。由于没有另外的兴趣和益处来重新编排一个更大的文档,这里就只给出了另外一些讨论模板的章节的指引。 在32.1章关于OpenFOAM湍流模型的植入中讨论了模板的使用,湍流模型原先不是采用模板的方式,而是直到OpenFOAM-2.3.0版本的发布才植入该模板类。 在40章关于拉格朗日粒子示踪以及链表的介绍中也讨论了模板的使用。 56.3.2章在字典文件中查找关键字的例子中也讨论了模板。

56.7.2 无需害怕模板

模板代码的语法与非模板代码不一致。鉴于模板代码对于新手来说比较神秘,这里接晒了一些关于模板代码的特征。

模板模板参数:

在本章节中,我们规定模板参数T是某种具体类型的占位符。但是模板参数可能自身也是一种模板类,模板模板参数是指模板化的模板参数。虽然可以避免对模板模板参数的使用,但是借助它可以避免代码复制,以及实现代码的安全。 在拉格朗日示踪库中有一个有具有说服力的嵌套案例,即模板类也被用当作为另一个模板的参数。此外,该例子同时展示了怎样从一个模板类派生出子类。具体请看40章。

1
https://gitee.com/roychang/openFoamUserManual.git
git@gitee.com:roychang/openFoamUserManual.git
roychang
openFoamUserManual
openFoamUserManual
master

搜索帮助