C++【对象模型】| 【07】构造、析构、拷贝做了哪些事?

本文深入探讨了C++对象模型,涉及纯虚函数、无继承对象构造、继承体系下的构造、虚拟继承以及对象赋值和析构的语意学。强调了纯虚函数的使用场景,无继承对象构造时的内存管理和抽象数据类型的设计,以及继承和虚拟继承对构造函数的影响。同时,文章指出对象赋值和析构函数的实现细节,包括析构函数的调用顺序和效率考虑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

索引

C++【对象模型】| 【01】简单了解C++对象模型的布局
C++【对象模型】|【02】构造函数何时才会被编译器自动生成?
C++【对象模型】|【03】拷贝构造是如何工作的,何时才会用到呢?
C++【对象模型】 | 【04】程序在内部被编译器如何转化?
C++【对象模型】 | 【05】类与类之间各种关系下对数据成员的存取、绑定、布局
C++【对象模型】| 【06】类中各种函数的刨析
C++【对象模型】| 【07】构造、析构、拷贝做了哪些事?
C++【对象模型】| 【08】类在执行期会处理哪些事呢?
C++【对象模型】| 【09】类模板、异常处理及执行期类型识别

1、纯虚函数

当一个类如以下设计,有哪些地方需要改进

class Abstract_base {
public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char* mumble() const { return _mumble; }

protected:
    char *_mumble;
};

初始化抽象类中的数据成员

当一个类中有纯虚函数,则该类不能有实例;
若该类中有一个数据成员,则它需要一个显示构造来将其初始化,否则其子类无法决定改初值;
此时可以提供一个protected construct;

唯一例外时定义纯虚析构

由于子类析构被扩张以静态方式调用【每一个虚基类】及【上一层基类】的析构,故缺乏上述任何一个都会导致链接失败;

为什么纯虚析构不能被编译器合成出来呢

由于编译器对一个可执行文件采取分离编译模型的设计;如果需要,则需要在链接时找出纯虚析构不存在,在重写激活编译器,赋予一个特殊指令来合成;
【建议】:
不要将一个虚析构设置为纯虚函数

virtual function的设置规格

当一个函数确定需要被后续子类使用并改写时,才将其声明为一个虚函数;
如果将一个(mumble)函数,且不会被后续子类改写的函数设置成虚函数将是没必要的,由于它将会是一个内联函数,如果被经常调用时,而被设置为虚函数
效率将会大打折扣;

虚拟函数中const

虚函数中不使用const;

综上所述,重写设计该类

class Abstract_base {
public:
    virtual ~Abstract_base();			// 改为虚函数
    virtual void interface() = 0;		// 去除const
    const char* mumble() const { return _mumble; }	// 去除virtual

protected:
    Abstract_base(char* pc=0);	// 提供一个构造
    char *_mumble;	
};

2、无继承下的对象构造

typedef struct {
	float x, y, z;
}Point;

Point global;
Point foobar() {
	Point local;
	Point *heap = new Point;
	*heap = local;
	delete heap;
	return local;
}
在C++中上述Point为Plain Ol' Data形式;编译时,会添加无用的默认构造、析构、拷贝构造、拷贝赋值;
C中支持`临时性的定义`,在程序中可多次发生,最后留下一份实例,于BSS中;
而C++中不支持临时性的定义,会被视为完全定义,C++对全局对象都被以初始化过的数据来对待;
2.1 抽象数据类型
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;
};
当定义一个全局的对象时,其初始化将延时到程序启动;
当给类中成员设定初值时,使用explicit initialization list效率较高;但也有以下缺点:
- 只有当类成员为public时,才可用;
- 只能指定常量;
- 初始化的可能性会高些;
explicit initialization list效率将会比inline constructor好很多,特别对于全局对象;
2.2 为继承做准备
class Point {
public:
	Point(float x = 0.0, float y = 0.0)
	: _x(x), _y(y) {}
	virtual float z();
private:
	float _x, _y;
};
当引入一个虚函数,类中将会多一个vptr,且代码会经由编译器变得膨胀:
- 在构造中加入vptr初始化,于基类构造后,程序代码前;
- 合成拷贝构造和拷贝赋值操作符,如果改类被初始化或以一个子类来赋值,将会对vptr进行重新设定;

3、继承体系下的对象构造

构造函数真正伴随着什么?

构造内可能扩充:
- 成员初始化列表中的数据成员初始化操作将会放入构造中;
- 调用类成员的默认构造;
- 若类有vptr,需要设定初值;
- 调用上一层的基类构造,若基类时多重继承下的第二或后续的基类,则this指针必须要被调整;
- 虚基类构造被调用,从左到右,由深到浅;
- 类中的每一个虚基类子对象的偏移位置要在执行期被存取;
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; }

protected:
	float _x, _y; 
};

class Line {
	Point _begin, _end;
public:
	Line(float=0.0, float=0.0, float=0.0, float=0.0);
	Line(const Point&, const Poitn&);
	/** 编译器转换
	Line* Line::Line(Line *this, const Point &begin, const Point &end) {
		this->_begin.Point::Point(begin);
		this->_end.Point::Point(end);
		return this;
	}
	*/ 
	draw();
};
当使用Line a创建时,会生成默认构造;而析构将会插入调用两个对象的析构;

4、虚拟继承

class Point3d : public virtual Point {};
class Vertex : virtual public Point {};
class Vertex3d : public Point3d, public Vertex {};
class PVertex : public Vertex3d {};

在这里插入图片描述

Vertex的构造必须要调用Point的构造,而Point3d和Vertex同为Vertex3d的子对象,他们不能对Point调用构造,而是由底层Vertex3d调用;如果在往后则交给Vertex3d来构造;
上述继承体系中,构造函数将会扩充更多内容(bool __most_derived参数),来控制虚基类构造是否要被调用;
例如上述的Point3d、Vertex、Vertex3d压制不调用Point的构造;
Vertex3d* Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, 
							float x, float y, float z) {
	if(__most_derived != false) 
		this->Point::Point(x, y);
	this->Point3d::Point3d(false, x, y, z);
	this->Vertex::Vertex(false, x, y);
	return this;
}

void test() {
	Point3d origin;	// 即可调用Point构造
}
虚基类构造被调用中,当一个完整的类对象被定义出来时,才会被调用;
如果在某个完整的类的子对象,它就不会被调用;

构造函数的分裂,从而提高程序速度

针对于完整的对象:
	将会调用虚基类构造,设定所有的vptr;
正对于子对象:
	不调用虚基类构造,不设定vptr;
4.1 vptr初始化语意学
当上述每个类都定义一个虚函数size用来传回class的大小;
而在虚函数中,是经由构造中的对象来调用的,作用于该对象;

当一个对象的基类构造被调用时,如何才能保证该对象有适当的虚函数能被调用

如果一个必须在构造中调用,则使用静态方式;

在执行构造时,限制一组虚函数候选名单,该需要决定类中的虚函数名单的关键

通过虚表,vptr;只要控制vptr的初始化和设定操作即可,而该操作由编译器进行;
那么,vptr在构造中该何时被初始化;
- 显然,是在基类构造调用之后,成员初始化之前;

5、对象赋值

当以一个类对象指定给另一个类对象

可以选择以下三种:
- 执行默认行为;
- 提供显示拷贝赋值操作;
- 显示拒绝赋值,将拷贝赋值操作设为private;

可以参考一下内容对于拷贝赋值操作符和拷贝构造函数
C++【对象模型】|【03】拷贝构造函数

拷贝赋值中没有提供类似成员初始化列表,故不能压抑上一层基类的拷贝操作调用;
在该函数中使用类::operator=(rhs);调用基类;

编译器如何在Point3d和Vertex的拷贝赋值中压抑Point的拷贝赋值

该做法不能同构造一样,由于取拷贝赋值的地址是合法的;
做法如下:
	编译器可能为拷贝赋值产生分化函数,来支持类称为most_derived class或中间的base class;
我们尽可能不要允许一个虚基类的拷贝操作,不要再任何虚基类中什么data member;

6、析构语意学

当类中没有定义析构时,其内含的类成员或基类有析构,编译器会自动生成一个;
一般delet操作符,再释放资源前会析构,但如果对象没有析构也无大碍;
若有多层析构函数被调用,那么析构的顺序和构造的顺序是相反的;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jxiepc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值