文章目录
联编
联编,binding,的任务是:确定程序调用每一个函数时具体去执行哪一个代码块。
联编有两种类型,各有千秋各有用途。
大部分联编工作在编译时进行,比如C就是通过把函数名作为地址,直接找到目标代码块的。C++由于有函数重载,不能只靠函数名,还要看参数类型和数目,即特征标,但是编译器一般会做名称修饰,所以也在编译期间就可以确定到目标代码位置。
但有时候也需要在运行时进行确定代码块位置,比如上一篇文章的多态特性展示,遍历一个基类指针数组的指针,调用虚函数ViewAcct,当指针指向的对象是基类对象时则程序调用Brass::ViewAcct()方法,指针指向的对象是派生类对象时,就调用BrassPlus::ViewAcct()方法。于是,必须在运行过程中才能确定要为该调用执行哪一块代码。
静态联编static binding or 早期联编early binding
当使用对象的指针或者引用去调用非虚成员函数时,使用静态联编,因为是根据指针和引用的类型来判断调用的方法是哪一个,因此编译时就可以确定。
静态联编的效率更高,因为不需要设置一些跟踪指针用于跟踪动态变量或者对象,不会增加额外的处理开销。
所以静态联编是C++的默认选择。
动态联编dynamic binding or 晚期联编late binding
当使用对象的指针或者引用去调用虚成员函数时,使用动态联编,因为不是根据指针和引用的类型来判断调用的方法是哪一个,而是要根据指向的对象的类型,对象的类型在运行时才可以确定,因为有可能会有隐式类型准换,因此编译时不可以确定。
- 如果类不会被用作基类,那就不会用到动态联编;
- 如果类被用作基类,但是派生类并没有重写基类的任何方法,那么也不需要用到动态联编。
- 所以动态联编只有在需要用时才用。而虚函数也只是在程序需要时才使用。比如不应该把在派生类中不被重写的方法声明为虚函数,只把要重写的定义为虚函数。但是,毕竟写基类的时候不能预知会不会被重写,所以说类的设计经常需要反反复复,不是一次成型。
把基类的方法设置为虚方法,就会启动动态联编
用对象的指针和引用去调用方法(虚成员方法)
虚函数,或者虚方法,是虚成员函数的简称。即虚函数一定是成员函数,友元函数等不能被定义为虚函数。
虚函数为什么得此名呢?那是因为虚函数真的很“虚空”,“虚无”,基类的虚函数定义可以在派生类中重写,所以基类的虚函数的定义就“虚无”了, “隐匿”了,本来有那个定义,但是在重写了该函数的派生类看来又是没有那些定义的,只有自己刚写的新定义,所以就似有还无,似无还有,虚无缥缈,故得此名。(此处推测纯属个人开脑洞,反正能帮助理解且说的通就是了)
先复习C++对类型一致性的严格要求(类型转换)
最近一直在说,C++不允许把一种类型的地址赋给另一个类型的指针,也不允许把一个类型的引用指向另一个类型
double x;
long * pl = &x;//报错
int & = x;//报错
向上强制转换upcasting 和 向下强制转换downcasting
- 向上强制转换upcasting (隐式)
但是,继承第一篇文章说过了,遇到基类和派生类时可以有个单向的例外:即基类的指针和引用可以指向派生类对象。
BrassPlus bp;
Brass * p = &bp;
Brass & r = bp;
这种把派生类对象的指针和引用转换为基类的指针和引用的过程实际上是一种隐式类型转换,且是向上强制类型转换,upcasting,这使得公有继承无需进行强制类型转换了。
示例 用指针或引用调用虚成员方法
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
const int NUM = 2;
void eatline();
void fr(Brass &);
void fp(Brass *);
void fv(Brass);
int main()
{
using std::cout;
using std::cin;
Brass ross("Ross Galler", 123786, 5600.0);//基类对象
BrassPlus mona("Mona White", 467838, 4500.0);//派生类对象
//按引用传递
fr(ross);//Brass 给Brass &
fr(mona);//BrassPlus 给Brass &,隐式向上强制转换
//按指针传递
std::cout << '\n';
fp(&ross);//Brass *给Brass *
fp(&mona);//BrassPlus *给Brass *,隐式向上强制转换
//按值传递
std::cout << '\n';
fv(ross);//Brass给Brass,调用复制构造函数Brass(const Brass &)
std::cout << '\n';
fv(mona);//BrassPlus给Brass,相当于只是把mona中的Brass类对象传进去了,,调用复制构造函数Brass(const Brass &)
std::cout << '\n';
return 0;//析构ross和mona对象,后者会先调用~BrassPlus,然后在其中调用~Brass
}
void eatline()
{
while (std::cin.get() != '\n')
;
}
void fr(Brass & r){r.ViewAcct();}
void fp(Brass * p){p->ViewAcct();}
void fv(Brass v){v.ViewAcct();}
当Brass::ViewAcct()不是虚方法,三种传递方式都是只调用基类方法Brass::ViewAcct()
//按引用传递
fr(ross);//Brass::ViewAcct()
fr(mona);//Brass::ViewAcct()
//按指针传递
fp(&ross);//Brass::ViewAcct()
fp(&mona);//Brass::ViewAcct()
//按值传递
fv(ross);//Brass::ViewAcct()
fv(mona);//Brass::ViewAcct()
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00
In ~Brass()
Client: Mona White
Account number: 467838
Balance: $4500.00
In ~Brass()
In ~BrassPlus()
In ~Brass()
In ~Brass()
当Brass::ViewAcct()是虚方法,可以看到按指针和按引用都使用了BrassPlus::ViewAcct(),但是按值传递仍然使用了Brass::ViewAcct()
//按引用传递
fr(ross);//Brass::ViewAcct()
fr(mona);//BrassPlus::ViewAcct()
//按指针传递
fp(&ross);//Brass::ViewAcct()
fp(&mona);//BrassPlus::ViewAcct()
//按值传递
fv(ross);//Brass::ViewAcct()
fv(mona);//Brass::ViewAcct()
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Ross Galler
Account number: 123786
Balance: $5600.00
In ~Brass()
Client: Mona White
Account number: 467838
Balance: $4500.00
In ~Brass()
In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
- 向下强制转换downcasting (显式)
把基类指针或引用转换为派生类是向下强制转换,是不允许的(因为这样做可能带来不安全的操作),除非显式强制转换。
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
int main()
{
using std::cout;
Brass ross("Ross Galler", 123786, 5600.0);//基类对象
BrassPlus mona("Mona White", 467838, 4500.0);//派生类对象
Brass & rb = mona;//隐式向上强制转换
BrassPlus & rbp = (BrassPlus &)ross;//必须显式向上强制转换
rb.ViewAcct();
cout << '\n';
rbp.ViewAcct();//如果ViewAcct()不是虚方法,则会出错
cout << '\n';
return 0;
}
当Brass::ViewAcct()不是虚方法,程序出错,因为rbp是派生类的引用,则要调用派生类的ViewAcct(),而这个方法中用到了派生类的数据成员,ross没有,所以就错了。
Client: Mona White
Account number: 467838
Balance: $4500.00
Process returned -1073741819 (0xC0000005) execution time : 12.498 s
Press any key to continue.
当Brass::ViewAcct()是虚方法,程序不会报错,因为虚方法使得程序调用正确的方法
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Ross Galler
Account number: 123786
Balance: $5600.00
In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
示例 派生类重写基类虚方法必须保证函数原型一样!否则就不是继承,而是新方法,还会覆盖基类的同名方法
否则程序就认为派生类的不是重写方法,而是一个全新的方法,会根据根据引用和指针的类型调用方法
这个例子展示了指针和引用调用虚方法都没有按照对象类型去判断使用哪一个函数,而是根据引用和指针的类型了
上面一个示例展示了用指针和引用调用虚方法会按照对象类型去判断使用哪一个函数,但是那是在基类和派生类的对应虚方法的函数原型完全一样的情况下的!!!
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
int main()
{
using std::cout;
using std::cin;
Brass ross("Ross Galler", 123786, 5600.0);//基类对象
BrassPlus mona("Mona White", 467838, 4500.0);//派生类对象
Brass & rb = mona;//隐式向上强制转换
BrassPlus & rbp = (BrassPlus &)ross;//必须显式强制转换
rb.ViewAcct();//我觉得应该调用BrassPlus::ViewAcct(),可是程序却试图调用Brass::ViewAcct(int),编译出错
cout << '\n';
rbp.ViewAcct();//运行时错误,因为调用了BrassPlus::ViewAcct(),但ross没有BrassPlus类的数据成员,所以错误,但我觉得应该调用Brass::ViewAcct(int)
cout << '\n';
Brass * rb = &mona;//隐式向上强制转换
BrassPlus * rbp = (BrassPlus *)&ross;//必须显式强制转换
rb->ViewAcct();//我觉得应该调用BrassPlus::ViewAcct(),可是程序却试图调用Brass::ViewAcct(int),编译出错
cout << '\n';
rbp->ViewAcct();//运行时错误,因为调用了BrassPlus::ViewAcct(),但ross没有BrassPlus类的数据成员,所以错误,但我觉得应该调用Brass::ViewAcct(int)
cout << '\n';
ross.ViewAcct(2);//调用Brass::ViewAcct(int)
//mona.ViewAcct(2);//报错,因为派生类新定义的同名但不同原型的函数BrassPlus::ViewAcct()覆盖了基类的同名函数Brass::ViewAcct(int)
mona.ViewAcct();//调用BrassPlus::ViewAcct()
return 0;
}
基类
virtual void ViewAcct(int n) const;
void Brass::ViewAcct(int n) const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
int i;
for (i = 0; i < n; ++i)
{
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
}
restore(initialState, prec);
}
派生类
virtual void ViewAcct() const;
void BrassPlus::ViewAcct() const//虚方法
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
Brass::ViewAcct(1);//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
因为这个虚方法ViewAcct()在基类和派生类的原型不同,所以程序就认为派生类的不是重写方法,而是一个全新的方法
派生类新定义的同名但不同原型的函数BrassPlus::ViewAcct()覆盖了基类的同名函数Brass::ViewAcct(int),对派生类对象只可使用BrassPlus::ViewAcct(),不能使用Brass::ViewAcct(int)
把主程序的rb.ViewAcct();和rb->ViewAcct();改为rb.ViewAcct(2);和rb->ViewAcct(2);,则这两句代码正确,打印两遍账户信息,因为他们调用的是基类的函数,所以要有参数。
唯一例外:返回类型协变(即允许返回类型随类的类型不同而变化,但特征标必须一样)(打脸了??)
上面示例说明了,继承基类的方法时,方法的原型必须一毛一样,才是继承,否则就是重新定义一个方法,且由于同名还会隐藏基类的方法。
但是原型在一个点上可以例外,不必一毛一样,即基类方法的返回类型是指向基类的指针或引用时,这时候派生类继承这个方法,可以允许把返回类型改为指向派生类的指针或引用
示例
基类方法原型,要声明为虚函数,这样派生类才可以重写,内联版本
virtual const Brass & Self() const {return *this;}//仅为示例,返回自己
派生类方法原型,我以为不用写定义,结果主程序报错,说没有BrassPlus::Self()方法,加定义就好了。所以对虚方法不写定义,则编译器认为没有重写,派生类就没有这个方法,只能用基类的版本,但是又写了原型,于是基类的就被覆盖了,派生类对象没法调用基类的Self方法。
virtual const BrassPlus & Self() const {return *this;}//如果不写定义,则编译器认为没重写,就没有BrassPlus::Self()方法
主程序
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
int main()
{
using std::cout;
using std::cin;
Brass ross("Ross Galler", 123786, 5600.0);//基类对象
BrassPlus mona("Mona White", 467838, 4500.0);//派生类对象
Brass r = ross.Self();
BrassPlus m = mona.Self();
ross.ViewAcct();
cout << '\n';
r.ViewAcct();
cout << '\n';
mona.ViewAcct();//如果派生类只写原型virtual const BrassPlus & Self() const;
//则不仅覆盖了基类的Self方法,还没定义自己的方法,于是报错undefined reference to `BrassPlus::Self() const'
//所以要写定义,即使定义和基类一毛一样
cout << '\n';
m.ViewAcct();
cout << '\n';
return 0;
}
输出
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
感觉我这个示例打脸了返回类型协变。。。。结果显示它并不是例外。
不过这个示例也体现了我的幼稚,我竟然以为想继承基类的虚函数,但是不改写,则只需要在派生类中写个原型。实际上,必须提供定义,编译器才算你真是个函数,否则只有原型,编译器就当没看见你。即使你的定义其实和基类的方法是一样的,实际并没重写,那也算派生类有一个自己的方法,可以用派生类加作用域解析运算符访问。
如果你在派生类不写基类某方法的原型和定义,或者只写原型,那编译器都认为没有改写,该方法仍然只有基类版本。但如果提供定义,则编译器认为你改写了。会有一个派生类版本一个基类版本。
派生类对像都是基类对象
实际上,派生类对像都是基类对象,派生类对像都是基类对象,派生类对像都是基类对象。
is-a公有继承关系是可传递的
并且is-a的继承关系是可传递的。如果你在BrassPlus类的基础上再继续继承,开发一个BrassPlusPlus类,那么Brass类的指针和引用可以指向Brass类,BrassPlus类,BrassPlusPlus类三个类的对象。
深入剖析虚函数的实现原理
编译器到底是怎么实现虚函数的(隐藏指针成员,虚函数表)
C++只是规定了虚函数的行为,即遇到对象的指针或引用调用虚成员函数时,要根据对象的类型来判断使用该方法的基类版本还是派生类版本。
很神奇。很智能。那编译器的实现者要怎么做到这一点呢?
通过给每个对象添加一个隐藏成员。这个成员是一个指针变量。这个指针指向一个数组。这个数组里存储了类的所有虚函数的地址。
这个数组被称为虚函数表,virtual function table, vtbl。是个函数地址表。调用虚函数时,程序就要查看对象中的隐藏指针,然后找到虚函数表,然后执行需要的那个函数。
具体来说,如果派生类没有重写基类的虚函数func,则派生类还是用func的基类版本,则派生类和基类的对象的隐藏指针成员执行的虚函数表中存储的func函数地址一样,都是基类的那个版本。
如果派生类重写了虚函数func,则基类对象的隐藏指针成员指向的虚函数表中存储了基类版本func的地址,而派生类对象的隐藏指针成员指向的虚函数表中存储了派生类版本的func的地址。
虚函数的空间和时间成本
虚函数机制如上,在内存和执行速度方面都有一定成本:
内存上
- 每个对象都需要增大一点空间,用于存储隐藏指针成员。如果类中没有虚函数,那么该类的对象不会有这个隐藏成员哈。
- 编译器需要给每一个有虚函数的类创建一个数组,作为他们的虚函数地址表。
速度上
- 每次调用虚函数,需要比非虚函数的调用多执行一个操作:到虚函数表中查找函数地址。
所以非虚函数的效率更高一些,但是只有虚函数才可以利用动态联编实现牛逼的多态,这是非虚函数无法做到的。
其他知识点
虚函数的“传递性”(派生链)
在基类中声明为虚函数的方法,在派生类,以及派生类的派生类,····,中都是虚函数。前面说过了,只有基类方法原型前面的virtual关键字才管事儿,但是在派生类中方法原型前面加一个也无关痛痒,但编译器会当你没加一样的看待。
如果从A类派生了AA类,从AA类又派生了AAA类,从AAA类又派生了AAAA类·····那么这条派生链条中,后面的类会使用最新的虚函数版本。即如果A类把成员函数func定义为虚函数,AA没修改定义,AAA改了定义,那么AA用的A类的func版本,AAA类对象和AAAA类对象用AAA修改的func版本。
这很好理解,因为前面说虚函数底层原理时说了,如果基类有虚函数,那么基类和后续所有派生类的对象都会被编译器添加一个隐藏指针成员,存储该类的虚函数地址表的地址。每个类的虚函数表中的虚函数的地址都是自己修改的函数版本的地址,或者自己上一层的对自己而言的基类的函数版本的地址。
构造函数不可以是虚函数
因为派生类并不继承基类的构造函数。继承中,派生类都会自己写自己的构造函数。并且创建派生类对象时,都是先调用派生类的构造函数,然后在派生类的构造函数中自动调用基类的某一个原型匹配的构造函数。
虚函数是为了继承中,需要修改基类方法定义的场合使用的。并不继承构造函数,所以无需将构造函数定义为虚函数。
析构函数一定要定义为虚函数,只要类被作为基类
就算基类的析构函数的函数体是空的,即什么也不做,也要显式写出来:
virtual ~Brass(){}
这样做是为了确保程序一定要先调用派生类析构然后调用基类析构,因为这样的顺序才可以确保派生类的新数据成员被释放。比如:
Brass * pb = new BrassPlus;
delete pb;//调用~Brass() or ~BrassPlus() ???
如果~ Brass()不是虚函数,则程序静态联编,根据指针pb的类型是基类,判断出来该调用~ Brass(),于是BrassPlus新增的数据成员们就不能在这被释放。
但是如果~ Brass()是虚函数,则动态联编,根据pb指向的对象时BrassPlus类型,于是调用~ BrassPlus(),再在~ BrassPlus()中调用~ Brass(),成功把新旧数据成员都释放了。保证不会出错。
如果类不是基类,即没被继承,那就无需把析构函数声明为虚函数。但是如果你非要将其声明为虚函数,也不会造成语法错误,只不过效率上差一点罢了(因为本可以使用静态联编的时候使用了动态联编)。
友元函数不可为虚函数(因为虚函数只针对成员函数)
这个太简单了,友元函数是friend,不是member,根本不是成员函数,更何谈虚函数呢?虚函数是只针对成员函数的。“虚函数”是“虚成员函数”的简称。
但是在友元函数中可以调用虚成员函数。
如果基类的虚方法被重载(同名不同特征标),则派生类中必须把所有重载版本重写
如果只写其中一个或两个,则没写的那些就会被隐藏。
示例
基类
//重载方法
virtual void ViewAcct() const;
virtual void ViewAcct(int n) const;
virtual void ViewAcct(double x) const;
void Brass::ViewAcct() const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
restore(initialState, prec);
}
void Brass::ViewAcct(int n) const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
int i;
for (i = 0; i < n; ++i)
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
restore(initialState, prec);
}
void Brass::ViewAcct(double x) const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
std::cout << "x is " << x << '\n';
restore(initialState, prec);
}
派生类
//基类重载方法,必须全部重写
virtual void ViewAcct() const;
virtual void ViewAcct(int n) const;
virtual void ViewAcct(double x) const;
void BrassPlus::ViewAcct() const//虚方法
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
Brass::ViewAcct();//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
void BrassPlus::ViewAcct(int n) const
{
format initialState = setFormat();
int i;
for (i = 0; i < n; ++i)
{
precis prec = std::cout.precision(2);
Brass::ViewAcct();//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
}
void BrassPlus::ViewAcct(double x) const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
Brass::ViewAcct();//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
std::cout << "x is " << x << '\n';
restore(initialState, prec);
}
主程序
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
int main()
{
using std::cout;
using std::cin;
Brass ross("Ross Galler", 123786, 5600.0);//基类对象
BrassPlus mona("Mona White", 467838, 4500.0);//派生类对象
ross.ViewAcct();
cout << '\n';
ross.ViewAcct(2);
cout << '\n';
ross.ViewAcct(2.0);
cout << '\n';
mona.ViewAcct();
cout << '\n';
mona.ViewAcct(2);
cout << '\n';
mona.ViewAcct(2.0);
cout << '\n';
return 0;
}
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00
x is 2.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $5e+002
Owed to bank: $0
Loan rate: 11.1%
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
x is 2.000
In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
如果我在派生类中把三个同名函数的任何一个或两个的定义删除,则相当于没定义该函数,则剩余的即派生类定义了的同名函数就会隐藏基类的特征标和派生类未定义的函数一样的函数,于是报错
比如,我删除了派生类对void BrassPlus::ViewAcct(double x) const的定义,则void BrassPlus::ViewAcct() const和void BrassPlus::ViewAcct(int n) const会隐藏void Brass::ViewAcct(double x) const,使得下面代码出错,因为基类的也被隐藏了,自己类又没有这个函数
undefined reference to `BrassPlus::ViewAcct(double) const