基类和派生类的定义以及虚函数
基类Quote的定义:
classs Quote {
public:
Quote() = default;
Quote(cosnt std::string& book, double sales_price) : bookNo(book), price(sales_price) {}
std::string isbn() const { return bookNo; }
virtual double net_price(size_t n) const { return n * price; }
virtual ~Quote() = default;
private:
std::string bookNo;
protected:
double price = 0.0;
};
PS: 基类通常都应该定义一个虚析构函数,尽管这个析构函数不执行任何实际的操作。
派生类可以直接继承基类的成员。基类应该讲它的成员函数分为两种:一种是基类希望它的派生类重写覆盖的函数,类似net_price()成员函数;另一种是基类希望派生类直接继承而不要改变的成员函数,类似isbn() 。 对于基类的成员函数,派生类如果需要覆盖,则将其声明为虚函数(virtual)的,它只能出现在成员函数的声明的最前面。
基类可以将任意非static成员函数(除了构造函数之外)定义为virtual函数。
如果成员函数被定义为虚函数,则其解析过程发生在运行阶段,如果未被定义为虚函数,解析过程发生在编译阶段。
定义派生类:
class Bulk_quote : public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, size_t, double);
// override : 覆盖基类的net_price版本
double net_price(size_t) const override;
private:
size_t min_qty = 0;
double discount = 0.0;
};
一个派生类不总是需要覆盖它的基类的虚函数,如果派生类没有覆盖掉基类的虚函数,则这个虚函数和普通函数没什么区别,派生类会直接继承基类中的版本;
如果派生类需要覆盖基类的虚函数版本来重写自己的版本,就需要用到C++11关键字override。
override允许派生类显示的注明它使用某个成员函数覆盖了它继承的基类的虚函数,具体的做法是:
在虚函数的参数列表后面添加override关键字,如果这个虚函数是一个const成员函数,则override写在const之后,如果这个虚函数还是一个引用成员函数,则override跟在引用限定符&或&&之后。
因为派生类对象中含有其基类对应的组成部分,因此一个派生类对象可以被当做一个基类来使用,也能把一个基类指针或引用绑定到派生类对象的基类部分上。
Quote item;
Bulk_quote bulk;
Quote* p = &item;
p = &bulk;
Quote& r = bulk;
这种转换称为“派生类向基类的转换”,在派生类中含有基类的组成部分,这是继承的关键所在。
派生类的构造函数
派生类并不直接构造基类的成员,而是使用基类的构造函数来构造,通过向基类的构造函数传递对应的参数来完成:
Bulk_quote(const std:string& book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {}
改构造函数构造顺序是:先进行基类Quote的构造,待Quote的函数体执行完毕后,按派生类Bulk_quote的成员的声明顺序来初始化它自己的成员,最后执行它的函数体。
首先进行基类Quote的构造,再按照声明的顺序依次初始化派生类的成员。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论派生类有多少个,这个静态成员只有这一个,同时, 静态成员遵循访问控制规则。
void Derived::f(const Derived& derived)
{
Base::statmem();
Derived::statmem();
derived.statmem(); // 通过derived对象访问
statmem(); // 通过this对象访问
}
一个类如果被用做基类,则这个类必须是已经定义的类,而非仅仅声明。因此,一个类不能做自己的基类。
防止一个类被继承的方法是,在这个类的类名后跟关键字final。
class NoDerived final {};
class NoDerived1 : public NoDerived {}; // 错误, NoDerived是final的, 不能被继承
类型转换和继承
静态类型与动态类型
可以将一个基类的指针或引用绑定到派生类的对象上,但是实际上并不知道该指针或引用所绑定对象的真实类型,可能是基类对象,也可能是派生类对象。
PS: 和内置指针一样,智能指针也支持派生类向基类的转换,于是我们就可以将派生类对象存储在一个基类的智能指针内。
在使用继承关系的类型时,需要将静态类型和动态类型区分开。 静态类型在编译时是已知的,它是变量声明时的类型或表达式生成的类型;而动态类型则是变量表示内存中的对象的类型,直到运行时才可知。
如果表达式既不是指针也不是引用,那么它的动态类型永远和静态类型一致。
void print_total(Quote& q){
q.net_price(1);
}
q的静态类型为Quote。
print_total在调用net_price时, 动态类型依赖于q绑定的实参,如果传递的是Quote对象,则它的静态类型和动态类型相同,如果传递的是一个Bulk_quote对象,则它的动态类型和静态类型不一样。
派生类对象可以当做基类使用,但是不存在从基类向派生类的转换,即使一个基类指针或引用绑定在一个派生类对象上,也不能执行基类向派生类的转换。
Bulk_quote bulk;
Quote* itemp = &bulk;
Bulk_quote* bulkp = itemp; // 错误,不能将基类转换成派生类
编译器在编译时无法确定某个转换在运行时是否安全,因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法。
如果我们已知从一个基类转换到派生类是安全的,我们可以使用static_cast来强制覆盖掉编译器的检查工作;如果基类中有虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查在运行时执行。
如果只是普通的基类对象和派生类对象,则不存在上述这些转换, 因为把一个派生类赋值给一个基类对象时,实际上执行的是基类的构造函数, 基类构造函数只负责将派生类中基类的部分赋值给自己,剩下的派生类的部分就被切掉了。
虚函数
对虚函数的调用在运行时被解析
void print_total(Quote& q){
q.net_price(1);
}
在print_total的调用中,q的静态类型为Quote,通过q来调用虚函数net_price(), 对虚函数的调用时,在运行时才会决定q的类型到底是Quote还是Bulk_quote。
当且仅当对通过指针或引用调用虚函数时,才会在运行时进行解析。也只有这种情况下,对象的动态类型才可能与其静态类型不同。
派生类中的虚函数如果覆盖了基类的虚函数,则它的形参类型必须和它所覆盖的虚函数的形参类型完全一致。 同样,派生类中重写的虚函数的返回类型也必须和覆盖了的函数的返回类型一致,但是有个例外,如果该返回类型是类本身的指针或引用时,该规则无效。
如果D继承自B, 则基类B中的虚函数如果返回B*或B&, 则D中的该虚函数可以返回D*或D&, 前提是从D到B的类型转换时可访问的。
final和override
override说明符可以避免虚函数覆盖中的一些问题:
派生类如果定义了一个函数名和基类中虚函数名相同的函数但是形参列表不同,则派生类定义的这个函数则认为是一个独立的函数,而这时,派生类没有覆盖掉基类的虚函数,对于实际的编程习惯而言,这有可能会引发错误。
可以通过override来帮助我们发现一些错误,如果override标记了一个函数,但该函数并没有覆盖掉已存在的虚函数,此时编译器将报错:
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B {
void f1(int) const override; // 正确,覆盖了基类的f1
void f2(int) override; // 错误,基类没有f2(int)的函数
void f3() override; // 错误,f3()不是虚函数
void f4() override; // 错误,基类中没有f4()函数
};
还可以将某个函数指定为final,将基类中一个虚函数指定为final,则它的派生类中试图覆盖该虚函数的操作都将发生错误:
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
virtual void func_final() final;
};
struct D2 : B {
void func_final() /*override*/; // 错误, func_final函数是final的
};
虚函数与默认实参
虚函数可以拥有默认实参,如果在某次的调用中使用了虚函数的默认实参,则该实参值由本次调用的静态类型决定。
也就是说,如果我们通过基类的指针或引用调用函数, 则使用基类中该虚函数给的默认实参, 即使动态类型时派生类也是如此。
回避虚函数的机制
如果派生类的虚函数需要调用它的基类的该虚函数的版本,需要基类的作用域符来强制要求,否则在运行时调用的是派生类的虚函数版本, 这样会导致无穷递归!
class Base {
public:
string name() { return basename; }
virtual void print(ostream& os) { os << basename; }
private:
string basename;
};
class derived : public Base {
public:
void print(ostream& os) { print(os); os << " " << i; } // 错误,会导致无穷递归
private:
int i;
};
在derived类中,print函数的目的是调用基类Base的print函数, 但在实际运行print函数中, 派生类的print函数相当于this->print, this绑定的是derived类,这样就会导致无限递归,修改的方法是使用Base的作用域:
void print(ostream& os) { Base::print(os); os << " " << i; }