《随笔九》—— C++中的 “ 虚函数 ”

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34536551/article/details/84375823

目录

定义虚函数

三种调用虚函数的方式比较

虚函数的访问方法


看不懂可以先看这个:《随笔八》—— C++中的“ 多态中的静态联编 、动态联编”https://blog.csdn.net/qq_34536551/article/details/84195882


虚函数的作用:

在同一个类中不能定义两个函数原型相同的函数, 否则就是重复定义。 但是在类的继承层次结构中,在不同的层次可以出现函数原型相同而功能不同的函数, 编译器按照同名覆盖的原则决定调用的哪个函数。 然后我们就可以用同一个调用形式, 既能调用派生类的虚函数又能调用基类的同名虚函数。

最后我们使用基类对象的指针 或者 基类对象的引用,指向派生类对象, 这样我们就可以用该指针或者引用 访问 基类和派生类中的同名函数(它们都是虚函数)。


定义虚函数


●   虚函数的定义是在基类中进行的, 即把基类中需要定义为虚函数的成员函数声明为 virtual、  当基类中的某个成员函数被声明为虚函数后, 我们就可以在派生类中重新定义该函数, 但是注意的是该函数的原型必须跟基类中的那个函数原型一致。 虚函数的定义语法为:

virtual  返回类型 标识符 (参数表)
{
  // Satement
}

那么需要注意的是: 虚函数是成员函数, 不能是 static 成员函数。

下面看一个程序代码:

class Animal
{
public:
	void sleep()
	{
		cout << "调用的是基类中的sleep" << endl;
	}
	virtual void breathe()
	{
		cout << "调用的是基类中的breathe" << endl;
	}

};
class Fish :public Animal
{
public:
	void breathe()
	{
		cout << "调用的是派生类中的breathe" << endl;
	}

};


int main()
{
	Fish Myfish;
	Animal *an = &Myfish;
	an->breathe(); // 调用的是派生类中的该函数, 因为该函数是virtual 函数
	an->Animal::breathe(); //调用的是基类中的该函数

	system("pause");
	return 0;
}

在上述代码中, 将基类中的 breathe() 定义为虚函数,我们在主调函数中 定义 Animal 对象指针指向类 Fish的对象 Myfish, 调用 breathe ()函数,调用的就是派生类中的该函数, 如果该函数在基类中不是虚函数, 如果用这样的方法调用breathe(),  那么调用的就是基类中的该函数。   

那么,当将基类中的成员函数 breathe() 声明为 virtual 时, 编译器在编译的时候 发现 Animal 类中有虚函数, 此时编译器会为每个包含虚函数的类创建一个虚函数表,  该表是一个一维数组, 在这个数组中存放每个虚函数的地址。 

在上述代码中 Animal 类 和 Fish 类都包含一个虚函数 breathe () ,  因此编译器会为这两个类分别建立一个虚函数表,如图所示:

在上述代码中, 当 Fish 类的 Myfish 的对象 构造完毕后, 其内部的虚表指针也被初始化为指向Fish 类的虚表。 在类型转换后, 调用an->breathe(), 由于 an 实际指向的是Fish 类的对象, 该对象内部的虚表指针指向的Fish 类的虚表, 因此最终调用的是Fish 类的 breathe() 函数。


下面简要说明声明虚函数需要注意的问题:虚函数属于它所在类层次结构, 而不是属于某一个类, 在派生类中对基类的虚函数进行覆盖时, 要求派生类中的声明的虚函数与基类中的被覆盖的虚函数之间满足如下条件:

与基类的虚函数的函数原型完全相同。

它们的返回值类型都要一致。

虚函数的声明只能出现在类定义中,不能出现在函数体定义的时候, 而且, 基类中只有保护成员和公有成员才能被声明为虚函数。

在派生类中重新定义虚函数时, 关键字virtual 可以写或不写。

若在派生类中没有重新定义虚函数时, 则该类的对象将使用其基类中的虚函数代码。

虚函数必须是类的成员函数, 不能是友元, 但它可以是另一个类的友元。

析构函数可以是virtual 函数, 但构造函数则不能是虚函数。

一个类的虚函数仅对派生类中重定义的虚函数其作用, 对其他函数没有影响, 意识就是说, 只有被说明为虚函数的那个成员函数才具有多态性。

简要说明使用基类指针或者基类引用指向派生类对象应注意的问题:

声明为基类对象的指针或者引用可以指向它的公有派生类对象, 但不允许指向它的私有派生类对象。

允许声明为基类对象的指针或者引用可以指向它的公有派生类对象, 但不允许声明一个派生类对象的指针或引用指向基类的对象。

声明为基类对象的指针或者引用时, 当其指向它的公有派生类的对象时, 只能直接访问派生类中从基类继承下来的成员, 不能直接访问公有派生类中定义的成员。 要想访问其公有派生类中的成员, 可将基类指针或者引用用显式类型转换方式转换为派生类指针。

在派生类中重新定义基类中的虚函数, 是函数重载的另一种形式,虚函数跟函数重载的区别是:

一般的函数重载, 要求其函数名一样, 但是参数的个数、顺序、参数类型必须有所不同。

重载函数可以是成员函数或友元函数, 虚函数不能是友元函数。

重载函数的调用是以所传递的参数序列的差别作为调用不同函数的依据; 虚函数是根据对象的不同去调用不同类的虚函数。

虚函数在运行时表现出多态功能, 而重载函数则在编译时表现出多态性。

重载一个虚函数时, 要求派生类中的该函数与基类中的该函数的函数原型完全相同。

如果仅仅返回类型不同,其余相同, 则系统会给出错误信息。 

如果函数名相同,而参数个数、参数的类型或参数的顺序不同, 虚函数的特性将被丢失。


注意:  有时我们在基类中定义的非虚函数会在派生类中被重新定义, 如果基类指针调用该函数, 那么调用的是基类中的该函数;

如果说用派生类指针调用该同名函数 , 那么调用的是派生类中的同名函数, 这并不是多态性(使用的是不同类型的指针),  并没有用到虚函数的功能。


三种调用虚函数的方式比较


class father
{
public:
	virtual void run() const
	{
		cout << "调用的是基类中的run函数!" << endl;
	}
};
class son : public father
{
public:
	virtual void run() const
	{
		cout << "调用的是派生类son中的run函数!" << endl;
	}
};

class daughter :public father
{
public:
	virtual void run() const
	{
		cout << "调用的是派生类中daughter的run函数!" << endl;
	}
};
void one(father one)
{
	one.run();
}
void two(father *two)
{
	two->run();
}
void three(father &three)
{
	three.run();
}
int main()
{
	father *p = new son;
	one(*p);

	father *pp = new daughter;
	two(pp);

	father *ppp = new father;
	three(*ppp);

	delete p;
	delete pp;
	delete ppp;
	system("pause");
	return  0;
}

输出结果为:
调用的是基类中的run函数!

调用的是派生类中daughter的run函数!
调用的是基类中的run函数!

在该代码中: one(*p);  该调用函数 one 传递了一个实参 “ *p” , 由于 p 是 son 类对象的内存地址, 因此 *p代表的是 son 对象, 我们可以看见我们是按值传递的, 输出的结果是 “ 调用的是基类中的run函数!  ” 我们调用的是 father 类的 成员函数 one,而不是 son类的成员函数 one。  所以说, 如果说 按对象名的方式调用虚函数, 虚函数不起作用,并没有起到运行时的多态作用,采用的是静态联编的方式。

只有在使用基类对象的指针 或者引用时来调用虚函数时, 虚函数才能起到运行时的多态作用,才能实现在运行时的动态联编,  所以说 后面的两个指针 *pp  和 *ppp,  都起到了多态的作用, 输出结果就可以看出。


虚函数的访问方法


对于虚函数的访问方法有:

基类对象的指针或者引用

对象名

类的成员函数访问该类层次中的虚函数

用构造函数和析构函数

(1 ) 下面用第一种方式和第二种方式调用虚函数,看示例代码:

class Point
{
private:
	float x, y;
public:
	virtual double area()
	{
		return 1.1;
	}
};
class Circle :public Point
{
private:
	float radius;
public:
	virtual double area()
	{
		return 2.2;
	}
};
int main()
{
	Circle cl;
	Point *pp = &cl;
	// 基类对象的指针指向公有派生类对象, 实现动态联编
	cout << "调用的是派生类中的area:" << pp->area() << endl;
	cout << "调用的是基类中的area:" << pp->Point::area() << endl;
	//下面这两个使用对象名调用虚函数, 实现的是静态联编
	cout << "调用的是派生类中的area:" << cl.area() << endl;
	cout << "调用的是基类中的area:" << cl.Point::area() << endl << endl;


	Point *p = new Circle;
	cout << "调用的是派生类中的area:" << p->area() << endl;
	cout << "调用的是基类中的area:" << p->Point::area() << endl << endl;
	delete p;

	// 基类对象的引用指向公有派生类对象, 实现动态联编
	Point &ppp = cl;
	cout << "调用的是派生类中的area:" << ppp.area() << endl;
	cout << "调用的是基类中的area:" << ppp.Point::area() << endl << endl; // 实现的是静态联编
	system("pause");
	return 0;
}

输出结果为:

调用的是派生类中的area: 2.2
调用的是基类中的area: 1.1
调用的是派生类中的area: 2.2
调用的是基类中的area: 1.1

调用的是派生类中的area: 2.2
调用的是基类中的area: 1.1

调用的是派生类中的area: 2.2
调用的是基类中的area: 1.1

(2)下面用类的成员函数访问该类层次中的虚函数

class A
{
public:
	virtual void func1()
	{
		cout << "调用的是基类中的func1 虚函数!" << endl;
		a1();
	}
	virtual void func2()
	{
		cout << "调用的是基类中的func2 虚函数!" << endl;
	}
	void a1()
	{
		cout << "调用的是基类中的a1 普通成员函数!" << endl;
		func2();
	}

};

class B : public A
{
public:
	virtual void func2()
	{
		cout << "调用的是派生类中的func2 虚函数!" << endl;
	}
};
int main()
{
	A a;
	a.func1();
	cout << endl;

	B b;
	b.func1();

	system("pause");
	return 0;
}

输出结果为:

调用的是基类中的func1 虚函数!
调用的是基类中的a1 普通成员函数!
调用的是基类中的func2 虚函数!

调用的是基类中的func1 虚函数!
调用的是基类中的a1 普通成员函数!
调用的是派生类中的func2 虚函数!

可以看出, 当用b.func1 调用虚函数时, 因为派生类中没有该函数,所以调用基类中的该函数, 然后基类中的 func1函数中调用 a1函数, a1函数实际调用的是派生类中的func2 函数,从输出结果可以看出。

(3) 使用构造函数和析构函数调用虚函数, 使用构造函数和析构函数将采用静态联编

#include<iostream>
#include<string>
#include<cstdlib>
using namespace std;

class A
{
public:
	A()
	{
		cout << "调用的是基类中的构造函数!\n" << endl;
	}
	virtual void func1()
	{
		cout << "调用基类中的func1 函数!\n" << endl;
	}

};

class B : public A
{
public:
	B()
	{
		cout << "调用的是派生类中B的构造函数!\n" << endl;
		func1(); // 构造函数访问虚函数
		A::func1();
	}
	virtual void func1()
	{
		cout << "调用派生类中B的func1 函数!\n" << endl;
	}
	~B() //析构函数访问虚函数
	{
		func1();
		A::func1();
	}
};

int main()
{
	{
		B b;
	}

	system("pause");
	return 0;
}
输出结果为:

调用的是基类中的构造函数!

调用的是派生类中B的构造函数!

调用派生类中B的func1 函数!

调用基类中的func1 函数!

调用派生类中B的func1 函数!

调用基类中的func1 函数!

该文章后期还会更新修改。

猜你喜欢

转载自blog.csdn.net/qq_34536551/article/details/84375823
今日推荐