目录
多态
1.概念
多态可以理解为“一种接口、多种状态”。只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。
多态的实现有三个前提条件:
●公有继承
●函数覆盖
●基类引用/指针指向派生类对象
多态的优点:多态的优势包括代码的灵活性,可扩展性和可维护性更高。他能够使代码更具通用性,减少重复代码的编写,并且能够轻松添加新的派生类或者拓展现有的功能。
多态的缺点:多态的缺点包括代码的复杂性、运行效率、易读性。当类的继承关系复杂性,会导致的多态相关的代码变得非常难以阅读。多态会在运行时产生一些额外的开销。在面向对象编程中,我们通常将多态分为两种类型:静态多态(也被称为编译时多态)和动态多态(也被称为运行时多态)。这两种多态都是多态性的不同表示方式。
静态多态:是在编译时就能确定要调用的方法,因为在编译阶段编译器就可以确定要调用的函数,通过函数重载、运算符重载、模板。
动态多态:是运行时根据对象的实际类型来确定要调用的函数,因为具体调用那个函数是在程序运行时根据对象的实际类型确定的,通过继承和函数覆盖来实现。
注意:本文中后续说的多态都是动态多态。
2.函数覆盖
函数覆盖、函数隐藏、这两个比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必要条件。函数覆盖比函数隐藏有以下区别:
1.函数隐藏是派生类中存在与基类同名同参数的函数,编译器会将基类的同名同参数的函数进行隐藏。注:函数隐藏,基类中的函数是一个普通的成员函数。
2.函数覆盖是基类中定义一个虚函数,派生类编写一个同名同参数的函数将基类中的虚函数进行重写并覆盖。注:覆盖的函数必须是虚函数。
3.虚函数的定义
一个函数使用virtual关键字修饰,就是虚函数,虚函数是函数覆盖的前提。
虚函数具有以下性质:
1.虚函数具有传递性,基类中的被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
2.只有普通成员函数与析构函数可以被声明为虚函数。3.在C++11中,可以在派生类新覆盖的函数上使用override关键字验证覆盖是否成功。
#include <iostream>
using namespace std;
class Animal
{
public:
// 错误,构造函数不能声明为虚函数
// virtual Animal(){}
// 错误,静态成员函数不能为虚函数
// virtual static void testStatic()
// {
// cout << "测试:静态成员函数虚函数" << endl;
// }
virtual void testStatic()const
{
cout << "测试:常成员函数虚函数" << endl;
}
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
void func()
{
cout << "测试:override关键字函数" << endl;
}
};
class Dog:public Animal
{
public:
// 函数覆盖,覆盖基类中的虚函数,派生类的virtual可写可不写
void eat() override
{
cout << "狗爱吃骨头" << endl;
}
// override关键字的作用是验证函数覆盖是否成功
// 注:这个是函数隐藏,并不是函数覆盖
// void func() override
// {
// cout << "测试:override关键字函数" << endl;
// }
};
int main()
{
return 0;
}
4.多态实现
1.实现运行时多态:当使用基类的指针或引用指向派生类对象时,程序在运行时会根据对象的实际类型来调用相应的函数,而不是根据指针或者引用类型。
2.统一接口:基类的指针可以作为一个通用的接口,用于操作不同类型的派生类对象。我们可以提供通用接口,参数设计为基类的指针或者引用,这样这个函数就可以访问到此基类所有派生类中的虚函数了。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗爱吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "猫爱吃鱼" << endl;
}
};
void animal_eat1(Animal &a1)
{
a1.eat();
}
void animal_eat2(Animal *a2)
{
a2->eat();
}
int main()
{
// 基类的指针指向派生类对象
Animal *a1 = new Dog;
// 调用派生类覆盖的虚函数
a1->eat(); // 狗爱吃骨头
// 基类的引用指向派生类对象
Dog d1;
Animal &a2 = d1;
a2.eat(); // 狗爱吃骨头
// 传递引用
Dog d2;
Cat c1;
animal_eat1(d2); // 狗爱吃骨头
animal_eat1(c1); // 猫吃鱼
// 传递指针
Dog *d3 = new Dog;
Cat *c2 = new Cat;
animal_eat2(d3);
animal_eat2(c2);
return 0;
}
5.多态原理
具有虚函数的类会存在一张虚函数表,每个类的对象内部会有一个隐藏的虚函数表指针成员,指向当前类的虚函数表。
多态的实现流程图:
在代码运行时,通过对象的虚函数表指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应的虚函数内容。
6.虚析构函数
如果不使用虚析构函数,且基类指针指向派生类对象,使用delete销毁对象时,只能调用基类的析构函数,如果在派生类中申请内存等资源,则会导致无法释放,出现内存泄漏
解决方案是给基类的析构函数使用virtual修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数,因此建议给一个可能为基类的类中的析构函数设置为虚析构函数。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
// 虚析构函数
virtual ~Animal()
{
cout << "基类析构函数被调用了" << endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗爱吃骨头" << endl;
}
~Dog()
{
cout << "派生类析构函数被调用了" << endl;
}
};
int main()
{
Animal *a1 = new Dog;
delete a1;
return 0;
}
7.类型转换
除了虚析构函数外,还可以使用类型转换解决内存泄漏的问题,在C++11中不建议使用C风格类型转换,因为可能会带来一些安全隐患,让程序的错误难以发现,所以C++11提供了一组适用于不同场景的强制转换函数
(1)static_cast(静态转换)
主要用于基本数据类型之间的转换。
static_cast没有运行时类型检查来确保转换的安全性,需要程序员手动判断转换是否安全。
static_cast也可以用于类层次转换中,即基类和派生类指针或者引用之间的转换。
static_cast进行上行转换是安全的,即把派生类的指针或者引用转换成基类的。
static_cast进行下行转换是不安全的,即把基类的指针或者引用转换成派生类的。static_cast和C语言的强制类型转换相比:
1.static_cast的表达式更清晰,方便管理。
2.static_cast会在编译时进行类型检查。3.static_cast也可以转换自定义类型,但是目标类型必须要包含对应参数的构造函数。
#include <iostream>
using namespace std;
class Father
{
public:
string a = "Father";
};
class Son:public Father
{
public:
string b = "Son";
};
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;
return 0;
}
(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 << s0 << " " << f0 << endl;
f0->func(); // Son
s0->func(); // Son
// 指针没有形成多态
Father *f1 = new Father;
Son *s1 = dynamic_cast<Son*>(f1);
cout << f1 << " " << s1 << endl; // 0xe72818 0
f1->func(); // Father
// s1->func(); // 非法调用
// 引用且形成多态
Son s;
Father &f2 = s;
Son &s2 = dynamic_cast<Son &>(f2);
cout << &s2 << " " << &f2 << " " << &s << endl;
s2.func();
f2.func();
s.func();
// 引用且不形成多态
Father f;
Son &s3 = dynamic_cast<Son &>(f); // 运行终止
cout << &s3 << " " << &f << endl;
s3.func();
return 0;
}
(3)const_cast(常量转换)
const_cast可以添加或者移除对象的const限定符。主要用于改变指针或者引用的const效果,以便于在一定的情况下修改原本被声明为常量的对象,避免使用const_cast,而是考虑通过设计良好的接口或者其他正常手段,避免使用const_cast进行此类转换。
#include <iostream>
using namespace std;
class Test
{
public:
string str = "A";
};
int main()
{
const Test* t1 = new Test;
// t1->str = "B"; // 错误
Test *t2 = const_cast<Test*>(t1);
t2->str = "B";
cout << t1->str << " " << t2->str << endl;
cout << t1 << " " << t2 << endl;
return 0;
}
(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;
B* b = reinterpret_cast<B*>(a);
cout << a << " " << b << endl;
a->print(); // A
b->print(); // B
return 0;
}
抽象类
如果基类只表达一些抽象的概念,并不与实际的对象相关联,这时候就可以使用抽象类了。
如果一个类中有纯虚函数,则这个类就是一个抽象类。
如果一个类是抽象类,则这个类中一定有纯虚函数。
纯虚函数是虚函数的一种,这种函数只有声明没有定义。
virtual 返回值类型 函数名(参数列表) = 0;不能直接使用抽象类作为声明类型,因为不存在抽象类类型的对象。
抽象类作为基类时,具有两种情况:
1.派生类继承抽象类,覆盖并实现其所有的纯虚函数,此时派生类可以作为普通类使用,即不再是抽象类。
2.派生类继承抽象类,没有把抽象类中所有纯虚函数覆盖并实现,此时派生类也会变为抽象类,等待他的派生类覆盖并实现剩余的纯虚函数。
抽象类的使用注意以下几点:
1.抽象类析构函数必须为虚析构函数。
2.抽象类支持多态,可以存在引用或者指针的声明格式。
3.因为抽象类的作用是指定算法框架。
#include <iostream>
using namespace std;
// 形状-抽象类
class Shape
{
public:
virtual void area() = 0; // 面积
virtual void perimeter() = 0; // 周长
virtual ~Shape()
{
}
};
// 圆形
class Circle:public Shape
{
public:
// 函数覆盖并实现所有的纯虚函数
void area()
{
cout << "圆形计算面积" << endl;
}
void perimeter()
{
cout << "圆形计算周长" << endl;
}
};
// 多边形
class polygon:public Shape
{
public:
void perimeter()
{
cout << "多边形计算周长" << endl;
}
};
// 矩形
class Rectangle:public polygon
{
public:
void area()
{
cout << "矩形计算面积" << endl;
}
};
int main()
{
// Shape s;
Circle c;
c.area();
c.perimeter();
// polygon p; // (多边形类) 错误 抽象类无法实例化对象
// p.perimeter();
Rectangle r;
r.area();
r.perimeter();
Shape *s = new Circle;
s->area();
s->perimeter();
return 0;
}
纯虚析构函数
纯虚析构的本质:是析构函数,作用是各个类的回收工作。而且析构函数不能被继承。
必须要给纯虚析构函数提供一个函数体。
纯虚析构函数,必须类内声明类外实现。
#include <iostream>
using namespace std;
// 抽象类
class Animal
{
public:
Animal()
{
cout << "基类构造函数被调用了" << endl;
}
// 纯虚析构函数
virtual ~Animal() = 0;
};
// 实现
Animal::~Animal()
{
cout << "基类的析构函数被调用了" << endl;
}
class Dog:public Animal
{
public:
Dog()
{
cout << "Dog类的构造函数被调用了" << endl;
}
};
int main()
{
// Animal a; // 错误基类的纯虚析构函数,无法实例化对象
Animal * a1 = new Dog;
delete a1;
return 0;
}
虚析构函数与纯虚析构函数的区别:
虚析构:virtual关键字修饰,有函数体,不会导致类为抽象类
纯虚析构函数:virtual关键字修饰,结果=0。函数体需要类外实现,会导致类为抽象类。(无法实例化对象)
私有析构函数
析构函数无法正常的执行时,会引发一些对象的销毁问题。析构函数私有化后,会出现两种情况:
1、外部的堆内存对象只能new,无法正常delete
2、外部的栈内存对象无法创建。
#include <iostream>
using namespace std;
class Test
{
private:
~Test(){}
};
int main()
{
Test *t1 = new Test;
// delete t1; // 错误
// Test t2; // 错误
return 0;
}