构造函数和析构函数常见面试题?
构造函数和析构函数常见面试题?
1、永远不要在构造函数或析构函数中调用虚函数
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
Function();
}
virtual void Function()
{
cout << "Base::Fuction" << endl;
}
};
class A : public Base
{
public:
A()
{
Function();
}
virtual void Function()
{
cout << "A::Fuction" << endl;
}
};
int main()
{
A a;
// Base * b=&a;
a.Function();
return 0;
}
首先回答标题的问题,调用当然是没有问题的,但是获得的是你想要的结果吗?或者说你想要什么样的结果?
有人说会输出:
A::Fuction
A::Fuction
如果是这样,首先我们回顾下C++对象模型里面的构造顺序,在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造(虚继承、多重继承这里不讨论),如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样,实际的输出结果是:
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。
所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
《Effective C++ 》条款9:永远不要在构造函数中调用虚函数
在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。
对上面这种看上去有点违背直觉的行为可以用一个理由来解释-因为基类构造器是在派生类之前执行
的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。
如果在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为和彻夜的代码调试。沿类层次往下调用尚未初始化的对象的某些部分本来就是危险的,所以C++干脆不让你这样做。
事实上还有比这更具基本的要求。在派生类对象的基类对象构造过程中,该类的类型是基类类型。不仅虚函数依赖于基类,而且使用运行时刻信息的语言的相应部分(例如, dynamic _cast(参见Item 27)和typeid)也把该对象当基类类型对待。
在C++编程中处处都这样处理,这样做很有意义:在基类对象的初始化中,派生类对象相关部分并未被初始化,所以其时把这些部分当作根本不存在是最安全的。 在一个派生类对象的构造器开始执行之前,它不会成为一个派生类对象的。
《Effective C++ 》条款9:永远不要在析构函数中调用虚函数
effective C++ 中有这样的描述:同样的原因也适用于析构过程。一旦派生类析构函数运行,这个对象的派生类数据成员就被视为未定义的值
,所以 C++ 就将它们视为不再存在。
一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
}
virtual void Function()
{
cout << "Base::Fuction" << endl;
}
~Base()
{
Function();
}
};
class A : public Base
{
public:
A()
{
}
virtual void Function()
{
cout << "A::Fuction" << endl;
}
~A()
{
Function();
}
};
int main()
{
A a;
// Base * b=&a;
a.Function();
return 0;
}
2、为什么构造函数不能定义为虚函数
- 虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。
- 构造函数必须清晰的构造类型
从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过父类的指针或引用来调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
3、为什么析构函数可以定义为虚函数
用基类类型指针或者引用指向派生类实例
,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。
为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
4、构造函数的执行顺序?析构函数的执行顺序?
1) 构造函数顺序
- ① 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
- ② 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
- ③ 派生类构造函数。
2) 析构函数顺序
- ① 调用派生类的析构函数;
- ② 调用成员类对象的析构函数;
- ③ 调用基类的析构函数。
构造函数执行顺序
1、调用虚基类构造函数(如有多个则按虚基类声明顺序从左到右)
2、调用基类构造函数(如有多个则按基类声明顺序从左到右)
3、设定虚函数表指针值(virtual table pointer)
4、执行初始化列表、调用成员变量构造函数(按成员变量声明顺序)
执行自身构造函数
1、析构函数执行顺序(与构造函数相反)
2、执行自身析构函数
3、调用成员变量析构函数(与成员声明顺序相反)
4、调整虚函数表指针值
5、调用基类析构函数(从右到左)
6、调用虚基类析构函数(从右到左)
特例
局部对象,在退出程序块时析构
静态对象,在定义所在文件结束时析构
全局对象,在程序结束时析构
继承对象,先析构派生类,再析构父类
对象成员,先析构类对象,再析构对象成员
应用举例
class ObjectD
{
public:
ObjectD()
{
cout << "ObjectD()......" << endl;
}
~ObjectD()
{
cout << "~ObjectD()......" << endl;
}
};
class Base
{
public:
Base(int b):b_(b)
{
cout << "Base()......" << endl;
}
~Base()
{
cout << "~Base()......" << endl;
}
//private:
int b_;
};
class Derived :public Base
{
public:
Derived(int d):d_(d),Base(100)
{
cout << "Derived()......" << endl;
}
~Derived()
{
cout << "~Derived()......" << endl;
}
int d_;
ObjectD objd_;
};
Derived d(10);
注意:
当基类中也有定义的类成员对象,同样遵从上述顺序,先构造成员对象,在构造基类。
class ObjectB
{
public:
ObjectB()
{
cout << "ObjectB()......" << endl;
}
~ObjectB()
{
cout << "~ObjectB()......" << endl;
}
};
class Base
{
public:
Base(int b):b_(b)
{
cout << "Base()......" << endl;
}
~Base()
{
cout << "~Base()......" << endl;
}
//private:
int b_;
ObjectB objb_;
};
特例 说明
#include <iostream>
using namespace std;
class Base1
{
public:
Base1(void) { cnt++; cout << "Base1::constructor(" << cnt << ")" << endl; }
~Base1(void) { cnt--; cout << "Base1::deconstructor(" << cnt + 1 << ")" << endl; }
private:
static int cnt;
};
int Base1::cnt = 0;
class Base2
{
public:
Base2(int m) { num = m; cout << "Base2::constructor(" << num << ")" << endl; }
~Base2(void) { cout << "Base2::deconstructor(" << num << ")" << endl; }
private:
int num;
};
class Example
{
public:
Example(int n) { num = n; cout << "Example::constructor(" << num << ")" << endl; }
~Example(void) { cout << "Example::deconstructor(" << num << ")" << endl; }
private:
int num;
};
class Derived :public Base1, public Base2
{
public:
Derived(int m, int n) :Base2(m), ex(n) { cnt++; cout << "Derived::constructor(" << cnt << ")" << endl; }
~Derived(void) { cnt--; cout << "Derived::deconstructor(" << cnt + 1 << ")" << endl; }
private:
Example ex;
static Example stex; //Example::constructor(1) //不能输出
static int cnt;
};
int Derived::cnt = 0;
Derived ge_a(1, 2); // Base1::constructor(1)
// Base2::constructor(1)
// Example::constructor(2)
// Derived::constructor(1)
static Derived gs_b(3, 4); // Base1::constructor(2)
// Base2::constructor(3)
// Example::constructor(4)
// Derived::constructor(2)
int main(void)
{
cout << "---------start---------" << endl;
Derived d(5, 6); // Base1::constructor(3)
// Base2::constructor(5)
// Example::constructor(6)
// Derived::constructor(3)
Derived e(7, 8); // Base1::constructor(4)
// Base2::constructor(7)
// Example::constructor(8)
// Derived::constructor(4)
cout << "----------end----------" << endl;
//Derived e(7,8) 析构
// Derived::deconstructor(4)
// Example::deconstructor(8)
// Base2::deconstructor(7)
// Base1::deconstructor(4)
//Derived d(5,6) 析构
// Derived::deconstructor(3)
// Example::deconstructor(6)
// Base2::deconstructor(5)
// Base1::deconstructor(3)
return 0;
}
结果输出说明;
这是定义的全局对象的输出结果:构造顺序——》基类构造函数、对象成员构造函数、派生类本身的构造函数
Derived ge_a(1,2); // Base1::constructor(1)
// Base2::constructor(1)
// Example::constructor(2)
// Derived::constructor(1)
static Derived gs_b(3,4); // Base1::constructor(2)
// Base2::constructor(3)
// Example::constructor(4)
// Derived::constructor(2)
main函数局部变量的输出结果: 构造顺序——》基类构造函数、对象成员构造函数、派生类本身的构造函数
Derived d(5, 6); // Base1::constructor(3)
// Base2::constructor(5)
// Example::constructor(6)
// Derived::constructor(3)
Derived e(7, 8); // Base1::constructor(4)
// Base2::constructor(7)
// Example::constructor(8)
// Derived::constructor(4)
main函数局部变量的输出结果: 析构顺序——》派生类本身的析构函数、对象成员析构函数、基类析构函数
//Derived e(7,8) 析构
// Derived::deconstructor(4)
// Example::deconstructor(8)
// Base2::deconstructor(7)
// Base1::deconstructor(4)
//Derived d(5,6) 析构
// Derived::deconstructor(3)
// Example::deconstructor(6)
// Base2::deconstructor(5)
// Base1::deconstructor(3)
全局对象没有结果输出的说明:
//static Derived gs_b(3,4) 析构
// Derived::deconstructor(2)
// Example::deconstructor(4)
// Base2::deconstructor(3)
// Base1::deconstructor(2)
//Derived ge_a(1,2) 析构
// Derived::deconstructor(1)
// Example::deconstructor(2)
// Base2::deconstructor(1)
// Base1::deconstructor(1)
//static Example stex 析构
//Example::deconstructor(1) //不能输出
5、构造函数可以是内联函数
构造函数可以是内联函数
1、只要是类的成员函数,都可以放在类内定义,在类内定义的函数,编译器一般视作内联
2、inline只是一种申请,其目的是为了提高函数的执行效率(速度)。究竟是不是内联还得由编译器决定,自动地取消不值得的内联。一般情况下,构造函数比较小的情况下,不管你是否指定其为内联函数,C++编译器会自动将其置为内联,如果函数太大,你即使将其指定为内联函数系统也会不理的。因为这会使程序过大。if ((构造函数的函数体写在类里面 || 定义构造函数时加上inline)&& 函数体合适用内联)
{
构造函数是内联函数 = true;
}
else
{
构造函数是内联函数 = false;
}
6、什么情况下,需要在派生类初始化列表中初始化,不能在构造函数体内初始化(应该叫做赋值)?
(1)const成员:因为定义的时候需要初始化
(2)引用成员:因为定义的时候需要初始化
(3)基类没有默认构造函数,在初始化列表中调用基类构造函数完成
(4)派生类里面的对象成员没有默认构造函数
参考
1、https://blog.csdn.net/magictong/article/details/6734241
2、https://blog.csdn.net/sumup/article/details/78174915
3、https://blog.csdn.net/hxz_qlh/article/details/14089895
4、https://blog.csdn.net/shilikun841122/article/details/79012779