深度探索c++对象模型之类对象的赋值

来源:互联网 发布:python 知乎 编辑:程序博客网 时间:2024/05/12 20:07

      在C++中,当我们声明一个类时,如果没有给这个类操作符“=”定义一个函数,那么一般情况下编译器会自动为这个类合成一个默认的copy assignment operator【拷贝赋值操作符】,而这种copy assignment operator的工作模式是bitwise copy,所谓的bitwise copy,意思就是按位施以拷贝【两个类对象除了在内存中的位置不一样,其它的一模一样】。但这种模式是存在一定危险性的,比如,考虑一个虚继承A和B,B继承自A,我们先声明了一个B对象b,然后声明一个A对象a,接着再声明一个A指针对象p;首先,我们让p指向&b,然后再把*p赋值给a,很显然这就是一种错误【在bitwise copy情况下】,因为*p中的所有内容都原封不动照搬给A对象a了,那么也包括*p中的虚表指针,但实际上这个虚表指针是指向B类的,而a中的虚表指针正确情况下应该指向A类!所以编译器是会选择性的给类默认合成bitwise copy式copy assignment operator的,在以下四种情况下,编译器是不会合成出一个bitwise copy模式的copy assignment operator的:

1):一个类中带有成员类对象,而这个内嵌类含有自己的【用户定义】copy assignment operator。

2):一个类的基类有copy assignment operator。

3):当一个类中声明了任何virtual function【虚函数】时。

4):当一个类继承自virtual base class时【无论这个基类有没有copy assignment operator】。

      所以,如果我们定义这样一个Point类:

class Point{public:Point(int x=0, int y=0):_x(x),_y(y){};protected:int _x,_y;}

然后我们再写【Point a,b;......b=a;......】时,其中的【b=a;】就是按位照搬【bitwise copy】,这期间并没有调用什么copy assignment operator,而且这种模式效率还很高。

      现在,让我们来给Point导入一个copy assignment operator:

inline Point&Point::operator=(const Pont &p){_x=p.x;_y=p.y;return this;}
接着,让我们再定义一个Point3d类,该类虚拟继承自Point:

class Point3d:virtual public Point{public:Point3d(int _x=0,int _y=0,int _z=0);protected:int _z;} 

在这里,如果我们没有为Point3d定义一个拷贝赋值操作符,编译器就会为Point3d合成一个类似这样的拷贝赋值操作符【伪代码】:

inline Point3d&Point3d::operator=(Point3d* const this, const Point3d &p){//调用基类的函数实体this->Point::operator=(p);//memberwise copy the derived class members_z = p->_z;return *this;}

      与constructor不同的是,constructor可以有一个member initialization list,而copy assignment operator却不能有一个member assignment list,因此以下代码是非法的:

//非法代码inline Point3d&Point3d::operator=(const Point3d &p3d):Point(p3d),_z(p3d._z){}

要改成如下形式,才能通过编译:1)【Point::operator=(p3d);】或者【( *(Point *) this ) = p3d;】

      好了,现在让我们再创建两个类,Vertex和Vertex3d。其中,Vertex跟Point3d一样,也是虚拟继承自Point;而Vertex3d,则派生自Point3d和Vertex,为了简洁,本文不再赘述这两个类的代码,而是直接看它们的copy assignment operator函数定义:

//Vertex的拷贝赋值操作符inline Vertex&Vertex::operator=(const Vertex& v){this->Point::operator(v);_next = v._next;//这里的_next是Vertex新增的一个指针成员return *this;}

//Vertex3d的拷贝赋值操作符inline Vertex3d&Vertex3d::operator=(const Vertex3d &v3d){this->Point::operator=(v3d);this->Point3d::operator=(v3d);this->Vertex::operator=(v3d);...//其它一些Vertex3d自己成员的拷贝赋值【如果有的话】return *this;}

仔细看上面那两段代码,您会发现问题吗?嗯,那就是在给Vertex3d类的对象们拷贝赋值时,Point的copy assignment operator会被调用三次!那么有什么办法可以压抑限制一下基类的copy assignment operator吗,比如像constructor中传递一个额外的参数?书中给出的答案是不行!但是为什么不行,我当时是左看右看都不明白!我想了想,还是先上传书中的原话吧:


因为自己看不懂的缘故,无奈之下,我去知乎求助,有幸遇到一位好心大牛的回复,总算明白了一点。关键在于那句红横线的话“取copy assignment operator函数地址是合法的”,在彻底理解这句话的背后含义之前,让我们先来回顾一下constructor的额外参数选构法:就是在隐式参数this后面,通过多传递一个叫做“_most_derived”的额外参数,利用它来判定要不要调用基类构造函数。同样的类似方法,为什么不能适用于copy assignment operator呢?让我们看一下上图中作者给出的代码,其中的第二句代码,可以被转换成如下形式:

Point3d& (Point3d::*pmf)(const Point3d&) = Point3d::operator=;
如果编译器仿照constructor,给copy assignment operator里面加一个额外的参数,那么在给函数指针pmf声明时参数列表里面就应该加上一个bool型【因为额外参数的类型很可能就是bool型】,所以上图书中的那些代码根本就编译不通过。总之就是如果有额外参数的话,我们在声明一个copy assignment函数指针的时候,参数表里面不应该只有一个“const Point3d&”。C++先驱们很显然不希望用户们为这个参数表而头疼,所以就索性没有仿照constructor的做法。。。

      那么是不是可以通过产生一些分化函数【split function】来解决这个问题呢?比如在编译时将copy assignment operator分化为两个,让它们的参数列表不同【有无额外参数】,一个用来对付函数指针问题,一个用来对付高效率拷贝问题。知乎大牛给出的回答是:符合语意!但是编译出来的代码体积将大大增加,也许会得不偿失。

      事实上,很多编译器根本就不打算在这上面尝试过多的努力,所以像我们上面的例子,那个Point类的copy assignment operator会被调用多次。大家并不在乎这些效率上的浪费,因为C++标准都说了:我们并没有规定那些代表virtual base class的subobject是否该被“隐喻定义(implicitly define)的copy assignment operator”指派内容一次以上。(C++ Standard,Section 12.8)

      本文的最后,首先套用作者的话给大家一个建议:不要在任何virtual base class中声明数据成员!其次,再套用知乎大牛的题外话作为结束:

对象构造和对象复制,有很多类似的地方,但也需要注意它们的区别。《深度探索 C++ 对象模型》很多章节,都是先讲述对象构造怎么怎么样,再讲述对象复制时怎么样。平时写代码也是,对象构造、复制、释放,都需要特别注意。另外这本书有些术语似乎跟大陆的其术语不太一致,看的时候注意。比如书中常说的对象复制、对象拷贝,经常是指赋值。而我们大陆这边,说对象复制,通常联想到复制构造函数。英文就没有歧义,一个是 copy assignment operator, 一个是 copy constructor。C++ 不断为旧特性打补丁,virtual 继承这特性已入歧途。Point 的数据会重复是因为使用了多重继承,假如一开始就只有单继承,并添加接口,就没有之后的一系列问题。这些看似复杂的问题,是自己弄出来的。《深度探索 C++ 对象模型》这本书中,一旦涉及到 virtual 继承,编译器实现时就会有很多小花招,那些章节看起来也比较难懂。实际工程中不应该使用多继承,而只使用“单继承 + 接口”,接口概念在 C++ 中实现为没有数据的纯虚类。这样关于 virtual 一系列的章节,实际工程中没有那么重要。


2 0