【C++基础】C++多态相关概念

C++多态总结

多态是面向对象编程领域的核心概念之一,那么什么是多态呢?

多态一词最初来源于希腊语,意思是具有多种形式和多种形态的情形,在C++中是指同样的消息被不同的对象接受时导致不同的行为,这里的消息就是指对象的成员函数的调用,不同的行为指的是不同的实现。

多态性可以简单的概括为“一个接口,多个方法”,程序在运行时才决定调用哪个函数。

C++的多态是用虚函数来实现的,下面介绍一下虚函数:

虚函数

类的成员函数中被virtual关键字修饰的函数是虚函数。

class Person{
public:
	void BuyTickets() {
		cout << "买票 —— 全价" << endl;
	}
protected:
	int _id;
};

如上代码,类Person中的BuyTickets()函数就是一个虚函数,那么它有什么用,以及如何实现多态呢?我们先看一个概念

虚函数重写

当在子类中定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(覆盖)了父类的这个函数,举个栗子

class Person{
public:
	virtual void BuyTickets() {
		cout << "买票 —— 全价" << endl;
	}
protected:
	int _id;
};

class Student : public Person {
public:
	virtual void BuyTickets() {
		cout << "买票 —— 半价" << endl;
	}
protected:
	//学号
	int _num;
};

Person类为基类,Student类为派生类,基类和派生类都具有虚函数BuyTickets,这时候就构成重写(覆盖),那么重写有什么作用?或者说它是如何支持多态的呢?

多态是,当虚函数重写时,使用基类的指针或引用调用重写的虚函数时,当指针指向父类时调用的就是父类的虚函数,指向子类时调用的就是子类的虚函数。

测试代码:

Student s;
Person* p2 = &s;
Person& p3 = s;
p2->BuyTickets();
p3.BuyTickets();

p2和p3虽然是父类对象的指针和引用,但是调用的却是子类对象的虚函数

这就是通过虚函数重写实现的多态,可以概括为,指向谁调用谁

纯虚函数

在普通的虚函数形参后面写上=0,则该虚函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类)

虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

再来举个栗子:

class Person {
public:
	virtual void Display() = 0;
protected:
	string _name;
};

Person类就是一个抽象类,它的Display函数是个纯虚函数

抽象类不能实例化出对象

我们试着去创建一个对象:


我们发现,抽象类是不能实例化出对象的,因为Display函数是纯虚函数

纯虚函数在派生类中重新定义以后,派生类才能实例化出对象

我们再定义一个派生类:

class Student : public Person {
protected:
	//学号
	string _num;
};

派生类没有Display函数进行了重新定义,我们试着实例化一个对象:

发现不能实例化出对象,因为纯虚函数没有强制替代项,所以我们在派生类中再定义一个Display函数

class Student : public Person {
public:
    virtual void Display() {
        cout << _num << endl;
    }
    //学号
    string _num;
};

为了方便初始化_num而我又不想写构造函数,我把_num也定义为公有成员变量,现在我重新定义了虚函数Display,我们再次进行实例化:

	Student s;
	s._num = "15060204111";
	s.Display();

	Person* p = &s;
	p->Display();

运行结果如下:

我们发现派生类可以实例化出对象而且实现了多态性

纯虚函数的引入原因

1.为了方便使用多态特性,我们常常需要在基类中定义虚函数

2.在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理

虚函数总结

接下来详细总结一下虚函数的几个要点:

1.派生类虚函数重写实现多态,要求虚函数的函数名,参数列表,返回值完全相同(协变除外)

我们知道,如果要实现多态,要求虚函数的函数名,参数列表返回值完全相同,但是协变除外,那么协变是什么呢?

在C++中,对于虚函数而言,只要原本基类的返回值类型试试基类的指针或引用,新的返回值类型是派生类的指针或引用,覆盖的方法就可以改变返回类型,这样的返回类型称为协变返回类型

举个大栗子:

class Person{
public:
	Person()
		:_id(00001)
	{}

	virtual Person& operator=(const Person& p) {
		cout << "virtual Person& operator=(const Person& p)" << endl;
		return *this;
	}
	virtual void BuyTickets() {
		cout << "买票 —— 全价" << endl;
	}
protected:
	int _id;
};

class Student : public Person {
public:
	Student()
		:_num(11111)
	{}

	virtual Student& operator=(const Person& p) {
		cout << "virtual Student& operator=(const Student& p)" << endl;
		return *this;
	}

	virtual void BuyTickets() {
		cout << "买票 —— 半价" << endl;
	}
protected:
	//学号
	int _num;
};

还是上面的两个类,operator=即赋值运算符重载函数,它加了virtual关键字,但是它的返回值是不同的,一个是父类的引用,一个是子类的引用,返回值不同,本来是不能构成虚函数重写的,但是它现在是协变,那么还能有多态性吗?我们测试一下:

Student s1, s2;
Person* p1 = &s1;
Person& p2 = s2;
	
p1->operator=(s2);
p2.operator=(s1);

如图,我们发现虽然返回值不同,但是还是具有了多态性,因为产生了协变。

协变返回类型的优势在于,总是可以在适当程度的抽象层面工作,目前一般认为返回值可以协变,但是参数不可以

2.基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性

可以理解为,一旦一个函数定义为虚函数,无论它传下去多少层,一直保持为虚函数。

只要基类定义了虚函数,那派生类的虚函数可以不加virtual,举个栗子,我们把上面的类Student中的operator=函数改为这样:

Student& operator=(const Person& s) {
	cout << "virtual Student& operator=(const Student& p)" << endl;
	return *this;
}

我们去掉virtual关键字,然后再运行,结果如下:

我们发现,这样还是实现了多态性

3.只有类的成员函数才能被定义为虚函数

这个就不用解释了吧,虚函数仅适用于有继承关系的类对象,所以普通函数不能为虚函数

4.静态成员不能被定义为虚函数

静态成员的特点就是不限制于某个对象,它是属于所有对象共有的

5.如果在类外定义虚函数,只能在声明函数时加上virtual,类外定义函数时不能加virtual关键字

6.构造函数不能为虚函数,最好不要把operator=定义为虚函数

构造函数构造对象的时候,对象还没被定义出来,只有构造完成后对象才是具体类的是实例

7.内联(inline)函数不能是虚函数

因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。

8.不要在构造函数和析构函数里面调用虚函数

在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。

9.最好把基类的析构函数声明为虚函数

这个得解释一下,我们举个例子:

class Person {
public:
	Person() {
		cout << "Person()" << endl;
	}
	Person(const Person& p) {
		cout << "Person(const Person& p)" << endl;
	}
	~Person() {
		cout << "~Person()" << endl;
	}
};

class Student : public Person{
public:
	Student() {
		cout << "Student()" << endl;
	}
	Student(const Student& s) {
		cout << "Student(const Student& s)" << endl;
	}
	~Student() {
		cout << "~Student" << endl;
	}
};

一个普通的公有继承,他们的析构函数不是虚析构函数,我们看个测试用例:

Student* s = new Student();
delete s;
不管析构函数是不是虚析构函数,delete时基类和子类都会被释放,都会调用子类和父类的析构函数,运行结果如下:


如果是这种情况:

Person* p = new Student();
delete p;	

父类的指针指向子类的对象,如果不是多态的话,那么这里就会只会释放父类,调用父类的析构函数,运行结果如下:

注意,这样可能会造成内存泄露,所以我们应该让析构函数变为虚析构函数,代码如下:

class Person {
public:
	Person() {
		cout << "Person()" << endl;
	}
	virtual ~Person() {
		cout << "~Person()" << endl;
	}
};

class Student : public Person{
public:
	Student() {
		cout << "Student()" << endl;
	}
	virtual ~Student() {
		cout << "~Student" << endl;
	}
};
还是刚才的测试用例,当析构函数变为虚析构函数之后,delete时子类和基类都被释放

重载、重写和重定义的区别

在继承时学习了重定义,在多态时了解了重写的意思,C++还有重载,在这里列一个表:

继承体系同名成员的关系

作用域

                                                    其他不同点

重载

在同一作用域

                函数名相同(参数不同)

                            返回值可以不同

重写(覆盖)

在基类和派生类

函数名/参数/返回值相同(协变除外)

基类函数必须有virtual关键字

访问修饰符可以不同

重定义(隐藏)

在基类和派生类

                                                    函数名相同

动态联编和静态联编

将源代码中的函数调用解释为执行特定的函数代码的行为成为联编(binding)

·在编译过程中进行联编被称为静态联编,又称为早期联编

·编译器在程序运行时选择正确的虚方法的代码,这种行为被称为动态联编,又称为晚期联编

静态多态和动态多态

我们前面也说过,多态就是多种形态,C++的多塔分为静态多态和动态多态

·静态多态就是重载,因为是在编译期间决定,所以称为静态多态

·动态多态就是在派生类中重写基类的虚函数来实现多态,因为这个是在运行时确定调用哪个,所以称为动态多态

为了使程序能在运行时决策,必须一些方法跟踪基类指针或引用指向的对象类型,这会增加额外的开销,所以因为静态联编的效率更高,所以被设置为C++的默认选择

多态的总结就到这里,关于多态的底层实现和各种继承下的多态对象模型,准备再开一篇博客,同一篇写的太多显得很臃肿

链接如下:C++多态原理和多态的对象模型



版权声明:本文为Qregi原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。