〇、面向对象的三个基本特征:封装、继承、多态
面向对象的三个基本特征是:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已知存在的代码模块(类);它们的目的是为了代码重用。而多态是为了实现另外一个目的,接口重用。
一、封装:代码模块化
1、基本概念
1)封装是把数据和操作数据的函数绑定在一起的概念,封装的本质是模块化。
2)C++通过创建类来支持封装和数据隐藏(public、protected、private),一般情况下都会将类成员状态设置为私有,进而能保证良好的封装性。
2、数据封装与数据抽象的区别(待更新)
1)数据封装是一种把数据和操作数据的函数捆绑在一起的机制;数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制;
2)数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程技术。接口独立于实现,如果改变底层实现,接口也将保持不变。
二、继承:代码重用
1、基本概念
1)面向对象程序设计中最重要的一个概念是继承,继承允许我们依据另一个类来定义一个类,继承使得创建和维护一个应用程序变得更加容易,也达到了重用代码功能和提高执行时间的效果。
2)在创建一个类是,不需要重新编写新的数据成员(属性)和成员函数(方法),只要指定新建的类继承了一个已有的类的成员即可,这个已有类称为基类(父类),新建的类称为派生类(子类)。
3)继承代表了is a关系,例如,哺乳动物是动物,狗是哺乳动物,狗是动物等等。
2、基类(父类)&派生类(子类)
一个类可以派生自多个类,意味着,它可以从多个基类继承数据和函数,定义一个派生类,我们使用一个类派生列表来指定基类,类派生列表以一个或多个基类命名。
3、访问控制和继承
派生类可以访问基类中所有的非私有成员。派生类一般继承所有基类的方法,但是以下情况除外:1)基类的构造函数、析构函数和拷贝构造函数;2)基类的重载运算符;3)基类的友元函数。
访问 | public | protected | private |
同一个类 | yes | yes | yes |
派生类 | yes | yes | yes |
外部的类 | yes | no | no |
4、继承类型
基类可以被继承为public、protected或private几种类型,但是几乎不使用protected或private继承,通常使用public继承,当使用不同类型的继承时,遵循以下表规则:
5、多继承
什么时候用到多继承?遇到的问题无法用“是一个”关系来描述的时候,就能用到多继承~!!!
多继承即一个子类可以有多个父类,它继承了多个父类的特性。C++类可以从多个类继承成员,语法如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
其中,访问修饰符继承方式是public、protected或private其中一个,用来修饰每个基类,各个基类之间用逗号分隔;
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}
三、多态:接口重用
1、基本概念
1)多态性是指用一个名字定义不同的函数,调用同一个名字的函数,却执行不同的操作,从而实现“一个接口,多种方法”!
2)编译时的多态性:通过重载实现,运行时的多态性:通过虚函数实现;
3)多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖,或者重写。
4)多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用在编译期间就可以确定函数的调用地址,并产生代码,是静态的,就是说地址是早绑定,而如果函数调用的地址不能再编译器期间确定,这就属于晚绑定。
以下代码参考至菜鸟教程网
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
此时,编译的结果是:
Parent class area
Parent class area
导致错误的原因是,调用函数area()被编译器设置为基类中的版本,area()函数在程序编译期间就设置好了,这就是所谓的静态多态或静态链接或者早绑定。通过如下修改:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area() //在area前用关键字virtual修饰,将area定义为虚方法,防止早绑定
{
cout << "Parent class area :" <<endl;
return 0;
}
};
此时的编译结果如下:
Rectangle class area
Triangle class area
通过修改后编译结果,子类中有与父类同名的函数area(),且都有独立的实现。一个接口,多种方法。这就是多态,即有多个不同的类,都带有同一个名称但具有不同实现函数。
2、虚函数与纯虚函数
1)虚函数 是在基类中使用关键字virtual声明的函数,在派生类中重新定义基类中定义的虚函数,会告诉编译器不要静态链接到该函数(不要早绑定)。程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
2)纯虚函数 当你想要在基类中定义虚函数,以便派生类中重新定义该函数更好的适用于对象,但是基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area() = 0; //纯虚函数,通过=0告诉编译器,函数没有主体,该虚函数是纯虚函数
};
3、重写(覆盖)与重载
3.1重载
重载一般是用于在一个类内实现若干重载的方法,这些方法名称相同但参数形式不同。重载时需要注意的有:
- 在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数形式可以使不同的参数类型、不同的参数个数、不同的参数顺序(参数类型必须不一样);
- 不能通过访问权限、返回类型、抛出的异常进行重载;
- 方法的异常类型和数目不会对重载造成影响。
2.1.1函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,但这些同名函数的形式参数(参数的个数、类型或者顺序)必须不同,不能仅通过返回类型的不同来重载函数。同样名字不同参数但有同样用途的函数
/*同一访问区被声明的几个具有不同参数列(参数类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型*/
class A{
public:
void test(int i);
void test(double i);
void test(int i, double j);
void test(double i, int j);
int test(int i); //错误,非重载
};
2.1.2运算符重载
1) 运算符也是可以重载的,实际上,我们常常不知不觉中使用了运算符重载,运算符重载的方法是定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算。也就是说,运算符重载是通过定义函数实现的。运算符重载实质上是函数的重载。
2) c++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载,除了以下五个不允许重载外,其他运算符允许重载:.(成员访问运算符);.*(成员指针访问运算符);::(域运算符);sizeof(尺寸运算符);?:(条件运算符)
3)重载不能改变运算符
3.2 重写
重写(覆盖)一个方法以实现不同的功能,是用于子类在继承父类时,重写(重新实现)父类中的方法;重写时需注意的的有:
- 重新方法的参数列表必须完全与被重写的方法的参数列表相同,否则不能称其为重新,而是重载;
- 重新的方法的返回值必须和被重写的方法的返回值一致;
- 重新的方法所抛出的异常必须和被重写方法所抛出的异常一致;
- 被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写;
- 静态方法不能被重写为非静态的方法;
- 重新方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>private)
/*派生类中存在重新定义的函数。函数名,参数列表、返回值类型,都必须同基类中被重新的函数一致,只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重新的函数必须有virtual修饰*/
#include<iostream>
using namespace std;
class A{
public:
virtual void fun3(int i){
cout << "A::fun3() : " << i << endl;
}
};
class B : public A{
public:
//重写
virtual void fun3(double i){
cout << "B::fun3() : " << i << endl;
}
};
int main(){
A a;
B b;
A * pa = &a;
pa->fun3(3);
pa = &b;
pa->fun3(5);
system("pause");
return 0;
}