C++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值
Posted on 2011-04-09 10:25 charley_yang 阅读(1751) 评论(0) 编辑 收藏 举报一、按成员初始化(与构造函数和拷贝构造函数有关)
用一个类对象初始化另一个类对象,比如:
Account oldAcct( "Anna Livia Plurabelle" );
Account newAcct( oldAcct );
被称为缺省的按成员初始化(default memberwise initialization),缺省是因为它自动发生,无论我们是否提供显式构造函数,按成员是因为初始化的单元是单个非静态数据成员,而不是对整个类对象的按位拷贝。
例如,Account 类的第一个定义:
class Account {
public:
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
我们可以认为缺省的 Account 拷贝构造函数被定义如下:
inline Account::
Account( const Account &rhs )
{
_name = rhs._name;
_acct_nmbr = rhs._acct_nmbr;
_balance = rhs._balance;
}
用一个类对象初始化该类另一个对象 发生在下列程序情况下:
1 用一个类对象显式地初始化另一个类对象,例如:
Account newAcct( oldAcct );
2 把一个类对象作为实参传递给一个函数,例如:
extern bool cash_on_hand( Account acct );
if ( cash_on_hand( oldAcct ))
// ...
把一个类对象作为一个函数的返回值传递回来,例如:
extern Account
consolidate_accts( const vector< Account >& )
{
Account final_acct;
// do the finances ...
return final_acct;
}
3 非空顺序容器类型的定义,例如:
// 五个 string 拷贝构造函数被调用
vector < string > svec( 5 );
(在本例中,用 string 缺省构造函数创建一个临时对象,然后通过 string 拷贝构造函数,该临时对象被依次拷贝到vector 的五个元素中。)
4 把一个类对象插入到一个容器类型中,例如:
svec.push_back( string( "pooh" ));
对于大多数实际的类定义, 由于考虑到类的安全性以及用法正确性,所以说缺省的按成员初始化是不够的,最经常出现的情况是 一个类的数据成员是一个指向堆内存的指针,并且这块内存将由该类的析构函数删除,就如Account 类中的_name 成员一样 。
在缺省按成员初始化之后,newAcct._name 和 oldAcct._name 指向同一个 C风格字符串,如果 oldAcct 离开了域, 并且 Account 的析构函数被应用在其上,则 newAcct._name 现在指向一个被删除了的内存区;另一种情况是 如果newAcct 修改了由_name 指向的字符串 则 oldAcct也会受到影响,这种指向错误很难跟踪 。
指针”别名 (aliasing) 问题”的一种解决方案是,分配该字符串的第二个拷贝 ,并初始化 newAcct._name 以指向这份新的拷贝,为实现这一点,我们必须改变 Account 类的缺省按成员初始化,我们通过提供一个显式的拷贝构造函数来做到这一点。
类的内部语义也可能使缺省的按成员初始化无效,比如前面所解释的,不能有两个Account 类的对象持有同一个帐号,为了保证这一点,我们必须改变 Account 类的缺省按成员初始化,下面是解决这两个问题的拷贝构造函数:
inline Account::
Account( const Account &rhs )
{
// 处理指针别名问题
_name = new char[ strlen(rhs._name)+1 ];
strcpy( _name, rhs._name );
// 处理帐号惟一性问题
_acct_nmbr = get_unique_acct_nmbr();
// ok: 现在可以按成员拷贝
_balance = rhs._balance;
}
除了提供拷贝构造函数,另一种替代的方案是完全不允许按成员初始化,这可以通过下列两个步骤实现:
1 把拷贝构造函数声明为私有的,这可以防止按成员初始化发生在程序的任何一个地方(除了类的成员函数和友元之外)。
2 通过有意不提供一个定义,但是,我们仍然需要第 1 步中的声明,可以防止在类的成员函数和友元中出现按成员初始化。C++语言不会允许我们阻止类的成员函数和友元访问任何私有类成员,但是通过不提供定义,任何试图调用拷贝构造函数的动作虽然在编译系统中是合法的,但是会产生链接错误, 因为无法为它找到可解析的定义。
例如,为了不允许 Account 类的按成员初始化 我们必须如下声明该类:
class Account {
public:
Account();
Account( const char*, double=0.0 );
// ...
private:
Account( const Account& );
// ...
};
二、成员类对象的初始化
把 C风格字符串的_name 声明,替换成 string 类类型的_name 声明,会发生什么变化?
缺省的按成员初始化依次检查每个成员,如果成员是内置或复合数据类型,则直接执行从成员到成员的初始化。例如,在我们原来的Account 类定义中,因为_name 是一个指针,所以它直接被初始化:
newAcct._name = oldAcct._name;
但是成员类对象的处理则不同,当我们写以下语句时:
Account newAcct( oldAcct );
这两个对象就被识别为 Account 类对象,如果 Account 类提供了一个显式的拷贝构造函数则调用它以完成初始化,否则应用缺省的按成员初始化;类似地,当一个成员类对象被识别出来时,则递归应用相同的过程。
在我们的例子中, string 类提供了显式拷贝构造函数,通过调用该拷贝构造函数,_name被初始化。 现在我们可以认为 缺省Account 拷贝构造函数被定义如下:
inline Account::
Account( const Account &rhs )
{
_acct_nmbr = rhs._acct_nmbr;
_balance = rhs._balance;
// C++伪代码
// 说明调用了一个类成员
// 对象的拷贝构造函数
_name.string::string( rhs._name );
}
Account 类的缺省按成员初始化过程现在可以正确地处理_name 的分配和释放,但是 拷贝帐号仍然不正确 ;因此,我们仍然必须提供一个显式的拷贝构造函数,下面的代码不是十分正确。你能看出为什么吗?
// 不太对
inline Account::
Account( const Account &rhs )
{
_name = rhs._name;
_balance = rhs._balance;
_acct_nmbr = get_unique_acct_nmbr();
}
该实现不完全正确是因为我们没有区分开初始化和赋值,结果,调用的不是string 拷贝构造函数,而是在隐式初始化阶段调用了缺省的 string 构造函数,并且在构造函数体内调用了string 拷贝赋值操作符。修正很简单:
inline Account::
Account( const Account &rhs )
: _name( rhs._name )
{
_balance = rhs._balance;
_acct_nmbr = get_unique_acct_nmbr();
}
再次强调 ,真正的工作是在一开始就意识到我们需要提供一个修正 两个实现的结果都是_name 持有 rhs._name 的值, 只不过 第一个实现要求做两次重复工作,一个一般性的规则是:在成员初始化表中初始化所有的成员类对象 。
三、按成员赋值(与拷贝赋值操作符有关)
缺省的按成员赋值( default memberwise assignment ),所处理的是 用一个类对象向该类的另一个对象的赋值操作,其机制基本上与缺省的按成员初始化相同;但是它利用了一个隐式的拷贝赋值操作符来取代拷贝构造函数,例如:
newAcct = oldAcct;
在缺省情况下,用 oldAcct 的相应成员的值依次向 newAcct 的每个非静态成员赋值,在概念上就好像编译器已经生成下列拷贝赋值操作符:
inline Account&
Account::
operator=( const Account &rhs )
{
_name = rhs._name;
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
}
一般来说,如果缺省的按成员初始化对于一个类不合适,则缺省的按成员赋值也不合。例如,对于原来的 Account 类的定义来说,其中_name 被声明为 char*类型 _name 和_acct_nmbr 的按成员赋值就都不合适了。
通过提供一个显式的拷贝赋值操作符的实例,可以改变缺省的按成员赋值,我们在这操作符实例中实现了正确的类拷贝语义,拷贝赋值操作符的一般形式如下:
// 拷贝赋值操作符的一般形式
className&
className::
operator=( const className &rhs )
{
// 保证不会自我拷贝
if ( this != &rhs )
{
// 类拷贝语义在这里
}
// 返回被赋值的对象
return *this;
}
这里条件测试是:
if ( this != &rhs )
应该防止一个类对象向自己赋值, 因为对于(先释放与该对象当前相关的资源 ,以便分配与被拷贝对象相关的资源)这样的拷贝赋值操作符 拷贝自身尤其不合适。例如 ,考虑Account拷贝赋值操作符:
Account&
Account::
operator=( const Account &rhs )
{
// 避免向自身赋值
if ( this != &rhs )
{
delete [] _name;
_name = new char[strlen(rhs._name)+1];
strcpy( _name,rhs._name );
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
}
return *this;
}
当一个类对象被赋值给该类的另一个对象时,如
newAcct = oldAcct;
下面几个步骤就会发生:
1 检查该类,判断它是否提供了一个显式的拷贝赋值操作符;
2 如果是, 则检查访问权限,判断是否在这个程序部分它可以被调用;
3 如果它不能被调用,则会产生一个编译时刻错误,否则,调用它执行赋值操作;
4 如果该类没有提供显式的拷贝赋值操作符,则执行缺省按成员赋值;
5 在缺省按成员赋值下,每个内置类型或复合类型的数据成员被赋值给相应的成员;
6 对于每个类成员对象,递归执行1到 6 步,直到所有内置或复合类型的数据成员都被赋值。
例如,如果我们再次修改 Account 类的定义,使_name 为一个 string 类型的成员类对象 ,则:
newAcct = oldAcct;
会调用缺省的按成员赋值,就好像编译器为我们生成了下面的拷贝赋值操作符:
inline Account&
Account::
operator=( const Account &rhs )
{
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
// 即使在程序员这个层次上,
// 这个调用也是正确的
// 等同于简短形式: _name = rhs._name
_name.string::operator=( rhs._name );
}
但是 Account 类对象的缺省按成员赋值仍然不合适,同为_acct_nmbr 成员也被按成员拷贝了,我们仍然必须提供一个显式的拷贝赋值操作符, 但是它以成员类 string 对象的方式来处理 name :
Account&
Account::
operator=( const Account &rhs )
{
// 避免类对象向自身赋值
if ( this != &rhs )
{
// 调用 string::operator=(const string& )
_name = rhs._name;
_balance = rhs._balance;
}
return *this;
}
如果希望完全禁止按成员拷贝的行为,那么就需要像禁止按成员初始化一样,将操作符声明为 private,并且不提供实际的定义。
一般来说,应该将拷贝构造函数和拷贝赋值操作符视为一个个体单元,因为在我们需要其中一个的时候,往往也需要另外一个;而试图禁止一个的时候,也很可能需要禁止另一个。