MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++非虚函数在继承中的表现

2022-06-153.6k 阅读

C++ 非虚函数在继承中的表现

非虚函数的基本概念

在 C++ 中,非虚函数是指那些在类中定义时没有使用 virtual 关键字修饰的成员函数。非虚函数为类提供了特定的行为实现,这些行为通常与类的具体特性紧密相关,而不是设计用于在派生类中被重写以实现多态行为。

例如,考虑一个简单的 Rectangle 类:

class Rectangle {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    // 非虚函数,计算矩形面积
    int calculateArea() {
        return width * height;
    }
};

在这个例子中,calculateArea 函数就是一个非虚函数。它基于 Rectangle 类自身的数据成员 widthheight 来计算矩形的面积。这个函数的行为是固定的,不会因为继承而发生改变。

继承中的非虚函数

当一个类继承自另一个包含非虚函数的类时,派生类会继承这些非虚函数的实现。这意味着派生类的对象可以直接调用这些非虚函数,就像调用自身定义的函数一样。

假设有一个 Square 类继承自 Rectangle 类:

class Square : public Rectangle {
public:
    Square(int side) : Rectangle(side, side) {}
};

在这个 Square 类中,虽然没有重新定义 calculateArea 函数,但由于它继承自 Rectangle 类,所以 Square 类的对象可以调用 calculateArea 函数。

int main() {
    Square s(5);
    int area = s.calculateArea();
    std::cout << "Square area: " << area << std::endl;
    return 0;
}

在上述代码中,Square 类的对象 s 调用了从 Rectangle 类继承而来的 calculateArea 函数,并且能够正确计算出正方形的面积(因为正方形是特殊的矩形,边长相等)。

非虚函数的调用绑定

在 C++ 中,非虚函数的调用是在编译时进行绑定的,也称为静态绑定。这意味着编译器在编译阶段就确定了调用哪个函数,而不是在运行时根据对象的实际类型来决定。

让我们通过一个稍微复杂一点的例子来理解这一点:

class Shape {
public:
    void draw() {
        std::cout << "Drawing a shape" << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Triangle : public Shape {
public:
    void draw() {
        std::cout << "Drawing a triangle" << std::endl;
    }
};

现在考虑以下代码:

void drawShape(Shape& shape) {
    shape.draw();
}

int main() {
    Circle c;
    Triangle t;
    drawShape(c);
    drawShape(t);
    return 0;
}

在这个例子中,drawShape 函数接受一个 Shape 类的引用作为参数,并调用其 draw 函数。由于 draw 函数是非虚函数,所以在编译时,编译器根据参数的类型 Shape& 确定调用 Shape 类的 draw 函数。因此,无论传递给 drawShape 函数的是 Circle 还是 Triangle 对象,输出结果都是 “Drawing a shape”。

非虚函数在继承体系中的特点

  1. 行为一致性:非虚函数为继承体系提供了一种固定的行为。无论派生类如何变化,非虚函数的行为始终保持一致。例如,在上述 RectangleSquare 的例子中,calculateArea 函数的计算逻辑在 RectangleSquare 类中是相同的,这保证了面积计算行为的一致性。
  2. 安全性:由于非虚函数的调用是在编译时绑定的,所以不存在运行时错误的风险。例如,不会因为对象类型的错误判断而调用错误的函数版本。这使得代码在某些情况下更加安全和可预测。
  3. 效率:静态绑定的非虚函数调用在执行效率上通常比虚函数调用更高。因为编译器可以在编译时进行优化,直接生成调用特定函数的机器码,而不需要在运行时进行额外的查找和判断。

隐藏与重写的区别(非虚函数相关)

在继承体系中,当派生类定义了一个与基类非虚函数同名的函数时,会发生隐藏现象,而不是重写。

重写(Override)是指派生类重新定义基类的虚函数,并且函数的签名(参数列表和返回类型)必须完全一致。重写是实现多态的关键机制,它依赖于运行时绑定。

而隐藏(Hide)则是指当派生类定义了一个与基类非虚函数同名的函数时,基类的同名函数在派生类中被隐藏。即使派生类的函数签名与基类不同,基类的函数也会被隐藏。

例如:

class Base {
public:
    void print(int num) {
        std::cout << "Base: " << num << std::endl;
    }
};

class Derived : public Base {
public:
    void print(const char* str) {
        std::cout << "Derived: " << str << std::endl;
    }
};

在这个例子中,Derived 类定义了一个与 Base 类中 print 函数同名但参数类型不同的函数。这导致 Base 类的 print 函数在 Derived 类中被隐藏。

int main() {
    Derived d;
    d.print("Hello");
    // 以下调用会报错,因为 Base 的 print 函数被隐藏
    // d.print(10);
    return 0;
}

如果要在 Derived 类中调用 Base 类被隐藏的 print 函数,可以使用作用域解析运算符 ::

int main() {
    Derived d;
    d.print("Hello");
    d.Base::print(10);
    return 0;
}

何时使用非虚函数

  1. 固定行为实现:当类的某个行为在整个继承体系中都应该保持一致,不会因为派生类的不同而改变时,适合使用非虚函数。例如,一个表示几何图形的类体系中,计算图形周长的某些基本算法可能在不同的图形类(如矩形、圆形等)中基于相同的数学原理,这种情况下可以使用非虚函数来实现周长计算。
  2. 安全性和效率优先:如果对函数调用的安全性和效率有较高要求,非虚函数是一个不错的选择。由于编译时绑定,非虚函数调用更加安全可靠,并且执行效率更高。例如,在一些性能关键的底层库代码中,非虚函数可以避免虚函数调用带来的额外开销。
  3. 明确的接口与实现:非虚函数可以用于提供明确的接口和实现。基类通过非虚函数定义了一个行为的具体实现,派生类继承这个实现,并且不期望对其进行修改。这种方式有助于保持代码的清晰性和可维护性。

非虚函数与虚函数的对比

  1. 调用绑定方式:非虚函数是静态绑定,在编译时确定调用的函数版本;虚函数是动态绑定,在运行时根据对象的实际类型来确定调用的函数版本。
  2. 多态性:非虚函数不支持多态,无论对象的实际类型是什么,只要调用的接口是基于基类类型,就会调用基类的非虚函数;虚函数支持多态,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用相应派生类的虚函数实现。
  3. 可重写性:非虚函数在派生类中不能被重写,只能被隐藏;虚函数在派生类中可以被重写,以实现不同的行为。
  4. 效率:一般来说,非虚函数的调用效率更高,因为不需要运行时的额外查找和判断;虚函数由于需要通过虚函数表进行动态查找,会有一定的性能开销。

非虚函数在复杂继承体系中的应用

在复杂的继承体系中,非虚函数的应用可以更加灵活和多样化。

考虑一个图形绘制的继承体系,假设有一个 GraphicObject 基类,然后有 RectangleCircleTriangle 等派生类。同时,还有一些更具体的派生类,如 FilledRectangleStrokedCircle 等。

class GraphicObject {
public:
    // 非虚函数,用于获取对象类型名称
    std::string getTypeName() {
        return "GraphicObject";
    }
};

class Rectangle : public GraphicObject {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    // 非虚函数,计算矩形面积
    int calculateArea() {
        return width * height;
    }
};

class FilledRectangle : public Rectangle {
private:
    Color fillColor;
public:
    FilledRectangle(int w, int h, Color c) : Rectangle(w, h), fillColor(c) {}
    // 非虚函数,获取填充颜色
    Color getFillColor() {
        return fillColor;
    }
};

在这个体系中,GraphicObjectgetTypeName 函数是一个非虚函数,它为所有派生类提供了一个获取对象类型名称的基本实现。虽然派生类可以根据需要隐藏这个函数,但默认情况下,所有对象都可以通过这个函数获取到基本的类型名称。

Rectangle 类的 calculateArea 函数也是非虚函数,它为矩形相关的派生类(如 FilledRectangle)提供了统一的面积计算行为。

FilledRectangle 类的 getFillColor 函数是其特有的非虚函数,用于获取填充颜色。

通过这样的设计,非虚函数在不同层次的类中发挥着不同的作用,既提供了统一的基础行为,又允许每个类有其特定的行为实现。

非虚函数在模板元编程中的应用

在模板元编程中,非虚函数也有着独特的应用。模板元编程是一种在编译期进行计算的技术,它利用模板实例化的过程来生成代码。

考虑一个简单的编译期计算阶乘的例子:

template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static const int value = 1;
};

class MathUtils {
public:
    // 非虚函数,返回编译期计算的阶乘结果
    int getFactorial(int n) {
        return Factorial<n>::value;
    }
};

在这个例子中,MathUtils 类的 getFactorial 函数虽然看起来是一个普通的非虚函数,但它利用了模板元编程在编译期计算阶乘的结果。这种方式结合了非虚函数的高效性和模板元编程的编译期计算能力。

注意事项

  1. 避免意外隐藏:在派生类中定义与基类非虚函数同名的函数时,要特别小心,避免意外隐藏基类的函数。如果确实需要改变函数行为,应该考虑使用虚函数进行重写,而不是隐藏。
  2. 一致性维护:由于非虚函数提供了固定的行为,在修改基类的非虚函数时,要确保所有依赖该行为的派生类都不会受到负面影响。这需要对整个继承体系有全面的了解。
  3. 权衡效率与灵活性:虽然非虚函数在效率上有优势,但在需要多态行为的场景下,虚函数是更好的选择。在设计类体系时,要根据具体需求权衡效率和灵活性。

综上所述,C++ 中的非虚函数在继承体系中有着独特的表现和应用场景。理解非虚函数的特性,包括调用绑定方式、隐藏现象、与虚函数的对比等,对于编写高效、可靠且易于维护的 C++ 代码至关重要。无论是在简单的类继承关系中,还是在复杂的继承体系和模板元编程中,非虚函数都能发挥其应有的作用,为开发者提供丰富的编程手段。