学习c++的第十二天

2023-11-05 13:04
文章标签 c++ 学习 第十二天

本文主要是介绍学习c++的第十二天,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

多态性

多态性

虚函数

虚析构函数

纯虚函数


多态性

多态性

多态性的实现依赖于继承和虚函数。在面向对象编程中,通过基类的指针或引用来调用派生类的成员函数时,可以根据实际的对象类型决定调用哪个类的成员函数,从而实现多态性。

  • 在静态联编中,编译器在编译阶段就能确定调用的具体函数,这种情况下多态性的效果并不明显。而在动态联编中,编译器只能确定调用的是基类的成员函数,具体调用哪个派生类的函数则在运行时决定,这就发挥了多态性的优势。
  • 动态联编和多态性的典型应用是通过虚函数实现的。虚函数是在基类中声明的带有 virtual 关键字的成员函数,它可以被派生类重写(Override)。当使用基类的指针或引用调用虚函数时,实际调用的是派生类中重写的函数。

下面,我们围绕静态联编,给大家举一个简单例子:

#include <iostream>class Shape {
public:void draw() {std::cout << "绘制一个形状" << std::endl;}
};class Circle : public Shape {
public:void draw() {std::cout << "绘制一个圆形" << std::endl;}
};class Rectangle : public Shape {
public:void draw() {std::cout << "绘制一个矩形" << std::endl;}
};int main() {Shape shape;Circle circle;Rectangle rectangle;// 基类指针指向派生类对象Shape* shapePtr1 = &circle;Shape* shapePtr2 = &rectangle;// 调用基类的成员函数,由于静态联编,在编译时就确定了调用的函数shape.draw();       // 绘制一个形状shapePtr1->draw();  // 绘制一个形状shapePtr2->draw();  // 绘制一个形状return 0;
}

在上述示例中,我们有一个基类 Shape 和两个派生类 Circle 和 Rectangle。基类和派生类都有一个名为 draw 的成员函数。

在 main 函数中,我们创建了一个基类对象 shape 和两个派生类对象 circle 和 rectangle。然后,我们将派生类对象的地址赋给基类指针 shapePtr1 和 shapePtr2。

在调用成员函数时,基类对象 shape 和派生类对象的指针 shapePtr1 和 shapePtr2 都调用了 draw 函数。由于静态联编,在编译时就确定了调用的函数,因此无论是通过基类对象还是派生类对象的指针调用,都会执行基类的 draw 函数,输出 "绘制一个形状"。

这个示例展示了静态联编的特点,即在编译时就确定了调用哪个函数,不会根据对象的实际类型来决定调用哪个函数。

通过多态性,我们可以以一种统一的方式处理不同类型的对象。这样做的好处是,我们可以将对象视为其基类类型,而不需要关注具体是哪个派生类的实例。这使得我们可以设计出更加灵活和可扩展的代码,同时提高了代码的可读性和可维护性。

总结来说,多态性是面向对象编程的重要特性,它通过继承和虚函数机制实现。它允许不同类型的对象对同一个消息做出不同的响应,提高了代码的灵活性和可扩展性。静态联编和动态联编是多态性的两种实现方式,静态联编由于编译时候就已经确定好怎么执行,因此执行起来效率高;而动态联编想必虽然慢一些,但优点是灵活。两者各有千秋,有各自不同的使用场景。

虚函数

虚函数是用于实现动态联编和多态性的一种机制。通过将父类中的成员函数声明为虚函数,可以在子类中重写该函数并根据对象的实际类型选择调用哪个函数,从而实现多态性。

在 C++ 中,将函数声明为虚函数需要在函数名前加上关键字 virtual。当一个函数被声明为虚函数时,它的子类中的同名函数也会自动成为虚函数。当使用一个指向子类对象的基类指针或引用调用虚函数时,程序会在运行时动态地确定调用哪个函数,这就是动态联编。

一般形式如下:

virtual 函数返回值 函数名(形参)
{函数体
}

需要注意的是:

  1. 虚函数不能是静态成员函数或友元函数:虚函数依赖于对象的实际类型来确定调用哪个函数,而静态成员函数和友元函数不属于特定的对象,因此无法实现动态多态性,所以它们不能被声明为虚函数。
  2. 内联函数不能是虚函数:内联函数是在编译时插入代码的,而虚函数的调用是在运行时动态决定的。即使虚函数在类的定义中被标记为 inline,编译器仍然会将其视为非内联函数,因为虚函数的调用需要在运行时根据对象的实际类型来确定。
  3. 构造函数不能是虚函数,析构函数可以是虚函数:构造函数在创建对象时被调用,并负责初始化对象的成员变量等工作。由于在调用构造函数时对象尚未完全构建,因此无法实现动态多态性,所以构造函数不能声明为虚函数。而析构函数在销毁对象时被调用,可以实现动态多态性,通常情况下我们将析构函数声明为虚函数,以确保在删除基类指针时正确调用派生类的析构函数,防止内存泄漏。

以下是一个简单的使用虚函数的示例:

#include <iostream>
using namespace std;class Shape {
public:virtual void draw() {cout << "绘制一个形状" << endl;}
};class Circle : public Shape {
public:void draw() {cout << "绘制一个圆形" << endl;}
};class Rectangle : public Shape {
public:void draw() {cout << "绘制一个矩形" << endl;}
};int main() {Shape shape;Circle circle;Rectangle rectangle;Shape* shapePtr1 = &circle;Shape* shapePtr2 = &rectangle;// 通过基类指针调用虚函数,程序会在运行时动态地确定调用哪个函数shape.draw();       // 绘制一个形状shapePtr1->draw();  // 绘制一个圆形shapePtr2->draw();  // 绘制一个矩形return 0;
}

在上述示例中,我们将 Shape 类的 draw 函数声明为虚函数,并在其派生类 Circle 和 Rectangle 中重写了该函数。在 main 函数中,我们创建了一个基类对象 shape 和两个派生类对象 circle 和 rectangle,并将派生类对象的地址赋给基类指针。

通过基类指针调用虚函数时,程序会在运行时动态地确定调用哪个函数。因此,通过基类对象 shape 调用 draw 函数时,输出 "绘制一个形状";通过派生类对象的指针 shapePtr1 和 shapePtr2 调用 draw 函数时,分别输出 "绘制一个圆形" 和 "绘制一个矩形",实现了多态性。

虚析构函数

在C++中,我们不能将构造函数声明为虚函数,因为构造函数的调用是在实例化对象时发生的,而虚函数的实现是通过虚函数表来实现的。在实例化对象之前,对象还没有被创建,也没有内存空间可供调用虚函数,所以将构造函数声明为虚函数是没有意义的。

然而,析构函数可以被声明为虚函数,并且通常情况下我们将其声明为虚析构函数。通过将析构函数声明为虚函数,可以在删除指向派生类对象的基类指针时,根据实际所指向的对象类型动态绑定调用相应的析构函数,从而实现正确的对象内存释放。

当使用基类指针指向派生类对象时,如果基类的析构函数不是虚函数,那么在删除指针时只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类对象中动态分配的资源没有得到正确释放,从而造成内存泄漏。

通过声明析构函数为虚函数,我们可以保证在删除基类指针时调用正确的析构函数,确保对象的内存得到正确释放,避免内存泄漏问题。

下面是一个使用虚析构函数的示例:

#include <iostream>
using namespace std;class Base {
public:virtual ~Base() {cout << "调用了基类的析构函数" << endl;}
};class Derived : public Base {
public:~Derived() {cout << "调用了派生类的析构函数" << endl;}
};int main() {Base* ptr = new Derived();delete ptr;return 0;
}

在上述示例中,我们定义了一个基类 Base 和一个派生类 Derived。基类 Base 的析构函数被声明为虚函数(使用 virtual 关键字),而派生类 Derived 的析构函数没有显式声明为虚函数。

在 main 函数中,我们创建了一个派生类对象的指针 ptr,并将其赋给基类指针。接着,我们使用 delete 关键字删除基类指针,这会触发析构函数的调用。

由于基类的析构函数是虚函数,因此在删除指针时,会调用正确的析构函数。输出结果为:

调用了派生类的析构函数
调用了基类的析构函数

可以看到,通过将基类的析构函数声明为虚函数,我们确保了派生类的析构函数也被正确地调用,从而释放了派生类对象中的资源。

纯虚函数

在C++中,纯虚函数是一种在基类中声明但没有实现的虚函数。纯虚函数的声明形式是在函数原型的末尾使用 "= 0" 进行标记。纯虚函数用于定义一个接口,要求派生类必须实现该函数。

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

纯虚函数的作用是定义一个基类的接口,这个接口可以被派生类继承并实现。通过在基类中声明纯虚函数,我们可以强制派生类提供相应的实现,以确保派生类具有某些特定的行为或功能。

下面是一个使用纯虚函数的示例:

#include <iostream>
using namespace std;class Shape {
public:virtual double getArea() const = 0; // 纯虚函数
};class Rectangle : public Shape {
private:double length;double width;
public:Rectangle(double l, double w) : length(l), width(w) {}double getArea() const override { // 派生类必须实现纯虚函数return length * width;}
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}double getArea() const override { // 派生类必须实现纯虚函数return 3.14159 * radius * radius;}
};int main() {Shape* shape1 = new Rectangle(5, 6);Shape* shape2 = new Circle(3);cout << "矩形的面积: " << shape1->getArea() << endl;cout << "圆形的面积: " << shape2->getArea() << endl;delete shape1;delete shape2;return 0;
}

在上述示例中,我们定义了一个基类 Shape,其中声明了一个纯虚函数 getArea()。这个函数没有提供实现,只是要求派生类必须实现它。派生类 Rectangle 和 Circle 都继承自基类 Shape,并实现了 getArea() 函数。

在 main 函数中,我们创建了基类指针 shape1 和 shape2,分别指向派生类对象 Rectangle 和 Circle。通过调用 getArea() 函数,我们可以获得矩形和圆形的面积。

注意到,基类 Shape 是抽象类,因为它包含一个纯虚函数。抽象类不能被实例化,只能用作其他类的基类。而派生类必须实现纯虚函数,否则它们也会成为抽象类。

这篇关于学习c++的第十二天的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/350056

相关文章

C++实现回文串判断的两种高效方法

《C++实现回文串判断的两种高效方法》文章介绍了两种判断回文串的方法:解法一通过创建新字符串来处理,解法二在原字符串上直接筛选判断,两种方法都使用了双指针法,文中通过代码示例讲解的非常详细,需要的朋友... 目录一、问题描述示例二、解法一:将字母数字连接到新的 string思路代码实现代码解释复杂度分析三、

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

c++中std::placeholders的使用方法

《c++中std::placeholders的使用方法》std::placeholders是C++标准库中的一个工具,用于在函数对象绑定时创建占位符,本文就来详细的介绍一下,具有一定的参考价值,感兴... 目录1. 基本概念2. 使用场景3. 示例示例 1:部分参数绑定示例 2:参数重排序4. 注意事项5.

使用C++将处理后的信号保存为PNG和TIFF格式

《使用C++将处理后的信号保存为PNG和TIFF格式》在信号处理领域,我们常常需要将处理结果以图像的形式保存下来,方便后续分析和展示,C++提供了多种库来处理图像数据,本文将介绍如何使用stb_ima... 目录1. PNG格式保存使用stb_imagephp_write库1.1 安装和包含库1.2 代码解

C++实现封装的顺序表的操作与实践

《C++实现封装的顺序表的操作与实践》在程序设计中,顺序表是一种常见的线性数据结构,通常用于存储具有固定顺序的元素,与链表不同,顺序表中的元素是连续存储的,因此访问速度较快,但插入和删除操作的效率可能... 目录一、顺序表的基本概念二、顺序表类的设计1. 顺序表类的成员变量2. 构造函数和析构函数三、顺序表