[C++学习笔记] 虚函数用法及注意事项

近期,由于经常会用到虚函数,加上太久没有实践用过此部分的知识,对概念和用法有些混淆,特写此篇来总结归纳一下虚函数的要点和注意事项,以加深自己对虚函数的印象,并希望此文能为他人解决疑惑。

参考书籍:《C++ Primer Plus》


虚函数初步认识

虚函数在C++中用来实现多态性,可以用指向基类的指针或引用访问派生类同名覆盖函数。这也便实现了同一个指针或引用,当指向不同的类是,调用不同的方法。


注意事项

1、在基类方法的声明中使用关键字 virtual 可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
2、如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法,称为动态联编或晚期联编。这样基类指针或引用可以指向派生类对象。

示例:

#include <iostream>
using std::cout;
class  A {
    public:
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
};
class B: public A {
    public:
        virtual void Print() {  // 该处的关键字virtual可以省略
            cout << "Print of Class B.\n";
        }
};
int main() {
    A testA;
    B testB;
    A *p;

    p = &testA;
    p->Print();
    p = &testB;
    p->Print();

    return 0;
}

输出结果:

  • Print() 为虚函数时
Print of Class A.
Print of Class B.
  • Print() 不是虚函数时(将A类和B类的 Print() 函数,即第5行和11行前面的关键字 virtual 去掉)
Print of Class A.
Print of Class A.

3、如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。


其他相关知识

介绍对于类成员函数中的不同函数,当他们被定义为虚函数时的不同特点,及特殊情况的讨论,大致分为如下几种:

  • 构造函数
  • 析构函数
  • 友元函数
  • 派生类没有重新定义虚函数
  • 派生类重新定义虚函数将隐藏基类方法

下面具体来说

1、构造函数

构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后派生类的构造函数将使用基类的一个构造函数(基类如未自定义任何构造函数,则将调用编译器提供的默认构造函数),这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚函数无意义。

2、析构函数

派生类的基类的析构函数应当是虚函数。

示例:

#include <iostream>
using std::cout;
class  A {
    public:
        virtual ~A() {
            cout << "Destruction of Class A.\n";
        }
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
};
class B: public A {
    public:
        ~B() {
            cout << "Destruction of Class B.\n";
        }
        virtual void Print() {
            cout << "Print of Class B.\n";
        }
};
int main() {
    A *p = new B;
    p->Print();
    delete p;

    return 0;
}

输出结果:

  • 析构函数为虚函数时
Print of Class B.
Destruction of Class B.
Destruction of Class A.
  • 析构函数不是虚函数时(将A类的析构函数,即第5行前面的关键字 virtual 去掉)
Print of Class B.
Destruction of Class A.

出现这种现象的原因是基类的析构函数是否为虚函数将导致析构函数的调用机制不同。

① 基类的析构函数没有定义为虚函数(第5行,关键字 virtual 去掉)

将首先调用 ~A() 析构函数,这将释放B类对象中(此处示例为testB)中的A类部分指向的内存(因为A类是B类的基类,这里可以想象为一个包含关系,即A类包含于B类,释放的是B类中的属于A类的部分),但是不会释放派生类的类成员指向的内存,也即不会调用 ~B() 析构函数。

                        --------------------------------
                        | B                            |
                        |       ------------------     |
                        | 不释  | A                |    |
                        | 放这  | 释放这里          |    |
                        | 一块  -------------------     |        
                        |                              |
                        --------------------------------

因此输出中没有 “Destruction of Class B.”

② 基类的析构函数定义为虚函数

此时,上述代码将先调用 ~B() 析构函数释放由B类组件指向的内存,然后调用 ~A() 析构函数来释放由 A类组件指向的内存,所以输出结果中含有“Destruction of Class B.“ 和 ”Destruction of Class A.”,且析构函数调用顺序为先B后A。

另外,给类定义一个虚析构函数并非错误,即使这个类不用做基类。这只是一个效率的问题(书上如是说)。

3、友元

友元不能是虚函数,因为友元不是类成员,而只有类成员才能是虚函数。如果由于这个原因引起了设计问题,可以让友元函数使用虚成员函数来解决。

示例:

#include <iostream>
using namespace std;
class  A {
    public:
        virtual void Print() const {
            cout << "Print of Class A.\n";
        }
        friend ostream& operator<<(ostream& os, const A& m);
};
class B: public A {
    public:
        virtual void Print() const {
            cout << "Print of Class B.\n";
        }
};
ostream& operator<<(ostream& os, const A& m) {
    m.Print();
}
int main() {
    A testA;
    B testB;

    cout << testA;
    cout << testB;

    return 0;
}

/*
注意:
A类和B类的 Print() 函数,即第5行和12行后边的 const 不可省略。因为友元函数的第二个形参声明的为const引用,如果 Print() 函数不为 const,则不能保证友元函数调用过程中,类中的成员不被改变。出于此原因,编译器会报错。
*/

输出结果:

Print of Class A.
Print of Class B.
4、派生类没有重新定义虚函数

如果派生类没有重新定义虚函数,将使用该函数的基类版本。

示例:

#include <iostream>
using std::cout;
class  A {
    public:
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
};
class B: public A {
    public:
        // virtual void Print() {
        //     cout << "Print of Class B.\n";
        // }
};
int main() {
    A testA;
    B testB;
    A *p;

    p = &testA;
    p->Print();
    p = &testB;
    p->Print();

    return 0;
}

输出结果:

Print of Class A.
Print of Class A.

如果派生类位于派生链中,则将使用最新的虚函数版本。

示例:

#include <iostream>
using std::cout;
class  A {
    public:
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
};
class B: public A {
    public:
        virtual void Print() {
            cout << "Print of Class B.\n";
        }
};
class C: public B {
    public:
        // virtual void Print() {
        //     cout << "Print of Class C.\n";
        // }
};
int main() {
    A testA;
    B testB;
    C testC;
    A *p;

    p = &testA;
    p->Print();
    p = &testB;
    p->Print();
    p = &testC;
    p->Print();
    return 0;
}

输出结果:

Print of Class A.
Print of Class B.
Print of Class B.

C类继承了B类,B类继承了A类,但C类并没有重新定义虚函数 Print(),因此调用了最新的 Print() 版本,即B类中定义的 Print() 方法。

5、派生类重新定义虚函数将隐藏基类方法

派生类重新定义虚函数不会生成函数的两个重载版本,而是派生类重新定义的函数隐藏了基类的虚函数,即重新定义基类重载的方法不是重载。具体来说,如果派生类重新定义基类中的虚函数,无论函数参数列表是否与基类的函数参数列表相同,该操作将覆盖基类的所有同名函数。

示例:

#include <iostream>
using std::cout;
class  A {
    public:
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
        virtual void Print(int a) {
            cout << "Int: Print of Class A.\n";
        }
};
class B: public A {
    public:
        virtual void Print(int a, int b) {
            cout << "Double int: Print of Class B.\n";
        }
};
int main() {
    B testB;
    testB.Print();
    // testB.Print(10); // 也会报相同的错误,只是错误的最后一行变为 "1 provided"。
    return 0;
}

/*

编译报错:

Error:
error: no matching function for call to 'B::Print()'
note: candidate is:
note: virtual void B::Print(int, int)
note:   candidate expects 2 arguments, 0 provided

*/

这引出了一条经验规则,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。

class  A {
    public:
        virtual void Print();
        virtual void Print(int a);
        virtual void Print(int a, int b);
};
class B: public A {
    public:
        virtual void Print();
        virtual void Print(int a);
        virtual void Print(int a, int b);
};

如果只重新定义基类中虚函数的一个版本,则其他的版本将被隐藏,派生类将无法使用它们。

#include <iostream>
using std::cout;
class  A {
    public:
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
        virtual void Print(int a) {
            cout << "Int: Print of Class A.\n";
        }
};
class B: public A {
    public:
        virtual void Print() {
            cout << "Print of Class B.\n";
        }
};
int main() {
    B testB;
    testB.Print();  // Valid
    // testB.Print(10); // inValid
    return 0;
}

如果不需要修改基类中虚函数的代码,则在派生类中重新定义虚函数时,可直接调用基类版本。

示例:

#include <iostream>
using std::cout;
class  A {
    public:
        virtual void Print() {
            cout << "Print of Class A.\n";
        }
        virtual void Print(int a) {
            cout << "Int: Print of Class A.\n";
        }
        virtual void Print(int a, int b) {
            cout << "Double int: Print of Class A.\n";
        }
};
class B: public A {
    public:
        virtual void Print() {
            A::Print();
        }
        virtual void Print(int a) {
            A::Print(a);
        }
        virtual void Print(int a, int b) {
            A::Print(a, b);
        }

};
int main() {
    B testB;
    testB.Print();
    testB.Print(10);
    testB.Print(1, 2);
    return 0;
}

输出结果:

Print of Class A.
Int: Print of Class A.
Double int: Print of Class A.

本人目前对C++的认识仍有欠缺,还在不断探索,如有错误,请不吝指出,谢谢。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值