面试例题(11-20)

面试例题11:析构函数可以是内联函数么?

解析:我们可以先构造一个类,让它的析构函数是内联函数,如下所示:

#include <iostream>
using namespace std;
calss A
{
	public:
	void foo() { cout<< ”A”; };
	~A();
};
inline A::~A() { cout<<inline; }
int main()
{
	A* niu = new A();
	niu->foo();
	delete niu;
	return 0;
}

该程序可以正确编译并得出结果。
答案:析构函数可以是内联函数。

构造函数和析构函数

面试例题12:MFC类库中,CObject类的重要性不言自明。在CObject的定义中,我们看到一个有趣的现象,即CObject的析构函数是虚拟的。为什么MFC的编写者认为virtual destructors are necessary(虚拟的析构函数是必要的)?

MFC(Microsoft Foundation Classes),是微软公司提供的一个类库(class
libraries),以C++类的形式封装了Windows的API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含的类包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。

句柄类(智能指针smart point)是存储指向动态分配(堆)对象指针的类。

解析:我们可以先构造一个类如下:

calss CBase
{
public:
	~CBase(){....};
	...
};
calss CChild : public CBase
{
	public :
	~CChild(){...};
	...
};

main()
{
	child c;
	...
return 0;
}

上段代码在运行时,由于在生成CChild对象 c 时,实际上在调用CChild类的构造函数之前必须首先调用其基类CBase的构造函数,所以当撤销c时,也会在调用CChild类析构函数之后,调用CBase类的析构函数析(构函数调用顺序与构造函数相反)。也就是说,无论析构函数是不是虚函数,派生类对象被撤销时,肯定会依次上调其基类的析构函数。
那么为什么CObject类要搞一个虚的析构函数呢?
因为多态的存在。
仍以上面的代码为例,如果main()中有如下代码:

CBase* pBase;
CChild c;
pBase = &c;

那么在pBase指针被撤销时,调用的是CBase的析构函数还是CChild的呢?显然是CBase的(静态联编)析构函数。但如果把CBase类的析构函数改成virtual型,当pBase指针被撤销时,就会先调用CChild类构造函数,再调用CBase类构造函数。

答案:在这个例子里,所有对象都存在于栈框中,当离开其所处的作用域时,该对象会被自动撤销,似乎看不出什么大问题。但是试想,如果CChild类的构造函数在堆中分配了内存,而其析构函数又不是virtual型的,那么撤销pBase时,将不会调用CChild::~CChild(),从而不会释放CChild::CChild()占据的内存,造成内存泄漏。
将CObject的析构函数设为virtual型,则所有CObject类的派生类的析构函数都将自动变为virtual型这保证了在任何情况下,不会出现由于析构函数未被调用而导致的内存泄漏。这才是MFC将CObject::~CObject()设为virtual型的真正原因。

面试例题13::析构函数可以为virtual型,构造函数则不能,那么为什么构造函数不能为虚呢?

答案:虚函数采用一种虚调用的办法,虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数。但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚。

解释一:所谓虚函数就是多态情况下只执行一个。而从继承的概念来讲,总是要先构
造父类对象,然后才能是子类对象。如果构造函数设为虚函数,那么当你在构造父类
的构造函数时就不得不显式的调用构造。还有一个原因就是为了防错,试想如果你在
子类中一不小心重写了个跟父类构造函数一样的函数,那么你的父类的构造函数将被 覆盖,也即不能完成父类的构造.就会出错。

解释二:虚函数的主要意义在于被派生类继承从而产生多态。派生类的构造函数中,
编译器会加入构造基类的代码,如果基类的构造函数用到参数,则派生类在其构造函
数的初始化列表中必须为基类给出参数,就是这个原因。虚函数的意思就是开启动态
绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时
候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不
能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之 前,这个对象根本就不存在,它怎么动态绑定?)

面试例题14:如果虚函数是非常有效的,我们是否可以把每个函数都声明为虚函数?

答案:不行,这是因为虚函数是有代价的:由于每个虚函数的对象都必须维护一个V表,因此在使用虚函数的时候都会产生一个系统开销。如果仅是一个很小的类,且不想派生其他类,那么根本没必要使用虚函数。

拷贝构造函数和赋值函数

拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象

面试例题15:编写类String的构造函数、拷贝构造函数、析构函数和赋值函数。

答案
已知类String的原型为:

class string
{
public:
	Sting(const char* str = nullptr); //普通的构造函数
	String(const String &other); //拷贝构造函数
	~String(void); //析构函数
	String & operate = (const String &other); //赋值函数
private;
	Char* m_data; //用于保存字符串
};

编写String的上述4个函数。

  1. String的析构函数
    为了防止内存泄漏,我们还需要定义一个析构函数。当一个String对象超出它的作用域时,这个析构函数将会释放它所占用的内存。代码如下:
String::~String(void)
{
	deleta [] m_data;
	//由于m_data是内部数据类型,也可以写成delete m_data;
}
  1. String的构造函数
    这个析构函数可以帮助我们根据一个字符串常量创建一个MyString对象。这个构造函数首先分配了足量的内存,然后把这个字符串常量复制到这块内存,代码如下:
String::String(const char* str)
{
if(str == nullptr)
{
	m_data = new char[1]; //若能加nullptr判断则更好
	*m_data = “\0;
}
else
{
	int length = strlen(str);
	m_data = new char[length + 1]; //若能加nullptr判断则更好
	strcpy(m_data, str);
}
}

Strlen函数返回这个字符串常量的实际字符数(不包括NULL终止符),然后把这个字符串常量的所有字符赋值到我们在String对象创建过程中为m_data数据成员新分配的内存中。有了一个构造函数后,我们可以向下面这样根据一个字符串常量创建一个新的String对象:
string str( “hello” )

  1. String的拷贝构造函数
    所有需要分配系统资源的用户自定义类型都需要一个拷贝构造函数,这样我们可以使用这样的声明:
MyString s1(hello);
MyString s2 = s1;

拷贝构造函数还可以帮助我们在函数调用中以传值方式传递一个MyString参数,并且在当一个函数以值的形式返回Mystring对象时实现“返回时复制”。

String::String(const String &other)
{
	int length = strlen(other.m_data);
	m_data = new char[length + 1];
	strcpy(m_data,other.m_data);
}
  1. String的赋值函数
    赋值函数可以实现字符串的传值活动:
MyString s1(hello);
MyString s2;
s1 = s2;

代码如下:

String& String::operate = (const String &other)
{
	//检查自赋值
	if(this == &other)
	return *this;
	//释放原有的内存资源
	delete [] m_data;
	//分配新的内存资源,并复制内容
	int length = strlen(other.m_data);
	m_data = new char[length + 1];
	strpy(m_data,other.m_data);
	//返回本对象的引用
	return *this;
}

面试例题16:下面关于拷贝构造函数的说法哪一个时正确的?

A. 给每一个对象拷贝一个构造函数。
B. 有一个默认的拷贝构造函数。
C. 不能拷贝队列。
D. 以上结果都正确。
解析:拷贝函数问题
答案:B

面试例题17:下面所列举的类哪个不需要自己重写拷贝构造函数?ps:这题没看懂

A. 一个矩阵类:动态分配,对象的建立是利用构造函数,删除是利用析构函数。
B. 一个花名册类:每一个对象对照着唯一的ID。
C. 一个word类,对象是字符串类和模板类。
D. 一个图书馆类:由一系列书籍对象构成。
解析:按照题意,寻找一个不需要拷贝构造函数的类。
A 选项要定义拷贝构造函数。
B 选项中,不自定义拷贝构造函数的话,势必造成两个对象的ID不唯一。至于说自定义了拷贝构造函数之后,如何保证新对象的ID唯一,那是实现的问题。实现的方法有多种多样,比如可以使用当前的系统tick数作为新ID。当然语义上有损失,不是完全意义上的拷贝,但在这儿只能在保持语义和实现目的之间来一个折中。
选C的原因是使用默认的拷贝构造,string子对象和vector子对象的类都是成熟的类,都有合适的赋值操作,拷贝构造函数以避免“浅拷贝”问题。
D选项显然是定义拷贝构造函数。
答案:C。

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
再说几句:
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:

1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;

2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。

面试例题18:哪个子类的虚函数重新声明时正确的?

A.

Base* Base::copy(Base*);
Base* Derived::copy(Derived*);

B.

Base* Base::copy(Base*);
Derived* Derived::copy(Derived*);

C.

ostream& Base::print(int, ostream& = cout);
ostream& Derived::print(int, ostream&);

D.

Void Base::eval() const;
Void Derived::eval();

解析:本题问的是哪个派生类的虚函数再声明是对的。
A是重载;B会导致编译错误,C是真正的多态;D是重载。
B选项在gcc测试下也可以算是一种多态,覆盖的虚函数必须返回与父类的同名函数一致的类型。Derived class的虚函数的返回类型可以是base class中对应虚函数的返回类型的public derived class。所谓多态指针的一致是指“子类虚函数返回的多态指针的静态类型是父类虚函数所返回的多态指针的动态类型集合中的某个类型”。

答案:C。

多态的概念

面试例题19:什么是多态?

答案:开门,开窗户,开电视。在这里的“开”就是多态!
多态性可以简单的概括为“一个接口,多种方法”,在程序运行的过程中才决定调用的函数。多态性是面向对象编程领域的核心概念。
多态,按字面的意思就是“多种形状”。多态性是允许你将父对象设置为和它的一个或更多的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单地说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal 和 C++中都是通过虚函数实现的。

Object Pascal指Pascal的面向对象的衍生分支,以Delphi的主要编程语言著称。Pascal编译器,包括那些Object
Pascal的编译器,在生成高优化代码同时,一般运行非常快。

面试例题20:重载和覆盖有什么不同?

答案:虚函数总在派生类中被改写,这种改写被称为“override”(覆盖)。
Override是指派生类重写基类的虚函数,就像我们前面在B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况下,但是很少有编译器支持这个特性)。Override这个单词好像一直没有什么合适的中文词汇来对应。有人译为“覆盖”,还贴切一些。
overload约定成俗的被翻译为“重载”,是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数既可以接受整型作为参数,也可以接受浮点类型作为参数。重载不是一种面向对象编程,而只是一种语法规则,重载与多态没有什么直接关系。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值