c++复习之语法特性::函数重写(override)、函数定义、函数重载与协变返回类型(covariant return type)

本文介绍了C++中的函数重载、重写和协变返回类型。函数重载要求同名不同参,而重写发生在继承关系中,需满足特定条件,包括返回值类型的一致或协变。协变返回类型允许子类重写函数时返回父类返回值类型的子类指针或引用,提高了代码的灵活性。此外,还讨论了重定义(重载)的情况,指出非虚函数在派生类中会被隐藏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在复习C++,对函数重载、函数重写、函数重定义与协变返回类型概念很不清晰,所以在此整理总结~~首先来看函数重载与函数重写


函数重载与函数重写

  • 函数重载
    1、两个函数在同一个作用域
    2、同名不同参(不同的参数类型、不同的参数个数、不同的参数顺序)

  • 函数重写
    1、两个函数不在同一个作用域(主要在继承关系中体现:分别在基类和派生类)
    2、子类重新定义父类中同名同参的虚函数


函数重写需要注意的条件

  • 基本条件
    1、派生类重写的函数与基类中被重写的函数都必须是virtual函数,子类中最好也写为虚函数
    2、参数必须保持一致,否则就是子类中自己的成员函数,未构成函数重写
    3、重写的函数与被重写的函数返回值类型相同,或者当返回指针或引用时,子类中重写的函数返回的指针或者引用就是父类中被重写函数所返回指针或引用的子类型(协同返回类型)
    4、子类中的函数与父类中的同名函数具有相同的参数和返回值类型时,如果一个是const函数,一个是非const函数,不构成重写
  • 注意
    1、重写的函数所抛出的异常必须和被重写的函数抛出的异常一致,或者是其子类
    2、重写的函数的访问修饰符可以不同于被重写的函数,如基类中被重写的函数访问修饰符为private,派生类中重写的函数的访问修饰符可以为public或protected
    3、静态方法不能被重写,因为static与virtual不能同时被使用
    4、子类中重写的函数virtual关键字可以加也可以不加, 本质上都是虚函数的属性,只要与基类中的相关函数构成重写关系。
class A{}; 
class B:public A{};

class C
{
public:
    virtual void func1()
    { cout << "C::func1()" << endl;}

    virtual A* func2()
    {
        cout << "C::func2()" << endl;
        return new A;
    } 

    virtual A& func3()
    {
        cout << "C::func3()" << endl;
        return *(new A);        
            // 这里返回的必须是一个堆对象(heap object),
            // 而不能是栈对象(stack object)或者叫局部对象
            // 因为栈对象会在函数退出时被销毁,皮之不存毛将焉附
            // 对象都不存在了,引用自然无所适从 
    }

    void showFunc4()
    {
        func4();
    }

    virtual void func5() const
    {
        cout << "C::func5()" << endl;
    }

    virtual A func6()
    {
        cout << "C::func6()" << endl;
    }

private:
    virtual void func4()
    {
        cout << "C::func4()" << endl;
    }
};

class D:public C
{
public:
    // 构成重写
    void func1()
    {
        cout << "D::func1()" << endl;
    }
    // 返回值的类型是父类返回值类型的子类的指针,构成重写
    B* func2()
    {
        cout << "D::func2()" << endl;
        return new B;
    } 
    // 返回值的类型是父类返回值类型的子类的引用,构成重写
    B& func3()
    {
        cout << "D::func3()" << endl;
        return *(new B);
    }

    // 可以对访问修饰符进行重写
    void func4()
    {
        cout << "D::func4()" << endl;
    }

    // 不构成重写
    void func5() 
    {
        cout << "D::func5()" << endl;
    }
    // 无法通过编译 
    //B func6()
    //{ 
    //  cout << "D::func6()" << endl;
    //}
};

int main(int, char**)
{
    C* c = new D;       // 父类指针指向子类对象
    c->func1();
    c->func2();
    c->func3();
    c->showFunc4();
    c->func5();
    return 0;
}

协变返回类型

有了上述的准备,我们便比较容易理解作为C++函数重写机制中的一个小的环节的协变返回类型了,上述的D::func2(), D::func3()正是对这一语法机制的运用。

我们来看一个更加具体的例子,一般来说,子类对父类某一函数进行重写,必须具备相同的返回值类型。

class Shape
{
public:
    virtual double area() const = 0;
}

class Circle :public Shape
{
public:
    float area() const;     // 错误,返回值类型不同
}

这一规则对covariant return type却有所放松,也即,子类对父类的某一函数进行重写时,子类函数的返回值类型可以父类函数返回值类型的子类的指针或者引用类型,语义上要求子类与父类能够构成一种is-a关系。

class Shape
{
public:
    virtual Shape* clone() const = 0;
}

class Circle:public Shape
{
public:
    Circle* clone() const;
}

重写的派生类函数被声明为返回一个Circle*而不是继续Shape*,因为语义上,Circle是一个Shape。这在什么情况下会产生便捷性呢?直接操纵派生类类型而不是通过基类接口来操纵它们时,协变返回类型的优势便会体现出来:

Circle* c = getACircle();
Circle* c2 = c->clone();
c2->someFunc();     
        // 如此不经过类型转换我们便可实现对子类函数的调用 

如果不使用协变返回类型机制,或者不存在协变返回类型机制,Circle::clone将不得不精确地匹配Shape::clone的返回类型,从而返回一个Shape*,我们就必须被迫将结果转换为Circle*。

Circle* c = getACircle();
Circle* c2 = dynamic_cast<Circle*>(c->clone());

再看另外一个例子,考虑Shape中的Factory Method成员,它返回一个引用,指向与具体的形状对应的形状编辑器:

class ShapeEditor{};
class Shape
{
public:
    const ShapeEditor& getEditor() const = 0;   
                // Factory method
};

class CircleEditor:public ShapeEditor{};

class Circle: public Shape
{
public:
    const CircleEditor& getEditor() const {}
}

协变返回类型的最大或者说根本优势在于,让程序员在适度程度的抽象层面工作。如果我们是处理Shape,将获得一个抽象的ShapeEditor;若正在处理某种具体的形状类型,比如Circle,我们便可直接获得CircleEditor。协变返回类型将使我们从这样的一种处境中解脱出来:不得不使用易于出错的动态类型转换操作(dynamic_cast)

Shape* s = getAShape();
const ShapeEditor& se = s->getEditor();
Circle* c = getACirle();
const CircleEditor& ce = c->getEditor();

一个协变返回类型的实例

enum AttrType { Unknown, Continuous, Discrete};
class AttrInfo
{
public:
    AttrInfo(const std::string& name, AttrType type)
        :_type(type), _name(name)
    {}
    virtual AttrInfo* clone() const = 0;
private:
    AttrType _type;
    std::string _name;
};

class CAttrInfo :public AttrInfo
{
public:
    // ...
    CAttrInfo* clone() const
    {
        return (new CAttrInfo(*this));
    }
    // ...
} 

class DAttrInfo :public AttrInfo
{
public:
    // ...
    DAttrInfo* clone() const
    {
        return (new DAttrInfo(*this));
    }
    // ...
}

如上述代码所示,父类(抽象基类)中定义的被重写的函数返回类型是AttrInfo*(父类类型的指针),而两个派生类各自对clone函数的重写版本返回类型为CAttrInfo*和DAttrInfo*,因为有了协变返回类型(covariant return type)的存在,这样的语法特性才得以支持。

重定义(redefining)

  • 特点
    1、两个函数不再同一个作用域(分别在基类和派生类)
    2、函数名相同
    3、在基类和派生类只要不构成重写就是重定义
    4、也叫同名隐藏,子类重新定义父类的非虚函数,屏蔽了父类的同名函数

  • 基本条件
    函数与被隐藏的函数作用域不同,也就是函数在子类中声明,被其隐藏的函数在父类中声明

  • 注意
    同名不同参,无论父类的函数是否是virtual,都将被隐藏
    同名同参,如果父类的函数不是virtual,将被隐藏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值