C++ - 类的继承和多态

总结以下有关类的概念,用于加深对类的理解。可能有不足或错误之处,欢迎指出,大家共同进步。

1、引言

介绍类,绕不过去两个概念:面向过程编程、面向对象编程(Object Oriented Programming)。其中面向过程的编程以C语言为代表的编程方式,面向过程程序的设计方式是:确定程序需要哪些过程,然后采用最合适的算法来实现;强调的是程序实现的过程以及实现过程中采用的算法。这程序开发的前期,采用这种方式是最直接也是最简单的,因为当时面对的程序规模还比较小,采用面向对象的设计方式能够满足基本的需要。随着程序的规模越来越大,采用面向对象的方式已不能很好满足编程的需要,此时,提出了OOP的概念。OOP与强调过程的编程方式不同,它强调的是对象,把程序中的组成分解成一个个对象,然后通过对象之间的操作或交互来满足程序的需要。

与面向过程相比,OOP具备以下几个重要的特性:

1)、抽象

2)、封装和隐藏

3)、多态

4)、继承

5)、代码重用性

为了实现这些特性并把它们组合在一起,提出了类的概念。

2、类的概念

何为类?类的英文表达为class,直译过来的意思就是“类型”,即,一个类是自定义的一种类型

这就不得不提及OOP中“抽象”的特性。世界是无序和复杂的,简单来说,一个人的组成就非常复杂,到目前为止,在医学上还在不断地探索人体的奥秘。而在OOP中,如果要表达一个人,我们不可能把所有与人体相关的组成或行为都表现出来,那样会非常复杂和繁琐,所以我们就需要化繁为简,找出人具备的明显的特征组合起来来表达“人”,这种化繁为简的过程就可以称之为抽象。通过抽象的分析,一个“人”可能包括:具有脑袋、五官、四肢、肤色等组成属性,具备直立行走、使用工具等行为。经过抽象,可以把“人”分析为一组具有多种属性、多种行为的描述,而在OOP中,类就是用来组合抽象出的属性和行为,从而形成一个用来描述“人”的一种类型。这种组合的过程其实可以理解为对类的封装的隐藏,通过封装后的类可以有效地用来表达一个“人”这种实体。

3、对象的概念

那么何为对象呢?我们现在已经知道类是一种类型,而对象就是该类型实例化出来的一个实体,该实体可以成为类的对象。比如“人”是一个类,那么我们每一个人个体都可以称之为一个个对象,一个类型为“人”的对象,我们都具备该类描述的属性和行为。对象是类的一种实例化,在OOP中,当定义好类之后,操作的主要就是对象。

4、类的继承

继承是类的一个很重要的概念,是实现OOP特性中代码重用性的一种手段。正如其字面意思,通过继承,不仅可以使用原始类中一些属性和行为,同时也可以对继承的类进行扩展,形成新的属性和行为。

继承,又可以称之为派生,当从一个类派生出另外一个类时,原始类称之为基类(超类),继承类则称之为派生类。集成的方式有三种:Public、Protected、Private,分别对应类中的三种类型的属性。继承的形式如下所示:

class CChildClass : public CBaseClass
{...}

其中CChildClass属于派生类,CBaseClass属于基类,继承方式为公有继承,即是是公有继承也不能直接调用基类的私有成员,只能通过基类的公有方法进行调用。还有protected和private继承,其继承之后属性的访问控制如下所示:

  继承方式 public protected private
子类访问控制 public public protected private
子类访问控制 protected protected protected private
子类访问控制 private private private private

注意,不管何种继承方式,子类都不能使用父类的private属性,不管是内部还是外部。判断继承中属性的使用是否规范需要遵守“三看”原则:一看属性的使用是内部还是外部;而看子类是以何种方式继承父类;三是看父类中属性是何种方式的属性;(Public即可内部使用也可外部使用;Protected只可内部使用;Private只可内部使用)

关于继承有以下需要注意的几点:

1)、构造函数和析构函数。需要注意的是,当创建派生类对象是,程序首先创建基类的对象。这也就是说,首先调用的是基类的构造函数,然后再调用派生类的构造函数。而析构函数的调用顺序刚好相反,是先调用派生类的析构函数然后再调用基类的析构函数;

对于继承和组合(即类的对象作为其他类的属性)同时存在的情况,先执行基类的构造函数,如果该基类还有基类,则执行基类的基类的构造函数(以此类推),然后再执行组合对象类的构造函数(执行顺序和对象属性的声明顺序一致),最后调用子类的构造函数,析构函数的调用顺序与构造函数相反。

2)、构造函数的初始化列表。当采用继承的方式时,如果基类中只有有参构造函数,可以通过初始化列表的形式初始化类的属性,具体使用方式如下所示:

CChildClass: :CChildClass(int p1, char p2, string p3): CBaseClass(p2, p3)
{attr1 = p1; } 

当定义一个CChildClass类的对象时如:CChildClass *cc = new CChildClass(1, 'a', "Hello");就会按照如上方式进行属性的初始化,先将p2和p3传递给基类的构造函数进行初始化操作,然后在执行派生类的构造函数进行属性的初始化。

当然,还有另外一种形式的初始化列表,即所有的属性都通过初始化列表的形式进行初始化,如下所示:

CChildClass: :CChildClass(int p1, char p2, string p3): CBaseClass(p2, p3),attr1(p1)
{...}

3)、类型的兼容性原则:当子类继承自父类时,可以当成一个特殊的父类

                 ①、子类对象可以当做父类对象使用

                 ②、子类对象可以直接赋值给父类对象

                 ③、子类对象可以直接初始化父类对象

                 ④、父类指针可以直接指向子类指针

                 ⑤、父类引用可以直接引用子类对象

4)、子类与父类存在同名成员变量和同名成员函数:在处理这种情况之前,需要先明白几个概念:函数重载、函数重写、函数重定义。

名称覆盖:如果子类中的函数名称与父类中的相同,则会产生名称覆盖的情况,对于子类来说,会默认调用子类的同名函数,如果想要调用父类的同名函数,只能显示调用:子类对象.父类名称::成员函数(成员变量)

函数重载:函数名称相同,参数不同;必须是在同一个类中;重载是在编译期间根据参数类型和个数决定函数调用;子类无法重载父类的函数,如果函数名称相同,则子类函数将会覆盖掉父类的同名函数;

函数重写:重写发生在子类和父类之间;并且父类与子类中的函数必须有完全相同的函数原型(函数名称、参数);分为两种形式:虚函数重写和非虚函数重写,虚函数重写就是在函数有virtual关键字修饰,使用virtual修饰后将会产生多态,非虚函数重写就是下面将的函数重定义;

函数重定义:一种特殊的函数重写,无virtual关键字修饰。

5)、虚继承

在继承时,存在这样一种情况,即C类继承自B1和B2类,而B1和B2类继承都继承自B类,如下所示:

class B
{
public: int b;
};
class B1:public B
{
public: int b1;
};
class B2:public B
{
public: int b2;
};
class C: public B1, public B2
{
public: int c;
};
void play11()
{
	C c1;
	c1.c = 10;        //ok
	c1.b1 = 20;       //ok
	c1.b2 = 30;       //ok
	c1.b = 40;//err, 因为B1和B2都从B类继承了成员b,而C又分别从B1和B2继承了b,所以不知调用哪一个b,即多继承的二义性
}

这种存在有共同基类的情况会发生多继承的二义性的问题,即继承的变量不知是从哪个父类继承下来的,而且在这种情况下,基类B的构造函数也会调用两次。此时的剞劂办法就是使用虚继承, 即B1和B2分别虚继承子B:

class B
{
public:
	int b;
	B(){cout << ">>> B() 构造函数:"<< endl;}
	~B(){cout << ">>> ~B() 析构函数:" << endl;}
};
//virtual
class B1: virtual public B
{public:int b1;
};
class B2: virtual public B
{public:int b2;
};
class C: public B1, public B2
{public:int c;
};
//有vitual,则只执行一次B的构造函数
//没有virtual,则执行两次B的构造函数
//虚继承解决的是多个父类还有一个共同父类的情况
void play11()
{
	C c1;
	c1.c = 10;
	c1.b1 = 20;
	c1.b2 = 30; 
	c1.b = 40;	//多继承的二义性,为了解决多继承的二义性,可以使用virtual关键字修饰继承关系,这样,即使是多继承,也只会执行一次B的构造函数
				//同时,B中的属性b也就不具备二义性了

	//在加了virtual关键字之后,编译器会给类添加属性(不可见)
	//cout << ">> sizeof(B): " << sizeof(B) << endl;
	//cout << ">> sizeof(B1): " << sizeof(B1) << endl;
	//cout << ">> sizeof(B2): " << sizeof(B2) << endl;
	//cout << ">> sizeof(C): " << sizeof(C) << endl;
}
/*虚继承:
1、在多继承情况下,只能解决有共同基类的情况
2、不能解决多个基类中存在变量重名的情况
*/
void main11()
{
	play11();
	return;
}

但是需要注意的是,在实际的项目开发中基本上不会出现上述情形。 

5、类的多态

在继承时,存在这样一种情况:Child类继承自Parent类,Parent中有一个打印函数print(),如下所示:

class Parent
{
public:
	Parent(int a)
	{
		this->a = a;
		cout << ">>> Parent()构造函数 a: " << this->a << endl;
	}
	void print(){cout << ">>> Parent 打印 a: " << this->a << endl;}
private:
	int a;
};
class Child: public Parent
{
public:
	Child(int b): Parent(10)
	{
		this->b = b;
		cout << ">>> Child()构造函数 b: " << this->b << endl;
	}
	virtual void print(){cout << ">>> Child 打印 b: " << this->b << endl;}
private:
	int b;
};
void paly13()
{
	Parent *base = NULL;
	Parent p1(20);
	Child c1(30);
	//Test1
	c1.print();			//执行的是子类的打印函数
	//Test2
	base = &p1;
	base->print();				//执行父类的打印函数
	//Test3
	base = &c1;			//类型兼容性原则,子类对象可以赋值给父类指针
	base->print();		//执行的仍然是父类的打印函数 
}

对于play13()函数中的Test1,很显然执行的是子类的打印函数,对于Test2,很显然执行的是父类的打印函数,对于Test3来说,尽管将子类对象赋值给了父类指针,但调用的依然是父类的打印函数,这是因为:在编译时,编译器自动根据指针类型判断指向的是一个什么样的对象,所以编译器会认为父类指针指向的是父类对象,如果不写virtual关键字,则当子类对象赋值给父类对象时(指针、引用、传参),仍然会调用父类对象的打印函数(静态联编)。此时就要引入多态的思想。

多态是类的一种很重要的特性。从字面意思上就可以理解,类可以有多种形态,同样的调用语句有多种不同的表示方式,这样就会调用子类的打印函数(自己调用自己的函数)。那么实现多态的手段是什么呢?虚函数。通过virtual来声明成员函数,如下所示:

class HeroFighter
{
public:
	virtual int power(){return 10;}
};
class AdvHeroFighter: public HeroFighter
{
public:
	virtual int power(){return 20;}
};
class EnemyFighter
{
public:
	int attack(){return 15;}
};
//可以体现多态的概念
void play14(HeroFighter *hf, EnemyFighter *ef)
{
	hf->power()>ef->attack() ? printf("主角win\n"): printf("敌人win\n");	
}
void main()
{
	HeroFighter hf;
	AdvHeroFighter advHf;
	EnemyFighter ef;

	play14(&hf, &ef);
	play14(&advHf, &ef);
	return;
}

如上示例就是一个典型的多态的应用场景,play14(HeroFighter *hf,  EnemyFighter *ef)函数有一个父类HeroFighter类型的额参数,这样他同时也可以处理类型为AdvHeroFighter的子类对象,体现了C++代码重用性的原则,可以用一个函数同时处理父类和子类,并且分别调用自己的power()函数,就是因为virtual关键字修饰了power()函数。

多态的实现效果:多态,同样的调用语句有多种不同的表现形式

面向对象的3大概念

①、封装,突破c语言函数的概念:用类做函数参数,可以使用对象的属性和方法

②、继承,A B 代码复用

③、多态,面向未来

多态实现的3个条件

①、要有继承

②、要有虚函数重写

③、用父类指针(父类引用)指向子类对象

多态的C++实现:virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用,而是要根据指针所指向的实际对象类型来判断如何调用

多态的理论基础:动态联编 和 静态联编。动态联编:根据实际的对象类型来判断重写函数的调用

多态的重要意义:多态是设计模式的基础,多态是框架的基础。

关于虚函数的使用有以下需要注意的地方:

1)、虚析构函数。解决的是想通过基类指针把所有子类对象的析构函数都执行一遍,即通过基类指针释放所有的子类资源,需要在基类指针的析构函数加上virtual关键字。

析构函数的调用事发生当delete释放由new创建的对象时,对于派生类来说,当delete时,如果基类中的析构函数没有虚化,则将只会调用基类中的析构函数。而如果基类的析构函数虚化了,则将会先调用派生类的析构函数,然后再调用基类的析构函数。虚化基类的析构函数就是为了保证析构的顺序是正确的。如果直接用delete删除掉子类对象也是可以顺序调用基类的析构函数的。

2)、纯虚函数。纯虚函数是更加高级一点的虚函数。虚函数在基类中也是要有实现的,而纯虚函数在基类中是不需要实现的,因为纯虚函数实际上是提供了一种接口,所有的派生类都可以从接口中继承对应的函数,对应有很多共性的对象而言,采用纯虚函数还是比较合适的。纯虚函数的声明方式如下所示:

class CCar

{...

public:

virtual void Drive() const=0;

}

class CManualCar : public CCar

{...

public:

virtual void Drive() const;   //在派生类中则不需要加 =0

}

需要注意的是,拥有纯虚函数的类成为抽象类,是不能创建该类的对象的,只能用作基类来使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章