C++多态详解

C++学习总结专栏:http://t.csdn.cn/xc7uG
Linux学习总结专栏:http://t.csdn.cn/ahk32

目录

  • 多态的概念
  • 多态的构成条件
  • 虚函数
  • 虚函数的重写
  • 两大关键字—override、final
  • 抽象类
  • 重载、覆盖(重写)、隐藏(重定义)的对比
  • 多态实现原理—虚函数表
  • 总结


多态的概念

多态是面向对象编程中的一个重要概念,它描述了不同对象对同一消息做出不同响应的能力。通俗来说,多态就是一个对象具有多种形态或表现方式。

具体来说,当一个对象调用某个方法时,不同的对象可能会根据自身的特性和类型,以不同的方式去完成这个行为。这意味着相同的方法调用可以产生不同的结果,因为不同的对象可能会根据自己的实现逻辑来响应这个方法调用。

例如,假设有一个动物类,它有一个名为"叫声"的方法。派生出的子类狗和猫分别继承了动物类,并重写了"叫声"方法。当调用狗对象的"叫声"方法时,它会输出"汪汪汪";而当调用猫对象的"叫声"方法时,它会输出"喵喵喵"。尽管调用的是相同的方法,但不同的对象表现出了不同的状态。

多态性使得程序更加灵活和可扩展,因为可以通过添加新的子类来扩展原有的类,并且不需要修改原有的代码。这样可以提高代码的可维护性和可复用性。

多态的构成条件

在C++中,多态的构成条件包括以下几个方面:

  • 继承关系:多态的实现需要通过类的继承关系来实现。子类继承自父类,可以通过父类的指针或引用来引用子类对象。

  • 虚函数:父类中声明的虚函数可以被子类重写,通过在父类中使用virtual关键字声明函数为虚函数,子类可以根据自身的需求重新实现该函数。

  • 动态绑定:多态需要在运行时动态绑定函数调用。当通过父类指针或引用调用虚函数时,实际调用的是根据对象类型确定的子类函数。

  • 使用基类指针或引用:要实现多态,需要使用基类的指针或引用来引用子类对象。这样可以通过统一的接口来调用不同子类的实现。

总结起来,多态的构成条件是通过继承关系,使用虚函数和基类指针或引用来实现动态绑定,从而实现根据对象类型调用不同子类的函数实现。

示例代码:

#include 

class person
{
public:
	virtual void action()
	{
		std::cout << "我是人" << std::endl;
	}

};

class student : public person
{
public:
	void action()
	{
		std::cout << "我是学生" << std::endl;
	}
};

class teacher : public person
{
public:
	void action()
	{
		std::cout << "我是老师" << std::endl;
	}
};

int main()
{
	student st;
	teacher tc;
	person ps;

	person& ps1 = tc;
	person& ps2 = st;

	ps1.action();
	ps2.action();

	std::cout << "------------" << std::endl;
	person* ps3 = &tc;
	person* ps4 = &st;

	ps3->action();
	ps4->action();

	ps.action();
	return 0;
}

运行结果:

C++多态详解_第1张图片

虚函数

在C++中,多态的实现依赖于虚函数。虚函数是在基类中声明为虚拟的成员函数,在派生类中可以进行重写。通过使用虚函数,可以在运行时根据对象的实际类型来调用相应的函数。

当基类指针或引用指向派生类对象时,通过调用虚函数,可以实现多态性。具体来说,当通过基类指针或引用调用虚函数时,会根据指针或引用所指向的对象的实际类型来确定调用哪个类的虚函数。

这样,即使使用基类指针或引用来调用虚函数,也可以根据对象的实际类型来执行相应的派生类函数,实现多态性。

虚函数的重写

重写(Override)是指子类重新定义了与父类同名的虚函数。当通过基类的指针或引用调用虚函数时,会根据实际指向的对象类型来决定调用哪个类的虚函数。这样可以实现在不同的对象上调用同一个虚函数时产生不同的行为。

重写虚函数的条件包括:

  • 基类中的函数必须是虚函数,即在函数声明前加上关键字"virtual"。

  • 子类中的函数必须有与基类中的虚函数相同的名称、参数列表和返回类型。

  • 子类中的函数必须也被声明为虚函数,可以使用关键字"virtual"进行声明,但不是必须的。

多态的构成条件是继承和重写,而C++中通过虚函数来实现多态,重写虚函数是实现多态的关键。


虚函数重写的两个例外:

1.协变

协变是指派生类重写基类虚函数时,返回值类型可以是基类函数返回值类型的派生类型。具体来说,如果基类的虚函数返回一个指向基类对象的指针或引用,派生类可以重写该虚函数并返回一个指向派生类对象的指针或引用,这种情况下称为协变。
示例代码:

class Animal 
{
public:
 virtual std::string getName() 
{
 return "Animal";
}
};

class Dog : public Animal 
{
public:
  virtual std::string getName() override 
 {
  return "Dog";
 }
};

2.析构函数的重写
如果基类的析构函数为虚函数,那么派生类的析构函数无论是否加上virtual关键字,都会与基类的析构函数构成重写。尽管基类和派生类的析构函数名称不同,但它们在语义上是相同的。这是因为编译器会对析构函数的名称进行特殊处理,将它们统一处理为"destructor"。因此,即使函数名不相同,仍然符合重写的规则。
示例代码:

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

class student : public person
{
public:
	~student() override
	{
		cout << "~student()" << endl;
	}
};

int main()
{
	person* ps1 = new person;
	person* ps2 = new student;

	delete ps1;
	delete ps2;
	return 0;
}

运行结果:
C++多态详解_第2张图片

两大关键字—override、final

在定义多态时我们可以发现,C++对函数重写的要求比较严格。然而,有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载。这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果时才会进行调试。这样的调试过程可能会耗费大量时间和精力。因此,C++11提供了override和final两个关键字,可以帮助用户检测是否重写。这两个关键字可以帮助程序员在编译期间发现重写错误,从而避免不必要的调试过程。

  1. final:修饰虚函数,表示该虚函数不能再被重写

当我们在基类中使用final修饰某个虚函数时,任何派生类都无法对该虚函数进行重写或覆盖操作。这种机制可以防止派生类修改基类中已经定义的行为,从而确保程序的正确性和稳定性。使用final关键字可以在设计上限制虚函数的修改,使代码更加可靠和可维护。

示例代码:

class person
{
public:
	virtual void action() final
	{
		cout << "我不能被重写" << endl;
	}
};

class student : public person
{
public:
	void action()
	{
		cout << "我就要重写你" << endl;
	}
};

C++多态详解_第3张图片

在这个例子中,我们定义了一个基类Person,其中的action()函数被声明为虚函数并使用final关键字修饰,表示该函数不能再被派生类重写。

然后我们派生了Student类,并尝试重写action()函数。由于action()函数在基类中被final修饰,所以在派生类中无法重写该函数。


  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

当我们在派生类中使用override关键字修饰某个虚函数时,编译器会检查该函数是否与基类中的虚函数具有相同的签名。如果派生类中的虚函数没有成功重写基类的虚函数,编译器会报错。

示例代码:

class person
{
public:
	virtual void action() 
	{
		cout << "我不能被重写" << endl;
	}
};

class student : public person
{
public:
	int action() override
	{
		cout << "我就要重写你" << endl;
	}
};

C++多态详解_第4张图片

在代码中,我们用override修饰action函数必须被重写,但是在派生类中action函数的返回值不一样,所以不能构成重写因此报错。

抽象类

在C++中,如果在虚函数的后面写上= 0,则该虚函数被称为纯虚函数。包含纯虚函数的类被称为抽象类,也可以称为接口类。抽象类不能被实例化为对象,派生类继承抽象类后也不能实例化为对象。只有在派生类中重写了纯虚函数,派生类才能被实例化为对象。

纯虚函数的存在规范了派生类必须重写该函数,否则派生类仍然被视为抽象类,不能实例化为对象。通过纯虚函数,我们可以定义一个接口,指定了派生类必须实现的方法。

示例代码:

class Shape {
public:
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    void draw() const override {
        cout << "绘制圆形" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        cout << "绘制矩形" << endl;
    }
};

int main() {
    // 编译错误:无法实例化抽象类Shape
    // Shape类包含纯虚函数draw(),不能被实例化为对象
    Shape* shape = new Shape();

    Shape* circle = new Circle();
    circle->draw(); // Output: 绘制圆形

    Shape* rectangle = new Rectangle();
    rectangle->draw(); // Output: 绘制矩形

    delete circle;
    delete rectangle;

    return 0;
}

C++多态详解_第5张图片

重载、覆盖(重写)、隐藏(重定义)的对比

重载、覆盖(重写)和隐藏是面向对象编程中常用的三个概念,用于描述不同方式的函数或方法的关系。

  1. 重载:
  • 定义:在同一个作用域内,可以有多个同名函数,但它们的参数列表不同(参数个数、类型或顺序不同)。
  • 特点:重载函数具有相同的函数名,但是参数列表不同,编译器根据调用时提供的参数类型和数量来确定调用哪个重载函数。
  • 作用:提供了一种方便的方式,可以使用相同的函数名进行不同的操作。
  1. 覆盖:
  • 定义:派生类中重新定义基类中的虚函数,使得在派生类对象上调用虚函数时,执行的是派生类中的实现。
  • 特点:覆盖函数具有相同的函数名、参数列表和返回类型,且在基类中该函数必须为虚函数。
  • 作用:允许派生类根据自身的需求来重新定义基类中的虚函数,实现多态性。
  1. 隐藏(Hiding):
  • 定义:派生类中定义了与基类中同名的非虚函数,隐藏了基类中的同名函数。
  • 特点:隐藏函数具有相同的函数名,但是参数列表不同,或者基类中的同名函数不是虚函数。
  • 作用:当派生类对象通过派生类类型访问同名函数时,会调用派生类中的函数,而不会调用基类中的同名函数。

总结:

  1. 重载函数是在同一个作用域内,根据参数列表的不同来区分同名函数。
  2. 覆盖函数是在派生类中重新定义基类中的虚函数,使得在派生类对象上调用虚函数时执行派生类中的实现。
  3. 隐藏函数是在派生类中定义了与基类中同名的非虚函数,隐藏了基类中的同名函数。

这些概念都是为了实现多态性和灵活的代码设计,根据需求选择合适的方式来处理函数的重用和扩展。

多态实现原理—虚函数表

虚函数表(Virtual Function Table,简称vtable)是C++中用于实现多态的一种机制。每个定义了虚函数的类都有一个对应的虚函数表,用于存储该类的虚函数的地址。

虚函数表是一个由函数指针构成的数组,每个函数指针指向一个虚函数的实现。当一个类中声明了虚函数时,编译器会在虚函数表中添加一个对应的函数指针。子类会继承父类的虚函数表,并在需要重写父类的虚函数时,将对应的函数指针替换为子类的实现。

通过虚函数表,可以实现动态绑定(Dynamic Binding)的特性,即在运行时根据对象的实际类型来调用对应的虚函数。这样可以实现基类指针或引用指向派生类对象时,根据对象的实际类型来调用相应的函数。

虚函数表的实现方式可能因编译器和操作系统的不同而有所差异,但基本原理是相似的。在大多数实现中,虚函数表是一个位于对象内存布局的固定位置的指针数组,通过对象的指针或引用可以直接访问到虚函数表,并通过表中的函数指针来调用虚函数。

示例代码:

#include 
using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}
private:
	int _b = 1;
};

class Driver : public Base
{
public:
	void Func1() override
	{
		cout << "重写Fuunc1" << endl;
	}
private:
	int _d = 1;
};

int main()
{
	Base bs;
	Driver dv;
	Base* b = &dv;
	b->Func1();
	bs.Func1();
	cout << sizeof(bs) << endl;
	
	return 0;
}

运行结果:

C++多态详解_第6张图片

C++多态详解_第7张图片

通过观察和调试,我们可以发现:

  1. 派生类对象中也有一个虚表指针,派生类对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是在部分,另一部分是自己的成员。
  2. 基类对象和派生类对象虚表是不一样的,这里我们发现Func1完成了重写,派生类对象的虚表中存的是重写的Driver::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 只要该类中有虚函数,那么指向该虚函数的指针就会存放到虚表中。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

派生类的虚表生成过程如下:

a. 首先将基类中的虚表内容拷贝一份到派生类的虚表中。
b. 如果派生类重写了基类中的某个虚函数,就用派生类自己的虚函数覆盖虚表中基类的虚函数。
c. 派生类自己新增加的虚函数按照在派生类中的声明顺序增加到派生类虚表的最后。

这个过程确保了派生类的虚表中包含了从基类继承的虚函数,并且派生类自己的重写的虚函数和新增加的虚函数也被正确地放置在虚表中。这样,通过派生类对象调用虚函数时,会根据对象的实际类型查找虚表,并根据虚表中的函数指针调用相应的虚函数。这就实现了多态性,允许在运行时根据对象的实际类型来决定调用哪个虚函数。


注意:

  • 虚函数是存在代码段中的,就像普通函数一样。每个虚函数都有一个地址,表示它在代码段中的位置。

  • 虚表是一个存储虚函数指针的指针数组。每个对象中都有一个指向其对应类的虚表的指针。这个指针通常被称为虚表指针(vptr)。

  • 虚表指针在对象的内存布局中的位置是由编译器决定的,通常是作为对象的第一个成员。通过虚表指针,对象可以找到其对应类的虚表,并通过虚表中的函数指针调用相应的虚函数。

  • 在Visual Studio(VS)下,虚表通常是存在代码段中的。

  • 虚表本身不是存在对象中的,而是在编译阶段由编译器生成,并在运行时被使用。它的位置和结构由编译器决定,通常是在代码段中,但具体的存储位置可能会因编译器和操作系统的不同而有所变化。

  • 需要注意的是,虚表的实际实现细节可能会因编译器和操作系统的不同而有所差异。以上是一种常见的实现方式,但并不保证适用于所有情况。

总结

本文介绍了C++中多态的一些使用特性和注意点。多态是面向对象编程中的重要概念,它允许在运行时根据对象的实际类型来决定调用哪个虚函数,实现了动态绑定和多态性。

在文章中,我们首先介绍了虚函数和虚表的概念。虚函数是在基类中声明为虚函数的函数,它可以被派生类重写。而虚表是一个存储虚函数指针的指针数组,每个类都有一个对应的虚表。

接着,我们讨论了派生类的虚表生成过程。派生类的虚表会先将基类的虚表内容拷贝一份,然后根据派生类的重写和新增的虚函数进行覆盖和添加,最终形成派生类自己的虚表。

我们还提到了虚表的存储位置。虚表本身是在编译阶段由编译器生成的,它通常存放在代码段中。对象中存储的是虚表指针,通过虚表指针可以找到对应类的虚表,并通过虚表中的函数指针调用相应的虚函数。

最后,我们强调了多态的重要性和优势,它使得代码更加灵活和可扩展,能够提高代码的复用性和可维护性。同时,我们也需要注意多态的一些细节,如虚函数的声明和实现要一致,虚函数不能是静态成员函数,虚函数的调用会有一定的性能开销等。

总的来说,多态是C++中面向对象编程的重要特性之一,它通过虚函数和虚表的机制实现了动态绑定和多态性。了解和正确使用多态可以使我们的代码更加灵活和可扩展,提高代码的复用性和可维护性。


补充:

虚函数不能是静态函数的原因是因为静态函数是与类绑定的,而不是与对象实例绑定的。虚函数的实现是通过对象的虚表来实现的,而静态函数是属于类本身的,不需要通过对象来调用。

虚函数的调用是根据对象的实际类型来确定的,而静态函数是在编译时就确定的,不会根据对象的实际类型来动态绑定。因此,如果将虚函数声明为静态函数,就无法实现多态性和动态绑定的特性,失去了使用虚函数的意义。

另外,静态函数是在类的作用域中直接调用的,而虚函数是通过对象的指针或引用来调用的。静态函数没有this指针,无法访问对象的成员变量和非静态成员函数,而虚函数可以访问对象的成员变量和非静态成员函数。

综上所述,虚函数不能是静态函数是因为静态函数与对象实例无关,无法实现多态性和动态绑定的特性,并且无法访问对象的成员变量和非静态成员函数。

C++多态详解_第8张图片

你可能感兴趣的:(C++,c++,开发语言)