我们知道我们可以使用指向基类的指针来访问派生类中基类的重写虚函数。
以下是此类示例。
#include <iostream>
class shape {
public:
virtual void draw() {
std::cout << "calling shape::draw()\n";
}
};
class square : public shape {
public:
virtual void draw() {
std::cout << "calling square::draw()\n";
}
int area() {
return width*width;
}
square(int w) {
width = w;
}
square() {
width = 0;
}
protected:
int width;
};
class rect : public square {
public:
virtual void draw() {
std::cout << "calling rect::draw()\n";
}
int area() {
return width*height;
}
rect(int h, int w) {
height = h;
width = w;
}
protected:
int height;
};
int main() {
/*
shape* pshape[3] = {
new shape,
new square(2),
new rect(2, 3)
};
for (int i = 0; i<3; i++){
pshape[i]->draw();
}
*/
square* psquare = new rect(2, 3);
psquare->draw();
system("pause");
return 0;
}
pshape[i]
指针可以轻松访问虚拟函数draw()
。
现在为混乱的部分。 &#34;正方形&#34; class是&#34; rect&#34;的基类。类。因此,如果有一个方形*指针,它可以访问&#34; rect&#34;的draw()
函数。 class( square * psquare = new rect(2,3); ),输出为:
calling rect::draw()
Press any key to continue . . .
现在,如果我删除虚拟&#39; square :: draw()定义中的关键字,代码仍然编译,输出相同:
calling rect::draw()
Press any key to continue . . .
最后,如果我删除虚拟&#39;从基函数来看,psquare->draw()
的输出是:
calling square::draw()
Press any key to continue . . .
这让我很困惑。到底发生了什么?
square
是rect
类的父级,square
的{{1}}函数应该是虚拟的,以便让draw()
覆盖它。但它仍在编译并提供与虚拟化时相同的输出。rect
是所有基类,因此在形状中删除draw()的shape
关键字会导致错误。但是这没有发生,它正在编译并在调用virtual
时给出另一个输出calling square::draw()
。我可能错在很多事情上。请更正错误并告诉我这里究竟发生了什么。
答案 0 :(得分:3)
如果函数在基类中声明为virtual,则它在所有派生类中自动为虚拟,就像在其中放置virtual
关键字一样。请参阅C++ "virtual" keyword for functions in derived classes. Is it necessary?。
如果该函数不是虚函数,则调用哪个版本将取决于您调用它的指针的类型。在父类中调用成员函数是绝对正确的,因为派生类的每个实例都是其每个父类的实例。那里没有错误。
答案 1 :(得分:0)
首先考虑编译器看到的内容。对于给定的指针,它向上观察层次结构,如果它看到同一方法限定为virtual
,则发出动态运行时调用。如果他看不到virtual
那么发出的调用是对应于函数的“最低”定义的调用,直到当前类型(或从类型向上发现的第一个定义)。
因此,如果您有(假设您有Square *p = new Rectangle()
):
Shape { virtual draw() }
Square : Shape { virtual draw() }
Rectangle : Square { virtual draw() }
一切都很清楚,总是虚拟的。
如果你有:
Shape { virtual draw() }
Square : Shape { draw() }
Rectangle : Square { virtual draw() }
然后编译器看到Shape的绘制是虚拟的,那么调用将是动态的,并且将调用Rectangle :: draw。
如果你有:
Shape { draw() }
Square : Shape { draw() }
Rectangle : Square { virtual draw() }
然后编译器看到Shape的绘制是非虚拟的,那么调用将是静态的并且将调用Shape :: draw(或者未定义Base :: draw())。
对于层次结构中同一功能混合虚拟非虚拟的情况,可能会发生最糟糕的事情......通常应该避免混合。
答案 2 :(得分:0)
当您创建静态对象并调用其中一个函数时,您将获得该类的函数版本。编译器在编译时知道对象是什么类。
当您创建指向对象的指针或引用并调用非虚函数时,编译器仍假定它在编译时知道该类型。如果引用实际上是一个子类,则可能会得到该类的超类'版本。
当您创建对象的指针或引用并调用虚函数时,编译器将在运行时中查找对象的实际类型,并调用特定于该对象的函数的版本。一个非常重要的示例是,如果要通过基类指针销毁对象,则析构函数必须在基类中是虚拟的,否则您将不会调用正确版本的析构函数。通常,这意味着子类分配的任何动态内存都不会被释放。
也许考虑虚拟课程的实际目的会有所帮助。该关键字不是为了它自己而添加的!让我们稍微改变一下这个例子来处理圆形,椭圆形和形状。
这里有很多样板文件,但请记住:椭圆是一组点与两个焦点的平均距离,圆是一个椭圆,其焦点相同。
#include <cmath>
#include <cstdlib>
#include <iostream>
using std::cout;
struct point {
double x;
double y;
point(void) : x(0.0), y(0.0) {}
point( const point& p ) : x(p.x), y(p.y) {}
point( double a, double b ) : x(a), y(b) {}
};
double dist( const point& p, const point& q )
// Cartesian distance.
{
const double dx = p.x - q.x;
const double dy = p.y - q.y;
return sqrt( dx*dx + dy*dy );
}
std::ostream& operator<< ( std::ostream& os, const point& p )
// Prints a point in the form "(1.4,2)".
{
return os << '(' << p.x << ',' << p.y << ')';
}
class shape
{
public:
virtual bool is_inside( const point& p ) const = 0; // Pure virtual.
protected:
// Derived classes need to be able to call the default constructor, but no
// actual objects of this class may be created.
shape() { cout << "Created some kind of shape.\n"; }
// Destructors of any superclass that might get extended should be virtual
// in any real-world case, or any future subclass with a nontrivial
// destructor will break:
virtual ~shape() {}
};
// We can provide a default implementation for a pure virtual function!
bool shape::is_inside( const point& _ ) const
{
cout << "By default, we'll say not inside.\n";
return false;
}
class ellipse : public shape {
public:
ellipse( const point& p1, const point& p2, double avg_dist )
: f1(p1), f2(p2), d(avg_dist)
{
cout << "Ellipse created with focuses " << f1 << " and " << f2
<< " and average distance " << d << " from them.\n";
}
bool is_inside( const point& p ) const
{
const double d1 = dist( p, f1 ), d2 = dist( p, f2 );
const bool inside = d1+d2 <= d*2;
cout << p << " has distance " << d1 << " from " << f1 << " and "
<< d2 << " from " << f2 << ", whose average is "
<< (inside ? "less than " : "not less than ") << d << ".\n";
return inside;
}
protected: // Not part of the public interface, but circle needs these.
point f1; // The first focus. For a circle, this is the center.
point f2; // The other focus. For a circle, this is the center, as well.
double d; // The average distance to both focuses. The radius of a circle.
};
class circle : public ellipse {
public:
circle( const point& center, double r )
: ellipse( center, center, r )
{
cout << "Created a circle with center " << center << " and radius " << r
<< ".\n";
}
// Override the generic ellipse boundary test with a more efficient version:
bool is_inside( const point& p ) const
{
const double d1 = dist(p, f1);
const bool inside = d1 <= d;
cout << p << " has distance " << d1 << " from " << f1 << ", which is "
<< (inside ? "less than " : "not less than ") << d << ".\n";
return inside;
}
};
int main(void)
{
const circle c = circle( point(1,1), 1 );
const shape& s = c;
// These call circle::is_inside():
c.is_inside(point(0,0));
s.is_inside(point(0.5, 1.5));
dynamic_cast<const ellipse&>(s).is_inside(point(0.5,0.5));
// Call with static binding:
static_cast<const ellipse>(c).is_inside(point(0,0));
// Explicitly call the base class function statically:
c.ellipse::is_inside(point(0.5,0.5));
// Explicitly call the ellipse version dynamically:
dynamic_cast<const ellipse&>(s).ellipse::is_inside(point(0.5,0.5));
return EXIT_SUCCESS;
}
这个输出是:
Created some kind of shape.
Ellipse created with focuses (1,1) and (1,1) and average distance 1 from them.
Created a circle with center (1,1) and radius 1.
(0,0) has distance 1.41421 from (1,1), which is not less than 1.
(0.5,1.5) has distance 0.707107 from (1,1), which is less than 1.
(0.5,0.5) has distance 0.707107 from (1,1), which is less than 1.
(0,0) has distance 1.41421 from (1,1) and 1.41421 from (1,1), whose average is not less than 1.
(0.5,0.5) has distance 0.707107 from (1,1) and 0.707107 from (1,1), whose average is less than 1.
(0.5,0.5) has distance 0.707107 from (1,1) and 0.707107 from (1,1), whose average is less than 1.
您可能需要考虑一下如何将此扩展到三维,从笛卡尔坐标变为极坐标,添加其他类型的形状(如三角形),或将椭圆的内部表示更改为中心和轴。 / p>
设计师经常认为,因为一个圆可以用一个点和一个距离定义,一个椭圆可以用两个点和一个距离来定义,ellipse
类应该是circle
类的子类。添加第二个焦点的成员。这是一个错误,而不仅仅是象牙塔的数学迂腐。
如果对椭圆采用任何算法(例如通过使用圆锥曲线的方程式快速计算其与线的交点)并在圆上运行它,它仍然可以工作。在这里,我们使用了一个例子,通过使用圆圈只有一个焦点的知识来查找一个点在圆圈内是否快两倍。
但这不起作用!如果ellipse
继承自circle
并且您尝试draw_fancy_circle(ellipse(f1, f2, d));
,则会得到一个比您想要绘制的椭圆更大的圆。因为省略号违反了圆圈类的合同,所以任何假定圆圈的代码都是圆圈,在你重新编写它们之前会默默地发出错误。这样就无法编写一堆可以在任何类型的对象上运行的代码,并重新使用它。
square
班级与rectangle
之间的关系应该对读者留下什么影响。