【转载】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++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值的更多相关文章
- 转载:百为STM32开发板教程之十二——NAND FLASH
http://bbs.21ic.com/icview-586200-1-1.html 百为STM32开发板教程之十二——NAND FLASH 参考资料:百为stm32开发板光盘V3\百为stm32开发 ...
- C++ Primer 与“类”有关的注意事项总结
C++ 与"类"有关的注意事项总结(一) 1. 除了静态 static 数据成员外,数据成员不能在类体中被显式地初始化. 例如 : class First { int memi = ...
- c++类 用冒号初始化对象(成员初始化列表)
c++类 用冒号初始化对象(成员初始化列表) 成员初始化的顺序不同于它们在构造函数初始化列表中的顺序,而与它们在类定义中的顺序相同 #include<iostream> ; using n ...
- Android(java)学习笔记118:类继承的注意事项
/* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. C:不要为了部分功能而去 ...
- 【转载】UML类图几种关系的总结
因为有的时候很久不弄UML图,老是忘记几个常见的连接线的意思,这篇完全说转载:UML类图几种关系的总结 在UML类图中,常见的有以下几种关系: 泛化(Generalization), 实现(Real ...
- Android(java)学习笔记59:类继承的 注意事项
1. 类继承的注意事项: /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. ...
- C++中与类有关的注意事项(更新中~~~)
关于构造函数的调用次序,见下列代码 #include<iostream> using namespace std; class A { private: int x; public: A( ...
- (转载)C++ const成员初始化问题
(转载)http://www.189works.com/article-45135-1.html Const成员如其它任何成员一样,简单考虑其出现在三个位置:全局作用域.普通函数内部.类里面. 下面请 ...
- Cocos2d-x 3.1.1 学习日志3--C++ 初始化类的常量数据成员、静态数据成员、常量静态数据成员
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/u011292087/article/details/37598919 有关const成员.stati ...
随机推荐
- php中防盗链使用.htaccess
下面开始讲解:比如你的图片都在img目录下,那就在该目录下放一个名为 .htaccess 的文件,内容如下: php代码: 以下为引用的内容:RewriteEngine onRewriteCond % ...
- P1514 引水入城
概述 首先,这是一道好题,这道题既考查了图论的dfs知识,又考察了区间贪心问题中很典型的区间覆盖问题,着实是一道好题. 大概思路说明 我们观察到,只有第一行可以放水库,而第一行在哪里放水库的结果就是直 ...
- [置顶] TortoiseGit和msysGit安装及使用笔记(windows下使用上传数据到GitHub)
eclipse .MyEclipse 配置安装 git:http://wenku.baidu.com/link?url=gMT4a7K6EJWAztuwun73oPHiKqlydEdn5F3S2Win ...
- 并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue用法
在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列(先进先出).Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQ ...
- 关于IOS的证书、App ID、设备、Provisioning Profile详述
首先,打开developer.apple.com ,在iOS Dev Center打开Certificates, Indentifiers & Profiles认识一下基本结构.列表就包含了开 ...
- 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 ...
- (喷血分享)利用.NET生成数据库表的创建脚本,类似SqlServer编写表的CREATE语句
(喷血分享)利用.NET生成数据库表的创建脚本,类似SqlServer编写表的CREATE语句 在我们RDIFramework.NET代码生成器中,有这样一个应用,就是通过数据库表自动生成表的CREA ...
- Nginx安装注意事项
因为nginx需要依赖pcre库.zlib库.openssl库,所以需要下载这三个库以及nginx源码. 下载以上文件到/usr/local/src/目录下 使用tar -zxvf ...
- csuoj 1114: 平方根大搜索
http://acm.csu.edu.cn/OnlineJudge/problem.php?id=1114 1114: 平方根大搜索 Time Limit: 5 Sec Memory Limit: ...
- 初试FitNesse
1.下载fitnesse-standalone.jar 2.在cmd中输入,开启fitnesse server 3.在浏览器中输入: 4.编写代码: package fitnesse.slim.tes ...