【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态

简介: 【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态

1. 起源

在C++的早期设计中,通过基类指针可以访问派生类的成员变量,这是由于派生类对象在内存中的布局是基类成员变量在前,派生类成员变量在后。因此,当我们使用基类指针指向派生类对象时,可以正常访问到派生类中从基类继承来的成员变量。

然而,对于成员函数,情况就不同了。在编译时期,成员函数并不会被放入对象的内存空间中,而是存放在一块单独的内存区域,每个类只有一份成员函数的代码。当我们通过基类指针调用成员函数时,编译器会根据指针的静态类型(也就是基类类型)去查找对应的成员函数,而不是动态类型(也就是实际指向的派生类类型)。这就导致了我们无法通过基类指针调用派生类的成员函数。

为了解决这个问题,C++引入了虚函数的概念。通过将基类的成员函数声明为虚函数,我们就可以通过基类指针调用派生类的成员函数,实现了所谓的多态性。这是C++支持面向对象编程的一个重要特性。

2. 构成多态的条件

多态是面向对象编程的一个重要特性,它允许我们通过基类指针或引用来操作派生类对象。在C++中,要实现多态,需要满足以下条件:

  • 存在继承关系:多态基于继承,因为只有在存在基类和派生类的情况下,我们才能通过基类来操作派生类。这是多态的基础。
  • 被调用的函数必须是虚函数:在C++中,只有声明为虚函数的成员函数才能实现多态。虚函数允许在派生类中被重写,这样当我们通过基类指针或引用调用这个函数时,会根据实际的对象类型来调用相应的函数,这就是动态绑定。
  • 虚函数必须被重写:虚函数的重写意味着在派生类中提供了一个与基类虚函数具有相同函数签名(即函数名和参数类型)的函数。这样,当我们通过基类指针或引用调用这个函数时,会调用派生类中的版本,而不是基类中的版本。

满足以上所有条件,我们就可以通过基类指针或引用来操作派生类对象,实现多态。这使得我们的代码更具有通用性和可扩展性,因为我们可以添加新的派生类,只要它们正确地重写了基类的虚函数,就可以被同样的基类指针或引用操作,而无需修改已有的代码。

3. 虚函数

虚函数指针 (virtual function pointer) 从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。

虚函数指针是确实存在的数据类型,在一个被实例化的对象中,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。与对象的成员不同,虚函数指针对外部是完全不可见的,除非通过直接访问地址的做法或者在DEBUG模式中,否则它是不可见的也不能被外界调用。

只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数也都会对应一个虚函数指针。所以拥有虚函数的类的所有对象都会因为虚函数产生额外的开销,并且也会在一定程度上降低程序速度。与JAVA不同,C++将是否使用虚函数这一权利交给了开发者,所以开发者应该谨慎的使用。

3.1 虚函数特征

  • 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  • 将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
  • 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  • 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。
  • 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。

3.2 虚函数表的生成

  • 先将基类中的虚表内容拷贝一份到派生类虚表中;
  • 如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类的虚表的最后。

3.3 不规范的重写行为

  • 在派生类中依旧保持虚函数的属性,我们只是重写了它,这是非常不规范的。
  • 在重写基类虚函数的时候,派生类的虚函数不加关键字virtual也可以构成重写,但是这种写法不规范,不建议这样使用。

补充:虚函数的主要目的是允许用基类的引用或指针调用派生类的实现,这称为动态绑定或延迟绑定。如果一个函数不是虚函数,那么编译器在编译时就会解析出函数的调用,这称为静态绑定或早期绑定。

4. 抽象类和纯虚函数

在C++中,纯虚函数是一种特殊的虚函数,它在基类中没有定义,只有声明。纯虚函数的声明形式如下:

virtual 返回值类型 函数名 (函数参数) = 0;

这里的= 0就表示这是一个纯虚函数。纯虚函数在基类中没有实现,需要在派生类中被重写。

包含纯虚函数的类被称为抽象类(也叫接口类)。抽象类不能被实例化,也就是说,你不能创建一个抽象类的对象。这是因为抽象类包含至少一个没有实现的函数,所以抽象类的对象是不完整的。

当派生类继承了抽象类后,如果派生类没有重写所有的纯虚函数,那么这个派生类也还是一个抽象类,也不能被实例化。只有当派生类重写了所有的纯虚函数,这个派生类才不再是抽象类,可以被实例化。

纯虚函数的存在,规定了所有继承这个抽象类的派生类都必须实现这个函数,这就是所谓的接口继承。接口继承强调的是派生类必须实现的一组公共接口,而不是继承了一些已经实现的行为。

总的来说,抽象类和纯虚函数是面向对象多态性的一个重要机制,它使得基类可以定义接口,而将具体的实现留给派生类去完成。

5. 虚拟继承

在C++中,virtual关键字在继承中的使用主要是为了解决多重继承中的菱形继承问题(Diamond Problem)。

菱形继承问题是指在多重继承过程中,一个类可能会通过多个路径继承到同一个基类,这会导致在最底层的派生类中,基类的成员会出现重复,造成资源浪费和可能的命名冲突。

当你使用public Animal进行继承时,这是普通的公有继承。如果一个类通过多个路径继承了Animal,那么在最底层的派生类中,Animal的每个实例都会有一个副本。这可能会导致二义性和不必要的资源浪费。

例如,假设你有以下的类结构:

class Animal {
public:
    void eat();
};
class Mammal : public Animal {
};
class Bird : public Animal {
};
class Bat : public Mammal, public Bird {
};

在这个例子中,Bat类通过MammalBird类继承了Animal类。这意味着在Bat类的对象中,有两个Animal类的实例。如果你调用Bat对象的eat方法,编译器将无法确定应该调用哪个Animal实例的eat方法,这就产生了二义性。

然而,如果你使用virtual public Animal进行继承,这就是虚拟继承。虚拟继承确保无论一个类通过多少个路径继承了基类,基类在派生类中只有一个实例。这就解决了菱形继承问题。

以下是使用虚拟继承的版本:

class Animal {
public:
    void eat();
};
class Mammal : virtual public Animal {
};
class Bird : virtual public Animal {
};
class Bat : public Mammal, public Bird {
};

在这个例子中,Bat类的对象只有一个Animal类的实例,因此调用eat方法时就不会有二义性了。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

目录
相关文章
|
4月前
|
存储 人工智能 编译器
c++--多态
上一篇文章已经介绍了c++的继承,那么这篇文章将会介绍多态。看完多态的概念,你一定会感觉脑子雾蒙蒙的,那么我们先以举一个例子,来给这朦胧大致勾勒出一个画面,在此之前,先介绍一个名词虚函数,(要注意与虚拟继承区分)重定义: 重定义(隐藏)只要求函数名相同(但要符合重载的要求,其实两者实际上就是重载);重定义下:在这种情况下,如果通过父类指针或引用调用函数,会调用父类的函数而不是子类。重定义(或称为隐藏)发生的原因是因为函数名相同但参数列表不同,导致编译器无法确定调用哪一个版本的函数。
72 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
173 0
|
7月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
363 6
|
8月前
|
安全 编译器 程序员
C++ noexcept 关键字的关键作用
`noexcept` 关键字在 C++ 中扮演着重要角色,通过正确使用 `noexcept`,可以提升程序的性能、增强代码的可读性和安全性,并且有助于编译器进行优化。在编写 C++ 代码时,应仔细考虑每个函数是否应该声明为 `noexcept`,以充分利用这一特性带来的优势。通过本文的介绍,希望开发者能够更好地理解和应用 `noexcept` 关键字,从而编写出更加高效、健壮的 C++ 程序。
242 8
|
8月前
|
编译器 C++
c++中的多态
c++中的多态
|
7月前
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
870 0
|
9月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
136 4
|
9月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如<string>、<cstdlib>等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
181 3
|
10月前
|
安全 编译器 C++
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
298 1
|
11月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
269 5