C++中的多态

    技术2023-10-09  100

    C++中的多态

    1.多态的概念2.多态的定义即实现2.1多态定义的构成条件2.2什么是虚函数2.3虚函数的重写2.4虚函数重写的例外 3. 重载、覆盖(重写)、隐藏(重定义)的对比4. C++11 override 和 final5.抽象类6.多态常见的问题

    1.多态的概念

    在面向对象语言中,接口的多种不同实现方式即为多态。具体来说就是去完成某个行为,当调用不同的对象去完成时会产生不同的状态。

    例如:在车站买票这个行为,学生去购买学生票就是半价;普通人去买票则是全价。

    2.多态的定义即实现

    2.1多态定义的构成条件

    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

    构成多态的两个条件:

    调用函数的对象必须是指针或者引用。被调用的函数必须是虚函数,且完成了虚函数的重写。

    2.2什么是虚函数

    虚函数:被virtual关键字修饰的类的成员函数称为虚函数, (virtual不能加在内联函数和静态成员函数之前) 继承后可以被重写。

    作用:实现多态性

    class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;} };

    2.3虚函数的重写

    虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数,(完全相同是指:函数名、参数、返回值都相同)另外虚函数的重写也叫作虚函数的覆盖。

    #include<iostream> using namespace std; class Person { public: virtual void BuyTicket() { //定义为虚函数 cout << "全票" << endl; } }; class Student :public Person { public: virtual void BuyTicket() { //重写基类虚函数 cout << "学生半价"<<endl; } }; class Soldier :public Person { public: virtual void BuyTicket() { //重写基类虚函数 cout << "军人优先购票"<<endl; } }; void Buy(Person& people) { people.BuyTicket(); } int main() { Person p; Student st; Soldier so; Buy(p); Buy(st); Buy(so); system("pause"); return 0; }

    在上述代码中,当我们调用Buy()函数传参时,把Student类和Solider类对象的引用赋值给Person的引用,这是因为继承中派生类对象可以赋值给基类的指针 / 基类的引用 / 基类的对象,把派生类中基类的那部分切割下来赋值过去(切片操作)。 这样,Student和Solider各自的对象调用Buy()函数就能调用到他们各自继承的Person类。 结果可以看到:继承同一个Person类, Buy中调用的是同一个函数, 却产生了不同的行为, 这就是多态。

    2.4虚函数重写的例外

    虚函数重写要求基类和派生类有完全相同(返回值类型、函数名字、参数列表完全相同)的虚函数, 但下面两种情况就是基类和派生类虚函数没有完全相同, 但也会发生重写。

    1.协变 重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。

    #include<iostream> using namespace std; class Person { public: //返回值为基类指针和派生类指针 virtual Person* f() { cout << "Person类的函数调用" << endl; return nullptr; } }; class Student:public Person{ public: virtual Student* f() { cout << "Student类的函数调用" << endl; return nullptr; } }; int main() { Person* p = new Student;//派生类对象赋值给基类指针(切片) p->f(); return 0; }

    Student类对象的指针赋值给Person类的指针,发生切片。调用了Student类中被重写的函数。

    2.析构函数的重写问题 基类中的析构函数如果是虚函数,在派生类中我们定义一个析构函数,无论有没有关键字virtual都会重写了基类的析构函数。 这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。

    C++支持派生类对象指针赋给基类对象指针,当我们new出一个派生类对象, 将其指针赋给一个基类指针。最后本该释放派生类对象指针时我们释放了基类指针, 这时就会造成只释放基类内存和调用基类析构函数, 而派生类自己的部分却没有释放, 造成了内存泄漏。

    #include<iostream> using namespace std; class Person { public: ~Person() { cout << "基类的析构函数调用" << endl; } }; class Student :public Person { public: ~Student() { cout << "派生类的析构函数调用" << endl; } }; int main() { Person* p = new Student;//将派生类指针赋值给基类指针 delete p; //只会调用基类析构 return 0; }

    只释放基类内存和调用基类析构函数, 而派生类自己的部分却没有释放, 造成了内存泄漏。 解决这个问题就需要用到重写析构函数。

    #include<iostream> using namespace std; class Person { public: virtual ~Person() { cout << "基类的析构函数调用" << endl; } }; class Student :public Person { public: virtual ~Student() { cout << "派生类的析构函数调用" << endl; } }; int main() { Person* p = new Student; delete p; return 0; }

    3. 重载、覆盖(重写)、隐藏(重定义)的对比

    4. C++11 override 和 final

    在继承中我们说在定义类时在后面加上final关键字, 则这个类不能被继承, 而final还能加在虚函数后面, 限定这个虚函数不能被继承。

    override修饰派生类虚函数强制完成重写, 派生类对这个override修饰的基类虚函数必须重写 , 否则编译不通过。

    5.抽象类

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    class Person { public: virtual void f() = 0; };

    抽象类中的纯虚函数对于其派生类来讲, 只继承了一个函数接口, 派生类要用的话, 只能重写。而基类虚函数对于其派生类而言, 接口、实现都有。在派生类使用这个函数时, 即可以重写, 也可以不重写。

    接口继承和实现继承

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,派生类继承后也只有一个接口。目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

    虚函数的继承, 当派生类重写基类虚函数时, 可以看作是接口继承。当派生类没有重写基类虚函数, 可以看做实现继承。

    6.多态常见的问题

    什么是多态?

    接口的多种不同实现方式即为多态。具体来说就是去完成某个行为,当调用不同的对象去完成时会产生不同的状态。

    什么是重载、重写(覆盖)、重定义(隐藏)?

    重载 : C++允许同一作用域中有同名函数, 这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同(返回值类型无要求),在处理实现功能类似数据类型不同的问题上保证了接口的统一性。 重写 : 也叫覆盖。在继承关系中, 派生类中有跟基类完全相同(返回值类型、函数名字、参数列表完全相同) 的虚函数,称派生类的虚函数重写了基类的虚函数。 重定义 : 一般叫隐藏, 在继承关系中, 在派生类中有跟基类同名的普通成员函数(只要函数名相同, 与返回值类型和参数列表无关),则基类在派生类中被隐藏。 隐藏对成员变量也一样, 不管变量类型, 只要变量名相同, 基类成员变量在派生类中也会被隐藏。

    多态的实现原理?

    利用了虚函数可以重写的特性, 当一个有虚函数的基类有多个派生类时, 通过各个派生类对基类虚函数的不同重写, 实现通过指向派生类对象的基类指针或引用调用同一个虚函数, 去实现不同功能的特性。

    inline函数可以是虚函数吗?

    不能,因为inline函数没有地址,无法把地址放到虚函数表中。

    静态成员可以是虚函数吗?

    不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。

    构造函数可以是虚函数吗?

    不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的。

    析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

    可以,并且最好把基类的析构函数定义成虚函数。为了防止程序中new出的派生类对象, 将其赋给基类指针后, 只delete了基类指针, 而派生类自己的部分没有被释放, 造成内存泄漏的后果。

    对象访问普通函数快还是虚函数更快?

    如果是普通对象,是一样快的。 如果是指针对象或者是引用对象,则调用的普通函数快。因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

    虚函数表是在什么阶段生成的,存在哪的?

    虚函数是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

    C++菱形继承的问题?虚继承的原理?

    当一个子类继承的多个父类中, 有两个以上的父类是同一个爷爷类(父类的的父类, 这里为了区分, 叫爷爷类), 则此时就发生了菱形继承, 产生了数据冗余性和二义性的问题, 用虚拟继承解决, 虚拟继承原理是:虚拟继承时, 派生类中引入了入一个虚基表来解决这两个问题, 虚基表中放着继承来的虚基类指针, 用来标识自己的父类, 当继承时通过虚基表中的指针来判别是否发生了菱形继承, 如果是, 则只继承一份。

    什么是抽象类?抽象类的作用?

    包含纯虚函数的类, 不能实例化处对象,抽象类(纯虚函数)强制重写了虚函数。作用: 体现了接口继承。

    Processed: 0.016, SQL: 9