c++复习自存--类型转换运算符、宏和模板
类型转换运算符
C++类型转换运算符
虽然类型转换有缺点,但也不能抛弃类型转换的概念。在很多情况下,类型转换是合理的需求,可解决重要的兼容性问题。C++提供了一种新的类型转换运算符,专门用于基于继承的情形,这种情形在 C 语言编程中并不存在。
4 个 C++类型转换运算符如下:
- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
这 4 个类型转换运算符的使用语法相同:
destination_type result=cast_operator<destination_type>(object_to_cast);1.static_cast
static_cast用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换—这种转换原本将自动或隐式地进行。用于指针时,static_cast实现了基本的编译阶段检查,确保指针被转换为相关类型。这改进了 C 风格类型转换,在 C 语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用 static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型,如下面的示例代码所示:
Base*objBase=newDerived();Derived*objDer=static_cast<Derived*>(objBase);// ok!将 Derived转换为 Base被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换:
Derived objDerived;Base*objBase=&objDerived;// ok!将 Base转换为 Derived被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换:
Derived objDerived;Base*objBase=&objDerived;// Upcast -> ok!Derived*objDer=objBase;// Error: Downcast needs explicit cast然而,static_cast只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast编写如下代码,而编译器不会报错:
Base*objBase=newBase();Derived*objDer=static_cast<Derived*>(objBase);// Still no errors!其中 objDer 实际上指向一个不完整的 Derived 对象,因为它指向的对象实际上是 Base()类型。由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此objDer -> DerivedFunction()能够通过编译,但在运行阶段可能导致意外结果。
除用于向上转换和向下转换外,static_cast 还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读者的注意:
doublePi=3.14159265;intnum=static_cast<int>(Pi);// Making an otherwise implicit cast, explicit在上述代码中,使用 num = Pi 将获得同样的效果,但使用 static_cast 可让代码阅读者注意到这里使用了类型转换,并指出(对知道 static_cast 的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。对于使用关键字 explicit 声明的转换运算符和构造函数,要使用它们,也必须通过 static_cast。
2.dynamic_cast
顾名思义,与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。
可检查 dynamic_cast 操作的结果,以判断类型转换是否成功。使用 dynamic_cast 运算符的典型语法如下:
destination_type*Dest=dynamic_cast<class_type*>(Source);if(Dest)// Check for success of the casting operationDest->CallFunc();例如:
Base*objBase=newDerived();// Perform a downcastDerived*objDer=dynamic_cast<Derived*>(objBase);if(objDer)// Check for success of the castobjDer->CallDerivedFunction();如上述代码所示,给定一个指向基类对象的指针,程序员可使用dynamic_cast进行类型转换,并在使用指针前检查指针指向的目标对象的类型。在上述示例代码中,目标对象的类型显然是Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将Derived*传递给接受Base*参数的函数时。该函数可使用dynamic_cast判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用 dynamic_cast 在运行阶段判断类型,并在安全时使用转换后的指针。
这种在运行阶段识别对象类型的机制称为运行阶段类型识别(runtime type identification,RTTI)。
使用动态转换判断 Fish 指针指向的是否是 Tuna 对象或 Carp 对象:
0:#include<iostream>1:usingnamespacestd;2:3:classFish4:{5:public:6:virtualvoidSwim()7:{8:cout<<"Fish swims in water"<<endl;9:}10:11:// base class should always have virtual destructor12:virtual~Fish(){}13:};14:15:classTuna:publicFish16:{17:public:18:voidSwim()19:{20:cout<<"Tuna swims real fast in the sea"<<endl;21:}22:23:voidBecomeDinner()24:{25:cout<<"Tuna became dinner in Sushi"<<endl;26:}27:};28:29:classCarp:publicFish30:{31:public:32:voidSwim()33:{34:cout<<"Carp swims real slow in the lake"<<endl;35:}36:37:voidTalk()38:{39:cout<<"Carp talked Carp!"<<endl;40:}41:;42:43:voidDetectFishType(Fish*objFish)44:{45:Tuna*objTuna=dynamic_cast<Tuna*>(objFish);46:if(objTuna)// check success of cast47:{48:cout<<"Detected Tuna. Making Tuna dinner: "<<endl;49:objTuna->BecomeDinner();50:}51:52:Carp*objCarp=dynamic_cast<Carp*>(objFish);53:if(objCarp)54:{55:cout<<"Detected Carp. Making carp talk: "<<endl;56:objCarp->Talk();57:}58:59:cout<<"Verifying type using virtual Fish::Swim: "<<endl;60:objFish->Swim();// calling virtual function Swim61:}62:63:intmain()64:{65:Carp myLunch;66:Tuna myDinner;67:68:DetectFishType(&myDinner);69:cout<<endl;70:DetectFishType(&myLunch);71:72:return0;73:}务必检查 dynamic_cast 的返回值,看它是否有效。如果返回值为 NULL,说明转换失败。
3.reinterpret_cast
reinterpret_cast 是 C++中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:
Base*objBase=newBase();Unrelated*notRelated=reinterpret_cast<Unrelated*>(objBase);// The code above compiles, but is not good programming!这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为 API(应用程序编程接口)能够接受的简单类型(例如,有些 OS 级 API 要求提供的数据为 BYTE 数组,即 unsigned char*):
SomeClass*object=newSomeClass();// Need to send the object as a byte-stream...unsignedchar*bytesFoAPI=reinterpret_cast<unsignedchar*>(object);上述代码使用的类型转换并没有改变源对象的二进制表示,但让编译器允许程序员访问 SomeClass对象包含的各个字节。由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用reinterpret_cast来执行不安全(不可移植)的转换。
应尽量避免在应用程序中使用
reinterpret_cast,因为它让编译器将类型 X 视为不相关的类型Y,这看起来不像是优秀的设计或实现。
4.const_cast
const_cast 让程序员能够关闭对象的访问修饰符 const。您可能会问:为何要进行这种转换?在理想情况下,程序员将经常在正确的地方使用关键字 const。不幸的是,现实世界并非如此,像下面这样的代码随处可见:
classSomeClass{public:// ...voidDisplayMembers();//problem - display function isn't const};在下面的函数中,以 const 引用的方式传递 object 显然是正确的。毕竟,显示函数应该是只读的,不应调用非 const 成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers()本应为 const的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对DisplayMembers()进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast将是您的救星。
voidDisplayAllData(constSomeClass&object){object.DisplayMembers();// Compile failure// reason: call to a non-const member using a const reference}在这种情况下,调用 DisplayMembers()的语法如下:
voidDisplayAllData(constSomeClass&object){SomeClass&refData=const_cast<SomeClass&>(object);refData.DisplayMembers();// Allowed!}除非万不得已,否则不要使用 const_cast 来调用非 const 函数。一般而言,使用 const_cast 来修改const 对象可能导致不可预料的行为。
另外,const_cast 也可用于指针:
voidDisplayAllData(constSomeClass*data){//>*pCastedData=const_cast<SomeClass*>(data);pCastedData->DisplayMembers();// Allowed!}宏和模板
宏也进行文本替换。预处理器只是就地将标识符替换为指定的文本。
使用 assert 宏验证表达式
编写程序后,立即单步执行以测试每条代码路径很不错,但对大型应用程序来说可能不现实。比较现实的做法是,插入检查语句,对表达式或变量的值进行验证。
assert 宏让您能够完成这项任务。要使用 assert 宏,需要包含<assert.h>,其语法如下:
assert(expression that evaluates totrueorfalse);下面是一个示例,它使用 assert( )来验证指针的值:
#include<assert.h>intmain(){char*sayHello=newchar[25];assert(sayHello!=NULL);// throws a message if pointer is NULL// other codedelete[]sayHello;return0;}在大多数开发环境中,assert( )通常在发布模式下被禁用,因此它仅在调试模式下显示错误消息。
另外,在有些开发环境中,assert()被实现为函数,而不是宏。
由于断言在发布模式下不可用,对于对应用程序正确运行至关重要的检查(如检查
dynamic_cast 的返回值),为了确保它们在发布模式下也会执行,应使用 if 语句,这很重要。断言可帮助您找出问题,但不能因此不在代码中对指针做必要的检查。
模板简介
模板无疑是 C++语言中最强大却最少被使用的特性之一。
在 C++中,模板让程序员能够定义一种适用于不同类型对象的行为。这听起来有点像宏(参见前
面用于判断两个数中哪个更大的简单宏 MAX),但宏不是类型安全的,而模板是类型安全的。
模板声明语法
所有模板必须以template关键字开头,尖括号内声明模板形参,有两种基础修饰符:
class/typename:代表类型参数,接收任意数据类型(int、string、自定义类等),二者在模板参数中等价;- 普通类型:代表非类型参数,仅接收编译期常量。
基础语法格式:
template<typenameT>// 后续函数/类定义各种类型的模板声明
- 类型参数模板(最常用)
template<classT>voidswap(T&a,T&b);- 非类型参数模板(常量参数)
template<intN>intgetSize(){returnN;}- 模板的模板参数(参数本身是模板)
template<template<typename>classContainer>voidtest(){}模板函数
也称泛型函数,将函数的参数、返回值类型抽象为模板参数,一套函数逻辑兼容所有匹配类型。
#include<iostream>usingnamespacestd;template<typenameT>TmaxVal(T a,T b){returna>b?a:b;}intmain(){cout<<maxVal(3,9);cout<<maxVal(2.5,1.8);return0;}编译器会根据调用时传入的实参类型,自动生成对应类型的函数实体。
模板与类型安全
模板是编译期展开,不存在运行时类型转换开销,同时具备强类型校验:
- 实例化时会对传入类型完整语法检查,类型不匹配直接编译报错;
- 不会像void*那样丢失类型信息,全程保留完整类型信息;
- 不支持隐式非法类型转换,从根源规避类型错乱问题。
模板类
将整个类抽象为模板,类内成员变量、成员函数均可使用模板参数,标准容器vector<T>、map<K,V>均为模板类。
template<typenameT>classStack{private:T data[100];inttop=0;public:voidpush(T val){data[top++]=val;}Tpop(){returndata[--top];}};intmain(){Stack<int>intStk;Stack<string>strStk;return0;}声明包含多个参数的模板
模板尖括号内可逗号分隔,定义多个模板形参,支持同时定义类型参数、常量参数。
// 两个类型参数template<typenameT,typenameU>autoadd(T a,U b){returna+b;}// 类型+常量混合多参数template<typenameT,intMAX>classArray{};声明包含默认参数的模板
模板参数可在声明时赋予默认值,使用时若不传入对应实参,自动采用默认类型/常量。
// 默认类型为inttemplate<typenameT=int>classNum{};// 默认常量为100template<intN=100>intgetLen(){returnN;}Num<>a;// 等价于 Num<int>规则:带默认值的参数必须写在参数列表尾部。
一个模板示例
完整综合示例:多参数、默认参数的模板类
#include<iostream>#include<string>usingnamespacestd;template<typenameT=string,intCap=50>classBuffer{private:T buf[Cap];public:voidset(intidx,T val){buf[idx]=val;}Tget(intidx){returnbuf[idx];}intcapacity(){returnCap;}};intmain(){Buffer<>strBuf;Buffer<int,20>intBuf;strBuf.set(0,"demo");cout<<strBuf.get(0);return0;}模板的实例化和具体化
- 隐式实例化
仅使用模板、不手动指定类型,编译器根据上下文自动生成对应代码:
template<typenameT>voidfunc(T){}func(10);// 隐式实例化 func<int>- 显式实例化
手动告知编译器生成指定类型模板实体,常用于分离编译场景:
templatevoidfunc<double>(double);- 模板特化(具体化)
针对某一种特定类型,重写专属实现,覆盖通用模板逻辑:
template<typenameT>voidprint(T x){cout<<x;}// 字符串类型专属特化版本template<>voidprint<string>(string s){cout<<"字符串:"<<s;}模板类和静态成员
模板类的静态成员每个实例化类型独立拥有一份,不同类型的模板对象不共享静态变量:
template<typenameT>classTest{public:staticintcnt;};template<typenameT>intTest<T>::cnt=0;intmain(){Test<int>::cnt++;Test<double>::cnt++;// Test<int>::cnt = 1,Test<double>::cnt = 1,互不干扰return0;}参数数量可变的模板(可变参数模板)
C++11引入,模板可接收任意个数、任意类型的参数,依靠递归展开处理参数包,广泛用于打印、转发函数。
// 递归终止函数voidprint(){}// 可变参数模板template<typenameT,typename...Args>voidprint(T first,Args...rest){cout<<first<<" ";print(rest...);}intmain(){print(1,3.14,"text",'c');return0;}使用static_assert执行编译阶段检查
static_assert结合模板实现编译期静态断言,在编译阶段校验模板参数合法性,不满足条件直接终止编译并输出提示,无运行时开销。
template<intN>voidcheckSize(){// 编译期校验:N必须小于100static_assert(N<100,"数值不能大于等于100");}checkSize<50>;// 正常checkSize<200>;// 编译报错