C++学习笔记:(五)继承&多态

7.组合、继承与多态性

面向对象设计的重要目的之一就是代码重用,这也是C++重要性能之一。软件的重用性鼓励人们使用已有的、得到认可并经过测试的高质量代码。多态性可以以常规方式书写程序来访问多种现有的且专门化了的相关类。继承和多态是面向对象程序设计方法的两个最主要的特征。继承可以将一群相关的类组织起来,并共享期间的相同数据和操作行为;多态使得设计者在这些类上编程时,可以如同操作一个单一体,而非相互独立的类,且给设计者更多灵活性来加入或删除类的一些属性或方法。

 

7.1 组合

对于比较简单的类,其数据成员可能都是基本数据类型,但对于某些复杂的类来说,其某些数据成员可能又是另一些类类型,这就形成了类的组合。实际上,C语言中一直在使用组合,如C语言中的结构体就可以看成是不同类型数据的组合,前面提及的类也是不同类型数据及操作的组合,只不过是基本数据类型的组合而已。

#include <iostream>
class Myclass
{
	enum{NUM = 50};
	char cName[20];
	int iNum;
	Student stulist[Num];
	public:
	const char* Getclassname();
	cosnt char* Getstuname(int ino);
};

Myclass::Myclass()
{
	iNum = 0;
}

inline const char* Myclass::Getclassname()
{
	return cName;
}

const char* Myclass::GetStuName(int iNo)
{
	for(int i = 0; i < iNum; i++)
	{
		if(stulist[i].Getno() == iNo)
			return stulist[i].GetName();
	}
	return NULL;
}

 

7.2 继承

类的继承就是新类从已有类哪里获得已有的属性和行为,或者说是基类派生了具有基类特征又有新特征的派生类。继承是软件可重用型的一种形式,新类通过继承从现有类中吸取其属性和行为,并对其进行覆盖或改写,产生新类所需要的功能。同样,新类也可以派生出更新的类。创建新类,但不是从头创建。可以使用其他人已经创建并调试过的类。关键是使用类,而不是更改已存在的代码。

类继承语法:

class 子类名:[public|private|protected] 父类名
{
    ...
}

子类具有父类的所有属性和行为,且可以增加新的行为和属性。

C++提供了三种继承的方式:公有继承(public)、私有继承(private)、保护继承(protected)。

公有继承

(1)基类的private、public、protected成员的访问属性在派生类中保持不变。

(2)派生类中继承的成员函数可以直接访问基类中所有成员,派生类中新增的成员函数只能访问基类的public和protected成员,不能访问基类的private成员。

(3)通过派生类的对象只能访问基类的public成员。

矩形移动:

#include <iostream>
using namespace std;
class Point
{
	private:
		float x,y;
	public:
		void InitP(float xx = 0, float yy = 0)
		{
			x = xx;
			y = yy;
		}
		void Move(float a, float b)
		{
			x += a;
			y += b;
		}
		float Getx(){return x;}
		float Gety(){return y;}
};

class Rectangle:public Point
{
	private:
		float w,h;
	public:
		void InitR(float x, float y, float w, float h)
		{
			InitP(x, y);
			this->w = w;
			this->h = h;
		}
		float Geth(){return h;}
		float Getw(){return w;}
};

int main()
{
	Rectangle rect;
	rect.InitR(2,3,20,10);
	//rect.h;             
	//rect.w;                 //错误,父类私有成员
	rect.Move(3,2);
	cout << rect.Getx() << ',' <<rect.Gety() << ',' <<rect.Geth() << ',' <<rect.Getw() <<endl;
	return 0;
}

 

保护继承

(1)基类的public、protected成员都以protected身份出现在派生类中。

(2)派生类中新增的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。

(3)通过派生类的对象不能访问基类的任何成员。

 

私有继承

(1)基类的public、protected成员都以private身份出现在派生类中。

(2)派生类中新增的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。

(3)通过派生类的对象不能访问基类的任何成员。

例如:把上述程序的继承改为私有继承。

class Rectangle:private Point
{
private:
    float w,h;
public:
    void InitR(float x, float y, float w, float h)
    {
    InitP(x, y);
    this->w = w;
    this->h = h;
    }
    float geth(){retrun h;}
    float getw(){return w;}
};

 

当声明的子对象调用父类的成员函数时,会发生错误:

Rectangle rect;
Rect.InitP();     //错误,通过私有继承,父类的InitP()在子类中变成私有的

如果确实需要在子类里对私有继承父类的公有成员变为公有的,只需要在子类中声明为公有即可:

class Rectangle:private Point
{
...
public:
    Point::InitP;
    Point::getx;
...
};

注意:在派生类中声明基类的函数时,只需要给出函数的名称,函数的参数和返回值类型不应出现。

class Rectangle:private Point
{
...
public:
    Point::InitP;
    Point::getx;
    void Point::InitR(float x, float y, float w, float h);    //错误
    Point::InitR(float x, float y, float w, float h);         //错误
    Point::InitR();                                           //错误
...
};

关于继承,我将专门用一篇来介绍,这里只是先了解基本概念。

 

7.3继承和组合

实际工作中往往需要在定义一个新类时这个新类的一部分内容时从已有类中继承的,还有一部分内容则是需要由其他类组合的,这就需要把组合和继承放在一起使用。例如,在定义Z类时它的一部分内容需要从X类继承,另一部分内容则是Y类型的,具体定义:

class X
{
    int i;
};
class Y
{
    float f;
};
class Z:public X
{
    double d;
    Y y;
};

组合通常在希望新类内部有已存在类性能时使用,而不希望已存在类作为它的接口。这就是说,嵌入一个计划用于实现新类性能的对象,而新类的用户看到的是新定义的接口,而不是来自父类的接口。

继承是取一个已存在的类,并制作它的一个专门的版本。通常,这意味着取一个一般目的的类,并为了特殊的需要对它进行专门化。

建立班级类:

#include <iostream>
#include <string.h>
using namespace std;

class Person
{
	char strname[20];
	int iage;
	char csex;
	public:
	Person(const char* cpname = NULL, int age = 0, char sex = 'm');
	const char* Getname(){return strname;}
	int Getage(){return iage;}
	char Getsex(){return csex;}
	void Setname(const char *cpname);
	void Setage(int age){iage = age;}
	void Setsex(char sex){csex = sex;}
};

class Teacher:public Person
{
	int wid;
	public:
	Teacher(const char* cpname = NULL, int age = 0, char sex = 'm', int no = 0):Person(cpname, age, sex),wid(no){}
	int Getwid(){return wid;}
	void Setwid(int no){wid = no;}
};

class Student:public Person
{
	int ino;
	float score[5];
	float ave;
	public:
	Student(const char* cpname = NULL,int age = 0, char sex = 'm', int no = 0):Person(cpname, age, sex),ino(no){}
	int Getno(){return ino;}
	void Setno(int no){ino = no;}
	void Setscore(const float fdata[]);
	float Getavescore(){return ave;}
	void print();
};

void Studentprint(Student &a)
{
	cout << "Student's infomation:";
	cout << "Ino:" << a.Getno()  << " " << "Age:" << a.Getage() << " " << "Name:" << a.Getname() << "Sex:" << a.Getsex() <<endl;
}

Person::Person(const char* cpname, int age, char sex)
{
	csex = sex;
	iage = age;
	if(strlen(cpname) < 20)
	{
		strcpy(strname, cpname);
	}
	else 
		strname[0] = '\0';
}

void Person::Setname(const char* cpname)
{
	if(strlen(cpname) < 20)
		strcpy(strname, cpname);
	else 
		strname[0] = '\0';
}

void Student::Setscore(const float fdata[])
{
	float fsum = 0;
	for(int i = 0; i < 5; i++)
	{
		score[i] = fdata[i];
		fsum += fdata[i];
	}
	ave = fsum/5;
}

int main()
{
	Student a("Tyler", 23, 'm', 00001);
	Studentprint(a);
	return 0;
}

 

7.4构造与析构顺序

当采用继承方式创建子类对象时,首先从父类开始执行构造函数,父类的成员,然后再执行子类的构造函数,子类成员;当撤销子类对象时,执行相反的顺序,即首先撤销子类的成员,执行子类的析构函数,在撤销父类成员,执行父类的析构函数。

#include <iostream>
using namespace std;
class A
{
	int a;
	public:
	A(int i = 0):a(i)
	{
		cout << "A is constructed" <<endl;
	}
	~A()
	{
		cout << "A is destructed" <<endl;
	}
};

class B:public A
{
	int b;
	public:
	B(int j = 0):b(j)
	{
		cout << "B is constructed" <<endl;
	}
	~B()
	{
		cout << "B is destructed" <<endl;
	}
};

int main()
{
	B b;
	return 0;
}

构造顺序:

(1)调用基类的构造函数。

(2)根据类中声明的顺序构造组合对象。

(3)派生类中构造函数的执行。

派生类中构造函数的格式:

class 派生类名:[public|private|protected]基类名
{
public:
派生类名(参数列表1):基类名(参数列表2),组合对象列表{...}
};

组合类中构造函数与析构函数的调用顺序:

#include <iostream>
using namespace std;

class X
{
	public:
		X()
		{
			cout << "X is constructed" <<endl;
		}
		~X()
		{
			cout << "X is destructed" <<endl;
		}
};

class A
{
	int a;
	X x;
	public:
	A(int i = 0)
	{
		cout << "A is constructed" <<endl;
	}
	~A()
	{
		cout << "A is destructed" <<endl;
	}
};

class Y
{
	int y;
	public:
	Y(int i = 0)
	{
		y = i;
		cout << "Y is constructed" <<endl;
	}
	~Y()
	{
		cout << "Y is destructed" <<endl;
	}
};

class Z
{
	int z;
	public:
	Z(int i = 0)
	{
		z = i;
		cout << "Z is constructed" <<endl;
	}
	~Z()
	{
		cout << "Z is destructed" <<endl;
	}
};

class B:public A
{
	int b;
	Y y;
	Z z;
	public:
	B(int j = 0):A(1),z(j),b(j),y(j)
	{
		cout << "B is constructed" <<endl;
	}
	~B()
	{
		cout << "B is destructed" <<endl;
	}
};

int main()
{
	B b;
	return 0;
}

如果基类的构造函数没有参数,或者没有显示定义构造函数时,派生类构造时可以不向基类传递参数,甚至可以不定义构造函数。

注意:

(1)派生类不能继承基类的构造函数和析构函数。当基类带有参数的构造函数时,派生类必须定义构造函数,以便把参数传递给基类构造函数。

(2)当派生类也作为基类使用时,则各派生类只负责其直接的基类的构造。

(3)因为析构函数不带参数,派生类中析构函数的存在不依赖于基类,基类中析构函数的存在也不依赖于派生类。

继承类中构造与析构函数的调用顺序:

#include <iostream>
using namespace std;

class A
{
	int a;
	public:
	A(int i = 0)
	{
		cout << "A is constructed" <<endl;
	}
	~A()
	{
		cout << "A is destructed" <<endl;
	}
};

class Y
{
	int y;
	public:
	Y(int i = 0)
	{
		y = i;
		cout << "Y is constructed" <<endl;
	}
	~Y()
	{
		cout << "Y is destructed" <<endl;
	}
};

class B:public A
{
	int b;
	Y y;
	public:
	B(int j = 0):b(j),y(j)
	{
		cout << "B is constructed" <<endl;
	}
	~B()
	{
		cout << "B is destructed" <<endl;
	}
};

class C:public B
{
    int c;
public:
    C(int i):B(1),c(i)
    {
    cout << "C is constructed" <<endl;
    }
    ~C()
    {
    cout << "C is destructed" <<endl;
    }
};

int main()
{
	C c(2);
	return 0;
}

 

7.5派生类重载基类函数的访问

如果在基类中有一个函数名被重载几次,在派生类中又重定义了这个函数名,则在派生类中会覆盖这个函数的所有基类定义。也就是说,通过派生类来访问该函数时,由于采用就近匹配的原则,只会调用在派生类中所定义的该函数,基类中所定义的函数都变得不再可用。

派生类中重载基类函数:

#include <iostream>
using namespace std;

class Number
{
	public:
		void print(int i)  {cout << i;}
		void print(char c) {cout << c;}
		void print(float f){cout << f;}
};

class Data:public Number
{
	public:
		void print(){}
		void f()
		{
			//print(5);    //错误,因为新定义的print()函数不带参数
		}
};

int main()
{
	Data data;
	data.print();            //正确
	data.print(1);           //错误
	//data.print(12.3);      //错误
	//data.print('a');       //错误
	return 0;
}

因为Data类中对print()函数做了重定义,所以没有找到一个匹配的函数给Data类对象调用。如果要访问基类中声明的函数,则用以下方法:

(1)使用作用域标识符限定。

Data.Number::print(5);    //正确,被调用的函数是基类Number的print(int)

(2)避免名称覆盖。

class Number
{
public:
    void print(int i)     {cout << i;}
    void print(char c)   {cout << c;}
    void print(float f)   {cout << f;}
};
class Data:public Number
{
public:
    void print2(){}
    void f()
    {
    print(1);
    }
};

函数f()中调用基类函数print()的方法有称为向上映射,即派生类向上调用基类的函数。向上映射是派生类中在不引用歧义的情形下才有,即没有名称覆盖发生。

 

7.6多态性&虚函数

面向对象程序设计的真正优势不仅仅在于继承,还在于将派生类对象当基类对象一样处理的功能。支持这种功能的机制就是多态和动态绑定。

多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。最简单的例子就是运算符,使用同样的加号”+”,就可以实现整型数之间、浮点数之间、双精度浮点数之间的加法,以及这几种数据类型混合的加法运算。同样的相加操作,被不同类型的对象接收后,不同类型的变量采用不同的方式进行加法运算。如果是不同类型的变量相加,例如浮点数和整型数,则要先将整型数转换为浮点数,然后再进行加法运算,这就是典型的多态现象。

多态的类型:面向对象的多态性可以分为4类,重载多态、强制多态、包含多态和参数多态。前面两种统称为专用多态,而后面的两种称为通用多态之前所学习的普通函数、类的成员函数的重载以及运算符重载都属于重载多态。上述加法运算符在进行浮点数与整型数相加时,首先进行类型强制转换,把整数变为浮点数再相加的情况,就是强制多态的实例。包含多态是类族定义与不同类中的同名成员函数的多态行为,主要是通过虚函数来实现的。参数多态与类模板相关联,在使用时必须赋予实际的类型才可以实例化。这样,由类模板实例化的各个类都具有相同的操作,而操作对象的类型却各不相同。

多态性是面向对象程序设计的重要特征,重载和虚函数是体现多态性的两个重要手段。虚函数体现了多态的灵活性,进一步减少冗余信息,显著提高了软件的可扩充性。通过学习函数重载与继承方法后,经常会遇到以下问题:在派生类中存在对基类函数的重载,当通过派生类对象调用重载函数时却调用了基类的原函数。

通过派生类对象间接调用重载函数:

#include <iostream>
using namespace std;

class Ins
{
	public:
		void play() const
		{
			cout << "Ins::play" <<endl;
		}
};

class Piano:public Ins
{
	public:
		void play() const
		{
			cout << "Piano::play" <<endl;
		}
};

void tune(Ins &i)        //由参数类型决定
{
	i.play();
}

int main()
{
	Piano a;
	tune(a);
	return 0;
}

7.6.1静态绑定与动态绑定

绑定,又称联编,是使一个计算机程序的不同部分彼此关联的过程。根据进行绑定所处阶段的不同,有两种不同的绑定方法,静态绑定和动态绑定。

(1)静态绑定在编译阶段完成,所有绑定过程都在程序开始之前完成。静态绑定具有执行速度快的特点,因为在程序运行前,编译程序能够进行代码优化。

函数重载(包括成员函数重载和派生类对基类函数的重载)就是静态绑定。而上例中的问题根源在于:通过指针或引用引起的对普通成员函数的调用,由参数的类型决定,而在指针或引用实际指向的对象无关。这也是静态绑定的限定性。

(2)如果编译器在编译阶段不确切地知道把发送到对象的消息和实现消息的哪段代码具体联系到一起,而是运行时才把函数调用与函数具体联系在一起,就称作动态绑定。相对于静态绑定,动态绑定是在编译后绑定,也称晚绑定,又称运行时识别。动态绑定具有灵活性好、更高级更自然的问题抽象、易于扩充和易于维护等特点。通过动态绑定,可以动态地根据指针或引用指向的对象实际类型来选择调用的函数。

 

7.6.2虚函数

虚函数定义格式:

class 基类名
{
...
    virtual 返回值类型 要在派生类中重载的函数名(参数列表);
};

用虚函数方法实现对派生类中重载函数的调用:

#include <iostream>
using namespace std;

class Ins
{
	public:
		virtual void play() const
		{
			cout << "Ins::play" <<endl;
		}
};

class Piano:public Ins
{
	public:
		void play() const
		{
			cout << "Piano::play" <<endl;
		}
};

class Newpiano:public Piano
{
	public:
		void play() const
		{
			cout << "Newpiano::play" <<endl;
		}
};

void tune(Ins &i)        //由参数类型决定
{
	i.play();
}

int main()
{
	Piano a;
	tune(a);
	Newpiano b;
	tune(b);
	Ins *p = &a;
	tune(*p);
	p = &b;
	tune(*p);
	Ins c;
	tune(c);
	return 0;
}

使用虚函数时需要注意:

(1)必须在基类中声明虚函数,即需要在派生类中重载的函数,必须在基类中声明为虚函数。

(2)虚函数一经声明,在派生类中重载的基类中的函数即是虚函数,不需要再加virtual。

(3)只有非静态成员函数可以声明为虚函数。静态成员函数和全局函数不能声明为虚函数。

(4)编译器把名称相同、参数不同的函数看做不同的函数。基类和派生类中有相同名字但是参数列表不同的函数,不需要声明为虚函数。

(5)普通对象调用虚函数,系统仍然以静态绑定方式调用函数。因为编译器编译时能确切知道对象的类型,能确切调用其成员函数。

Piano x;
Newpiano y;
X = y;
tune(x);

运行结果是Piano::play

 

7.6.3虚析构函数

通过学习虚函数,可以掌握虚函数在继承和派生中的调用方式。

(1)构造函数不能声明为虚函数。因为构造函数有特殊的工作,它处在对象创建初期,首先调用基类构造函数,然后调用按照继承顺序派生的派生类的构造函数。

(2)析构函数能够且常常必须是虚函数。析构函数调用顺序与构造函数完全相反,从最晚派生类开始,依次向上到基类。析构函数确切地知道它是从哪个类派生而来的。

虚析构函数声明格式:
virtual ~析构函数名称();
虚析构函数定义:
class Ins
{
public:
    virtual void play()
    {
    cout << “Ins::play” <<endl;
    }
    virtual ~Ins();
};

虚函数的目的是让派生类去做自己想做的事所以应该在基类中声明虚析构函数。当类中存在虚函数时,也应该使用虚析构函数。这样保证类对象销毁时得到完整空间。如果某个类不包含虚函数,一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,建议不要将析构函数声明为虚函数,以保证程序执行的高效性。

 

7.7纯虚函数和抽象基类

工作有时需要定义这么一个类,对这个类中的处理函数只需要说明函数的名称、参数列表,以及返回值的类型,也就是只提供一个接口,以说明和规范其他程序对此服务的调用。至于这个函数如何实现,则根据具体需要在派生类中定义即可。通常把这样的类称为抽象基类,而把这样的函数称为纯虚函数。纯虚函数定义格式:

virtual 返回值类型 函数名称(参数列表) = 0;

当一个类中存在纯虚函数时,这个类就是抽象类。

class Ins
{
public:
    virtual void play()const = 0;   //纯虚函数
};

抽象类的主要作用是通过它为一个类建立一个公共的接口,使他们能够更有效地发挥多态特性。使用抽象类时要注意:

(1)抽象类只能用做其他类的基类,不能建立抽象类对象。抽象类处于继承层次结构的较上层,抽象类自身无法实例化,只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化(抽象类提供了若干函数接口,当有其他类需要时,可以通过多重继承来使用这些函数接口,而新派生类不是一个抽象类,所以可以实例化)。

(2)抽象类不能用做参数类型、函数返回值或显式转换的类型。

(3)可以声明一个抽象类的指针和引用。通过指针或引用,可以指向并访问派生类对象,以访问派生类的成员。

(4)抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以声明自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。

用抽象类的方法:

#include <iostream>
using namespace std;

class Ins
{
	public:
		virtual void play() const = 0;
};

class Piano:public Ins
{
	public:
		void play() const
		{
			cout << "Piano::play" <<endl;
		}
};

class Newpiano:public Piano
{
	public:
		void play() const
		{
			cout << "Newpiano::play" <<endl;
		}
};

void tune(Ins &i)        //由参数类型决定
{
	i.play();
}

int main()
{
	Piano a;
	tune(a);
	Newpiano b;
	tune(b);
	Ins *p = &a;
	tune(*p);
	p = &b;
	tune(*p);
	//Ins c;             //错误,Ins是抽象类,不能实例化
	return 0;
}

纯虚函数非常有用,因为它使得类有明显的抽象性,并告诉用户和编译器希望如何使用。在基类中,对纯虚函数提供定义时有可能的,告诉编译器不允许纯抽象基类声明对象,而且纯虚函数在派生类中必须定义,以便于创建对象。然而,如果希望一块代码对于一些或所有派生类定义能共同使用,而不希望在每个函数中重复这段代码,具体实现方法如下:

#include <iostream>
using namespace std;

class Ins
{
	public:
		virtual void play() const = 0;
		virtual void showmsg() const
		{
			cout << "Ins::showmsg()" <<endl;
		}
};

class Piano:public Ins
{
	public:
		void play() const
		{
			cout << "Piano::play" <<endl;
		}
		void showmsg()const
		{
			Ins::showmsg();
		}
};

class Newpiano:public Piano
{
	public:
		void play() const
		{
			cout << "Newpiano::play" <<endl;
		}
		void showmsg()const
		{
			Ins::showmsg();
		}
};

void tune(Ins &i)        //由参数类型决定
{
	i.play();
}

int main()
{
	Piano a;
	tune(a);
	a.showmsg();
	Newpiano b;
	tune(b);
	b.showmsg();
	return 0;
}

7.8 多重继承

在派生类的声明中,基类名可以有一个,也可以有多个。如果只有一个基类名,则这种继承方式称为单继承;如果基类名有多个,则这种继承方式称为多继承,这时的派生类同时得到了多个已有类的特征。

 

7.8.1多继承语法

多继承允许派生类有两个或多个基类的能力,这是为了使多个类以这种方式组合起来,使得派生类对象的行为具有多个基类对象的特征。多继承声明语法:

class 派生类名:[继承方式]基类名1,[继承方式]基类名2,...[继承方式]基类名n

多继承的使用:

#include <iostream>
using namespace std;

class A
{
	int a;
	public:
	void SetA(int x)
	{
		a = x;
	}
};

class B
{
	int b;
	public:
	void SetB(int x)
	{
		b = x;
	}
};

class C:public A, private B
{
	int c;
	public:
	void SetC(int , int, int);
};

void C::SetC(int x, int y, int z)
{
	SetA(x);
	SetB(y);
	c = z;
}

int main()
{
	C obj;
	obj.SetA(5);
	obj.SetC(6, 7, 9);
	//obj.setB(6);       //错误,不能访问私有继承的基类成员
	return 0;
}

7.8.2多继承中的二义性

多继承中的二义性例子:

#include <iostream>
using namespace std;

class A
{
	int a;
	public:
	void f(int x)
	{
		a = x;
	}
};

class B
{
	int b;
	public:
	void f(int x)
	{
		b = x;
	}
};

class C:public A, private B
{
	int c;
	public:
	void SetC(int);
};
void C::SetC(int x)
{
	c = x;
}

int main()
{
	C obj;
	//obj.f(5);       //错误,不能访问私有继承的基类成员
	return 0;
}

对编译器来说,不知道是调用基类A的f(),还是调用基类B的f()。解决的办法是使用域名控制来解决:

obj.A::f(5);
obj.B::f(5);

增加作用域分辨符可以消除二义性,但降低了程序的可读性。因此,尽量避免在基类中使用同名函数。

多继承里还有这样的情况:有相同基类带来的二义性。例如:

class base
{
...
public:
    void show();
};

class c1:public base
{
...
public:
    void show();
};

class c2:public base
{
...
public:
    void show();
};

class a:public c1,public c2
{
...
public:
    void show();
};

这种情况被称为菱形继承。类a的父类c1、c2继承了基类base的成员函数show(),当然类a也继承了成员函数show(),a调用show()则产生了二义性;类a继承c1、c2的同时也包含了两份基类base,菱形继承使得子对象重叠,增加了额外的空间开销。为了解决此类问题,C++引入了虚基类。

如把一个基类定义为虚基类,必须在派生子类时在父类的名字前加关键字virtual。

Class 派生类名:virtual 访问权限修饰符 父类名{};

虚基类使用方法:

#include <iostream>
using namespace std;

class base
{
	public:
		virtual char* show()const = 0;
};

class d1:virtual public base
{
	public:
		char* show()const{return (char*)"d1";}
};
class d2:virtual public base
{
	public:
		char* show()const{return (char*)"d2";}
};
class m:public d1,public d2
{
	public:
		char* show()const
		{
			return d2::show();
		}
};

int main()
{
	m m1;
	cout << m1.show() <<endl;
	return 0;
}

7.8.3最终派生类

在上一个例子中,各类没有构造函数,使用的是默认构造函数。如果类里有了构造函数,情形将有所不同。如在上面的base基类里增加构造函数:

Base(int){}

则用派生类声明对象时,编译器报错:没有合适的构造函数调用。为解决此类问题,首先引入最终派生类的概念。

最终派生类,又称为最晚辈派生类,指当前所在的类。例如,在基类base的构造函数里,base就是最终派生类;在派生类a里,a就称为最终派生类;在类c1里,类c1就成为最终派生类。当使用虚基类时,尤其是带有参数的构造函数的虚基类时,最终派生类的构造函数必须对虚基类初始化。不管派生类离虚基类多远,都必须对虚基类初始化。

含虚基类的构造函数使用方法:

#include <iostream>
using namespace std;

class base
{
	int i;
	public:
	base(int x):i(x){}
	int geti(){return i;}
	virtual char* show()const
	{
		return (char*)"base";
	}
};

class d1:virtual public base
{
	int id1;
	public:
	d1(int x = 1):base(0),id1(x){}
	char* show()const{return (char*)"d1";}
};
class d2:virtual public base
{
	int id2;
	public:
	d2(int x = 2):base(1),id2(x){}
	char* show()const{return (char*)"d2";}
};
class m:public d1,public d2
{
	int im;
	public:
	m(int x = 0):base(3),im(x){}
	char* show()const
	{
		return d2::show();
	}
};

int main()
{
	m m1;
	cout << m1.show() <<endl;
	cout << m1.geti() <<endl;
	d1 d11;
	cout << d11.geti() <<endl;
	cout << d11.show() <<endl;
	return 0;
}

使用虚基类时要注意:

(1)必须在派生类的构造函数中调用初始化虚基类的构造函数。

(2)给虚基类安排默认构造函数。使得虚基类的使用者变得简单易行。

 

7.8.4多继承的构造顺序

#include <iostream>
using namespace std;

class base
{
	int i;
	public:
	base(int x):i(x)
	{
		cout << "base is cnostructed" <<endl;
	}
	virtual ~base()
	{
		cout << "base is destructed" <<endl;
	}
	int geti(){return i;}
	virtual char* show()const = 0;
};

class d1:virtual public base
{
	int id1;
	public:
	d1(int x = 1):base(0),id1(x)
	{
		cout << "d1 is constructed" <<endl;
	}
	virtual ~d1()
	{
		cout << "d1 is destructed" <<endl;
	}
	char* show()const{return (char*)"d1";}
};
class d2:virtual public base
{
	int id2;
	public:
	d2(int x = 2):base(1),id2(x)
	{
		cout << "d2 is constructed" <<endl;
	}
	virtual ~d2()
	{
		cout << "d2 is destructed" <<endl;
	}
	char* show()const{return (char*)"d2";}
};
class m:public d1,public d2
{
	int im;
	public:
	m(int x = 0):base(3),im(x)
	{
		cout << "m is constructed" <<endl;
	}
	virtual ~m()
	{
		cout << "m is destructed" <<endl;
	}
	char* show()const
	{
		return d2::show();
	}
};

int main()
{
	m m1;
	cout << m1.show() <<endl;
	return 0;
}

多继承构造顺序与单继承构造顺序类似,从基类开始,沿着派生顺序逐层向下,当同一层次派生同一个类时,按照声明继承的顺序自左到右。例如,c1、c2派生a时,先构造c1,再构造c2。析构顺序与构造顺序相反。

 

7.9派生类成员的标识与访问

经过类的派生,就形成了一个具有层次结构的类族。接下来就看看派生类使用过程中的一些问题,也就是标识和访问派生类及其对象的成员问题。

派生类中,成员可以按访问属性划分为以下4种:

(1)不可访问的成员。这是从基类私有成员继承而来的,派生类或是建立派生类对象的模块都没有办法访问到它们,如果从派生类继续派生新类,也是无法访问的。

(2)私有成员。包括从基类继承过来的成员以及新增加的成员,在派生类内部可以访问,但是建立派生类对象的模块中无法访问,继续派生,就成为了新的派生类中的不可访问成员。

(3)保护成员。可能是新增也可能是从基类继承过来的,派生类内部成员可以访问,建立派生类对象的模块无法访问,进一步派生,在新的派生类中可以成为私有成员或保护成员。

(4)公有成员。派生类、建立派生类的模块都可以访问,继续派生,可能是新派生类中的私有、保护或者共有成员。

 

7.9.1作用域分辨符

作用域分辨符,就是我们经常见到的”::”,它可以用来限定要访问的成员所在的类的名称。一般使用形式是:

类名::成员名           //数据成员
类名::成员名(参数表)    //函数成员

对于在不同的作用域声明的标识符,可见性原则是:如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层仍然可见;如果在内层声明了同名标识符,则外层标识符在内层不可见,这时称内层标识符隐藏了外层同名标识符,这种现象称为隐藏规则。

在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层。这是如果派生类声明了一个和某个基类成员同名的新成员,派生的新成员就隐藏了外层同名成员,直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定。

对于多继承情况,首先要考虑各个基类之间有没有任何继承关系,同时考虑有没有共同基类的情况。最经典的情况就是所有基类都没有上级基类。如果某派生类的多个基类拥有同名的成员,同时,派生类有新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。这时使用对象名.成员名对象指针->成员名方式可以唯一标识和访问派生类新增成员,基类的同名成员也可以使用基类名和作用域分辨符访问。但是,如果派生类没有声明同名成员,对象名.成员名对象指针->成员名方式就无法唯一标识成员。这时,从不同基类继承过来的成员具有相同的名称,同时具有相同的作用域,系统仅仅根据这些信息根本无法判断到底是调用哪个基类的成员,这时就必须通过基类名和作用域分辨符来标识成员。

细节:如果子类中定义的函数与父类的函数同名但是具有不同的参数数量或参数类型,不属于函数重载,这时子类中的函数将使父类中的函数隐藏,调用父类中的函数必须使用父类名称来限定。只有在相同的作用域中定义的函数才可以重载。

#include <iostream>
using namespace std;
class Base1
{
public:
    int var;
    void fun(){cout << “Member of Base1” <<endl;}
};

class Base2
{
public:
    int var;
    void fun(){cout << “Member of Base2” <<endl}
};

class Derived:public Base1,public Base2
{
public:
    int var;
    void fun(){cout << “Member of Derived” <<endl;}
};

int main()
{
    Derived d;
    Derived *p = &d;
    d.var = 1;
    d.fun();
    d.Base1::var = 2;
    d.Base2::fun();
    p->Base2::var = 3;
    p->Base2::fun();
    return 0;
}

在主函数中,创建了一个派生类的对象d,根据隐藏规则,如果通过成员名称来访问该类成员,就只能访问到派生类新增加的两个成员,从基类继承过来的成员由于处于外层作用域而被隐藏。这时要想访问从基类继承来的成员,就必须使用类名和作用域分辨符。程序中后两组语句就是分别访问由基类Base1,Base2继承来的成员。通过作用域分辨符,就明确地唯一标识了派生类中由基类所继承来的成员,达到了访问的目的,解决了成员被隐藏的问题。

如果将类Derived写成这样:
class Derived:public Base1,public Base2{};
此时在主函数中写:
d.var = 1;   //错误,这样会有二义性
d.fun();     //错误,这样会有二义性

如果希望d.var和d.fun()的用法不产生二义性,可以使用using关键字加以澄清。例如:

class Derived:public Base1,public Base2
{
public:
    using Base1::var;
    using Base2::fun;
};

这样,d.var和d.fun()都可以明确表示对Base1中相关成员的引用了。using的一般功能是将一个作用域中的名字引入到另一个作用域中,它还有一个非常有用的用法:将using用于基类中的函数名,这样派生类中如果定义同名但参数不同的参数,基类的函数不会被隐藏,两个重载的函数将会并存在派生类的作用域中。例如:

class Derived2:public Base1
{
public:
    using Base1::fun;
    void fun(int i){...}
};

这时,使用Derived2的对象,既可以直接调用无参数的fun()函数,又可以直接调用带int型参数的fun函数。

猜你喜欢

转载自blog.csdn.net/qq_38289815/article/details/81607003