virtual关键字

虚函数是通过虚函数表实现的,虚函数表实际上是一个函数指针数组,它保存了本类中的虚函数的地址。
虚函数表属于类中而不属于类的某个实例,所以不会为每个实例专门生成一个虚函数表,但每个类的实例中保存指向了这个虚函数表的指针(所以包含虚函数的对象的大小会增加一个指针的大小),而且这个指针保存在对象实例空间的最前面。

1. 在类的方法中使用:虚函数,纯虚函数,析构函数为虚函数

  • 虚函数的应用:多态

    C++的多态就是靠父类指针指向子类对象+虚函数来实现的,父类指针可以指向子类对象,反之则不能。
    父类指针指向子类对象,可以调用子类从父类继承来的那一部分,但如果父类中声明了virtual,则可以调用子类中的方法,这样就实现了多态。
    而子类指针指向父类对象,可能会调用到父类中没用的方法,因此这是不对的。
    例如:

    class a
    {
    public:
        int aa
    };
    class b:public a
    {
    public:
        int bb;
    };
    
    // 从内存的来看:
    // a占一个int数据大小(aa数据)
    // b占一个int数据大小(从a继承过来的aa数据)+占一个int数据大小(bb数据)
    // 定义一个基类类型的指针a *p, 这个指针指向的是a类型的数据
    
    // 当p指针指向派生类的时候,因为p是a类型的指针,所以*p只解释为a类型数据的长度,即占一个int数据大小(aa数据)
    // 因此,当基类的指针p指向派生类的时候,只能操作派生类中从基类中继承过来的数据。
    // 指向派生类的指针,因为内存空间比基类长,所以不允许派生类的指针指向基类。
    

    C++的多态性能解决基类指针不能操作派生类的数据成员的问题。

  • 纯虚函数

    virtual void func() = 0;
    
    1. 为了方便实用多态,常常需要在基类中定义虚拟函数。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出猴子、犀牛等子类,但动物本身生成对象明显不合理。

    2. 只要是拥有纯虚拟函数的类,就是抽象类,它们是不能够被实例化的(只能被继承)。如果一个继承类没有改写父类中的纯虚函数,那么他也是抽象类,也不能被实例化。

    3. 抽象类不能被实例化,不过我们可以拥有指向抽象类的指针,以便于操纵各个衍生类。

  • 析构函数为虚函数

    基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

    如果析构函数不被声明成虚函数,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

    所以,将析构函数声明为虚函数是十分必要的。

思考:

1. 静态函数可以被声明为虚函数吗?

静态函数不可以声明为虚函数。
static成员函数不属于任何类对象或类实例,所以即使给此函数加上virtual也是没有意义的,virtual必须有this的感念,根据this的实际类来决定如何调用
虚函数依靠vptr和vtable来处理,vptr是指向虚函数表的指针,在类的构造函数中创建生成,并且智能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。

2. 构造函数可以为虚函数吗

构造函数不可以声明为虚函数。同时除了inline之外,构造函数不允许使用其他任何关键字,原因如下:

虚函数表vtable是在编译阶段建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。
如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。因此,构造函数没有必要被声明为虚函数。

3. 虚函数可以为私有函数吗

基类指针指向继承类对象,则调用继承类对象的函数
继承类必须声明为Base类的友元,否则编译失败,编译器报错:ptr无法访问私有函数。当然,把基类声明为public,继承类为private,该问题就不存在了。

4. 虚函数可以被内联吗

inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略。

inline:

内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。

  • 头文件中不仅要包含inline函数的声明,还要包含inline函数的定义。

  • 如果定义的inline函数过大,为了防止生成的obj文件太大,编译器会忽略这里的inline声明。

  • 编译器需要把inline函数体替换到函数调用处,所以编译器必须要知道inline函数的函数体是啥,所以要将inline函数的函数定义和函数声明一起写在头文件中,便与编译器查找替换。

  • 可以在同一个项目的不同源文件内定义函数名相同,实现相同的inline函数。同一个inline函数可以多处声明和定义,但是必须要完全相同。

  • 定义在class声明内的成员函数默认是inline函数。

#include <iostream>
using namespace std;

class Animal
{
public:
	Animal(){cout << "Animal is construct ...\n";} // 构造函数不可以是虚函数
	virtual ~Animal(){cout << "Animal is deconstruct ...\n";} //析构函数可以是虚函数,必须有定义体
	virtual void run() {cout << "Animal is running ...\n";}
};

class Dog : public Animal
{
public:
	Dog() {cout << "Dog is construct ...\n";}
	virtual ~Dog() {cout << "Dog is deconstruct ...\n";}
	void run() {cout << "Dog is running ...\n";}
};

class Black
{
public:
	Black() {cout << "Black is construct ...\n";}
	virtual ~Black() {cout << "Black is deconstruct ...\n";}
	void run() {cout << "Black is running ...\n";}
};

class BlackDog : public Black, public Dog
{
public:
	BlackDog(int count) {cout << "BlackDog is construct ..., count is " << count << endl;}
	~BlackDog() {cout << "BlackDog is deconstruct\n";}
	void run() {cout << "BlackDog is running\n";}
};

int main()
{
    // 1. 第一部分
    //指针和普通对象的区别,指针在函数结束后不会调用析构函数,用指针new出来的对象, 必须进行手动delete. 析构函数不会帮你自动析构
    Animal * A = new Dog(); // 父类指针可以指向子类对象
    // Dog * ptrDog = new Animal(); // 子类指针不可以指向父类对象
    A->run(); //只有父类的为虚函数,才调用子类的
    // 第一部分结果:
    // Animal is construct ...
    // Dog is construct ...
    // Dog is running ...

    // 2. 第二部分
    Dog D;
    D.run();

    // 第二部分结果:
    // Animal is construct ...
    // Dog is construct ...
    // Dog is running ...
    // Dog is deconstruct ...
    // Animal is deconstruct ...

    // 3. 第三部分
    BlackDog* B = new BlackDog(5);
    B->run(); // 子类调用子类的函数
    delete B;
    B = NULL; //注意:如果没有置为空,还是指向原来的地址,会导致野指针的问题

    // 第三部分结果:
    // Black is construct ...
    // Animal is construct ...
    // Dog is construct ...
    // BlackDog is construct ..., count is 5
    // BlackDog is running
    return 0;
}

2. 类继承时使用

   在多重继承中,如果发生了如:类B继承类A,类C继承类A,类D同时继承了类B和类C。最终在类D中就有了两份类A的成员,这在程序中是不能容忍的。当然解决这个问题的方法就是利用虚继承。
C++编译系统在实例化D类时,只会将虚基类的构造函数调用一次,忽略虚基类的其他派生类(class B,class C)对虚继承的构造函数的调用,从而保证了虚基类的数据成员不会被多次初始化。

3. 动态绑定

静态类型: 对象在声明时采用的类型,在编译期既已确定;
动态类型: 通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定: 绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定: 绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

参考网站:
https://www.cnblogs.com/lizhenghn/p/3657717.html

① 变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。
② 变量声明:用于向程序表明变量的类型和名字。
③ 定义也是声明:当定义变量时我们声明了它的类型和名字。
④ extern关键字:通过使用extern关键字声明变量名而不定义它。
例如:extern int i; //声明
int i; //定义

posted on 2020-06-08 11:20  JJ_S  阅读(222)  评论(0编辑  收藏  举报