C 多态总结

1、多态的实现三个条件:

(1)要有继承

(2)要有虚函数重写(即动态绑定)

(3)至少有一个基类类型的指针或基类类型的引用。这个指针和引用可以对virtual成员函数进行调用

C++的多态性分为两种:编译时的多态、运行时的多态; 使用重载来实现编译时的多态,使用虚函数来实现运行时的多态。

 

2、基类中的虚函数必须要实现吗?

(1)在main函数中,如果有基类或者派生类的实例对象,就需要有基类的虚函数的实现。

(2)在main函数中,如果没有基类或者派生类的实例对象,可以不实现基类的虚函数。

(3)如果把虚函数写成纯虚函数,也就不需要实现了。

 

3、如果基类中有纯虚函数,为什么基类不能实例化对象。

先从反面来讲:如果含有纯虚函数的类可以定义对象,那么该对象就应该可以调用类中的纯虚函数,但是纯虚函数是没有实现的,这就是个矛盾的。

正面来讲:普通类具有成员函数,构造类的对象时,会对成员变量和成员函数分配内存。含有纯虚函数的类,定义了成员函数的地址是空,无法分配内存,该成员函数对类是没有意义的,失去了普通类的数据和方法绑定于同一对象中的意义,因此无法构造对象,只能通过派生类继承这些成员函数并实现,才能构造派生类对象。此时抽象类就起到了定义接口的作用。

抽象类将事物的描述和实现区分开来,选择纯虚函数的概念,是想将一个类声明为抽象类的思想明确化,选择性的定义函数​是一种灵活的多的方式,是实现多态的基础。

 

4、什么是动态绑定(晚绑定)?

动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。

含有虚函数的基类对象和派生类对象都有vptr指针,每个类的vptr指针指向相应的虚函数表。基类指针(或引用)指向派生类对象(或引用)时调用虚函数时,派生类vptr指针找到虚函数表,根据虚函数表找到相应虚函数的入口地址,然后进行调用。这是动态联编。

动态联编是指在程序运行的时候,编译器才去判断调用哪个函数或如何执行程序。如if-else结构、switch结构等。

静态联编是指程序在编译阶段就确定了如何执行。

为什么虚函数在编译期间无法绑定?

因为不确定运行期的状态,假设 func() 为虚函数,指针p 的类型为基类A,那么 p->func() 可能调用 A 类的函数,也可能调用派生类 B、C 的函数,不能根据指针 p 的类型对函数重命名。也就是说,虚函数在编译期间无法绑定。

 

5、继承中的指针问题

(1)指向基类的指针可以指向派生类对象,当基类指针指向派生类对象时,这种指针只能访问派生对象从基类继承而来的那些成员,不能访问子类特有的元素,除非应用强类型转换,例如有基类B和从B派生的子类D,则B *p;D dd; p=&dd是可以的,指针p只能访问从基类派生而来的成员,不能访问派生类D特有的成员.因为基类不知道派生类中的这些成员。

例如:

class A
{
public:
	virtual void func()
	{
		cout << "father" << endl;
	}
private:
};

class B :public A
{
public:
	virtual void func1()
	{
		cout << "son" << endl;
	}
private:
};

int main()
{
	A *pa = NULL;
	B* pb = NULL;
	pb = (B*)pa;  //父类指针强制转换为子类指针

	system("pause");	
	return 0;
}	

 

(2)不能使用派生类指针指向基类对象,派生类对象可以直接给父类对象赋值。

当基类的指针(P)指向派生类的时候,只能操作派生类中从基类中继承过来的数据。

派生类可能不仅仅从基类处继承了基类成员,还可能自己拥有特有的成员,所以派生类的指针,内存空间可能会比基类长,派生类指针可能会调用基类对象所不具有的数据,可能会造成错误,因此不能用派生类指针指向基类对象。

因为子类对象会继承父类的数据,因此子类对象可以对父类对象赋值;而父类可能会不具有子类特有的数据,因此父类对象不能对子类对象进行赋值。

类型兼容性原则:

子类对象可以当做父类对象使用;

子类对象可以直接给父类对象赋值;

子类对象可以直接初始化父类对象;

父类指针可以指向子类对象;

父类引用可以直接引用子类对象;

(3)如果派生类中覆盖了基类中的成员变量或函数(无虚函数重写),则当声明一个基类指针指向派生类对象时,这个基类指针只能访问基类中的成员变量或函数。

(4)如果基类指针指向派生类对象,则当对其进行增减运算时,它将指向它所认为的基类的下一个对象,而不会指向派生类的下一个对象,因此,应该认为对这种指针进行的增减操作是无效的。

 

6、虚函数使用和定义规则:

(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。

因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。

(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。

(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名、同返回值、同参数个数、同参数类型函数。

 

7、类的静态函数和构造函数不可以定义为虚函数:

(1) 静态函数的目的是通过类名+函数名访问类的static变量,或者通过对象调用staic函数实现对static成员变量的读写,要求内存中只有一份数据。而虚函数在子类中重写,并且通过多态机制实现动态调用,在内存中需要保存不同的重写版本。

(2)  构造函数的作用是构造对象,而虚函数的调用是在对象已经构造完成,并且通过调用时动态绑定。动态绑定是因为每个类对象内部都有一个指针,指向虚函数表的首地址。而且虚函数,类的成员函数,static成员函数都不是存储在类对象中,而是在内存中只保留一份。

 

8、为什么C++调用空指针对象的成员函数可以运行通过?

当我们定义一个类的对象为空时,这时我们调用该对象中的函数,我们会发现当调用非虚函数时仍可以正常调用,而如果要调用虚函数则会报错。如下示例:

class Fa
{
public:
	virtual void func1()
	{
		cout << "father  func1" << endl;
	}

	void func2()
	{
		cout << "father func2" << endl;
	}
};

int main()
{	
	Fa *fa = NULL;
	fa->func2();  

	system("pause");	
	return 0;
}	

结果为:

当我们调用虚函数func1()时,会报一个内存错误:

 

当我们把申请的内存delete后,再测一下,看情况如何:

int main()
{
	Fa *fa = new Fa;;
	delete fa;
	fa->func2();
	
	system("pause");	
	return 0;
}	

结果如下,可以看出我们仍然可以调用该成员函数

但当我们,调用虚函数func1()时,仍然会报错。

 

由以上几种情况我们发现当类的对象指针为空时,并不是指该对象就不存在,当我们的指针没有用来指向其他地方时,此时该指针都可以调用类中成员函数,但无法调用其虚函数。同样的道理,当我们用 delete 释放一个类的对象内存空间时,并不是将该对象内存空间进行清除,而是将这块内存的使用权释放了,但里面的东西可能还依然存在,所以在这块内存被其他对象占用之前里面的东西依然会存在,所以即使我们将对象指针指向空后,依然可以调用类中的函数。

这里要强调的是,delete只是把指针所指的内存释放掉,并没有把指针本身给干掉。我们平常将指针所指内存释放掉之后会让内存指向空,这是为了防止产生野指针,为了下次调用指针的时候方便判断使用,并不是将该指针指向NULL后指针什么都没有了。

C++只关心你的指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性。

编译期绑定(静态绑定)的函数,会给出一个入口地址,因此可以通过这个地址调用函数。

而在运行期绑定(动态绑定)的函数(如func1),不会提前确定该函数的入口地址,指针为NULL,无法找到虚函数表,所以也就无法调用func1()函数。

 

 

 

 

 


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值