由一道面试题引发的“血案”(静态变量,虚函数,构造/析构函数调用顺序等)

由一道面试题引发的“血案”(静态变量,虚函数,构造/析构函数调用顺序等)

      前几天去深圳某大型的医疗器械企业面试C++开发,整个面试过程花了大概有三个小时。面试当然还是老规矩了:HR介绍->做笔试题->技术官面试->HR再聊->回去等通知,。笔试题就只有两道题目,一个就是以下这道题,一个就是单链表插入算法(要给出最优算法)。其实我觉得两道题都不是很难,但可能是自己的基础不够踏实吧,忽略了很多细节上的东西。

      技术面试官分别有两位,(在这里我想说一句,别想着忽悠就能pass了,在这些大公司要求严格的技术官眼皮底下,没有真材实料是逃不掉的。)前一位主要是问笔试上的题目,他让我解释每个答案的由来,技术的要点等等;后一位面试官主要是问我一些系统架构,做过的项目和工作的职业生涯规划等。面试结束后,他们送我下楼,我也知道自己被KO了。如果真要我总结一下这次面试的话,可能就以下几点吧:

1、    细节问题没把握,没有深入去了解C++,以为会用就好了;

2、   项目开展时,只是单纯从完成任务这个角度出发,而不是从设计的最优性、用户体验和效率出发;

3、   没有医疗器械软件系统研发和嵌入式的经验;

4、   自信还是很重要的。

   好吧,废话少说,我只想好好地把这道面试题详解一遍,以警示自己:细节为王。

#include "stdafx.h"
#include <iostream>
using namespace std;

static int x = 1;                                         
class A
{
public:
	A()
	{
		x = 4;
		cout<<"A constructor!"<<endl;
	}
	virtual ~A()                           //基类的析构函数为虚函数
	{
		x = 7;
		cout<<"A destructor!"<<endl;
	}
	virtual void foo()                      //虚函数,支持动态绑定。
	{
		x = 9;
	}
private:
	float a;
};

class B:public A
{
public:
	B()
	{
		x = 5;
		cout<<"B constructor!"<<endl;
	}
	virtual ~B()
	{
		x = 8;
		cout<<"B destructor!"<<endl;
	}
	virtual void foo()                             
	{
		x = 10;
	}
	void Bar()
	{
		foo();
	}
};

class C:public B
{
public:
	C()
	{
		x = 6;
		cout<<"C constructor!"<<endl;
	}
	virtual ~C()
	{
		x = 9;
		cout<<"C destructor!"<<endl;
	}
	virtual void foo()
	{
		x = 11;
	}
	void Bar()                      //与基类相同,但基类该函数前无virtual关键字,则基类函数被隐藏
	{
		foo();
	}
};

class D
{
public:
	D()
	{
		x = 12;
		cout<<"D constructor!"<<endl; 
	}
	~D()                                     //没有设置为虚函数
	{
		x = 13;
		cout<<"D destructor!"<<endl;
	}
private:
	float a;
};

class E:public D
{
public:
	E()
	{
		x = 14;
		cout<<"E constructor!"<<endl;
	}
	~E()
	{
		x = 15;
		cout<<"E destructor!"<<endl;
	}
};

static D dd;                                 //全局静态变量dd,类型为D。程序结束时,程序会自动调用它的析构函数

int _tmain(int argc, _TCHAR* argv[])
{
cout<<"x="<<x<<endl;
	/*
	output:
	x=12
	*/

	/*
	Why:
	为什么不等于1呢?因为我们定义了“static D dd;”,它是静态全局变量,存储在静态数据区,它必须在main()函数前已经被构造初始化(先调用类D的构造函数)。这种执行是由C++ 编译器startup代码实现的,而startup代码是更早于程序进入点(main 或WinMain)执行起来的代码。
  main函数还未执行,所以不能打印。
	*/

	A *p1 = NULL;
	cout<<"A *p1 = NULL; x="<<x<<endl;
	/*
	output:
	x=12
	*/

	/*
	定义指针*p,它指向A对象,它没有使用new/delete关键字分配内存资源,所以它不能自动初始构造,不调用A的构造函数。
	*/

	B *p2 = NULL;
	cout<<"B *p2 = NULL;x="<<x<<endl;
	/*
	output:
	x=12
	*/

	A aObj;
	cout<<"A aObj;x="<<x<<endl;
	/*
	output:
	x=4
	*/

	/*
	why:
	当用类创建一个实例对象时,C++会为它分配足够的能存放对象所有成员的存储空间,所以它会调用该类的构造函数。这种定义对象方法,内存分配是分配到栈中的,由C++缺省创建构造和撤销析构,与new创建不同。
	记住,对象是具体实例,占用内存空间。
	*/

	A aObjArray[2];
	cout<<"A aObjArray;x="<<x<<endl;
	/*
	outout:
	A constructor!
	A constructor!
	x=4

	/*
	why:
	与上题原理一样,只不过是定义含有两个元素的对象数组,每个对象元素会占用内存,它会分别调用每个对象元素的构造函数,即两次。
  记住,N个元素的对象数组,调用N次构造函数。
	*/
	
	C cObj;
	cout<<"C cObj;x="<<x<<endl;
	/*
	output:
	A constructor!
	B constructor!
	C constructor!
	x=6
	*/

	/*
	why:
	对象是由“底层向上”开始构造的,当建立一个对象时,首先调用最原始基类的构造函数,然后调用下一个派生类的构造函数,依次类推,直至到达自身为止。在对象析构时,其顺序正好与之相反。
	记住,调用直接基类构造函数时,如果无专门说明,就调用直接基类的默认构造函数。我们必须明确的是当一个类继承与基类,并且自身还包含有其他类的成员对象的时候,构造函数的调用顺序为:调用基类的构造函数->调用成员对象的构造函数->调用自身的构造函数。
	构造函数的调用次序完全不受构造函数初始化列表的表达式中的次序影响,与基类的声明次数和成员对象在函数中的声明次序有关。
	*/

	A *pA1 = (C*)&cObj;
	cout<<"A *pA1 = (C*)&cObj;x="<<x<<endl;
	/*
	output:
	x=6
	*/

	/*
	why:
	与以上同理,这里把派生类C对象cObj地址赋给基类A的指针pA1,它涉及到了派生类的向上转换,不用调用构造函数。
	*/

	pA1->foo();
	cout<<"pA1->foo();x="<<x<<endl;
	/*
	output:
	x=11
	*/

	/*
	why:
	我们知道,虚函数它支持动态绑定,基类的指针可以在程序运行时可以根据需要指向其派生类,调用不同的派生类成员函数。由前可知,基类指针动态绑定为派生类指针,所以当基类指针调用foo()函数时,会调用子类C的foo()函数。这里其实可以通过对象内存模型中的虚函数表中更深入去了解,只不过虚函数可是个大专题,在此只能讲个大概了。
	但有一点,基类与派生类中必须要有完全相同的虚函数才行。记住,虚函数是面向对象多态特性的体现。
	*/

	C *pC1 = new C();
	cout<<"C *pC1 = new C();x="<<x<<endl;
	/*
	output:
	A constructor!
	B constructor!
	C constructor!
	x=6
	*/

	/*
	why:
	使用new关键字分配内存资源时,它会自动初始构造,而且同上一样(C cObj;)按照继承顺序分别调用基类的构造函数。
	这是创建对象的另外一种方法,它是在堆上分配内存来创建对象的(与上A aObj;不同);不同的是,C++用new创建对象时返回的是一个对象指针,pC1指向C的一个对象,C++分配给pC1的仅仅是存放指针值的空间。而且,用new 动态创建的对象必须用delete来撤销该对象,只有delete对象才会调用其析构函数,C++不会缺省撤销析构。
	注意:new创建的对象不是用“*”或“.”来访问该对象的成员函数的,而是用运算符“->”;
	*/

	pC1->Bar();
	cout<<"pC1->Bar();x="<<x<<endl;
	/*
	output:
	x=11
	*/

	/*
	why:
	该函数与基类B相同,但基类B中该函数前无virtual关键字,则基类函数被隐藏,所以它只能调用自身的foo()函数。而且pC1在编译期间已经被静态绑定为C类型的,所以会调用C的Bar函数。
	*/

	B *pbObj = new B();
	cout<<"B *pbObj = new B();x="<<x<<endl;
	pbObj->Bar();
	cout<<"pbObj->Bar();x="<<x<<endl;
	/*
	output:
	A constructor!
	B constructor!
	x = 5
	pbObj->Bar();x=10
	*/

	delete pC1;
	cout<<"delete pC1;x="<<x<<endl;
	/*
	output:
	C destructor!
	B destructor!
	A destructor!
	x = 7
	*/

	/*
	why;
	在对象析构时,其顺序正好与构造函数相反。
	*/

	pA1 = NULL;
	cout<<"pA1 = NULL;x="<<x<<endl;
	/*
	output:
	x = 7
	*/

	/*
	why:
	与A *p1 = NULL;同理
	*/

	delete pbObj;
	cout<<"delete pbObj;x="<<x<<endl;
	/*
	output:
	B destructor!
	A destructor!
	x = 7
	*/

	D *pdObj = new E();  
	/*
	output:
	D constructor!
	E constructor!
	x = 14
	*/

  /*
	why:
	与以上同理,因为是要new 类E的对象,所以先调用基类D的构造函数,然后子类E的构造函数。
	*/


	delete pdObj;
	cout<<"delete pdObj;x="<<x<<endl;
	pdObj = NULL;
	/*
	output:
	D destructor!
	x = 13
	*/

	/*
	why:
	在类的继承中,如果有基类指针指向派生类,那么delete基类指针时,如果析构函数不定义成虚函数,则派生类中派生的那部分无法析构,从而可能导致内存泄露。我们可以看到类D中的析构函数不是虚函数,所以当我们delete基类指针(指向子类E)时,它只调用基类D的析构函数。
  记住,当基类指针指向子类时,我们最好把基类的析构函数定义为虚函数。
	*/

	/*
	output:
	C destructor!
	B destructor!
	A destructor!
	A destructor!
	A destructor!
	A destructor!
	D destructor!
	x = 13
	*/

	/*
	why:
	C destructor!
	B destructor!
	A destructor!
	前三个析构函数是对应“C cObj;”语句的,因为对象是栈上分配内存的,所以程序结束时自动缺省调用析构函数,而析构顺序与构造顺序相反。
	
	A destructor!
	是对应“A aObj;”语句的,程序结束时自动缺省调用析构函数。

	A destructor!
	A destructor!
	是对应“A aObjArray[2];”语句的,有两个对象元素,则要调用析构函数两次。

	D destructor!
	是对应“static D dd;”语句的。因为它的类型是全局静态变量dd,程序结束时会最后才自动调用它的析构函数。
	*/

	 return 0;
}

Output:
x=12
A *p1 = NULL; x=12
B *p2 = NULL;x=12
A constructor!
A aObj;x=4
A constructor!
A constructor!
A aObjArray;x=4
A constructor!
B constructor!
C constructor!
C cObj;x=6
A *pA1 = (C*)&cObj;x=6
pA1->foo();x=11
A constructor!
B constructor!
C constructor!
C *pC1 = new C();x=6
pC1->Bar();x=11
A constructor!
B constructor!
B *pbObj = new B();x=5
pbObj->Bar();x=10
C destructor!
B destructor!
A destructor!
delete pC1;x=7
pA1 = NULL;x=7
B destructor!
A destructor!
delete pbObj;x=7
D constructor!
E constructor!
D destructor!
delete pdObj;x=13
C destructor!
B destructor!
A destructor!
A destructor!
A destructor!
A destructor!
D destructor!
请按任意键继续. . .


        好了,到这里就几乎把这道题讲解完了,自己也能轻松地呼一口气了。这道题看似简单,其实包含的知识点还真不少呢。只不过在这里我并没有根据每个知识点(虚函数,静态变量,初始化顺序等)去写概要总结,毕竟书本里有大把这样的理论知识。我喜欢在例子中穿插知识点,这样记忆起来更加发散,更加清晰易懂。

        记住:细节为王!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值