zoukankan      html  css  js  c++  java
  • 3、继承与派生

    1、继承与派生

    1、继承的概念及意义

    1、继承的概念及意义

      一般情况下,谈到继承,都起源与一个基类的定义,基类定义了其所有派生类的公有属性。从本质上将,基类具有同一类集合中的公共属性,派生类继承了这些属性,并且增加了自己特有的属性。作为C++语言的一种重要机制,用继承的方法可以自动为一个类增加了自己的特有属性。作为C++的一种重要机制,用继承的方法可以自动为一个类提供来自另一个类的操作和数据结构,进而使程序设计人员在一个类的基础上很快建立一个新的类,而不必从零开始设计每个类。

    2、单继承和多继承

      从一个基类派生的继承称为单继承,换句话说,派生类只有一个直接基类。与此相对地,从多个基带派生的继承称为多继承和多重继承,也就是说,一个派生类有多个直接基类。例如,类A和类B共同派生了类C,类C继承了类A和类B的所有数据成员和成员函数,如下图所示:

      C++中,基类和派生类是相对而言的,可以把派生类作为基类再次供别的类继承,产生多层次的继承关系。例如,类A派生类B,类B派生类C,则类A是类B的直接基类,类B是类C的直接基类,类C通过类B为媒介也继承了类A的成员,称类A为类C的间接基类。如下图所示。

       在C++中,一个基类可以派生多个派生类,一个派生类也可以有多个基类,而派生类又可以作为新的基类继续一代一代的派生下去,就形成类的继承的复杂层次结构。入下图,给出了一种复杂的类的继承关系示例:

    3、继承的意义

      在传统的程序设计中,每一个应用项目具有不同的目的和要求,程序的结构和具体的编码是不同的,无法使用已有的软件资源,单独的进行程序的开发。即使两种应用具有许多相同或相似的特点,程序设计者可以参考已有资料,但也要从写程序或者对程序进行较大改进,这样,在程序设计过程中必定有许多重复工作,这就造成了人力和时间的浪费。C++提供的类的继承问题,解决了软件重用,大大提高了代码的效率。

    2、派生类的定义

      单继承的派生类定义格式为:

    class 派生类名:[继承方式] 基类名
    {
        [派生类新增数据成员和成员函数]
    };
    

      多重继承的派生类定义格式:

    class 派生类名:[继承方式] 基类名1,[继承方式] 基类名2,...[继承方式] 基类名n
    {
       [派生类新增数据成员和成员函数]
    };

      说明:

    • class是关键字,冒号“:”后面的内容指明派生类是从哪个基类继承而来的,并且指出继承的方式是什么。
    • 继承方式有三种,分别为public、private和protected。如果省略,则默认是private.继承方式决定了派生类的成员对其基类的访问权限。
    • 基类名必须是已经定义的一个类。
    • 派生类新增的成员定义在一对大括号内。
    • 不要忘记在大括号的最后加上分号“;”,以表示该派生类定义结束。

     例如,类A为基类,派生类B公有继承类A,代码如下:

    class A
    {
    public:
    	int x;
    	void funa()
    	{
    		cout << "number of A" << endl;
    	}
    };
    class B :public A
    {
    public:
    	int y;
    	void funb()
    	{
    		cout << "member of B" << endl;
    	}
    };
    

      

    3、派生类成员的访问权限

      派生类是在基础上产生的。派生类的成员包括以下三种:

    • 吸收基类成员:派生类继承了基类的处理构造函数和析构函数以外的全部数据成员和成员函数。
    • 新增成员:增添新的数据成员和成员函数,体现了派生类和基类的不同和个性特征。
    • 对基类成员进行改造:对基类成员的访问控制方式进行改造,或者定义域基类同名的成员,进行同名覆盖。

     1、公有继承

        公有继承是指在派生一个类时继承方式为public的继承方式。在public继承方式下,基类成员在派生类中的访问权限为:基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。即基类的公有成员和保护成员被继承到派生类中仍作为派生类的公有成员和保护成员。派生类的其他成员可以直接访问它们。但需要注意的是,这与派生类的对象和基类成员的访问权限是两个不同概念。派生类的对象是在类外定义的。派生类的对象可以访问基类的公有成员。

      下面是一个示例,用来熟悉派生类采用公有继承方式时对基类成员的访问权限。

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    class A
    {
    public:
    	int a1;
    protected:
    	int a2;
    private:
    	int a3;
    public:
    	A() { a1 = 1; a2 = 2; a3 = 3; }			//类A的构造函数
    	void print1()
    	{
    		cout << "a1=" << a1 << endl << "a2=" << a2 << endl << "a3=" << a3 << endl << endl;
    	}
    };
    class B1 :public A//派生类B1公有继承基类A
    {
    public:
    	int b1;			//B1新增公有成员
    	B1(int cb1) { b1 = cb1; }	//类B1的构造函数
    	void print2()
    	{
    		cout << "a1=" << a1 << endl;//派生类成员函数访问基类公有成员
    		cout << "a2=" << a2 << endl;//派生类成员函数访问基类保护成员
    									//cout<<"a3="<<a3<<endl;	a3是基类私有成员,在派生类不可访问
    		cout << "b1=" << b1 << endl;
    	}
    };
    int main()
    {
    	B1 ob1(100);		//定义派生类对象ob1
    	cout << "ob1.a1=" << ob1.a1 << endl;//派生类的对象ob1可以访问基类公有成员
    	//cout << ob1.a2 << endl;
    	//cout << ob1.a3 << endl; 这个不能正常执行,因为ob1是派生类的对象,不能访问基类保护成员a2和私有成员a3
    	ob1.print1();
    	ob1.print2();
    	return 0;
    }

      程序运行结果为:

    2、私有继承

       私有继承是指在派生一个类时继承方式为private的继承方式。基类中的公有成员和保护成员都已私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。即基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生类的对象无法访问。

      下面给出一个私有继承时基类的成员在派生类中的访问权限。

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    class A
    {
    public:
    	int a1;
    protected:
    	int a2;
    private:
    	int a3;
    public:
    	A() { a1 = 1; a2 = 2; a3 = 3; }			//类A的构造函数
    	void print1()
    	{
    		cout << "a1=" << a1 << endl << "a2=" << a2 << endl << "a3=" << a3 << endl << endl;
    	}
    };
    class B1 :private A//派生类B1公有继承基类A
    {
    public:
    	int b1;			//B1新增公有成员
    	B1(int cb1) { b1 = cb1; }	//类B1的构造函数
    	void print2()
    	{
    		cout << "a1=" << a1 << endl;//派生类成员函数访问基类公有成员
    		cout << "a2=" << a2 << endl;//派生类成员函数访问基类保护成员
    									//cout<<"a3="<<a3<<endl;	a3是基类私有成员,在派生类不可访问
    		cout << "b1=" << b1 << endl;
    	}
    };
    int main()
    {
    	B1 ob1(100);		//定义派生类对象ob1
    	//cout << "ob1.a1=" << ob1.a1 << endl;//私有继承时,派生类的对象ob1不能字节访问基类公有成员
    	//cout << ob1.a2 << endl;//私有继承时,派生类的对象ob1不能字节访问基类保护成员
    	//cout << ob1.a3 << endl; //私有继承时,派生类的对象ob1不能字节访问基类私有成员
    	//ob1.print1();
    	ob1.print2();
    	return 0;
    }
    

      

       该例中无论是派生类的成员函数print2()还是通过派生类的对象ob1,都无法访问基类的私有成员a3.所以如果要输出a3的值,可以在类B1中增加一个成员函数,如:

    void print3(){print1();}
    

      这样,在主函数中可以通过ob1.print3()间接访问a3。

      如果继续派生,那么该基类的所有成员对于新的派生类来说都是不可访问的,即以private方式的继承,只能“传递一代”,基类成员无法在进一步的派生中发挥作用。例如,B1继续派生一个新类B11时,在B11中无法访问B1从A继承的任何成员了。

    3、保护继承

       保护继承是在派生一个类时继承方式为protected的继承方式。保护继承方式的访问控制权限介于公有继承和私有继承之间。保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,基类的private成员和不可访问成员在派生类中不可访问。

      派生类的成员可以直接访问基类的public成员和protected成员,而派生类的对象无法直接访问基类的任何成员,这跟私有继承的效果一样。如果把上例中的private改为protected,则派生类由私有继承改为保护继承,程序运行结果与上例完全一样。但是,如果从派生类再往下派生新的类时,保护继承和私有继承就有区别了。

      例如:

    • 类B以protected方式继承类A,不管类C以何种方式继承类B,那么类C的成员函数可以访问间接基类类A的public或protected成员。
    • 类B以private方式继承类A的所有成员。因为类A的成员已经变成类B的private成员,类B再派生类C,类B的私有成员对于类C的成员自然是不可见的。

       基类成员在不同继承方式时,再派生类中的访问权限见下表:

       下面是将继承方式改为保护继承的一个示例:

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    class A
    {
    public:
    	int a1;
    protected:
    	int a2;
    private:
    	int a3;
    public:
    	A() { a1 = 1; a2 = 2; a3 = 3; }			//类A的构造函数
    	void print1()
    	{
    		cout << "a1=" << a1 << endl << "a2=" << a2 << endl << "a3=" << a3 << endl << endl;
    	}
    };
    class B1 :protected A//派生类B1公有继承基类A
    {
    public:
    	int b1;			//B1新增公有成员
    	B1(int cb1) { b1 = cb1; }	//类B1的构造函数
    	void print2()
    	{
    		cout << "a1=" << a1 << endl;//派生类成员函数访问基类公有成员
    		cout << "a2=" << a2 << endl;//派生类成员函数访问基类保护成员
    									//cout<<"a3="<<a3<<endl;	无法访问类A的private成员
    		cout << "b1=" << b1 << endl;
    	}
    	void print3()
    	{
    		print1();
    	}
    };
    int main()
    {
    	B1 ob1(100);		//定义派生类对象ob1
    	//cout << "ob1.a1=" << ob1.a1 << endl;//私有继承时,派生类的对象ob1不能字节访问基类公有成员
    	//cout << ob1.a2 << endl;//私有继承时,派生类的对象ob1不能字节访问基类保护成员
    	//cout << ob1.a3 << endl; //私有继承时,派生类的对象ob1不能字节访问基类私有成员
    	//ob1.print1();
    	ob1.print2();
    	ob1.print3();
    	return 0;
    }
    

      

      值得注意的是,基类成员在派生类中的访问权限是指在派生内部(即派生类成员函数)对基类成员的访问权限,派生类的对象只能直接访问基类的公有成员,不能访问基类的其他类型成员。而在私有继承和保护继承时,派生类的对象不能直接访问基类的任何成员。

       

    4、派生类的构造函数和析构函数

      派生类把基类的大部分特征都继承下来了,但是有两个例外,这就是基类的构造函数和析构函数。因为基类的构造函数和析构函数。因为基类的构造函数和析构函数负责基类对象的初始化以及清理工作,而派生类新增了一些成员,其对象的初始化以及清理工作必须由派生类自身的构造函数和析构函数来完成。

     1、派生类的构造函数

      在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员初始化,还要考虑基类的数据成员初始化——这里为它的构造函数传递参数,而不是重写构造函数。

    •  最简单的构造函数

       如果一个派生类只有一个基类,也没有虚基类或其他类的内嵌对象时,是最简单的派生类,其构造函数格式为:

    派生类构造函数名(总参数表列):基类构造函数名(参数表列)
    {    派生类中新增数据成员初始化语句}
    

      派生类构造函数,即要初始化派生类新增的成员,同时还要为它的基类的构造函数传递参数,而且是采用参数列表的方式进行数据成员初始化的。

      派生类的构造函数和基类的构造函数的执行顺序是:派生类构造函数先调用基类的构造函数,再执行派生类构造函数本身(派生类构造函数的函数体)。

    下面为一个构造函数使用方法的示例:

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    class A
    {
    public:
    	A(int i)
    	{
    		x = i;
    		cout << "基类A的构造函数被调用" << endl;
    	}
    	void display1()
    	{
    		cout << "基类私有数据成员X" << x << endl;
    	}
    private:
    	int x;
    };
    class B :public A
    {
    public:
    	B(int i, int j) :A(i)
    		//派生类B的构造函数要以参数列表方式为基类A传递参数,用于x的初始化
    	{
    		y = j;
    		cout << "派生类B的构造函数被调用" << endl;
    	}
    	void display2()
    	{
    		display1();
    		cout << "派生类私有成员 y=" << y << endl;
    	}
    private:
    	int y;
    };
    
    void main()
    {
    	B b(1, 2);
    	b.display2();
    }
    

      

    •  派生类中包含内嵌对象

       如果派生类中包含内嵌对象,则派生类除了要给基类数据成员传递参数,还进行内嵌对象的初始化,其构造函数格式为:

    派生类构造函数名(总参数列表):基类构造函数名(基类参数表列),内嵌对象名(内嵌对象参数)
    {    派生类中新增数据成员初始化语句  }
    

      有内嵌对象的派生类构造函数的执行顺序是:首先是基类的构造函数,然后是内嵌对象的构造函数,最吼才执行派生类构造函数本身的函数体。

      下面为一个含有内嵌对象的派生类构造函数的使用。

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    class Student
    {
    public:
    	Student(int n,string nam)
    	{
    		num = n;
    		name = nam;
    	}
    	void display()
    	{
    		cout << "num:" << num << endl << "name:" << name << endl;
    	}
    protected:
    		int num;
    		string name;
    };
    
    class Student1 :public Student
    {
    public:
    	Student1(int n, string nam, int n1, string nam1, int a, string ad) :Student(n, nam), monitor(n1, nam1)
    	{
    		age = a;
    		addr = ad;
    	}
    	void show()
    	{
    		cout << "该学生是" << endl;
    		display();
    		cout << "age:" << age << endl;
    		cout << "address:" << addr << endl << endl;
    	}
    	void show_monitor()
    	{
    		cout << endl << "班长是:" << endl;
    		monitor.display();
    	}
    private:
    	Student monitor;
    	int age;
    	string addr;
    };
    int main()
    {
    	Student1 stud1(10010 ,"x", 10001, "y", 19, "112 beijing road,shanghai	");
    	stud1.show();
    	stud1.show_monitor();
    	return 0;
    }
    

      

      再此例中,派生类Sttudent1的构造函数的任务应该包括以下三个部分。

      •   对基类数据成员初始化;
      •   对子类对象数据成员初始化;
      •   对派生类数据成员初始化。
    • 多层派生的派生类的构造函数

       当类A作为基类派生出类B,类B又作为基类派生处类C时,就形成了所谓的多层派生类。在写类C的构造函数初始化列表是,只要列出类B及其构造函数所需要的参数,而不必列出类A及其参数,也就是说,每一个派生类仅负责给它的直接基类准备参数,而不必关心它的间接基类的参数如何赋值(虚基类情况除外)。

      总之,在包含派生类的程序中,各个类的构造函数要注意以下几点:

      •   当不需要对派生类新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为空。
      •   如果在基类中没有定义构造函数,或定义了没又参数的构造函数,那么在派生类构造函数初始化列表中就不用列出基类名及其参数。
      •   如果在基类和内嵌对象的声明中都没定义带有参数的构造函数,而且不需要对派生类自己的数据成员初始化,则可不必显式的定义派生类构造函数。
      •   如果在基类或内嵌对象类型的声明中定义了带参数的构造函数,就必须显式的定义派生类构造函数。
      •   如果基类中即定义了无参的构造函数,又定义了有参的构造函数,在定义派生类构造函数是,可以不想基类构造函数传递参数。

    2、派生类的析构函数

       析构函数的作用是在对象撤销之前,进行必要的清理工作,当对象被删除时,系统会自动调用析构函数,析构函数比构造函数简单,没有类型,也没有参数。

      在派生时,派生类是不能继承基类的析构函数的,也需要通过派生类的析构函数区调用基类的析构函数,在派生类中可以根据需要定义自己的析构函数,用来对派生类中所增加的成员进行清理工作。基类的清理工作仍然可以根据需要定义自己的析构函数,用来对派生类中所增加的成员进行清理。基类的清理工作仍然由基类的析构函数负责。在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理。

      调用的顺序与构造函数正好相反:先执行派生类自己的析构函数,对派生类所增加的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进行清理。

      与构造函数类似,析构函数也不能被继承,需要在派生类中自行定义。派生类的析构函数的特点如下。

    • 派生类的析构函数的定义方法与一般(无继承关系时)类的析构函数相同。
    • 不需要显式的调用基类的析构函数,系统会自动隐式调用。
    • 析构函数的调用次序与构造函数正好相反。

    5、多继承

     1、定义

      多继承是指派生类的直接父类多于一个,多继承的定义格式如下:

    class 派生类名:继承方式1  基类名1,继承方式2 基类名2
    {
    private:
    	新增的私有数据成员和函数成员的描述;
    public:
    	新增的公有数据成员和函数成员的描述;
    protected:
    	新增的保护数据成员和函数成员的描述;
    }
    

      其中,各个基类的继承方式可以相同,也可以不同,多个基类间用逗号分隔。

      在多个继承时,派生类的构造函数格式如下:

    <派生类名>(<总参数表>) : <基类名1>(<参数表1>), <基类名2>(<参数表2>),..., <基类名>(<参数表>)
    {
    	<派生类构造函数体>
    }
    

      其中,<总参数表>中多个参数包含其后的各个分参数表。

      如果派生类中包含子对象,则构造函数格式为:

    <派生类名>(<总参数表>) : <基类名1>(<参数表1>), <基类名2>(<参数表2>),..., <基类名n>(<参数表n>)(<参数表n>), <子对
    	象名1>(<参数表1>), <子对象名2>(<参数表2>), ..., <子对象名n>(<参数表n>)
    {
    	<派生类构造函数体>
    }
    

      下面为一个示例用来熟悉多重继承构造函数和析构函数的执行顺序。

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    
    class B1
    {
    public:
    	B1(int i)
    	{
    		cout << "B1的构造函数被调用" << endl;
    		x = i;
    		cout << "x=" << x << endl;
    	}
    	~B1()
    	{
    		cout << "B1的析构函数被调用" << endl;
    	}
    private:
    	int x;
    };
    class B2
    {
    public:
    	B2(int j)
    	{
    		cout << "B2构造函数被调用" << endl;
    			y = j;
    		cout << "y=" << y << endl;
    	}
    	~B2()
    	{
    		cout << "B2的析构函数被调用" << endl;
    	}
    private:
    	int y;
    };
    
    class B3
    {
    public:
    	B3( )
    	{
    		cout << "B3构造函数被调用" << endl;
    	}
    	~B3()
    	{
    		cout << "B3的析构函数被调用"<<endl;
    	}
    private:
    	int y;
    };
    
    class C :public B1, public B2, public B3
    {
    public:
    	C(int a, int b, int c, int d) :B1(a), ob1(b), ob2(c), B2(d)
    	{}/*构造函数列表中给出所有参数值,B3中没有要数据成员,其构造函数不带参数,可以不写*/
    private:
    	B1 ob1;
    	B2 ob2;
    	B3 ob3;
    };
    int main()
    {
    	C obj(1, 2, 3, 4);
    	return 0;
    }
    

      派生类C进行实例化时,先调用其基类B1,B2,B3的构造函数,然后再调用C的对象成员obj1,obj2,obj3

    2、多继承和多层次继承的二义性问题

      在多继承情况中,如果在派生类中使用的某个成员名在多个基类中出现,而在派生类中没有重新定义,这时候使用该成员名就会产生二义性问题。例如,类B1和B2都有一个公有数据成员int a,类C公有继承了类B1、B2,在类C中没有重新定义成员a,则在类C中访问a时,如果不加以说明,系统将无法确定要访问的是B1的成员还是B2的成员。

      在多层次继承时,如果某个派生类的部分或全部成员的基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此,派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域运算符来唯一标识,而且必须用直接基类进行限定。

      例如,基类A有一公有成员int a,类A公有派生了类B1和B2,类B1和B2共同派生了类C,类B1和B2是类C的直接基类,类A是C的间接基类,则类C会从B1和B2继承两份成员a。

    下面是一个示例帮助理解多重继承重点的二义性:

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    
    class A
    {
    public:
        int a;
        void fun()
        {
            cout << "number of A is " << a << endl;
        }
    };
    
    class B1 :public A
    {
    public:
        int b1;
    };
    class B2 :public A
    {
    public :
        int b2;
    };
    
    class C :public B1, public B2
    {
    public:
        int c;
        void fun()
        {
            cout << "member of C" << endl;
        }
    };
    int main()
    {
        C objc1;
        objc1.fun();
        objc1.B1::a = 2;
        objc1.B1::fun();
        objc1.B2::a = 3;
        objc1.B2::fun();
        return 0;
    }

      在该例中,类C中定义了与类A同名的函数fun(),如果不加以声明,objc1.fun()调用的就是类C的成员函数,而不是继承的类A的函数。


      派生类C的对象objc1在内存中同时拥有类A的成员a及成员函数fun()的两份副本。分别是从类B1、B2继承的,而B1、B2中的成员a及fun()是从它们的共同基类A继承的。派生类C的对象虽然可以通过使用基类名和作用域运算符避免二义性,但是在C中,只需要一份间接基类A的成员及fun()就足够了,同一成员的多分副本增加了内存的开销。为避免这种情况,可以采用虚基类。

    3、虚基类

      在声明派生类时,如果在继承方式前面加上关键字virtual,该基类就称为虚基类。派生类的继承也称为虚继承。

      声明虚基类的一般形式为:

    class 派生类名:virtual 继承方式  基类名
    {...};
    

      例如,在上例中,如果由类A派生类B1和B2时采用虚继承,则语句应该改为:

    class B1:virtual public A{...}
    class B2:virtual public A{...}
    

      类C继承B1、B2不必再采用虚继承,即:class  C:public B1,public B2{...};这样,就能保证类C只继承类A一次,避免了二义性。上例中的派生对象objc1对成员的使用可以直接携程objc1.a,不必再写成objc1.B1::a;、objc1.B2::a;但要注意的是,因为类C中由同名函数的存在,对虚基类的函数fun()的调用格式还要保持原来的形式,即:objc1.B1::fun();、objc1.B2::fun();

      在多层次继承的程序中,如果包含虚基类,对虚基类的初始化不是在其字节派生类中进行,而是在间接派生类中进行的。虚基类的初始化与一般多继承的初始化语法格式是一样的,但派生类构造函数调用次序是不同的,首先调用的是虚基类的构造函数,然后是非虚基类的构造函数。最后是派生类的构造函数。在上例中,如果采用虚基类后,在类C的对象objc1构建时,构造函数调用次序为A(),B1(),B2(),C(),析构函数调用次序与构造函数的次序相反。

      如果在同一层次中包含多个虚基类,则按照它们的说明次序调用构造次序。例如:

    class C:pulic B1,public B2,virtual public B3,virtual public B4{...};
    

      其构造函数执行顺序为B3(),B4(),B1(),B2(),C().

    例:类Person作为类Teacher和类Student 的公共基类,类Person包含人员的一些基本数据,如姓名、性别、年龄,在类Teacher中增加数据成员职称,在类Student中增加数据成员成绩,由类Teacher和类Student共同派生类Graduate,分析下列程序的执行结果,理解基类的使用方法。

      

    #include "stdafx.h"
    #include<iostream>
    #include<string>
    using namespace std;
    
    class Person
    {
    public:
    	Person(string nam, string s, int a)
    	{
    		name = nam;
    		sex = s;
    		age = a;
    	}
    protected:
    	string name;
    	string sex;
    	int age;
    };
    
    class Teacher :virtual public Person
    {
    public:
    	Teacher(string nam, string s, int a, string t) :Person(nam, s, a)
    	{
    		title = t;
    	}
    protected:
    	string title;
    };
    
    class Student :virtual public Person
    {
    public:
    	Student(string nam, string s, int a, float sco) :Person(nam, s, a), score(sco) {}	
    protected:
    	float score;
    };
    
    class Graduate :public Teacher, public Student
    {
    public:
    	Graduate(string nam, string s, int a, string t, float sco, float w) :Person(nam, s, a),
    		Teacher(nam, s, a, t), Student(nam, s, a, sco), wage(w) {}
    	void show()
    	{
    		cout << "name:" << name << endl;
    		cout << "age" << age << endl;
    		cout << "sex:" << sex << endl;
    		cout << "score" << score << endl;
    		cout << "title" << title << endl;
    		cout << "wages" << wage << endl;
    	}
    private:
    	float wage;
    };
    
    
    int main()
    {
    	Graduate grad1("ax", "male", 23, "assistant", 90, 22200.5);
    	grad1.show();
    	return 0;
    }

      在本例中,即包含多重继承也包含多继承,作为虚基类的类Person,其初始化是在其间接派生类Graduate中进行的。Person的两个直接派生类构造函数为:

    Teacher(string nam, string s, int a, string t) :Person(nam, s, a)
    	{
    		title = t;
    	}
    
    Student(string nam, string s, int a, float sco) :Person(nam, s, a), score(sco) {}	
    

      Person的间接派生类Graduate的构造函数为:

    Graduate(string nam, string s, int a, string t, float sco, float w) :Person(nam, s, a),
    		Teacher(nam, s, a, t), Student(nam, s, a, sco), wage(w) {}
    

      初始化参数表中既包含其直接基类Teacher和Student的初始化项,也包括间接基类Person的初始化项。程序执行时,先对虚基类Person初始化,然后对类Teacher和类Student进行初始化,虽然这两个类是虚基类Person的直接派生类,它们的参数表中都包含公共基类Person的参数项,但不会再对类Person进行初始化,这样就避免虚基类的重复初始化。

      总之,在类的多重继承和多继承时,为解决二义性问题,可以采用以下三种方法:

    • 通过类名和作用域运算符(::)明确指出访问的是哪一个基类中的成员。
    • 在派生类中定义同名成员,进行同名覆盖。
    • 采用虚基类。

    2、多态性

    1、多态的概念

      多态性和封装、继承并称为面向对象编程领域的核心概念。封装可以使得代码模块化,继承可以扩展已存在的代码,是为了代码的重用。而多态的目的则是为了接口重用。C++的多态性简单的概括为用同一个接口实现多种功能过呢。利用多态性,用户发送一般形式的消息后,接收到消息的对象可做出不同的动作,既有不同的反应。C++的多态性分为静态多态性和动态多态性(即静态绑定和动态绑定两种现象)。静态多态发生在编译期,可通过一般的函数重载和运输符重载来实现。函数重载和运输符重载来实现。函数重载允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。

      动态多态性发生在程序运行期,是动态绑定。动态多态则是通过继承、虚函数、指针来实现的。程序在运行时才决定调用的函数,通过父类指针调用子类的函数,可以让弗雷指针有多种形态。

       

    2、运算符重载

      在C++中,可以同故宫重新定义运算符,使同意运算符实现多种功能,达到重载的目的。例如,符号<<和符号>>本来在C++中被定义为左、右位移运算符的,由于iostream头文件中对它们进行了重载,所以它们能用作标准数据类型数据的输入和输出运算符。因此,在使用它们的程序中必须包含#include <iostream>.

      在VC++中,程序对用某个运算符进行的相关运算,是通过函数调用实现的,例如,对于语句c=a+b,VC编译器将其解释为c=+(a,b).即“+”为函数名,a、b为两个实参。C++中预定义的运算符的操作对象只能是基本数据类型。但实际上,对于许多用户自定义类型(例如类),也需要类似的运算操作,如将两个分数相加,将时间相加或则将两个复数相加。例如:算术运算符“+、-、*、/”用于基本数据运算,由于复数不是基本数据类型,无法直接进行运算,在C++中,允许重新定义这些运算符的功能,时期能够进行复数的运算。

    1、运算符重载

       在VC++中,运算符重载是通过创建运算符函数实现的,运算符函数定义了重载的运算符要进行的操作,运算符函数定义的一般格式如下:

    <返回函数类型>operator<运算符符号>(<参数表>)
    {
        <函数体>
    }
    

      运算符函数的定义与其他函数的定义类似,主要区别是运算符函数的函数名是由关键字operator和其后要重载的运算符符号构成的。

      进行运算符函数重载时,要注意以下几点:

    • 出来以下5种运算符外,C++中的其他运算符都可以进行重载,这五种运算符为:类属关系运算符“.",成员指针运算符”.*”,作用域运算符"::",sizeof运算符和条件运算符“?:",。另外,重载的运算符只能说C++语言中已有的运算符,不能创建新的运算符。
    • 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算操作符的个数及语法结构。
    • 运算符重载不能改变该运算符用于内部类型对象的含义,他只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。
    • 运算符重载是针对新类型数据的实际需要对原有运算符进行的适当改造,重载功能应当与原有功能相类似,避免没有目的的使用重载运算符。
    • 运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载选择的原则,重载运算符的函数不能有默认的参数。
    • 用户自定义类的运算符一般都必须重载后方可使用,但有两个例外,运算符”==“和”&”不必用户重载。

    2、运算符重载的形式

      运算符重载实质上就是函数重载,既可以将运算符作为普通函数,也可以作为类的成员函数或友元函数。

      例:复数类complex包含两个私有成员,real表示是不,imag表示虚部,编程实现两个复数的加法操作,要求将运算符重载为普通函数。

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class Complex
    {
    public:
    	double real;
    	double imag;
    	Complex(double real = 0, double imag = 0)
    	{
    		this->real = real;
    		this->imag = imag;
    	}
    	void display()
    	{
    		cout << "(" << real << "," << imag << "i)" << endl;
    	}
    };
    Complex operator+(Complex com1, Complex com2)//运算符重载函数
    {
    	Complex c;
    	c.real = com1.real + com2.real;
    	c.imag = com1.imag + com2.imag;
    	return c;
    }
    int main()
    {
    	Complex c1(1, 2), c2(3, -4), sum;
    	sum = c1 + c2;
    	cout << "c1=";
    	c1.display();
    	cout << "c2=";
    	c2.display();
    	cout << "c3=c1+c2=";
    	sum.display();
    	return 0;
    }
    

      运行结果如下:

      在这个类的定义中,real,imag被定义为公有成员,所以在类外的运算符重载函数中Complex operator+(Complex com1, Complex com2)可以正常访问,但一般类的数据成员都为私有或保护成员,则在类外无法直接访问,如果要访问类的私有和保护成员时,必须设置类的公有函数,来进行数据的存取,而调用这些函数时会降低性能,所以,运算符函数重载时一般不推荐使用普通函数,运算符重载主要是作为成员函数或友元函数两种形式。

    • 重载为成员函数

       重载为类的成员函数一般格式为:

    <函数类型> operator <运算符>(<参数表>)
    {
        <function>
    }
    

      调用成员函数运算符的格式如下:

    <对象名>.operator <运算符>(<参数>)
    等价于
    <对象名><运算符><参数>
    

      例子:改写上例中的程序,要求将运算符函数重载为类的成员函数。

      如果在上例中 ,将类Complex的数据成员改为私有的,再增加一条声明语句:Complex operator+(Complex com1, Complex com2);,将运算符函数声明为类的成员函数,如果直接编译程序,则会出错,因为类的成员函数有个隐含参数this指针,所以要将运算符参数减少一个,程序如下:

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class Complex	//复数类
    {
    private:
    	double real;//
    	double imag;
    public:
    	Complex(double real = 0, double imag = 0)
    	{
    		this->real = real;
    		this->imag = imag;
    	}
    	Complex operator +(Complex com2);//声明运算符"+”函数
    	void display()
    	{
    		cout << "(" << real << "," << imag << "i)" << endl;
    	}
    };
    Complex Complex::operator + ( Complex com2)  //运算符重载函数
    {
    	Complex c;
    	c.real = this->real + com2.real;//等价于 c.real=real+com2.real;
    	c.imag = this->imag + com2.imag;
    	return c;
    }
    int main()
    {
    	Complex c1(1, 2), c2(3, -4), c3;
    	c3 = c1 + c2;
    	cout << "c1=";
    	c1.display();
    	cout << "c2=";
    	c2.display();
    	cout << "c3=c1+c2=";
    	c3.display();
    	return 0;
    }

      一般的,以对象为函数参数时,通常将参数改为对象引用。该例中,参数值是固定的,在函数体中没有进行修改,所以,函数参数可以用常对象引用,即运算符函数可以改为:

    Complex Complex::operator + (const Complex &com2)  //运算符重载函数
    {
    	Complex c;
    	c.real = this->real + com2.real;//等价于 c.real=real+com2.real;
    	c.imag = this->imag + com2.imag;
    	return c;
    }
    

      C++编译系统将程序中的表达式c1+c2解释为:c1.operator+(c2),即以C2为实参调用对象C1的运算符重载函数operatro +(Complex &com2),实际上,运算符重载函数有两个参数,由于重载函数是Complex类中的成员函数,有一个参数是隐含的,运算符函数是用this指针隐式的访问了类对象的成员,如this->real + com2.real;代表c1.real+c2.real.

      在本例中,当运算符+重载为类的成员函数时,函数的参数个数比原来的操作数少了一个,Complex operator + (Complex &2),但还是可以正常进行两个复数的加运算,这是因为成员函数用this指针隐式的访问了类的一个对象,它充当了运算符函数最左边的操作数,c1.operator+(c2)相当于.operator+(&c1,c2)。因此,当运算符函数重载为类的成员函数时,最左边的操作数必须是运算符类的一个类对象(或者是对该类对象的引用),也就是说第一个参数和运算符重载函数的类型相同。

      一般运算符重载为类的成员函数是,需注意以下几点。

      •    双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。
      •   前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。
      •   后置单目运算符重载为类的成员函数时,函数腰带一个整型形参。
    •  重载为友元函数

       运算符重载为类的友元函数的一般格式为:

    friend <函数类型> operator<运算符>(<参数表>)
    {
    	<function>
    }
    

      当运算符重载为类的友元函数时,由于没有隐含的this指针,因此操作数的个数没有变化,所有的操作数都必须通过函数的形参进行传递,函数的参数与操作数自左向右意义对应。

      调用友元函数运算符的格式如下:

    operator<运算符>(<参数表>)
    等价于
    <参数1>运算符<参数2>

      例子:将前面的例子中的运算符重载为类的友元函数。

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class Complex	//复数类
    {
    private:
    	double real;//
    	double imag;
    public:
    	Complex(double real = 0, double imag = 0)
    	{
    		this->real = real;
    		this->imag = imag;
    	}
    	friend Complex operator + (const Complex &com1, const Complex &com2);//声明友元函数
    	void display();
    };
    void Complex::display()
    {
    	cout << "(" << real << "," << imag << "i)" << endl;
    }
    Complex operator + (const Complex &com1,const Complex &com2)//运算符函数定义
    {
    	Complex c;
    	c.real = com1.real + com2.real;
    	c.imag = com1.imag + com2.imag;
    	return c;
    }
    int main()
    {
    	Complex c1(1, 2), c2(3, -4), c3;
    	c3 = c1 + c2;
    	cout << "c1=";
    	c1.display();
    	cout << "c2=";
    	c2.display();
    	cout << "c3=c1+c2=";
    	c3.display();
    	return 0;
    }
    

      

    • 重载为成员函数和友元函数的区别

       一般情况下,将运算符重载为类的成员函数和友元函数都是可以的。成员函数形式比较简单,就是在类里面定义一个与操作符相关的函数。友元函数因为没有this指针,所以参数会多一个,但成员函数和友元函数各自也具有一些特点,通常需注意:

      • 一般情况下,单目运算最好重载为类的成员函数,双目运算符则最好重载为类的友元函数。
      • =、()、[]、->4个双目运算符和类型转换函数指针重载为类的成员函数。将这些操作符定义为非成员函数将在编译时标记为错误。
      • <<、>>操作符一般重载为类的友元函数。
      • 当一个运算符的操作需要修改对象时,重载为成员函数。
      • 若运算符函数所需参数(尤其是第一个参数)希望有隐式转换时,则只能选用友元函数。
      • 当运算符函数是一个成员函数时,第一个参数必须是运算符的一个类对象(或者是对该类对象的引用)。如果第一个参数是另一个类的对象,或者是一个内部类型的对象,则该运算符函数必须作为友元函数。

     

    3、其他运算符的重载

    •  赋值运算符重载

       赋值运算符的重载只能是成员函数。这个函数的返回类型是左操作数的引用,也就是 *this,并且这个函数的参数是一个同类型的常引用变量。例如:

    A& operator = (const A&);
    

      因为赋值运算符必须是类的成员函数,所以this绑定到左操作数的指针。因此,赋值操作符只接受一个形参,且该形参是同一类型的对象,右操作数一般作为const引用,与拷贝构造函数相同。

      下面通过一个示例来熟悉赋值运算符的重载。

      

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class A
    {
    public:
    	A(int i = 0, int j = 0)
    	{
    		a1 = i;
    		a2 = j;
    	}
    	A & operator =(A &p);
    	void print()
    	{
    		cout << "a1=" << a1 << endl;
    		cout << "a2=" << a2 << endl;
    	}
    private:
    	int a1, a2;
    };
    A &A::operator=(A &p)//运算符函数定义
    {
    	a1 = p.a1;
    	a2 = p.a2;//等价于this->a1=p.a1;
    	return *this;
    }
    int main()
    {
    	A obj1(1, 2), obj2;
    	obj2 = obj1;//调用赋值运算符重载函数给对象obj2赋值,即obj.operator=(obj1)
    	obj2.print();
    	int x;
    	x = 5;
    	cout << "x=" << x << endl;//这里的等于是一般赋值运算符
    	return 0;
    }
    

      

     

      另外,算术赋值运算符(+=、-=、*=、/=、%=、|=、^=、&=、>>=、<<=)重载时既可以是成员函数,也可以是友元函数。重载为成员时,函数参数和返回只类型与复制运算符重载相同,例如A& operator + =(const A&)、A& operator - =(const A&)和A& operator * =(const A&)等。

    • 自增自减运算符重载

       自增(++)和自减(- -)运算符根据位置的不同有四种情况,如果为整型变量i,可以有i++、++i、i- -、- -四种情况,如果想用于类类型,也有类似的四种情况,都可以重载,由于自增自减函数修改了操作数,所以最好是成员函数重载的方式。例如类名为A,前置运算符重载格式为:A& operator ++(); A& operator ++();因为前置时的对象是运算符的左值,函数返回值应该是一个引用而不是对象。

      为了与前置运算符相区别,C++规定了后置形式有一个int类型参数,使用A operator ++(int)或A operator - -(int)来重载后置预算符,参数int没有实际意义,调用时,被复制为0。

       下面是一个示例用来熟悉它们的重载。

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class PMOne
    {
    private:
    	int x;
    public:
    	PMOne(int a = 0) { x = a; }
    	PMOne operator ++();
    	PMOne operator --();
    	PMOne operator ++(int);
    	PMOne operator --(int);
    	int getval()
    	{
    		return this->x;
    	}
    };
    PMOne PMOne::operator++()
    {
    	this->x += 1;
    	cout << this->x << endl;
    	return *this;
    }
    
    PMOne PMOne::operator++(int)
    {
    	PMOne tmp(*this);
    	this->x += 1;
    	cout << this->x << endl;
    	return tmp;
    }
    
    
    PMOne PMOne::operator--()
    {
    	this->x -= 1;
    	cout << this->x << endl;
    	return *this;
    }
    PMOne PMOne::operator--(int)
    {
    	PMOne tmp(*this);
    	this->x -= 1;
    	cout << this->x << endl;
    	return tmp;
    }
    int main()
    {
    	PMOne a(5);
    	PMOne b1, b2, b3, b4;
    	b1 = ++a;
    	b2 = a++;
    	b3 = --a;
    	b4 = a--;
    	cout << "b1.x=" << b1.getval() << endl;
    	cout << "b2.x=" << b1.getval() << endl;
    	cout << "b3.x=" << b1.getval() << endl;
    	cout << "b4.x=" << b1.getval() << endl;
    	return 0;
    }
    

      

    • 插入符和提取符重载

      插入符和提取符<<和>>只能重载为友元函数,一般形式为

    friend inline ostream &operator <<(ostream&, 自定义类名&);//输出流
    friend inline ostream &operator >>(ostream&, 自定义类名&);//输入流
    

      例如:

    ostream &operator<<(ostream& output, Complex &c)
    {
    	output << "<" << c.real << "+" << c.imag << "i)" << endl;
    	return output;
    }
    

      如果有如下输出语句:

    cout<<c3<<c2;
    

      先处理cout<<c3,即(cout<<c3)<<c2,而cout<<c3其实是operator <<(cout,c3),返回的是流提取对象cout,所以cout再和后面的c2结合,输出c2的内容。可见为什么C++规定“流提取运算符重载函数的第一个对象和函数的类型必须是ostream的引用了”,就是为了返回cout以便连续输出。

    • 关系运算符重载

       关系运算符有==、!=、>、<、<=、>=。其返回值为布尔型数据,可以重载为成员函数,例如

    bool operator ==(const A&);
    bool operator !=(const A&);
    

      也可以重载为友元函数,例如

    bool operator ==(const A&,const A&);
    bool operator !=(const A&,const A&);
    

      示例:定义一个日期date,类date包含三个私有成员mm、dd、yy,分别表示月、日、年,从键盘输入两个日期,编写程序,运用重载比较两个日期的大小。

     

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class date
    {
    public:
    	friend bool operator>(const date&, const date&);
    	friend bool operator<(const date&, const date&);
    	friend bool operator==(const date&, const date&);
    	friend ostream & operator<<(ostream &, date&);//插入符和提取符只能重载为友元函数
    	friend istream& operator>>(istream&, date&);
    private:
    	int mm, dd, yy;
    };
    bool operator>(const date& d1, const date& d2)
    {
    	if (d1.yy == d2.yy)
    	{
    		if (d1.mm == d2.mm)
    		{
    			if (d1.dd > d2.dd)
    				return true;
    			else
    				return false;
    		}
    		else
    		{
    			if (d1.mm > d2.mm)
    				return true;
    			else
    				return false;
    		}
    	}
    	else
    	{
    		if (d1.yy > d2.yy)
    			return true;
    		else
    			return false;
    	}
    }
    bool operator<(const date& d1, const date& d2)
    {
    	if (d1.yy == d2.yy)
    	{
    		if (d1.mm == d2.mm)
    		{
    			return d1.mm < d2.mm;
    		}
    		return d1.mm < d2.mm;
    	}
    	return d1.yy < d2.yy;
    }
    bool operator ==(const date&d1, const date& d2)
    {
    	return(d1.yy == d2.yy&&d1.mm == d2.mm&&d1.dd == d2.dd);
    }
    ostream & operator<<(ostream&output, date& d)
    {
    	output << d.mm << "/" << d.dd << "/" << d.yy;
    	return output;
    }
    istream & operator>>(istream& input, date& d)
    {
    	input >> d.mm >> d.dd >> d.yy;
    	return input;
    }
    int main()
    {
    	date d1, d2;
    	cout << "输入日期d1 m-d-y";
    	cin >> d1;
    	cout << "输入日期d2 m-d-y";
    	cin >> d2;
    	if (d1 > d2)
    		cout << d1 << ">" << d2 << endl;
    	if (d1 < d2)
    		cout << d1 << "<" << d2 << endl;
    	if (d1 == d2)
    		cout << d1 << "==" << d2 << endl;
    	return 0;
    }
    

      

     

    3、虚函数与纯虚函数

      1、虚函数

       在类的继承关系中,为类实现多态特性,需要在基类中定义虚函数,它允许函数调用与函数体之间的联系在运行时才建立,即在运行时才决定如何动作。虚函数声明的格式为:

    virtual 返回类型 函数名(形参表)
    {
        函数体
    }
    

      虚函数允许子类重新定义成员函数,但要实现多态还有个关键之处就是要用指向基类的指针或引用来操作对象,即将派生类对象赋值给基类指针。

      这是因为系统可以进行不同数据类型的自动转换,例如,如果把整型数据赋值给双精度类型的变量,在赋值之前,系统先把整型数据转换为双精度,再把它赋值给双精度类型的变量。这种不同类型数据之间的自动转换和赋值,称为赋值兼容。同样的,在基类和派生类之间也存在着赋值兼容关系,它是指可以将公有派生类对象赋值给基类对象,但要注意的是,只有公有集成的派生类才可以和基类赋值兼容。因为在公有集成时,派生类保留了基类的所有成员(构造函数和析构函数除外),派生类按照原样保留了基类的公有和保护成员,在派生类外可以调用基类的公有函数来访问基类的私有成员。因此派生类可以实现基类的所有功能。基类和派生类的赋值兼容包括以下几种情况。

    •  派生类对象直接向基类赋值,例如A为基类,B为A的公有派生类,a为基类对象,b为派生类对象,b可以给a赋值,即a=b,赋值后,基类数据成员和派生类中数据成员的值相同。
    • 派生类对象可以初始化基类对象引用,即A &ra=b.
    • 派生类对象的地址可以赋给基类对象的指针,即A *p; p=&b;.
    • 函数形参是基类对象或基类对象的引用,在调用函数时,可以用派生类的对象作为实参。例如,对函数int fun(A &)来说,调用时时参数可以是b,即fun(b).

       下面是一个示例来理解赋值兼容和虚函数的概念。

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class A
    {
    public:
    	void print1()
    	{
    		cout << "this is A-1" << endl;
    	}
    	virtual void print2()
    	{
    		cout << "this is A-2" << endl;
    	}
    };
    class B :public A
    {
    public:
    	void print1()
    	{
    		cout << "This is B-1" << endl;
    	}
    	virtual void print2()
    	{
    		cout << "this is B-2" << endl;
    	}
    };
    int main()
    {
    	A a;
    	B b;
    	A* p1 = &a;	//指向基类的指针P1被赋值为基类对象a的地址。
    	B* p2 = &b;//指向基类的指针p2被赋值为派生类对象b的地址,赋值兼容
    	p1->print1();
    	p2->print1();//必须用基类指针
    	p1->print2();
    	p2->print2();
    }
    

      

      从程序运行结果可以看出,p1、p2都是指向基类A的指针,p1指向基类对象a,p2指向派生类对象b,print1()不是虚函数,p1->print1()、p2->print1()都是调用的是基类的成员函数,其实,作为指向派生类对象的指针p2,p2->print1()调用的是从基类继承的成员函数,而不是派生类中定义的同名成员函数。print2()被声明为虚函数,p1->print2()调用的是基类的成员函数,p1->print2()调用的是派生类的成员函数。这就是虚函数的实现的多态性。在执行过程中,该函数可以不断改变它所指向的对象,调用不同版本的成员函数,而且这些动作都是在运行时动态实现的。一般的,经常定义一个动态指针,将主函数部分改为:

    int main()
    {
        ...
      A * p2=new B;
     p2->print1();
     p2->print2();
     delete p2;
    }
    

      使用虚函数需要注意以下几点:

    • 在基类中的某成员函数被声明为虚函数后,在之后的派生类中可以重新来定义它。但定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须和基类中的原型完全相同。
    • 必须通过基类指针指向派生类,才能通过虚函数实现运行时的多态性。
    • 虚函数具有继承性,只有在基类中显式声明了虚函数,在派生类中函数名前的virtual可以略去,因为系统会更具其是否和基类中虚函数原型完全相同来判断是不是虚函数。一个虚函数无论被公有继承多少次,他仍然是虚函数。
    • 使用虚函数,派生类必须是基类公有派生的。
    • 虚函数必须是所在类的成员函数,而不能是友元函数,也不能是静态成员函数。因为虚函数调用要靠特定的对象类决定激活哪一个函数。
    • 构造函数不能是虚函数,但析构函数可以是虚函数。

     示例:利用虚函数进行矩阵、圆形和三角形面积的计算,分析下列程序,了解虚函数的使用。

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    class Graph
    {
    protected:
    	double x;
    	double y;
    public:
    	Graph(double x, double y);
    	virtual void ShowArea();
    };
    Graph::Graph(double x, double y)
    {
    	this->x = x;
    	this->y = y;
    }
    void Graph::ShowArea()
    {
    	cout << "the area is" << endl;
    }
    class Rectangle :public Graph
    {
    public:
    	Rectangle(double x, double y) :Graph(x, y) {};
    	void ShowArea();
    };
    
    void Rectangle::ShowArea()
    {
    	cout << "the area of Rectangle is" << x * y << endl;
    }
    class Triangle :public Graph
    {
    public:
    	Triangle(double d, double h) :Graph(d, h) {};
    	void ShowArea();
    };
    
    void Triangle::ShowArea()
    {
    	cout << "the area of Triangle is" << x * y*0.5 << endl;
    }
    
    class Circle :public Graph
    {
    public:
    	Circle(double r) :Graph(r, r) {};
    	void ShowArea();
    };
    
    void Circle::ShowArea()
    {
    	cout << "the area of Circle is" << x* y*3.14 << endl;
    }
    int main()
    {
    	Graph *gp;
    	Rectangle rectangle(8, 5);
    	gp = &rectangle;
    	gp->ShowArea();
    	Triangle tirangle(6,4);
    	gp = &tirangle;
    	gp->ShowArea();
    	Circle circle(2);
    	gp = &circle;
    	gp->ShowArea();
    	return 0;
    }

     2、纯虚函数与抽象类

      如果在基类中定义虚函数时,只有函数名,没有函数体,则该虚函数成为纯虚函数。纯虚函数定义方法如下:

    virtual 返回函数 函数名(形参表)=0。
    

      要注意的是,这里的=0并不是函数的返回值等于0,他只是起到形式上的作用,告诉编译系统该函数是纯虚函数。纯虚函数不具备函数功能,不能被调用。

      含有纯虚函数的类被称为抽象类。抽象类是一种特殊的类,它不能实例化,即不能定义对象,但可以声明指针,该类的派生类负责给出这个纯虚数的定义。抽象类的主要作用就是描述一组相关子类的通用操作接口。而具体实现是在派生类中完成的。

      例如,如果将上例的基类修改为抽象类,需要将基类的showArea()实现部分删除,将声明改为virtual void showArea()=0,则基类Graph就成为抽象类,基类的showArea()就是纯虚函数,再次运行程序是,结果不变,Graph变成抽象类后,不能在进行实例化对象操作,即Graph gp就是错误的,但可以定义指针,Graph *gp还是允许的。

       在继承关系中,抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有宠幸定义纯虚函数,只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类总给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,他就可以创建该类的实例了。

      不能从普通类(非抽象类)中派生出抽象类。

      在很多时候,基类本身是不合理的。例如,动物作为一个基类可以派生出老虎等动物,但动物本身生成对象明显是不合理的。为了解决这个问题,方便使用类的多态性,引入了纯虚数(viurtual ReturnTypeFunction()=0),则编译其要求在派生类中必须予以重写以实现多态性。同时含有纯虚函数的类成为抽象类,他不能生成对象。

     3、虚析构函数

      在析构函数前面加上virtual,该析构函数就被声明为虚析构函数。一般来说,如果一个类中定义了虚函数,析构函数也应该定义为虚析构函数。

      当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会个类添加一个虚函数表,里面用来存放虚函数指针,这样就在增加了类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

      

  • 相关阅读:
    binary tree解题模板
    done infosys 八股文springboot员工管理系统
    好题 telus国际_从镜子里看到另一台显示器的面试
    done 对于spring boot oa的面试思考
    done 有一点题 infosys 秀出你的ID spring经历问得很细
    Eliassen Group vendor 检测眼球视线的oa 又细又多做不完
    done tek 把你从牛角尖钻出来的node list算法题
    contagious.ly 自豪的纽约客,政府vendor
    Apple screening carlos白人 头晕脑胀的三道简单算法题
    done apple_Infosys_Jugal 要求完整写出Java8 stream
  • 原文地址:https://www.cnblogs.com/noticeable/p/9063910.html
Copyright © 2011-2022 走看看