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