C++预处理器,typeid与强制类型转换专题

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

本文作者:黄邦勇帅(编著)(原名:黄勇)
本文是属于学习C++的附加内容,本文主要讲解了预处理器,#define,#if指令,typid的作用与使用方法,C++使用的四种强制类型转换,其中对dynamic_cast和reinterpret_cast两种转换作了深入细致的讲解。

本文由浅入深易学易懂,内容全面,是学习C++的不错的资料,相信本文能给读者带来对C++的重新认识和更深入的理解。

本文使用的是x86机器(主流计算机都是x86机器),windows xp操作系统,VC++2010编译器进行讲解的。

本文内容完全属于个人见解与参考文现的作者无关,限于水平有限,其中难免有误解之处,望指出更正。

声明:禁止抄袭,复印,转载本文,本文作者拥有完全版权。

主要参考文献:
1、C++.Primer.Plus.第五版.中文版[美]Stephen Prata著孙建春韦强译人民邮电出版社 2005年5月
2、C++.Primer第四版.中文版 Stanley B.Lippman、Barbara E.Moo、Josee Lajoie著李师贤、蒋爱军等译人民邮电出版社 2006年3月
3、C++.Primer第三版.中文版 Stanley B.Lippman、Josee Lajoie著潘爱民、张丽等译中国电力版社 2002年5月
4、《C++程序设计》作者谭浩强清华大学出版社 2004年6月
5、《C++程序设计语言》特别版[美]Bjarne Stroustrup著裘宗燕译机械工业出版社 2010年3月
第18部分 C++预处理器,typeid与强制类型转换专题(2016-7-14)
(共5页)
1、预处理器:预处理器是编译器把C++代码编译为机器指令之前执行的一个过程,所有的预处理器都以#开头,以便
与C++语句区分开来,#include预处理器指令在前面已经用过不少了
2、注意:预处理器指令必须作为第一个非空白字符,在之前不能有其他语句;而且预处理器之后不能有其他与预处理
器不相关的语句,也就是非预处理器语句必须另起一行。

本文把预处理器指令的语法都写在一行上,假设用户已做了正确的格式处理。

一、#define指令
1、#define指令格式:#define 标识符字符序列。

比如#define N 3;
1)、表示以后凡是使用到“标识符”的地方都被后面的“字符序列”替换。

注意该语句不以分号结束。

2)、编译器不会对“字符序列”进行类型检查。

也就是说字符序列可以是任意的字符。

3)、语句中的字符序列可以是任意的字符序列,而不仅仅是数字,比如#define PI HYONG这样的话在使用PI时就
会用HYONG来替换掉PI,替换之后HYONG可能会是一个未定义的标识符。

2)、示例:#define N 3
1)、表示以后使用到符号PI时都会被置换为3,比如int a[N];定义大小为3的数组。

2)、虽然N看起来和变量一样,但N和变量没有任何关系,N只是一个符号或标志,在程序代码编译前该符号
会用一组指定的字符来替换。

3)、可以看到,N被3置换后,相当于拥有常量的性质,但在C++中最好是用const来声名常量,比如const long
double PI=3.1416;。

2、怎样把预处理指令放在多行上:其方法为使用续行符符号”\”,该符号应在上一行的最后一个字符。

比如#define m \
kkielfml
3、删除#define定义的标志的语法:#define 标识符
在#define语句中,不为标识符指定置换字符串就表示该标识符被删除了。

比如#define PI表示在程序中该语句后面删除所有的PI标识符。

比如int a=PI;则PI不会替换为任何东西,语句int a=PI相当于是int a=;所以程序出错。

4、使用#undef取消#define的定义的语法:#undef 标识符
表示取消在#undef后定义的“标识符。

标识符被取消后,就不能再使用该标识符了。

5、带参数的#define的格式:#define 标识符(参数列表) 置换字符串
#define f(v) cout<<(v)<<endl;表示将f(v)用后面的字符串替换,其中的参数v也可以进行替换,比如在程序中可以这样调用f(3);就会把程序转换为cout<<(3)<<endl;结果程序输出3,注意这里的括号不会被输出。

参数列表也可以有多个参数,参数间用逗号隔开即可,比如#define f(m,n) cout<<(m)<<(n)<<endl;如果有调用f(3,4);程序转换为cout<<(3)<<(4)<<endl;输出34。

6、重点:使用#define只能对标识符进行简单的置换,也就是他会将标识符直接替换为后面表示的字符序列,而不会
进行算术优先级或类型的检查。

比如#define f(m,n) m*n 如果有调用f(4+2, 3);则该语句会被简单的替换为4+2*3这与我们所希望的(4+2)*3不一致,要解决这个问题就是给参数加上括号,比如#define f(m,n) (m)*(n)
8、使用字符串作为#define参数:比如#define m “kdi”如果有语句cout<<m;则会输出字符串kdi,但要注意,不能这样
做#define m dki cout<<”m”;不能在标志符前加上双引号以试图输出字符串dki,这样只会输出字符串m,因为程序会把”m”解释为一个字符串,而不会把它解释为cout<<”dki”。

9、把#define参数指定为字符串:其方法是在参数前加上符号”#”,比如#define f(m) cout<<#m 如果这时有f(dikl);则程
序将会转换为cout<<”dikl”,最后输出字符串dikl。

这里要注意的是该方法只能用于参数,而不能用于其他地方,比如#define m #kidkl这样就是错误的,这里试图用m来代替字符串”kidkl”,这是不成功的,正确方法为#define m “kidkl”
二、逻辑预处理器指令:
1、逻辑#if指令:该指令原理与条件语句if相同,如果测试为真就执行后面的语句,如果为假则跳过后面的语句。


指令有两种用法,其一可以用#if指令测试某个符号以前是否用#define指令定义过,这是最常用的用法,其二可以用来测试某个条件表达式是否为真。

2、#if指令用法一:测试某个符号是否以前用#define定义过,共有以下几种情形
1)、#if指令语法形式一:
语法为: #if defined 标识符…. #endif //注意关键字defined比define多一个字母d。

缩写形式为:#ifdef 标识符….#endif
意义:表示如果指定的“标识符”已被#define定义,则中间的语句就包含在源文件中,如果该标识符还未被#define定义,则跳过#if和#endif之间的语句,该语句以#endif结束。

2)、#if指令语法形式二:
语法:#if !defined 标识符…..#endif //注意关键字defined比define多一个字母d。

缩写形式为:#ifndef 标识符…..#endif
意义:表示如果指定的标识符没有定义,则把#if和#endif之间的语句包含在源文件中,如果标识符已定义则跳过#if和#endif之间的代码,系统常使用该语句防止头文件被多次包含,
3)、示例1:#define N //定义标识符N,此例可以不需要指定值。

void main(){ //注意:预处理器指令必须作为第一个非空白字符,因此不能在”{“后使用预处理器指令。

#if defined N //若标识符N被定义则执行以下语句,注意预处理器指令结束后必须另起一行(即必须输入换行符)。

cout<<"A"<<endl; //输出A。

#endif//注意:预处理器指令必须另起一行进行输入。

#ifndef N //若标识符N未被定义则执行以下语句
cout<<"B"<<endl; //无输出
#endif
} //注意:大括号必须另起一行,不能在#endif预处理器指令的后面
3、防止头文件被包含多次的语法:
#ifndef HY #define HY语句#endif //此处应使用正确的回车换行符
说明:程序在开始遇到标识符HY时没有被定义,这时执行后面的语句,并使用#define定义标识符HY,在第二次被使用时则标识符HY已经被定义,这时就不会执行后面的语句了,从而防止了同一头文件被包含多次的情
况。

这里要注意,使用#define后面定义的标识符可以不需要值。

4、#if指令用法二:测试某个表达式的值是否为真,其语法格式为:#if 常量表达式….#endif,注意常量表达式的求值
结果应是整数常量表达式,比如#if a=2 …. #endif测试a的值是否为2,如果为2则执行#if与#endif之间的语句。

5、多个#if选择块:和常规的if语句一样#if也有对应的#else和#elif语句,比如#if a=3 …. #else …. #endif表示如果a=3
则执行if后面且在#else前面的语句,如果为假则执行#else与#endif间的语句。

#elif用来实现多个选择,该语句和常规语句的else if相似,比如#if a=1 …. #elif a=2 …. #elif a=3…. #else …. #endif表示,如果a=1则执行#if后的语句,如果a=2则执行该条件后的语句。

三、RTTI运行时类型识别
使用以下函数需要包含头文件<typeinfo>
注意:有些编译器需要把RTTI的特性打开才能使用,比如VC++2010就有这项设置,位于项目/属性然后展开C/C++选项卡选择“语言”,在右侧找到“运行时类型识别”,然后选是。

1、静态类型与动态类型:静态类型指的是对象(包括指针和引用)在声明时的类型,动态类型指的是当前对象(包括指针
和引用)实际指向的类型。

比如B mb; A* p=&mb; 则p的静态类型是A*,而动态类型是B*,详见《C++继承,虚函数与多态性专题(修订版)》
2、RTTI运行时类型识别:使用运行时类型识别的主要作用是让我们能获得指向父类的指针实际所指向的子类的类型。

C++使用两个操作符来实现RTTI,即typeid和dynamic_cast,本小节介绍typeid操作符,dynamic_cast是强制类型转换运算符,位于下一小节。

3、typeid操作符形式:type_info& typeid(object) 其中object是任何类型的对象,也可以是一个类型。

typeid反回type_info
类型的引用。

4、通过引用typeid反回的type_info的成员函数,可以使用typeid获得对象类型的名称、判断两个类型是否相等等操
作。

详见下一条。

5、type_info类:
1)、type_info类的具体实现是依编译器而定的,不同编译器拥有不同的内容。

2)、type_info类的默认构造函数、复制构造函数和赋值操作符都是private私有的,因此不能创建、复制type_info
类型的对象,若要向type_info类中增加成员函数,则应该从type_info继承。

3)、程序中要引用type_info中成员函数的唯一方法是使用typeid反回的type_info类型的引用。

4)、type_info类的成员函数(重点):C++标准规定,所有编译器必须至少要实现以下成员函数:重载==操作符,重
载的!=操作符,成员函数name(),成员函数before(const type_info&),下面分别介绍这4个成员函数
①、重载的==和!=操作符用于判断两个类型是否相等,其中= =相等则反回true,!=不等反回true,比如
if(typid(3)!=typid(4.4))
②、name();成员函数:表示反回C风格字符串形式的类型名称,反回的称名依编译器而定,也就是说不同编译
器反回的名称可能会有一些不同。

比如typeid(3).name();反回字符串”int”。

③、before成员函数主要是为了使type_info类的信息能够排序,因此意义不大。

6、使用typeid操作符
1)、cout<<typeid(3).name(); //输出int; 表示反回整型值的类型。

2)、class A{}; cout<<typeid(A).name(); //输出class A; 反回类型A的类型。

3)、if(typeid(3)==typeid(4))cout<<”A”; //输出A,typeid比较和的类型,他们都是int型。

7、重点:只有当typeid的操作数是指向带有虚函数的类类型对象的指针时,且对该指针进行解引用后,才会反回指针
的动态类型,其余情况都是反回的指针的静态类型。

示例2:class A{public:virtual void f(){}}; class B:public A{}; B mb; A*pa=&mb;
cout<<typeid(pa).name(); //输出class A*,因为未对指针解引用,反回的是指针的静态类型。

cout<<typeid(*pa).name(); //输出class B; 反回指针实际指向的动态类型。

示例3:class A{}; class B:public A{}; B mb; A* pa=&mb;
cout<<typeid(pa).name(); //输出class A*,因为未对指针解引用,同时A没有虚函数,反回的是指针的静态类型。

cout<<typeid(*pa).name(); //输出class A; 因为A不含虚函数,所以反回指针的静态类型。

8、若指针p是指向带有虚函数类型的指针,且p=0;则typeid(*p)会抛出一个bad_typeid异常,typeid(p).name()仍然会得
到指针的静态类型。

比如class A{public:virtual void f(){}}; class B{}; B mb; A*pa=0; 则typeid(*p).name();会抛出bad_typeid异常,需把该语句放入try块对异常做处理,否则是错误的。

9、若指针p指向的类型不带虚函数,且p=0;则typeid(*p).name();则反回指针的静态类型。

四、强制类型转换
1、强制类型转换运算符:C++有四种强制类型转换符,分别是dynamic_cast,const_cast,static_cast,reinterpret_cast。

其中dynamic_cast与运行时类型转换密切相关。

使用这四种强制类型转换,主要是提高转换时的安全性,减少传统C语言强制转换时的随意性。

2、dynamic_cast强制转换运算符格式:dynamic_cast<目标类型>(表达式),//表示把表达式转换为尖括号中的目标类型
3、dynamic_cast转换符的作用:主要用于把父类类型的指针或引用转换为子类类型的指针或引用,以便能访问子类中
除了虚函数之外的其他特有成员,
示例4:class A{public:virtual void f(){}}; class B:public A{public: void g(){}}; A *pa=new B();
pa->g(); //错误,因为g不是虚函数也不是类A的成员函数
dynamic_cast<B*>(pa)->g(); //正确,表示把pa强制转换为子类B类型的指针,由此可以访问子类B中特有的成员。

4、使用dynamic_cast转换成功的条件(重难点):若被转换的指针或引用未指向或引用目标类型或目标类型的子类型,
则转换失败。

若表达式所指向的对象不止一个表示目标类型的父类则转换也是失败的(此一点出现于多重继承时,详见后文示例11)
1)、若是使用dynamic_cast转换到指针类型时失败,则反回0。

2)、若是使用dynamic_cast转换到引用类型时失败,则抛出bad_cast类型的异常,该异常位于<typeinfo>头文件
3)、理解dynamic_cast转换成功的条件(示例见下一条):比如:dynamic_cast<B&>(ma);则要求被转换的对象ma引
用目标类型B的对象,假设为B mb;即要求有形如:A& ma=mb;这样的语句,转换才能成功,当然若ma的类型就是B,则也能转换成功。

注意A sa; A& ma=sa; B mb; ma=mb;并不是使ma重新引用mb,而是使用mb 对ma所引用的对象sa赋值。

5、dynamic_cast强制转换符的限制(重点)
1)、dynamic_cast被转换的对象必须是含有虚函数的类,因此内置类型间的转换是不允许的。

示例5:dynamic_cast<int>(3.3);//错误,不能用于内置类型间的转换。

示例6:class A{}; class B:public A{}; A* pa=new B(); B *pb=dynamic_cast<B*>(pa);//错误,因为类A没有虚函数。

2)、dynamic_cast的目标类型必须是指针或引用。

示例7:class A{public:virtual void f(){}}; class B:public A{public: void g(){}}; A ma;
dynamic_cast<B>(ma); //错误。

目标类型B不是指针或引用
3)、除了满足前两个条件之外,被转换的指针必须要是已经初始化的,否则出错。

示例8:class A{public:virtual void f(){}}; class B:public A{public: void g(){}}; A *pa=0; A* pa1;
dynamic_cast<B*>(pa1);//错误。

pa1未被初始化
B
*pb=dynamic_cast<B*>(pa);//正确,转换失败,结果为0,注意pa使用0初始化,也是初始化。

4)、dynamic_cast的目标类型可以不是多态的,在多重继承下比较明显。

示例9、class A{public:virtual void f(){}}; class B{public:void g(){cout<<"GB"<<endl;}};
class C:public A,public B{}; //类C同时继承自类B和类A,其中类B是非多态的。

C mc; A* pa=&mc;
dynamic_cast<B*>(pa); //正确,目标类型B可以不是多态的,但pa必须是多态的。

5)、若被转换的指针或引用未指向或引用目标类型或目标类型的子类型,则转换失败;因此被转换的对象就是目标
类型的对象,转换是能成功的。

示例10:class A{public:virtual void f(){}}; class B:public A{public: void g(){cout<<"A"<<endl;}};
A ma;
B mb; A ma1=mb; A &sa=mb; A &sa1=ma; A* pa=&ma; A* pa1=&mb; B* pb=&mb;
//dynamic_cast<B&>(ma); //错误,ma未引用目标类型B的对象,抛出bad_cast异常
dynamic_cast<B&>(mb); //正确,mb的类型就是目标类型B
//dynamic_cast<B&>(ma1); //错误,ma1未引用目标类型B的对象,抛出bad_cast异常
dynamic_cast<B&>(sa); //正确,sa引用了目标类型B的对象mb
//dynamic_cast<B&>(sa1); //错误,sa1引用的是类A的对象ma,ma不是目标类型B的对象。

抛出bad_cast异常
cout<<dynamic_cast<B*>(pa)<<endl; //输出0,转换失败,pa未指向目标类型B
cout<<dynamic_cast<B*>(pa1)<<endl; //输出地址,转换成功,pa1指向目标类型B
cout<<dynamic_cast<B*>(pb)<<endl; //输出地址,转换成功,pb就是目标类型B*
6)、目标类型有多个时的转换失败,此时指针反回0,在多重继承时能表现出这种错误
示例11:class A{public:virtual void f(){}};
class B:public virtual A{public:}; //B虚拟继承自A,因此A只会有一个副本
class C:public B{}; //非虚拟继承,本例会存在类B的两个副本
class D:public B{}; //非虚拟继承,本例会存在类B的两个副本
class E:public D,public C{};
E me; A* pa=&me;
cout<<dynamic_cast<B*>(pa); //输出0,转换失败,因为存在两个B的副本。

6、由以上各点可看出dynamic_cast操作符在转换时执行两层检测,第一次检测转换是否有效,比如转换的目标类型是
否是指针等,若转换有效才进行转换,第二层检测发生在运行时刻,比如对指针转换失败反回0,对引用转换失败抛出bad_cast异常。

7、const_cast操作符格式:const_cast<目标类型>(表达式),//表示把表达式转换为尖括号中的目标类型
作用:该操作符用于改变const和volatile限定符(增加或删除),const_cast最常用的用途就是删除const属性,如果某个变量在大多数时候是常量,而在某个时候又是需要修改的,这时就可以使用const_cast操作符了。

比如int a=3; const int *p=&a; 则*p=4;错误,但*const_cast<int*>(p)=4;正确,cout<<*p; 输出4。

8、const_cast操作符的限制
1)、const_cast操作符只能改变const或volatile限定符(增加或删除),不能对其他类型进行转换,即const_cast不能
进行从int到double的转换等。

比如int a=4; int *p=&a; 则const_cast<float*>(p); 错误。

2)、const_cast只能用于指针或引用。

比如const int a=3; 则const_cast<int>(a);错误。

9、static_cast操作符:static_cast<目标类型>(表达式);该操作符用于非多态类型的转换,任何标准转换都可以使用他,
即static_cast可以把int转换为double,但不能把两个不相关的类对象进行转换,比如类A不能转换为一个不相关的类B类型。

static_cast本质上是传统c语言强制转换的替代品。

10、reinterpret_cast操作符格式:reinterpret_cast<目标类型>(表达式)
1)、要明白此操作符的函数,需要明白内存与类型的解释,请看本节最后或《C++指针与数组专题》
2)、目标类型必须是一个指针、引用、整型(包括所有int型、char型、bool型、枚举型)
3)、reinterpret_cast可以实现从整型到指针,从指针到整型,从一个指针到其他任意指针间的相互转换。

4)、深入理解reinterpret_cast操作符(难点):reinterpret_cast操作符可以用来处理与类型无关的转换,转换后的值
与原始表达式所表示的值有完全相同的比特位。

说简单一点,就是对内存中的比特位重新赋予了一种类型或者说对比特位重新进行解释。

因此这种转换是很危险的,需要小心使用。

比如假设在内存中的比特位为0111 1111 0100 0110,若这串比特位按int型解释,那就是十进制数的32582,若按char型解释则因为char只占一个字节,因此只能解释8位,假设解释的是最后8位,则0100 0110=70,相当于char型的字符F,那么假设有int a=32582;
则char c=reinterpret_cast<char&>(a);则cout<<c;将输出F,reinterpret_cast<char&>(a)表示把存储在内存中a的值的比特位按char型进行解释,结果就是字符F。

5)、为什么reinterpret_cast可以在任意指针之间相互转换:因为指针类型占据的内存宽度都是一样(一般为4字节),
因此指针之间是可以相互转换的,指针指向的类型只影响指针解释的内存的宽度,但对指针本身没有影响,比如假设int *p指向位置0000 0001(16进制),因为p是int型(假设占4字节),因此*p会解释4字节的内容假设为0000 0001~0000 0004; 若p是float型,即float *p;同样指向位置0000 0001,假设float占8字节,因此*p会解释8字节的内容,假设为0000 0001~0000 0008,此处可见,对于指针是int型还是float型,对指针本身p指向的位置没有影响,int和float只影响指针对内存中的内容怎样解释。

6)、由于内存的地址是用一个数字表示的原因,因此reinterpret_cast操作数允许整型到指针间的相互转换。

7)、因为是对比特位重新进行解释而没有进行相互的值转换,因此不能在不是比特级别上的类型之间进行相互转换,
比如reinterpret_cast<int>(3.3);错误。

按照常规转换之后的值可能为3,这会导致对比特位进行重新排列。

8)、从以上的原理可以看到,使用reinterpret_cast在指针间进行相互转换时,最好是在按比特位进行解释的相同宽
度的范围之内进行转换。

比如int a; reinterpret_cast<float *>(&a);则转换之后按float型解释的比特位超过了按int 类型进行解释的范围。

示例12:int a=3; float *p=reinterpret_cast<float*>(a);
分析:转换后p指向内存地址为0000 00003(16进制)处的位置。

即把a的值在内存中的表示形式转换为了内存地址。

这种转换会发生严重错误,因为两个原因,首先地址0000 00003的内存地址不知道存储的是什么,其
次在解引用时,假设int只占4字节,而float占8字节,这里会多解释4字节的未知内存的内容。

示例13:int a=3; float *p=reinterpret_cast<float*>(&a);
分析:转换后,p指向a所在的内存位置,假设为0000 00001(16进制),但对*p的解释就各不相同了,因为*a会按int型只解释0000 00001~0000 00004这4个字节,但*p因为需要对内存按float型进行解释,float型比int
占据更宽的内存,假设为8字节,则*p可能需要解释0000 00001~0000 00008,解释超出了4个字节未知的
内容,因此*p的值是未知的。

11、从C语言继承来的似统的(T)e强制类型转换方法,可以实现static_cast,reinterpret_cast、const_cast三种组合的任
意转换。

12、类型与内存
内存中的比特值的含义完全决定于这块内存单元所表示的类型,保存在计算机中的值是一些二进制比特,这些二进制比特对于计算机来讲,它并不知道代表什么意义,只有当我们决定如何解释这些比特时才有意义,比如65或字符’a’在内存中的比特值是相同的,若将比特值解释为int型,则他是一个十进制数,若解释为char型,则是字符a,因此在内存单元中的数据应具有一个类型,当类型确定后就能对其中的数据作出正确的解释了。

作者:黄邦勇帅(原名:黄勇)
(2016年7月14日)。

相关文档
最新文档