C++特性之多态

本文深入探讨C++中的多态特性,包括虚函数的定义与使用,析构函数的虚重写,C++11的override和final关键字,以及抽象类的概念与接口继承。通过实例解析多态的实现原理,如虚函数表和动态绑定,帮助理解C++的多态机制。

多态的定义及实现

通俗的来讲,就是多形态,不同的对象去完成同一个行为会有不同的状态。(比如买票的话,成人票是200,学生可以半价买票,军人可以优先买票)
在编程中,多态就是不同继承关系的类对象,去调用同一个函数,会有不同的行为。

1.多态的构成条件

  1. 必须用基类的引用或指针来调用这个虚函数
  2. 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写

在这里插入图片描述
如果不满足多态的话,调用哪个函数跟类型有关,people是哪个类型的,调用的就是哪个类型的成员函数。
如果满足多态的话,调用哪个函数跟对象有关,传的是哪个对象,调用的就是哪个的成员函数。

2.虚函数

被virtual修饰的类成员函数称为虚函数

比如下面这个函数就是虚函数

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

3.虚函数的重写(覆盖)

派生类中有一个跟基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,这样称之为派生类的虚函数重写(覆盖)了基类的虚函数

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

class Student : public Person
{
public:
	virtual void BuyTicket()
	//重写基类虚函数时,派生类也可以不加上virtual关键字,这样也构成重写,但是这样的写法不规范,不推荐使用
	//void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

void Func(Person& people)
{
	people.BuyTicket();
}

int main()
{
	Person a;
	Func(a);

	Student b;
	Func(b);
	return 0;
}
析构函数的重写

如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与积累的析构函数构成重写,即使基类与派生类的析构函数名字不同。虽然函数名+同,看似违背了重写的规则(函数名必须相同),但其实编译器对析构函数的名字进行了特殊处理,编译后析构函数的名字统一处理成了destructor
在这里插入图片描述问:析构函数能不能被定义成虚函数?
答:可以,并且最好定义成虚函数,因为在某些特殊情况下,如果不定义成虚函数构成多态的话,就不能正确的调用析构函数了,比如下面这种情况

Person* p1 = new Person;
delete p1;

Person* p2 = new Student;
delete p2;

一个基类指针new了一个派生类对象给它,如果析构函数不是虚函数从而没有构成多态的话,调用哪个析构函数就由p的类型决定了,p1p2的类型都是Person,所以就会不会调用派生类的析构函数,如下图在这里插入图片描述
如果析构函数写成虚函数构成多态的话,就能避免这个问题了,运行结果如下图在这里插入图片描述
所以在继承过程中可以不把基类的析构函数写成虚函数,但是最好把基类的析构函数写成虚函数构成多态,这样一定不会错,要不然碰到上面的问题就会发生内存泄露(直接先把基类析构清理了,派生类的还没有进行处理)

C++11中的override和final关键字

1.final:修饰虚函数或者类,表示它们不能再被继承

//修饰虚函数
class Person
{
public:
	virtual void BuyTicket() final
	{}
};

//修饰类
class Person final
{
public:
	virtual void BuyTicket() 
	{}
};

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

class Student
{
public:
	virtual void BuyTicket() override
	{}
}
重载、重写(覆盖)、重定义(隐藏)的对比

重载:在同一个作用域内,函数名相同、参数不同。
重写:两个函数分别在基类和派生类作用域中,且函数必须是虚函数,函数名、参数、返回值都必须相同(协变除外)
重定义:两个函数分别在基类和派生类的作用域中,他们的函数名相同。两个基类和派生类的同名函数不构成重写那就构成重定义。


抽象类

1.概念及定义

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写虚函数,派生类才可以实例化出对象。

在虚函数后面加上 =0,就是纯虚函数,如下

class Car
{
public:
	virtual void Drive() = 0;
};

纯虚函数规范了派生类必须重写。

2.接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现(说白了就是要用函数里面的东西)。

虚函数的继承是一种接口继承,派生类继承的是基类函数的接口,目的是为了进行重写,构成多态,继承的是接口(说白了就是要用函数接口的设计,然后用这个函数接口写不同的内容,如下图)在这里插入图片描述
所以如果不是为了实现多态,就不要把函数定义成虚函数。

多态的原理

1.虚函数表

先看下面代码

class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};

结果是8(32位下),除了_b成员,还有一个指针,这个指针是虚函数表指针。
一个含有虚函数的类中至少有有一个虚函数表指针,因为虚函数的地址要放到虚函数表中(虚函数表是一个指针数组,放虚函数指针的数组)

注意虚函数表中存的是虚函数的指针,而不是虚函数。虚函数和普通代码一样,都存在代码段(常量区),而虚表在vs编译器下也是存在代码段的。虚表是在编译时产生的。

对下列代码进行测试

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

发现以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

2.多态的原理

根据上面的一堆原理和下面的代码进行分析

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

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "半价" << endl;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person a;
	Func(a);
	
	Person b;
	Func(b);
}

简单的来说就是,p指向的是基类对象,那它就会去基类对象的虚表里找虚函数。如果指向的是派生类对象,那就是去派生类对象的虚表里找虚函数

3.动态绑定与静态绑定

静态绑定又称为前期绑定,在程序编译期间就确定了程序的行为,也成为静态多态,比如函数重载。
动态绑定又称为后期绑定,在程序运行区间,根据具体得到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

<think>嗯,用户想了解C++中的继承、多态和接口相关的用法,特别是实现和使用教程。首先,我需要回忆一下C++的这些概念。继承是面向对象编程中的一个核心概念,允许子类继承父类的属性和方法。多态则是通过虚函数实现的,使得不同子类对象可以对同一方法有不同的实现。接口在C++中通常通过纯虚函数和抽象类来实现,类似于Java中的接口。 用户提供的三个引用里提到了一些关键点。引用1提到多态性允许根据对象实际类调用不同函数版本,这涉及到虚函数表和动态绑定。引用2给出了一个具体的代码示例,展示了基类A1和派生类B1、C1中的虚函数重写。引用3指出子类可以拥有父类的非私有属性和方法,并可以扩展或重写它们。 用户可能需要一个结构化的教程,涵盖基本概念、语法、接口实现方法以及实际应用示例。首先应该解释继承的基本用法,比如公有继承、保护继承和私有继承的区别,但通常主要使用公有继承。然后是多态,需要详细说明虚函数的作用,以及如何通过基类指针或引用调用派生类的方法。接口的实现部分需要介绍纯虚函数和抽象类,因为C++没有专门的接口关键字,但可以通过纯虚类模拟接口。 接下来要考虑用户可能的疑问点。例如,虚函数表的工作原理,这在引用1和2中都有涉及,可能需要简要解释。另外,构造函数和析构函数在继承中的注意事项,特别是虚析构函数的重要性,避免资源泄漏。还有override和final关键字的用法,这些C++11引入的特性可以帮助避免错误,提高代码可读性。 实际应用场景方面,可以举例说明多态在框架设计中的用途,比如插件系统或GUI事件处理,这些地方需要统一的接口但不同的实现。性能方面,可能需要提到虚函数调用会有一定的开销,但通常可以忽略,除非在极端性能敏感的场景。 最后,生成相关问题时,需要考虑用户可能进一步想知道的内容,比如虚函数表的具体结构,多态的性能影响,设计模式中的应用,以及如何避免常见的陷阱如对象切片等。这些问题可以帮助用户深入理解并正确应用这些概念。</think>### C++继承与多态详解:接口实现与应用 #### 一、继承基础 1. **核心概念** - 子类继承父类非`private`成员属性和方法[^3] - 继承类:`public`/`protected`/`private`(常用`public`) - 代码示例: ```cpp class Animal { public: void eat() { cout << "Eating" << endl; } }; class Cat : public Animal { public: void meow() { cout << "Meow!" << endl; } }; ``` 2. **内存结构** 子类对象包含父类子对象,内存布局为:父类成员在前,子类新增成员在后[^1] #### 二、多态实现 1. **虚函数机制** - 虚函数声明:`virtual void func()` - 虚函数表(vtable)存储函数指针 - 代码示例: ```cpp class Shape { public: virtual void draw() = 0; // 纯虚函数 virtual ~Shape() {} // 虚析构函数 }; class Circle : public Shape { public: void draw() override { cout << "Drawing circle" << endl; } }; ``` 2. **多态调用** 通过基类指针/引用实现动态绑定: ```cpp Shape* shape = new Circle(); shape->draw(); // 输出"Drawing circle" delete shape; ``` #### 三、接口实现 C++通过纯虚函数实现接口: ```cpp class IPrintable { public: virtual void print() const = 0; virtual ~IPrintable() = default; }; class Document : public IPrintable { public: void print() const override { cout << "Printing document" << endl; } }; ``` #### 四、关键实践技巧 1. **override关键字** 明确表示覆盖父类虚函数,增强代码可读性: ```cpp class Derived : public Base { public: void func() override { ... } }; ``` 2. **虚析构函数** 必须为多态基类声明虚析构函数,防止内存泄漏: ```cpp class Base { public: virtual ~Base() = default; }; ``` 3. **final关键字** 禁止类被继承或方法被重写: ```cpp class FinalClass final { ... }; class Base { public: virtual void func() final { ... } }; ``` #### 五、典应用场景 1. GUI框架:不同控件统一事件处理接口 2. 插件系统:通过基类接口扩展功能 3. 算法策略:运行时选择不同算法实现
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值