上篇文章里谈及了基于对象的编程思想,重在封装特性的实现,与其使用一堆基本数据类型来进行操作,为何不将其封装起来同时给其增加一些操作,当在使用类对象的时候可以直接通过调用其成员函数来对其做一些操作,这不是很方便吗?!包括如何设计类:几个特殊的成员函数,如何使用对象。但是啊,C++被称为面向对象的编程,而不是基于对象的编程,显然需要对基于对象的编程再做改进!
问题来了!有了基于对象的思想就够了吗?针对现实的问题其实是不足的,为此有了更进化的思想,面向对象的思想!面向对象,就是说从对象间的关系出发,基于对象呢,不在乎对象之间有何种关系,通通设计成类,独立的使用这些类,其实也能用但是真的很麻烦,代码冗余,切除了对象间的关系!为此,在设计类的时候想全局考虑,需要哪些类,这些类是否存在某种关系,能否利用一个类的设计来升级得出另外一些类(继承通用方法,增加新的方法,改写方法)?这里最常用的技术就是继承!当存在继承关系了,在程序设计时,能否仅仅利用基类表示但是程序执行阶段根据基类所引用的实际对象来决定调用不同的成员函数?(这里就是多态的特性)为了支持多态,除了增加了继承语法,还离不开动态绑定!在基于对象的编程中,类之间没有关系,因而调用方法时,在编译期间根据对象的类型就能确定方法来源。而现在全部都是使用基类对象来调用方法,在编译期间无法确定方法的来源,因而动态绑定就是使得方法可以在动态运行阶段来确定!继承有其语法特征,那么动态绑定也必定有其语法特征来使其和静态绑定得以区别,即关键词virtual。
继承能带来什么好处呢?减少代码冗余,为实现多态做好铺垫。
多态能带来什么好处呢?利用一个函数便能处理多个类,本质上还是减少了代码冗余!
总之可以看出面向对象编程可以使得代码更简洁,类的表达更有力,操作类更加简单。
在探讨面向对象的设计细节之前,需要先想好需要哪些类,有哪些方法是所有类所通用的,类之间有何种关系。将这些通用的方法抽象出来就是我们抽象基类。
抽象基类:
step1:找出所有子类的共通操作
step2:判断这些操作哪些是与子类类别有关的(子类不一样,方法实现也不一样),声明为virtual,使得方法的决议在运行阶段进行;静态成员方法不能被声明为虚函数(根本就不包含与对象有关的数据成员,跟调用对象无关)
step3:确定操作的访问权限和层级,若希望操作只能在类中使用则声明为私有成员;操作可在派生类中使用但不能在类外使用,声明为保护成员;操作在类外可使用声明为公有。
看着上面的类声明,面对虚函数我们是否有必要对其进行定义,根据是否需要生成对象来决定,如果抽象基类的用途仅是在于继承作为派生类的子对象,那么不需要定义,但是必须将其声明为纯虚函数!因此一个虚函数,要么有定义,要么是个纯虚函数。不过当利用抽象基类进行派生的时候,派生类一定要为纯虚函数提供定义,否则还是个纯虚函数,还是不能生成对象。
看着上面的抽象基类声明可以发现没有提供构造函数,却提供了以虚的析构函数。现在来解释一下原因:构造函数的目标是什么?初始化对象的非静态数据成员,显然抽象基类不包含这样的数据成员,因此不需要显示提供构造函数。利用系统的默认构造函数就好。析构函数为什么是虚的呢?由于多态的特性,可以定义一个基类的指针使其指向一个动态的内存空间,但在使用delete释放这片空间的时候,我们希望释放实际指向的对象空间而不是一个基类对象空间,为此将析构函数声明为虚函数,且不要将析构函数定义为纯虚函数。
下面就是一个数列的抽象基类和实现,可以看出每个函数要么进行了定义,要么就是纯虚函数:
#include<iostream>
class Num_sequence {
public:
virtual int get_value(int pos) const=0;
static int max_size() {
return max;
}
virtual std::ostream & display(std::ostream &os) const=0;//每个类都需要定义的display函数,为了便于之后<<的重载
virtual ~Num_sequence(){}//析构函数 注意定义为虚函数啊
virtual char* show_sequence_type() const = 0;
protected:
static const int max = 1024;
bool check_illegal(int pos, int size);
virtual void gen_sequence(int pos)=0;
};
#include"num_sequence.h"
bool Num_sequence::check_illegal(int pos, int size)//非虚函数,由于虚函数表的存在,也是可以根据实际调用的对象类型来决定函数所调用的虚函数所属版本
{
if (pos > max || pos < 0)
return false;
if (pos > size)
{
gen_sequence(pos);//这里是个虚函数 当通过指向派生类的指针来调用check_illegal函数时,会根据实际的对象类型到虚函数表查找该函数,遇到gen_sequence的调用,也是会从指向的对象的虚函数表中查找该函数
}
return true;
}
派生类定义:
派生类对象可能包含两个部分(基类构成的子对象和派生类的部分):如果基类有非静态的数据成员,则包含一个基类子对象;如果派生类有新增的非静态的数据成员,该部分则属于派生类对象部分。
注意在下图中,新增了方法length(),由于这个方法不在基类声明,为此使用基类指针(指向派生类对象)无法调用到这个方法的!这里存在一个折中的问题:若是希望能通过指向派生类的基类指针调用方法,那就得在基类中声明方法,但是可能违背抽象基类设计初衷(所有子类通用的方法),因此需要考虑是否真的需要加入到基类中。这里对所有的数列来说,都需要这个方法,因此加入到基类更方便!
定义派生类需要对基类中虚函数做出抉择,保留呢?还是改写呢?如果是纯虚函数,保留基类的此方法,将导致派生类也是抽象类无法创建对象。。希望重写这个虚函数,就要保证在派生类中声明定义时这个方法要和基类的虚函数方法完全一致!(参数列表、返回类型、const性),这是为什么呢?编译器构造虚函数表时,将根据派生类中的函数声明和基类中的函数原型进行匹配,匹配到了,则改写从基类继承来的方法;没有就新增一个;基类中剩余的方法加入(继承)到虚函数表中。而在函数调用时,使用基类指针只能调用到基类中声明的方法,而不能调用到派生类改写的方法(由于原型的不同,相当是新增方法)
有一个例外的情况:基类中虚函数的声明为返回类型为指向基类的指针或是引用时,在派生类中将返回类型更改为指向派生类的指针或是引用。这个可以允许!
在基类中使用virtual声明过虚函数后,在派生类中可以不再使用virtual进行声明(为什么可以不加virtual?根据函数原型来决定派生类中的函数是改写?还是新增?但是基类一定要加,不然真的没办法确定是改写还是新增,若是被认定为新增的方法,那么多态性就无法实现了。。改写为什么可以实现多态?因为改写使得基类存在这个方法(同一个方法在不同类进行改写),进而能达到指向对象不同时方法实现不同),在类外定义的时候可以不加virtual。但是派生类在声明或是定义虚函数的时候,要保证特征标和基类的完全一样。
为什么多态实现时,一定要使用指向基类对象的指针或是引用?而不能使用基类对象呢?下图中,第一个参数发生了值传递,预先分配了符合基类类型的存储空间,在值复制的时候,也只是复制了派生类中的基类对象,由于缺少派生类的数据成员,为此调用方法只能是基类实现的虚函数。而第二三个参数由于使用了址传递,分配的空间存储了实现指向的派生类对象的地址,因此根据这个地址,可以得到派生类中的数据,进而可实现调用派生类实现的虚函数。
公有继承下,基类的公有成员在派生类仍是公有的,可以在类外调用;基类的保护成员仍是保护成员,只能在派生类中直接使用,类外无法调用。
公有继承:
成员访问权限为protected的,可在其派生类中直接使用。成员访问权限为私有的,在派生类中不可以直接使用。
继承语法:
class 新类名:继承方式 已有类名
派生类中构造函数写法:
继承相当于派生类包含基类,为此在创建派生类对象的时候,需要先创建基类对象!当类间存在多层级的继承关系时,只需要调用直接基类的构造函数即可。
在继承关系下,基类和派生类都有相同的方法名(非虚函数),在派生类调用方法时不会调用基类的方法,除非采用类作用域明确指出同名方法的来源。既然是非虚函数,并不是期望派生类的类型不同时,该函数的实现也不同。那么为什么要使用相同的函数名?一点没实现多态特性(不是虚函数,怎么会根据对象的类型来决定调用哪个方法)。这里想说的是,既然基类和派生类的函数特征标相同,那么是希望派生类对象方法覆盖基类方法,因此与多态联系起来,初衷是需要将基类方法声明为虚函数的,那么基类没有声明为虚函数时,派生类又声明了同名的方法,可能出现了编程的小粗心。(与常理不符)实际中,但凡是基类和派生类有同名的方法,基类的方法都需要被声明为虚方法。
让我们进一步拓展!设计出了抽象基类、派生类,目前为止其实函数代码也没减少多少!这和之前说的利用类间的派生关系和多态机制能减少代码的冗余不符啊!那是因为我们没有把这些派生类和基类的使用连在一起,若是单独直接使用各个派生类对象,自然无法显现出继承关系的优势!为了使用这些具有关联的类对象,我们可以针对这些类对象定义出一系列的操作!而这些操作的参数使用基类对象的引用或是指向基类对象的指针!这样一个函数就可以利用不同的子对象,调用不同子对象的方法,实现多样化的效果!这里凸显出了减少代码冗余的功能!
在派生类中使用虚函数,无法达到多态效果!这是为什么呢?因为派生类的构造函数目的是为了初始化派生类对象,这说明派生类对象还没有创建好,用怎么能调用派生类中定义的虚函数(很可能使用了派生类的新增数据成员)。因此派生类的构造函数凡是调用虚函数均是使用基类实现的内容。(这个特点在visual studio2015上已经失效了,即便在派生类构造函数内调用虚函数,也可以调用到正确的虚函数,能满足多态性!)
下面是个对抽象基类进行派生的小例子:
#include<iostream>
#include<vector>
class Num_sequence {
public:
virtual int get_value(int pos)=0;
static int max_size() {
return max;
}
virtual std::ostream & display(std::ostream &os) =0;
virtual ~Num_sequence(){}
virtual char* show_sequence_type() const = 0;
virtual int begin() const=0;
virtual int len() const=0;
protected:
static const int max = 1024;
bool check_illegal(int pos, int size);
virtual void gen_sequence(int pos)=0;
};
class Fon :public Num_sequence {
public:
Fon(int l,int b);
int get_value(int pos) ;
std::ostream & display(std::ostream &os) ;
char* show_sequence_type() const {
return "fon";
}
int begin() const
{
return _beg;
}
int len() const {
return _length;
}
protected:
int _beg;
int _length;
virtual void gen_sequence(int pos) ;
private:
static std::vector<int> values;
};
#include"num_sequence.h"
std::vector<int> Fon::values;
bool Num_sequence::check_illegal(int pos, int size)
{
if (pos > max || pos < 0)
return false;
if (pos > size-1)
{
gen_sequence(pos);//这里是个虚函数 当通过指向派生类的指针来调用check_illegal函数时,会根据实际的对象类型到虚函数表查找该函数,遇到gen_sequence的调用,也是会从指向的对象的虚函数表中查找该函数
}
return true;
}
int Fon::get_value(int pos)
{
if (check_illegal(pos,values.size()))
{
return values[pos];
}
return -1;
}
std::ostream & Fon::display(std::ostream &os)
{
if (_beg + _length > values.size()) {
gen_sequence(_beg + _length - 1);
}
for (int i = 0; i < _beg + _length; i++)
os << values[i] << " ";
os << std::endl;
return os;
}
void Fon::gen_sequence(int pos)
{
for (int i = values.size(); i <= pos; i++)
{
if (i == 0 || i == 1)
values.push_back(1);
else {
values.push_back(values[i - 1] + values[i - 2]);
}
}
}
Fon::Fon(int l, int b)
{
_length = l;
_beg = b;
if (values.size() < _length + _beg)
gen_sequence(_length + _beg - 1);
}
#include"num_sequence.h"
#include<iostream>
std::ostream & operator<<(std::ostream &os, Num_sequence& f);
int get_pos_value(Num_sequence & f,int pos);
int main()
{
Fon f1(1, 3);
//f1.display(std::cout);
//std::cout<<f1.get_value(6);
std::cout << f1;
std::cin.get();
return 0;
}
std::ostream & operator<<(std::ostream &os, Num_sequence& f)//这才是面向对象编程的最大便利之处啊!这样的一个函数可以被多个对象所利用
{
os << f.show_sequence_type() << std::endl;
f.display(os);
return os;
}
int get_pos_value(Num_sequence & f, int pos)//运用类间继承关系、多态优势!
{
return f.get_value(pos);
}
上面所述可以被认为是一种设计思路,抽象基类真的是非常抽象了!由于不包含任何的数据成员,所以抽象基类的方法大都是纯虚函数,这使得在派生类的书写相对比较麻烦!另一种设计思路呢,是稍稍将抽象基类具体化一些,将子类的通用(这里要注意永远不能违反通用性)实现抽离出来放入到抽象基类中,在基类中根据这些数据实现一些方法,这样派生类就不必在重新定义(声明)这些在基类获得实现的函数了!
抽离?如何做到呢?
数据成员写在保护域内,使得在派生类中可以直接使用这些数据成员,而关于这些数据的方法如果需要在类外被访问,放入到公有域中)
对于实际存储数据的变量(比如一个数列类,真实的数据存储在容器中,而且我们知道所有子类的容器表示都为vector<int> ,如何把这个数据成员抽离出来以帮助我们实现更多的通用方法?比如,取某一个位置的元素,显示数列元素值,都是离不开这个数据成员!),可以使用指向这个类型的指针或是引用作为基类的数据成员!
接着说,既然为抽象基类添加了数据成员,就不能像之前的设计那样可以不加构造函数,现在必须要有构造函数对数据成员进行初始化!此外,既然是个抽象基类,我们的目的不在于利用其生成具体的子对象,仅仅是作为派生类对象的子对象使用!为此,构造函数也不应该出现在公有域中!写在保护域是最佳选择!可以被派生类直接使用来初始化派生类对象!
派生类对象包含多个对象(多个基类对象和派生类对象),因此在派生类构造函数中需要在初始化列表中调用直接基类的构造函数,再初始化派生类中的新增成员。这套机制编译时聊熟于心,因此若是没有提供明确基类的构造函数,那么编译器将会调用默认的基类构造函数以初始化基类对象,这是如果没有合适的基类构造函数,很遗憾编译器将会报错!
讲起构造函数,不得不提复制构造函数,他的原理就是各个对象(派生类有多个基类对象)进行逐成员的复制,基类对象的复制通过调用基类的复制构造函数完成(函数调用!!!),只需要在派生类中为新增的数据成员复制即可,如果对象间的逐成员复制可以满足要求,没必要重定义复制构造函数,但是派生类包含指向动态分配内存空间的指针时,就需要重写一个复制构造函数!同理,基类可以没有复制构造函数,派生类的构造函数中也可以不明确调用复制构造函数,那么将调用默认的复制构造函数。
赋值运算符重载函数,也是各个对象逐成员的赋值,对于基类对象,调用基类的赋值运算符重载函数,让这个函数实现逐成员赋值,同时在为新增的派生类成员逐个赋值。在这个函数中,注意调用显示基类的赋值运算符函数!
需要深刻理解的地方是:继承过程是类似于搭积木的过程!上层的派生类是基于下面的基类,基于也是利用,通用的东西直接调用基类的实现,而不是一个个去写,那就是基于对象的单纯想法!所以当创建一个派生类对象的时候,并不是显示指出所有的初始化步骤,而是调用已经实现好的构造函数,而已经实现好的构造函数中的实现可能又是调用其他实现好的构造函数,多次调用直到完成所有数据成员的初始化。多次调用也是编译器能理解的方式!
#include<iostream>
#include<vector>
class Num_sequence {
public:
int get_len() {
return _len;
}
int get_beg() {
return _beg;
}
int get_value(int pos);
virtual char * seq_type() = 0;
void diaplay(std::ostream &os) ;
static int max_size() { return size; }
virtual ~ Num_sequence(){}
protected:
Num_sequence(int l,int b, std::vector<int> &c):copy(c){
_len = l;
_beg = b;
}
virtual void gen_seq(int pos)=0;
const static int size = 1024;
int _len;
int _beg;
std::vector<int> ©
};
class Fon :public Num_sequence {
public:
Fon(int l,int b):Num_sequence(l,b,num){
if (num.size() < _len + _beg)
gen_seq(_len + _beg);
}
virtual char * seq_type() {
return "fon";
}
protected:
std::vector<int> num;
virtual void gen_seq(int pos) ;
};
int Num_sequence::get_value(int pos)
{
if (pos > copy.size() - 1)
{
//std::cout << "aa";
gen_seq(pos);//虚函数
}
return copy[pos];
}
void Num_sequence::diaplay(std::ostream &os)
{
if (_beg + _len > copy.size())
gen_seq(_beg + _len - 1);
for (int i = _beg; i < _beg + _len; i++)
os << copy[i] << " ";
os << std::endl;
}
void Fon::gen_seq(int pos)
{
for (int i = num.size(); i <= pos; i++)
{
if (i == 0 || i == 1)
num.push_back(1);
else {
num.push_back(num[i - 1] + num[i - 2]);
}
}
}
#include"num_sequence.h"
int main()
{
Fon f1(20,0);
f1.diaplay(std::cout);
std::cout<<f1.get_value(20);
std::cout << f1.seq_type() << std::endl;
std::cin.get();
return 0;
}
析构函数,则是先调用派生类对象的析构函数,在调用基类的析构函数。
RUN-TIME TYPE IDENTIFICATION(RTTI):
为什么需要这种语法呢?
之前通过虚函数、继承、指向基类的指针或是引用机制,我们已经实现了多态,通过基类引用类型或是指针可以达到根据实际的派生类对象来调用派生类中实现的方法。可以使用基类指针永远也无法调用到派生类中新增的方法!这个时候,我们如果可以知道基类指针所指向的对象的真实类型,并进行类型转化为派生类的类型后来调用派生类中的方法!
简单说,就是利用已知的基类指针尝试做类型转化来调用派生类中新增的方法!
这里比较常用的方法是dynamic_cast(),将基类指针转化为派生类指针。
#include<string>
#include<vector>
typedef std::string Eletype;
class Stack {
public:
Stack(int capcity=0) {
_top = 0;
if(capcity)
_eles.reserve(capcity);
}
bool push(Eletype &);
bool pop(Eletype &);
bool isfull() const;
bool isempty() const;
int get_top() const;
std::ostream& diaplay(std::ostream &os) const;
virtual bool get_value(int pos, Eletype &result) ;
protected:
std::vector<Eletype> _eles;
int _top;
};
class Peekstack:public Stack {
public:
Peekstack(int c=0):Stack(c){}
bool get_value(int pos, Eletype &result) ;
};
#include"stack.h"
bool Stack::push(Eletype & a)
{
if (isfull())
return false;
_eles.push_back(a);
_top++;
return true;
}
bool Stack::pop(Eletype &a)
{
if (isempty())
return false;
a = _eles.back();
_eles.pop_back();
_top--;
return true;
}
bool Stack::isfull() const
{
if (_eles.size() >= _eles.max_size())
return true;
else
return false;
}
bool Stack::isempty() const
{
if (_eles.size() <= 0)
return true;
else
return false;
}
int Stack::get_top() const
{
return _top;
}
std::ostream& Stack::diaplay(std::ostream &os) const
{
auto it = _eles.rbegin();
while (it != _eles.rend())
os << *it++ << " ";
os << std::endl;
return os;
}
bool Stack::get_value(int pos, Eletype &result)
{
return false;
}
bool Peekstack::get_value(int pos, Eletype &result)
{
if (pos<0 || pos>_eles.size()||isempty())
return false;
result = _eles[pos];
return true;
}
#include"stack.h"
#include<iostream>
std::ostream & operator<<(std::ostream & os, const Stack &);
void peek(Stack & a,int pos);
int main()
{
Stack s1;
std::string str;
std::cin >> str;
while (str!="quit" && !s1.isfull()) {
s1.push(str);
std::cin >> str;
}
peek(s1, 6);
std::cout << s1;
Peekstack s2;
while (!s1.isempty())
{
s1.pop(str);
s2.push(str);
}
peek(s2, 3);
std::cout << s2;
std::cin.get();
std::cin.get();
return 0;
}
std::ostream & operator<<(std::ostream & os, const Stack &a)
{
return a.diaplay(os);
}
void peek(Stack & a, int pos)
{
std::string temp;
if (!a.get_value(pos,temp))
{
std::cout << "peek fail!" << std::endl;
}
else {
std::cout << temp << std::endl;
}
}