原文: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++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值的更多相关文章

  1. 转载:百为STM32开发板教程之十二——NAND FLASH

    http://bbs.21ic.com/icview-586200-1-1.html 百为STM32开发板教程之十二——NAND FLASH 参考资料:百为stm32开发板光盘V3\百为stm32开发 ...

  2. C++ Primer 与“类”有关的注意事项总结

    C++ 与"类"有关的注意事项总结(一) 1. 除了静态 static 数据成员外,数据成员不能在类体中被显式地初始化. 例如 : class First { int memi = ...

  3. c++类 用冒号初始化对象(成员初始化列表)

    c++类 用冒号初始化对象(成员初始化列表) 成员初始化的顺序不同于它们在构造函数初始化列表中的顺序,而与它们在类定义中的顺序相同 #include<iostream> ; using n ...

  4. Android(java)学习笔记118:类继承的注意事项

    /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. C:不要为了部分功能而去 ...

  5. 【转载】UML类图几种关系的总结

    因为有的时候很久不弄UML图,老是忘记几个常见的连接线的意思,这篇完全说转载:UML类图几种关系的总结 在UML类图中,常见的有以下几种关系: 泛化(Generalization),  实现(Real ...

  6. Android(java)学习笔记59:类继承的 注意事项

    1. 类继承的注意事项: /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. ...

  7. C++中与类有关的注意事项(更新中~~~)

    关于构造函数的调用次序,见下列代码 #include<iostream> using namespace std; class A { private: int x; public: A( ...

  8. (转载)C++ const成员初始化问题

    (转载)http://www.189works.com/article-45135-1.html Const成员如其它任何成员一样,简单考虑其出现在三个位置:全局作用域.普通函数内部.类里面. 下面请 ...

  9. Cocos2d-x 3.1.1 学习日志3--C++ 初始化类的常量数据成员、静态数据成员、常量静态数据成员

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/u011292087/article/details/37598919 有关const成员.stati ...

随机推荐

  1. php中防盗链使用.htaccess

    下面开始讲解:比如你的图片都在img目录下,那就在该目录下放一个名为 .htaccess 的文件,内容如下: php代码: 以下为引用的内容:RewriteEngine onRewriteCond % ...

  2. P1514 引水入城

    概述 首先,这是一道好题,这道题既考查了图论的dfs知识,又考察了区间贪心问题中很典型的区间覆盖问题,着实是一道好题. 大概思路说明 我们观察到,只有第一行可以放水库,而第一行在哪里放水库的结果就是直 ...

  3. [置顶] TortoiseGit和msysGit安装及使用笔记(windows下使用上传数据到GitHub)

    eclipse .MyEclipse 配置安装 git:http://wenku.baidu.com/link?url=gMT4a7K6EJWAztuwun73oPHiKqlydEdn5F3S2Win ...

  4. 并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue用法

    在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列(先进先出).Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQ ...

  5. 关于IOS的证书、App ID、设备、Provisioning Profile详述

    首先,打开developer.apple.com ,在iOS Dev Center打开Certificates, Indentifiers & Profiles认识一下基本结构.列表就包含了开 ...

  6. Saving changes is not permitted in SQL Server

    From Save (Not Permitted) Dialog Box on MSDN : The Save (Not Permitted) dialog box warns you that sa ...

  7. (喷血分享)利用.NET生成数据库表的创建脚本,类似SqlServer编写表的CREATE语句

    (喷血分享)利用.NET生成数据库表的创建脚本,类似SqlServer编写表的CREATE语句 在我们RDIFramework.NET代码生成器中,有这样一个应用,就是通过数据库表自动生成表的CREA ...

  8. Nginx安装注意事项

    因为nginx需要依赖pcre库.zlib库.openssl库,所以需要下载这三个库以及nginx源码.       下载以上文件到/usr/local/src/目录下     使用tar -zxvf ...

  9. csuoj 1114: 平方根大搜索

    http://acm.csu.edu.cn/OnlineJudge/problem.php?id=1114 1114: 平方根大搜索 Time Limit: 5 Sec  Memory Limit:  ...

  10. 初试FitNesse

    1.下载fitnesse-standalone.jar 2.在cmd中输入,开启fitnesse server 3.在浏览器中输入: 4.编写代码: package fitnesse.slim.tes ...