首页 > 代码库 > 【转载】C++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值

【转载】C++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值

原文:C++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值

 

 

一、按成员初始化(与构造函数拷贝构造函数有关) 

 

 

 

    用一个类对象初始化另一个类对象,比如:

 


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,并且不提供实际的定义。

 

  
     一般来说,应该将拷贝构造函数和拷贝赋值操作符视为一个个体单元,因为在我们需要其中一个的时候,往往也需要另外一个;而试图禁止一个的时候,也很可能需要禁止另一个。 

 

【转载】C++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值