C++学习之路-多态

一、父类指针指向子类对象

学习多态之前,我们先学一个关键的知识点:父类指针指向子类对象

class Person
{
public:
	int m_age;
	Person() 
	{
		cout << "Person::Person()" << endl;
	}
};


class Student : public Person
{
public:
	int m_score;
	Student() 
	{
		cout << "Student::Student()" << endl;
	}
};

我们声明了一个父类Person,并且创建子类Student继承Person类。

什么叫父类指针指向子类对象呢?

Person *p = new Student();
p->m_age = 10;

new Student() 相当于创建一个子类的对象,然后让父类指针p指向这个对象

为什么允许父类指针指向子类对象?

首先子类会继承父类,对象的内存是8个字节,存放 m_age 和 m_score。父类指针p,指向这块内存,但只能访问m_age不可以访问m_score,所以这种指向是安全的,不会担心父类指针乱指

那么辩证的考虑问题,如果是子类指针指向父类对象呢?会发生什么事情?

Student *s = (Student *) new Person();
s->m_age = 20;
s->m_score = 100;

(Student *)是强制转换的意思,骗一下编译器。由于是子类指针s,所以子类指针可以访问 m_age 和 m_score

子类指针访问这块内存的前四个字节,可以访问到m_age,继续访问后四个字节(代码这样写s->m_score,但这句代码是访问接下来四个字节,而不是访问 m_score的意思,之前指针访问本质中提到过)

由于这块内存(new Person())只存放m_age,不存放m_score,指向的示意图如下图所示:

所以 s->m_score = 100 可能就给一块不知道是啥的内存赋值100,那这就不安全了。

OK,我们到此就了解到父类指针可以指向子类对象,子类指针不可以指向父类对象,这就够了。

二、什么是多态

回答这个问题之前,我们先回顾一下继承的知识点(具体可以看我的博文)。我们举一个小例子来直观的感受一下继承的应用场合。

假设,我声明了三个类(Dog、Cat、Pig)它们都有两个功能,一个是speak,一个是run。由于它们的speak和run的方式不同,所以声明三个类是合理的。

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

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

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

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

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

	void run()
	{
		cout << "Pig::run()" << endl;
	}
};

现在我们需要在main函数中调用这三个类的功能,我们可以写三个函数来实现它

// 函数的重载,形参不同
void CallFunction(Dog *p)
{
	p->run();
	p->speak();
}

void CallFunction(Cat *p)
{
	p->run();
	p->speak();
}

void CallFunction(Pig *p)
{
	p->run();
	p->speak();
}

int main()
{
	// 分别调用三个类中的功能
	CallFunction(new Dog());
	CallFunction(new Cat());
	CallFunction(new Pig());
	return 0;
}

这样的话,就会分别调用三个类的功能。但是,不觉得有些繁琐吗?明明三个类中的成员函数都是相同的,还需要调用三次。由于三个类有很强的共同点,何不都继承于一个父类试试。于是都继承动物Animal类。

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

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

我们先说明一个概念,子类中的成员函数和父类中的成员函数,函数相同,这叫做重写(重写的概念就是函数名称与父类相同,但是里面的实现是不同的,可以添加自己的特色)。(就像我们经常说的多线程,重写run函数一样)

然后,我只需写一个CallFunction函数,然后用父类指针作为形参,指向不同的子类对象不就可以了吗?

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

int main()
{
	// 分别调用三个类中的功能
	CallFunction(new Dog());
	CallFunction(new Cat());
	CallFunction(new Pig());
	return 0;
}

但是结果好像不是我们想的那样,下图为控制台打印的输出。尽管父类指针指向不同的对象,但是父类指针调用的speak仍然是父类的成员函数。

这是怎么回事?我明明把Dog、Cat、Pig对象传给了p指针,按理说应该调用Dog、Cat、Pig类中的speak和run函数啊?

这是因为C++默认情况下,只会根据指针类型,去调用相应的类中的函数。默认情况下不存在多态,想要实现多态,就需要一个关键字 virtual 修饰父类中需要被重写的函数。

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

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

};

被关键字 virtual 修饰的函数,叫做虚函数,是虚拟函数的简称。

然后,我们执行下述代码,就可以打印出想要的了

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

int main()
{
	// 分别调用三个类中的功能
	CallFunction(new Dog());
	CallFunction(new Cat());
	CallFunction(new Pig());
	return 0;
}

这样,我们就实现了多态。传什么参数调什么函数(传的是Dog对象,就去调用Dog类里的speak和run函数),一份函数抵好几份函数,不需要重载三份函数了。多态真的做到了,根据对象的不同,调用不同类中的成员函数。

总结:
多态是面向对象一个非常重要的概念。其核心思想就是,在子类和父类有相同的函数情况下,只需区分不同的调用者,就可以调用不同类中函数(同一操作作用于不同对象,可以有不同的解释,产生不同的执行结果,这就是多态)。在运行时,识别出真正的对象类型,调用对应子类中的对象

三、多态的要素

  1. 父类中的成员函数必须是虚函数
  2. 子类重写父类中的成员函数
  3. 父类指针指向子类对象
  4. 利用父类指针调用重写的子类成员函数

四、多态的本质(虚函数的本质)

多态是依靠虚函数实现的。虚函数的实现原理是虚表(内存中的一张表)。这个表里存储着最终需要调用的虚函数地址(函数的调用地址),所以这个虚表也叫虚函数表。

我们在创建对象的时候,用父类指针指向这个子类对象。此时在堆空间有一块区域存放着对象里的内容(包括子类继承的父类成员变量和子类本身的成员变量)。但如果父类中有虚函数,那么子类重写的也是虚函数,这毋庸置疑。此时,堆空间中的那一块内存就不只有父类成员变量和子类成员变量,还有占用4个字节(x86环境下)的指针,这个指针指向虚表,虚表里存放着子类的虚成员函数。

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

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

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

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

当我创建一个父类指针,指向子类对象时。虚表便产生了

Animal *p = new Dog();

下图是,有无虚函数时,对象的内存布局。可以看出,有虚函数时,对象内存中会存在一个指针dog,指向虚表(需表中存放着被调用的函数地址)。无虚函数时,则没有这个指针,内存中仅仅是成员变量。

创建完对象,就把虚函数提前存在了虚表里,所以下面两行代码才可以直接调用Dog类下的成员函数。

p->run();
p->speak();

多张虚表的情况

我们创建一个父类指针,指向子类对象。若父类中有虚函数,那么此时就会产生一张虚表。那一定存在创建多个父类指针,指向子类对象的情况,比如下方:

Animal *dog1 = new Dog();
dog1->run();
dog1->speak();

Animal *dog2 = new Dog();
dog2->run();
dog2->speak();

这种情况,会不会产生两个不同虚表呢?答案是不会的,dog1和dog2指向的堆空间内存中的前四个字节指向的是同一张虚表。为什么呢?因为我创建的都是Dog对象,最终dog1->run()和dog2->run()调用的都是Dog类下的run函数,是一份函数,所以为什么要创建两个虚表呢?所以只需创建一个虚表即可!!

虚表的一些注意事项

  • 父类中的成员函数,不都是虚函数,虚表中存放着什么呢?

前面的例子,我们在父类中创建了两个成员函数,并都设置为虚函数。假设我们在父类中不全设置为虚函数,就像下方代码一样,只设置speak函数为virtual。此时还存在虚表吗?如果存在,虚表中存放着什么东西?

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

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

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

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

答案是:指定有虚表啊。记住只要有虚函数,就存在虚表,几个虚函数,虚表中就有几个虚函数的调用地址。所以这个虚表中只存Dog::speak的函数地址。至于run函数,不存在虚表中,存在栈空间的某一个位置。

  • 子类中,不全部重写,只重写了部分父类中的函数,虚表中存放着什么呢?

父类中声明了两个虚函数,子类中只重写了一个speak函数

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

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

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

此时调用speak函数,毋庸置疑,可以调用Dog类里的speak函数。那调用run函数呢?会调用Dog类里的函数吗?

Animal *dog1 = new Dog();
dog1->run();
dog1->speak();

结果已经打印出来了,run函数依然是调用的父类中的run函数。这个我们其实可以理解,因为子类中都没有run函数,只是继承父类的run函数,那调用的时候,调用父类中的函数,这说得通。

这时候一个关键的问题来了:虚表中都有啥?按理说我只重写了speak函数,那虚表中只有Dog::speak函数就行了呗。结果不是这样的,虚表中不仅仅有Dog::speak函数,还有Animal::run函数。结论就是:如果子类中没有重写父类中的部分函数,那么就把这部分父类函数的调用地址直接存入虚表。

  • 虚表中的函数,根据什么原理存放的?

先看一个例子:BlackDog : Dog : Animal,这是一个多重继承的问题。现在的情况是:Animal中两个函数都声明为虚函数, Dog 里只重写一个speak函数,BlackDog里什么也不写。

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

};

class BlackDog : public Dog
{
public:

};

那,我们用Animal指针指向BlackDog 时,会不会创建虚表?如果会,虚表中都有哪些函数的调用地址?

Animal * dog2 = new BlackDog();
dog2->speak();
dog2->run();

答案是:虚表中存两个函数的调用地址(Dog ::speak和Animal::run)。为什么呢?C++在创建虚表的过程中,会做这么一件事:首先看看指向的是什么对象,如果指向的是BlackDog对象,那就先去BlackDog类下找有没有重写的函数,如果有存于虚表,如果没有再去父类中查找。此时来到了父类Dog里,将Dog里的虚函数speak,存于虚表中。由于Dog类没有重写run函数,所以再次向上查找:因此需要把Animal中的run函数放在虚表中。找完之后,把虚表写死,之后调用函数就不用再重新找了。

虚表的意义

当父类指针指向子类对象,这一行代码运行完之后,虚表便产生了,虚表里的东西固定了。然后调用成员函数,是直接去虚表中调用的。C++没有先去子类找找然后再去父类找找这种操作,直接在虚表中写死了。所以虚表的意义就在于,直接写死,省的不断地查找该调用哪个函数。

五、虚析构函数

定义:存在父类指针指向子类对象的时候(不能果断的说存在含有虚函数的类中),应该将析构函数设置为虚函数(虚析构函数)

解释一下为什么要说存在父类指针指向子类对象的时候而不是存在含有虚函数的类的时候。因为存在父类指针指向子类对象的时候不一定要有虚函数,平常情况也可以这样做。但是一旦有虚函数的出现,那就必须用父类指针指向子类对象。这两个事是包含与被包含的关系

为什么要这样呢?首先我们先看一个例子,回顾一下构造函数析构函数(可以看我之前的博文)

首先我们定义了两个类,Person和Student,让Student继承Person。分别声明构造函数和析构函数,并且子类重写了父类的speak虚函数。

class Person
{
	int m_age;
public:
	Person()
	{
		cout << "Person::Person()" << endl;
	}

	virtual void speak()
	{
		cout << "Person::speak()" << endl;
	}

	~Person()
	{
		cout << "Person::~Person()" << endl;
	}
};


class Student : public Person
{
	int m_score;
public:
	Student()
	{
		cout << "Student::Student()" << endl;
	}

	void speak()
	{
		cout << "Student::speak()" << endl;
	}

	~Student()
	{
		cout << "Student::~Student()" << endl;
	}
};

我们知道,在对象创建的时候(new Student()),就会默认调用类中的构造函数,而子类的构造函数又会默认的调用父类的构造函数。

int main()	
{
	Person *p = new Student();
	p->speak();

	getchar();
	return 0;
}

所以,上方代码打印结果为下图,这我们是可以预估到的。

但是,我在堆空间申请内存,是要释放掉的啊。由于是new出来的,所以需要delete掉

int main()	
{
	Person *p = new Student();
	p->speak();
	delete p;

	getchar();
	return 0;
}

释放指针,按理说是会调用析构函数的。打印结果如下:

这里我们就存在疑问了。Student里的析构函数怎么没被调用?我创建的明明是Student对象,怎么可能不调用Student的析构函数呢?

到这里,回归定义:含有虚函数的类,应该将析构函数设置为虚函数(虚析构函数)。因此我们将父类中的析构函数声明为虚析构函数,如下所示:

class Person
{
	int m_age;
public:
	Person()
	{
		cout << "Person::Person()" << endl;
	}

	virtual void speak()
	{
		cout << "Person::speak()" << endl;
	}

	virtual ~Person()
	{
		cout << "Person::~Person()" << endl;
	}
};

再次,释放指针,打印结果:可以看到Student类的析构函数被调用了。

总结:假如父类中有虚函数,想要通过释放内存调用子类中的析构函数时,必须将父类中的析构函数设置为虚析构函数。

六、多重继承下的虚函数问题

有这样一个例子,Dog继承 Animal,BlackDog 继承Dog。只在Animal声明为虚函数

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 BlackDog : public Dog
{
public:
	void speak()
	{cout << "BlackDog::speak()" << endl;}

	void run()
	{cout << "BlackDog::run()" << endl;}
};

然后,我用爷爷类指向孙子类,调用成员函数,会实现多态吗?

Animal * dog2 = new BlackDog();
dog2->run();
dog2->speak();

有人会有疑问:两重继承了,BlackDog类里的函数,还是虚函数吗?爷爷类可以指向孙子类吗?

答案是:只要子类中重写了父类中的虚函数,那子类这个函数也变成了虚函数。所以Dog类里的两个函数,其实只是省略了virtual关键字,本质上就是虚函数。既然Dog类中的函数是虚函数,那Dog的子类BlackDog里的重写函数,自然就是虚函数了,那自然就实现了多态了。爷爷类可以指向孙子类,这没啥好说的,知道就行。

同时还存在另外一个例子:Dog继承 Animal,BlackDog 继承 Dog。只在Dog声明为虚函数

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

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

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

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

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

	void run()
	{cout << "BlackDog::run()" << endl;}
};

这种情况下,下面代码会是怎样的运行结果?

Animal *dog1 = new BlackDog();
dog1->run();
dog1->speak();

Dog * dog2 = new BlackDog();
dog2->run();
dog2->speak();

由于 Animal中的函数不是虚函数,所以不会实现多态,只根据dog1的指针类型去调用相应类中的成员函数。反观,Dog和BlackDog都是虚函数,所以会根据对象类型去调用相应类中的成员函数

七、纯虚函数

定义:没有函数体,且初始化为0的虚函数,叫作纯虚函数。用来定义接口规范。

class Animal
{
public:
	virtual void speak() = 0;
	virtual void run() = 0;
};

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

纯虚函数就是这样定义的。没有大括号,只有函数后面 = 0 即可

virtual void speak() = 0;
virtual void run() = 0;

为什么要有纯虚函数?这个接口规范又是什么?下面回答这些问题。

  • 为什么要有纯虚函数?

纯虚函数的意思是:没有实现,只有声明的虚函数。为什么要没有实现呢?假设某个抽象的东西被当作父类,抽象中有很多具体的东西被当作子类。比如:动物是抽象的概念,狗、猫、猪这些是具体的概念。我们可以说狗是怎么怎么跑的,猫是怎么怎么跑的,猪是怎么怎么叫的,但是没人会说动物是怎么怎么跑的,因为动物很多,我也不知道你说的是哪一种动物。所以动物的跑、动物的叫,没有具体的范式,那就在写函数时,只需要声明,不需要实现。纯虚函数的设置是为了没有实现的意义的那些函数。纯虚函数所在的类就像是专门用来写声明的一个抽象类。

  • 接口规范又是什么?

动物有很多种,每种动物都能跑,能叫,这就叫两个规范。所以把这种大家都有的功能写在父类里,供所有子类重写,且子类有自己的具体特色。虽然都是叫、都是跑,但是每种动物的叫法、跑法不同。这就是继承的意义啊!那这个接口规范方便到什么程度呢?我可以通过父类指针,指向不同的子类,就可以调用不同的子类功能,这多规范啊!!

抽象类(Abstract class)

定义:含有纯虚函数的类,就叫做抽象类(只要有一个纯虚函数就叫作抽象类)。这种类,不可以实例化对象,也不可以创建对象

Animal anim; //报错,不可以创建对象
Animal *anim = new Animal(); //报错,不可以实例化对象

所以,含有纯虚函数的类,就老老实实的指向子类对象就行了。

如果父类是抽象类,子类没有完全重写纯虚函数,那么子类也是抽象类,也不可以创建对象。

比如下面的例子。 Animal类中全部是虚函数,但是其子类Dog,只重写了一个speak函数,没有完全重写父类中所有的纯虚函数,所以子类Dog跟父类 Animal一样,也是抽象类。

class  Animal
{
public:
	virtual void speak() = 0;
	virtual void run() = 0;
};

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

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

	void run()
	{cout << "BlackDog::run()" << endl;}
};

既然Dog 也是抽象类,那就不可以创建对象,不可示例化对象了。

但,仍然可以指向子类对象

这里我们正好借着抽象类学习一个知识点:父类重写过的纯虚函数,子类无需重写,直接继承即可。

所以,没有完全重写,意味着需要继承下来父类中的纯虚函数,一旦该类有了纯虚函数,那就意味着该类是抽象类。

class  Animal
{
public:
	virtual void speak() = 0;
	virtual void run() = 0;
};

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

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

我们可以看到,对于类BlackDog来说,上面有二重继承。父类Dog重写了speak函数,这个speak函数是爷爷类Animal的纯虚函数。如果按照之前所说的,Dog类是抽象类,这毋庸置疑。那BlackDog没有完全重写speak函数,应该也是抽象类啊。但是,BlackDog并不是抽象类,这是怎么回事呢?

我来梳理一下:首先Animal有两个纯虚函数,Dog只重写了一个,所以Dog也是抽象类。我们知道只有含有至少一个纯虚函数的类才叫做抽象类,那Dog类含有哪个纯虚函数呢?答案自然是run函数,因为没有重写,直接就继承下来了。现在我们知道了Dog类中speak不是纯虚函数,run是纯虚函数。再看BlackDog类,重写了父类Dog中唯一的一个纯虚函数run,这叫完全重写父类中的纯虚函数,所以不可以叫抽象类。也就是说, BlackDog类中如果只重写speak,那这三个类都是抽象类。如下面代码所示,三个类都是抽象类。

class  Animal
{
public:
	virtual void speak() = 0;
	virtual void run() = 0;
};

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

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

注意:抽象类依然可以含有成员变量,虽然不可以通过抽象类本身的对象去访问,但仍可以继承给子类,让子类的对象去访问。所以抽象类是可以含有成员变量的

八、多态总结

  • 父类中是虚函数,子类如果重写了,默认就是virtual修饰的虚函数。
  • 多态实现的前提是,父类指针指向子类对象。如果是子类指针指向子类对象,那就不是多态
  • 实现了多态,就会根据对象类型去调用相应类中的成员函数;没有实现多态,则只会根据指针类型去调用相应类中的成员函数。
  • 当父类中的虚函数没有实现意义时,就要设置为纯虚函数。
  • 子类没有完全重写完父类中的所有虚函数,就会自动继承那些虚函数
  • 虚表是父类指针指向子类对象完毕之后创建的,将被调用的函数地址直接写死在虚表中
  • 一旦有了虚函数,就可以根据对象的类型去调用相应的成员。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值