总结以下有关类的概念,用于加深对类的理解。可能有不足或错误之处,欢迎指出,大家共同进步。
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
}
需要注意的是,拥有纯虚函数的类成为抽象类,是不能创建该类的对象的,只能用作基类来使用。