文章目录
一、c++多态性
1.C++多态性实例讲解
多态性
多态性是面向对象程序设计的重要特性之一,从字面意思上可以简单理解就是:多种形态,多个样子。其实本质意思也是这样,在面向对象程序设计中,指同样的方法被不同对象执行时会有不同的执行效果。
多态的实现又可以分为两种:编译时多态和运行时多态。前者是编译的时候就确定了具体的操作过程,后者是在程序运行过程中才确定的操作过程。这种确定操作过程的就是联编,也称为绑定。
联编在编译和连接时确认的,叫做静态联编,前面我们学习的函数重载、函数模板的实例化就属于这一类。另一种是在运行的时候,才能确认执行哪段代码的,叫做动态联编,这种情况是编译的时候,还无法确认具体走哪段代码,而是程序运行起来之后才能确认。
静态联编,简单例子:
#include <iostream>
using namespace std;
#define PI 3.1415926
class Point
{
private:
int x,y;
public:
Point(int x=0,int y=0)
{
this->x = x;
this->y = y;
}
double area()
{
return 0.0;
}
};
class Circle:public Point
{
private:
int r;
public:
Circle(int x,int y,int R):Point(x,y)
{
r = R;
}
double area()
{
return PI*r*r;
}
};
int main()
{
Point A(10,10);
cout<<A.area()<<endl;
Circle B(10,10,20);
cout<<B.area()<<endl;
Point *p;
p = &B;
cout<<p->area()<<endl;
Point &pp=B;
cout<<pp.area()<<endl;
return 0;
}
定义了两个类,一个圆点类,一个派生出来的圆类,可以看到主函数的代码,四个输出面积的结果,结果如下
大家可以对照代码理解四个输出:
第一个cout输出A的面积,是Point类中的area方法,面积为0,没有问题。
第二个cout输出B的面积,很明显是派生类Circle的area方法,面积自然按公式计算得出1256.64的值,也没问题。
第三个cout输出的是Point类型指针p指向的Circle类对象的area方法,它输出了0很明显是执行了Point类里的area方法。这里C++实行的是静态联编,即在编译的时候就依据p的类型来定执行哪个area,因此是0。
第四种cout也同理,把Circle类型的对象赋给Point类型的引用,C++同样实行静态联编,也输出0。
如果想达到我们的要求,即无论指针和引用为什么类型,都以实际所指向的对象为依据灵活决定。那么就要更改这种默认的静态联编的方法,采用动态联编,即在运行的时候灵活决定。
2.C++虚函数实例详解
简单来讲,就是一个函数前面用virtual声明的函数,一般形式如下:
virtual 函数返回值 函数名(形参)
{
函数体
}
那它有什么用呢?虚函数的出现,允许函数在调用时与函数体的联系在运行的时候才建立,即所谓的动态联编。那么在虚函数的派生类的运行时候,就可以在运行的时候根据动态联编实现都是执行一个方法,却出现不同结果的效果,就是所谓的多态。这样解决上一节的问题就有了办法。
接下来,我们只需要把基类中的area方法声明为虚函数,那么主函数中无论Point类型的指针还是引用就都可以大胆调用,无用关心类型问题了。因为他们会依据实际指向的对象类型来决定调用谁的方法,来实现动态联编。
#include <iostream>
using namespace std;
#define PI 3.1415926
class Point
{
private:
int x,y;
public:
Point(int x=0,int y=0)
{
this->x = x;
this->y = y;
}
virtual double area()
{
return 0.0;
}
};
class Circle:public Point
{
private:
int r;
public:
Circle(int x,int y,int R):Point(x,y)
{
r = R;
}
double area()
{
return PI*r*r;
}
};
int main()
{
Point A(10,10);
cout<<A.area()<<endl;
Circle B(10,10,20);
cout<<B.area()<<endl;
Point *p;
p = &B;
cout<<p->area()<<endl;
Point &pp=B;
cout<<pp.area()<<endl;
return 0;
}
3.C++虚析构函数实例详解
虚析构函数
在C++中,不能把构造函数定义为虚构造函数,因为在实例化一个对象时才会调用构造函数,且虚函数的实现,其实本质是通过一个虚函数表指针来调用的,还没有对象更没有内存空间当然无法调用了,故没有实例化一个对象之前的虚构造函数没有意义也不能实现。
但析构函数却是可以为虚函数的,且大多时候都声明为虚析构函数。这样就可以在用基类的指针指向派生类的对象在释放时,可以根据实际所指向的对象类型动态联编调用子类的析构函数,实现正确的对象内存释放。
#include <iostream>
using namespace std;
class Point
{
private:
int x,y;
int *str;
public:
Point(int x=0,int y=0)
{
this->x = x;
this->y = y;
str = new int[100];
}
~Point()
{
delete []str;
cout<<"Called Point's Destructor and Deleted str!"<<endl;
}
};
class Circle:public Point
{
private:
int r;
int *str;
public:
Circle(int x,int y,int R):Point(x,y)
{
r = R;
str = new int[100];
}
~Circle()
{
delete []str;
cout<<"Called Circle's Destructor and Deleted str!"<<endl;
}
};
int main()
{
Point *p;
p = new Circle(10,10,20);
delete p;
return 0;
}
可以看到代码,基类中没有用virtual声明的析构函数,且基类和派生类当中都有动态内存开辟,那么我们在主函数中也动态开辟内存的方式创建一个Circle类,然后删除,之后运行后截图如下:
可以清楚的看到,仅仅调用了基类的析构函数,这样一来派生类中new出来的4*100字节的内存就会残留,造成内存泄漏!
而如果把基类中析构函数声明为virtual,则结果大有不同!这个时候多态效应出现,会先调用释放派生类的空间,然后再释放基类的内存空间,完美结束,如下图:
4.C++纯虚函数与抽象类总结
虚函数与抽象类
纯虚函数,就是没有函数体的虚函数。什么叫没有函数体?就是这样定义的函数:
virtual 返回值 函数名(形参)=0;
可以看到,前面virtual与虚函数定义一样,后面加了一个=0。表示没有函数体,这就是一个纯虚函数。包含纯虚函数的类就是抽象类,一个抽象类至少有一个纯虚函数。
抽象类的存在是为了提供一个高度抽象、对外统一的接口,然后通过多态的特性使用各自的不同方法
抽象类的特点总结如下:
- 抽象类无法实例出一个对象来,只能作为基类让派生类完善其中的纯虚函
数,然后再实例化使用。- 抽象类的派生来依然可以不完善基类中的纯虚函数,继续作为抽象类被派生。直到给出所有纯虚函数的定义,则成为一个具体类,才可以实例化对象。
- 抽象类因为抽象、无法具化,所以不能作为参数类型、返回值、强转类型。
- 接着第三条,但抽象类可以定义一个指针、引用类型,指向其派生类,来实现多态特性。
二、C++异常处理
1.C++异常的概念
程序的错误通常包括:语法错误、逻辑错误、运行异常。
下面分别介绍:
- 语法错误:
这个不必多说,大家在之前的学习一定都知道,就是只程序代码不符合语法要求,在编译、链接时候就由编译器提示出来的错误,好发现。
- 逻辑错误:
这种情况,是指编译没问题,没有错误,可以运行起来。但程序的输出结果或执行过程不如我们所愿,达不到预期的结果,这种错误就叫做逻辑错误,需要不断的调试、测试来发现。
- 运行异常:
运行异常(exception)是指程序在运行过程中由于意外的情况,造成的程序异常终止,比如内存不足、打开的文件不存在、除数为0的情况等等。
前面两种我们都已经知道,第三种则是我们本章要详细展开讲解的知识了。通常情况下,导致程序异常错误,虽然无法避免,但是确可以预料,进行预见性的处理,来避免程序崩溃,从而保障程序的健壮性。这种行为我们称之为异常处理。
我们捕获和处理异常的方法也很多,比如通过if…else判断调用函数的返回值,或在执行代码之前对关键的数据进行检查等等,如果出现问题,则用exit()或abort()等函数来终止程序。
cin>>a>>b;
if(b==0)//捕获异常
{
cout<<"Drivide 0!"<<endl;
}
else
{
cout<<a<<"/"<<b<<"="a/b<<endl;
}
往往通过if来进行判断,从而对关键部分进行捕获和预防,但这种方式在使用过程中往往会因为if判断过多,使程序的易读性降低,并且对于需要判断函数返回值的情况,对于那些没有返回值的函数,就束手无策了,为此C++为我们提供了异常处理的方案。
2.C++异常处理机制try catch实例详解
C++为我们提供了一种结构化形式的,更为优雅的异常处理机制,这种结构化机制可以把程序中正常执行的代码和异常处理的部分分开表示,使程序变得更清晰易读,更为优雅!
异常处理的结构,共分两部分:
try
{
//正常程序执行语句
throw (异常类型表达式);
}
catch(异常类型1)
{
//异常处理代码
}
catch(异常类型2)
{
//异常处理代码
}
catch(异常类型3)
{
//异常处理代码
}
//后续代码
用到了try、throw、catch三个关键词。
代码在执行时,首先遇到try代码块,作用就是启动异常处理机制,检测try代码执行中遇到的异常,然后通过throw进行抛出,throw当中的异常类型表达式是常量或变量表达式。接下来会和后面的catch语句块进行匹配(捕获),然后执行对应的代码。如果没有发现可以匹配的类型则,则继续向下执行。如若未找到匹配,则自动调用terminate()结束函数,默认功能是abort()终止程序.
举一个除法运算时,除数为0时候的一个异常处理
#include <iostream>
using namespace std;
int main()
{
int a,b;
cin>>a>>b;
try
{
if(b==0)
throw "error! b<0";
}
catch(const char *str)
{
cout<<str<<endl;
}
catch(int)
{
cout<<"throw int "<<endl;
}
return 0;
}
可以看到,在try中,如果发现b为0会抛出一个字符串,那么此时会进入catch匹配,很明显将匹配第一个catch,进而输出str的值
3.C++标准异常exception处理类
C++给我们提供了标准的异常处理类,它用来抛出C++标准库中函数执行时的异常。C++提供的标准异常类的层次结构如图:
可以看到,所有的异常类都继承自exception基类,exception类下的logic_error和runtime_error又是两个比较大类,包含有多个自类,它们分表代表逻辑类错误和运行时错误。举例说明,如:1. 我们使用new开辟内存时,如果遇到空间不足,则会抛出bad_alloc异常。
- 我们使用dynamic_cast()进行动态类型转化失败时,则抛出bad_typeid异常。
- 我们在计算数值超过该类型表示的最大范围时,则抛出overflow_error异常,表示运算上溢,同理,underflow_error表示运算下溢。
- 我们在使用string类下标但越界时,则抛出out_of_range异常。
总结
fighting!!!
来自“https://www.dotcpp.com”实现理解