OOP

虚函数:基类希望它的派生类个自定义适合自身的版本,此时基类就将这些函数声明成虚函数。

作为继承关系中根节点的类通常都会定义一个虚析构函数。

基类希望派生类有权访问成员,同时禁止其他成员访问。此时使用protected访问运算符说明这样的成员。

派生类经常覆盖它继承的虚函数。如果派生类没有覆盖基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也要使用基类的构造函数来初始化它的基类部分。

如果基类定义了一个静态成员,则在整个继承体系中值存在该成员的唯一定义。不论从基类派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。

派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表:

1
2
class Bulk_quote : public Quote;   //错误,派生列表不能出现在这里
class Bulk_quote;   //正确

如果想将某个类用作基类,则该类必须已经定义而非仅仅声明。

在类名后跟一个关键字final即可防止继承发生:

1
2
3
class NoDerived final{};    //NoDerived不能作为基类
class Base{};
class Last final : Base{};   //Last不能作为基类

当使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。

因为一个基类的对象可嗯是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
基类的指针可以指向派生类,但是派生类的指针不能指向基类。
即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换。

1
2
3
Bulk_quote bulk;
Quote *itemp = &bulk;        //正确,动态类型是Bulk_qoute
Bulk_quote *bulkp = itemp;   //错误,不能将基类转换成派生类

使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为直到运行时才知道到底调用哪个版本的虚函数,所以所有虚函数都必须有定义。

当在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。但是不是必须要这么做,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
派生类中虚函数的返回类型也必须与基类函数匹配。

可以使用override关键字来说明派生类中的虚函数。
如果使用override标记了某个函数,但是该函数没有覆盖已经存在的虚函数,此时编译器将报错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct B{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};

struct D1:B{
    void f1(int) const override;  //正确
    void f2(int) override;        //错误,没有f2(int)的函数
    void f3() override;           //错误,f3不是虚函数
    void f4() override;           //错误,B中没有f4函数
};

如果已经把函数定义成final,则之后的任何覆盖该函数的操作都啊经引发错误:

1
2
3
4
5
6
7
struct D2:B{
    void f1(int) const final;
};
struct D3:D2{
    void f2();             //正确
    void f1(int) const;    //错误,D2将f2声明成final
};

如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。

如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

和普通的虚函数不一样,一个纯虚函数无须定义。通过在函数体的位置书写**=0**就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。
可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,不能在类的内部为一个=0的函数提供函数体。

抽象基类负责定义接口,而后续的其他类可以覆盖该接口。不能直接创建一个抽象基类的对象。

每个类分别控制其成员对于派生类来说是否可访问

protected关键字用来声明希望与派生类分享但是不想被其他公共访问使用的成员。

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
  • 派生类的成员或友元只能通过派生类对象来访问基类受保护的成员。派生类对于一个基类对象中的受保护成员没有任何访问权限。

派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。

公有继承和私有继承均能访问受保护的成员,但是都不能访问私有成员。

  • 只有当D公有继承B时,才能使用派生类向基类的转换;如果D继承B的方式是私有或是受保护的,则不能使用转换。
  • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
  • 如果D继承B的方式是公有的或是受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有地,则不能使用。

友元关系不能继承。基类的友元在访问派生类成员是不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。
不能继承友元关系;每个类控制负责各自成员的访问权限。

有时需要改变派生类继承的某个名字的访问级别,通过使用using声明达到这一目的:

using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。如果using语句出现在private部分,则该名字只能被类的成员和友元访问;如果using语句位于public部分,则类的所有用户都能访问它;protected同理。

默认情况下,使用class关键字定义的派生类是私有继承的,而使用struct关键字定义的派生类是公有继承的:

1
2
3
class Base{};
struct D1:Base{};  //默认public继承
class D2:Base{};   //默认private继承

一个私有派生的类最好显式的将private声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。

如果一个名字在派生类的作用域内无法正确解析,则编译器降级须在外层的基类作用域中寻找该名字的定义。
派生类的作用域位于基类作用域之内。

除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也依然会被隐藏掉。

通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。

基类需要一个虚析构函数还会对基类和派生类的定义产生另外一个版本的影响:如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的。原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
  • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象爱国的基类部分。
  • 编译器不会合成一个删除掉的移动操作。当使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。

在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。
因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,当确实需要执行移动操作时应该首先在基类中进行定义。

派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。

当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分。

1
2
3
4
5
class Base{};
class D:public Base{
    D(const D& d): Base(d){}        //拷贝基类成员
    D(D&& d):Base(std::move(d)){}   //移动基类成员
};

默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式的使用基类的拷贝或构造函数。

与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式的为其基类部分赋值。

和构造函数及赋值运算符不同的是,派生类析构函数值负责销毁由派生类自己分配的资源。
对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承够体系的反方向直至最后。

一个类只初始化它的直接基类,出于同样的原因,一个类也只继承直接基类的构造函数。
派生类继承基类的构造函数的方式是提供用一条著名了基类明德using声明语句:

1
2
3
4
5
class Bulk_quote : public Disc_quote{
public: 
    using Disc_quote::Disc_quote;   //继承Disc_quote的构造函数
    double net_price(std::size_t) const;
};

一个构造函数的using声明不会改变该构造函数的访问级别。不管using声明出现在哪,基类的私有构造函数在派生类中还是一个私有构造函数;受保护的构造函数和公有构造函数也是同样的规则。
当一个基类构造函数含有默认实参时,这些实参并不会被继承。