C++多重继承及多态性原理实例详解

一、多重继承的二义性问题

举例:

#include <iostream>
using namespace std;
class BaseA {
public:	void fun() { cout << "A.fun" << endl; }
};
class BaseB {
public:
	void fun() { cout << "B.fun" << endl; }
	void tun() { cout << "B.tun" << endl; }
};
class Derived :public BaseA, public BaseB {
public:
	void tun() { cout << "D.tun" << endl; }
	void hun() { fun(); } //此处调用出现二义性,编译无法通过
};
int main() {
	Derived d, * p = &d; d.hun();
	return 0;
}

类名限定

void hun() { BaseA::fun(); } //改写上述代码的第14行,用BaseA类名限定调用的函数
d.BaseB::fun(); //派生类对象调用基类同名函数时,使用类名限定
p->BaseB::fun(); //派生类指针调用基类同名函数时,使用类名限定

名字支配规则

如果存在两个或多个包含关系的作用域,外层声明了一个名字,而内层没有再次声明相同的名字,则外层名字在内层可见;如果在内层声明了相同的名字,则外层名字在内层不可见——隐藏(屏蔽)规则。

在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用域不同:基类在外层,派生类在内层。如果派生类声明了一个和基类成员同名的新成员,则派生类的新成员就会屏蔽基类的同名成员,直接使用成员名只能访问到派生类新增的成员。(如需使用从基类继承的成员,应当使用基类名限定)

#include <iostream>
using namespace std;
class Base {
public:
	void fun() { cout << "A.fun" << endl; }
	Base(int x = 1, int y = 2) :x(x), y(y) {}
	int x, y;
};

class Derived :public Base{
public:
	void tun() { cout << "D.tun" << endl; }
	void fun() { cout << "D.fun" << endl; }
	Derived(int x = 0) :x(x) {}
	int x;
};
int main() {
	Derived d, * p = &d;
	d.fun();	//输出的结果为	D.fun
	cout << p->x << " " << p->y << " " << p->Base::x << endl; //输出为 0 2 1
	d.Base::fun(); //输出为	A.fun
}

二、虚基类

虚基类的使用目的:在继承间接基类时只保留一份成员。

声明虚基类需要在派生类定义时,指定继承方式的时候声明,只需要在访问标号(public、protected、private继承方式)前加上virtual关键字。注意:为了保证虚基类在派生类中只继承依次,应当在该基类的所有直接派生类中声明为虚基类,否则仍会出现多次继承。

派生类不仅要负责对直接基类进行初始化,还要负责对虚基类初始化;若多重继承中没有虚基类,则派生类只需要对间接基类进行初始化,而对基类的初始化由各个间接基类完成(会因此产生多个基类的副本保存在各个间接基类中)。

#include <iostream>
using namespace std;
class Base {
public:
	Base(int n) { nv = n; cout << "Member of Base" << endl; }
	void fun() { cout << "fun of Base" << endl; }
private:
	int nv;
};
class A:virtual public Base {	//声明Base为虚基类,作为间接基类都需要使用virtual关键字
public:
	A(int a) :Base(a) { cout << "Member of A" << endl; }
private:
	int na;
};
class B :virtual public Base { //声明Base为虚基类,作为间接基类都需要使用virtual关键字
public:
	B(int b) :Base(b) { cout << "Member of B" << endl; }
private:
	int nb;
};
class Derived :public A, public B {
public:
	Derived(int n) :Base(n), A(n), B(n) { cout << "Member of Derived" << endl; }
  //派生类的构造函数初始化列表,先调用基类Base的构造函数,再依次调用间接基类A、B的构造函数
  //由于虚基类Base中没有默认构造函数(允许无参构造),所以从Base类继承的所有派生类的构造函数初始化表中都需要显式调用基类(包括间接基类)的构造函数,完成初始化
private:
	int nd;
};
int main() {
	Derived de(3); de.fun();//不会产生二义性
	return 0;
}

关于虚基类的说明:

一个类在一个类族中既可以被用作虚基类,也可以被用作非虚基类;

如果虚基类中没有默认构造函数(或参数全部为默认参数的),则在派生类中必须显式声明构造函数,并在初始化列表中列出对虚基类构造函数的调用;

在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

三、虚函数

虚函数概念:被virtual关键字修饰的成员函数,即为虚函数,其作用就是实现多态性。

虚函数使用说明:

虚函数只能是类中的成员函数,且不能是静态的;

virtual关键字只能在类体中使用,即便虚函数的实现在类体外定义,也不能带上virtual关键字;

当在派生类中定义了一个与基类虚函数同名的成员函数时,只要该函数的参数个数、类型、顺序以及返回类型与基类中的完全一致,则派生类的这个成员函数无论是否使用virtual关键字,它都将自动成为虚函数;

利用虚函数,可以在基类和派生类中使用相同的函数名定义函数的不同实现,达到「一个接口,多种方式」的目的。当基类指针或引用对虚函数进行访问时,系统将根据运行时指针(或引用)所指向(或引用)的实际对象来确定调用的虚函数版本;

使用虚函数并不一定产生多态性,也不一定使用动态联编。如:在调用中对虚函数使用类名限定,可以强制C++对该函数使用静态联编。

在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,仍将发生多态性。

#include <iostream>
using namespace std;
class Base {
public:
	virtual void print() { cout << "Base-print" << endl; }
};
class Derived :public Base {
public:
	void print() { cout << "Derived-print" << endl; }
};
void display(Base* p, void(Base::* pf)()) {
	(p->*pf)();
}
int main() {
	Derived d; Base b;
	display(&d, &Base::print);	//输出Derived-print
	display(&b, &Base::print);	//输出Base-print
  return 0;
}

使用虚函数,系统要增加一定的空间开销存储虚函数表,但是系统在进行动态联编时的时间开销时很少的,因此,虚函数实现的多态性是高效的。

虚函数实现多态的条件(同时满足)

类之间的继承关系满足赋值兼容规则;

改写了同名的虚函数,但函数形参、返回类型要保持一致;

根据赋值兼容规则使用指针(或引用);

  • 使用基类指针(或引用)访问虚函数;
  • 把指针(或引用)作为函数参数,这个函数不一定是类的成员函数,可以是普通函数,并且可以重载。

虚析构函数

派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但如果用new运算符建立了派生类对象,且定义了一个基类的指针指向这个对象,那么当用delete运算符撤销对象时,系统会只执行基类的析构函数,而不执行派生类的析构函数,因而也无法对派生类对象进行真正的撤销清理工作。

如果希望delete关键字作用于基类指针时,也执行派生类的析构函数,则需要将基类的析构函数声明为虚函数。

如果将基类的析构函数声明为虚函数,则由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。

C++支持虚析构函数,但是不支持虚构造函数,即构造函数不能声明为虚函数!

纯虚函数

许多情况下,不能在基类中为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数,将具体定义留给派生类去做。纯虚函数的定义形式为:virtual 返回类型 函数名(形式参数列表) = 0;

包含有纯虚函数的类称为抽象类,一个抽象类只能作为基类来派生新类,因此又称为抽象基类,抽象类不能定义对象(实体)。

四、多态性

多态的含义:指同一操作作用于不同的对象时产生不同的结果。

  • 重载多态——函数重载、运算符重载
  • 强制多态——也称类型转换
  • C++的基本数据类型之间转换规则:char→short→int→unsigned→long→unsigned→float→double→long double
  • 可以在表达式中使用3中强制类型转换表达式:static_cast<T>(E)或T(E)或(T)E 其中E代表运算表达式(获得一个值),T代表一个类型标识符。强制多态使得类型检查复杂化,尤其在允许重载的情况下,会导致无法消解的二义性。
  • 类型参数化多态——模板(函数模板、类模板)
  • 包含多态——使用虚函数

至少含有一个虚函数的类称为多态类,虚函数使得程序能够以动态联编的方式达到执行结果的多态化。这种多态使用的背景是:派生类继承基类的所有操作,或者说,基类的操作能被用于操作派生类的对象,当基类的操作不能适应派生类时,派生类就需要重载基类的操作;其表现为C++允许用基类的指针接收派生类的地址或使用基类的引用绑定派生类的对象。

静态联编和动态联编

联编:将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数分配内存地址,并且对外部访问也分配正确的内存地址。

静态联编:在编译阶段就将函数实现和函数调用绑定。静态联编在编译阶段就必须了解所有函数的或模块执行所需要的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。C语言中,所有的联编都是静态联编,C++中一般情况下的联编也是静态联编。

动态联编:在程序运行的时候才进行函数实现和函数调用的绑定称之为动态联编(dynamic binding)

#include <iostream>
#define PI 3.14159265
using namespace std;
class Point {
public:
	Point(double x = 0, double y = 0) :x(x), y(y) {}
	double area_static() { return 0; }	//不是虚函数,只会在编译期绑定,形成静态联编
	virtual double area_dynamic() { return 0; } //用虚函数声明,则编译时只做赋值兼容的合法性检查,而不做绑定
private:
	double x, y;
};
class Circle :public Point {
public:
	Circle(double r = 1.0) :r(r) {} //由于基类中的构造函数非必须显式传参,所以系统会自动调用基类带默认参数的构造函数
	Circle(double x, double y, double r=1.0) :Point(x, y), r(r) {} //重载一个可传坐标点、半径值参数的构造函数
	double area_static() { return PI * r * r; } //静态联编
	double area_dynamic() { return PI * r * r; } //动态联编(仍为虚函数),为使可读性更好,可在不缺省virutal关键字
private:
	double r;
};
int main() {
	Point o(2.5, 2.5); Circle c(2.5, 2.5, 1);
	Point* po = &o, * pc = &c, & y_c = c;
//下面五个全部为静态联编,无论指针指向的是基类还是派生类,由于指针类型为基类类型,且调用的不是虚函数,则统一绑定为基类中的函数
	cout << "Point area =" << o.area_static() << endl;	//值为0
	cout << "Circle area=" << c.area_static() << endl;	//值为3.14159
	cout << "the o area from po:" << po->area_static() << endl; //值为0
	cout << "the c area from pc:" << pc->area_static() << endl;	//值为0
	cout << "the c area from cite y_c:" << y_c.area_static() << endl; //值为0
//下面三个为动态联编,有指针(或引用)、虚函数,则所调用的虚函数会在运行时通过vptr指针找到虚函数表	,根据指针指向的实际对象(而非指针类型)来判定调用谁的函数
	cout << "the o area from po:" << po->area_dynamic() << endl; //值为0
	cout << "the c area from pc:" << pc->area_dynamic() << endl; //值为3.14159
	cout << "the c area from cite y_c:" << y_c.area_dynamic() << endl; //值为3.14159
  //强制使用静态联编
  cout << "the c area calculated by Point::area_():" << pc->Point::area_dynamic() << endl; //值为0
	return 0;
}

动态联编与虚函数

  • 当调用虚函数时,先通过vptr指针(编译虚函数时,编译器会为类自动生成一个指向虚函数表的vptr指针)找到虚函数表,然后再找出虚函数的真正地址,再调用它
  • 派生类能继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动成为虚函数。如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数;如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建立地址,函数指针会调用改写后的虚函数。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C++多重继承二义性原理实例解析

    在派生类中对基类成员访问应该是唯一的,但是在多继承时,可能会导致对基类某成员访问出现不一致的情况,这就是C++多继承中的二义性. 有两种继承的情况会产生多义性 一.如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的成员变量进行访问时,可能产生二义性,继承关系如下图所示: #include <iostream> using namespace std; class A{ public: int a; }; class B1 : public A{ public: i

  • 实例代码讲解c++ 继承特性

    --派生类需要自己的构造函数. 派生类中可以根据需要添加额外的数据成员和成员函数,甚至可以给予继承的原成员函数新的定义. 基类指针或引用可指向派生对象,反过来则只能使用强制类型转换. 派生类对象可使用基类的非私有成员. 可使用派生对象初始化基类对象或赋值. 一般不允许将基类对象赋给派生类对象(上面第三条),特殊情况下可以. 已有派生类对象初始化创建的派生类对象. 已有派生类对象给另一个派生类对象赋值. 派生类对象的析构函数被调用后会自动调用基类的析构函数. C++11增加了允许继承构造函数的机制

  • C++类继承 继承后函数的值实现详解

    类的继承会首先寻找基类,若基类未实现,则会寻找派生类的函数 1. class继承,函数不继承 #include <stdio.h> class Base { public: Base(){} ~Base(){} int a; void setA() { a = 1; } }; class A:public Base { public: A(){} ~A(){} void setA() { a = 2; } }; class B:public Base { public: B(){} ~B(){

  • C++中的封装、继承、多态理解

    封装(encapsulation):就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成"类",其中数据和函数都是类的成员.封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,特定的访问权限来使用类的成员.封装可以隐藏实现细节,使得代码模块化. 继承(inheritance):C++通过类派生机制来支持继承.被继承的类型称为基类或超类,新产生的类为派生类或子类.保持已有类的特性而构造新类的过

  • 详解c++ 继承

    面向对象程序设计中最重要的一个概念是继承.继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易.这样做,也达到了重用代码功能和提高执行效率的效果. 当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可.这个已有的类称为基类,新建的类称为派生类. 继承代表了 is a 关系.例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等. 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派

  • C++类继承之子类调用父类的构造函数的实例详解

    C++类继承之子类调用父类的构造函数的实例详解 父类HttpUtil: #pragma once #include <windows.h> #include <string> using namespace std; class HttpUtil { private: LPVOID hInternet; LPVOID hConnect; LPVOID hRequest; protected: wchar_t * mHostName; short mPort; string send

  • C++中的多态与多重继承实现与Java的区别

    多态问题 笔者校招面试时被问到了著名问题「C++ 与 Java 如何实现多态」,然后不幸翻车.过于著名反而没有去准备,只知道跟虚函数表有关.面试之后比较了 C++ 和 Java 多态的实现的异同,一并记录在这里. C++ 多态的虚指针实现 首先讨论 C++. 多态也即子类对父类成员函数进行了重写 (Override) 后,将一个子类指针赋值给父类,再对这个父类指针调用成员函数,会调用子类重写版本的成员函数.简单的例子: class Parent1 { public: virtual void s

  • C++多重继承及多态性原理实例详解

    一.多重继承的二义性问题 举例: #include <iostream> using namespace std; class BaseA { public: void fun() { cout << "A.fun" << endl; } }; class BaseB { public: void fun() { cout << "B.fun" << endl; } void tun() { cout &l

  • Java包装类的缓存机制原理实例详解

    这篇文章主要介绍了Java包装类的缓存机制原理实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java 包装类的缓存机制,是在Java 5中引入的一个有助于节省内存.提高性能的功能,只有在自动装箱时有效 Integer包装类 举个栗子: Integer a = 127; Integer b = 127; System.out.println(a == b); 这段代码输出的结果为true 使用自动装箱将基本类型转为封装类对象这个过程其实

  • Java CAS底层实现原理实例详解

    这篇文章主要介绍了Java CAS底层实现原理实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.CAS(compareAndSwap)的概念 CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制. CAS(V, A, B),V为内存地址.A为预期原值,B为新值.如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值.否则,说明已经被其他线程更新,处理器不做任何操作:无论哪种情

  • C++与Lua交互原理实例详解

    首先,不同语言交互,我们一般把这种编程方式称为混合编程.开发中为什么要多语言混合使用,只用c++不行吗?答案是因为脚本语言语法糖多使用方便.沙盒式安全机制使系统更稳定.简单易学降低开发成本等,那么,只用脚本不行吗?那也是不合理的,因为与系统api的接口.计算密集性模块的性能要求等是脚本语言不擅长的,这一部份仍然需要c/c++来完成.因此,为了综合各自的优势,就出现了混合编程的需要. 那么,在一个程序的生命周期里,具体哪些部份c++写,哪些部份是脚本写?它们的交互接口又在哪里?一般与系统紧密相关的

  • PHP Curl多线程原理实例详解

    给各位介绍一下Curl多线程实例与原理.不对之处请指教相信许多人对php手册中语焉不详的curl_multi一族的函数头疼不已,它们文档少,给的例子 更是简单的让你无从借鉴,我也曾经找了许多网页,都没见一个完整的应用例子.curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle

  • JavaScript解析机制与闭包原理实例详解

    本文实例讲述了JavaScript解析机制与闭包原理.分享给大家供大家参考,具体如下: js解析机制: js代码解析之前会创建一个如下的词法环境对象(仓库):LexicalEnvironment{ } 在扫描js代码时会把: 1.用声明的方式创建的函数的名字; 2.用var定义的变量的名字存到这个词法环境中; 3.同名的时候:函数声明会覆盖变量,下面的函数声明会覆盖上面的同名函数; 4.函数的值为:对函数的一个引用; 变量的值为undefined; 5.如果用函数表达式的方式创建一个函数: va

  • JAVA线程池原理实例详解

    本文实例讲述了JAVA线程池原理.分享给大家供大家参考,具体如下: 线程池的优点 1.线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用. 2.可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃. 线程池的创建 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQu

  • JavaScript模板引擎实现原理实例详解

    本文实例讲述了JavaScript模板引擎实现原理.分享给大家供大家参考,具体如下: 1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> <a href="{{href}}" rel="external nofollow" > {{title}} </a> </h2> <img src

  • Node.js控制台彩色输出的方法与原理实例详解

    前言 我们都知道,在nodejs环境下使用普通的console.log,console.error,console.info输出都是不会有颜色的,如果你不知道,那你现在知道啦

  • SpringMVC工作原理实例详解

    介绍 SpringWeb MVC是Spring Framework中的一部分,当我们需要使用spring框架创建web应用的时候就需要引入springweb mvc.对于程序员来说,我们只需要增加@Controller ,@RequestMapping注解然后,浏览器中的请求就会到达springweb应用.我们只需要在 controller中编写相关逻辑即可.然而,请求是在哪里接收的?@Controller ,@RequestMapping注解做了什么?本文我们来探讨一下. 从一个项目开始 本文

随机推荐