【C++】04.继承、多态、模板

【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM

【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym

【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM

【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0

继承

67.什么是类的继承?

类的继承是面向对象编程中的一个概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以获得父类的所有公有属性和方法,并且可以在此基础上添加自己的属性和方法

子类可以重写或扩展父类的方法,也可以添加新的方法和属性。继承允许程序员在不破坏已有代码的情况下创建新的类,并且可以减少代码的重复性和提高代码的复用性。

在继承关系中,子类可以访问父类的公有属性和方法,但不能访问私有属性和方法。子类也可以调用父类的构造函数,并可以在子类的构造函数中添加自己的初始化代码。

68.什么是虚拟继承

什么是虚基类

虚拟继承是C++中一种特殊的继承方式,它用于解决继承中的“钻石继承”问题

“钻石继承”是指在继承关系中,存在多个派生类同时继承自同一个基类,而这些派生类又被另一个派生类所继承的情况。这种情况会导致同一个基类在最终的派生类中出现多次,从而产生了二义性。

为了解决这个问题,C++提供了虚拟继承。虚拟继承使用关键字“virtual”来声明继承关系,使得同一个基类在最终的派生类中只出现一次,从而消除了二义性

当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。虚基类和虚拟继承是一起使用的,其中虚基类是在虚拟继承中被声明的基类。它们的组合可以解决多重继承中的菱形继承问题。

虚拟继承的语法如下:

#include <iostream>
using namespace std;

class A{}
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};

int main()
{
    cout << "sizeof(A):" << sizeof A <<endl; // 1,空对象,只有一个占位
    cout << "sizeof(B):" << sizeof B <<endl; // 4,一个bptr指针,省去占位,不需要对齐
    cout << "sizeof(C):" << sizeof C <<endl; // 4,一个bptr指针,省去占位,不需要对齐
    cout << "sizeof(D):" << sizeof D <<endl; // 8,两个bptr,省去占位,不需要对齐
} 

在这个例子中,类D继承自类B和类C,而类B和类C都通过虚拟继承方式继承自类A。这样,在类D中,类A只会出现一次,从而消除了二义性。

这种方式是一种菱形继承或者钻石继承

**虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。**虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr,如上图所示。如果既存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针。

虚拟继承的原理是,虚拟继承会为基类添加一个虚拟表指针(vptr),所有继承了同一个虚拟基类的派生类都共享同一个虚拟表指针。这样,当派生类调用虚拟基类的成员函数时,只会使用一份虚拟表,无论基类被继承多少次,只会存在一个实体,从而消除了二义性。

需要注意的是,虚拟继承会带来一些额外的开销,包括额外的内存开销和运行时开销。因此,在使用虚拟继承时,需要考虑这些开销对程序性能的影响。

69.多继承的优缺点,作为一个开发者怎么看待多继承

多继承是C++中一种特殊的继承方式,它允许一个类同时继承自多个基类。

优点:

  1. 代码重用:多继承可以使派生类同时拥有多个基类的特性和行为,从而提高代码的重用性。
  2. 灵活性:多继承允许派生类在不同的基类中选择需要的成员,从而增强了代码的灵活性。
  3. 多态性:多继承可以实现多态性,使得派生类可以表现出不同的行为。

缺点:

  1. 复杂性:多继承可能导致代码结构的复杂性,尤其是当多个基类有相同的成员时,派生类需要进行显式调用以避免二义性。
  2. 命名冲突:多继承可能导致命名冲突,因为派生类可能会继承自多个基类,而这些基类可能有相同的成员名称。
  3. 不稳定性:多继承可能导致不稳定性,尤其是当基类之间的关系发生变化时,派生类可能需要进行大量的修改。

作为一个开发者,应该根据实际需求和设计要求来选择使用多继承还是其他的继承方式。在使用多继承时,需要注意以下几点:

  1. 避免命名冲突:派生类应该使用作用域解析符来显式指定使用哪个基类的成员。

  2. 简化继承结构:尽量避免过于复杂的继承结构,可以使用组合等其他技术来替代多继承。

  3. 谨慎使用多继承:多继承应该谨慎使用,只在必要的情况下使用,并尽量避免过度使用。

  4. 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性

  5. 加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。

  6. 使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。

70.C++设计实现一个不能被继承的类
  • 把构造函数和析构函数都定义为私有函数。当一个类试图从它那继承的时候,必然会由于试图调用构造函数、析构函数而导致编译错误。

这个类的构造函数和析构函数都是私有函数了,怎样得到该类的实例?

可以通过定义静态来创建和释放类的实例

/// Define a class which can't be derived from /// 
class FinalClass1 {
public : 
    static FinalClass1* GetInstance() 
    {
    	return new FinalClass1; 
    }
	static void DeleteInstance( FinalClass1* pInstance) 
	{
		delete pInstance; pInstance = 0;
	}
private : 
	FinalClass1() {} 
	~FinalClass1() {}
};

这个类是不能被继承,但在总觉得它和一般的类有些不一样,使用起来也有点不方便。比如,我们只能得到位于堆上的实例,而得不到位于栈上实例。能不能实现一个和一般类除了不能被继承之外其他用法都一样的类呢?办法总是有的,不过需要一些技巧。请看如下代码:

/// Define a class which can't be derived from /// 
template <typename T> 
class MakeFinal {
	friend T; 
private : 
	MakeFinal() {} 
	~MakeFinal() {}
};
class FinalClass2 : virtual public MakeFinal<FinalClass2>
{ 
public : 
	FinalClass2() {}
	~FinalClass2() {} 
};

这个类使用起来和一般的类没有区别,可以在栈上、也可以在堆上创建实例。尽管类 MakeFinal < FinalClass2> 的构造函数和析构函数都是私有的,但由于类 FinalClass2 是它的友元函数,因此在 FinalClass2 中调用 MakeFinal < FinalClass2> 的构造函数和析构函数都不会造成编译错误。但当我们试图从 FinalClass2 继承一个类并创建它的实例时,却不通过编译。

class Try : public FinalClass2 
{
	public : Try() {} 
	~Try() {}
}; 
Try temp;

由于类 FinalClass2 是从类 MakeFinal < FinalClass2 > 虚继承过来的,在调用 Try 的构造函数的时 候,会直接跳过 FinalClass2 而直接调用 MakeFinal < FinalClass2 > 的构造函数。非常遗憾的是Try 不是 MakeFinal < FinalClass2 > 的友元,因此不能调用其私有的构造函数。 基于上面的分析,试图从 FinalClass2 继承的类,一旦实例化,都会导致编译错误,因此是 FinalClass2 不能被继承。这就满足了我们设计要求。

  • final 关键字来实现一个不能被继承的类。下面是一个使用 C++ 的示例代码:
cpp复制代码class FinalClass final {
public:
    FinalClass() { /* 构造函数代码 */ }
    ~FinalClass() { /* 析构函数代码 */ }

    // 其他成员函数和成员变量

private:
    // 私有成员变量和函数
};

在上面的示例中,我使用了 final 关键字将 FinalClass 声明为最终类,因此它不能被其他类继承。请根据你的需求修改构造函数、析构函数和其他成员函数、成员变量等。

71.继承机制中对象之间如何转换?
  1. 向上转型(upcasting):将一个派生类对象转换为基类对象,可以通过将派生类对象的指针或引用赋值给基类指针或引用实现。这样做是安全的,因为基类指针或引用只能访问派生类对象的基类部分,而不能访问派生类部分。
  2. 向下转型(downcasting):将一个基类对象转换为派生类对象。这个转换需要使用强制类型转换(static_cast/dynamic_cast)操作符,但只有在基类对象实际上是派生类对象的情况下才能成功。如果基类对象不是派生类对象,则向下转型会导致未定义的行为。
72.知道C++中的组合吗?它与继承相比有什么优缺点吗?

继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

组合是一种关联关系,它使得一个类对象可以包含其他类对象作为成员变量。组合可以用来表示“has-a”关系,即一个类对象包含另一个类对象。

与继承相比,组合有以下优缺点:

优点:

  1. 灵活性更高:组合使得类对象可以包含不同类型的其他类对象,这使得代码更加灵活。
  2. 更好的封装性:组合可以实现更好的封装,因为成员变量是私有的,只能被包含类的成员函数访问,外部无法直接访问。
  3. 更少的耦合性:组合不会像继承一样导致紧密耦合的代码,因为它只是一种关联关系,不涉及代码重用或多态。

缺点:

  1. 更多的代码复杂性:由于组合涉及到多个类的关系,因此它可能会导致更多的代码复杂性,例如构造函数、析构函数等。
  2. 更多的内存管理:组合需要更多的内存管理,因为类对象的生命周期由其包含类对象的生命周期决定,而包含类对象的生命周期通常比较复杂。
  3. 更少的代码重用:与继承相比,组合不支持代码重用,因为它不能继承基类的成员函数和数据成员。

多态

73.C++多态性

多态有几种类型

C++多态性是一种面向对象编程的特性,它使用同一种类型的对象来执行不同的操作。有两种类型的多态性:

  1. 编译时多态性:通过函数重载和运算符重载实现,编译器在编译期间根据函数或运算符的参数类型和个数来决定调用哪个函数或运算符重载函数。
  2. 运行时多态性:通过虚函数和抽象类实现。在运行时,程序根据实际对象的类型来确定调用哪个函数,从而实现多态性。这种多态性可以通过继承来实现,派生类重写基类的虚函数,从而实现对虚函数的动态绑定。
74.C++的多态如何实现

在C++中,如果我们想要实现多态性,我们需要使用虚函数。虚函数是指在基类中使用virtual关键字声明的函数。派生类可以通过重写基类的虚函数来实现对虚函数的动态绑定。动态绑定使得程序在运行时,根据实际对象的类型来确定调用哪个函数,从而实现多态性。

举个例子:

#include <iostream>
using namespace std;

class Base{
public:
	virtual void fun(){
		cout << " Base::func()" <<endl;
	}
};

class Son1 : public Base{
public:
	virtual void fun() override{
		cout << " Son1::func()" <<endl;
	}
};

class Son2 : public Base{

};

int main()
{
	Base* base = new Son1;
	base->fun();
	base = new Son2;
	base->fun();
	delete base;
	base = NULL;
	return 0;
}
// 运行结果
// Son1::func()
// Base::func() 

例子中,Base为基类,其中的函数为虚函数。

子类1继承并重写了基类的函数,子类2继承基类但没有重写基类的函数,从结果分析子类体现了多态性。

75.C++ 多态的底层实现原理

C++如何实现多态?

虚函数表具体是怎样实现运行时多态的?

虚表指针vptr的初始化时间

十分钟带你搞明白虚函数、虚表、多态的原理以及多重继承带来的问题_哔哩哔哩_bilibili

C++多态的底层原理_卖寂寞的小男孩的博客-CSDN博客_c++ 多态的本质回调

我如何理解C++虚表和动态绑定_哔哩哔哩_bilibili

C++中通过虚函数实现多态。虚函数的本质就是通过基类指针访问派生类定义的函数。每个含有虚函数的类,其实例对象内部都有一个虚函数表指针。该虚函数表指针被初始化为本类的虚函数表的内存地址。所以,在程序中,不管对象类型如何转换,该对象内部的虚函数表指针都是固定的,这样才能实现动态地对对象函数进行调用,这就是C++多态性的原理。

虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表,是一个存储类成员函数指针的数据结构

虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针

alt

上图中展示了虚表和虚表指针在基类对象和派生类对象中的模型,下面阐述实现多态的过程:

  1. 编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址

  2. 编译器会在每个对象的前四个字节中保存一个虚表指针,即 *vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数

  3. 在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表

  4. 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面。

这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。

76.基类的虚函数表存放在内存的什么区

虚函数表的特征:

  • 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成
  • 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段
  • 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中

C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

虚函数表属于常量数据,它是在编译时就生成的,且在程序运行期间不会被修改,因此通常会被放在只读数据段(.rodata)中。

虚函数则属于可执行代码,它是程序的一部分,需要在运行期间被执行。因此,虚函数通常会被放在代码段(.text)中。虽然代码段是可执行的,但是对于虚函数来说,由于其代码在程序运行期间不会被修改,因此通常也被视为常量数据。

77.什么是虚函数?

虚函数的作用

虚函数是指可以被子类覆盖的成员函数。当使用一个基类指针或引用来调用一个虚函数时,程序会根据实际对象类型来选择调用哪个函数,这被称为运行时多态。

78.虚函数的代价是什么?
  1. 虚函数调用的性能损失:虚函数的调用需要额外的开销,因为程序需要根据对象的实际类型来查找正确的虚函数表和调用对应的函数。相比于普通函数的调用,虚函数的调用需要更多的时间和空间开销,尤其是在对程序性能要求比较高的场景下,可能会影响程序的运行速度和响应时间。
  2. 对象内存的增加:由于每个对象都需要存储指向自己所属类的虚函数表的指针,因此使用虚函数会增加对象的内存开销,尤其是当类的继承层次较深时,对象的内存开销会进一步增加。
  3. 编译器优化难度的增加:由于虚函数调用的动态性质,编译器很难在编译时对虚函数调用进行优化。这也就使得在一些需要编译器优化的场景下,虚函数的使用可能会影响程序的性能。
79.虚函数可以私有化吗

在C++中,虚函数可以是私有的,但这样做会限制其使用的范围和功能。当一个虚函数被声明为私有时,它只能在定义该函数的类内部访问,而不能在派生类中直接访问或覆盖

将虚函数声明为私有可能是有用的,在某些特定情况下可以提供更严格的封装和控制。私有虚函数可以用作类内部的实现细节,而不会被派生类直接访问或修改。这种做法通常用于防止派生类覆盖某些特定的行为或提供一种基于多态的内部实现

示例,在C++中

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

【嵌入式八股】一、语言篇 文章被收录于专栏

查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)

全部评论

相关推荐

手撕没做出来是不是一定挂
Chrispp3:不会,写出来也不一定过
点赞 评论 收藏
分享
10-24 13:36
门头沟学院 Java
Zzzzoooo:更新:今天下午有hr联系我去不去客户端,拒了
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
2
3
分享
牛客网
牛客企业服务