一、多态
1. 多态的概念
多态可以理解为“一种接口,多种状态”,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。
多态的使用具有三个前提条件:
①.公有继承 ②.函数覆盖
③.基类的指针/引用指向派生类对象
2. 多态的分类和优缺点
多态通常分为两种类型:静态多态和动态多态。
静态多态:指在编译时就能确定要调用的方法,通过函数重载,运算符重载,模板来实现。
动态多态:动态多态时指在运行时根据对象的实际类型来确定要调用的函数,同=通过继承和函数覆盖来实现。
多态的优点:多态的优势包括代码的灵活性、可扩展性和可维护性。他能够使代码更具通用性,减少重复代码的编写。
多态的缺点:多态的缺点包括代码的复杂性,不易读、运行效率。当类的继承关系复杂时,理解和维护多态相关代码会变得非常困难。
注:本文后续说的多态都是动态多态
3. 函数覆盖
函数覆盖是基类中定义了一个虚函数,派生类编写一个同名同参数的函数将基类中的虚函数进行重写并覆盖,注意覆盖的函数必须是虚函数。
注意区分函数覆盖和函数隐藏,函数隐藏不支持多态,函数隐藏是派生类中存在与基类同名同参的函数,编译器会将基类的函数进行隐藏,函数覆盖需要virtual关键字修饰。
4. 虚函数
一个函数使用virtual关键字修饰就是虚函数,虚函数是函数覆盖的前提。
虚函数有以下性质:
1.虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual 关键字可以不写
void eat()
{
cout << "狗爱吃骨头" << endl;
}
};
int main()
{
return 0;
}
2.只有普通成员函数和析构函数可以被声明为虚函数。
3.在C++11中,可以在派生类的新覆盖的函数上使用override关键字验证函数覆盖是否成功。
5. 多态的实现
要实现多态,需要有三个前提条件: 公有继承,函数覆盖,基类的指针/引用指向派生类对象
【思考】为什么要基类的指针/引用指向派生类的对象?
实现运行时多态:当使用基类的指针或引用指向派生类的对象时,程序在运行时会根据对象的实际类型来调用相应的函数,而不是根据指针或者引用类型。
统一接口:基类的指针可以作为一个通用的接口,用于操作不同类型的派生类对象,这样可以使代码更灵活,减少重复的代码。并且的支持和拓展更好进行维护。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
void eat()override
{
cout << "狗爱吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()override
{
cout << "猫爱吃鱼" << endl;
}
};
int main()
{
// 基类指针指向派生类对象
Animal *a1 = new Dog;
// 调用派生类覆盖的虚函数
a1->eat(); // 狗爱吃骨头
Animal *a3 = new Cat;
a3->eat(); // 猫爱吃鱼
Dog d1;
Animal &a2 = d1;
a2.eat(); // 狗爱吃骨头
return 0;
}
6. 多态的原理
具有虚函数类会存在一张虚函数表,每个类的对象内部会有一个隐藏的虚函数表指针成员变量,指向当前类的虚函数表。
多态实现的流程:
7. 虚析构函数
如果不使用虚析构函数,且基类的指针指向派生类的对象,使用delete销毁对象时,只能触发基类的析构函数,如果在派生类中申请内存等资源,则会导致内存无法释放,出现内存泄漏的问题。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
// 虚析构函数
virtual ~Animal()
{
cout << "Animal析构函数被调用了" << endl;
}
};
class Dog:public Animal
{
public:
void eat()override
{
cout << "狗爱吃骨头" << endl;
}
~Dog()
{
cout << "Dog 析构函数被调用了" << endl;
}
};
int main()
{
// 基类指针指向派生类对象
Animal *a1 = new Dog;
// 调用派生类覆盖的虚函数
a1->eat(); // 狗爱吃骨头
delete a1;
return 0;
}
解决方案是给基类的析构函数使用virtual修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数,因此建议给一个可能为基类的类中的析构函数设置成虚析构函数。
8. 类型转换
类型转换也可以解决内存泄漏的问题。
int main()
{
Animal *a1 = new Dog;
// 可以把a1转换回Dog*类型
Dog *d = (Dog*)a1;
delete d;
return 0;
}
以上是传统的类型转换的写法,但是在C++11中不建议使用这种写法,因为可能会带来一些安全隐患,让程序的错误难以发现。C++提供了一组适用于不同场景的强制类型转换函数:
静态转换:static_cast
动态转换:dynamic_cast
常量转换:const_cast
重解释转换:reinterpret_cast
8.1 static_cast
主要用于基本数据类型之间的转换,没有运行时类型检查来保证数据转换的安全性,需要程序员手动判断转换是否安全。
#include <iostream>
using namespace std;
int main()
{
double x = 3.14;
int y = static_cast<int>(x);
cout << y << endl;
return 0;
}
static_cast进行下行转换是不安全的,即把基类的指针或者引用转换为派生类的。
#include <iostream>
using namespace std;
class Father
{
public:
string a = "Father";
};
class Son:public Father
{
public:
string b = "Son";
};
int main()
{
// 上行转换 派生类->基类
Son *s1 = new Son;
Father *f1 = static_cast<Father*>(s1);
cout << f1->a << endl; // Father
// 下行转换 基类->派生类
Father *f2 = new Father;
Son *s2 = static_cast<Son*>(f2);
cout << s2->a << endl; // Father
cout << s2->b << endl; // 结果不定
return 0;
}
static_cast和C语言的强制类型转换性比:
static_cast的表达式更清晰,方便管理。
static_cast会在编译时进行类型检查。
#include <iostream>
using namespace std;
class Student
{
private:
string name;
public:
Student(string name):name(name){}
string get_name()const
{
return name;
}
};
int main()
{
Student s = static_cast<Student>("Tom");
cout << s.get_name() << endl; // Tom
return 0;
}
8.2 dynamic_cast
dynamic_cast主要用于类层次之间的上行与下行转换。在进行上行转换时,dynamic_cast与static_cast效果相同。但是进行下行转换时,dynamic_cast会比static_cast更加安全。
关于下行转换类型检查如下:
#include <iostream>
using namespace std;
class Father
{
public:
virtual void func()
{
cout << "Father" << endl;
}
};
class Son:public Father
{
public:
void func()
{
cout << "Son" << endl;
}
};
int main()
{
// 指针且形成多态
Father *f0 = new Son;
Son* s0 = dynamic_cast<Son*>(f0);
cout << f0 << " " << s0 << endl;
f0->func(); // Son
s0->func(); // Son
// 指针未形成多态
Father *f1 = new Father;
Son *s1 = dynamic_cast<Son*>(f1);
cout << f1 << " " << s1 << endl; // 0xf827c8 0
f1->func(); // Father
// s1->func(); // 非法调用
// 引用且形成多态
Son s;
Father &f2 = s;
Son& s2 = dynamic_cast<Son&>(f2);
cout << &s2 << " " << &f2 << " " << &s << endl; // 0x61fe74 0x61fe74 0x61fe74
s2.func(); // Son
f2.func(); // Son
s.func(); // Son
Father f;
// Son &s3 = dynamic_cast<Son&>(f); // 运行终止
// cout << &s3 << " " << &f <<endl;
return 0;
}
8.3 const_cast
#include <iostream>
using namespace std;
class Test
{
public:
string str = "A";
};
int main()
{
const Test *t1 = new Test;
// t1->str = "B"; // 错误
cout << t1->str <<endl;
Test *t2 = const_cast<Test*>(t1);
t2->str = "B";
cout << t2->str << endl; // B
cout << t1->str << endl; // B
return 0;
}
8.4 reinterpret_cast
reinterpret_cast可以把内存里的值重新解释,这种转换风险极高,慎用!
#include <iostream>
using namespace std;
class A
{
public:
void print()
{
cout << "A" <<endl;
}
};
class B
{
public:
void print()
{
cout << "B" <<endl;
}
};
int main()
{
A *a = new A;
a->print(); // A
B *b = reinterpret_cast<B*>(a);
b->print(); // B
return 0;
}
二、异常处理
1. 概念
异常提供了一种转移控制权的方式。程序一旦出现没有经过处理的异常,就会造成程序运行崩溃。处理异常的方式有两种:抛出异常(throw)和捕获异常(try-catch)
2. 抛出异常
可以使用throw语句在代码块任何的位置抛出异常。throw语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出异常的类型,抛出的异常是抛出到函数调用的上一级 。
一个手动抛出异常的案例:
#include <iostream>
using namespace std;
double division(double a,double b)
{
if(b == 0)
{
string text("除数等于0!");
throw text; // 抛出一个std::string
}
return a/b;
}
double input()
{
cout <<"input函数开始执行" <<endl;
double a;
double b;
cout <<"请输入两个浮点型" <<endl;
cin >> a >> b;
double c = division(a,b); // text对象在这 1(无人处理)
cout <<"input执行结束" <<endl;
return c;
}
int main()
{
cout << "程序开始执行" <<endl;
cout << input() << endl; // 第二次抛出 text对象又在这(还是无人处理)
cout << "程序执行结束" <<endl;
return 0;
}
3. 捕获异常
如果有一个try代码块抛出了一个异常,捕获异常则使用catch代码块。
#include <iostream>
using namespace std;
double division(double a,double b)
{
if(b == 0)
{
string text("除数等于0!");
throw text; // 抛出一个std::string
}
return a/b;
}
double input()
{
cout <<"input函数开始执行" <<endl;
double a;
double b;
cout <<"请输入两个浮点型" <<endl;
cin >> a >> b;
double c;
try // 尝试执行代码块
{
c = division(a,b);
}
catch(string &e) // 尝试捕获 catch小括号中写抛出异常的类型(类型跟抛出的类型不符合,会出现捕获不到的情况)
{
// 验证异常对象
cout << e << endl;
// 补救措施
return 0;
}
cout <<"input执行结束" <<endl;
return c;
}
int main()
{
cout << "程序开始执行" <<endl;
cout << input() << endl; // 第二次抛出 text对象又在这(还是无人处理)
cout << "程序执行结束" <<endl;
return 0;
}
上述的代码中可能会出现几种情况:
1.无异常抛出,此时程序正常执行,不进入catch块
2.异常抛出,正确捕获,此时程序执行进入catch块
3.异常抛出,错误捕获(捕获类型不对),此时程序仍然会向上抛出异常寻求正确捕获,如果每一层都没有正确捕获,程序仍然会运行终止。
4. 标准异常体系
这个体系还是太薄弱,因此可以对其进行拓展。自定义一个类型,继承自某个异常类型即可。catch块可以匹配基类异常类型,提高匹配成功率,但是会降低匹配精度。
一个抛出自定义异常的例子:
#include <iostream>
#include <stdexcept>
using namespace std;
class MyException :public exception
{
public:
// 覆盖what函数
// throw():异常规格说明
// 表示此函数不会出现任何异常的抛出
const char* what() const throw()
{
return "自定义类型异常";
}
};
void show(string a,string b)
{
if(a == "#" || b == "#")
{
throw MyException();
}
cout << a << b << endl;
}
int main()
{
cout << "请输入两个字符串" << endl;
string a;
string b;
cin >> a >> b;
try
{
show(a,b);
}
catch(MyException &e)
{
cout << "返回异常信息:" << e.what() << endl;
}
cout << "您输入的是:" << a << b << endl;
return 0;
}
一个捕获标准类型的例子:
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
string s = "helloworld";
try
{
cout << s.at(100) <<endl;
}
catch(out_of_range &e)
{
// 输出错误信息
cout << e.what() << endl;
// 弥补措施
cout << -1 << endl;
}
cout << "hello" << endl;
return 0;
}
5. 多重捕获
一个try块可以配合多个catch块同时匹配。
#include <iostream>
#include <stdexcept>
using namespace std;
class MyException :public exception
{
public:
// 覆盖what函数
// throw():异常规格说明
// 表示此函数不会出现任何异常的抛出
const char* what() const throw()
{
return "自定义类型异常";
}
};
void show(string a,string b)
{
if(a == "#" || b == "#")
{
throw MyException();
}
cout << a << b << endl;
}
int main()
{
int type;
cout << "请输入1或者2或者其他数字" << endl;
cin >> type;
try
{
if(type == 1)
{
string s = "fddfd";
cout << s.at(100) << endl;
}
else if(type == 2)
{
throw overflow_error("异常2");
}
else
{
show("#","111");
}
}
catch(out_of_range &e)
{
cout << e.what() << "异常A" << endl;
}
catch(overflow_error &e)
{
cout << e.what() << "异常B" << endl;
}
catch(MyException &e)
{
cout << "异常C" << e.what() << endl;
}
cout << "程序正常执行结束" << endl;
return 0;
}
6. 粗略捕获
除了可以直接捕获异常类型外,也可以捕获异常的基类,甚至所有异常类型。
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
string s = "fsffs";
try
{
cout << s.at(100) << endl;
}
catch(logic_error &e)
{
cout << e.what() << "异常A" << endl;
}
cout << "程序运行结束" << endl;
return 0;
}
也可以粗略捕获与多重捕获同时使用,此时要注意捕获的顺序为派生类异常优先。
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
int type;
cout << "请输入1或者2或者3或者其他数字" << endl;
cin >> type;
try
{
if(type == 1)
{
string s = "fddfd";
cout << s.at(100) << endl;
}
else if(type == 2)
{
throw overflow_error("异常1");
}
else if(type == 3)
{
throw invalid_argument("异常2");
}
else
{
throw length_error("异常3");
}
}
catch(out_of_range &e)
{
cout << e.what() << "异常A" << endl;
}
catch(logic_error &e)
{
cout << e.what() << "异常B" << endl;
}
catch(overflow_error &e)
{
cout << e.what() << "异常C" << endl;
}
catch(runtime_error &e)
{
cout << e.what() << "异常D" << endl;
}
catch(exception &e)
{
cout << "异常E" << e.what() << endl;
}
cout << "程序正常执行结束" << endl;
return 0;
}
#include <iostream>
#include <stdexcept>
using namespace std;
double division(double a,double b)
{
if(b == 0)
{
throw "除数为0,错误!!";
}
return a/b;
}
int main()
{
try
{
cout << division(1,0);
}
catch(...) // 捕获所有异常类型
{
cout << "异常" << endl;
}
cout << "程序正常执行结束" << endl;
return 0;
}