1、深入虚函数
1.1 所有虚函数都必须提供定义
由于虚函数的动态绑定特性,编译器无法在编译阶段就确定到底会使用哪个版本的虚函数,所以所有的虚函数都必须提供定义。
1.2 派生类中的虚函数要 严格 模仿基类虚函数
派生类的虚函数返回值,和参数列表都要和基类的一模一样。
只有一个例外:
1.3 override
说明符
被override
修饰的函数,必然覆写了基类中同名的的虚函数,必然和他有着相同的返回值和参数列表。
override
存在这种特性的原因之一是:当我们在派生类中定义一个与基类虚函数同名,但是返回值和参数列表不尽相同的函数时,编译器会把它当作一个新的函数,不会报错。然而,这很有可能是程序员出错了。那么,有了override
,这种情况就会报错啦,啊哈哈哈哈。
所以,overirde
修饰的函数必须满足虚函数的要求。
说白了,其实就是override
关键字用来说明这是派生类的虚函数。
1.4 final
修饰的虚函数无法被覆盖
1.5 虚函数 与 默认实参
虚函数可以有默认实参。
若某次函数调用使用了虚函数的默认实参,那么,使用的默认实参由本次调用的静态类型决定。
1.6 回避虚函数的动态绑定机制
当想要调用虚函数的某个特定版本时,我们可以使用作用域运算符限定到底调用哪个版本,如下所示:
Bulk_quote* pbulk
double undiscount_price = pbulk->Quote::net_price(42);
尽管上面的pbulk
是指向派生类的指针,但可以使用作用域限定符,强制调用基类版本的net_price
虚函数。
此条调用可以在编译阶段就完成解析。
什么情况下需要回避动态绑定机制?
通常是,当一个 派生类的虚函数 调用 它覆盖的基类的虚函数版本时。
这其实不难理解,因为,基类版本的虚函数完成的是常规工作。派生类的版本需要执行一些与派生类本身密切相关的别的操作,这时,常规工作就直接调用虚函数版本来完成,减少代码量,提高代码复用性。
有时,成员函数(或友元)中的代码也使用作用域运算符回避动态绑定机制。
当然了,派生类的虚函数是包括在成员函数里的。
2、抽象基类
2.1 纯虚函数
用 = 0
来替代虚函数的函数体,来表示这是个纯虚函数,纯虚函数无须(也不能)定义。
与virtual
和 override
同理, = 0
只能出现在类内部。
含有纯虚函数的类是 抽象基类
不能(直接)创建抽象基类的对象。因为它定义了纯虚函数。但是我们可以通过创建抽象基类的派生类间接创建抽象基类对象。
子类必须定给出纯虚函数的定义,否则子类也还是抽象类。
派生类的构造函数值初始化他的直接基类
创建派生类时,派生类调用其抽象基类的构造函数,创建一个抽象基类的对象,抽象基类又调用其基类的构造函数,创建一个基类对象。
书中给的例子:
Quote.h
基类:
#pragma once
#include<string>
#include<iostream>
using namespace std;
class Quote
{
public:
Quote() = default;
Quote(const string& book, double sales_price) : bookNo(book), price(sales_price) { }
string isbn() const { return bookNo; }
virtual double net_price(size_t n) const { return n * price; }
virtual ~Quote() = default;
virtual void debug(ostream&) const;
private:
string bookNo;
protected:
double price = 0.0;
};
void Quote::debug(ostream& os) const
{
os << typeid(bookNo).name() << ": " << "bookNo = " << bookNo << endl;
os << typeid(price).name() << ": " << "price = " << price << endl;
}
Disc_quote.h
抽象基类:
#pragma once
#include"Quote.h"
using namespace std;
// 重构
// 这是个抽象基类,无法声明对象,因为含有 纯虚函数 nte_price
class Disc_quote : public Quote
{
public:
Disc_quote() = default;
Disc_quote(const string& book, double price, size_t n, double dis) :
Quote(book, price), quantity(n), discount(dis) { }
double net_price(size_t) const = 0; // 纯虚函数
protected:
size_t quantity = 0;
double discount = 0.0;
};
Bulk_quote.h
抽象基类的派生类:
#pragma once
#include"Disc_quote.h"
using namespace std;
class Bulk_quote : public Disc_quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string& book, double price, size_t n, double dis) :
Disc_quote(book, price, n, dis) { }
virtual double net_price(size_t cnt) const override
{
if (cnt >= quantity)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
~Bulk_quote() = default;
virtual void debug(ostream&) const override;
};
void Bulk_quote::debug(ostream& os) const
{
this->Quote::debug(os);
os << typeid(quantity).name() << ": " << "quantity = " << quantity << endl;
os << typeid(discount).name() << ": " << "discount = " << discount << endl;
}
2、访问控制
2.1 protected
成员
和private
类似:
- 对于本类,
protected
成员只能在类内部(及友元)访问 - 对于其派生类,只能在派生类内部访问,派生类的友元也只能通过派生类的对象访问,而不能通过基类对象访问,因为他只是派生类的友元,而不是基类的友元。
2.2 公有、私有、受保护继承
为什么这篇文章中说:
private
继承在软件设计层面上没有意义,其意义只在于软件实现层面。?
private
继承,父类中所有的方法到了子类中都变成了私有的,不能作为接口提供给用户使用,所以只能用于实现子类的功能,所以他的意义只在于软件实现层面。
派生列表里的访问说明符对子类中的成员 及 子类的友元能否访问基类中成员,没有任何影响。子类原来能访问的还能访问,原来不能访问的还是不能访问。(只与基类中的访问说明符有关)
有影响的 是子类的用户 和 子类的子类(以及子类的子类的友元)对基类成员的访问:
private
继承,相当于父类中public
和protected
的成员定义为子类的private
成员protected
继承,相当于父类中public
和protected
的成员定义为子类的protected
成员public
继承,父类中protected
和public
的成员在子类中不发生改变,相当于把原有的非private
成员直接拿到子类,成了子类定义的成员
子类的用户 和 子类的子类(以及子类的子类的友元)根据上述三条原则访问子类继承自父类的成员。
注:上面说的都是相当于,并不是子类真的有了这些成员,仍然是从父类继承而来的。
(上面说的子类的用户也可理解为用户代码)
2.3 派生类向基类转换的可访问性
派生列表的访问说明符除了对 子类的用户 和 子类的子类 使用父类成员有影响外,还对 子类的用户 和 子类的子类 使用子类向父类的类型转换有影响。
- 子类中的成员 及 子类的友元能否使用子类向父类的转换仍然不受任何影响。
- 只有
public
继承,子类的用户 才能使用子类向父类转换 - 只有
public
或protected
继承,子类的子类 和 子类的子类的友元 才能使用子类向父类转换
下面分别通过代码,依次证明上述三条:
class B{}; 父类 B
class D: public B
{
void function(D &d) { B b = d; } 子类的 成员函数进行转换,没问题
friend void friendFunction(D &d) { B b = d; } 子类的 友元进行转换,没问题
};
class E:protected B
{
void function(E &e) { B b = e; } 子类的 成员函数进行转换,没问题
friend void friendFunction(E &e) { B b = e; } 子类的 友元进行转换,没问题
};
class F:private B
{
void function(F &f) { B b = f; } 子类的 成员函数进行转换,没问题
friend void friendFunction(F &f) { B b = f; } 子类的 友元进行转换,没问题
};
这全是子类,随便转换,不受影响
#include <iostream>
class A 父类 A
{
public:
virtual void print() { cout << "我是A" << endl; }
};
class B : public A 子类 B
{
public:
void print() { cout << "我是B 继承A" << endl; }
};
class C : private A 子类 C
{
public:
void print() { cout << "我是C 继承A" << endl; }
};
int main()
{ 用户,也称 用户代码,也就是 类外
A *p;
B b; 子类 B 的用户
C c; 子类 C 的用户
p = &b; 正确
p = &c; 错误,C 是 private 继承,不能转换
p->print();
}
class D : public B
{
void function(D &d) { B b = d; }
friend void friendFunction(D &d) { B b = d; }
};
class E: protected B
{
void function(E &e) { B b = e; }
friend void friendFunction(E &e) { B b = e; }
};
class F: private B
{
void function(F &f) { B b = f; }
friend void friendFunction(F &f) { B b = f; }
}; 上面是 子类 及其 友元
// 分 割 线 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
class G : D 下面是 子类的子类 及其 友元
{
void function(D &d) { B b = d; }
friend void friendFunction2(F &f) { B b = f; }
};
class H : E
{
void function(E &e) { B b = e; }
friend void friendFunction2(F &f) { B b = f; }
};
class I : F
{
void function(F &d) { B b = f; } 报错
friend void friendFunction2(F &f) { B b = f; } 报错
};
本例中的前三个类(DEF)都是子类,随便转换,不受任何影响
后三个类(GHI)都是子类的子类 及友元,但前两个分别是 public 和 protected 的,也能转换
最后一个是 private 的,不能转换
2.4 友元 与 继承
友元关系不能继承。
一个可能搞错的情况:
class father
{
friend class X; 类 X 是 father 的友元
protected:
int protected_member;
};
class son : public father {}; son 是 father 的子类,友元关系不会继承,X 不能访问 son 的成员
class X
{
public:
int func(son s) { return s.protected_member; } 注意这里,这是正确的
};
上面代码中,X
的fun
成员虽然访问了son
,但这边并没有违背友元不能继承的特性。因为访问的是father
的成员protected_member
。
一个友元对他朋友的可访问性,包括了朋友对象内嵌在其子类对象中的情况。
2.5 改变单个成员的可访问性
首先声明,我们更改某成员的可访问性后,当前的类 得不到任何好处。
也就是说,更改成员的可访问性是用来造福 或 坑害他人的。(他人指的是类的用户,这个类的子类及其友元)
我们使用using
关键字来更改,using
语句前的访问限定符是啥,就是改成了啥。举个例子:
class father 父类
{
private:
int private_member; 私有
protected:
int protect_member; 保护
};
class son 子类
{
public:
using father::protect_member; 改成共有,这样 son的子类,son的用户就能用了,造福了他们
protected:
using father::private_member; 错误,son都不能访问,你还想改它的访问类型???痴心妄想!
};
正如上例所示,son
得不到任何好处,而且它也没有能力更改自己也访问不了的成员的可访问性。
2.6 默认的继承保护级别
struct
和class
唯二的区别之一:
class
默认以private
来继承父类struct
默认以public
来继承父类
3、继承中的类作用域
子类的作用域嵌套在父类的作用域里。
如果一个名字在子类中无法解析,编译器会继续在外层父类作用域寻找该名字的定义。
3.1 编译时 进行名字查找
编译时进行名字查找,就是说,静态类型 指定编译器查找某个名字的作用域。
因为,编译时,只能通过静态类型来查找了,这个时候还没有动态类型。
因此,你就不能做出下面代码所示的这种操作:
class Disc_quote : public Quote
{
public:
void aaa() { return; }
};
Disc_quote dq;
Quote* p = &dq; 发生隐式转换
p->aaa(); 错误,p 的类型是Quote的指针,对 aaa 的搜索在Quote的作用域内,显然不包含aaa
要提防这种错误。
3.2 名字隐藏
和其他的作用域一样,子父类的作用域之间也存在名字隐藏。
子类定义了和父类同名的成员时,就会隐藏掉父类的成员。
-
子父类都有同名成员函数 不构成重载 ,仍然是隐藏。
这里涉及到重载与作用域的关系:
若在内层作用域中声明名字,他将隐藏外层作用域中声明的同名实体。在不同的作用域中复发重载函数名。
父类和子类是两个作用域,子类成员根本不可能重载任何父类的成员函数。 -
名字查找优先于类型检查。
当调用一个成员时,编译器不管三七二十一,先查找成员的名字,找到名字之后,再判断这个调用传入的实参类型是否和形参类型相同。
也就是说,当你调用了一个成员函数,类里确实定义了同名的函数,但是参数类型对不上,这时,不存在说,哎?参数类型对不上啊!那咱去它父类里找找有没有参数能对上的吧!
这是不对的。因为是先查找名字,找到一样的名字就定死了,不去找别的了,你参数类型对不上就报错啦!!!就是下图所示的情况:
要是想使用父类的同名成员就只能加上作用域运算符了。
class father { public: int mem; };
class son : public father
{
public:
int mem;
int get_father_mem() { return father::mem; }
};
作用域运算符直接覆盖原有查找规则,让编译器从指定作用域开始查找名字。
小节一波:
类成员的查找步骤:( 假设调用p->mem()
)
- ① 先确定
p
(对象)的静态类型 - ② 在
p
(对象)的静态类型的作用域中查找mem
。若找不到,再到父类里面找直到找到继承链顶端,还找不到,报错。 - ③ 若找到了,检查此调用是否合法(参数对不对的上啊等等)
- ④ 若调用合法。编译器根据调用的是否是虚函数产生不同的代码
-
- 若
mem
是虚函数,且 此次是通过引用或指针进行的调用,则编译器产生的代码在运行时确定运行该虚函数的哪个版本,依据就是对象的动态类型。
- 若
-
mem
不是虚函数 或 此次调用是通过对象进行的,则直接调用相应函数即可。
3.3 覆盖重载的函数
虚函数可以被重载。
子类可以覆盖0个或多个父类中重载的函数。
但如果子类想使用父类重载函数的所有版本,就必须覆盖所有的版本 或 一个都不覆盖。
举个例子:
class father
{
public:
virtual void aaa() { cout << "空的版本" << endl; } 这四个
virtual void aaa(int) { cout << "int的版本" << endl; } 全都是
virtual void aaa(double) { cout << "double的版本" << endl; } aaa
virtual void aaa(string) { cout << "string的版本" << endl; } 的重载
};
class son : public father
{
public:
virtual void aaa(string) override { cout << "重载的string的版本" << endl; } aaa 的覆盖
};
son s;
s.aaa(string()); 正确,你覆盖了 string 的版本,当然可以使用
s.aaa(3.14); 错啦!你没有 全部 覆盖,只能用 string 的版本
你想用的话,要么把剩下的全tm覆盖了,要么全不覆盖
原因仍然是名字隐藏,当你覆盖了一个重载版本后,他隐藏了外部作用域中的同名函数。所以无法访问那些没被覆盖的重载版本。
那我就想 只覆盖一部分,还想用其他没覆盖的重载版本怎么办?使用using
声明。
using
声明语句指定一个名字而不指定形参列表,所以一条父类成员函数的using
声明语句就可以把该函数的所有重载版本添加到子类作用域中。
用using
把他们添加进来,重载函数的名字就都可见了,就避免了名字隐藏。
此时就可以只覆盖某一个重载版本,同时使用没有覆盖的重载版本。
对那些 子类没有覆盖的重载版本的访问实际上是对using
声明点的访问。
class son : public father
{
如果写在这种地方,那就相当于是把他们改成了 私有继承 下来的,还是访问不了
public:
using father::aaa; using 声明一定要写在 public 下
virtual void aaa(string) override { cout << "重载的string的版本" << endl; }
};
也就是说,不能这样:
class son : public father
{
using father::aaa; 修改了 aaa() 的继承限制,成了私有继承下来的了
public:
virtual void aaa(string) override { cout << "重载的string的版本" << endl; }
};