你好,我是雨乐!
群里经常有这样一个现象,当有新人进群的时候,总会有个面试环节,经常问的一个问题就是std::string能否被继承,一开始可能是技术问题,后面多了,就被玩成了梗,不过梗归梗,今天借助这篇文章,聊聊继承相关的Mordern新特性--override和final
。这俩特性相对于其他引入的特性,性能上没有带来大的提升,唯一或者说比较重要的好处则是能让我们的程序在继承类和覆写虚函数时更安全,更清晰。
记得之前在实现某个功能的时候,发现预期输出与实际输出不相符,查了好久,最后才发现,在继承类中声明和定义的虚函数与父类中不是一个😁
通常情况下,我们会像如下这样写代码:
class Base {
public:
virtual void f() {
std::cout << "Base::f()";
}
};
class Derived : public Base {
public:
virtual void f() {
std::cout << "Derived::f()";
}
};
当然了,有时候也会写如下这种:
class Base {
public:
virtual void f() {
std::cout << "Base::f()";
}
};
class Derived : public Base {
public:
virtual void f() const {
std::cout << "Derived::f() const";
}
};
然后像往常一样去调用虚函数:
Base *b = new Derived;
b->f();
满心欢喜的以为会输出Derived::f() const,结果却输出Base::f()。emm,这是因为void f()和void f() const是两个不同的类型函数,子类中定义的void f() const并没有覆盖父类的void f(),这就是上面指向子类的指针调用输出的是父类函数的原因。
为了能尽早的发现问题所在,C++11引入了新的关键字override:
In a member function declaration or definition,
override
specifier ensures that the function is virtual and is overriding a virtual function from a base class. The program is ill-formed (a compile-time error is generated) if this is not true. override is an identifier with a special meaning when used after member function declarators: it's not a reserved keyword otherwise.
使用override来避免上述方式如下:
class Base {
public:
virtual void f() {
std::cout << "Base::f()";
}
};
class Derived : public Base {
public:
virtual void f() override {
std::cout << "Derived::f() const";
}
};
此时,如果像前面那样,在子类中新增一个const关键字,即:
class Derived : public Base {
public:
virtual void f() override {
std::cout << "Derived::f() const";
}
};
编译器会报错如下:
'virtual void Derived::f() const' marked 'override', but does not override
很简单吧,加个关键字,让编译器来检查我们又没有正确覆盖父类的虚函数,这样可以将很多问题暴露在编译阶段,何乐而不为呢~~
如果说override的出现是为了更好的为继承服务,那么final的出现则是为了结束继承。
回到我们文首的那道题目:std::string能否被继承,如果时间在2008年的话,单纯针对这个问题,我可能会回答是,如果是现在的话,可能会犹豫,毕竟Modern C++中新的关键字final的出现,称其为继承终结者也不为过哈哈。
不过,看了gcc11.2的源码,也尝试在本地对std::string继承尝试了下,是可以的(此处仅针对能否继承,撇开内存泄漏等其它因素哈)。至于为什么没有做限制,下面这个答案摘自SO,应该具有说服力:
The LWG discussed this issue at the recent meeting in Kona Feb. 6-10, 2012. This is LWG issue 2113. The LWG decided to mark LWG 2113 as NAD (not a defect), with the rationale that the standard is already clear that existing classes such as containers and
std::string
can not be marked final by the implementation. The discussion included the fact that while it may be frowned on to derive from such classes, it is clearly legal to do so in C++98/03. And making it illegal in C++11 would break far too much code.
其实,前面已经提到了,C++11中引入了final就是为了终结继承,不过这种也分为两种:函数和类。
如果想要一个函数不再被其子类覆盖,只需要在函数后加final即可:
class Base {
public:
virtual void f() {
std::cout << "Base::f()";
}
};
class Derived : public Base {
public:
void f() final {
std::cout << "Derived::f()";
}
};
此时,如果有另外一个类继承于Derived,且覆写了f()函数,即:
class Derived1 : public Derived {
public:
void f() {}
};
那么编译器则会报如下错误:
error: virtual function 'virtual void d::f()' overriding final function
如果想要一个类不被继承,则在该类的定义后面加上final即可:
class Base {
public:
virtual void f() {
std::cout << "Base::f()";
}
};
class Derived final : public Base {
public:
void f() final {
std::cout << "Derived::f()";
}
};
此时,如果某个类继承与Derived,则编译器会报如下错误:
error: cannot derive from 'final' base 'Derived' in derived type 'Derived1'
提到继承或者虚函数,很多人会想到性能差,虚函数表跳转等,其实,这个观点有点笼统或者说过于垄断,且看下面一个例子:
class Base {
public:
virtual void f() {
std::cout << "Base::f()";
}
};
class Derived : public Base {
public:
void f() final {
std::cout << "Derived::f()";
}
};
void Call(Derived& d) {
d.f();
}
在Call()函数中,其参数为const Derived类型引用,在函数体内直径调用该对象的f()函数,此时,不妨闭眼一分钟,看看这个时候会不会通过虚函数表跳转来调用相应的函数?
好了,且看下汇编的实现吧(仅仅是Call函数部分):
.LC0:
.string "Derived::f()"
Call(Derived&):
mov edx, 12
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
通过上述汇编,可以看出,虽然void f()是一个虚函数,但经过编译器分析有化后(此步骤称为Devirtualization
),编译器会尝试在编译阶段而不是运行阶段去调用虚函数,因此省略了通过虚函数表跳转这个过程,最终得到的是如上结果,即直接将f() inline到Call中,直接调用std::cout输出。
此时,如果将Call()函数参数变成**Base&**,则汇编代码如下:
Call(Base&):
mov rax, QWORD PTR [rdi]
jmp [QWORD PTR [rax]]
此时,会先通过d获取到虚函数表,然后通过虚函数表间接调用**Derived::f()**。
其实,讲到这块,想要说的已经差不多了,在继承某个类的时候,加上一个final,无非是告诉编译器,这个类以后不会再被继承,你随意发挥随意优化吧~~