C++:组合、继承与多态

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

在C++中可以用类的方法解决代码重用,通过创建新类重用代码,而不是从头创建,这样可以使用其他人已经创建并调试过的类,其关键是使用类而不是更改已存在的代码。下面将介绍两个方法:第一种方法是简单的创建一个包含已存在的类对象的新类称为组合,因为这个新类是由于存在类的对象组合的;第二种方法是创建一个新类作为一个已存在类的类型,采用这个已存在类的形式,只对它增加代码,但不修改,这种方法称为继承,其中大量的工作有编译器完成。继承是面向对象程序设计的核心。

组合

对于比较简单的类,其数据成员对位基本数据类型,但对于某些复杂的类来说,其某些数据成员可能又是另一些类的类型,这就形成了类的组合(聚集)。

例如:一类方式板寸某个班级的名称、人数以及每个学生的学号、姓名、学习成绩(省略类Student

cpp 复制代码
//MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H_
#include "student.h"
class MyClass
{
	enum
	{
		NUM = 50
	};
	char Name[20];
	int Num;
	Student stuList[NUM];
public:
	MyClass();
	const char* GetClassName();
	const char* GetStuName(int No);
};

#endif // !MYCLASS_H

//MyClass.cpp
#include "MyClass.h"
#include <cstddef>
MyClass::MyClass()
{
	Num = 0;
}

inline const char* MyClass::GetClassName()
{
	return Name;
}

const char* MyClass::GetStuName(int No)
{
	for (int i = 0; i < NUM; i++)
	{
		if (stuList[i].GetNo() == No)
			return stuList[i].GetName();
	}
	return NULL;
}

若嵌入的对象是公有的,也可以用"多级"访问。

继承

类的继承就是新类从已有类中获得以有的属性和行为,或者说就是从基类派生出既有基类特征又有新特征的派生类。

创建新类,但不是从头创建,开源使用其他人已经创建并调试过的类。关键是使用类,而不是更改已存在的代码。

类继承格式为:class 子类名:[pubilc|private|protected]父类名{···}

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

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

  1. 公有继承
    a. 基类的private、public和protected成员的访问属性在派生类中保持不变。
    b.派生类中继承的成员函数可以直接访问基类中的所有成员,派生类中新增二点成员函数只能访问基类的public和protected成员,不能访问基类的private成员。
    c.通过派生类的对象只能访问基类的public成员。
  2. 受保护继承
    a.基类的public和protected成员都已protected身份出现在派生类中。
    b.派生类中新增的成员函数可以直接访问基类中的public和protected成员,但不能访问基类中private成员。
    c.通过派生类的对象不能访问基类中的任何成员。
  3. 私有继承
    a.基类的public和protected成员都已private身份出现在派生类中。
    b.派生类中新增的成员函数可以直接访问基类中的任何成员。
    c.通过派生类的对象不能访问基类中的任何成员。

继承与组合

在实际工作中往往一个新类中既有从已有类中继承的,也有由其他类组合的,这需要把组合和继承放在一起使用。

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

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

继承与组合中的构造和析构

成员对象初始化

对于继承,应在冒号之后和这个类体的左花括号"{"之前放基类。而在过早函数的初始化表达式中,可以将对子对象构造函数的调用语句放在构造函数参数表和冒号之后,在函数体的左花括号"{"之前。

对于组合应给出对象的名字而不是类名。若在初始化表达式中有多于一个的构造函数调用,应当用逗号隔开,例如:

Student::Student(const char*Name = NULL,int Age = 0, char Sex = 'm', int No = 0):Person(CpName,Age,Sex),No(No),Ave(0){···}

需要注意的是,这里对基本数据类型的初始化工作成为"伪构造函数",甚至可以应用到类外:int i(10);

构造和析构顺序

对于析构函数来说,执行次序与构造函数相反,系统调用构造函数生成对象,调用析构函数释放对象所占用的内存空间。当采用继承方式创建子类对象时,先从父类开始执行构造函数->父类的成员->执行子类的构造函数->子类成员;当撤销子类对象时,执行相反的顺序,即先撤销子类的成员->执行子类的析构函数->撤销父类成员->执行父类的析构函数。

例如:在采用继承方式生成的类中,构造函数与析构函数的调用顺序。

cpp 复制代码
#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 i = 0) :b(i)
	{
		cout << "B is constructed" << endl;
	}
	~B()
	{
		cout << "B is destructed" << endl;
	}
};

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

程序运行结果:

A is constructed
B is constructed
B is destructed
A is destructed

相对于基类和派生类,若类中有静态数据成员,构造顺序又是怎样?仍然时"仿生"自然界的顺序,即先父类再子类,按照属性成员声明的先后顺序进行构造:

  1. 调用基类的构造函数;

  2. 根据类中声明的顺序构造函数组合对象;

  3. 派生类中构造函数的执行。
    派生类构造函数的格式为:

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

析构函数的顺序正好相反。

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

cpp 复制代码
#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) :a(i)//基类构造函数
	{
		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 i = 0) :A(1), b(i), z(i), y(i)//派生类构造函数的后面为内嵌对象列表
	{
		cout << "B is constructed" << endl;
	}
	~B()//派生析构函数
	{
		cout << "B is destructed" << endl;
	}
};

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

程序运行结果为:

X is constructed
A is constructed
Y is constructed
Z is constructed
B is constructed
B is destructed
Z is destructed
Y is destructed
A is destructed
X is destructed

注意:

  1. 派生类不能继承基类的构造函数和析构函数,当基类有带参数的构造函数时,则派生类必须定义构造函数,以便把参数传递给基类构造函数
  2. 当派生类也作为基类使用时,则各派生类子负责其直接的基类构造。
  3. 因为析构函数不带参数,所以派生类中析构函数的存在不依赖于基类,基类中析构函数的存在也不依赖于派生类。
    例如:继承类中构造函数与析构函数的调用顺序。
cpp 复制代码
#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 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 i = 0) :b(i), y(i)
	{
		cout << "B is constructed" << endl;
	}
	~B()
	{
		cout << "B is destructed" << endl;
	}
};

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

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

程序运行结果为:

A is constructed
Y is constructed
B is constructed
C is constructed
C is destructed
B is destructed
Y is destructed
A is destructed

名字覆盖

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

若要访问基类中声明的函数,则有以下方法:

  1. 使用作用域标识符限定。
  2. 避免名称覆盖。

虚函数

多态是面向对象程序设计的重要特性,重载和虚函数是体现多态的两个重要手段。虚函数体现了多态的灵活性,可进一步减少冗余信息,显著提高软件的可扩充性。

学习函数重载与继承的方法后,经常会遇见到下面问题,在派生类中存在对基类函数的重载,当通过派生类对象调用重载函数时却调用了基类中的原函数。

例如:通过派生类对象简介调用重载函数

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	void play() const
	{
		cout << "A::play" << endl;
	}
};

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

void tune(A& i)
{
	i.play();
}

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

程序运行结果为:

A::play

可以看出,输出结果并不是我们想要的B::play,而是A::play。显然,这不是所希望的输出结果,因为这个对象实际上就是B类型,而不只是A类型。C++类型虽然语法检验很严格,但是函数tune(通过引用)接受一个A类型的对象,也不拒绝任何从A派生的类对象。为了理解这个问题引入下面的概念。

虚函数的定义

虚函数定义格式为:

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

例如,用虚函数修改上例

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	virtual void play() const
	{
		cout << "A::play" << endl;
	}
};

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

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

void tune(A& i)
{
	i.play();
}

int main()
{
	B b;
	tune(b);
	C c;
	tune(c);
	A* p = &b;
	tune(*p);
	p = &c;
	tune(*p);
	A a;
	tune(a);
	return 0;
}

程序运行结果为:

B::play
C::play
B::play
C::play
A::play

不管有多少层,虚函数都能得到很好的应用。因为一旦在基类中声明为虚函数,就可根据类对象的类型动态决定调用基类或派生类的函数。

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

  1. 在基类中声明虚函数,即需要在派生类中重载的函数,必须在基类中声明。
  2. 虚函数一经声明,在派生类中重载的基类的函数即是虚函数,不在需要加virtual
  3. 只有非静态成员函数可以声明为虚函数,静态成员函数和全局函数不能声明为虚函数。
  4. 编译器把名称相同、参数不同的函数看作不同的函数。基类和派生类中名字相同但参数不同的函数,不需要声明为虚函数。
  5. 普通对象调用虚函数时,系统仍然以静态绑定方式调用函数。因为编译器编译时能确切地知道对象的类型,且能确切地调用其成员函数。

虚析构函数

通过了解虚函数可以掌握虚函数在继承和派生中的调用方式。那么类的两种特殊的函数------构造函数和析构函数是否可以声明为虚函数呢?

  1. 构造函数不能声明为虚函数。因为构造函数有其特殊的工作,它处在对象创建初期,先要调用基类构造函数,然后调用按照继承顺序派生的派生类的构造函数。
  2. 析构函数能够且经常是虚函数。系后汉书调用顺序与构造函数完全相反,从最晚派生类开始,以此向上到基类。因此,析构函数确切地知道它是从按个类派生而来的。
    虚析构函数声明格式为:virtual~析构函数名称();

虚函数的目的是让派生类编制自己的行为,所以应该在基类中声明虚析构函数。当类中存在虚函数时,也应该使用虚析构函数,这样保证类对象销毁时能得到"完全"的空间释放。

若某个类不包含虚函数时,一般表示它将不作为一个基类来使用,建议不要将析构函数声明为虚函数,以保证程序执行的高效性。

纯虚函数和抽象基类

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

纯虚函数定义格式为:virtual 返回值类型 函数名称(参数列表)=0;

当一个类中存在纯虚函数时,这个类就是抽象类。抽象类的主要作用是,为一个类建立一个公共的接口,使它们能够更有效地发挥多态特性。使用抽象类需要注意:

  1. 抽象类只能用于其他类的基类,不能建立抽象类对象。抽象类处于继承层次结构的较上层,抽象类自身无法实例化,只能通过继承机制生成抽象类的非抽象派生类,然后再实例化。
  2. 抽象类不能用于参数类型、函数返回值或显示转换的类型。
  3. 可以声明一个抽象类的指针和引用。通过指针或引用可以指向并访问派生类对象,以访问派生类的成员。
  4. 抽象类派生出新的类之后,若派生类给出所有纯虚函数的函数实现,这个派生类就可以声明自己的对象,因而其不再是抽象类;反之,若派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类

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

例如:虚函数与纯虚函数的使用。

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	virtual void play() const = 0;
	virtual void show()const = 0
	{
		cout << "A:show()" << endl;
	}
};

class B :public A
{
public:
	void play() const
	{
		cout << "B::play" << endl;
	}
	void show() const
	{
		A::show();
	}
};

class C :public B
{
public:
	void play() const
	{
		cout << "C::play" << endl;
	}
	void show() const
	{
		A::show();
	}
};

void tune(A& i)
{
	i.play();
}

int main()
{
	B b;
	tune(b);
	b.show();
	C c;
	tune(c);
	c.show();
	return 0;
}

程序运行结果为:

B::play
A:show()
C::play
A:show()

多重继承

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

多继承语法

多继承允许派生类有两个或多个基类的能力,就是想使多个类以这种方式组合起来,使派生类对象的行为具有多个基类对象的特征。在多继承中各个基类名之间用逗号隔开。

多继承的声明格式为:class 派生类名:[继承方式]基类名1,[继承方式]基类名2,···,[继承方式]基类名n{···};

例如:多继承的使用

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
	int a;
public:
	void SetA(int i)
	{
		a = i;
	}
};

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

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;
}

虚基类

在多继承中,经常会遇到这样的情况,若两个及其以上的基类中有相同的成员函数,那么它们的派生类声明的对象将调用哪个基类的函数呢?

解决的方法是使用域名进行控制,但是增加作用域分辨符虽然可以消除二义性,但显然降低了程序的可读性。同时,多继承里还有一种极端的情况,即由相同基类带来的二义性,该继承方式称为"菱形"方式,它使子类对象重叠,增加了额外的空间开销。为此,C++引入了虚基类。

二义性问题需要在基类中重新定义函数,额外的空间开销问题可采用虚基类的方式。

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

定义格式为:class 派生类名:virtual 访问权限修饰符 父类名{};

例如:虚基类使用方法举例。

cpp 复制代码
#include <iostream>
using namespace std;
class base
{
public:
	virtual const char* show() = 0;
};

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

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

class m :public d1, public d2
{
public:
	const char* show()
	{
		return d2::show();//为消除二义性,使用作用域运算符
	}
};

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

这样不仅消除了二义性,而且类m1中只有一个基类base,也节省了空间。

最终派生类

在上述例中,各类没有构造函数,使用的是默认构造函数。若类里有了带有参数的构造函数,情形将有所不同。

在派生类声明对象时,编译器报错,表示没有合适的构造函数调用。即使在基类的派生类d1,d2中也会增加对基类base的构造,情况也是如此。为了解决此类问题,可引入最终派生类(most derived class)的概念。

最终派生类(最晚辈派生类)指当前所在的累。在基类base的构造函数,base就是最终派生类;在基类d1的构造函数,d1就是最终派生类;在基类m的构造函数,m就是最终派生类。

当使用虚基类时,尤其是带有参数的构造函数的虚基类时,最终派生类的构造函数必须对虚基类初始化。不管派生类离虚基类有多远,都必须对虚基类进行初始化。

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

cpp 复制代码
#include <iostream>
using namespace std;
class base
{
	int i;
public:
	base(int x):i(x){}
	int geti()
	{
		return i;
	}
	virtual const char* show()
	{
		return "base";
	}
};

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

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

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

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

程序运行结果是:

d2
3
0
d1

使用虚基类时要注意:

  1. 必须在派生类的构造函数中调用初始化虚函数的构造函数;
  2. 给虚基类安排默认构造函数,可使虚基类的程序开发变得简单易行。

多继承的构造顺序

例如:修改上例。

cpp 复制代码
#include <iostream>
using namespace std;
class base
{
	int i;
public:
	base(int x):i(x)
	{
		cout << "base is constructed" << endl;
	}
	virtual ~base()
	{
		cout << "base is destructed" << endl;
	}
	virtual const char* show() = 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;
	}
	const char* show()
	{
		return "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;
	}
	const char* show()
	{
		return "d2";
	}
};

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

int main()
{
	m m1(5);
	return 0;
}

程序运行结果是:

base is constructed
d1 is constructed
d2 is constructed
m is constructed
m is destructed
d2 is destructed
d1 is destructed
base is destructed

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

相关推荐
唐诺1 小时前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨2 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客3 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin3 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos4 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室5 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0015 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我585 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc5 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很5 小时前
C++ 集合 list 使用
c++