C++学习之多态

        前面我们已经学习了C++的两大重要特性:封装和继承。接下来我们将接着来学习C++最后一个重要的特性:多态

        作者的个人gitee:楼田莉子 (riko-lou-tian) - Gitee.com喜欢可以支持一下谢谢

目录

多态

        概念

多态的定义和实现

        多态的构成条件

        实现多态的必要条件

        虚函数

        虚函数的重写和覆盖

        虚函数重写的其他问题

        协变(了解)

        析构函数重写

        override 和 final关键字

        重载\重写\隐藏的对比

关键区别详解:

纯虚函数和抽象类

多态的原理

        虚函数的表指针

       多态是如何实现的

        动态绑定和静态绑定   

虚函数表

        总体介绍

        单继承虚函数表

        多继承虚函数表


多态

        概念

        多态(polymorphism)的概念:通俗来说,就是多种形态。

        多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。

        编译时多态(静态多态)主要就是函数重载和函数模板他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态。之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。

        运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是”(>^ω^<)喵“,传狗对象过去,就是"汪汪"。

多态的定义和实现

        多态的构成条件

        多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了Person。Person对象买票全价,Student对象优惠买票。


        实现多态的必要条件

        1、必须是基类的指针或者引用调用虚函数

        2、被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

        说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到。


        虚函数

        类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。

class Person
{ 
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl;}
}

        虚函数的重写和覆盖

        虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。

        注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

        满足多态的条件下,多态是利用基类的壳来进行重写实现,比如下面两种代码写法是不影响的(将public替换为private)

//运行时多态演示1
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}
//运行时多态演示1

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
//将public替换为private
	private:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

        但是这样不行

        例题:

        以下程序输出结果是什么()B

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确     

        

//例题
class A
{
	public :
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
	B* p = new B;
	p->test();
	return 0;
}

        继承不是把基类成员拷贝到派生类中,而是先找基类然后开辟自己的成员。就像下图这样

        调用函数也是一样。派生类成员调用函数先去派生类里面去找,找不到了再去基类里去找。

        上图所示的代码中,p指针调用的是A的this指针。多态与缺省值、变量名字没有任何关系,因此func()函数构成多态,重写虚函数的实现。如果不构成多态调用的就是A的func函数,构成多态指向B,因为p是B的对象,将B对象的指针传给this。因此调用的逻辑是这样的。

        而当题目变成这样的时候答案为D

//例题
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
	B* p = new B;
	p->func();
	return 0;
}

        因为这次调用不满足多态的条件,因为func的调用不符合堕胎的条件

        多态是利用基类的虚函数声明+派生类重写的虚函数实现

        虚函数重写的其他问题

        协变(了解)

        派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。

//协变
class A {};
class B : public A {};
class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

        析构函数重写

        基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

        下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。

//析构函数重写
class A
{
	public :
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A 
{
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派?类Student的析构函数重写了Person的析构函数,
//下?的delete对象调?析构函数,才能构成多态,
// 才能保证p1和p2指向的对象正确的调?析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	//多态调用,指向谁调用谁的析构函数
	// 构成隐藏关系
	//p1->destructor()+operator delete(p1)
	delete p1;
	delete p2;
	return 0;
}

        注意:这个问题⾯试中经常考察,结合类似下⾯的样例才能讲清楚,为什么基类中的析构函数建议设计为虚函数。

        上图所示的代码中,如果基类析构函数不是虚函数,执行delete p2的时候就会根据指针类型执行A的析构函数,导致p2未释放,导致内存泄漏。

        override 和 final关键字
 

        由上面的内容我们发现,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助⽤⼾检测是否重写。

        如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。

//override 和 final关键字
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A
{
public:
	virtual ~B() override
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派?类Student的析构函数重写了Person的析构函数,
//下?的delete对象调?析构函数,才能构成多态,
// 才能保证p1和p2指向的对象正确的调?析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	//多态调用,指向谁调用谁的析构函数
	// 构成隐藏关系
	//p1->destructor()+operator delete(p1)
	delete p1;
	delete p2;
	return 0;
}

        如果上面的代码中基类的析构函数没有虚函数,那么会报错


        final关键字的例子

//final关键字
// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{
	public :
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{

	return 0;
}

        重载\重写\隐藏的对比

        总结:

      下面详细了解:

对比维度重载 (Overload)重写 (Override)隐藏 (Hide)
定义同一作用域内同名函数参数列表不同派生类重新定义基类虚函数派生类定义与基类同名函数,屏蔽基类版本
作用域同一作用域(同一类或命名空间)不同作用域(基类和派生类之间)不同作用域(基类和派生类之间)
函数签名要求函数名相同,参数列表必须不同函数名、参数列表、返回类型必须相同函数名相同,参数列表可同可不同
virtual关键字不要求基类必须声明为虚函数(virtual不要求(非虚函数)
多态类型静态多态(编译时绑定)动态多态(运行时绑定)静态多态(编译时绑定)
目的提供功能相似但参数不同的接口实现运行时多态,定制派生类行为在派生类中完全替换基类实现
调用决定方式根据实参类型和数量在编译时决定根据对象实际类型在运行时决定根据指针/引用类型在编译时决定
继承关系不需要必须有继承关系必须有继承关系
const修饰符可作为重载依据必须严格匹配const属性可作为隐藏依据
返回类型可不同(但通常建议相同)必须相同(协变返回类型除外)可不同
代码示例cpp<br>void print(int);<br>void print(double);<br>cpp<br>Base: virtual void func()<br>Derived: void func() override<br>cpp<br>Base: void func()<br>Derived: void func()<br>

关键区别详解:

  1. 重载 (Overload)

    • 发生在同一作用域(如类内部或全局作用域)

    • 编译器通过参数列表差异区分函数

    • 示例:构造函数重载、工具函数多版本

  2. 重写 (Override)

    • 必须满足三大条件:

      1. 基类函数为 virtual

      2. 函数签名完全一致

      3. 继承关系存在

    • 实现运行时多态的核心机制

    • 示例:多态析构函数、接口实现

  3. 隐藏 (Hide)

    • 派生类定义同名函数时自动发生

    • 两种形式:

      1. 同名同参:隐藏基类非虚函数

      2. 同名不同参:隐藏基类所有同名函数

    • 可通过 using Base::func; 解除隐藏

    • 重载函数编译的时候方便查找,重载的函数名进行修饰处理。尽管各个编译器规则不同,但都会把形参类型带入到函数名字

示例:

class Base 
{
public:
    virtual void vfunc() {}    // 虚函数
    void hfunc(int) {}         // 非虚函数
    void ofunc() {}            // 非虚函数
};

class Derived : public Base
{
public:
    void vfunc() override {}   // ✅ 重写虚函数(多态)
    void hfunc(double) {}      // ❗ 隐藏基类hfunc(参数不同)
    void ofunc() {}            // ❗ 隐藏基类ofunc(同名同参)
    void print(int);           // ✅ 重载1
    void print(double);        // ✅ 重载2
};

// 使用场景
int main()
{
    Derived d;
    Base* pb = &d;

    pb->vfunc();  // 动态绑定→Derived::vfunc()
    pb->hfunc(1); // 静态绑定→Base::hfunc(int) 
    //(因隐藏不会调用Derived版本)

    d.print(1.0); // 重载决议→Derived::print(double)
    return 0;
}

最佳实践

  1. 多态基类声明虚析构函数

  2. 需要重写时使用 override 关键字明确意图

  3. 避免非必要的函数隐藏(使用 using 导入基类函数)

纯虚函数和抽象类

        在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象
        

//虚函数和抽象类
//包含虚函数的抽象类Car
class Car
{
	public :
	virtual void Drive() = 0;//抽象函数,没有实现,不能实例化
	//强制了子类重写这个函数,否则无法实例化没有意义
};
//抽象类Benz继承Car
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
//抽象类BMW继承Car
class BMW :public Car
{
	public :
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
	Car car;//抽象类无法实例化出对象
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

多态的原理

        虚函数的表指针

        下⾯编译为32位程序的运⾏结果是什么()D

        A. 编译报错 B. 运⾏报错 C. 8 D. 12

//例题
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

        解析:通过调试的监视窗口我们会发现b除了两个成员之外还有一个指针_vfptr

        _vfptr就是虚函数表指针,存储虚函数的地址。本质上是一个函数指针数组

       多态是如何实现的

        在下面所示的代码中,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。
        

// 多态的实现原理
// 1. 虚函数表
// 2. 虚函数调用
// 3. 虚基类指针
// 4. 虚继承
// 5. 纯虚函数
// 6. 虚函数重载
// 7. 虚析构函数	
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
	string _name;
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
	string _id;
};
class Soldier : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
	string _codename;
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
	Person ps;
	Student st;
	Soldier sr;
	Func(&ps);
	Func(&st);
	Func(&sr);
	return 0;
}

        因此调用图如下。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

        本质上是运行时指向哪个对象就去调用哪个对象的虚函数表中找到对应的地址进行调用。

        person对象的调用

        student对象的调用

        动态绑定和静态绑定   

        对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。

        满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。

// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)

虚函数表

        总体介绍

        基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。

        派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。

        派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。

        派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。

        虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)

        虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中

        虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定。vs下是存在代码段(常量区)

        单继承虚函数表

        以下面的代码为例:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//单继承虚函数表
class Base
{
public:
	virtual void Func1() { cout << "Base::Func1()" << endl; }
	virtual void Func2() { cout << "Base::Func2()" << endl; }
protected:
	int _a;
};
class Derive : public Base
{
public:
	virtual void Func1() { cout << "Derive::Func1()" << endl; }
	virtual void Func3() { cout << "Derive::Func3()" << endl; }
	virtual void Func4() { cout << "Derive::Func4()" << endl; }
	void Func5()
	{
		cout << "Derive::Func5()" << endl;
	}
protected:
	int _b;
};
typedef void(*VFPTR)();
void PrintTable(VFPTR* vfptr)//参数为函数指针数组
//也可以这么写void PrintTable(VFPTR vfptr[])
{
	//依次打印虚函数表
	cout << "虚函数表地址:" << vfptr << endl;
	//如果在g++下面不能用nullptr去判断访问虚函数表是否结束
	for (int i = 0; vfptr[i] != nullptr; i++)
		//g++下只能写死为i<4
	{
		cout << "第" << i << "个虚函数地址>" << vfptr[i] << "->";
		VFPTR f = vfptr[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	//32位程序的访问思路如下:
	//需要注意的是如果是在64位下,指针是8byte,对应程序位置就需要进行更改
	//思路:取出b、d对象的头4bytes,就是虚表的指针,
	//前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
	//vs下这个数组最后面放了一个nullptr,g++下面最后没有nullptr
	//1.先取b的地址,强转成一个int*的指针
	//2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
	//3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	//4.虚表指针传递给PrintVTable进行打印虚表
	//5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,
	//虚函数表最后面没有放nullptr,导致越界,这是编译器的问题。
	//我们只需要点目录栏的-生成-清理解决方案
	//再编译就好了。
	VFPTR* vfptr1 = (VFPTR*)(*(int*)&b);
	PrintTable(vfptr1);
	VFPTR* vfptr2 = (VFPTR*)(*(int*)&d);
	PrintTable(vfptr2);
	return 0;
}

        多继承虚函数表

        

//多继承问题
class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2()" << endl;
	}
private:
	int _a;
};
class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2()" << endl;
	}
private:
	int _b;
};
class Derive :public Base1 , public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "Derive::func3()" << endl;
	}
private:
	int _b;
};
typedef void(*VFPTR)();
void PrintTable(VFPTR* vfptr)//参数为函数指针数组
//也可以这么写void PrintTable(VFPTR vfptr[])
{
	//依次打印虚函数表
	cout << "虚函数表地址:" << vfptr << endl;
	//如果在g++下面不能用nullptr去判断访问虚函数表是否结束
	for (int i = 0; vfptr[i] != nullptr; i++)
		//g++下只能写死为i<4
	{
		cout << "第" << i << "个虚函数地址>" << vfptr[i] << "->";
		VFPTR f = vfptr[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vfptr1 = (VFPTR*)(*(int*)&d);
	PrintTable(vfptr1);
	VFPTR* vfptr2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintTable(vfptr2);
	Base1* p1 = &d;
	p1 -> func1();
	Base2* p2 = &d;
	p2 -> func2();


	//d.func1();
	return 0;
}

       

        本期内容就到这里,喜欢请点个赞谢谢

封面图自取:

<think>我们正在讨论C++多态特性。根据引用内容,多态的实现原理主要涉及虚函数和虚函数表(vtable)。使用方法是在基类函数前加virtual关键字,在派生类中重写该函数,通过基类指针或引用调用时会根据实际对象类型调用相应函数。 首先,我们需要明确多态分为静态多态(编译时多态,如函数重载)和动态多态(运行时多态,即虚函数机制)。用户问题主要关注动态多态。 原理部分: 1. 当类中有虚函数时,编译器会为该类生成一个虚函数表(vtable),表中存放虚函数的函数指针。 2. 每个对象在创建时,会在其内存布局的开头(通常)加入一个指向虚函数表的指针(vptr)。 3. 当通过基类指针或引用调用虚函数时,程序会根据vptr找到虚函数表,然后根据函数在表中的偏移量找到实际要调用的函数地址。 使用方法: 1. 在基类中声明虚函数(使用virtual关键字)。 2. 在派生类中重写(override)该虚函数(函数签名相同)。 3. 通过基类指针或引用来指向派生类对象,并调用虚函数,此时会调用派生类中的函数。 注意:构造函数不能是虚函数,而析构函数通常声明为虚函数(特别是基类),以确保正确释放派生类资源。 下面我们用代码示例来说明: 首先,定义一个基类Animal,其中有一个虚函数speak: ```cpp #include <iostream> using namespace std; class Animal { public: virtual void speak() { cout << "Animal speaks." << endl; } // 虚析构函数,确保正确释放资源 virtual ~Animal() {} }; class Dog : public Animal { public: void speak() override { // override关键字(C++11)表示重写,可加可不加,但推荐使用 cout << "Dog barks." << endl; } }; class Cat : public Animal { public: void speak() override { cout << "Cat meows." << endl; } }; int main() { Animal* animal1 = new Dog(); Animal* animal2 = new Cat(); animal1->speak(); // 输出:Dog barks. animal2->speak(); // 输出:Cat meows. delete animal1; delete animal2; return 0; } ``` 在上面的例子中,Animal类中的speak函数被声明为虚函数。当我们通过基类指针(Animal*)调用speak时,实际调用的是指针所指向的对象的实际类型(Dog或Cat)中的speak函数。 关于虚函数表(vtable)和虚指针(vptr): 每个有虚函数的类(或从有虚函数的类派生)都有一个虚函数表,该表在编译时创建。每个对象在构造时,其虚指针被初始指向相应的虚函数表。当调用虚函数时,通过虚指针找到虚函数表,再通过偏移量找到正确的函数地址。 注意:多态只能通过指针或引用来实现,直接使用对象不会发生多态(因为对象类型在编译时确定)。 另外,引用[4]提到MFC库使用消息映射机制而不是虚函数,这是为了避免每个类都拥有虚表带来的空间开销。但在一般情况下,虚函数机制是C++实现多态的标准方式。 最后,我们总结一下: 1. 动态多态通过虚函数实现,在运行时根据对象类型确定调用的函数。 2. 虚函数表是实现动态多态的关键,每个类一个,存放虚函数地址。 3. 每个对象有一个指向虚函数表的指针(vptr)。 4. 使用基类指针或引用调用虚函数时,通过vptr找到实际类的虚函数表,从而调用正确的函数。 相关问题: 1. C++中虚函数表是如何创建的? 2. 为什么构造函数不能是虚函数? 3. 虚析构函数的作用是什么? 4. 多态在什么情况下不会发生? 5. C++11中的override和final关键字有什么作用?</think>### C++多态的实现原理及使用方法 #### 一、多态的实现原理 C++多态的核心机制是**虚函数表(vtable)** 和 **虚表指针(vptr)**,其工作原理如下: 1. **虚函数表(vtable)** 当类中包含虚函数时,编译器会为该类生成一个虚函数表: - 表中存储该类所有虚函数的入口地址 - 每个有虚函数的类拥有独立的虚函数表 - 示例:`Animal` 类的虚函数表包含 `virtual void speak()` 的地址 2. **虚表指针(vptr)** 每个对象在创建时: - 编译器隐式添加一个指向vtable的指针(vptr) - vptr通常位于对象内存布局的首部 - 大小通常为4字节(32位系统)或8字节(64位系统) 3. **动态绑定过程** 当通过基类指针/引用调用虚函数时: ```cpp Animal* animal = new Dog(); animal->speak(); // 动态绑定 ``` 执行流程: - 通过对象的vptr找到对应的虚函数表 - 在虚函数表中查找函数偏移位置 - 调用实际指向的派生类函数[^3] $$ \text{调用过程} : \text{对象} \xrightarrow{\text{vptr}} \text{vtable} \xrightarrow{\text{偏移量}} \text{实际函数地址} $$ #### 二、多态的使用方法 实现多态需要三个关键步骤: 1. **基类声明虚函数** 使用 `virtual` 关键字声明接口函数: ```cpp class Animal { public: virtual void speak() { // 虚函数声明 cout << "Animal sound" << endl; } virtual ~Animal() {} // 虚析构函数(必需!) }; ``` 2. **派生类重写虚函数** 派生类使用相同签名覆盖函数: ```cpp class Dog : public Animal { public: void speak() override { // C++11推荐使用override cout << "Woof!" << endl; } }; ``` 3. **通过基类指针/引用调用** 运行时根据实际对象类型决定调用: ```cpp int main() { Animal* animals[2]; animals[0] = new Dog(); // 派生类对象 animals[1] = new Cat(); // 另一个派生类 animals[0]->speak(); // 输出"Woof!" (动态绑定) animals[1]->speak(); // 输出"Meow!" // 必须通过基类指针删除对象 delete animals[0]; delete animals[1]; } ``` #### 三、关键注意事项 1. **虚析构函数** $$ \text{基类析构函数必须声明为virtual} $$ 避免派生类资源泄漏: ```cpp Base* obj = new Derived(); delete obj; // 若基类析构非虚,仅调用~Base() ``` 2. **override关键字(C++11)** 显式标记重写,避免隐藏错误: ```cpp class Derived : public Base { void foo() override; // 编译检查是否成功重写 }; ``` 3. **final关键字(C++11)** 禁止进一步重写: ```cpp class Base { virtual void foo() final; // 禁止派生类重写 }; ``` 4. **纯虚函数与抽象类** 定义接口规范: ```cpp class Shape { // 抽象类 public: virtual double area() = 0; // 纯虚函数 }; ``` #### 四、多态类型对比 | 类型 | 实现方式 | 绑定时机 | 示例 | |------------|---------------|----------|--------------------| | 动态多态 | 虚函数 | 运行时 | `virtual void f()` | | 静态多态 | 函数重载/模板 | 编译时 | `template <typename T>` | > ⚠️ **重要限制**:构造函数不能是虚函数,但虚构造函数可通过工厂模式模拟[^3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值