[C++]虚函数用法

news/2024/7/24 8:13:07 标签: c++, 开发语言

讲虚函数之前先讲讲面向对象的三大特性:封装、继承、多态。

1、封装

封装是指将数据(属性)和操作数据的方法(函数)封装在一个单元中,这个单元就是类。封装的主要目的是隐藏类的内部实现细节,只暴露必要的接口给外部使用者。

优点:

  • 信息隐藏: 封装可以将类的内部细节隐藏起来,不暴露给外部,提高了安全性和防止误用。
  • 简化接口: 封装通过提供清晰的接口简化了类的使用,使用者只需关注如何使用接口而不需要了解内部实现。
  • 提高可维护性: 内部实现的修改不会影响外部使用者,从而提高了代码的可维护性。

示例:

#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int age;

public:
    // 构造函数
    Student(const std::string& n, int a) : name(n), age(a) {}

    // 获取姓名
    std::string getName() const {
        return name;
    }

    // 设置年龄
    void setAge(int a) {
        if (a >= 0) {
            age = a;
        }
    }

    // 显示学生信息
    void displayInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Student student("Alice", 20);
    
    // 使用公有接口获取和设置信息
    student.setAge(21);
    std::cout << "Student Name: " << student.getName() << std::endl;
    student.displayInfo();

    return 0;
}

2、继承

继承允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。通过继承,子类可以获得父类的特征,并可以添加新的特征或修改继承的特征。

优点:

  • 代码重用: 继承允许在不重复编写代码的情况下扩展和修改现有类,提高了代码的重用性。
  • 层次结构: 继承可以创建类的层次结构,使得代码更有组织性和可扩展性。
  • 多态性支持: 继承是多态性的基础,通过基类指针或引用调用派生类的方法实现多态行为。

示例:

#include <iostream>
#include <string>

// 基类
class Animal {
protected:
    std::string name;

public:
    Animal(const std::string& n) : name(n) {}

    void eat() {
        std::cout << name << " is eating." << std::endl;
    }
};

// 派生类
class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}

    void bark() {
        std::cout << name << " is barking." << std::endl;
    }
};

int main() {
    Dog myDog("Buddy");

    myDog.eat();  // 继承自基类
    myDog.bark(); // 派生类自己的方法

    return 0;
}

3、多态

多态性是指同一个操作可以作用于不同类型的对象,并且可以根据对象的类型执行不同的行为。多态性通过虚函数和函数重载实现。

  • 编译时多态性(静态多态性): 通过函数重载实现,编译器在编译时根据函数参数的类型和数量来选择调用合适的函数。这种多态性是在编译时解析的。
  • 运行时多态性(动态多态性): 通过虚函数和继承实现,允许在运行时根据对象的实际类型来调用适当的函数。这种多态性是在运行时解析的。

优点:

  • 灵活性: 多态性允许在不同的情境下以通用的方式处理不同类型的对象,提高了代码的灵活性。
  • 可扩展性: 可以轻松地添加新的派生类而不影响现有的代码,增加了系统的可扩展性。
  • 简化接口: 多态性简化了代码的接口,允许使用者按统一的方式与不同类型的对象交互。

示例:

#include <iostream>
#include <vector>

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a square." << std::endl;
    }
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle());
    shapes.push_back(new Square());

    for (Shape* shape : shapes) {
        shape->draw(); // 多态性:根据对象的实际类型调用适当的方法
    }

    // 释放内存
    for (Shape* shape : shapes) {
        delete shape;
    }

    return 0;
}

讲了这么多,进入今天主题吧,C++实现多态的虚函数。

在C++中,函数继承的方法可以让我们快速开发,为了满足多态和泛型编程,C++允许用户使用虚函数来完成运行时解析,与一般的编译时解析也有着本质区别。

4、虚函数在内存中的分布

对于C++了解的人都应该知道虚函数是通过一个虚函数表来实现的。在这个表中,主要是一个类的虚函数的地址表,这个表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样的话,在有虚函数的类的实例中,这个表被分配在这个实例的内存中。当我们用父类的指针操作其子类的时候,这个虚表就非常重要了,它指明了实际所应该调用的函数。

class A {
public:
    virtual void v_a(){}
    virtual ~A(){}
    int64_t _m_a;
};
int main()
{
    A* a = new A();
    return 0;
}

定义一个类A,那么它在内存中分布的情况是什么样的呢?接下来一起看看

  • 首先在主函数的栈帧上有一个 A 类型的指针指向堆里面分配好的对象 A 实例。
  • 对象 A 实例的头部是一个 vtable 指针,紧接着是 A 对象按照声明顺序排列的成员变量。(当我们创建一个对象时,便可以通过实例对象的地址,得到该实例的虚函数表,从而获取其函数指针。)
  • vtable 指针指向的是代码段中的 A 类型的虚函数表中的第一个虚函数起始地址。
  • 虚函数表的结构其实是有一个头部的,叫做 vtable_prefix ,紧接着是按照声明顺序排列的虚函数。
  • 注意到这里有两个虚析构函数,因为对象有两种构造方式,栈构造和堆构造,所以对应的,对象会有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行 delete 函数,会自动被回收。
  • typeinfo 存储着 A 的类基础信息,包括父类与类名称,C++关键字 typeid 返回的就是这个对象。
  • typeinfo 也是一个类,对于没有父类的 A 来说,当前 tinfo 是 class_type_info 类型的,从虚函数指针指向的vtable 起始位置可以看出。

5、虚函数表实现原理

虚函数表是一个指向虚函数的指针数组,每个带有虚函数的类都有一个对应的虚函数表。

虚函数指针

其本质就是一个指向函数的指针,与普通的函数指针并没有什么大的区别。它指向程序员自己定义的虚函数,当子类调用虚函数的时候,实际上就是通过调用这个虚函数指针从而找到接口。

虚函数指针是一个真实存在的数据类型,在对象实例化的时候,放在这个对象地址的首位,目的就是为了保证运行的快速性。与对象的成员函数不一样的是,虚函数指针对外部是完全不可见的,除非直接访问地址或者是debug模式,否则它是不能被外部调用的。

只有拥有虚函数的类才能拥有虚函数指针,每个虚函数都会对应一个虚函数指针。那么,拥有虚函数的类都会产生额外的开销,并且也会在一定程度上影响程序的运行速度。

虚函数表

当一个类包含虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针。这个指针通常位于对象的内存布局的开头(虚指针),它们按照一定的顺序组织起来就会构成一个表状结构,叫做虚函数表。虚函数表本身是一个全局的、类特定的数组,其中包含了该类中所有虚函数的地址。

先来定义一个基类:

class Panent
{
public:
    virtual void A(){cout<<"Panent::A"<<endl;}
    virtual void B(){cout<<"Panent::B"<<endl;}
    virtual void C(){cout<<"Panent::C"<<endl;}
};

对于基类Base的虚函数表记录的只有自己定义的虚函数。

下来再看看子类:

class Children: public Panent
{
public:
    virtual void A(){cout<<"Children::f"<<endl;}
    virtual void B1(){cout<<"Children::B1"<<endl;}
    virtual void C1(){cout<<"Children::C1"<<endl;}
}

最常见的继承,就是子类对基类的虚函数进行覆盖继承。

此时的虚函数表:

基函数的表项仍然会保留,而得到正确继承的虚函数的指针将会被覆盖,而子类自己的虚函数将跟在表后。

当多继承的时候,表项将会增多,顺序将会体现为继承的顺序,那么子类的虚函数就跟在第一个表项后。

C++中一个类是公用一个虚函数表的,基类有基类的虚函数表,子类有子类的虚函数表,这样极大的节省了内存。

虚表指针

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,在编译阶段,编译器在类中添加了一个指针 __vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表,__vptr一般在对象内存分布的最前面。

虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{“和”}"之前。 为了更好的理解这一问题, 我们可以把构造函数的调用过程细分为两个阶段,即:

  • 进入到构造函数体之前。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
  • 进入到构造函数体内。这一阶段是我们通常意义上说的构造函数。

带缺省参数的虚函数

当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。


http://www.niftyadmin.cn/n/5392091.html

相关文章

Git笔记——3

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、合并模式和分支策略 二、bug分支 三、强制删除分支 四、创建远程仓库 五、克隆远程仓库_HTTPS和_SSH 克隆远程仓库_HTTPS 克隆远程仓库_SSH 六、向远程仓库…

区块链笔记(三)

超级账本 如果说比特币为代表的加密货币提供了区块链技术应用的原型&#xff0c;以太坊为代表的智能合约平台延伸了区块链技术的适用场景&#xff0c;那么面向企业场景的超级账本项目则开拓了区块链技术的全新阶段。超级账本首次将区块链技术引入到了联盟账本的应用场景&#…

使用Outlook邮箱保护您的隐私

在数字时代&#xff0c;我们的电子邮件地址就像是我们的数字身份证&#xff0c;它连接着我们的个人信息和网络世界。无论是注册新服务、购物还是预订餐桌&#xff0c;电子邮件地址都是我们身份的关键部分。然而&#xff0c;这也使我们容易受到垃圾邮件和隐私泄露的影响。但是&a…

C#中的关键字params的用法

C#中有一个关键字params&#xff0c;它相对于一些主要关键字来说&#xff0c;还算是较为低频的&#xff0c;但也会用到。我们可以了解和学习下。 一、定义及约束 params关键字的作用在于可以让方法参数的数目可变。 params的参数类型必须是一维数组。 一旦在方法加入了para…

maven工程打包引入本地jar包

1、通过maven生成本地区仓库包 mvn install:install-file --settings D:\lkx\download\apache-maven-3.6.3\conf\settings.xml -Dfileaspose-cad-21.8.jar -DartifactIdaspose-cad -DgroupIdsystem.core -Dversion21.8 -Dpackagingjar -DgeneratePomtrue # --settings&#xf…

如何连接ACL认证的Redis

点击上方蓝字关注我 应用程序连接开启了ACL认证的Redis时与原先的方式有差别&#xff0c;本文介绍几种连接开启ACL认证的Redis的Redis的方法。 对于RedisACL认证相关内容&#xff0c;可以参考历史文章&#xff1a; Redis权限管理体系(一&#xff09;&#xff1a;客户端名及用户…

爬虫工作量由小到大的思维转变---<第四十八章 Scrapy 的请求和follow问题>

前言: 有时,在爬取网页的时候,页面可能只能提取到对应的url,但是具体需要提取的信息需要到下一页(url)里面; 这时候,不要在中间件去requests请求去返回response; 用这个方法.... 正文: 在Scrapy框架内&#xff0c;如果你想从一个页面提取URL&#xff0c;然后跳转到这个URL以…

08-BL31对异常中断的支持

快速链接: . 👉👉👉 个人博客笔记导读目录(全部) 👈👈👈 付费专栏-付费课程 【购买须知】我的联系方式-自建交流群-学习群 【必看/必看!!】ATF架构开发精讲-专题目录👈👈👈【精选】ARMv8/ARMv9架构入门到精通-[目录] 👈👈👈BL31是ATF的runtime环境…