IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 《深度探索C++对象模型》学习笔记 — 构造、析构、拷贝语义学(The Semantics of Construction,Destruction,and copy) -> 正文阅读

[C++知识库]《深度探索C++对象模型》学习笔记 — 构造、析构、拷贝语义学(The Semantics of Construction,Destruction,and copy)

一、抽象类和纯虚函数

如果我们尝试在抽象类中声明数据成员,那么我们至少应该提供protected权限的初始化数据成员的构造函数;相应的,如果这些成员需要手动释放或解锁,我们需要提供析构函数执行相应操作。一般而言,我们会选择将析构函数声明为纯虚函数,然后提供一份该函数的声明:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	virtual ~CLS_Base() = 0;

protected:
	CLS_Base(size_t size = 5)
	{
		m_pCh = new char(size);
	}

private:
	char* m_pCh;
};

CLS_Base::~CLS_Base()
{
	delete m_pCh;
	cout << "~CLS_DerCLS_Baseived()" << endl;
}

class CLS_Derived : public CLS_Base
{
public:
	virtual ~CLS_Derived()
	{
		cout << "~CLS_Derived()" << endl;
	}
};

int main()
{
	CLS_Derived derived;
}

在这里插入图片描述

二、无继承下的对象构造

1、POD

POD表示Plain Old Data(我看了下C++标准,并没有提及书中写的Plain Ol’ Data)。这种数据类型能够和C语言兼容,所有需要编译器合成的构造、析构、拷贝函数都是trivial的。如下:

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

这类结构的trivial成员我们认为根本没有被定义或调用。这里有一种例外,就是全局变量。对于全局变量,编译器负责对其执行默认初始化。

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

1、构造顺序

(1)所有虚基类的构造函数必须被调用,从左到右,从最深到最浅:
???????? a.如果基类位于初始化列表中,那么任何显式指定的参数都应该传递过去;否则,如果该基类有默认函数,则调用该函数。
???????? b.类中的每个虚基类子对象的偏移量,必须在执行期可被存取。
???????? c.如果class object是最底层(most-derived)的类,其构造函数可能被调用;某些用以支持这一行为的机制必须被放进来。
(2)所有上一层基类的构造函数必须被调用(层层递归调用),以基类的声明顺序为顺序:
???????? a.如果基类位于初始化列表中,那么任何显式指定的参数都应该传递过去。
???????? b.否则,如果该基类有默认函数,则调用该函数。
???????? c.如果基类是多重继承中的第二或后继的基类,this指针必须有所调整。
(3)如果类对象有vptr,它们必须被设定初值指向适当的虚函数表。
(4)初始化列表中的成员对象初始化操作会被放进构造函数体,并且按照成员的声明顺序调用。
(5)如果一个成员没有出现在初始化列表中,但是它有默认构造函数,则该函数必须被调用。

2、虚继承和最底层类

我们知道当类结构中使用虚继承时,这些类是希望共享基类资源的。这就涉及到一个问题,共享的部分该由谁初始化呢?最底层类。以如下的继承体系为例:
在这里插入图片描述
这里最底层类指的就是CLS_Vertex3DDerived。结合代码分析下:

class CLS_Point 
{
public:
	CLS_Point()
	{
	}
};

class CLS_Point2D : virtual public CLS_Point {};
class CLS_Point3D : public CLS_Point2D {};
class CLS_Vertex : virtual public CLS_Point {};

class CLS_Vertex3D : public CLS_Point3D, public CLS_Vertex 
{
public:
	CLS_Vertex3D() :
		CLS_Point3D(),
		CLS_Vertex()
	{
	}
};

class CLS_Vertex3DDerived : public CLS_Vertex3D
{
public:
	CLS_Vertex3DDerived() :
		CLS_Vertex3D()
	{
	}
};

int main()
{
	CLS_Vertex3DDerived obj;
}

结合我们前面查看反汇编的知识,我们可以看到在CLS_Vertex3DDerived执行构造函数之前,执行了这样一段汇编代码:

{
00EE10D0  push        ebp  
00EE10D1  mov         ebp,esp  
00EE10D3  push        ecx  
00EE10D4  mov         dword ptr [this],ecx  
00EE10D7  cmp         dword ptr [ebp+8],0  
00EE10DB  je          CLS_Vertex3DDerived::CLS_Vertex3DDerived+2Bh (0EE10FBh)  
00EE10DD  mov         eax,dword ptr [this]  
00EE10E0  mov         dword ptr [eax],offset CLS_Vertex3D::`vbtable' (0EE2100h)  
00EE10E6  mov         ecx,dword ptr [this]  
00EE10E9  mov         dword ptr [ecx+4],offset CLS_Point2D::`vbtable' (0EE20F8h)  
00EE10F0  mov         ecx,dword ptr [this]  
00EE10F3  add         ecx,8  
00EE10F6  call        CLS_Point::CLS_Point (0EE1000h)  
};

这保证了继承体系中共享的部分将会是最先被构造的。

3、vptr构造

前面我们学习过,如果在构造函数或者析构函数中调用虚函数,该调用将会被静态决议,而非通过vptr进行调用。这样做很合理,因为vptr只是子对象部分的vptr。那么如果我们在虚函数中再调用虚函数呢?被调用的虚函数如何知道此次调用来自构造函数还是外部呢?我们可以考虑模仿虚基类构造函数的调用,在每次调用前在栈中放置一个参数,控制是否需要通过虚拟机制调用,但这样会大大降低函数的效率。在msvc中,考虑下面的代码:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	CLS_Base()
	{
		test();
	}

	virtual void test()
	{
		cout << "CLS_Base::test" << endl;
		testInner();
	}

	virtual void testInner()
	{
		cout << "CLS_Base::testInner" << endl;
	}
};

class CLS_Derived : public CLS_Base
{
};

int main()
{
	CLS_Derived obj;
}

查看反汇编:

		testInner();
00221047  mov         ecx,dword ptr [this]  
0022104A  mov         edx,dword ptr [ecx]  
0022104C  mov         ecx,dword ptr [this]  
0022104F  mov         eax,dword ptr [edx+4]  
00221052  call        eax  

我们可以看出在微软的编译器中是直接通过vptr编译的。这就要求我们在进入构造函数体之前要把vptr放置好。更具体来讲,我们应该在任何基类构造函数调用之后,在成员变量初始化之前设置好vptr

因此,如果在初始化列表中调用虚函数,当其调用于成员变量初始化时,从虚函数表的初始化角度来说,这是个安全的行为。然而,从语义上讲,这未必安全,因为虚函数中可能会使用未初始化的成员变量。

4、代码验证

#include <iostream>
using namespace std;

class CLS_CommonBase1
{
public:
	CLS_CommonBase1(void* ptr, string str = "")
	{
		cout << "CLS_CommonBase1::CLS_CommonBase1 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_CommonBase2
{
public:
	CLS_CommonBase2(void* ptr, string str = "")
	{
		cout << "CLS_CommonBase2::CLS_CommonBase2 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_VirtualBase1
{
public:
	CLS_VirtualBase1(void* ptr, string str = "")
	{
		cout << "CLS_VirtualBase1::CLS_VirtualBase1 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_VirtualBase2
{
public:
	CLS_VirtualBase2(void* ptr, string str = "")
	{
		cout << "CLS_VirtualBase2::CLS_VirtualBase2 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_MemberObject1
{
public:
	CLS_MemberObject1(string _str)
	{
		cout << "CLS_MemberObject1::CLS_MemberObject1 _str = " << _str << endl;
	}

	virtual void test() {}
};

class CLS_MemberObject2
{
public:
	CLS_MemberObject2(string _str = "")
	{
		cout << "CLS_MemberObject2::CLS_MemberObject2 _str = " << _str << endl;
	}

	virtual void test() {}
};

class CLS_Derived : public CLS_CommonBase1, virtual public CLS_VirtualBase1, virtual public CLS_VirtualBase2, public CLS_CommonBase2
{
public:
	CLS_Derived() :
		CLS_CommonBase1(this),
		CLS_VirtualBase1(this),
		CLS_VirtualBase2(this),
		CLS_CommonBase2(this, typeid(*this).name()),
		obj1(typeid(*this).name())
	{

	}

private:
	CLS_MemberObject1 obj1;
	CLS_MemberObject2 obj2;
};

int main()
{
	CLS_Derived obj;
}

在这里插入图片描述
从这个输出中,我们可以与上面的构造过程中的顺序相对应。这里,我们在调用过程中只给CLS_CommonBase2传递了typeid.name() 的参数。这是因为在前面的基类构造过程中,该name还不可用。

四、对象复制语义学

1、拷贝构造与虚继承

与构造函数类似,拷贝构造函数采用了相同的方式以防止共享成分的多次拷贝:

#include <iostream>
using namespace std;

class CLS_VirtualBase
{
public:
	CLS_VirtualBase() {};

	CLS_VirtualBase(const CLS_VirtualBase& other)
	{
		cout << "CLS_VirtualBase(const CLS_VirtualBase&)" << endl;
	}
};

class CLS_Derived1 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived1() {};

	CLS_Derived1(const CLS_Derived1& other) :
		CLS_VirtualBase(other)
	{
		cout << "CLS_Derived1(const CLS_Derived1&)" << endl;
	}
};

class CLS_Derived2 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived2() {};

	CLS_Derived2(const CLS_Derived2& other):
		CLS_VirtualBase(other)
	{
		cout << "CLS_Derived2(const CLS_Derived2&)" << endl;
	}
};

class CLS_DerivedMost : public CLS_Derived1, public CLS_Derived2
{
public:
	CLS_DerivedMost() {};

	CLS_DerivedMost(const CLS_DerivedMost& other) :
		CLS_Derived1(other),
		CLS_Derived2(other)
	{
		cout << "CLS_DerivedMost(const CLS_DerivedMost&)" << endl;
	}
};

int main()
{
	CLS_DerivedMost obj;
	CLS_DerivedMost objCopy(obj);
}

在这里插入图片描述
反汇编:

008E1180  push        ebp  
008E1181  mov         ebp,esp  
008E1183  push        ecx  
008E1184  mov         dword ptr [this],ecx  
008E1187  cmp         dword ptr [ebp+0Ch],0  
008E118B  je          CLS_DerivedMost::CLS_DerivedMost+2Bh (08E11ABh)  
008E118D  mov         eax,dword ptr [this]  
008E1190  mov         dword ptr [eax],offset CLS_DerivedMost::`vbtable' (08E31E8h)  
008E1196  mov         ecx,dword ptr [this]  
008E1199  mov         dword ptr [ecx+4],offset CLS_Derived1::`vbtable' (08E31E0h)  
008E11A0  mov         ecx,dword ptr [this]  
008E11A3  add         ecx,8  
008E11A6  call        CLS_VirtualBase::CLS_VirtualBase (08E1000h)

2、拷贝赋值与虚继承

然而上述的策略并不适用于拷贝赋值。一方面,拷贝赋值运算符并不支持初始化列表,那么我们就没法通过参数控制是否只初始化非共享部分。另一方面,我们可以通过函数指针调用拷贝赋值函数。

#include <iostream>
using namespace std;

class CLS_VirtualBase
{
public:
	CLS_VirtualBase() {};

	CLS_VirtualBase& operator=(const CLS_VirtualBase& other)
	{
		cout << "CLS_VirtualBase& operator=(const CLS_VirtualBase& other)" << endl;
		return *this;
	}
};

class CLS_Derived1 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived1() {};

	CLS_Derived1& operator=(const CLS_Derived1& other)
	{
		this->CLS_VirtualBase::operator=(other);
		cout << "CLS_Derived1& operator=(const CLS_Derived1& other)" << endl;
		return *this;
	}
};

class CLS_Derived2 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived2() {};

	CLS_Derived2& operator=(const CLS_Derived2& other)
	{
		this->CLS_VirtualBase::operator=(other);
		cout << "CLS_Derived2& operator=(const CLS_Derived2& other)" << endl;
		return *this;
	}
};

class CLS_DerivedMost : public CLS_Derived1, public CLS_Derived2
{
public:
	CLS_DerivedMost() {};

	CLS_DerivedMost& operator=(const CLS_DerivedMost& other)
	{
		this->CLS_Derived1::operator=(other);
		this->CLS_Derived2::operator=(other);
		cout << "CLS_DerivedMost& operator=(const CLS_DerivedMost& other)" << endl;
		return *this;
	}
};

int main()
{
	CLS_DerivedMost obj;
	CLS_DerivedMost objCopy;
	objCopy = obj;

	auto pf = &CLS_DerivedMost::operator=;
	(objCopy.*pf)(obj);
}

在这里插入图片描述
从结果我们可以看出,共享部分的拷贝赋值函数确实被调用了两次。事实上,从编译器的角度,这个问题基本上是无法解决的。从语义的角度上讲,我们可以先调用非共享基类的拷贝赋值函数,然后调用共享基类的拷贝赋值函数。但这样并不能解决多次拷贝的问题。作者提到,最好的解决办法就是不要在虚基类中声明数据(那还要虚继承干嘛呢?)

五、析构语义学

1、析构顺序

(1)执行析构函数体;
(2)执行成员函数的析构函数(与声明顺序相反);
(3)如果一个object内含一个vptr,那么首先重设相关的虚函数表;
(4)执行非虚基类的析构函数(与声明顺序相反);
(5)执行虚基类的析构函数(与声明顺序相反)。

2、代码验证

#include <iostream>
using namespace std;

class CLS_CommonBase1
{
public:
	virtual ~CLS_CommonBase1()
	{
		cout << "~CLS_CommonBase1 typeid(*this).name() = "<< typeid(*this).name() << endl;
	}
};

class CLS_CommonBase2
{
public:
	virtual ~CLS_CommonBase2()
	{
		cout << "~CLS_CommonBase2 typeid(*this).name() = " << typeid(*this).name() << endl;
	}
};

class CLS_VirtualBase1
{
public:
	virtual ~CLS_VirtualBase1()
	{
		cout << "~CLS_VirtualBase1 typeid(*this).name() = " << typeid(*this).name() << endl;
	}
};

class CLS_VirtualBase2
{
public:
	virtual ~CLS_VirtualBase2()
	{
		cout << "~CLS_VirtualBase2 typeid(*this).name() = " << typeid(*this).name() << endl;
	}
};

class CLS_MemberObject1
{
public:
	~CLS_MemberObject1()
	{
		cout << "~CLS_MemberObject1" << endl;
	}
};

class CLS_MemberObject2
{
public:
	 ~CLS_MemberObject2()
	{
		cout << "~CLS_MemberObject2()"<< endl;
	}
};

class CLS_Derived : public CLS_CommonBase1, virtual public CLS_VirtualBase1, virtual public CLS_VirtualBase2, public CLS_CommonBase2
{
public:
	virtual ~CLS_Derived()
	{
		cout << "~CLS_Derived() typeid(*this).name() = " << typeid(*this).name() << endl;
	}

private:
	CLS_MemberObject1 obj1;
	CLS_MemberObject2 obj2;
};

int main()
{
	CLS_Derived obj;
}

在这里插入图片描述
构造时,vptr的调整在成员对象的构造之前可以解释为,成员对象的构造支持传参,而参数可能由虚函数返回。因此要保证虚函数调用的正确性。然而,对于析构函数,我个人认为vptr的调整和成员对象的析构不需要严格的顺序要求。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-15 15:59:28  更:2021-07-15 16:01:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/5 8:06:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码