目录
1、有关纯虚函数
抽象基类的数据成员初始化
如果一个类被声明为抽象基类(其中有 pure virtual function),则抽象基类不能实例化,但它仍需要一个显示的构造函数以初始化其成员变量。如果没有这个初始化操作,其 derived class 的局部对象 _mumble 将无法决定初值。
class ABC
{
public:
virtual void interface() = 0; //考虑尽量不用const
const char* mumble() const { return _mumble; } //不会被派生类改写的不要用virtual
virtual ~ABC() { }; //不要将析构函数声明为pure virtual
protected:
char* _mumble;
ABC(char* mumble_value = 0);
}
//初始化其成员变量
ABC::ABC(char* mumble_value) : _mumble( mumble_value ) { };
这样,可以由 derived class 的构造函数去初始化其 ABC 部分的成员变量。
关于虚析构函数,不要将 virtual destructor 声明为 pure。因为每一个 derived class destructor 会被编译器加以扩张,以静态的方式调用其每一个 virtual base class 以及上一层 base class 的 destructor,只要缺乏任何一个基类析构函数的定义就会导致链接失败。而此时如果此时是纯虚函数,则不可能有析构函数来调用。
2、无继承情况下的函数构造
2.1struct 构造
global 变量存放于全局静态区,局部变量存放于栈,用 new 方法生成的对象存放于堆。
Point global;
Point foobar() {
Point local;
Point *heap = new Point;
*heap = local;
delete heap;
return local;
}
C++中的Plain Ol' Data 声明,类似于结构体,只有成员无相关操作。以C++来编译这段代码的话,编译器会为Point声明:trivial默认构造函数,trivial析构函数,trivial拷贝构造函数,trival拷贝赋值运算符。
1.第一行代码Point global;会在程序起始时调用Point的constructor,然后在系统产生的exit()之前调用destructor。
在C++中,全局对象都是被以“初始化过的数据”来对待,因此置于data segment。
2.第四行代码 Point *heap = new Point;声明一个堆上的对象,其中new运算符会被转化为:Point *heap = __new(sizeof(Point));
此时并没有default constructor施行于*heap object(无自身定义的default constructor,系统提供的构造函数会使初值不确定)。
3.第五行代码 heap = local;由于local没有初始化,因此会产生编译警告”local is used before being initaalized”。
接着delete heap;会被转化为__delete(heap);这样会触发heap的trival destructor。
4.最后函数已传值的方式将local当作返回值传回,这样会触发trival copy constructor,不过由于该对象是个POD类型,所以return操作只是一个简单的bitwise copy。
2.2抽象数据类型
提供private数据实现封装,但是没有virtual function:
class Point {
public:
Point( float x = 0.0, float y = 0.0, float z = 0.0 )
: _x(x), _y(y), _z(z) { }
private:
float _x, _y, _z;
};
对于global实例: Point global;
现在有了默认构造函数作用在其身上,由于global定义在全局范畴,其初始化操作会延迟到程序启动(startup)时才开始。(统一构造一个_main()函数,该函数内调用所有global对象的默认构造函数), 具体见第六章。
考虑Point *heap = new Point;与之前不同在于,Point 有自己的 nontrival 默认构造函数,所以会显式调用这个默认构造函数如下:
Point *heap = __new(sizeof(Point));
if(heap != 0)
heap->Point:Point():
观念上,我们的 Point class 有一个相关的默认拷贝构造、拷贝运算符和析构函数会生成,然而他们都是不重要的(trivial),而且编译器实际上根本没有生成他们。
2.3有虚函数的情况
class Point {
public:
Point(float x = 0.0, float y = 0.0) :
_x(x), _y(y) { }
virtual float z();
private:
float _x, _y;
};
构造时,不仅会为每个对象多引入一个4 bytes 的 vptr 外,虚函数的导入也会引发编译器对类的膨胀作用:
Point::Point(Point *this, float x, float y) :
_x(x), _y(y) {
this->__vptr_Point = __vbtl_Point;//设定虚表
this->_x = x; //扩展初值列表
this->_y = y;
return this;//返回this
}
人为合成一个copy constructor和一个copy assignment operator,其操作是 nontrival,但是 implicit destructor 仍然是trival。如果以一个子类对象初始化父类对象,且是以位运算为基础,那么vptr的设定有可能是非法的。
//拷贝构造函数的内部合成
inline Point*
Point::Point(Point *this, const Point& rhs) {
this->__vptr_Point = __vbtl_Point;
//将rhs坐标的连续位拷贝到this对象
return this;
}
同样在foobar()中, *heap = local 的操作很有可能触发拷贝赋值运算符函数的生成,及其调用操作的一个 inline expansion :以 this 取代 heap 。以 rhs 取代 local 。
3.继承体系下的对象构造
3.1概述
当我们定义一个object如:T object 实际上会发生什么呢?如果T有一个constructor(不管是trival还是nontrival),它都会被调用,构造函数可能含有大量的隐藏代码。根据 class T 的继承体系,编译器会扩充每一个 constructor 。
1.如果有 virtual base class,虚基类的构造函数必须被调用,由浅到深,从左往右:
- 如果class位于成员初始化列表,有任何显示指定的参数都应该传递过去;若没有位于初始化列表,而class含有一个默认构造函数,也应该调用。
- class中的每一个virtual base class subobject的偏移量必须在执行期可存取。
- 如果class是最底层的class,其constructors可能被调用。
2.如果有base class,基类的构造函数必须被调用;
- 如果class位于成员初值列,有任何显示指定的参数都应该传递过去。
- 若没有位于初值列,而class含有一个默认构造(拷贝)函数,也应该调用。
- 如果class是多重继承下的第二或者后继的base class,那么this指针应该有所调整。
3. 如果有虚函数,必须设定vptr指向适当的虚表;
4. 如果一个member没有出现在成员初值列表中,但是该成员有一个默认构造函数,那么这个默认构造函数必须被调用;
5. 成员初值列表中的member初始化操作放在constructor的函数体内,且顺序和声明顺序一致。
即继承情况下的对象构造顺序为:虚基类 -> 基类 -> vptr与虚表 -> 类成员初始化 -> 自定义的代码。
再次以 Point 为例,增加拷贝构造、拷贝赋值运算符、虚析构函数,Line class 在 Point 基础上扩充。
class Point {
public:
Point(float x = 0.0, float y = 0.0);
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
virtual float z() { return 0.0; }
private:
float _x, _y;
};
class Lines {
Point _begin, _end;
public:
Lines(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
Lines(const Point&, const Point&);
draw();
};
来看 Line 构造函数的定义与内部转化:
Lines::Lines(const Point& begin, const Point& end) :
_begin(begin), _end(end){} //定义
//下面是内部转化,将初值列中的成员构造放入函数体,调用这些成员的构造函数
Lines* Lines::Lines(Lines *this, const Point& begin, const Point& end)
{
this->_begin.Point::Point(begin);
this->_end.Point::Point(end);
return this;
};
如果使用了
Lines b = a; 这个时候调用合成的拷贝构造函数,合成的拷贝构造在内部可能如下:
inline Lines&
Lines::Lines(const Lines& rhs) {
if(*this = rsh) return *this;
//证同测试,或者可以采用copy and swap,具体见effective C++
//还要注意深拷贝和浅拷贝
this->_begin.Point::Point(rhs._begin);
this->_end.Point::Point(rhs._end);
return *this;
}
3.2虚拟继承的情况
考虑下面这个虚拟继承(继承自Point)
class Point3d : public virtual Point {
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
: Point(x, y), _z(z) { }
Point3d(const Point3d& rhs)
: Point(rhs), _z(rhs._z) { }
~Point3d();
Point3d& operator=(const Point3d&);
virtual float z() { return _z; }
private:
float _z;
}
虚拟继承时,由于virtual base class的共享性的原因,传统的 constructor 扩张并没有用。例如以下继承关系:
实际Point3d的constructor的扩充如下:
//c++伪码
Point3d*
Point3d::Point3d(Point3d* this, bool __most_derived,
float x = 0.0, float y = 0.0, float z = 0.0) {
if(__most_derived != false) this->Point::Point();
//虚拟继承下两个虚表的设定
this->__vptr_Point3d = __vtbl_Point3d;
this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
this->z = rhs.z;
return this;
}
_most derived 在自己(最底层派生类) 构造时为 true ,在派生路径上的任一父类调用时设为 false 。
在更加深层次的继承情况下,例如Vextex3d,调用Point3d和Vertex的constructor时,总会将该参数设置为false,于是就压制了两个constructors对Point constructor的调用操作。 例如:
//c++伪码
Vextex3d*
Vextex3d::Vextex3d(Vextex3d* this, bool __most_derived,
float x , float y , float z )
{
if(__most_derived != false)
this->Point::Point(); //只由Vertex3d构造一次Point
//设定__most_derived为false
//调用上一层 base classes
this->Point3d::Point3d(false, x, y, z);
this->Vertex::Vertex(false, x, y);
//设定vptrs
return this;
}
从而保证了子类中只有一个虚拟继承的 subobject 。
3.3vptr的初始化
vptr的初始化操作:在base class constructor调用操作之后,但是在程序员供应的代码或”成员初值列中所列出的成员初始化操作”之前。
不能放在任何操作之前:基类中有 virtual function 调用,基类构造完成时,派生类构造才能覆盖重写的 virtual function。
不能在所有初始化过程之后:当一个 subobj constructor 调用一个 virtual function 时,希望调用的是自己类对应的 virtual function 而不是派生类的 virtual function 。
之前的PVertex constructor可能被扩张成:
PVertex*
PVertex::Pvertex(PVertex*this, bool __most_derived,
float x, float y, float z)
{
//有条件调用virtual base class constructor
if(__most_derived != false) this->Point::Point();
//无条件调用上一层base class constructor
this->Vertex3d::Vertex3d(x, y, z);
//设定vptr
this->__vptr_PVertex = __vtbl_PVertex;
this->__vptr_Point3d__PVertex = __vtbl_Point3d__PVertex;
//执行程序员的代码
if(spyOn)
cerr << "Within Pvertex::PVertex()"
<< "size: "
//虚拟机制调用
<< (*this->__vptr_PVertex[3].faddr)(this)
<< endl;
return this;
}
在 class 的 constructor 的 成员初始化列表中调用该 class 的一个 virtual function 安全吗?
实际而言,vptr 保证能够在成员初始化之前由编译器设定好,总是安全的。但在语意上这可能是不安全的,因为 virtual 函数本身可能会依赖未被设定初值的成员变量。
4.对象复制语意学
在不涉及虚拟继承只有一个子对象的情况下,编译器合成的派生类的赋值运算符函数会调用所有即时基类的 operator = 函数:
inline Point&
Point::operator=(const Point& p) {
_x = p._x;
_y = p._y;
return *this;
}
//Point3d虚拟继承自Point
class Point3d : virtual public Point {
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
private:
float _z;
};
//如果没有为Point3d设定一个copy assignment operator,编译器就会为其合成一个
inline Point3d&
Point3d::operator=(point3d* this, const Point3d& p) {
this->Point::operator=(p); //base class operator=
_z = p._z; //memberwise copy
return *this;
}
同样考虑之前的继承体系,类Vertex虚拟继承自Point,并且从Point3d和Vertex派生出Vertex3d。则编译器生成的 copy operator 如下:
inline Vertex&
Vertex operator=(const Vertex& v) {
this->Point::operator=(v);
_next = v._next;
return *this;
}
inline Vertex3d&
Vertex operator=(const Vertex3d& v) {
this->Point::operator=(v);
this->Point3d::operator=(v); //在 Point3d 的复制运算符函数抑制 Point 的复制运算符函数
this->Vertex::operator=(v); //在 Vertex 的复制运算符函数抑制 Point 的复制运算符函数
return *this;
}
编译器如何在Point3d和Vertex的copy assignment operator抑制 Point 的 copy assignment operator 呢?constructor 中的解决办法是添加一个额外的参数 _most_derived。
编译器生成默认 copy assignment operator 的解决办法是:为copy assignment operator提供 分化函数( split functions )以支持这个 class 称为 most-derived class 或者成为中间的base class。( 即分情况特殊处理 )
也可以将虚基类的copy assignment operator 调用放在最后,虽然也会造成重复拷贝,但是语意正确。
建议:尽可能不要允许一个virtual base class的拷贝操作,更进一步,不要在任何virtual base class中声明数据。
5.析构函数语意学
析构函数也是根据编译器的需要才会合成出来,两种情况:
1. class中内含的某个object拥有析构函数;
2. 继承自某个base class,该base class含有析构函数。
例如,在 Point 类中,默认情况下并没有被编译器合成出一个 destructor ,甚至它拥有一个 virtual function 。如果把两个 Point 对象 _begin 和 _end 组合成一个 Line 类,也不会合成出一个 destructor。当我们从 Point 派生出 Point3d(即使是虚拟派生),如果没有声明一个 destructor ,编译器就没有必要合成一个 destructor 。使用 new 与 delete 管理的对象在非必要情况下也不会生成 destructor 。
与构造函数相比,即使拥有虚函数或者虚拟继承,不满足上述两个条件,编译器是不会合成析构函数的。
如果说 Vertex 析构函数牵扯从链表中删除一个节点(或者其他语意),那么这时候定义 destructor 就是必要的。而且其派生的类Vertex3d 如果我们不定义一个 explicit destructor ,编译器也会合成一个,其唯一任务是调用 Vertex destructor。若自己定义了析构函数,则编译器会扩展它使它调用 Vertex destructor。
在继承体系中,由我们定义的destructor的扩展方式和constructor类似,只是顺序相反,顺序如下:
1. destructor的函数体首先执行。
2. 如果class拥有member class object,且该class含有destructor,那么它们会以声明顺序相反的顺序依次被调用。
3. 如果object内含一个vptr,重新被设定指向适当的base class的virtual table(即对象在析构的过程中,依次蜕变为其基类)。
4. 如果有任何上层的nonvirtual base classes拥有destructor,那么它们会以声明顺序相反的顺序依次被调用。
5. 如果有任何virtual base classes拥有destructor,那么它们会以原来构造顺序相反的顺序依次被调用。
主要参考:
作者:幸福的起点_
来源:CSDN
原文:https://blog.csdn.net/qq_25467397/article/details/80451635
版权声明:本文为博主原创文章,转载请附上博文链接!