从读研究生开始接触编程,都是根据项目需求,完成任务为主,学习很被动,虽然编了不少东西,但是鲜有对本质的东西有透彻的理解,缺乏指导是一个原因,缺乏总结才是更主要的原因,所以产生了写这篇文章的想法,适时的提炼总结,是提升自我的重要途径。

简单说说现在的需求:为了测试数值算法,需将有限元前处理器HyperMesh生成的网格文件按Eureka格式进行转换,网格文件格式已知,目标文件格式已知,那么要做的事情就是把网格文件解析,然后按目标格式存储即可实现目标,这是一般的做法。

如果以后换了一种格式,那么可能又要重新写过转换程序了,并没有通用性。能想到的解决方法,自然是设计一个类,这个类和输出什么格式没有任何关系,这个类只是单纯的将网格文件的所有有效数据(例如:nodes、elements、sets等等)保存在类中,用户只需要直接引用或继承这个类,就可以访问到所有的网格数据,进而根据自己的需求输出想要的格式。

这样的思路,其实是数据解析、数据存储(类来保存)、数据重组(用户自定义)的过程,由于输出不是这个类考虑的,而是由用户来决定,因此有了通用性。

在设计这个类的时候遇到了几个问题,下面说说:

一. 将成员变量声明为private

最开始我是用public的,简单嘛,这也是我一直以来的做法,[黎老师][2] 看了之后提出了修改要求,作为一个完善、安全、高效的类,必须把成员变量声明为私有类型,即private,如果你没有用过,你就一直没有概念,我就是这样,于是我找了《C++ Primer》和《Effective C++》相关的内容补充了一下概念,最重要的是封装性。

封装性

成员变量声明为private后,只能通过成员函数进行访问,因此如果以后对成员变量有改动,例如以某个计算替换这个成员变量或者是其他的一些改动,使用这个类的用户并不知道,也不会受到影响,顶多重新编译一下。

将成员变量声明为private后就对使用这个类的用户隐藏了成员变量,即封装,这样便确保了class的约束条件总是会获得维护,因为只有成员函数可以影响它们,并且保留了日后变更实现的权利。如果不隐藏它们,我们很快会发现,即使拥有class原始代码,改变任何public事物的能力还是极端受到束缚,因为那会破坏太多的用户代码。public意味着不封装,不封装意味着不可改变,特别是对被广泛使用的classes而言。

假设我们有一个public成员变量,而我们修改甚至最终取消了它,那么多少代码会被破坏呢?取决于该class被使用的范围,如果就你自己用,可能修改代码的工作量是可以接受的,如果有更多一些人用,那么被破坏的代码就是一个未知量,往往是很大的。

一旦你将一个成员变量声明为public而用户开始使用它,就很难改变那个成员变量涉及的一切,太多的代码需要重写、重新测试、重新编写文档和重新编译,可见在一个类的设计之初,考虑周全是多么的重要。

除了public类型之外,protected类型同样也存在封装问题(此处不展开),因此从封装的角度,就只有两种访问权限:private提供封装其他不提供封装

上述封装讨论源自参考文献[1]并结合我的理解。

二. 将访问private成员变量的成员函数定义为“返回const引用类型”

前面讲到了将成员变量声明为private,通过提供成员函数来访问这些private变量。那么现在就有一个问题,这些成员函数怎么设计才合适呢?基于本文的目的是想将这个class设计成适用于各种有限元计算,这个类只要将Hypermesh网格文件作为输入,这个类便完全提取并保存了数据,这些数据可以直接被用户在各自的有限元计算代码中调用,也可以按用户的指定的格式输出。对于第一种直接应用的情况,由于网格数量巨大,计算过程中有大数量级的数据操作,因此我们需要从成员函数访问private变量的效率方面进行考虑。

首先说说引用类型

“引用类型”(reference type)是C++的一种变量类型,它的作用是为变量起一个别名。
假如有一个变量a,想给它起一个别名,可以这样写:

1
2
int a;
int& b = a;

这就表明了b是a的“引用”,即a的别名。经过这样的声明,使用a或b的作用相同,都代表同一变量。在上述引用中,&是“引用声明符”,并不代表地址,可以这样理解:在这里int&是一种变量类型,它和int、double、string一样,都是一种变量的类型。不要理解为“把a的值赋给b的地址”。

其次说说函数返回值

在这里主要讨论返回值的类型为:引用类型和非引用类型

函数返回值用于初始化在调用函数时创建的临时对象(temporary object),如果返回类型不是引用,在调用函数的地方会将函数返回值拷贝给临时对象,这样的话,每调用一次该函数就会得到一个新的拷贝值。因此如果我们设计的成员函数,获得的是private变量的一个拷贝,那么在大数级量的操作中就会消耗不可估量的内存,因此对于这种问题,需要我们的成员函数获得的是private变量的一个引用,由于计算过程中的调用,并没有拷贝变量,而是直接引用原对象,因此能节约内存同时不需要拷贝也提高了效率。

例如:

1
2
3
4
5
vector<hypermeshElement_tria3>& AskElementData(vector<hypermeshElement_tria3>& tria3);

上面复杂的函数其实是以下这个形式:

type& FunctionName(type&);

type& FunctionName()即代表,函数返回的是引用类型,这样,成员函数返回的则是引用。

然后说说返回const引用类型

试想,类中保存着诸多的数据,单元节点、节点坐标等,由于我们提供的成员函数是对原private变量的直接引用,用户对该引用的任何修改都是直接对原变量的修改,这样会导致数据的不安全,即便一般情况下用户不会主动去修改原始数据,但是不排除误操作的情况,从数据安全角度,应该从类的设计上杜绝这种修改的可能性,而不是靠用户的自律性来维护。在这里突然很想说个个人观点,一个社会不应该靠社会成员的自律性来维护正常秩序,而应该是通过完善的体制来维护。只是突然想到的。回归正题,那么const关键字则提供了这样的功能,const类型的引用,既避免了复制的低效高耗操作又能防止用户直接对引用变量进行修改的可能,多么好,是吧。

例如:

1
2
3
4
5
6
const vector<hypermeshElement_tria3>& Hypermesh::AskElementData(
vector<hypermeshElement_tria3>& tria3)
{
tria3 = _element_tria3; // _element_tria3为类的private变量
return tria3;
}

接下来谈谈对type& functionName(type&)中形参type&的困惑

由于我之前的编程经验都是野路子,并没有太多系统的知识,在明白了const type& functionName()之后,还有一个困惑,那就是为什么函数的形参中需要一个引用类型的参数(type&)呢?

这还得从返回的变量说起,如果我们的成员函数定义的是返回非引用类型,那么它是可以返回局部变量的,因为返回非引用类型,返回的是原变量的一个拷贝值,所以,当成员函数执行完毕,在它函数体内定义的局部变量虽然被销毁,但是返回的是该局部变量的一个拷贝值,这个拷贝值独立于原变量,所以这样用是没有问题的。

例如:

1
2
3
4
5
6
7
8
vector<hypermeshElement_tria3> Hypermesh::AskElementData()
{
vector<hypermeshElement_tria3> tria3; // 局部变量
tria3 = _element_tria3; // _element_tria3为类的private变量
return tria3;
// 返回的是tria3的一个拷贝,所以当AskElementData()执行完毕,
// tria3被销毁,AskElementData()返回的拷贝值是依然存在可用的。
}

但是,如果我们定义的成员函数是返回引用类型,当成员函数执行完毕时,将释放分配给局部变量的存储空间,此时对局部变量的引用就会指向不确定的内存,返回指向局部变量的指针也是一样的,当函数结束时,局部变量被释放,返回的指针就变成了不再存在的变量的悬垂指针。

例如下面这种做法是错误的:返回局部变量的引用

1
2
3
4
5
6
7
vector<hypermeshElement_tria3>& Hypermesh::AskElementData(
vector<hypermeshElement_tria3>& tria3)
{
vector<hypermeshElement_tria3> element_tria3; // 局部变量
tria3 = element_tria3; // 对局部变量的引用
return tria3; // 返回的是对局部变量的引用
}

那么就明确了,如果你的成员函数要返回的是对变量的引用,那么它不可以是局部变量。

而,返回非局部变量的引用时,要求在函数的形参中,包含有以引用方式或指针方式存在的,需要被返回的参数。

例如我们现在所讨论的类中的private成员变量,下面的做法也是错误的,因为它的形参中没有提供以引用方式或指针方式存在的,需要被返回的参数。

1
2
3
4
5
vector<hypermeshElement_tria3>& Hypermesh::AskElementData()
{
vector<hypermeshElement_tria3>& tria3 = _element_tria3; // 对private变量的引用
return tria3; // 引用类型变量tria3是局部变量
}

正确的用法应该是

1
2
3
4
5
6
vector<hypermeshElement_tria3>& Hypermesh::AskElementData(
vector<hypermeshElement_tria3>& tria3) // 提供一个引用方式存在的需要返回的参数
{
tria3 = _element_tria3;
return tria3;
}

最后总结一下

根据前面的需求分析,本文设计的类需要:

  • 类的成员变量为私有类型private
  • 用来访问private成员变量的是返回const引用类型的成员函数

所以最后采用的是下面这个形式:

1
2
3
4
5
6
const vector<hypermeshElement_tria3>& Hypermesh::AskElementData(
vector<hypermeshElement_tria3>& tria3)
{
tria3 = _element_tria3;
return tria3;
}


1
2
3
4
5
const type& FunctionName(type& xx)
{
xx = privateObject;
return xx;
}

关于这方面的总结大概就是这些了,日后若有进一步的理解再进行补充。
以上内容皆为个人理解,有错之处欢迎斧正和讨论。

2016年1月21日03:23:06
于克利夫兰

参考文献

[1] 《Effective C++》第三版,条款22。
[2]: http://engineering.case.edu/emae/Faculty/Bo_Li