C++: 虚函数,一些可能被忽视的细节

C++: 虚函数,一些可能被忽视的细节

引言:关于C++虚函数,对某些细节的理解不深入,可能导致我们的程序无法按预期结果运行,或是表明我们对其基本原理理解不够透彻。本文详细解答以下几个问题:实现多态,忘记写virtual会怎么样?虚函数的默认参数可以重载吗?纯虚函数真的不能有实现吗?析构函数可以是纯虚函数吗?

1.1 虚函数是什么?

  • 虚函数是在基类中使用关键字virtual声明的函数,它在派生类中可以被重写,且在运行时根据对象的类型来调用相应的函数。

  • 虚函数的作用是实现多态。多态是指同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。多态分为编译时多态和运行时多态,编译时多态是指函数重载,运行时多态是指虚函数。

  • 虚函数动态绑定的实现原理:每个含有虚函数的类都有一个虚函数表,虚函数表中存储着虚函数的地址,当基类指针绑定了类对象后,通过类对象虚表指针指向的虚函数表找到虚函数的地址,然后调用对应的虚函数。

1.2 实现多态,忘记写virtual会怎么样?

如果忘记在派生类中写virtual关键字,那么就不会实现多态,而是静态绑定。因此以下例子中,调用的是基类的函数,而不是派生类的函数。

class Base {
public:
    void func() {
        std::cout << "Base func" << std::endl;
    }
};

class Derived : public Base {
public:
    void func() {
        std::cout << "Derived func" << std::endl;
    }
};

int main() {
    Base *b = new Derived();
    b->func();
    delete b;
    return 0;
}

输出:

Base func

为了避免这种情况发生,C++11中引入了override关键字对需要重写的函数进行声明,这样如果派生类中没有重写基类的函数,编译器就会报错。

1.3 虚函数的默认参数可以重载吗?

虚函数的默认参数不可以重载,因为虚函数的调用是在运行时确定的,而默认参数是在编译时确定的。从设计角度来说,这样做是合理的,如果虚函数的默认参数可以重载,那么在运行时,编译器就需要在运行时选择合适的默认参数,这样就会增加编译器的复杂度。因此,虚函数的默认参数不可以重载。


class Base {
public:
    virtual void func(int i = 0) = 0;
};

void Base::func(int i) {
    std::cout << "Base func: " << i << endl;
}

class Derived : public Base {
public:
    void func(int i = 2) {
        std::cout<< "Derived func: " << i << endl;
    }
};

int main() {
    Base *b = new Derived();
    b->func();
    delete b;
    return 0;
}

输出:

Base func: 0

1.4 纯虚函数是什么?

  • 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法,否则编译失败。

  • 纯虚函数的声明格式为:virtual 函数类型 函数名(参数表) = 0;,其中“= 0”是纯虚函数的标志,它告诉编译系统,该虚函数没有实现。

  • 含有纯虚函数的类是抽象类,抽象类是不能实例化的。

纯虚函数真的不能有实现吗?其实不然,纯虚函数是可以有自己的实现的,但是这个实现是在类外部实现的,而不是在类内部实现的。详见1.5中的代码实例。

1.5 析构函数可以是纯虚函数吗?

我们知道,析构函数是在对象销毁时调用的,而纯虚函数是没有实现的虚函数,含有纯虚函数的类是抽象类,那么,析构函数可以是纯虚函数吗?

程序验证如下:

class Base {
public:
    virtual ~Base() = 0;
};

Base::~Base() {
    std::cout << "Base destructor" << std::endl;
}

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor" << std::endl;
    }
};

int main() {
    Base *b = new Derived();
    delete b;
    return 0;
}

输出:

Derived destructor
Base destructor

结论:析构函数可以是纯虚函数,含有纯虚析构函数的类无法实例化。因为析构函数是在派生类析构函数调用之后才调用基类析构函数,而派生类析构函数在派生类对象销毁时才会调用,而派生类对象的销毁必须要调用基类的析构函数,因此基类析构函数必须要在类外提供定义。

如果不定义纯虚析构函数的实现,则会链接失败,报以下错误。

:(.text$_ZN7DerivedD1Ev[__ZN7DerivedD1Ev]+0x3e): undefined reference to `Base::~Base()'
collect2.exe: error: ld returned 1 exit status

为什么在类外部实现就可以呢?因为含有纯虚函数的类是抽象类,抽象类是不能实例化的,但是抽象类可以有指针和引用,因此,我们可以通过抽象类的指针或引用调用纯虚函数,但是如果纯虚函数没有实现,那么就会出现问题,因此,我们需要在类外部实现纯虚函数。

1.6 纯虚函数可以被显示调用吗?

派生类的成员函数可以通过限定函数id自由调用基类在类外定义的纯虚函数。

class Base {
public:
    virtual void func() = 0;
};

void Base::func() {
    std::cout << "Base func" << std::endl;
}

class Derived : public Base {
public:
    void func() {
        Base::func();
        std::cout << "Derived func" << std::endl;
    }
};

int main() {
    Base *b = new Derived();
    b->func();
    delete b;
    return 0;
}

输出:

Base func
Derived func

参考

posted on 2024-03-31 13:45  七昂的技术之旅  阅读(7)  评论(0编辑  收藏  举报

导航