首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【c++】多态

【c++】多态

作者头像
用户11972710
发布2025-12-30 19:20:40
发布2025-12-30 19:20:40
40
举报

概念

多态(Polymorphism)是面向对象编程(OOP)中的一个重要概念,指的是同一个接口或方法在不同情况下表现出不同的行为。多态性允许不同的类对同一消息做出不同的响应,增强了代码的灵活性和可扩展性。多态分为运行时多态(动态多态)和,编译时多态(静态多态),在这里我们主要介绍运行时多态。

编译时多态(静态多态)

编译时多态就是我们前边讲的函数重载和函数模板,传不同的参数就会调用不同的函数,通过参数不同来达到多种形态。函数重载和函数模板在编译时就已经确定要调用的函数,所以称为编译时多态(静态多态),实参传给形参的参数匹配是在编译时完成的。

运行时多态(动态多态)

不同于编译时多态,运行时多态在编译时还没有确认要调用函数,在要调用一个函数时,不同的对象去调用,完成的行为是不一样的。例如,同样一个叫声的函数,用Dog(狗)类型实例化出的对象调出来的是 '汪汪'。用Cat(猫)类实例化出的对象去调出来的则是’喵喵‘。

运行时多态通过不同类型的对象去调用实现多种形态,编译时多态通过参数匹配来实现多种形态。

多态的定义和实现

多态是继承关系下的类对象,去调用同一函数,产生不同的行为。

实现多态的两个必须条件

• 必须是基类的指针或者引⽤调⽤虚函数
• 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
代码语言:javascript
复制
class Person
{
public:
	virtual void buyticket()
	{
		cout << "全价买票" << endl;
	}
private:
};

class Student :public Person
{
public:
	virtual void buyticket()
	{
		cout << "七五折买票" << endl;
	}
};
void func(Person& a)
{
	a.buyticket();
}
int main()
{
	Person a;
	Student b;
	func(a);
	func(b);
	return 0;
}

如果func()中参数类型为Student:

代码语言:javascript
复制
class Person
{
public:
	virtual void buyticket()
	{
		cout << "全价买票" << endl;
	}
private:
};

class Student :public Person
{
public:
	virtual void buyticket()
	{
		cout << "七五折买票" << endl;
	}
};
void func(Student& a)
{
	a.buyticket();
}
int main()
{
	Person a;
	Student b;
	func(a);
	func(b);
	return 0;
}

如果buyticket()不是虚函数:

代码语言:javascript
复制
class Person
{
public:
	void buyticket()
	{
		cout << "全价买票" << endl;
	}
private:
};

class Student :public Person
{
public:
	virtual void buyticket()
	{
		cout << "七五折买票" << endl;
	}
};
void func(Person& a)
{
	a.buyticket();
}
int main()
{
	Person a;
	Student b;
	func(a);
	func(b);
	return 0;
}

 虚函数

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

饰。

代码语言:javascript
复制
class Person
{
public:
	void buyticket()
	{
		cout << "全价买票" << endl;
	}

};
虚函数的重写/覆盖

虚函数的重写(Override)是面向对象编程中的一个重要概念,主要用于实现多态性。当一个派生类继承自基类时,派生类可以重写基类中的虚函数,以提供自己的实现。这样,当通过基类指针或引用调用该函数时,实际执行的是派生类中的版本。

注意:派生类的虚函数前面可以不加virtual关键字,一个函数在基类中被声明为virtual,在派生类中自动成为虚函数。但是这样牺牲了代码的可读性,尽量加上。

代码语言:javascript
复制
class Person
{
public:

	virtual void buyticket()
	{
		cout << "全价买票" << endl;
	}
};

class Student:public Person
{
public:
	void buyticket()//不加virtual也是虚函数
	{
		cout << "半价买票" << endl;
	}
};



void func(Person* ptr)
{
	ptr->buyticket();
}

int main()
{
	Person a;
	Student b;
	a.buyticket();
	b.buyticket();

	return 0;
}

观察下面一段代码:

代码语言:javascript
复制
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(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

 这段程序的输出结果是什么呢?

p是指向B类型的指针,指向谁,谁调用,所以是调用B类型中的func()函数,但是这里要注意一点:默认参数的值是根据调用者的静态类型(编译时)决定的,而不是动态类型(运行时)。也就是说,编译器在编译时会根据调用者的静态类型来确定默认参数的值。val在编译时就已经确定为1,所以不会再运行时调用B的func()而改变。

所以这道题的答案时B->1。

析构函数的重写

如果基类的析构函数为虚函数,那么在派生类中,只要定义了虚函数,⽆论是否加virtual关键字,都与基类的析构函数构成重写。这样似乎和重写的定义有些不符,这是一个特殊情况,编译后析构函数的名称统⼀处理成destructor。那么,为什么要这样处理呢?如果没有虚函数重写,当我们通过基类指针或引用管理派生类成员时,析构时仅会调用基类的析构函数,会导致内存的泄露。

代码语言:javascript
复制
class Base {
public:
    ~Base() { std::cout << "Base destructor" << std::endl; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destructor" << std::endl; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 仅调用 Base 的析构函数,Derived 的析构函数不会被调用!
    return 0;
}

override和final关键字

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

代码语言:javascript
复制
class Car {
public:
	void Drive()//不给基类的Drive()加virtual
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}

加上final之后,函数不能被重写

代码语言:javascript
复制
class Car {
public:
	virtual void Drive() final
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}

重载/重写/隐藏对比

 纯虚数和抽象类

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

代码语言:javascript
复制
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

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

多态的原理

观察下面一段程序,在64位系统下,判断程序的输出结果。

代码语言:javascript
复制
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;
}

程序的结果为16

在不加virtual的情况下结果8。 这是为什么呢?

当一个类中,如果有virtual标记的函数,那么在编译时,会生成一个虚函数表,具体位置在全局数据区的只读数字段当中,会将虚函数的地址存到虚函数表中。虚函数表指针(v代表virtual,f代

表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

在64位系统中,指针为8字节,所以上面程序的结果为16。

多态时如何实现的

代码语言:javascript
复制
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;
}

几个类对象在Func()函数中都是通过ptr调用BuyTicket()函数,那么是怎么做到ptr指向不同的对象就在不同的类中调用函数的呢?编译时,生成虚函数表,在运行时会绑定具体的虚函数地址(不同的类虚函数表是不同的),然后就可以根据指向对象的不同调用不同类的虚函数。

下面是调试时上面程序三个对象的虚函数表指向的内容:

ps虚表中指向的是Person::BuyTicket

st的虚表中指向的是Student::BuyTicket

sr的虚表中指向的是Soldier::BuyTicket

动态绑定和静态绑定 

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

代码语言:javascript
复制
class Base {
public:
    void show() { cout << "Base show" << endl; }  // 非虚函数
};

class Derived : public Base {
public:
    void show() { cout << "Derived show" << endl; }  // 隐藏基类函数
};

int main() {
    Derived d;
    Base* pb = &d;
    pb->show();  // 静态绑定,调用Base::show()
    return 0;
}

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

代码语言:javascript
复制
class Base {
public:
    virtual void show() { cout << "Base show" << endl; }  // 虚函数
};

class Derived : public Base {
public:
    void show() override { cout << "Derived show" << endl; }  // 重写虚函数
};

int main() {
    Derived d;
    Base* pb = &d;
    pb->show();  // 动态绑定,调用Derived::show()
    return 0;
}

两者的区别:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概念
    • 编译时多态(静态多态)
    • 运行时多态(动态多态)
  • 多态的定义和实现
    • 实现多态的两个必须条件
      • • 必须是基类的指针或者引⽤调⽤虚函数
      • • 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
      • 虚函数的重写/覆盖
      • 析构函数的重写
    • override和final关键字
    • 重载/重写/隐藏对比
  •  纯虚数和抽象类
  • 多态的原理
    • 多态时如何实现的
    • 动态绑定和静态绑定 
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档