C++(面向对象编程——多态)
多态
1.概念
多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),它允许不同对象对同一消息做出不同响应。
2.函数覆盖
2.1 概念:
函数覆盖是实现运行时多态的核心,它允许派生类重新定义基类的虚函数,实现特定于派生类的行为,是继承体系中最重要的特性之一。被virtual修饰的成员函数就是虚函数,如果一个类有虚函数,就会给该类生成一个vtable(虚机类表)。
2.2 虚函数性质:
(1)虚函数具有传递性,基类的虚函数可以把派生类新覆盖的函数编程虚函数。
(2)成员函数可以设置为虚函数,静态成员函数不能设置为虚函数。
(3)构造函数不能设置为虚函数,析构函数可以设置为虚函数。
(4)如果函数声明与定义分离,只需要使用virtual修饰声明即可。
(5)在C++11中,可以使用override关键字验证是否覆盖成功
2.3 和函数隐藏的区别
特点 | 函数重写(函数覆盖) | 函数隐藏 |
---|---|---|
关键字 | virtual/override | 无需关键字 |
参数列表 | 必须完全一致 | 可以不同 |
返回类型 | 必须一致(或协变) | 可以不同 |
多态 | 支持(虚函数) | 不支持 |
3.多态的原理
3.1 定义,继承和声明的原理:
定义类的时候,当一个类中有虚函数的时候,编译器会为这个类分配一个表记录这些虚函数,这个表就是虚机类表。简单理解就是你创建一个类的时候,如果有使用virtual修饰函数,就认定这个类中存在虚函数,就会自动分配一个表(虚函数表)来存放这个类的虚函数,是把你这个类所有的虚函数都放在这一个表里。所以虚函数表是一个类只可能存在一个。
当继承存在虚函数的类的时候,派生类是复制一份虚函数表,单独给派生类使用,如果派生类和基类共用一个表,函数覆盖就会改变基类的虚函数,会改变基类的原始功能,基类可能会定义其他对象或者被别的类继承,“动一发而牵全身”,就会乱套了。
当声明存在虚函数的类的对象的时候,就会自动生成一个隐藏成员指针,这个指针指向类的虚机类表。
3.2 补充:
如图中定义Dog和Cat类的对象,对象中会自动生成一个成员变量,以便能调用虚函数和覆盖(重写)的虚函数。(图中类对应源代码中的类)
我们都知道指针类型大小是4字节,所以成员Dog的大小是12(两个string和一个隐藏的成员指针)。
如果类中还存在其他没有被virtual修饰的函数,这些函数存在哪里呢?为什么不算在对象所占的大小能?这不是虚函数的函数被存在特定的代码段里,这就涉及到底层设计,我们只需要知道这些函数被存放在其他位置存在对象中就可以了。这样做很明显是为了减少代码复用,每创建一次对象就存一份函数,开销太大了。
#include <iostream>using namespace std;class Animal{
private:string breed;
public:Animal(string breed):breed(breed){}string get_breed(){return breed;}void set_breed(string breed){this->breed = breed;}virtual void print(){ //使用virtual修饰,函数覆盖,实现多态的条件cout << breed << endl;}/*其他虚函数*/
};class Dog:public Animal{
private:string name;
public:Dog(string breed,string name):Animal(breed),name(name){}void print(){ //函数覆盖Animal::print(); //调用基类的print函数打印breed成员变量cout << name << endl;}
};class Cat:public Animal{
private:string favorite;
public:Cat(string breed,string favorite):Animal(breed),favorite(favorite){}void print(){ //函数覆盖Animal::print();cout << favorite << endl;}
};int main (){return 0;
}
4.多态的使用
4.1 分类
广义上将,多态分为静态多态和动态多态。
静态多态被称为编译时多态,发生在程序编译阶段;(函数重载)
动态多态也被称为运行时多态,发生在程序的运行阶段,这才算是正真意义上的多态,后面称呼的多态都是动态多态。
4.2 静态多态和动态多态的区别:
特点 静态多态(Static) 动态多态(Dynamic) 决定时机 编译时 运行时 实现方式 重载、模板 虚函数、继承 性能 高,无虚表开销 有虚表开销 典型场景 算法泛型、重载 多态接口、插件架构 关键字 无需 virtual 需要 virtual/override
4.3 多态实现的三个条件:
(1)必须是拥有继承
(2)发生函数覆盖
(3)基类指针指向/引用派生类对象
4.4 实现多态的思想:
多态按照字面的理解就是“多种状态”,可以概括为“一个接口,多种状态”,例如一个函数接收参数,程序的运行的过程中根据传入的参数类型的不同,自动决定调用不同的函数逻辑。说白了就是可以通过基类定义其他任意子类的对象,还能实现不同类之间转换(下面会说)。
下面代码中,注释掉的部分是使用最多的方式,可以使用基类创建不同类型的派生类对象,只要满足多态三个条件,这就体现出一个接口,多个状态。
第二种常用的就是成员函数接口,为什么要写两遍?这是为了对应定义的两中存储方式,一个是堆区指针,一个是栈区引用。
#include <iostream>using namespace std;class Animal{
private:string breed;
public:Animal(string breed):breed(breed){}string get_breed(){return breed;}void set_breed(string breed){this->breed = breed;}virtual void print(){ //使用virtual修饰,函数覆盖,实现多态的条件cout << breed << endl;}
};
class Dog:public Animal{ //1.公有继承
private:string name;
public:Dog(string breed,string name):Animal(breed),name(name){}void print(){ //2.函数覆盖Animal::print();cout << name << endl;}
};
class Cat:public Animal{
private:string favorite;
public:Cat(string breed,string favorite):Animal(breed),favorite(favorite){}void print(){ //函数覆盖Animal::print();cout << favorite << endl;}
};void print(Animal &s){ //3.引用派生类s.print();
}
void print(Animal *s){ //3.基类函数指向s->print();
}int main (){/*Dog d1("狗","金毛");Animal &a1 = d1; //栈类型定义a1.print();Animal *a2 = new Cat("猫","鲨鱼"); //堆类型定义a2->print();delete a2;*/Animal a1("猫");Dog d1("狗","金毛");Cat c1("猫","鱼");print(a1); //调用函数print(d1);print(c1);Animal *a2 = new Animal("狗");Dog *d2 = new Dog("狗","哈巴狗");Cat *c2 = new Cat("猫","猫条");print(a2);print(d2);print(c2);delete a2;delete d2;delete c2;return 0;
}
5.虚析构函数
当基类指针指向或引用派生类对象时,容易出现内存泄露的问题,因为派生类构造函数可能不会被调用。
当你使用上面第一种常用的方式声明一个堆区对象,编译器只会调用基类的析构函数,继承的时候,析构函数和构造函数是没有继承的,因为虚函数表中没有析构函数,只会调用基类析构函数,不然就会发生内存泄露问题(内存被没有释放的派生类一直占用知道程序结束才释放)。
下面代码基类的析构函数就是没有添加virtual修饰的,delete堆区多态的对象就会只调用基类的析构函数。可以自行在基类析构函数前面添加virtual试试。
在设计类时,如果使用默认的析构函数会存在上面的隐患,建议给所有顶层基类的析构函数都设置为虚析构函数。
#include <iostream>using namespace std;class Animal{
private:string breed;
public:Animal(string breed):breed(breed){}virtual void print(){ //使用virtual修饰,函数覆盖,实现多态的条件cout << breed << endl;}//其他虚函数~ Animal(){cout << breed << "析构函数" << endl;}
};
class Dog:public Animal{
private:string name;
public:Dog(string breed,string name):Animal(breed),name(name){}void print(){ //函数覆盖Animal::print();cout << name << endl;}~ Dog(){cout << "Dog" << endl;}
};
class Cat:public Animal{
private:string favorite;
public:Cat(string breed,string favorite):Animal(breed),favorite(favorite){}void print(){ //函数覆盖Animal::print();cout << favorite << endl;}~ Cat(){cout << "Cat" << endl;}
};int main (){/* //不使用多态定义和不使用堆区定义对象都不会出问题Dog d1("狗","金毛");Cat c1("猫","鱼");Dog *d2 = new Dog("狗","哈巴狗");Cat *c2 = new Cat("猫","猫条");*/Dog d0("狗","金毛");Animal &d1 = d0;Cat c0("猫","鱼");Animal &c1 = c0;Animal *d2 = new Dog("狗","哈巴狗");Animal *c2 = new Cat("猫","猫条");delete d2;delete c2;cout << "#####################" << endl;return 0;
}
类型转换
1.概念:
就是类型转换,延续了C语言类型转换还增加了其他的转换。为什么放在多态里说,是因为这里包含的同属于一个基类的不同状态之间的转换。
2.分类:
static_cast 静态转换
dynamic_cast 动态转换
const_cast 常量转换
reinterpret_cast 重解释转换
2.1 static_cast
(1)主要用于基本数据类型转换。
#include <iostream>using namespace std;int main()
{int x = 1;
// double y = (double)x;double y = static_cast<double>(x);cout << y << endl;return 0;
}
(2)static_cast相比于C转换,可以在编译时进行类型检查,确保转换是合法的。如果转换不合法,编译器将报错,从而帮助开发者在编译阶段就发现并修复问题。
(3)但是它没有运行时类型检查来保证转换的安全性,需要程序员来判断转换是否安全。(比如double转换成int精度丢失不会提示)
(4)static_cast也可以用于类层次结构转换(但不擅长),即基类和派生类之间的指针或引用的转换,但是只能安全的进行上行转换:派生类指针或引用转换为基类的。
上行转换:
是把不是基类的成员直接去掉,如果对上行转换过的对象再下行转换,是可以成功的,上行转换去掉的成员会保存。
#include <iostream>using namespace std;class Animal{
public:string breed;Animal(){}Animal(string breed):breed(breed){}virtual void print(){cout << breed << endl;}
};
class Dog:public Animal{
public:string name;Dog(){}Dog(string breed,string name):Animal(breed),name(name){}void print(){Animal::print();cout << name << endl;}
};int main (){//上行转换Dog d0("狗","金毛");Animal &d1 = d0; //定义栈区多态对象Animal& a1 = static_cast<Animal&>(d1); //上行转换//d1.print();cout << a1.breed << endl;//cout << a1.name << endl; //错误(上行转换后不存在name成员函数)return 0;
}
下行转换:
对没有上行转换的对象,直接下行转换的时候,static_cast不知道该如何补齐少的成员,可能会访问到电脑的内存,能运行,但是会输出很多访问到的电脑信息(不安全)。
#include <iostream>using namespace std;class Animal{
public:string breed;Animal(){}Animal(string breed):breed(breed){}virtual void print(){cout << breed << endl;}
};
class Dog:public Animal{
public:string name;Dog(){}Dog(string breed,string name):Animal(breed),name(name){}void print(){Animal::print();cout << name << endl;}
};int main (){//上行转换Dog d0("狗","金毛");Animal &d1 = d0; //定义栈区多态对象Animal &a1 = static_cast<Animal&>(d1); //上行转换//d1.print();cout << a1.breed << endl;//cout << a1.name << endl; //错误(上行转换后不存在name成员函数)Animal a2("猫");Dog &d2 = static_cast<Dog&>(a2);cout << d2.breed << endl;cout << d2.name << endl;return 0;
}
(6)static_cast在特殊情况下,也可以转换为自定义类型,实际上是隐式调用了构造函数。
下面对Dog类进行隐式调用构造函数,传了两个值,但是结果不是“猫”和“金渐层”,这是static_cast只能传最后一个参数,所以只把金渐层当做第一个参数传入。(了解)
2.2 dynamic_cast
dynamic_cast主要用于类层次之间的转换:
上行转换与static_cast完全相同
下行转换比static_cast更加安全,有类型检查功能:
#include <iostream>using namespace std;class Animal{
public:string breed;Animal(){}Animal(string breed):breed(breed){}virtual void print(){cout << breed << endl;}
};
class Dog:public Animal{
public:string name;Dog(){}Dog(string breed,string name):Animal(breed),name(name){}void print(){Animal::print();cout << name << endl;}
};int main (){//上行转换Dog d("狗","金毛");Animal &a = d; //定义栈区多态对象Animal& d1 = dynamic_cast<Animal&>(a); //上行转换//d1.print();cout << d1.breed << endl;//cout << d1.name << endl; //错误(上行转换后不存在name成员函数)return 0;
}
2.3 const_cast
const_cast用于添加或移除对象的const修饰符。
主要的用法是改变指针或引用的const特性,以便于在某些情况下修改const变量。正常的代码不应该使用const_cast,而是应该通过设计良好的代码逻辑避免const_cast的出现。
#include <iostream>using namespace std;class Test
{
public:string str = "A";
};int main()
{const Test* t1 = new Test;cout << t1->str << endl;
// t1->str = "B"; 错误Test* t2 = const_cast<Test*>(t1);cout << t1 << " " << t2 << endl; // 0x1001118 0x1001118t2->str = "B";cout << t1->str << endl; // Breturn 0;
}
2.4 reinterpret_cast
reinterpret_cast可以把内存中的值按照制定的规则重新解释,这种转换风险极高,慎用!
#include <iostream>using namespace std;class A
{
public:string str = "A";
};class B
{
public:char str = 'B';
};int main()
{A* a = new A;B* b = reinterpret_cast<B*>(a);cout << a << " " << b << endl; // 0x10b1118 0x10b1118cout << b->str << endl;char c = '@';char* ptr_c = &c;int* ptr_i = reinterpret_cast<int*>(ptr_c);cout << *ptr_i << endl; // 1644068672return 0;
}
const_cast和reinterpret_cast都偏向底层操作的,一般用不到,对系统感兴趣的可以详细学习。
抽象类
1.基本使用:
1.1 概念:
抽象类是一种不能被实例化的类,通常用于定义其他类的通用结构和行为。它允许声明抽象方法(即没有具体实现的方法),子类必须实现这些方法。可以用来做框架。
1.2 使用:
存在一个纯虚函数的类就是抽象类,这样的抽象类就不能用来声明对象。什么是纯虚函数呢?就是像下面代码中set_breed函数和set_name函数,前面加上virtual修饰,后面加上=0,这样的没有函数体的函数就是纯虚函数。(纯虚函数就是特殊的虚函数)
class Animal{
public:string breed;string name;virtual void set_breed(string breed) = 0;virtual void set_name(string breed) = 0;void print(){cout << breed << " " << name << endl;}
};
2.继承
继承抽象类的派生类是不是抽象类取决于派生类是否实现基类中的纯虚函数。
2.1 派生类实现所有的纯虚函数
实现(reimplement)指的是覆盖基类的纯虚函数,并增加函数体。
如果派生类实现了所有的纯虚函数(继承来的),此时派生类中就没有纯虚函数了,派生类可以作为普通类使用。
#include <iostream>using namespace std;class Animal{
public:string breed;string name;virtual void set_breed(string breed) = 0;virtual void set_name(string breed) = 0;void print(){cout << breed << " " << name << endl;}
};class Cat:public Animal{
public:void set_breed(string breed){ //覆盖纯虚函数this->breed = breed;}void set_name(string name){ //覆盖纯虚函数this->name = name;}
};int main (){Cat c;c.set_breed("猫");c.set_name("mike");c.print();return 0;
}
2.2 派生类没有实现所有纯虚函数
这种情况下,派生类(Polygon)中仍然存在纯虚函数(继承来的),此时派生类仍然是一个抽象类,需要继续向下继承,直到某个派生类(Rectangle)全部实现纯虚函数。
#include <iostream>using namespace std;class Animal{
public:string breed;string name;virtual void set_breed(string breed) = 0;virtual void set_name(string breed) = 0;void print(){cout << breed << " " << name << endl;}
};class Cat:public Animal{
public:void set_breed(string breed){ //覆盖纯虚函数this->breed = breed;}//此时派生类还存在一个纯虚函数,此类仍是抽象类,不可以用来声明对象/*void set_name(string name){ //覆盖纯虚函数this->name = name;}*/
};class Mycat:public Cat{
public:void set_name(string name){ //覆盖纯虚函数this->name = name;}
};int main (){//Cat c0; //错误!Mycat c; c.set_breed("猫");c.set_name("mike");c.print();return 0;
}
3.多态
抽象类型不能声明,但是抽象类型的引用和指针可以声明,因为抽象类支持多态。如果派生类实现了纯虚函数,可以使用基类声明对象,但是必须使用派生类构造。
#include <iostream>using namespace std;class Animal{
public:virtual void print()=0;virtual ~Animal(){} //防止堆区内存泄露,基类虚析构
};class Cat:public Animal{
public:void print(){cout << "这是猫,喵喵喵" << endl;}
};class Dog:public Animal{
public:void print(){cout << "这是狗,汪汪汪" << endl;}
};int main (){//栈区对象多态Cat c0;Animal &c = c0;c.print();Dog d0;Animal &d = d0;d.print();cout << "######################" << endl;//堆区对象多态Animal *d1 = new Dog;Animal *c1 = new Cat;d1->print();c1->print();delete d1;delete c1;return 0;
}
4.纯虚析构函数:
如果一个类没有纯虚函数又想成为抽象类,可以把析构函数写成纯虚析构函数“占坑”。
#include <iostream>using namespace std;class Animal{
public:virtual void print(){cout << "hello" << endl;}// 声明纯虚析构函数virtual ~Animal() = 0;
};// 为了不产生析构问题,添加析构函数的定义
Animal::~Animal(){ cout << "必要" << endl;
}class Cat:public Animal{
public:void print(){cout << "这是猫,喵喵喵" << endl;}
};class Dog:public Animal{
public:void print(){cout << "这是狗,汪汪汪" << endl;}
};int main (){//栈区对象多态Cat c0;Animal &c = c0;c.print();Dog d0;Animal &d = d0;d.print();cout << "######################" << endl;//堆区对象多态Animal *d1 = new Dog;Animal *c1 = new Cat;d1->print();c1->print();delete d1;delete c1;cout << "######################" << endl;return 0;
}