《C++新经典对象模型》之第6章 对象构造语义学

《C++新经典对象模型》之第6章 对象构造语义

  • 6.1 继承体系下的对象构造
    • 6.1.1 对象的构造顺序
    • 6.1.2 虚函数
    • 6.1.3 构造函数中对虚函数的调用
        • 06.01.cpp
  • 6.2 对象复制语义学与析构函数语义学
    • 6.2.1 对象的默认复制行为
    • 6.2.2 拷贝赋值运算符与拷贝构造函数
    • 6.2.3 禁止对象的拷贝构造和赋值
    • 6.2.4 析构函数语义
        • 06.02.cpp
  • 6.3 局部对象、全局对象的构造和析构
    • 6.3.1 局部对象的构造和析构
    • 6.3.2 全局对象的构造和析构
        • 06.03.cpp
    • 6.4 局部静态对象、对象数组构造析构和内存分配
      • 6.4.1 局部静态对象的构造和析构
      • 6.4.2 局部静态对象数组的内存分配
        • 06.04.cpp
    • 6.5 new、delete运算符与内存高级话题
        • 06.05.cpp
    • 6.6 临时性对象的详细探讨
      • 6.6.1 拷贝构造函数相关的临时性对象
      • 6.6.2 拷贝赋值运算符相关的临时性对象
      • 6.6.3 直接运算产生的临时性对象
        • 06.06.cpp

6.1 继承体系下的对象构造

6.1.1 对象的构造顺序

从父类到子类,从根源到末端。
先成员变量(定义顺序,从上到下),后构造函数(先初始化列表,后函数体)。

析构顺序:与构造顺序相反。
从子类到父类,先析构函数,后成员变量(从下到上)。

C::成员变量1构造函数	//7
C::成员变量2构造函数	//8
C::C()	//9
	B::成员变量1构造函数	//4
	B::成员变量2构造函数	//5
	B::B()	//6
		A::成员变量1构造函数	//1
		A::成员变量2构造函数	//2
		A::A()	//3
			构造函数初始化列表
			构造函数函数体
		
		A::~A()	//7
		A::成员变量2析构函数	//8
		A::成员变量1析构函数	//9
	B::~B()	//4
	B::成员变量2析构函数	//5
	B::成员变量1析构函数	//6
C::~C()	//1
C::成员变量2析构函数	//2
C::成员变量1析构函数	//3

6.1.2 虚函数

给虚函数表指针赋值的语句,编译器会插入到构造函数的函数体之前。

C::C()
	B::B()	
		A::A()
			vptr = A::vftable;	//虚函数表指针赋值
			A构造函数初始化列表
			A构造函数函数体
		vptr = B::vftable;
		B构造函数初始化列表
		B构造函数函数体
	vptr = C::vftable;
	C构造函数初始化列表
	C构造函数函数体

6.1.3 构造函数中对虚函数的调用

(1)构造函数中调用虚函数,并不通过虚函数表来调用,而是直接调用(虚函数有真实地址)。
(2)在构造函数中调用的虚函数从所在类往根类回溯,逐次找这个虚函数,找到哪个就直接调用(静态方式)。
(3)构造函数中调用虚函数时,对象未构造完整,不宜采用虚函数表机制调用虚函数。
(4)不要在类的构造函数和析构函数中调用虚函数。

#include <iostream>
using namespace std;

namespace _n1
{
	class TA
	{
	public:
		TA()
		{
			cout << "TA::TA(), this = " << this << endl;
		}
		~TA()
		{
			cout << "TA::~TA(), this = " << this << endl;
		}
	};

	class TB
	{
	public:
		TB()
		{
			cout << "TB::TB(), this = " << this << endl;
		}
		~TB()
		{
			cout << "TB::~TB(), this = " << this << endl;
		}
	};

	class T1
	{
	public:
		T1()
		{
			cout << "T1::T1(), this = " << this << endl;
		}
		~T1()
		{
			cout << "T1::~T1(), this = " << this << endl;
		}
	};

	class T2 : public T1
	{
	public:
		T2()
		{
			cout << "T2::T2(), this = " << this << endl;
		}
		~T2()
		{
			cout << "T2::~T2(), this = " << this << endl;
		}
	};

	class T3 : public T2
	{
	public:
		T3()
		{
			cout << "T3::T3(), this = " << this << endl;
		}
		~T3()
		{
			cout << "T3::~T3(), this = " << this << endl;
		}

	private:
		TA ta;
		TB tb;
	};
}

namespace _n2
{
	class A
	{
	public:
		A()
		{
			myvirfunc();
			cout << "A::A(), this = " << this << endl;
		}
		virtual ~A()
		{
			myvirfunc();
			cout << "A::~A(), this = " << this << endl;
		}

	public:
		virtual void myvirfunc()
		{
			myvirfunc2();
			cout << "A::myvirfunc(), this = " << this << endl;
		}
		virtual void myvirfunc2()
		{
			cout << "A::myvirfunc2(), this = " << this << endl;
		}
	};

	class B : public A
	{
	public:
		B()
		{
			myvirfunc();
			cout << "B::B(), this = " << this << endl;
		}
		virtual ~B()
		{
			myvirfunc();
			cout << "B::~B(), this = " << this << endl;
		}

	public:
		virtual void myvirfunc()
		{
			myvirfunc2();
			cout << "B::myvirfunc(), this = " << this << endl;
		}
		virtual void myvirfunc2()
		{
			cout << "B::myvirfunc2(), this = " << this << endl;
		}
	};

	class C : public B
	{
	public:
		C() : m_c(11)
		{
			myvirfunc(); // 调用一个虚函数
			cout << "C::C(), this = " << this << endl;
		}
		virtual ~C()
		{
			myvirfunc();
			cout << "C::~C(), this = " << this << endl;
		}

	public:
		int m_c;

	public:
		virtual void myvirfunc()
		{
			myvirfunc2();
			cout << "C::myvirfunc(), this = " << this << endl;
		}
		virtual void myvirfunc2()
		{
			cout << "C::myvirfunc2(), this = " << this << endl;
		}
	};
}

int main()
{
	if (0)
	{
		_n1::T3 t3;
	}

	if (0)
	{
		_n2::C c;
	}

	if (1)
	{
		_n2::A *mycobj = new _n2::C();
		mycobj->myvirfunc();
		delete mycobj;
	}

	return 0;
}
06.01.cpp
#include <cstdio>
#include <iostream>
using namespace std;

struct A
{
	A() { printf("A::A(), this = %p\n", this); }
	virtual ~A() {}

	virtual void myvirfunc2()
	{
		printf("A::myvirfunc2()\n");
	}
	virtual void myvirfunc()
	{
		myvirfunc2();
		printf("A::myvirfunc()\n");
	}
};
struct B : A
{
	B()
	{
		myvirfunc();
		printf("B::B(), this = %p\n", this);
	}
	virtual ~B() {}

	virtual void myvirfunc2()
	{
		printf("B::myvirfunc2()\n");
	}
	virtual void myvirfunc()
	{
		myvirfunc2();
		printf("B::myvirfunc()\n");
	}
};
struct C : B
{
	int m_c;
	C()
		: m_c(11)
	{
		myvirfunc(); // 调用一个虚函数
		printf("C::C(), this = %p\n", this);
	}
	virtual ~C()
	{
		myvirfunc();
	}

	virtual void myvirfunc2()
	{
		printf("C::myvirfunc2()\n");
	}
	virtual void myvirfunc()
	{
		myvirfunc2();
		printf("C::myvirfunc()\n");
	}
};

int main()
{
	// C c;

	C *mycobj = new C();
	// mycobj->myvirfunc();
	delete mycobj;

	cout << "Over!\n";
	return 0;
}

6.2 对象复制语义学与析构函数语义学

6.2.1 对象的默认复制行为

无拷贝构造函数和拷贝赋值运算符时,默认的对象复制行为会发挥作用。

6.2.2 拷贝赋值运算符与拷贝构造函数

提供拷贝赋值运算符或拷贝构造函数时,需提供默认构造函数(此时编译器不会生成)。


struct JI
{
	JI() { cout << "JI::JI()" << endl; }
	virtual ~JI() { cout << "JI::~JI()" << endl; }
};

struct A : JI
{
	int m_i, m_j;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A &operator=(const A &tmp)
	{
		if (&tmp == this)
			return *this;

		// static_cast<JI&>(*this) = tmp; // 调用父类的拷贝赋值运算符
		// JI::operator=(tmp);// 调用父类的拷贝赋值运算符

		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::operator=(const A&)" << endl;
		return *this;
	}
	A(const A &tmp)
	//: JI(tmp) // 显式调用父类拷贝构造函数
	{
		// 编译器会插入代码,父类按位复制,调用父类的构造函数或者拷贝构造函数?
		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::A(const A&)" << endl;
	}
};

6.2.3 禁止对象的拷贝构造和赋值

(1)类中声明私有的拷贝构造函数和拷贝赋值运算符,无函数体。

class A {
	private:
		A& operator=(const A&);
		A(const A&);
};

(2)c++11,=delete,将拷贝构造函数和拷贝赋值运算符标记为禁用。

class A {
	public:
		A& operator=(const A&) =delete;
		A(const A&) =delete;
};

6.2.4 析构函数语义

  1. 析构函数被合成
    编译器合成析构函数的情况:
    (1)类继承的父类具有析构函数,会合成析构函数并调用父类的析构函数。
    (2)类中类类型成员变量具有析构函数,会合成析构函数并调用该成员变量所属类的析构函数。

  2. 析构函数被扩展
    已写析构函数时,向其中增加代码的情况:
    (1)类继承的父类具有析构函数,会在析构函数后面插入代码,调用父类的析构函数。
    (2)类中类类型成员变量具有析构函数,会在析构函数后面插入代码,调用该成员变量所属类的析构函数。

06.02.cpp
#include <iostream>
using namespace std;

struct JI
{
	JI() { cout << "JI::JI()" << endl; }
	virtual ~JI() { cout << "JI::~JI()" << endl; }
};

struct A : JI
{
	int m_i, m_j;

	// private:
	//	A& operator=(const A& tmp);
	//	A(const A& tmptime);

	// public:
	//	A& operator=(const A& tmp) = delete;
	//	A(const A& tmptime) = delete;

	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A &operator=(const A &tmp)
	{
		if (&tmp == this)
			return *this;

		// static_cast<JI&>(*this) = tmp; // 调用父类的拷贝赋值运算符
		// JI::operator=(tmp);// 调用父类的拷贝赋值运算符

		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::operator=(const A&)" << endl;
		return *this;
	}
	A(const A &tmp)
	//: JI(tmp) // 显式调用父类拷贝构造函数
	{
		// 编译器会插入代码,调用父类的构造函数或者拷贝构造函数?
		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::A(const A&)" << endl;
	}
};

struct ParC
{
	virtual ~ParC() { cout << "ParC::~ParC()" << endl; }
};
struct MemC
{
	ParC m_j;

	~MemC() { cout << "MemC::~MemC()" << endl; }
};

int main()
{
	{
		A aobj;
		aobj.m_i = 15;
		aobj.m_j = 20;

		A aobj2 = aobj; // 执行拷贝构造

		A aobj3;
		aobj3.m_i = 13;
		aobj3.m_j = 16;
		aobj2 = aobj3; // 执行拷贝复制运算符
	}

	{
		MemC mobj;
	}

	cout << "Over!\n";
	return 0;
}

6.3 局部对象、全局对象的构造和析构

6.3.1 局部对象的构造和析构

只要超出了对象的作用域,编译器总会在适当的地方插入调用对象析构函数的代码。
尽量把对象定义在需要立即用到它的代码段的附件。

6.3.2 全局对象的构造和析构

全局对象的初始化和释放过程:
(1)全局对象获得地址(可执行文件中确定的,和堆、栈中分配内存不一样,程序运行期间一直存在)。
(2)全局对象内存清零(静态初始化)。
(3)调用全局对象对应类的构造函数。
(4)执行main函数。
(5)main函数执行完毕后,调用全局对象对应类的析构造函数。
(6)整个可执行程序执行完毕。

06.03.cpp
#include <iostream>
using namespace std;

struct A
{
	int m_i;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }
};

void myfunc()
{
	// A obja; // 这里定义不合适
	if (1 == 1)
	{
		// 这里会被编译器插入调用obja对象析构函数的代码,影响执行效率完全没必要
		return;
	}

	A obja; // 这里定义合适
	obja.m_i = 10;
	cout << "obja.m_i = " << obja.m_i << endl;
	return;
}

A g_aobj;

int main()
{
	{
		A obja;
		int mytest = 1;
		if (mytest == 0)
			return 0;
		myfunc();
	}

	g_aobj.m_i = 6; //

	cout << "Over!\n";
	return 0;
}

6.4 局部静态对象、对象数组构造析构和内存分配

6.4.1 局部静态对象的构造和析构

只有当函数调用时,局部静态对象的构造函数才会执行。
多次调用,只有第一次才构造。
编译时内存开始地址和大小已确定(BSS段),运行到对应代码时,才从事先约定好的地址分配出来。
构造实现方式:局部静态对象地址旁增加字节做标记(是否已构造),第一次构造,第二次标记存在,不构造,跳过static A s_aobj;代码行。
析构实现方式:第一次构造时,增加代码_atexit登记信息,main函数执行完后执行析构。

6.4.2 局部静态对象数组的内存分配

同局部静态对象类似。
但编译器优化,大数组不分配实际的物理地址,对象数组做有用事情时,才实际分配。

06.04.cpp
#include <cstdio>
#include <iostream>
using namespace std;

struct A
{
	int m_i;
	// A() { cout << "A::A()" << endl; }
	//~A() { cout << "A::~A()" << endl; }
};

void myfunc1()
{
	static A s_aobj1;
	static A s_aobj2;
	printf("&s_aobj1=%p\n", &s_aobj1);
	printf("&s_aobj2=%p\n", &s_aobj2);
}
const A &myfunc2()
{
	static A s_aobj1;
	printf("&s_aobj1=%p\n", &s_aobj1);
	return s_aobj1;
}

void myfunc()
{
	static A s_aobj[1000'0000]; // '是数字分隔符, C++14
	for (int i = 0; i < 1000'0000;
上一篇:Elasticsearch:让你的 Elasticsearch 索引与 Python 和 Google Cloud Platform 功能保持同步


下一篇:BM55 没有重复项数字的全排列(回溯)