首页 > 编程语言 >C++虚函数、虚继承:virtual

C++虚函数、虚继承:virtual

时间:2023-08-23 22:22:49浏览次数:35  
标签:函数 派生类 virtual class C++ 当中 基类 public

​1.引子

在类的继承当中曾经出现过这样一种情况:B、C继承自A,D继承自B和C。

 

之前提到过,这种情况下,关于类A当中的内容,会被复制成两份给到D,当进行访问的时候,需要指定C或者B,才能够定位到A当中的变量是来自哪里。就像下面这样。

 代码表示:

class A {
public:
    A(int a) : m_A(a) {}
    int m_A;
};
class B :  public A {
public:
    B(int a) : A(a) {}
};
class C :  public A {
public:
    C(int a) : A(a) {}
};
class D : public B, public C {
public:
    D(int a = 1) : B(a),C(a) {}
};
int main() {
    D d;
    //std::cout << "size of D.m_A is " << d.m_A << endl; d.m_A++;
    std::cout << "size of D.B::m_A is " << d.B::m_A << endl; d.B::m_A++;    // 输出1
    std::cout << "size of D.C::m_A is " << d.C::m_A << endl; d.C::m_A++;    // 因为与B中继承的A不是同一参数,因此++无效,输出1
    std::cout << "size of D.A::m_A is " << d.A::m_A << endl;    //依据类的构造顺序,决定直接访问A时指定的位置,这里B类先构造,所以默认是指向B当中的A,++后输出2
}

需要注意的是,可以直接使用d.A::m_A来访问子类当中的成员,这是依据类的构造顺序,决定直接访问A时指定的位置,这里B类先构造,所以默认是指向B当中的A。

2.虚继承

通常在上面的情况下,我们是不希望从A当中得到两个衍生变量的,只想得到其中的一个。那么虚继承就是用来解决这个问题的。

虚继承的写法是在常规继承的优先级前面,加上virtual关键字。

class B : virtual public A

B和C分别虚继承A,就可以使得后面所有同时继承B、C的子类当中,只会保留一份A的内容。

class A {
public:
    A(int a) : m_A(a) {}
    int m_A;
};
class B : virtual public A {
public:
    B(int a) : A(a) {}
};
class C : virtual public A {
public:
    C(int a) : A(a) {}
};
class D : public B, public C {
public:
    D(int a = 1) : B(a),C(a),A(a) {}    //注意,这里需要对A进行额外构造
};
int main() {
    D d;
    std::cout << "size of D.m_A is " << d.m_A << endl; d.m_A++;    // 输出1
    std::cout << "size of D.B::m_A is " << d.B::m_A << endl; d.B::m_A++;    // 输出2
    std::cout << "size of D.C::m_A is " << d.C::m_A << endl; d.C::m_A++;    // 输出3
    std::cout << "size of D.A::m_A is " << d.A::m_A << endl;    // 输出4
}

因此在上面这个例子当中,每次的++都是对同一变量的操作,因此会分别输出1、2、3、4.

注意:一旦使用了虚继承,那么必须在后面的子类当中,都对虚继承的基类进行构造。

3.虚函数

virtual用以修饰继承,就是虚继承。如果用来修饰函数,那么就是虚函数,它的基本格式如下。

class test{
    virtual void Function()
};

虽然都使用了virtual关键字,但它们解决的根本问题并不一样。

虚函数最主要的作用是C++多态性的表现,即同一事件(函数),发生在不同对象(类)当中,会呈现不同的特性。和函数重载类似,只不过多态性是在不同类当中的相同函数,而实现的方法,则是使用类指针。

3.1.类指针

如果在基类和派生类当中,都存在一个名称和参数完全相同的函数,在派生类当中就会确实地存在两个相同的函数。派生类直接调用就是自己的函数,指定基类就可以调用基类的函数,然而这样一种用法并不方便,于是我们会使用类的指针。

通常,对于引用和指针来说,必须要求参数类型完全一样。

int a;
double* p_d = &a;    //错误,不允许不同类型的转换
char& c = a;    //错误,不允许不同类型的转换

对于类来说,基类和派生类的指针和引用,是可以互相转换的。

将派生类引用或指针转换为基类引用或指针被称为向上转换

class Base{
};
class Drived : public Base{
};
Drived d;
Base * p_b = &d;
Base & r_b = d;

将基类引用或指针转换为派生类引用或指针被称为向下转换但是向下转换必须使用显式的转换。

class Base{
};
class Drived : public Base{
};
Base d;
Drived * p_d = (Drived*)&b;
Drived & r_d = (Drived&)b;

那么相互转换的意义在哪里呢?向下转换意义不是很大,但通过向上转换,即基类指针指向派生类对象,可以调用让基类指针对象调用派生类当中的函数,这个过程也被叫动态绑定。

3.2.静态绑定和动态绑定

程序执行时,调用哪段函数块的过程,被称为函数的绑定

在C语言当中比较简单,全部都会在编译时完成,这就被称为静态绑定

对于C++来说,因为有重载和虚函数的存在,想要找到对应的函数块就没那么简单,而有时候则会在程序运行时,根据场景选择不同的函数块,这就被称为动态绑定。

class Base {
public:
    virtual void Fun1(void) {
        std::cout << "Base::Fun1..." << endl;
    }
    void Fun2(void) {
        std::cout << "Base::Fun2..." << endl;
    }
};
class Derived :public Base {
public:
    void Fun1(void) {
        std::cout << "Derived::Fun1..." << endl;
    }
    void Fun2(void) {
        std::cout << "Derived::Fun2..." << endl;
    }
};

int main() {
    Base* p_b;
    Derived d;

    p_b = &d;   
    p_b->Fun1();    // 基类指针指向派生类,虚函数调用派生类
    p_b->Fun2();    // 基类指针指向派生类,函数调用基类
    return 0;
}

在上面的例子当中,Fun1在基类当中是虚函数,Fun2不是,因此在使用基类指针指向派生类对象方法的调用中,调用的是派生类的Fun1和基类的Fun2.

3.3.纯虚函数和抽象类

不是所有时候,基类的函数都需要实现。

例如这种情况,对于不同的图形,定义一个基类shape,其中包含了一个函数draw用以绘制图形。然而在不确定图形是什么的情况下,draw没有任何意义。即一个函数在基类当中没有意义,但是在派生类当中存在意义,那么这样的基类函数就可以被定义为纯虚函数。

标准写法是,在函数后面写上'= 0 ',并且不给出函数实现:

class test{
    virtual void Fun() = 0;
};

则上面这个例子用代码可以这样实现:

class Shape {
public:
    virtual void Draw(void) = 0;
};
class Circle :public Shape {
public:
    void Draw(void) {
        std::cout << "Draw::Circle..." << endl;
    }
};
class Square :public Shape {
public:
    void Draw(void) {
        std::cout << "Draw::square..." << endl;
    }
};

int main() {
    Shape* p_s;
    //Shape s;  //纯虚函数不允许直接作为对象
    Circle c;   Square sq;
    p_s = &c;
    p_s->Draw();    //绘制圆形
    p_s = &sq;
    p_s->Draw();    //绘制正方形
    return 0;
}

4.虚函数想要解决的问题

费了这么大劲了,虚函数到底是想要干什么?主要目的是为了代码后续的维护性。

在C语言当中,库文件都是依据编译好的,使用时需要包含这些库文件后,再将这些文件添加到程序内容当中。我们修改的部分是程序的框架,而里面的模块即库文件是写好的。

虚函数带来这样一种用法,我们可以先将程序的框架搭好,然后再填充库文件或配置文件,这样更加有利于模块化的开发过程,每个小组或成员只需要负责自己模块的那部分内容。

例如:使用Process类作为基类,run虚函数作为方法,在主函数当中读取配置文件,遍历调度表当中的类模块,并执行它们。这样只需要更改配置文件和各个模块,就可以实现程序的更改。

5.虚函数的注意事项

5.1.构造函数不能是虚函数

尽管声明的是基类指针,但是我们首先必须要有一个派生类的对象,这个对象在构造的时候,会自动调用基类的构造函数,因此将基类的构造函数声明为虚函数没有任何意义。

5.2.析构函数必须是虚函数

除非当前类不用作基类,否则析构函数尽量声明为虚函数。在删除基类指针时,如果基类的虚构函数不是虚函数,那么就无法调用到派生类的析构函数,可能会造成内存风险。

5.3.友元不能是虚函数

因为友元函数不是类成员,所以友元函数不能是虚函数。

5.4.不能重定义

如果基类的虚函数,在派生类当中没有重新定义,那么还是会使用基类当中的该函数。

5.5.如果不涉及到类的指针,那么定义虚函数在不具备太大的意义

标签:函数,派生类,virtual,class,C++,当中,基类,public
From: https://www.cnblogs.com/wyqmx/p/17652915.html

相关文章

  • C++类与对象(二)
    一、类的默认成员函数类内的默认成员函数:用户不显示实现,编译器就会自动生成的成员函数,被称为类的默认成员函数。这些默认成员函数各有各存在的作用。但实际上,很多时候,需要自己写这些成员函数,而不是使用编译器生成的。翻译一下就是,在类内有这样六个成员函数,如果你不写,编译器就会自动......
  • Citrix Virtual Apps and Desktops云桌面内网Storefront登录流程详解
    哈喽大家好,欢迎来到微信公众号虚拟化时代君(XNHCYL)。 大家好,我是虚拟化时代君,一位潜心于互联网的技术宅男。这里每天为你分享各种你感兴趣的技术、教程、软件、资源、福利……(每天更新不间断,福利不见不散)第1章前言本文主要介绍Citrix虚拟桌面通过CitrixStorefront(不过CitrixNe......
  • C++笔记
    C++笔记将数字以十六进制输出:cout<<hex<<100<<endl;将数字以八进制输出:cout<<oct<<100<<endl;精度控制include保存a位小数:setprecision(a)将b保留a位:cout<<setprecision(a)<<b<<endl将b保留小数点后a位:cout<<setiosflags(ios::fixed)<<se......
  • 成员函数
    一、对象的创建和销毁过程Ⅰ.对象的创建过程给对象划分内存空间执行初始化列表①根有参构造据继承表的顺序调用父类的无参构造或者通过:父类名(val)调用父类的有参构造②根据成员变量的定义顺序调用类类型成员的无参构造或有参构造通过:类类型命成员(val)调用类类......
  • C++面向对象笔记(转载自黑马程序员)
    C++核心编程本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。1内存分区模型C++程序在执行时,将内存大方向划分为4个区域代码区:存放函数体的二进制代码,由操作系统进行管理的全局区:存放全局变量和静态变量以及常量栈区:由编译器自动分配释放,存放函数的......
  • openGauss学习笔记-48 openGauss 高级数据管理-函数
    openGauss学习笔记-48openGauss高级数据管理-函数openGauss常用的函数如下:48.1数学函数abs(x)描述:绝对值。返回值类型:和输入相同。示例:openGauss=#SELECTabs(-17.4);abs------17.4(1row)cbrt(dp)描述:立方根。返回值类型:doubleprecision示例:openGauss......
  • c语言的可重入和不可重入函数
    先贴上一篇优秀的博文链接:C语言之可重入函数和不可重入函数_c可重入函数_KiranWang的博客-CSDN博客  总结:不可重入函数的四种情况1.静态数据结构:如静态局部变量活全局变量2.malloc()或者free()函数因为这两个函数都会操作全局的链表,如果第一次malloc没结束时,再被一次malloc......
  • 无涯教程-PHP Mock Test函数
    本节介绍了与PHP相关的各种模拟测试。您可以在本地计算机上下载这些样本模拟测试,并在方便时离线解决。每个模拟测试均随附一个模拟测试键,可让您验证最终分数并为自己评分。MockTestIMockTestIIMockTestIIIMockTestIVQ1-关于PHP,以下哪项是正确的?A-PHP是......
  • memmove函数
    参考引入问题intmain(){intarr1[10]={1,2,3,4,5,6,7,8,9,10};my_memcpy(arr1+2,arr1,20);for(inti=0;i<10;i++){printf("%d",arr1[i]);}return0;}上面一段代码我们的目的是想把到5的数字共20个字节拷贝到原来3到7的数字的位置上。然后我们看到其......
  • isEmpty工具函数
    在**项目开发中,需要在对未知数据类型做判空处理,期待空值列表:undefined、null、''、NaN、[]、{},注意非空:0、false;而常见的lodash.isEmpty,!value均不能直接满足我们的需求,那么我们需要抽离一个工具函数isEmpty; 接口返回表格字段的数据:表格中使用switch:0(或者false)......