C++中的虚函数,纯虚函数,虚继承,虚析构函数详解

2023年2月12日 22:53 ry 653

最近在学C++,遇到许多有关虚字的名词,特此记录下,先来看虚函数,虚函数的应用场景是什么呢,通常发生在多态中,什么是多态呢,通俗的来将是为了实现某种功能,不同的对象可以表现出不同的状态。请看以下代码,

#include<iostream>
#include<stdio.h>
using namespace std;
/*
静态多态的地址早绑定->编译阶段确定函数地址
动态多态的地址晚绑定->运行阶段缺点函数地址
*/
class Animal
{
    public:
        void say()
        {
            cout<<"动物在说话"<<endl;
        }
};
class Dog:public Animal
{
    public:
        void say()
        {
            cout<<"狗在说话"<<endl;
        }
};
void work(Animal *animal)
{
    animal->say();
}
int main()
{
  
    Dog dog = Dog();
    work(&dog);
    return 0;
}

创建了一个Animal类,其成员函数为say,子类Dog也有一个成员函数say,然后通过animal类指向dog对象。我们来看下这段程序的结果

PS C:\Users\14499\Desktop\vsProjects>  & 'c:\Users\14499\.vscode\extensions\ms-vscode.cpptools-1.13.9-win32-x64\debugAdapters\bin\WindowsDebugLauncher.exe' '--stdin=Microsoft-MIEngine-In-ahdubnzu.c2g' '--stdout=Microsoft-MIEngine-Out-db2dfebr.omi' '--stderr=Microsoft-MIEngine-Error-njlx5n3r.b01' '--pid=Microsoft-MIEngine-Pid-40upwgii.0fr' '--dbgExe=C:\Users\14499\Desktop\mingw64\bin\gdb.exe' '--interpreter=mi' 
动物在说话
PS C:\Users\14499\Desktop\vsProjects> 

这里执行了父类的成员函数,如何访问子类dog的成员函数呢,在父类成员函数加个关键字virtual,此时子类dog的成员函数重写或者覆盖了父类的成员函数。总之虚函数的目的是让父类可以访问子类的成员函数->实现重写->泛型编程->接口的统一性。那纯虚函数有是什么呢?抽象类由纯虚函数实现的,代码如下

#include<stdio.h>
#include<iostream>
using namespace std;
class Base
{
    public:
        virtual void func() = 0;//纯虚函数->只要有一个纯虚函数,这个类称为抽象类,无法实例化
                                //抽象类的子类,必须重写父类的纯虚函数,否则也为抽象类,无法实例化
};
class Son:public Base
{
    public:
        void func()
        {
            cout<<666<<endl;
        }

};
int main()
{
    Son s;
    s.func();
    return 0;
}

抽象类不能实例化,子类要想实例化必须重写纯虚函数。再来看虚继承,来看下案例。

像这种B,C继承A,DD继承B和C,一般也成为菱形继承。代码如下所示

#include<stdio.h>
#include<iostream>
using namespace std;
class A
{
    public:
        int a_ = 10;
};
class B: public A
{
    
};
class C: public A
{
    
};
class DD:public B,public C
{
    
        
};
int main()
{

    DD d = DD();
    
  
    cout<<d.a_<<endl;
    return 0;
}

这样输入a_属性会报错,因为不知道是B类还是C类的成员。当然,你可以直接使用类作用域来区分,像这样

cout<<d.B::a_<<endl;
cout<<d.C::a_<<endl;

但是为了更好的封装,一般在继承的公共父类前加上virtual关键字,如下所示

#include<stdio.h>
#include<iostream>
using namespace std;
class A
{
    public:
        int a_ = 10;
};
class B: virtual public A//虚继承
{
    
};
class C: virtual public A
{
    
};
class DD:public B,public C
{
    
        
};
int main()
{

    DD d = DD();
    
    
    cout<<d.a_<<endl;
    return 0;
}

我们来看下内部实现,我们利用vs的命令行工具来看,使用命令cl /d1 reportSingleClassLayoutDD "virtualinherit.cpp"来查看其结构,如图所示

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.34.31937 版
版权所有(C) Microsoft Corporation。保留所有权利。

virtualinherit.cpp
virtualinherit.cpp(1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失

class DD        size(12):
        +---
 0      | +--- (base class B)
 0      | | {vbptr}
        | +---
 4      | +--- (base class C)
 4      | | {vbptr}
        | +---
        +---
        +--- (virtual base A)
 8      | a_
        +---

DD::$vbtable@B@:
 0      | 0
 1      | 8 (DDd(B+0)A)

DD::$vbtable@C@:
 0      | 0
 1      | 4 (DDd(C+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A       8       0       4 0
D:\Program Files\vs2022\VC\Tools\MSVC\14.34.31933\include\ostream(287): warning C4530: 使用了 C++ 异常处理程序,但未启用展开语义。请指定 /EHsc
D:\Program Files\vs2022\VC\Tools\MSVC\14.34.31933\include\ostream(272): note: 在编译 类 模板 成员函数“std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(int)”时
virtualinherit.cpp(28): note: 查看对正在编译的函数 模板 实例化“std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(int)”的引用
virtualinherit.cpp(28): note: 查看对正在编译的 类 模板 实例化“std::basic_ostream<char,std::char_traits<char>>”的引用
Microsoft (R) Incremental Linker Version 14.34.31937.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:virtualinherit.exe
virtualinherit.obj

再对比下没用用virtual关键字的对应的内存结构

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.34.31937 版
版权所有(C) Microsoft Corporation。保留所有权利。

virtualinherit.cpp
virtualinherit.cpp(1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失

class DD        size(8):
        +---
 0      | +--- (base class B)
 0      | | +--- (base class A)
 0      | | | a_
        | | +---
        | +---
 4      | +--- (base class C)
 4      | | +--- (base class A)
 4      | | | a_
        | | +---
        | +---
        +---
Microsoft (R) Incremental Linker Version 14.34.31937.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:virtualinherit.exe
virtualinherit.obj

没用virtual时DD类有2份数据,使用了virtual通过vbptr指针指向一份数据,实现共用的效果,减少了内存的开销,我们用代码来验证看下改变类中的一个,另一个类中a_是否和此类中的a_一致,代码如下

#include<stdio.h>
#include<iostream>
using namespace std;
class A
{
    public:
        int a_ = 10;
};
class B: virtual public A//虚继承
{
    
};
class C: virtual public A
{
    
};
class DD:public B,public C
{
    
        
};
int main()
{

    DD d = DD();
    
    
    d.B::a_ = 20;
    d.C::a_ = 30;
    cout<<d.a_<<endl;
    return 0;
}

vscode运行结果如下

PS C:\Users\14499\Desktop\vsProjects>  & 'c:\Users\14499\.vscode\extensions\ms-vscode.cpptools-1.13.9-win32-x64\debugAdapters\bin\WindowsDebugLauncher.exe' '--stdin=Microsoft-MIEngine-In-ox4pnws1.01m' '--stdout=Microsoft-MIEngine-Out-gnqpfih5.mbz' '--stderr=Microsoft-MIEngine-Error-msnjczvp.zgb' '--pid=Microsoft-MIEngine-Pid-fbvxfvcz.0pt' '--dbgExe=C:\Users\14499\Desktop\mingw64\bin\gdb.exe' '--interpreter=mi'
30

得到验证!现在我们来看虚析构函数,代码如下

#include<iostream>
#include<stdio.h>
using namespace std;
//多态:把子类对象伪装成父类类型
//使用虚析构函数->多态中释放子类的堆区的数据
class Father
{
    public:
         ~Father();//虚析构
        Father();
      
        virtual void func() = 0;//抽象类
};
class Son:public Father
{
    public:

        Son(string name)
        {
            p_name  = new string(name);//从堆区开辟内存
            cout<<"son构造函数"<<endl;
        }
        ~Son()
        {
            cout<<"son析构函数"<<endl;
        }
        void func()
        {
            if(p_name !=NULL)//释放堆区数据
            {
                delete p_name;
                p_name = NULL;
            }
            cout<<"儿子"<<endl;
        }
        string *p_name;
};
Father::Father()
{
    cout<<"父类构造函数"<<endl;
}
Father::~Father()
{
    cout<<"父类析构函数"<<endl;
}
int main()
{
    //多态特性
    Father *f = new Son("lyp");//堆区开辟内存
    f->func();
    delete f;

    return 0;
}

运行结果如下所示

父类构造函数
son构造函数
儿子
父类析构函数

可以看见,在多态中无法释放子类中的堆区数据,容易造成内存泄露,因此必须使用virtual关键字,注意必须在父类的析构函数前面加上virtual,如下所示

#include<iostream>
#include<stdio.h>
using namespace std;
//多态:把子类对象伪装成父类类型
//使用虚析构函数->多态中释放子类的堆区的数据
class Father
{
    public:
        virtual ~Father();//虚析构
        Father();
      
        virtual void func() = 0;//抽象类
};
class Son:public Father
{
    public:

        Son(string name)
        {
            p_name  = new string(name);//从堆区开辟内存
            cout<<"son构造函数"<<endl;
        }
        ~Son()
        {
            cout<<"son析构函数"<<endl;
        }
        void func()
        {
            if(p_name !=NULL)//释放堆区数据
            {
                delete p_name;
                p_name = NULL;
            }
            cout<<"儿子"<<endl;
        }
        string *p_name;
};
Father::Father()
{
    cout<<"父类构造函数"<<endl;
}
Father::~Father()
{
    cout<<"父类析构函数"<<endl;
}
int main()
{
    //多态特性
    Father *f = new Son("lyp");//堆区开辟内存
    f->func();
    delete f;

    return 0;
}

 

如果上述代码帮助您很多,可以打赏下以减少服务器的开支吗,万分感谢!

欢迎发表评论~

点击此处登录后即可评论


评论列表
暂时还没有任何评论哦...

赣ICP备2021001574号-1

赣公网安备 36092402000079号