[C++] 虚函数、override、final、父类对象与子类对象详解

1. 父类对象与子类对象

先看如下代码:

class Base 
{
public:
	void base_fun1() {}
	virtual void base_fun2() 
	{
		cout << "this is Base fun2()" << endl;
	}
public:
	int a_a;
protected:
	int a_b;
private:
	int a_c;
};

class Son :public Base 
{
public:
	void son_fun1() {}
	void base_fun2()//重写父类虚函数
	{
		cout << "this is Son fun2()" << endl;
	}
public:
	int b_a;
protected:
	int b_b;
private:
	int b_c;
};

int main()
{
	Base* ba = new Son();//父类指针指向子类

	//父类指针只能访问从父类继承而来的成员变量和成员函数
	//只能访问从父类继承而来的成员变量
	ba->a_a = 10;
	//只能访问从父类继承过来的成员函数
	ba->base_fun1();
	//由于子类对象用的是从父类继承而来的虚函数表,所以父类指针可以通过查找父类虚函数表的方式,调用被子类重写后的函数
	ba->base_fun2();

	//子类指针对所有的成员变量和成员函数随意访问(受访问权限限制)
	Son* so = new Son();//子类指针指向子类
	so->a_a = 10;
	so->b_a = 10;
	so->base_fun1();
	so->son_fun1();
	so->base_fun2();
	so->Base::base_fun2();

	return 0;
}

分析:

  • so是子类指针,指向子类对象,可以通过so访问任何子类的东西。前提是在遵守访问权限限制的情况下。
  • ba是父类指针,指向子类对象,但是只能通过ba访问继承过来的成员变量和成员函数。也就是说,不能通过指向子类对象的的父类指针访问子类对象的本身的成员变量和成员函数。
  • 父类指针ba,可以访问子类对父类虚函数重写的函数。因为子类和父类共用一个虚函数表,子类用的是从父类继承过来的虚函数表,父类指针也只是通过父类的虚函数表查找函数地址,然后调用,之所以可以通过父类指针访问子类重写父类虚函数后的函数,根本上还是因为子类用的还是父类的虚函数表。

2. 父类指针可以指向子类对象

父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)

#include <iostream>
using namespace std;

class Person
{
public:
	int m_age;
};

class Student : public Person
{
public:
	int m_score;
};

int main()
{
	Person* p = new Student(); // 父类指针可以指向子类对象,只看左边定义的指针类型
	p->m_age = 10;
	//p->m_score = 100; // 报错,Person类中没有m_score这个成员变量

	return 0;
}

3. 子类指针不可以指向父类对象

子类指针不可以指向父类对象,是不安全的。

#include <iostream>
using namespace std;

class Person
{
public:
	int m_age;
};

class Student : public Person
{
public:
	int m_score;
};

int main()
{
	//Student* p = new Person(); // 报错,子类指针不可以指向父类对象

	Student* p = (Student*) new Person(); // 强制类型转换
	p->m_age = 10;
	p->m_score = 100; // 不报错,但不安全

	return 0;
}

4. 类型兼容原则

类型兼容性原则 : C++ 的 " 类型兼容性原则 “ 又称为 ” 赋值兼容性原则 " ;

子类代替父类 : 需要 基类 ( 父类 ) 对象的 地方 , 都可以使用 " 公有继承 " 的 派生类 ( 子类 ) 对象 替代 , 该 派生类 ( 子类 ) 得到了 除 构造函数 和 析构函数 之外的 所有 成员变量 和 成员方法 ;

功能完整性 : " 公有继承 " 的 派生类 ( 子类 ) 本质上 具有 基类 ( 父类 ) 的 完整功能 , 使用 基类 可以解决的问题 , 使用 公有继承派生类 都能解决 ;

特别注意 : " 保护继承 " 和 " 私有继承 " 的 派生类 , 是 不具有 基类 的 完整功能的 , 因为 最终继承 后的派生类 , 无法在 类外部调用 父类的 公有成员 和 保护成员 ;

" 类型兼容性原则 " 应用场景 :

  • 直接使用 : 使用 子类对象 作为 父类对象 使用 ;
  • 赋值 : 将 子类对象 赋值给 父类对象 ;
  • 初始化 : 使用 子类对象 为 父类对象 初始化 ;
  • 指针 : 父类指针 指向 子类对象 , 父类指针 值为 子类对象 在 堆内存 的地址 , 也就是 将 子类对象 地址 赋值给 父类类型指针 ;
  • 引用 : 父类引用 引用 子类对象 , 将 子类对象 赋值给 父类类型的引用 ;
#include "iostream"
using namespace std;

class Parent {
public:
    void funParent()
    {
        cout << "父类 funParent 函数" << endl;
    }

private:
    int c;
};

// 子类 公有继承 父类
class Child : public Parent {
public:
    void funChild() 
    {
        cout << "子类 funChild 函数" << endl;
    }
};

// 函数接收父类指针类型
// 此处可以传入子类对象的指针
void fun_pointer(Parent* obj)
{
    obj->funParent();
}

// 函数接收父类引用类型
// 此处可以传入子类对象的引用
void fun_reference(Parent& obj)
{
    obj.funParent();
}

int main() {

    // 父类对象
    Parent parent;
    // 子类对象
    Child child;


    // 父类对象 可以调用 父类公有函数
    parent.funParent();
    // 子类对象 可以调用 子类自身公有函数
    child.funChild();
    // 子类对象 可以调用 父类公有函数
    child.funParent();

    // 将指向子类对象的指针传给接收父类指针的函数也是可以的
    fun_pointer(&child);

    // 接收父类引用 , 此处传入子类引用
    fun_reference(child);


    // 赋值兼容性原则 : 
    cout << "\n赋值兼容性原则示例 : \n" << endl;


    // 常规操作 : 父类指针 指向 父类对象
    Parent* p_parent = NULL;
    p_parent = &parent;
    // 通过父类指针调用父类函数
    p_parent->funParent();

    // 将指向子类对象的指针传给接收父类指针的函数也是可以的
    fun_pointer(p_parent);

    // 接收父类引用参数
    fun_reference(*p_parent);


    // I. 类型兼容性原则 : 父类指针 指向 子类对象
    Parent* p_parent2 = NULL;
    p_parent2 = &child;
    // 通过父类指针调用父类函数
    p_parent2->funParent();


    // II. 类型兼容性原则 : 使用 子类对象 为 父类对象 进行初始化
    Parent parent3 = child;
    
	return 0;
}

/* 输出结果:
 * 父类 funParent 函数
 * 子类 funChild 函数
 * 父类 funParent 函数
 * 父类 funParent 函数
 * 父类 funParent 函数
 * 
 * 赋值兼容性原则示例 :
 * 
 * 父类 funParent 函数
 * 父类 funParent 函数
 * 父类 funParent 函数
 * 父类 funParent 函数
*/

5. virtual虚函数

首先:强调一个概念

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

默认情况下,编译器只会根据指针类型调用对应的函数。

#include <iostream>
using namespace std;

class Animal
{
public:
	void run()
	{
		cout << "Animal::run()" << endl;
	}
};

class Dog : public Animal
{
public:
	void run()
	{
		cout << "Dog::run()" << endl;
	}
};

class Cat : public Animal
{
public:
	void run()
	{
		cout << "Cat::run()" << endl;
	}
};

int main()
{
	Animal* p1 = new Animal(); // 只看左边定义的指针类型
	p1->run();
	delete p1;

	Animal* p2 = new Dog();  // 只看左边定义的指针类型
	p2->run();
	delete p2;

	Animal* p3 = new Cat(); // 只看左边定义的指针类型
	p3->run();
	delete p3;

	return 0;
}

/* 输出结果:
 * Animal::run()
 * Animal::run()
 * Animal::run()
*/

可以通过一个父类指针调用父类和子类中的同名同参的函数,只需要在父类中该函数声明之前加上 virtual 关键字使其成为虚函数即可。

只要在父类中声明为虚函数,那么在所有子类中重写的函数也自动变成虚函数,也就是说子类中可以省略 virtual 关键字(为了阅读代码方便,建议加上)。

子类继承父类时,子类可以不重写父类的虚函数,则相当于原样继承了父类的虚函数;也可以重写,则相当于覆盖了父类的虚函数实现。不论是否重写虚函数都不影响子类的实例化。

调用虚函数执行的是“动态绑定”,如下代码所示,在程序运行的时候才能知道调用了哪个子类的 run() 函数。

#include <iostream>
using namespace std;

class Animal
{
public:
	virtual void run()
	{
		cout << "Animal::run()" << endl;
	}
};

class Dog : public Animal
{
public:
	virtual void run()
	{
		cout << "Dog::run()" << endl;
	}
};

class Cat : public Animal
{
public:
	virtual void run()
	{
		cout << "Cat::run()" << endl;
	}
};

int main()
{
	Animal* p1 = new Animal();
	p1->run();
	delete p1;

	Animal* p2 = new Dog();
	p2->run();
	delete p2;

	Animal* p3 = new Cat();
	p3->run();
	delete p3;

	return 0;
}

/* 输出结果:
 * Animal::run()
 * Dog::run()
 * Cat::run()
*/

6. override重写

函数的签名包括:函数名、参数列表、const属性。

C++11 增加了 override 关键字,保证子类重写的虚函数与父类的虚函数有相同的签名。也就是说,加了 override,明确表示子类的这个虚函数是重写父类的,如果子类与父类虚函数的签名不一致,编译器就会报错。

因此,为了减少程序运行时的错误,重写的虚函数都建议加上 override

class Animal
{
public:
	virtual void run()
	{
		cout << "Animal::run()" << endl;
	}
};

class Dog : public Animal
{
public:
	virtual void run() override // 正确,若与父类虚函数的签名不一致,编译器就会报错
	{
		cout << "Dog::run()" << endl;
	}
};

class Cat : public Animal
{
public:
	virtual void run() override // 正确,若与父类虚函数的签名不一致,编译器就会报错
	{
		cout << "Cat::run()" << endl;
	}
};

7. final阻止重写

C++11 增加了 final 关键字,阻止类的进一步派生和虚函数的进一步重写。

如果不希望某个类被继承或者不希望某个虚函数被重写,则可以在类名和虚函数后加上 final 关键字,加上 final 关键字后,若再被继承或重写,编译器就会报错。

class Animal
{
public:
	virtual void run() final
	{
		cout << "Animal::run()" << endl;
	}
};

class Dog : public Animal
{
public:
	virtual void run() override // 报错,一旦父类的虚函数被声明为final,则子类不能再重写它
	{
		cout << "Dog::run()" << endl;
	}
};

class Cat : public Animal
{
public:
	virtual void run() override // 报错,一旦父类的虚函数被声明为final,则子类不能再重写它
	{
		cout << "Cat::run()" << endl;
	}
};

8. 多态

多态是面向对象非常重要的一个特性:

  • 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
  • 在运行时,可以识别出真正的对象类型,调用对应子类中的函数。
  • 默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态。

随着虚函数的提出,面向对象编程中的“多态性”就浮出了水面。多态性体现在具有继承关系的父类和子类之间,父类把成员函数声明为虚函数,子类重写父类的成员函数,在程序运行时期,找到动态绑定到父类指针上的对象(可能是某个子类对象,也可能是父类对象),然后系统内部查一个虚函数表,找到函数的入口地址,从而通过父类指针调用父类或者子类的成员函数,这就是运行时期的多态性。

#include <iostream>
using namespace std;

class Animal
{
public:
	virtual void speak() { cout << "Animal::speak()" << endl; }
	virtual void run() { cout << "Animal::run()" << endl; }
};

class Dog : public Animal
{
public:
	void speak() { cout << "Dog::speak()" << endl; }
	void run() { cout << "Dog::run()" << endl; }
};

class Cat : public Animal
{
public:
	void speak() { cout << "Cat::speak()" << endl; }
	void run() { cout << "Cat::run()" << endl; }
};

class Pig : public Animal
{
public:
	void speak() { cout << "Pig::speak()" << endl; }
	void run() { cout << "Pig::run()" << endl; }
};

void fun(Animal* p)
{
	p->speak();
	p->run();
}

int main()
{
	fun(new Dog());
	fun(new Cat());
	fun(new Pig());

	return 0;
}

/* 输出结果:
 * Dog::speak()
 * Dog::run()
 * Cat::speak()
 * Cat::run()
 * Pig::speak()
 * Pig::run()
*/

9. 纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
在基类中实现纯虚函数的方法是在函数原型后加 =0:

virtual void funtion1()=0

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

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

10. 参考文章

  1. https://blog.csdn.net/qq_42815188/article/details/122733305
  2. https://blog.csdn.net/qq_42048450/article/details/117282640
  3. https://blog.csdn.net/yi_chengyu/article/details/120911770
  • 38
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值