文章目录
关键字protected(带来方便同时带来危险,最好不用)
关于访问控制,即访问类的私有数据成员,之前一直是只用private和public来控制的。现在介绍一个新工具,关键字protected,但他并不是一个多好的东西,有利有弊,是一把双刃剑,使用双刃剑一定要十分小心,很容易杀敌一万自损八千,其实最好不要用这个关键字。下面仔细解释我说的这些。
private数据成员,外部不可以访问,必须通过类的公有接口访问。所以上篇文章写银行账户取款方法和存款方法的时候,访问账户余额必须要通过公有方法Balance()才可以访问到私有数据成员balance,看似麻烦(要多写一个返回私有数据成员的值的方法),实则安全。因为银行账户类的设计,希望账户余额绝对安全,只能让存款取款方法访问和改写余额的值,这样,余额不可能被其他方法修改,是很安全的。
现在有了关键字protected,我们为了方便,不想写Balance()方法,于是干脆把余额设为保护成员,而非私有成员
protected:
double balance;
保护成员的双重身份:
保护成员对于外部世界和私有成员一样,不可以被直接访问,必须通过类的公有接口;
但是对于派生类,以及派生链条上的所有类,保护成员却和公有成员一样,任何派生类都可以直接访问他们。
对于我们的程序,在plus会员派生类中确实不需要写Balance()方法访问balance成员了,可以直接在派生类的方法中访问,方便了,但也危险了,因为派生类的任何方法都可以修改余额,派生类的派生类,派生链条上的所有类,的公有方法都可以随意修改余额。。。你敢把钱存到这家银行吗?
所以protected是一个给成员函数偷懒的好工具,但是如果不是脑子里很清楚被派生类随意访问并无大碍,那就不要用保护成员,乖乖设置为私有成员(C++的开创者Stroustrup都是这么建议的),然后写一个返回其值的公有方法,并不很费神,但百分百安全,毫无后顾之忧,何乐而不为呢?
balance是私有成员,用Balance()访问,下面是派生类取钱方法的代码,要用double bal = Balance();,如果balance是保护成员,则后面都直接用balan
ce了
void BrassPlus::Withdraw(double amt)//虚方法
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
double bal = Balance();
if (amt < 0)
std::cout << "Withdraw amount must be positive!\n"
<< "Withdraw cancelled!\n";
else if (amt <= bal)//不可写amt <= balance,因为不能直接访问基类数据成员
Brass::Withdraw(amt);//不可写balance -= amt;
else if(amt - bal + owesBank <= maxLoan)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);//rate是BrassPlus类的私有成员
std::cout << "Bank advance: $" << advance << '\n';
std::cout << "Finance charge: $" << advance * rate << '\n';
//分两步实现扣除账户全部余额(由于基类不允许取款金额超出余额,所以只能先存再取)
Deposit(advance);//放贷
Brass::Withdraw(amt);
}
else
std::cout << "Credit limit exceeded. Transaction cancelled.\n";
restore(initialState, prec);
}
抽象基类和纯虚函数(is-a关系用公有继承实现有时候也不太合适)
前面说了有三种继承——公有继承,私有继承,保护继承。
但是目前还是一直在讨论公有继承。
我们说到基类和派生类的五种常见关系:is-a, has-a, is-like-a, uses-a, is-implemented-by-a,其中只有is-a用公有继承来实现是比较好的,其他四个关系都不适合公有继承。
但是现在我们又要说,is-a关系和公有继承之间也不是百分百合拍,完美和谐。有时候,is-a关系不能简单使用公有继承,否则麻烦不断后患无穷,总之不是好的设计。
用圆和椭圆的笨拙派生为例,挑拨is-a和公有继承的搭档关系
数学上,圆是一种特殊的椭圆,是椭圆的一个特例,特殊在于长半轴和短半轴长度相等。于是满足is-a关系,于是我们使用公有继承,从Ellipse类派生出Circle类。
Ellipse类声明,私有数据成员包括位置坐标,长短半轴长度,角度。方法有移动,旋转,缩放。
由于Circle类也要用这三种方法,所以都设置为了虚函数。
由于Ellipse类要成为基类,所以设置了虚析构函
数。
//ellipse.h
#ifndef ELLIPSE_H_
#define ELLIPSE_H_
class Ellipse{
private:
double x;//位置横坐标
double y;//位置纵坐标
double a;//长半轴
double b;//短半轴
double angle;//x轴和长轴的夹角
public:
Ellipse(double nx = 0.0, double ny = 0.0, double na = 1.0, double nb = 1.0, double ang = 0.0):x(nx), y(ny), a(na), b(nb), angle(ang){}
virtual ~Ellipse(){}
virtual void Move(double mx, double my){x = mx;y = my;}
virtual void Rotate(double ang){angle += ang;}
virtual void Scale(double sx, double sy){a *= sx; b *= sy;}
};
#endif // ELLIPSE_H_
于是Circle类的声
明是
//circle.h
#ifndef CIRCLE_H_
#define CITCLE_H_
class Circle : public Ellipse
{
public:
Circle(double nx, double ny, double a, double b):x(nx), y(ny), a(r), b(r){}
};
#endif // CIRCLE_H_
写Circle类声明的时候,会发现很多不太好的地方:
- Cirlcle类本不需要长半轴,短半轴和角度三个数据成员,可是公有继承使得Circle被迫有了这几个不相干的成员,就好像你家里非要养几个别人的孩子,显然是不合理的。每个Circle对象都要多几个完全用不着的成员。
虽Circle的构造函数把半径传给a,b(Ellipse类的轴),但用两个变量来表示半径很冗余。
- Circle类本不需要旋转方法,圆旋转是没有意义的。但是公有继承使它被迫有了。
- Circle类虽然不需要改写移动,缩放方法,但是方法的实现里用了轴,但是圆本没有轴,虽然计算和树枝上没错,但总觉得怪拐的。
所以公有继承并不适用于圆和椭圆的这种is-a关系。
替代笨拙继承的办法1:单独定义Circle类(不做父子,自立门户)
一种办法是,直接单独定义Circle类,和Ellipse类毫无瓜葛,两个独立
的类
//circle.h
#ifndef CIRCLE_H_
#define CITCLE_H_
class Circle{
private:
double x;//位置横坐标
double y;//位置纵坐标
double radius;//半径
public:
Circle(double nx, double ny, double r):x(nx), y(ny), radius(r){}
~Circle(){}
double Area() const {return 3.1415 * radius * radius;}
void Move(double mx, double my){x = mx;y = my;}
void Scale(double s){radius *= s;}
};
#endif // CIRCLE_H_
现在Circle无需再包含自己不需要的成员
替代笨拙继承的办法2:ABC, 抽象类(做不了父子就做兄弟)
第一种办法虽然可以解决问题,但终究只是次优解。因为它还是造成了冗余。冗余来自于椭圆和圆的诸多共性。
前面因为他们的共性,我们使用了公有继承,但是发现他们的不同之处使得公有继承并不合适,所以我们又取消了公有继承,直接划分界限,彼此独立,大家都单干。但是这样虽然是一个办法,但是我们等于是蒙住自己的眼睛,让自己看不见圆和椭圆的共性。不能因为二者的相异之处来阻挡一下,就放弃融合二者的共性。
那到底怎么拉拢像这种情况下的共性呢?
C++给出的答案是抽象基类。就像Java的抽象类一样。(所以以前的那些类都是具体类concrete class)
它的思路是:
圆和椭圆有共性,是is-a,但是又不适合公有继承,为了避免二者自立门户带来的冗余,就把二者的共性剥离出来,用这些共性凌驾于圆和椭圆之上,单独建立一个基类,把共性都封装在这个基类里。
由于共性是通用的,所以是抽象的,所以把这个基类叫做抽象基类。Abstract base class。
让Circle类和Ellipse类都去继承这个抽象基类。
简单地说,就是做不了父子就做兄弟,找一个共同的家长。
这样做不仅不再有冗余,并且每个类都只有自己需要的成员,更重要的是,可以建立指向基类对象的指针数组,同时管理Ellipse类对象和Circle类对象,从而实现多态。
说了解决方案,我们看看具体实现,会不会再碰到什么问题,从而引出什么新的工具呢?(套路脸)
Circle类和Ellipse类的共性有:
数据:
- 中心坐标,即横坐标,纵坐标两个数据成员
方法:
- 计算面积的方法,两个类实现不同;且要用到抽象基类没有的数据成员(圆:半径;椭圆:长半轴,短半轴)
- 移动方法,两个类实现相同;且只需要用抽象基类的数据(中心点的横纵坐标)
- 缩放方法,两个类实现不同;且要用到抽象基类没有的数据成员(圆:半径;椭圆:长半轴,短半轴)
问题来了:移动方法好说,那缩放和面积怎么写代码,抽象基类中没法写他俩的定义啊。也不能只写个原型,前面有教训了,只有原型没有定义的函数在编译器势利的眼睛里形同虚设。
所以C++只好再出手,祭出一个纯虚函数(这名字有没有很玄乎的感觉,又纯又虚,倒是很适合做仙侠玄幻类影视作品里某个神座的名字,纯虚元君,纯虚真人,哈哈哈,开脑洞了)
抽象基类至少有一个纯虚函数
纯虚函数由于是“纯粹虚无”的,所以一般是不会写他的具体定义的。他只是描述了一个接口,具体怎么实现还是派生类自己去根据需要实现,注意纯虚函数在派生类时自动成为虚函数哈。
之前说的虚函数是有定义的哦,只是派生类可以重写新定义从而隐藏基类的旧定义从而使得虚函数的旧定义似有还未似无还有虚无缥缈。但是纯虚函数自然是要更虚的,虚无到直接没有定义了。
但是纯虚函数也可以有定义的哦。只是一般不写。
方法原型后的 = 0 指出类的身份是ABC。
一个抽象基类至少有一个纯虚函数。
至少有一个纯虚函数才能成为抽象基类。
所有虚函数中至少有一个是纯虚的。
有几点需要注意:
- 类声明有纯虚函数,则该类只能作为抽象基类,且不可以创建该类的对象。
即不能创建抽象基类的对象。因为抽象基类是抽象的,没法实例化一个具体的对象出来。只有具体类才可以创建自己的对象,因为他们够具体,他们的对象有具体的数据成员和具体的操作方法。
但是可以创建该类的指针或引用,以管理他的派生类。
- 如果抽象基类的所有方法,包括纯虚函数,都有定义(可以在头问价写内联定义,也可以在方法文件写定义)。那也要把这个类声明为抽象的——通过在所有函数的原型后面加上 = 0。
- ABC的纯虚函数最后都会被派生类的实现所覆盖,隐藏。
示例 圆
和椭圆
//BaseEllipse.h
#ifndef BASEELLIPSE_H_
#define BASEELLIPSE_H_
class BaseEllipse{
private:
double x;
double y;
public:
void Move(double nx, double ny) {x = nx; y = ny;}
virtual void Scale(double s) =0;//´¿Ð麯Êý
virtual double Area() const =0;//´¿Ð麯Êý
};
#endif // BASEELLIPSE_H_
抽象基类的开发设计,对组件编程的影响
开发ABC就像开发一个接口,是抽象的,这对于基于组件的编程模式很常见,ABC的设计人员设计了纯虚函数等“接口约定”,派生类具体类的设计人员就要严格遵循ABC接口规则来编程,所以由ABC派生出来的所有组件全部都支持ABC想要的功能。
只要可以,就设计包含纯虚函数的抽象基类,这是一个很好很好的工具,然后从它派生出其他类。
示例 银行账户
设计开发ABC之前,应该仔细分析编程问题所需要的类,以及这些类之间的关系,是否有共性以设计一个ABC类。
要把这些类的继承层次梳理清楚,把那些不会被继承的类设计为具体类。
代码
三个类的声明放
在一起
//AcctABC.h
#ifndef ACCTABC_H_
#define ACCTABC_H_
class AcctABC{
private:
//这三个成员不可以是保护成员,那样不安全
std::string fullName;
long acctNum;
double balance;
protected:
//保护数据成员,派生类对象可以访问
struct Formatting
{
std::ios_base::fmtflags flag;
std::streamsize pr;
};
//保护成员方法,提供了对客户名等数据的只读访问
const std::string & FullName() const {return fullName;}
long AcctNum() const {return acctNum;}
Formatting SetFormat() const;
void Restore(Formatting & f) const;
public:
AcctABC(const std::string & s = "None None",
long an = -1, double bal = 0.0);
virtual ~AcctABC(){}
void Deposit(double amt);//普通会员和plus会员存钱方法一样,直接在抽象基类定义
virtual void WithDraw(double amt) = 0;//普通会员和plus会员存钱方法不一样,在抽象基类定义定义为虚函数,在派生类修改重写
//这里不好写定义,要重写,所以是纯虚函数
double Balance() const {return balance;}//不需要重写,所以不是虚方法
virtual void ViewAcct() const = 0;//这里不好写定义,要重写,所以是纯虚函数
};
class Brass : public AcctABC
{
public:
Brass(const std::string & s = "None None", long an = -1,
double bal = 0.0):AcctABC(s, an, bal){}
~Brass(){}
virtual void WithDraw(double amt);
virtual void ViewAcct() const;
};
class BrassPlus : public AcctABC
{
private:
double maxLoan;
double rate;
double owesBank;
public:
BrassPlus(const std::string s = "None None", long an = -1,
double ba = 0.0, double max = 500.0,
double r = 0.1125, double owe = 0.0):AcctABC(s, an, ba), maxLoan(max), rate(r), owesBank(owe){}
~BrassPlus(){}
virtual void WithDraw(double amt);
virtual void ViewAcct() const;
void ResetMax(double m){maxLoan = m;}
void ResetRate(double r){rate = r;}
void ResetOwes(){owesBank = 0.0;}
} ;
#endif // ACCTABC_H_
三个类的方法放
在一起
#include <iostream>
#include "AcctABC.h"
using std::cout;
//AcctABC methods
AcctABC::AcctABC(const std::string & s, long an, double bal)
{
fullName = s;
acctNum = an;
balance = bal;
}
void AcctABC::Deposit(double amt)
{
if (amt < 0)
cout << "Negative deposit not allowed!"
<< " deposit is cancelled.\n";
else
balance += amt;
}
void AcctABC::WithDraw(double amt)
{
if (amt < 0)
cout << "Negative withdraw not allowed!"
<< " withdraw is cancelled.\n";
else
balance -= amt;
}
AcctABC::Formatting AcctABC::SetFormat() const
{
//set up ###.## format
Formatting f;
f.flag = cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
f.pr = cout.precision(2);
return f;
}
void AcctABC::Restore(Formatting & f) const
{
cout.setf(f.flag, std::ios_base::floatfield);
cout.precision(f.pr);
}
//Brass methods
void Brass::WithDraw(double amt)
{
if (amt < 0)
cout << "Negative withdraw not allowed!"
<< " withdraw is cancelled.\n";
else if (amt <= Balance())
AcctABC::WithDraw(amt);
else
cout << "Withdraw amount of $" << amt
<< " exceed account balance.\n"
<< "Withdraw cancelled!\n";
}
void Brass::ViewAcct() const
{
Formatting f = SetFormat();
cout << "Brass Client: " << FullName() << '\n'
<< "Account Number: " << AcctNum() << '\n'
<< "Balance: $" << Balance() << '\n';
Restore(f);
}
//BrassPlus methods
void BrassPlus::WithDraw(double amt)
{
Formatting f = SetFormat();
if (amt < 0)
cout << "Negative withdraw not allowed!"
<< " withdraw is cancelled.\n";
else if (amt <= Balance())
AcctABC::WithDraw(amt);
else if (amt <= Balance() + maxLoan - owesBank)
{
double advance = amt - Balance();
Deposit(advance);
AcctABC::WithDraw(amt);//ук╩╖н╙0
owesBank += advance * (1.0 + rate);
cout << "Bank advance: $" << advance << '\n'
<< "Finance Charge: $" << advance * rate << '\n';
}
else
cout << "Withdraw amount of $" << amt
<< " exceed account's overdraft limit.\n"
<< "Withdraw cancelled!\n";
Restore(f);
}
void BrassPlus::ViewAcct() const
{
Formatting f = SetFormat();
cout << "BrassPlus Client: " << FullName() << '\n'
<< "Account Number: " << AcctNum() << '\n'
<< "Balance: $" << Balance() << '\n'
<< "Max Loan: $" << maxLoan << '\n'
<< "Owed Bank: $" << owesBank << '\n';
cout.precision(3);
cout << "Loan Rate: " << rate * 100 << "%\n";
Restore(f);
}
主程序和继承的第二篇文章里面的基类指针数组实现多态的主程序一样,只是基类改为了抽
象基类
//main.cpp
#include <iostream>
#include "AcctABC.h"
const int NUM = 4;
void eatline();
int main()
{
using std::cout;
using std::cin;
AcctABC * p_clients[NUM];//抽象基类指针数组
std::string tempName;
long tempAcct;
double openBal;
char kind;//会员种类,普通会员或plus会员
int i;
for (i = 0; i < NUM; ++i)
{
cout << "Enter client's name: ";
getline(cin, tempName);
cout << "Enter client's accout number: ";
cin >> tempAcct;
eatline();
cout << "Enter opening balance: $";
cin >> openBal;
eatline();
cout << "Enter 1 for Brass Account or 2 for BrassPlus Account:";
while ((kind = cin.get()) != '1' && (kind != '2'))
continue;
eatline();//否则换行符会被读取为下一个客户的名字
if (kind == '1')
{
p_clients[i] = new Brass(tempName, tempAcct, openBal);
cout << '\n';
}
else if (kind == '2')
{
cout << "Enter the overdraft limit: ";
double tempLimit;
cin >> tempLimit;
eatline();
cout << "Enter the interest rate: ";
double tempRate;
cin >> tempRate;
eatline();
p_clients[i] = new BrassPlus(tempName, tempAcct, openBal, tempLimit, tempRate);
cout << '\n';
}
}
//显示四位顾客的信息
for (i = 0; i < NUM; ++i)
{
cout << '\n';
p_clients[i]->ViewAcct();//展示多态特性的核心代码
delete p_clients[i];//总是记不住delete
}
return 0;
}
void eatline()
{
while (std::cin.get() != '\n')
;
}
`
``
```cpp
Enter client's name: gfd fg
Enter client's accout number: 456465
Enter opening balance: $456456
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Enter client's name: adsfasd fdfd
Enter client's accout number: 465465
Enter opening balance: $456456
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 465465
Enter the interest rate: 0.2
Enter client's name: fasdf df
Enter client's accout number: 4564564
Enter opening balance: $4545
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 454
Enter the interest rate: 0.1
Enter client's name: fdasf
Enter client's accout number: 465
Enter opening balance: $554
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Brass Client: gfd fg
Account Number: 456465
Balance: $456456.00
BrassPlus Client: adsfasd fdfd
Account Number: 465465
Balance: $456456.00
Max Loan: $465465.00
Owed Bank: $0.00
Loan Rate: 20.000%
BrassPlus Client: fasdf df
Account Number: 4564564
Balance: $4545.00
Max Loan: $454.00
Owed Bank: $0.00
Loan Rate: 10.000%
Brass Client: fdasf
Account Number: 465
Balance: $554.00
遇到的问题
- 保证项目中只有自己需要使用的文件被编译
我习惯只建立一个项目,然后不断添加头文件和方法文件,修改主程序,代码都是保存在博客的。
之前新建了一个文件,后缀不小心写为.c,于是就直接叉掉了,但是他仍然在项目中,且没有生成.o目标代码,于是编译报错:
在项目的路径下没有untitled2.c, debug目录下也没有untiled2.o,很奇怪,但是项目认为还有,所以必须要找到项目的文件目录
code::blocks 菜单栏 点击 项目–属性–编译目标–勾选需要编译的文件, 搞定
其他问题都是小问题,比如方法调用时忘记首字母大写,结果找不到这个方法,但是好多地方都写错了,还是要反省
,不严谨