c++学习笔记(19.动态类型识别)

本节知识点:

1.动态类型识别的前因:

   a.首先我们先说类之间的几种强制类型转换:
      第一、具有继承关系的类对象之间的转换(即子类对象与父类对象之间,或叫对象之间),由于赋值兼容性原则,child c1;   parent p1 = c1; 子类是特殊的父类,因为子类是大于等于父类的,所以将子类对象赋值给父类对象,是安全的(将大的赋值给小的是安全的,这样由于默认强制类型转换,不会造成访问到未知数据,造成未知错误)。编译器会默认将子类中父类没有成员砍掉,这个过程就是强制类型转换的过程,其实是默认强制类型转换,所以可以省略parent p1 = (parent) c1; 或者 parent p1 = static_cast<parent>(c1); 的过程。上面一种是c方式的强制类型转换,另一种是c++方式的强制类型转换,其实实质是一样的,都是砍掉多的成员!!!当然也会出现相反的情况,即将父类对象赋值给子类对象,这个过程就需要强制类型转换了,否则会编译出错,因为这个过程不是编译器默认的! child c2;   parent p1 = c2;  child c1 = (child&)p1;或 child c1 = static_cast<child&>(p1);这个过程c2对象是不会赋值给c1的,因为当c2赋值给p1的时候,p1会丢失一部分成员,丢失的那部分是传递不到c1中的,所以c1中就会出现未知数据,所以这样的语句是错误的,也是没有意义的,容易造成未知错误,应该避免!!!

      第二、具有继承关系的类对象指针之间的转换(即子类对象指针与父类对象指针之间,或叫指针引用之间,因为引用实质上是一种指针),由于赋值兼容性原则,child* c1; parent* p1= c1;这个过程中隐藏了一个强制类型转换的过程,即parent* p1 = (parent*) c1;   或   parent* p1 = static_cast<parent*>(c1); 或 parent* p1 = static_cast<parent*>(c1);(注:两者区别后面再详细说)。同理子类指针赋值给父类指针也是安全的,因为当通过父类指针去寻找成员的时候,只能找到父类中有的成员,其实也就等同了砍掉了子类中父类没有的成员了,因为多的这些子类特有成员,通过父类p1访问不到!!!但无论怎么说指针之间的强制类型转换后,仅仅了改变了指针的行为(因为指针类型变了,类型决定行为),但是p1和c1中存放的数值是相同的,即地址是相同的!但是对象之间的强制类型转换后,可是实实在在的把多余的部分砍掉了!!!这是两者最大的区别,也就是因为这个区别,才造成了需要动态类型识别!!!这样当把父类对象指针赋值给子类对象指针的时候,即parent* p1;  child* c1 = (child*)p1;,就不能像对象之间一样,判断这个语句是绝对错误的!!!为什么?因为你不能确定p1的内容是什么,当parent* p1 = &c2; p1指向一个子类对象的时候,这几条语句就是对的!但当parent* p1 = &p2; p1指向一个父类对象的时候,这几条语句就是错的!所以说需要动态类型检查!!!这里再说说把父类对象指针强制类型转换成子类对象指针的语句,child* c1 = (child*)p1; 或者 child* c1 = static_cast<child*>(p1); 或者 child* c1= dynamic_cast<child*> (p1);(区别后面说)

      第三、没有继承关系的类对象之间的转换,是一点意义都没有的,只能使用c方式的野蛮转换,test t2;  child c2 = (child&)t1;

      第四、没有继承关系的类对象指针之间的转换,同样也是一点意义都没有的,也只能使用c方式的野蛮转换,test* t1 = NULL;  child* c2 = (child*)t1;
示例代码:
#include <iostream>
#include <cstdlib>

using namespace std;

class parent
{
public:
	int a;
};
class child : public parent
{
public:
	int b;
};
class test
{
	
};

int main()
{
	child c1;
	c1.a = 10000;
	c1.b = 20000;
	child* cp1 = &c1;
	parent* pp1 = cp1; //由于赋值兼容性原则 所以默认强制类型转换 
	parent* pp2 = (parent*)cp1; //此语句与上面语句一样 
	parent* pp3 = static_cast<parent*>(cp1); //此语句与上面语句一样 
	
	child* cp2 = (child*)pp1; 
	cout << "cp2 " << cp2->a << " " << cp2->b << endl;
	child* cp3 = static_cast<child*>(pp1);
	cout << "cp3 " << cp3->a << " " << cp3->b << endl;
	
/*没有继承关系的类之间的强制类型转换,只能用c语言的方式,static_cast报错*/
/*因为没有继承关系的类之间的强制类型转换,无论是指针之间的,还是变量之间的,都没有意义*/ 
	test* t1 = NULL;
	child* c2 = (child*)t1; 
//	child* cc2 = static_cast<child*>(t1);
	test t2;
	child c3 = (child&)t2;
//	child c4 = static_cast<child&>(t2); 
 	return 0;
} 
注意:通过上面分析,出现了一个问题,就是在对象之间进行强制类型转换的时候,parent p1 = (parent) c1; 把子类对象赋值给父类对象的时候,把c1强制类型转换成为parent类型就可以了,但是当反过来,child c1 = (child&) p1;  child c1 = static_cast<child&>(p1); 把父类对象赋值给子类对象的时候,就必须要把p1强制类型转换成为child的引用类型!!!为什么一定要是引用类型?不明白!!!不是引用就报错!!!

2.动态类型:

   a.由于基类指针可以直接指向派生类对象,因此可能存在指针所指类型与具体指向的对象类型不同的情况,如parent* p1 = &c1; 或者  parent* p1 = new child;  动态类型指的是基类指针所指向的对象的实际类型!即p1指向的到底是父类对象还是子类对象!
所以:动态类型导致,child *c2 = (child*) p1; 这样的语句不知道是否正确,当p1的动态类型为child时,(child* )p1强制类型转换成功,否则,可能出现无法预知的错误!即基类指针是否可以强制类型转换为子类指针取决于动态类型!

3.动态类型的识别(多态的方式):

   a.利用多态进行动态类型识别:c++中的多态根据实际的对象类型调用对应的虚函数,可以在基类中定义虚函数返回具体的类型信息,所有的派生类都必须实现类型相关的虚函数,每个类中的类型虚函数都需要不同的实现。
示例代码:
#include <iostream>

using namespace std;

class parent
{
public:
	/*如果想在类中定义一个常量并且完成对常量的赋值
	  第一种,static const int ID = 0;使用静态常量
	  第二种,const int ID; 然后通过构造函数的初始化列表赋值  使用const常量
	  第三种,enum{ ID = 0 }; 使用枚举常量 
	  注意:静态变量其实是种特殊的全局变量  静态常量其实就是特殊的全局常量
	  静态变量要在类外面进行初始化,静态常量由于是常量要在定义处初始化,所以不在外面初始化 
	*/
	enum{ ID = 0 };
	virtual int type()
	{
		return ID;
	}
};

class child : public parent
{
public:
	enum{ ID = 1 };
	int type()
	{
		return ID;
	}
	int add(int a, int b)
	{
		return a+b;
	}
};

void test(parent* p)
{
	if(p->type() == child::ID) //类中的常量都可以通过这样的方式访问 
	{
		child* c = (child*)p;
		cout << c->add(2,7) << endl; 
	}
	else
	{
		cout << "error!" << endl;
	}
}
int main()
{
	parent p1;
	child c1;
	test(&p1);
	test(&c1);
	return 0;
}

针对这个示例代码总结了以下几个问题(当做对以往知识的回顾):
    第一、关于静态成员变量,static int a; 这样的静态成员变量,如果在任何地方(无论是类内还是类外)都不使用,可以不去声明这个静态变量(静态变量的声明在类外面),如果使用这个静态变量,不管是在任何位置使用,都要在类外进行声明或初始化!原因是,这个静态变量定义在类中,由于它是静态属性的(即全局属性),所以可以使用在类外面。当然使用前就要去声明了!同理 static const int a = 10;这样的静态成员常量,在类中定义的过程中,就应该完成初始化赋值,因为它是一个常量!同样它不需要在类外面进行声明就可以使用,原因就是,它是一个static属性的全局常量,全局常量的使用,还需要声明吗!只要注意下,命名空间就好了!
    第二、静态成员函数中是不可以使用,普通成员变量的,因为普通成员变量,是在类中直接调用,在类外通过对象进行调用的!而静态成员函数,没有定义在类内,它是static属性的!只是定义在全局区中的某个特定的命名空间中的!
    第三、类中的变量,都是不能直接在类中定义变量的时候进行赋值的!!!能够直接赋值的都是常量,枚举常量,static const int a = 10; 静态成员常量,还有就是通过构造函数初始化列表赋值的const int a;普通成员常量!
    第四、对于构造函数的初始化列表的赋值,不仅仅可以给常量赋值,也可以给成员变量赋值!
 b.使用虚函数进行多态类型识别的缺点:
     第一、必须从基类开始提供类型虚函数
     第二、所以派生类都必须重写类型虚函数
     第三、每个派生类的类型ID必须唯一,当派生类增多的时候,很难保证ID唯一
     利用虚函数进行多态类型识别的方法可以满足工程的需要,但是维护性会随着派生类的增多而成指数级增长!!!

4.动态类型识别(dynamic_cast的方式):

   a.dynamic_cast关键字,主要是用于基类和派生类之间的强制类型转换用的,但是前提是类族中至少有一个虚函数(即父类中必须有一个虚函数,因为这样子类中也同样继承了父类的这个函数,就构成了类族中的一个多态)。当用于指针转换时,转换失败返回空指针,当用于引用转换时,转换失败将引发bad_cast异常!
   b.这里说下dynamic_cast与static_cast的区别:当对于基类和派生类之间不管是对象还是指针进行强制类型转换的时候,static_cast都可以将其转换,这个转换的过程就跟使用c语言强制类型转换的过程是一样的,是野蛮的,是不计后果的,是在编译期间进行的。但是dynamic_cast只能转换具有继承关系对象的指针和引用,且它是在程序运行的过程中,进行转换的,是动态的,是安全的,需要去判断结果的(有成功有失败很安全)。static_cast则直接就是转换了,不分情况。但是dynamic_cast需要有虚函数,而且对于没有继承关系且有虚函数的对象的指针也可以进行转换,虽然没有意义!
示例代码:
#include <iostream>

using namespace std;

class parent
{
public:
	virtual ~parent()
	{
		
	}
};

class child : public parent
{
public:
	int add(int a, int b)
	{
		return a + b;
	}
};

void test(parent* p)
{
	child* c = dynamic_cast<child*>(p);
	if(NULL != c) 
	{
		
		cout << c->add(2,7) << endl; 
	}
	else
	{
		cout << "error!" << endl;
	}
}
int main()
{
	parent p1;
	child c1;
	test(&p1);
	test(&c1);
	return 0;
}
c.dynamic_cast的优势:不用显示的声明和定义类型虚函数,不用为类族中的每一个类型分配类型ID
   dynamic_cast的缺陷:只能用于具有虚函数的类族
注意:使用dynamic_cast进行动态类型识别可以取代类型虚函数的方案,但是在本质上dynamic_cast还是需要类族中存在虚函数的(在工程中常把析构函数作为这个虚函数定义)

5.typeid关键字:

   a.c++提供了typeid关键字用于动态获取类型信息,typeid关键字返回对应参数的类型信息,typeid返回一个type_info类对象,type_info类的使用需要包含<typeinfo>头文件!
c++学习笔记(19.动态类型识别)
如图:typeid(N)会返回上图中的这个类的一个对象,这个对象中包含了所有N的类型信息,且这个类还有两个 == 和 != 的操作符重载函数!当typeid的参数为NULL时,抛出bad_typeid异常!
示例代码:
#include <iostream>
#include <typeinfo> //使用typeid关键字的时候 需要这个头文件 

using namespace std;

class parent
{
	
};
int main()
{
	parent p;
	int a;
	char* c;
	
	const type_info& tip = typeid(p); //当使用type_info创建对象的时候,一定是常引用 
	const type_info& tip1 = typeid(parent); //因为typeid是关键字 不是函数,所以后面可以是类型 
	const type_info& tii = typeid(a);
	const type_info& tic = typeid(c);
	
	cout <<  tip.name() << endl;
	cout <<  tip1.name() << endl;
	cout <<  tii.name() << endl;
	cout <<  tic.name() << endl;
	
	/*一般不去判断tip.name()这个字符串,都是用==重载函数进行判断*/ 
	cout << (typeid(p) == typeid(parent)) << endl;
	cout << (typeid(a) == typeid(int)) << endl;
	cout << (typeid(c) == typeid(char*)) << endl;
	cout << (typeid(*c) == typeid(char)) << endl;	
	return 0;
}
注意:第一、typeid关键字 需要 <type_info>头文件
           第二、typeid是关键字 后面可以跟变量也可以是类型
           第三、type_info类中的name()函数得到的字符串,不同编译器是不一样的,是编译器内部处理的,也不一定就是类型名(可能会有点不一样),所以不能去比较这个字符串和类型名,来判断是什么类型!
           第四、由于typeid(N)返回一个type_info类对象,所以可以typeid(p) == typeid(parent) 这样使用,type_info类中的==操作符重载函数!
           第五、通过type_info创建对象的时候,一定是一个常引用!const type_info& tip 
示例代码:
#include <iostream>
#include <typeinfo>

using namespace std;

class parent
{
public:
	virtual ~parent()
	{
		
	}
};

class child : public parent
{
public:
	int add(int a, int b)
	{
		return a + b;
	}
};

void test(parent* p)
{
	if(typeid(*p) == typeid(child)) 
	{
		child* c = (child*)p;
		cout << c->add(2,7) << endl; 
	}
	else
	{
		cout << "error!" << endl;
	}
}
int main()
{
	parent p1;
	child c1;
	test(&p1);
	test(&c1);
	return 0;
}
注意:最后这个程序有两个疑问~~~~
           第一、就是父类中一定还是需要有一个虚函数,如果类族中没有这个虚函数,typeid(*p)会一直跟typeid(parent)相等。根本就不会等于typeid(child),可以去掉上面程序中父类的虚函数试试!!!
           第二、为什么typeid(*p)能与typeid(child)相等,而typeid(p)却一直与typeid(parent*)相等!!!不能与typeid(child*)相等!!!



c++学习笔记(19.动态类型识别)

上一篇:C语言有哪些标准?


下一篇:C语言有哪些关键字