突破编程_C++_面试(基础知识(11))

面试题34:什么是继承,它有哪些类型

继承是面向对象编程的一个基本概念,它允许一个类(派生类、子类)继承另一个类(基类、父类)的属性和方法。继承可以减少代码冗余,提高代码重用性,并且有助于创建更复杂的类结构。
要在派生类中继承基类,只需在派生类定义的时候列出基类的名称,并指定继承方式(公有、保护或私有):

class BaseClass 
{  
    // 基类的成员  
};  
  
class DerivedClass : public BaseClass 
{  
    // 派生类的成员  
};

在上面代码中,Derived 类公有地继承了 Base 类。这意味着 Base 类中的公有成员和保护成员在 Derived 类中保持原有的访问权限(公有和保护),而 Base 类中的私有成员在 Derived 类中是不可访问的。
派生类是基类的一个特殊化版本。这意味着派生类拥有基类所有的属性和方法,同时还可以定义自己的新属性和方法。这种关系通常被称为 “IS-A” 关系,即派生类 “是一种” 基类。
C++支持三种继承方式:
(1)公有继承( public ):
在公有继承中,基类中的公有成员在派生类中保持公有,基类的保护成员在派生类中保持保护,而基类的私有成员在派生类中是不可访问的。这是最常见的继承方式。
(2)私有继承( private ):
在私有继承中,基类中的所有成员(公有、保护和私有)在派生类中都是私有的。这意味着派生类的对象不能访问基类中的公有和保护成员。私有继承通常用于实现接口继承,即派生类仅继承基类的接口而不继承其实现。
(3)保护继承( protected ):
在保护继承中,基类中的公有成员和保护成员在派生类中都是保护的,而基类的私有成员在派生类中是不可访问的。保护继承是公有继承和私有继承之间的一种折中方式,它允许派生类访问基类的公有和保护成员,但不允许派生类的对象这样做。

继承方式比较

继承方式 基类公有成员 基类保护成员 基类私有成员
公有继承 继承为公有成员 继承为保护成员 不继承
保护继承 继承为保护成员 继承为保护成员 不继承
私有继承 继承为私有成员 继承为私有成员 不继承

面试题35:继承的优缺点是什么

继承在面向对象编程中具有重要的地位,它提供了代码重用和扩展性的机制。然而,就像任何其他编程概念一样,继承也有其优点和缺点。
优点:
(1)代码重用:继承允许子类重用父类的代码,这避免了在多个类中重复编写相同的代码。
(2)扩展性:通过继承,子类可以添加或覆盖父类的行为,从而实现对父类功能的扩展或修改。
(3)多态性:继承是实现多态性的基础,允许在运行时根据对象的实际类型来调用相应的方法。
(4)层次结构:继承有助于创建类的层次结构,这有助于组织和管理代码,并使得程序结构更加清晰。
(5)简化设计:通过继承,可以将复杂系统分解为更小的、更易于管理的部分。
缺点:
(1)代码耦合:继承可能导致代码之间的耦合度过高。如果父类发生变化,可能需要修改所有子类以适应这些变化。
(2)破坏封装性:继承可能会破坏封装性,因为子类可以访问父类的私有和保护成员。这可能导致父类的内部实现细节暴露给子类,增加了代码的复杂性。
(3)单一继承的限制:在C++中,类只能继承自一个基类(单继承)。这限制了代码的组织方式和可能的扩展性。
(4)菱形继承问题:在多重继承中,可能会出现菱形继承问题,即一个类从两个或多个路径继承同一个基类。这可能导致名称冲突和歧义。
(5)不易维护:如果继承层次结构过于复杂,可能会增加代码的维护难度。子类可能会依赖于父类的特定实现细节,而这些细节可能会在未来的版本中发生变化。
综上所述,继承是一种强大的机制,但也需要在使用时谨慎考虑其优缺点。在设计中,应该根据具体情况来决定是否使用继承,并尽量避免其潜在的问题。

面试题36:多态在C++中是如何实现的

多态( Polymorphism )是面向对象编程的三大基本特性之一,它允许我们使用相同的接口来表示不同类型的对象。在 C++ 中,多态通常通过虚函数( virtual functions )和指针或引用来实现。
虚函数是 C++ 中实现多态的关键机制。通过在基类的成员函数前加上 virtual 关键字,可以将其声明为虚函数。当派生类重写( override )这个虚函数时,就可以通过基类指针或引用来调用派生类的实现,这就是所谓的动态绑定或运行时多态。与之相对应的是静态绑定,即在使用父类指针或引用调用子类对象的成员函数时,如果没有使用虚函数,则会进行静态绑定,从而只能调用父类的成员函数,无法调用子类特有的成员函数。
虚函数的原理主要涉及虚函数表和虚函数指针:
(1)虚函数表
当一个类含有至少一个虚函数时,编译器会为这个类创建一个虚函数表( vtable ),并在每个该类的对象中嵌入一个指向这个虚函数表的指针(通常被称为 vptr )。
虚函数表是一个函数指针数组,其中每个元素都是指向类中定义的虚函数的指针。每个类,包括它的所有派生类,都会有自己的虚函数表。当派生类重写基类的虚函数时,派生类的虚函数表会包含指向这些重写函数的指针。
(2)虚函数指针
虚函数指针( virtual function pointer ,简称为 vptr )是一个隐藏的成员变量,它存在于包含至少一个虚函数的类的对象中。 vptr 指向一个虚函数表( vtable ),该表包含了类中所有虚函数的地址。虚函数表是一个函数指针数组,每个元素都指向一个虚函数的实现。
vptr 的存在是实现多态性的关键,它允许程序在运行时动态地确定应该调用哪个类的虚函数实现。当通过基类指针或引用调用一个虚函数时,实际调用的是 vptr 所指向的虚函数表中的函数。
虚函数的具体工作原理如下:
(1)定义虚函数:在基类中声明一个或多个虚函数。
(2)创建虚函数表:编译器为包含虚函数的类创建一个虚函数表。这个表是一个函数指针数组,每个元素指向类中的一个虚函数。
(3)初始化虚函数指针:当创建类的对象时,编译器会为该对象的虚函数指针成员变量分配内存,并初始化虚函数指针以指向该类的虚函数表。
(4)动态绑定:当通过基类指针或引用调用虚函数时,程序会查找虚函数指针所指向的虚函数表,并调用表中对应的函数。这个查找过程是在运行时进行的,因此被称为动态绑定。
C++中的多态主要有两种类型:静态多态和动态多态。
静态多态
也称为编译时多态或早绑定( Early Binding )。这主要通过函数重载( Function Overloading )和模板( Templates )来实现。在编译时,编译器就能确定应该调用哪个函数。
动态多态
也称为运行时多态或晚绑定( Late Binding )。这是通过虚函数( Virtual Functions )和继承来实现的。动态多态也被称为函数重写( Function Overriding )。在运行时,程序根据对象的实际类型来确定应该调用哪个函数。
如下为样例代码:

#include   

class BaseClass
{
public:
	void overloadFunc()
	{
		printf("BaseClass overloadFunc()\n");
	}

	virtual void overrideFunc()
	{
		printf("BaseClass overrideFunc()\n");
	}
};

class DerivedClass : public BaseClass
{
public:
	void overloadFunc()			// 函数重载
	{
		printf("DerivedClass overloadFunc()\n");
	}

	void overrideFunc()			// 函数重写
	{
		printf("DerivedClass overrideFunc()\n");
	}
};

int main() 
{	
	BaseClass* obj = new DerivedClass;
	obj->overloadFunc();					// 调用基类的函数
	((DerivedClass*)obj)->overloadFunc();	// 强制类型转换后调用继承类的重载函数
	obj->overrideFunc();					// 调用继承类的重写函数

	delete obj;
	obj = nullptr;
	return 0;
}

上面代码的输出为:

BaseClass overloadFunc()
DerivedClass overloadFunc()
DerivedClass overrideFunc()

在上面代码中, BaseClass 是一个基类,它有一个成员函数 overloadFunc() 以及一个虚函数 overrideFunc()。 DerivedClass 是 BaseClass 的派生类,它重载了 overloadFunc() 函数,并且重写了 overrideFunc() 函数。在 main 函数中,使用 BaseClass 基类的指针变量创建了一个 DerivedClass 派生类的对象,并调用其 overloadFunc() 以及 overrideFunc(),注意使用强制类型转换后才能真正调用到继承类的重载函数,而重写函数则可以直接调用。

面试题37:静态多态和动态多态有什么区别

静态多态和动态多态是面向对象编程中两种重要的多态形式,它们在实现机制、运行时期和适用场景等方面存在显著的差异。
实现机制:
静态多态:也称为编译时多态,主要通过函数重载和运算符重载实现。在编译时期,根据函数参数的类型和数量或运算符的类型,编译器可以确定应该调用哪个函数或运算符。
动态多态:也称为运行时多态,主要通过虚函数和继承实现。在运行时,根据对象的实际类型(即动态类型),动态地确定调用哪个虚函数。
运行时期:
静态多态:在编译时期就可以确定函数的调用地址,并生成代码。因此,地址是早绑定的。
动态多态:函数调用的地址不能在编译期间确定,需要在运行时才能确定,属于晚绑定。
适用场景:
静态多态:适用于编译时期就能确定函数调用的场景,如函数重载和运算符重载。它主要用于增强代码的灵活性和可读性。
动态多态:适用于需要在运行时才能确定函数调用的场景,如通过基类指针或引用调用派生类的虚函数。它主要用于实现继承和多态性,以实现代码的扩展性和灵活性。
总之,静态多态和动态多态在面向对象编程中都扮演着重要的角色。静态多态主要关注编译时期的函数选择,而动态多态则关注运行时期的动态行为。理解它们的区别和适用场景有助于更好地设计和实现面向对象程序。

面试题38:为什么要将析构函数声明为虚函数

将析构函数定义为虚函数是一个重要的代码构建技术点,特别是在设计基类时。这是因为在多态性的场景中,当使用基类指针或引用指向派生类对象时,如果没有虚析构函数,可能会导致派生类对象的析构过程不完整,从而引发资源泄漏和其他问题。
当基类的析构函数不是虚函数时,如果通过基类指针删除派生类对象,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类对象中的资源(如动态分配的内存)没有被正确释放,从而产生资源泄漏。
通过将基类的析构函数声明为虚函数,可以确保当使用基类指针或引用删除派生类对象时,派生类的析构函数也会被调用。这是多态性的一个关键方面,它允许在运行时确定应该调用哪个类的析构函数。
如下为样例代码:

#include   

class BaseClass
{
public:
	// 将析构函数声明为虚函数  
	virtual ~BaseClass()
	{
		printf("virtual ~BaseClass() \n");
	}
};

class DerivedClass : public BaseClass
{
public:
	~DerivedClass()
	{
		printf("virtual ~DerivedClass() \n");
	}
};

int main() 
{	
	BaseClass* obj = new DerivedClass;
	delete obj;
	obj = nullptr;

	// 如果 BaseClass 的析构函数不是虚函数,这里只会调用 BaseClass 的析构函数,  
    // 而不会调用 DerivedClass 的析构函数,导致资源泄漏。  
    // 如果 BaseClass 的析构函数是虚函数,则会先调用 DerivedClass 的析构函数,  
    // 然后调用 BaseClass 的析构函数,确保资源被正确释放。 
	
	return 0;
}

上面代码的输出为:

virtual ~DerivedClass()
virtual ~BaseClass()

在上面代码中,BaseClass 类的析构函数被声明为虚函数。当通过基类指针 obj 删除派生类 DerivedClass 的对象时,由于析构函数是虚函数,所以会先调用 DerivedClass 的析构函数,然后再调用 BaseClass 的析构函数。这样,派生类中的资源能够被正确释放,避免了资源泄漏。
因此,通常建议在设计基类时将析构函数定义为虚函数,以确保在删除派生类对象时能够正确地调用析构函数链。

你可能感兴趣的:(突破编程_C++_面试,c++,面试)