C++中的封装、继承、多态理解
封装(encapsulation):就是将抽象得到的数据和⾏为(或功能)相结合,形成⼀个有机的整体,也就是将数据与操作数据的源代码进⾏有机的结合,形成”类”,其中数据和函数都是类的成员。封装的⽬的是增强安全性和简化编程,使⽤者不必了解具体的实现细节,⽽只是要通过外部接⼝,特定的访问权限来使⽤类的成员。封装可以隐藏实现细节,使得代码模块化。
继承(inheritance):C++通过类派⽣机制来⽀持继承。被继承的类型称为基类或超类,新产⽣的类为派⽣类或⼦类。保持已有类的特性⽽构造新类的过程称为继承。在已有类的基础上新增⾃⼰的特性⽽产⽣新类的过程称为派⽣。继承和派⽣的⽬的是保持已有类的特性并构造新类。继承的⽬的:实现代码重⽤。派⽣的⽬的:实现代码扩充。三种继承⽅式:public、protected、private。
继承时的构造函数:(1)、基类的构造函数不能被继承,派⽣类中需要声明⾃⼰的构造函数;(2)、声明构造函数时,只需要对本类中新增成员进⾏初始化,对继承来的基类成员的初始化,⾃动调⽤基类构造函数完成;(3)、派⽣类的构造函数需要给基类的构造函数传递参数;(4)、单⼀继承时的构造函数:派⽣类名::派⽣类名(基类所需的形参,本类成员所需的形参):基类名(参数表) {本类成员初始化赋值语句;};(5)、当基类中声明有默认形式的构造函数或未声明构造函数时,派⽣类构造函数可以不向基类构造函数传递参数;(6)、若基类中未声明构造函数,派⽣类中也可以不声明,全采⽤缺省形式构造函数;(7)、当基
类声明有带形参的构造函数时,派⽣类也应声明带形参的构造函数,并将参数传递给基类构造函数;(8)、构造函数的调⽤次序:A、调⽤基类构造函数,调⽤顺序按照它们被继承时声明的顺序(从左向右);B、调⽤成员对象的构造函数,调⽤顺序按照它们在类中的声明的顺序;C、派⽣类的构造函数体中的内容。
继承时的析构函数:(1)、析构函数也不被继承,派⽣类⾃⾏声明;(2)、声明⽅法与⼀般(⽆继承关系时)类的析构函数相同;(3)、不需要显⽰地调⽤基类的析构函数,系统会⾃动隐式调⽤;(4)、析构函数的调⽤次序与构造函数相反。
同名隐藏规则:当派⽣类与基类中有相同成员时:(1)、若未强⾏指名,则通过派⽣类对象使⽤的是派⽣类中的同名成员;(2)、如要通过派⽣类对象访问基类中被覆盖的同名成员,应使⽤基类名限定:基类名::数据成员名。
虚基类:作⽤:(1)、主要⽤来解决多继承时可能发⽣的对同⼀基类继承多次⽽产⽣的⼆义性问题;(2)、为最远的派⽣类提供唯⼀的基类成员,⽽不重复产⽣多次拷贝。
继承、组合:组合是将其它类的对象作为成员使⽤,继承是⼦类可以使⽤⽗类的成员⽅法。(1)、A继承B,说明A是B的⼀种,并且B的所有⾏为对A都有意义;(2)、若在逻辑上A是B的“⼀部分”,则不允许B从A派⽣,⽽是要⽤A和其它东西组合出B;(3)、继承属于”⽩盒”复⽤,组合属于”⿊盒”复⽤。
多态(Polymorphic)性可以简单地概括为“⼀个接⼝,多种⽅法”,程序在运⾏时才决定调⽤的函数。C++多态性是通过虚函数来实现的,虚函数允许⼦类重新定义成员函数,⽽⼦类重新定义⽗类的做法称为覆盖或者称为重写。⽽重载则是允许有多个同名的函数,⽽这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。关于多态,简⽽⾔之就是⽤⽗类型别的指针指向其⼦类的实例,然后通过⽗类的指针调⽤实际⼦类的成员函数。
多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调⽤,在编译器编译期间就可以确定函数的调⽤地址,并产⽣代码,是静态的,就是说地址是早绑定的。⽽如果函数调⽤的地址不能在编译期间确定,需要在运⾏时才确定,这就是属于晚绑定。
封装可以使得代码模块化,继承可以扩展已存在的代码,它们的⽬的都是为了代码重⽤。⽽多态的⽬的则是为了接⼝重⽤。也就是说不论传递过来的究竟是哪个类的对象,函数都能够通过同⼀个接⼝调⽤到适应各⾃对象的实现⽅法。
最常见的⽤法就是声明基类的指针,利⽤该指针指向任意⼀个⼦类对象,调⽤相应的虚函数,可以根据指向的⼦类的不同⽽实现不同的⽅法。如果没有使⽤虚函数的话,即没有利⽤C++多态性,则利⽤基类指针调⽤相应的函数的时候,将总被限制在基类函数本⾝,⽽⽆法调⽤到⼦类中被重写过的函数。因为没有多态性,函数调⽤的地址将是⼀定的,⽽固定的地址将始终调⽤到同⼀个函数,这就⽆法实现⼀个接⼝,多种⽅法的⽬的了。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派⽣类都要定义⾃⼰的实现⽅法。在基类中实现纯虚函数的⽅法是在函数原型后加“= 0”。为了⽅便使⽤多态特性,常常需要在基类中定义虚函数,在很多情况下,基类本⾝⽣成对象是不合情理的。为了解决这些问题,引⼊了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派⽣类中必须予以重写以实现多态性。同时含有纯虚函数的类称为抽象类,它不能⽣成对象。由于纯虚函数所在的类中没有它的定义,在该类的构造函数和析构函数中不允许调⽤纯虚函数,否则会导致程序运⾏错误,但其它成员函数可以调⽤纯虚函数。
C++⽀持两种多态性:(1)、编译时多态性(静态多态,在编译时就可以确定对象使⽤的形式):通过重载函数实现;(2)、运⾏时多态性(动态多态,其具体引⽤的对象在运⾏时才能确定):通过虚函数实现。
C++中,实现多态有以下⽅法:虚函数、抽象类、重载、覆盖、模板。
函数重载(Overload):指在相同作⽤域⾥(如同⼀类中),函数同名不同参,返回值则不⽤理会,不同参可以是不同个数,也可以是不同类型。效果:根据实参的个数和类型调⽤对应的函数体。
函数覆盖(Override)(函数重写):指派⽣类中的函数覆盖基类中的同名同参虚函数,因此作⽤域不同。效果:基类指针或引⽤访问虚函数时会根据实例的类型调⽤对应的函数。
函数隐藏(Hide):对于⼦类中与基类同名的函数,如果不是覆盖那就成了隐藏。两种情况:(1)、同名不同参;(2)、同名同参但基类不是virtual函数。
派⽣类的构造函数使⽤说明:(1)、在派⽣类构造函数中,只要基类不是仅使⽤⽆参的默认构造函数,都要显⽰的给出基类名称参数表;(2)、基类没有定义构造函数,派⽣类也可以不定义,使⽤默认构造函数;(3)、基类有带参构造函数,派⽣类必须定义构造函数。
虚函数的重载函数仍是虚函数。在派⽣类重定义虚函数时必须有相同的函数原型,包括返回类型、函数名、参数个数、参数类型的顺序必须相同。虚函数必须是类的成员函数,不能为全局函数,也不能为静态函数。不能将友员说明为虚函数,但虚函数可以是另⼀个类的友员。析构函数可以是虚函数,但构造函数不能为虚函数。⼀般地讲,若某类中定义有虚函数,则其析构函数也应当说明为虚函数。特别是在析构函数需要完成⼀些有意义的操作,⽐如释放内存时,尤其应当如此。在类系统中访问⼀个虚函数时,应使⽤指向基类类型的指针或对基类类型的引⽤,以满⾜运⾏时多态性的要求。当然也可以像调⽤普通成员函数那样利⽤对象名来调⽤⼀个函数。若在派⽣类中没有重新定义虚函数,则该类的对象将使⽤其基类中的虚函数代码。
抽象类:如果⼀个类中⾄少有⼀个纯虚函数,那么这个类被称为抽象类。抽象类不仅包括纯虚函数,也可包括虚函数。抽象类中的纯虚函数可能是在抽象类中定义的,也可能是从它的抽象基类中继承下来且重定义的。抽象类有⼀个重要特点,即抽象类必须⽤作派⽣其它类的基类,⽽不能⽤于直接创建对象实例。抽象类不能直接创建对象的原因是其中有⼀个或多个函数没有定义,但仍可使⽤指向抽象类的指针⽀持运⾏时多态性。派⽣类中必须重载基类中的纯虚函数,否则它仍将被看作⼀个抽象类。从基类继承
来的纯虚函数,在派⽣类中仍是虚函数。
虚函数表:虚函数是通过⼀张虚函数表来实现的。简称为V-Table,在这个表中,主要是⼀个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们⽤⽗类的指针来操作⼀个⼦类的时候,这张虚函数表就显得有⽆重要了,它就像⼀个地图⼀样,指明了实际所应该调⽤的函数。
⼀个多态的例⼦:
#include <iostream>
using namespace std;
class A
{
public:
void foo()
{
printf("1\n");
}
virtual void fun()
{
printf("2\n");
}
};
class B : public A
{
public:
void foo()
{
printf("3\n");
}
void fun()
{
printf("4\n");
}
};
int main(void)
{
A a;
B b;
A* p = &a;
p->foo();//1
p->fun();//2
p = &b;
p->foo();//1
p->fun();//4
B* ptr = (B*)&a;
ptr->foo();//3
ptr->fun();//2
return 0;
}
另⼀个例⼦:
#include <iostream>
using namespace std;
int main(void)
{
class CA
{
public:
virtual ~CA() {cout<<"delete CA"<<endl;}
virtual int GetValue() {return 1;}
};
class CB : public CA
{
public:
~CB() {cout<<"delete CB"<<endl;}
virtual int GetValue() {return 2;}
};
CA* pA = new CB;
cout<<pA->GetValue()<<endl;
delete pA;
/
* result:
2
delete CB
delete CA
*/
/*若⽗类CA中没有将析构函数定义为虚函数,则result:
2
delete CA
由结果看出,如果不将⽗类CA的析构函数定义为虚函数,则不会调⽤到⼦类的析构函数
*/
/*若⽗类CA中的成员函数GetValue没有定义为虚函数,则result:
1
delete CA
*/
多态性与虚函数
return 0;
}
对C++继承,封装,多态的理解
⽤了C++⼀段时间,感觉对C++慢慢有了⼀点认识,在这和⼤家分享⼀下。
C++是⼀款⾯向对象的语⾔,拥有⾯向对象语⾔的三⼤核⼼特性:继承,封装,多态。每⼀个特性的良好理解与使⽤都会为我们的编程带来莫⼤的帮助。下⾯我就这三个特性讲⼀下我对C++的理解。
继承
学过⾯向对象语⾔的⼈基本都可以理解什么是继承,但我们为什么要使⽤继承?
很多⼈说继承可以使代码得到良好的复⽤,当然这个是继承的⼀个优点,但代码复⽤的⽅法除了继承还有很多,⽽且有些⽐继承更好。我认为使⽤继承最重要的原因是继承可以使整个程序设计更符合⼈们的逻辑,从⽽⽅便的设计出想要表达的意思。⽐如我们要设计⼀堆苹果,橘⼦,梨等⽔果类,使⽤⾯向对象的⽅法,我们⾸先会抽象出⼀个⽔果的基类,⽽后继承这个基类,派⽣出具体的⽔果类。如果要设计的⽔果很多,我们还可以在⽔果基类基础上,继续⽣成新的基类,⽐如热带⽔果类,温带⽔果类,寒带⽔果类等,⽽后再继承这些基类。这样的设计思想就相当于⼈类的分类思想,简单易懂,⽽且设计出来的程序层次分明,容易掌握。
既然继承这么好,那该如何使⽤继承?
继承虽好但不能滥⽤,否则设计出来的程序会杂乱不堪。根据上⾯的介绍,可以发现继承主要⽤来定义⼀个东西是什么,⽐如热带⽔果是⽔果,菠萝是热带⽔果等,即继承主要⽤来设计⼀个程序的类的框架,将所要设计的东西⽤继承来设⽴⼀个基本结构。如果想为⼀个类添加⼀个⾏为或格外的功能,最好是使⽤组合的⽅式。如果想了解组合的⽅式,可以看⼀下⽐较著名的策略模式。
封装
封装是什么?
在C++中,⽐较狭隘的解释就是将数据与操作数据的⽅法放在⼀个类中,⽽后给每个成员设置相应的权限。从⼤⼀点的⾓度来说,封装就是将完成⼀个功能所需要的所有东西放在⼀起,对外部只开放调⽤它的接⼝。
为什么要封装?
我认为模块化设计是封装的本质原因。
对软件设计或其他⼯程设计,特别是⽐较复杂的⼯程,⼀般都是模块化设计。模块化设计的好处就是可以将⼀个复杂的系统拆分成不同的模块。每⼀个模块⼜可以独⽴的设计,调试,这就让多⼈⼀起做⼀个复杂的⼯程成为现实。如果想做到模块化设计,封装是不可缺少的⼀部分。⼀个好的模块,⽐如⼀块inter的CPU芯⽚,它有强⼤的功能,虽然我们不知道它内部是如何实现的,但却可以很好的使⽤它。
多态
什么是多态?
多态简单的说就是“⼀个函数,多种实现”,或是“⼀个接⼝,多种⽅法”。多态性表现在程序运⾏时根据传⼊的对象调⽤不同的函数。
C++的多态是通过虚函数来实现的,在基类中定义⼀个函数为虚函数,该函数就可以在运⾏时,根据传⼊的对象调⽤不同的实现⽅法。⽽如果该函数不设为虚函数,则在调⽤的过程中调⽤的函数就是固定的。⽐如下⾯⼀个例⼦
//
//定义⼀个Duck基类,⽽后继承Duck派⽣出⼀个RedHandDuck类。
//其中display()⽅法,第⼀次运⾏设为普通函数,第⼆次设为虚函数
#include "iostream"
class Duck {
public:
Duck(){}
~Duck(){}
//定义⼀个虚函数display
virtual void display(){
std::cout<<" I am a Duck !"<<std::endl;
}
};
class RedHandDuck:public Duck{
public:
RedHandDuck(){}
~RedHandDuck(){}
//重写display
void display(){
std::cout<<" I am a RedHandDuck !"<<std::endl;
}
};
int main(){
RedHandDuck* duck1 = new RedHandDuck();
Duck* duck2 = duck1;
duck1->display();
duck2->display();
std::getchar();
}
第⼀次运⾏结果(不使⽤虚函数):
第⼆次运⾏结果(使⽤虚函数):
由结果可以看到,由于虚函数的使⽤,Duck对象(可以理解为接⼝),调⽤的display()⽅法是根据传⼊的对象决定的。这就实现了“⼀个接⼝,多种⽅法”。
从⽹上看到⼀个关于多态的介绍,⾮常精辟,分享给⼤家
  多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调⽤,在编译器编译期间就可以确定函数的调⽤地址,并⽣产代码,是静态的,就是说地址是早绑定的。⽽如果函数调⽤的地址不能在编译器期间确定,需要在运⾏时才确定,这就属于晚绑定。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。