为什么 C++ 支持带有实现的纯虚函数?
Why does C++ support for a pure virtual function with an implementation?
今天做了一个简单的测试:
struct C{virtual void f()=0;};
void C::f(){printf("weird\n");}
程序没问题,但我觉得很奇怪,当我们使用=0
时,意味着函数体应该定义在继承的类中,但似乎我仍然可以给它实现功能。
I tried both GCC and VC, both OK. So it seems to me this should be part of C++ standard.
But why this is not a syntax error?
我能想到的一个原因是像 C# 同时具有 'interface' 和 'abstract' 关键字,接口不能有实现,而抽象可以有一些实现。
我的困惑是这样吗,C++应该支持这种奇怪的语法?
必须定义析构函数,即使它是纯虚拟的。如果您不定义析构函数,编译器将生成一个。
编辑:你不能在没有定义的情况下声明析构函数,这将导致 link 错误。
您无论如何都可以从派生的 classes 调用函数体。
您可以实现纯虚函数的主体以提供默认行为,同时您希望派生 class 的设计者明确使用该函数。
基本上,两全其美(或最差...)。
派生的 class 需要实现纯虚方法,基 class 的设计者出于某种原因需要这样做。并且基础 class 还提供了此方法的默认实现,如果派生 class 需要或需要它,则可以使用它。
所以一些示例代码可能看起来像;
class Base {
public:
virtual int f() = 0;
};
int Base::f() {
return 42;
}
class Derived : public Base {
public:
int f() override {
return Base::f() * 2;
}
};
那么常见的用例是什么...
此技术的一个常见用例与析构函数有关 - 基本上,基 class 的设计者希望它是一个抽象 class,但是方法 none作为纯虚函数很有意义。析构函数是一个可行的候选者。
class Base {
public:
~Base() = 0;
};
Base::~Base() { /* destruction... */ }
其他人提到了与析构函数的语言一致性,所以我会从软件工程的角度出发:
这是因为您定义的 class 可能有一个有效的默认实现,但调用它是 risky/expansive/whatever。如果您不将其定义为纯虚拟,派生的 classes 将隐式继承该实现。并且可能永远不会知道直到 运行 时间。
如果将其定义为纯虚拟,则派生的 class 必须实现该功能。如果 risk/cost/whatever 没问题,它可以静态调用默认实现 Base::f();
重要的是,这是一个有意识的决定,而且是明确的。
必须在子classes 中覆盖纯虚函数。但是,您可以提供一个默认实现,它适用于子 classes,但可能不是最佳的。
构造的用例用于抽象形状,例如
class Shape {
public:
virtual Shape() {}
virtual bool contains(int x, int y) const = 0;
virtual int width() const = 0;
virtual int height() const = 0;
virtual int area() const = 0;
}
int Shape::area() const {
int a = 0;
for (int x = 0; x < width(); ++x) {
for (int y = 0; y < height(); ++y) {
if (contains(x,y)) a++;
}
}
return a;
}
面积法适用于任何形状,但效率极低。鼓励 Subclass 提供合适的实现,但如果有 none 可用,他们仍然可以显式调用基础 class 的方法
C++ 支持带有实现的纯虚函数,因此 class 设计人员可以强制派生 classes 覆盖函数以添加特定细节,但仍提供有用的默认实现,他们可以将其用作一个共同的基础。
经典示例:
class PersonBase
{
private:
string name;
public:
PersonBase(string nameIn) : name(nameIn) {}
virtual void printDetails() = 0
{
std::cout << "Person name " << name << endl;
}
};
class Student : public PersonBase
{
private:
int studentId;
public:
Student(string nameIn, int idIn) : PersonBase(nameIn), studentId(idIn) { }
virtual void printDetails()
{
PersonBase::printDetails(); // call base class function to prevent duplication
std::cout << "StudentID " << studentId << endl;
}
};
请注意,您不能实例化具有纯虚方法的对象。
尝试实例化:
C c;
用VC2015,如期出现错误:
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): error C2259: 'C': cannot instantiate abstract class
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: due to following members:
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: 'void C::f(void)': is abstract
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(6): note: see declaration of 'C::f'
回答你的问题:
该机制仅将函数声明为纯虚函数,但仍然存在虚函数 table 和基类。它会避免你实例化基类(C),但不会避免使用它:
struct D : public C { virtual void f(); };
void D::f() { printf("Baseclass C::f(): "); C::f(); }
...
D d;
d.f();
纯虚的意思"child must override"。
所以:
struct A{ virtual void foo(){}; };
struct B:A{ virtual void foo()=0; };
struct C:B{ virtual void foo(){}; };
struct D:C{ virtual void foo()=0; };
void D::foo(){};
struct E:D{ virtual void foo(){D::foo();}; };
A 有一个虚拟 foo。
B使它抽象。在创建实例之前,派生类型现在必须实现它。
C 实现它。
D 将其抽象化,并添加了一个实现。
E通过调用D的实现来实现。
A、C 和 E 可以创建实例。 B和D不能。
抽象与实现技术可用于提供部分或低效的实现,派生类型可以在想要使用它时显式调用它,但不要获得 "by default",因为这样做是不明智的。
另一个有趣的用例是 parent 接口在不断变化,而且代码库很大。它有一个功能齐全的实现。 Children 使用默认值的人必须重复签名并明确转发给它。想要覆盖的直接覆盖即可。
当基本 class 签名发生变化时,代码将无法编译,除非每个 child 显式调用默认值或正确覆盖。在 override
关键字之前,这是确保您不会意外创建新虚函数而不是覆盖 parent 的唯一方法,并且它仍然是在 [=] 中执行策略的唯一方法39=]类型。
今天做了一个简单的测试:
struct C{virtual void f()=0;};
void C::f(){printf("weird\n");}
程序没问题,但我觉得很奇怪,当我们使用=0
时,意味着函数体应该定义在继承的类中,但似乎我仍然可以给它实现功能。
I tried both GCC and VC, both OK. So it seems to me this should be part of C++ standard.
But why this is not a syntax error?
我能想到的一个原因是像 C# 同时具有 'interface' 和 'abstract' 关键字,接口不能有实现,而抽象可以有一些实现。
我的困惑是这样吗,C++应该支持这种奇怪的语法?
必须定义析构函数,即使它是纯虚拟的。如果您不定义析构函数,编译器将生成一个。
编辑:你不能在没有定义的情况下声明析构函数,这将导致 link 错误。
您无论如何都可以从派生的 classes 调用函数体。 您可以实现纯虚函数的主体以提供默认行为,同时您希望派生 class 的设计者明确使用该函数。
基本上,两全其美(或最差...)。
派生的 class 需要实现纯虚方法,基 class 的设计者出于某种原因需要这样做。并且基础 class 还提供了此方法的默认实现,如果派生 class 需要或需要它,则可以使用它。
所以一些示例代码可能看起来像;
class Base {
public:
virtual int f() = 0;
};
int Base::f() {
return 42;
}
class Derived : public Base {
public:
int f() override {
return Base::f() * 2;
}
};
那么常见的用例是什么...
此技术的一个常见用例与析构函数有关 - 基本上,基 class 的设计者希望它是一个抽象 class,但是方法 none作为纯虚函数很有意义。析构函数是一个可行的候选者。
class Base {
public:
~Base() = 0;
};
Base::~Base() { /* destruction... */ }
其他人提到了与析构函数的语言一致性,所以我会从软件工程的角度出发:
这是因为您定义的 class 可能有一个有效的默认实现,但调用它是 risky/expansive/whatever。如果您不将其定义为纯虚拟,派生的 classes 将隐式继承该实现。并且可能永远不会知道直到 运行 时间。
如果将其定义为纯虚拟,则派生的 class 必须实现该功能。如果 risk/cost/whatever 没问题,它可以静态调用默认实现 Base::f();
重要的是,这是一个有意识的决定,而且是明确的。
必须在子classes 中覆盖纯虚函数。但是,您可以提供一个默认实现,它适用于子 classes,但可能不是最佳的。
构造的用例用于抽象形状,例如
class Shape {
public:
virtual Shape() {}
virtual bool contains(int x, int y) const = 0;
virtual int width() const = 0;
virtual int height() const = 0;
virtual int area() const = 0;
}
int Shape::area() const {
int a = 0;
for (int x = 0; x < width(); ++x) {
for (int y = 0; y < height(); ++y) {
if (contains(x,y)) a++;
}
}
return a;
}
面积法适用于任何形状,但效率极低。鼓励 Subclass 提供合适的实现,但如果有 none 可用,他们仍然可以显式调用基础 class 的方法
C++ 支持带有实现的纯虚函数,因此 class 设计人员可以强制派生 classes 覆盖函数以添加特定细节,但仍提供有用的默认实现,他们可以将其用作一个共同的基础。
经典示例:
class PersonBase
{
private:
string name;
public:
PersonBase(string nameIn) : name(nameIn) {}
virtual void printDetails() = 0
{
std::cout << "Person name " << name << endl;
}
};
class Student : public PersonBase
{
private:
int studentId;
public:
Student(string nameIn, int idIn) : PersonBase(nameIn), studentId(idIn) { }
virtual void printDetails()
{
PersonBase::printDetails(); // call base class function to prevent duplication
std::cout << "StudentID " << studentId << endl;
}
};
请注意,您不能实例化具有纯虚方法的对象。
尝试实例化:
C c;
用VC2015,如期出现错误:
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): error C2259: 'C': cannot instantiate abstract class
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: due to following members:
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: 'void C::f(void)': is abstract
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(6): note: see declaration of 'C::f'
回答你的问题: 该机制仅将函数声明为纯虚函数,但仍然存在虚函数 table 和基类。它会避免你实例化基类(C),但不会避免使用它:
struct D : public C { virtual void f(); };
void D::f() { printf("Baseclass C::f(): "); C::f(); }
...
D d;
d.f();
纯虚的意思"child must override"。
所以:
struct A{ virtual void foo(){}; };
struct B:A{ virtual void foo()=0; };
struct C:B{ virtual void foo(){}; };
struct D:C{ virtual void foo()=0; };
void D::foo(){};
struct E:D{ virtual void foo(){D::foo();}; };
A 有一个虚拟 foo。
B使它抽象。在创建实例之前,派生类型现在必须实现它。
C 实现它。
D 将其抽象化,并添加了一个实现。
E通过调用D的实现来实现。
A、C 和 E 可以创建实例。 B和D不能。
抽象与实现技术可用于提供部分或低效的实现,派生类型可以在想要使用它时显式调用它,但不要获得 "by default",因为这样做是不明智的。
另一个有趣的用例是 parent 接口在不断变化,而且代码库很大。它有一个功能齐全的实现。 Children 使用默认值的人必须重复签名并明确转发给它。想要覆盖的直接覆盖即可。
当基本 class 签名发生变化时,代码将无法编译,除非每个 child 显式调用默认值或正确覆盖。在 override
关键字之前,这是确保您不会意外创建新虚函数而不是覆盖 parent 的唯一方法,并且它仍然是在 [=] 中执行策略的唯一方法39=]类型。