第16章 override和final说明符(C++11)
16.1 重写、重载和隐藏
重写(override)、重载(overload)和隐藏(overwrite)在C++中是3个完全不同的概念:
1.重写(override)的意思更接近覆盖,在C++中是指派生类覆盖了基类的虚函数,这里的覆盖必须满足有相同的函数签名和返回类型,也就是说有相同的函数名、形参列表以及返回类型。
2.重载(overload),它通常是指在同一个类中有两个或者两个以上函数,它们的函数名相同,但是函数签名不同,也就是说有不同的形参。这种情况在类的构造函数中最容易看到,为了让类更方便使用,我们经常会重载多个构造函数。
3.隐藏(overwrite)的概念也十分容易与上面的概念混淆。隐藏是指基类成员函数,无论它是否为虚函数,当派生类出现同名函数时,如果派生类函数签名不同于基类函数,则基类函数会被隐藏。如果派生类函数签名与基类函数相同,则需要确定基类函数是否为虚函数,如果是虚函数,则这里的概念就是重写;否则基类函数也会被隐藏。另外,如果还想使用基类函数,可以使用using关键字将其引入派生类。
16.2 重写引发的问题
重写虚函数很容易出现错误,原因是C++语法对重写的要求很高,稍不注意就会无法重写基类虚函数。即使我们写错了代码,编译器也可能不会提示任何错误信息,直到程序编译成功后,运行测试才会发现其中的逻辑问题
class Base {
public:
virtual void some_func() {}
virtual void foo(int x) {}
virtual void bar() const {}
void baz() {}
};
class Derived : public Base {
public:
virtual void sone_func() {}
virtual void foo(int &x) {}
virtual void bar() {}
virtual void baz() {}
};
16.3 使用override说明符
C++11标准提供了一个非常实用的override说明符,这个说明符必须放到虚函数的尾部,它明确告诉编译器这个虚函数需要覆盖基类的虚函数,一旦编译器发现该虚函数不符合重写规则,就会给出错误提示。
class Base {
public:
virtual void some_func() {}
virtual void foo(int x) {}
virtual void bar() const {}
void baz() {}
};
class Derived : public Base {
public:
virtual void sone_func() override {}
virtual void foo(int &x) override {}
virtual void bar() override {}
virtual void baz() override {}
};
编译后编译器给出了4条错误信息,明确指出这4个函数都无法重写。
16.4 使用final说明符
C++11标准引入final说明符,它告诉编译器该虚函数不能被派生类重写。final说明符用法和override说明符相同,需要声明在虚函数的尾部。
class Base {
public:
virtual void foo(int x) {}
};
class Derived : public Base {
public:
void foo(int x) final {};
};
class Derived2 : public Derived {
public:
void foo(int x) {};
};
因为基类Derived的虚函数foo声明为final,所以派生类Derived2重写foo函数的时候编译器会给出错误提示。
有时候,override和final会同时出现。这种情况通常是由中间派生类继承基类后,希望后续其他派生类不能修改本类虚函数的行为而产生的,举个例子:
class Base {
public:
virtual void log(const char *) const {…}
virtual void foo(int x) {}
};
class BaseWithFileLog : public Base {
public:
virtual void log(const char *) const override final {…}
};
class Derived : public BaseWithFileLog {
public:
void foo(int x) {};
};
final说明符不仅能声明虚函数,还可以声明类。如果在类定义的时候声明了final,那么这个类将不能作为基类被其他类继承
class Base final {
public:
virtual void foo(int x) {}
};
class Derived : public Base {
public:
void foo(int x) {};
};
16.5 override和final说明符的特别之处
在C++11标准中,override和final并没有被作为保留的关键字,其中override只有在虚函数尾部才有意义,而final只有在虚函数尾部以及类声明的时候才有意义,因此以下代码仍然可以编译通过:
class X {
public:
void override() {}
void final() {}
};
第17章 基于范围的for循环(C++11 C++17 C++20)
17.1 烦琐的容器遍历
通常遍历一个容器里的所有元素会用到for循环和迭代器
std::map<int, std::string> index_map{ {1, "hello"}, {2, "world"},
{3, "!"} };
std::map<int, std::string>::iterator it = index_map.begin();
for (; it != index_map.end(); ++it) {
std::cout << "key=" << (*it).first << ", value=" << (*it).second
<< std::endl;
}
使用标准库提供的std::for_each函数,使用该函数只需要提供容器开始和结束的迭代器以及执行函数或者仿函数即可,例如:
std::map<int, std::string> index_map{ {1, "hello"}, {2, "world"},
{3, "!"} };
void print(std::map<int, std::string>::const_reference e)
{
std::cout << "key=" << e.first << ", value=" << e.second << std::endl;
}
std::for_each(index_map.begin(), index_map.end(), print);
17.2 基于范围的for循环语法
C++11标准引入了基于范围的for循环特性,该特性隐藏了迭代器的初始化和更新过程,让程序员只需要关心遍历对象本身,其语法也比传统for循环简洁很多:
for ( range_declaration : range_expression ) loop_statement
范围表达式可以是数组或对象,对象必须满足以下2个条件中的任意一个。
1.对象类型定义了begin和end成员函数。
2.定义了以对象类型为参数的begin和end普通函数。
#include <iostream>
#include <string>
#include <map>
std::map<int, std::string> index_map{ {1, "hello"}, {2, "world"},
{3, "!"} };
int int_array[] = { 0, 1, 2, 3, 4, 5 };
int main()
{
for (const auto &e : index_map) {
std::cout << "key=" << e.first << ", value=" << e.second << std::endl;
}
for (auto e : int_array) {
std::cout << e << std::endl;
}
}
代码使用了两种形式的范围声明,前者是容器或者数组中元素的引用,而后者是容器或者数组中元素的值。一般来说,我们希望对于复杂的对象使用引用,而对于基础类型使用值,因为这样能够减少内存的复制。
#include <vector>
struct X
{
X() { std::cout << "default ctor" << std::endl; }
X(const X& other) {
std::cout << "copy ctor" << std::endl;
}
};
int main()
{
std::vector<X> x(10);
std::cout << "for (auto n : x)" << std::endl;
for (auto n : x) {
}
std::cout << "for (const auto &n : x)" << std::endl;
for (const auto &n : x) {
}
}
for(auto n : x)的循环调用10次复制构造函数,如果类X的数据量比较大且容器里的元素很多,那么这种复制的代价是无法接受的。而for(const auto &n : x)则解决了这个问题,整个循环过程没有任何的数据复制。
17.3 begin和end函数不必返回相同类型
在C++11标准中基于范围的for循环相当于以下伪代码:
{
auto && __range = range_expression;
for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
这段伪代码有一个特点,它要求begin_expr和end_expr返回的必须是同类型的对象。但实际上这种约束完全没有必要,只要__begin !=__end能返回一个有效的布尔值即可,所以C++17标准对基于范围的for循环的实现进行了改进,伪代码如下:
{
auto && __range = range_expression;
auto __begin = begin_expr;
auto __end = end_expr;
for (; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
17.4 临时范围表达式的陷阱
无论是C++11还是C++17标准,基于范围的for循环伪代码都是由以下这句代码开始的:
auto && __range = range_expression;
对于这个赋值表达式来说,如果range_expression是一个纯右值,那么右值引用会扩展其生命周期,保证其整个for循环过程中访问的安全性。但如果range_ expression是一个泛左值,那结果可就不确定了,参考以下代码:
class T {
std::vector<int> data_;
public:
std::vector<int>& items() { return data_; }
// …
};
T foo()
{
T t;
return t;
}
for (auto& x : foo().items()) {} // 未定义行为
这里的for循环会引发一个未定义的行为,因为foo().items()返回的是一个泛左值类型std::vector&,于是右值引用无法扩展其生命周期,导致for循环访问无效对象并造成未定义行为。
将数据复制出来是一种解决方法:
T thing = foo();
for (auto & x :thing.items()) {}
在C++20标准中,基于范围的for循环增加了对初始化语句的支持,所以在C++20的环境下我们可以将上面的代码简化为:
for (T thing = foo(); auto & x :thing.items()) {}
17.5 实现一个支持基于范围的for循环的类
要完成这样的类型必须先实现一个类似标准库中的迭代器。
1.该类型必须有一组和其类型相关的begin和end函数,它们可以是类型的成员函数,也可以是独立函数。
2.begin和end函数需要返回一组类似迭代器的对象,并且这组对象必须支持operator *、operator !=和operator ++运算符函数。
#include <iostream>
class IntIter {
public:
IntIter(int *p) : p_(p) {}
bool operator!=(const IntIter& other)
{
return (p_ != other.p_);
}
const IntIter& operator++()
{
p_++;
return *this;
}
int operator*() const
{
return *p_;
}
private:
int *p_;
};
template<unsigned int fix_size>
class FixIntVector {
public:
FixIntVector(std::initializer_list<int> init_list)
{
int *cur = data_;
for (auto e : init_list) {
*cur = e;
cur++;
}
}
IntIter begin()
{
return IntIter(data_);
}
IntIter end()
{
return IntIter(data_ + fix_size);
}
private:
int data_[fix_size]{0};
};
int main()
{
FixIntVector<10> fix_int_vector {1, 3, 5, 7, 9};
for (auto e : fix_int_vector)
{
std::cout << e << std::endl;
}
}
FixIntVector是存储int类型数组的类模板,类IntIter是FixIntVector的迭代器。这里使用成员函数的方式实现了begin和end,但有时候需要遍历的容器可能是第三方提供的代码。这种情况下我们可以实现一组独立版本的begin和end函数,这样做的优点是能在不修改第三方代码的情况下支持基于范围的for循环。