在C语言中如何存储并初始化成员变量
扫描二维码
随时随地手机看文章
成员变量必须在构造函数的初始化列表中完成初始化。Smart pointer members minimize dependencies while allowing exception safety。
通过以指针存储成员变量的方法最小化依赖
当成员变量的头文件非常大或者非常复杂;或者当你有大量的数据成员,并且不想减慢编译速度和强化相互依赖时。你会怎么做?简单来说就是将成员变量保存为指针形式,并用在类的构造函数中使用new为其分配空间。(在某种特殊的情况下,可用引用形式的成员变量代替)。同样要确保在析构函数中删除它们。下面是一段雏形代码。
// User.h
class PointerMember;
class RefParam;
class User
{
public:
User( const RefParam &inParam );
virtual ~User();
private:
PointerMember *mPointerMember;
};
// User.cpp
#include "User.h"
User::User( const RefParam &inParam )
: mPointerMember( new PointerMember( inParam ) )
{
return;
}
User::~User()
{
delete mPointerMember;
return;
}
这样当你要使用成员变量时,原来使用mValMember.Something的地方就要用mPointerMember->Something了。文本编辑器或者集成开发环境的查询替换方法可以很容易地在切换存储方法。
初始化列表
注意,在构造函数初始化列表中初始化对象的指针成员(可以是任何类型成员)是非常重要的。对于C++的初学者来说,像上面的例子中所看到的,下面语句位于大括号之前看起来感觉非常别扭。
: mPointerMember( new PointerMember( inParam ) )
在类对象的生命周期中,如果实际应用时不需要经常使用指针成员变量时,可以选择将该指针成员初始化为nil(注意:删除一个nil指针永远是安全的。因为delete方法的实现在将指针变量传递给堆管理器前,首先检验指针的值)。如果指针变量需要在构造之前分配存储空间的话,一定要在初始化列表中完成,而不像下面代码一样在构造函数体中完成。
User::User( const RefParam &inParam )
{
mPointerMember = new PointerMember( inParam ); // DON'T DO THIS
return;
}
我所工作的大型C++项目中,那些很少使用初始化列表初始化成员变量的,都到处充斥着错误。其中有一个项目,源码共70多兆,我在那家公司工作的时候除了调试错误没做其他任何事情。搞定了一摞错误,又会出现一筐错误。适当的初始化成员变量失败不只是代码的问题,还与更高层次问题相关。
一般来说,构造函数体应该只用来开展对成员变量的操作,或者是全部完成初始化后对整个对象的操作。基本原则是保留函数体给不适合由初始化列表完成的代码。
开始学习适当的使用初始化列表以来,在写信构造函数或者重写老的构造函数后,函数体往往是空的,或者仅包含不多的几行代码,因为全部的实际工作都在初始化列表中完成了。要完成这些工作有时候需要一些额外的工作,但是最后还是能把这些工作量找回来的。
注意了,初始化列表是引用型和常量型成员变量初始化的唯一地方,如果在初始化列表中初始常量成员变量失败了,它可能已在默认构造函数中初始化了,你在其他任何地方都不能改变它,构造函数体也不例外。如果以这种方式初始化引用型成员变量失败,代码就不能通过编译。下面的代码在g++中将发生致命错误。
class HasRefMember
{
public:
HasRefMember( int &inIntToAlias );
private:
int &mSomebodyElsesInt;
};
HasRefMember::HasRefMember( int &inIntToAlias ) // No
initialization list!
{ // refinit.cpp: In method `HasRefMember::HasRefMember(int &)':
// refinit.cpp:11: uninitialized reference member `HasRefMember::mSomebodyElsesInt'
mSomebodyElsesInt = inIntToAlias;// The compiler doesn't even get this far
}
关于初始化列表的其他一些注意事项:在初始化列表中,成员变量的初始化顺序要与类型生命中的顺序一致。实际情况下,C++编译器总是按照变量声明的顺序初始化成员变量。初始化列表顺序与声明相匹配将避免混淆。进一步说,如果你理解了成员变量按照一定的顺序初始化的话,你可以安排顺序使得构造函数的后面的变量使用前面的变量作为参数。如下例所示。
class Example
{
public:
Example( double inVal );
private:
double mSqrt;
double m2Sqrt;
};
Example::Example( double inVal )
: mSqrt( sqrt( inVal ) ),
m2Sqrt( mSqrt * 2 )
{
return;
}
如果我们改变了了成员变量的声明顺序,但是保留初始化列表原样不变的话,m2Sqrt将初始化为内存残留的垃圾值(一个未定义的值),mSqrt将初始化为inVal的平方根。
因此,如果要改变类声明中成员变量的顺序的话,确保同时检验并更新初始化列表。周期性地检验程序中的构造函数,以确保成员变量的正确顺序。一些编译器会非常友好的提示发生的顺序错误。
对于初始化列表中的一些语法方面的局限性,需要另外的一些工作才能行得通。
列表中的每一项都是一次对成员变量构造函数的调用。某些类型看起来没有调用构造函数,但你可以用一个值或者相同类型的对象的引用(你调用了拷贝构造函数)来初始化它。如果你没有重写自己的拷贝构造函数的话,编译器将会提供一个默认的拷贝构造函数(尽管默认构造函数不能提供正确的功能)。仅仅分配了内置数据类型的构造。
构造函数的参数可以仅仅是一串由逗号分割的0值或者表达式,不可以是声明语句、基本块或者对无返回值类型的函数的调用。可以是返回对应数据类型的一个函数(例如传递给拷贝构造函数一个值)。有一点很重要,你不能使用loop循环或者if语句。如果需要的话,就只能放在调用初始化的子过程中了。
不要在初始化列表中调用非静态的成员函数。对象还没有完全创建之前,如果你或者将来某个程序维护者引用一个还没有初始化的成员时,将会导致未定义的行为之类的结果。如果需要写一个子过程来计算得到一个参数给一个构造成员的话,声明该字过程为静态并显示地给他传递它可能用到的参数,但只能传递那些在调用它时已经构造好的参数,而且不要传递this指针。
注意:可能需要调用基类的成员变量,因为基类已经充分的构造好了,还可以调用在其他类中定义的函数,只要不传递this指针作为参数,同样是因为对象还没有完全构造好。
我谨慎的建议,在希望接受的函数是基类的成员函数指针时,是可以传递this指针给初始化列表的。这是因为在子类的构造函数调用时,基类对象已经完全构造好了。这种情况只适用于所用的指针必须为基类指针的情况,相反的情况是不允许的,这正体现了封装——作为一个集成类对象,在它起作用的位置,不能改变基类部分的特征。
有时你可能想要在初始化中调用一个子过程。可能你不想写一个只在一个地方用到的完整的子过程。也可能你不想在类的头文件中声明子过程原型,从而引起所有依赖该头文件的源码的漫长的重编译过程。也可能构造函数需要频繁调用,而你避开子过程调用的开销(这种情况考虑使用内联函数)。还有可能是子过程的返回值是一个很大的对象,构造、拷贝并销毁原对象将导致很大一笔运行时开销。
这里顺带介绍一下条件表达式操作符:“?:”。它时对值进行判断的if语句的缩写,因此,他可以作为一个表达式用于初始化列表。很多人因为感觉他晦涩而不喜欢使用它,我感觉表达式复杂时它能够胜任。但它只是用于初始化列表参数的情况,要不然你就要些一个包含if语句的子过程了。
#include
class InitExample
{
public:
InitExample( std::string const &inFileName, bool inWritable );
private:
int mFileDescriptor;
};
#include
#include
#include
#include
InitExample::InitExample( std::string const &inFileName, bool inWritable )
: mFileDescriptor( open( inFileName.c_str(),
inWritable ? O_RDWR : O_RDONLY ) )
{
if ( mFileDescriptor < 0 )
throw std::exception();
return;
}
到最后,有时候初始化列表变得很长很难阅读。从这一点上来看,就要考虑是否你的类包含了太多的成员。也许寻找一组位于不同类中的自然协作的成员更合适,最后通过组合来实现最初的类。
来源:2008前进0次