C++基础(十二)
<第十三章—动态联编与静态联编、抽象类>
一、联编
程序调用函数时,将源代码中的函数调用解释为执行特定地函数代码块称为函数名联编。在编译过程中进行联编被称为静态联编。然而虚函数以及函数重载地存在使得静态联编出现困难,因为编译器可能不知道到底使用的是哪一个函数。
编译器生成能够在程序运行时选择正确地虚方法地代码,被称为动态联编,又称为晚期联编。
如果派生类不重新定义基类的方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。C++编译器默认使用静态联编。
1、虚函数的工作原理
通常编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组被称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。
使用虚函数时,在内存和执行速度上都有一定的成本:
每个对象都将增大,增大量为存储地址的空间
对于每个类,编译器都创建一个虚函数地址表
对于每个函数调用,都要执行一项额外的操作,即到表中查找地址
2、对于基类与其派生类,析构函数应当是虚函数,除非类不作为基类使用。如果不把基类函数定义为虚函数,假设创建一个基类指针指向一个派生类对象:
基类*p = new 派生类;
那么在释放内存的时候,只会释放该派生类对象中基类的那一部分数据,而不会释放派生类对象的独有数据。将基类析构函数设置为虚函数的话,编译器就会首先调用基类析构函数释放积累数据,接着调用派生类析构函数释放派生类对象。
3、友元函数不能是虚函数,因为友元函数其实并不属于类成员。
4、声明与定义虚函数时,需要保证虚函数的特征完全一致:返回值类型,函数名,传入参数等。
如果积累定义虚函数与派生类中重新定义的函数特征不一致,比如传入参数类型、数目不相同,那么派生类的新函数将会隐藏基类当中的函数,并不会引发函数重载那样的效果。
两条经验:
第一,重新定义继承答方法,应确保与原来的原型完全相同,但如果返回类型时基类引用或者指针,则可以修改为指向派生类的引用或者指针。这种特性被称为返回类型协变。
第二,如果基类声明被重载了(包含多个重载函数),如果还想使用虚方法,那么应当在派生类中重新定义所有的基类版本。
二、访问控制:protected
到目前为止,创建类使用到了关键字private和public,表示成员是私有/公有的。公有成员可以作为类的接口使用,允许类外部函数调用,但私有成员是受保护的,仅仅可以类内部成员使用。
除了上面的两中类别,还存在另外一种访问类别,使用protected表示,其特性与private类似,在类外只能用public类成员来访问。protected和private的区别在于基类与派生类之间的关系。**派生类的成员可以直接访问protected的成员,但是不能直接访问private类型的成员。**对于外界来说,保护成员与私有成员类似,但是对于派生类来说,保护类相当于共有类。
三、抽象基类
与简单继承和较复杂的多态继承相比,抽象基类更为复杂。假设有两个类具有很多共性,那么可以把其中一个类作为基类,另外一个类作为其派生类(书上给的例子是圆和椭圆),但是由于椭圆中的一些特性,圆也用不到,圆作为基类的话又无法满足椭圆的要求,因此更好的办法就是在圆和椭圆当中抽取他们的共有特性作为基类,然后圆和椭圆都作为派生类继承其基类特性。
在C++当中对于未实现的函数,将其定义为纯虚函数,纯虚函数的声明结尾处添加=0;当类声明中包含纯虚函数时,则不能创建该类的对象。也就时说,包含纯虚函数的类只能作为(抽象)基类。
当函数被声明为纯虚函数时,那么该函数可以定义也可以不定义。
使用上节笔记当中的Brass和BrassPlus类作为模板继续完成,在这两个类当中抽象出来一个基类,然后这两个类变为其派生类。
1、头文件
#pragma once
#include"list.h"
class BaseBrass
{
private:
string name;
double balance;
long accnum;
public:
BaseBrass(const string& s = "none", long acc = 0, double b = 0);
void Deposit(double amt);
virtual void withdraw(double amt)=0; //纯虚函数声明,无法实例化该基类
double show_balance()const{ cout << balance<<endl; return balance; }
virtual void show_info()const;
virtual ~BaseBrass(){}
protected:
const string& fullname()const { return name; }
long Acctnum()const { return accnum; }
};
class Brass:public BaseBrass
{
public:
Brass(BaseBrass&b);
virtual void show_info()const { BaseBrass::show_info(); } //显示的内容一样,调用原函数即可
virtual ~Brass(){}
};
class BrassPlus :public BaseBrass
{
private:
double maxLoan; //最大可借款
double rate; //利率
double owesbank; //银行欠款
public:
BrassPlus(const string& s = "none", long an = 0, double bl = 0, double ml = 0, double ra = 0);
BrassPlus(const BaseBrass& b, double ml = 0, double ra = 0);//另外一种写法,使用复制构造函数
virtual void withdraw(double amt);
virtual void show_info()const; //显示信息
void ResetMax(double m) { maxLoan = m; } //重置最大额度
void ResetRate(double r) { rate = r; } //重置利率
void ResetOwes() { owesbank = 0; } //清零欠款额度
};
2、.cpp文件
由于在原基础上进行操作,因此改来改去显得代码逻辑很乱,不过最简单的办法就是把由于基类发生变化,因此把作用域限定名改一下,然后不可调用的成员改一下就可以了:
#include "Brass.h"
BaseBrass::BaseBrass(const string& s, long acc, double b)
{
name = s;
accnum = acc;
balance = b;
}
void BaseBrass::withdraw(double amt)
{
if (amt < 0)
cout << "not allowed!" << endl;
else if (amt <= balance)
balance -= amt;
else
cout << "unable!" << endl;
}
void BaseBrass::Deposit(double amt)
{
balance += amt;
}
void BaseBrass::show_info()const
{
cout << "name: " << name
<< " " << "Accountid: " << accnum
<< " " << "Balance: " << balance << endl;;
}
Brass::Brass(BaseBrass &t):BaseBrass(t)
{
;//没数据所以不用初始化啥
}
BrassPlus::BrassPlus(const string& s, long an, double bl, double ml, double ra) : BaseBrass(s, an, bl)
{
maxLoan = ml;
rate = ra;
owesbank = 0;
}
BrassPlus::BrassPlus(const BaseBrass& b, double ml, double ra):BaseBrass(b)
{
maxLoan = ml;
rate = ra;
owesbank = 0;
}
void BrassPlus::show_info()const
{
BaseBrass::show_info();
cout << "max load:" << maxLoan
<< " "
<< "rate is: " << rate
<< " "
<< "owesbank is:" << owesbank << endl;;
}
void BrassPlus::withdraw(double amt)
{
double bal = show_balance();
if (amt <= bal)
BaseBrass::withdraw(amt);
else if (amt <= (bal + maxLoan - owesbank))
{
double advanced = amt - bal;
owesbank = advanced * (1 + rate); //更新后的欠款
cout << "BANK ADVANCED: " << advanced << endl;
cout << "rate charge:" << advanced * rate
<< endl;/*当前欠款的手续费*/
Deposit(advanced);
BaseBrass::withdraw(amt);
}
else
cout << "unable!" << endl;
}
3、使用该方法
如果不是为了验证知识点,其实完全没必要再抽象基类,因为抽象之后相当于把原来的基类内容抽空了,可有可无。
int main()
{
BrassPlus str1("jieck", 1000, 1000, 2000, 0.001);
str1.show_info();
......
完成了基本操作。需要注意的是,基类对象不允许实例化
BaseBrass str2("jieck", 1000, 1000); //报错显示,不允许实例化该基类
总结:抽象基类属于是比较好的方法,可以用来处理很多复杂关系,提取公共的特征,然后作为基类,其他类只需要继承就好了。