第10章 表 达 式
本章描述C++的表达式,表达式是用于一个或多个以下目的的运算符和操作数序列:
* 从操作数计算出一个值
* 设计对象或函数
* 产生“副作用”(副作用是非表达式求值的任何动作,例如,修改一个对象的值)。
在C++中,运算符可被重载而且它们的含义可由用户定义,但是它们的优先级以及所带操作数的个数不能被修改。本章描述该语言中所提供的而非重载的运算符的语法和语义,包括以下主题:
* 表达式的类型
* 表达式的语义
* 造型转换
(有关重载的运算符的更多信息参见第12章“重载”中的“重载的运算符”)。注意:内部类型的运算符不能被重载,它们的行为是预先定义好的。
C++表达式分为以下几类:
* 基本表达式,这些是构成所有其它表达式的基础。
* 后缀表达式,这些是基本表达式后面跟随一个运算符,例如,数组下标或后缀增1运算符。
* 由单目运算符构成的表达式,单目运算符只能作用表达式中的一个操作数。
* 由双目运算符构成的表达式,双目运算符作用于表达式中的两个操作数。
* 带条件运算符的表达式,条件运算符是一个三目运算符,在C++语言中只有这种运算符带三个操作数。
* 常量表达式,常量表达式完全由常量数据构成。
* 带显式类型转换的表达式,显式类型转换或“造型转换”可以用在表达式中。
* 带成员指针运算符的表达式。
* 造型转换,类型安全的“造型转换”可以用于表达式中。
* 运行类型信息,指出程序执行期间对象的类型。
基本表达式
基本表达式是构造更复杂表达式的基础。它们是文字、名称和范围分辨运算符(::)
限定的名称。
语法
基本表达式:
文字
this
::标识符
::运算符函数名称
::限定名称
(表达式)
名称
文字是一个常量基本表达式,其类型依赖于其说明的形式,有关说明文字的全部信息参见第1章“词法规定”中的“文字”。
this关键字是一个类对象指针,它在非静态成员函数内可以使用,且指向被调用函数的类的实例,this关键字不能用在类成员函数体之外。
this指针的类型是函数内的type *const(其中type是类名),它特别地不修改this指针。以下例子给出了成员函数说明以及this类型 :
class Example
{
public:
void Func();//*const this
void Func() const;//const * const this
void Func() volatile; //volatile *const this
}
有关修改this指针的类型的更多信息参见第8章“类”中的“this指针的类型”。
范围分辨运算符(::)后面跟随一个标识符、运算符函数名称或限定名称构成一个基本表达式。这种表达式的类型通过标识符、运算符函数名称或名称的说明加以指定。如果说明的名称值为1(1-value),则它也是值为1(1-value)。范围分辨运算符允许指示一个全局名称,即使那个名称被隐藏在当前范围内。有关如何使用范围分辨运算符的例子参见第2章“基本概念”中的“范围”。
包含在圆括号内的表达式是一个基本表达式,其类型和值与那些非括号括起的表达式的是完全相同的。如果非括号括起的表达式是l值的,则它也是l值的。
名称
在C++基本表达式的语法中,一个名称是一个基本表达式,它仅可出现在成员选择运算符(.或->)之后并命名一个类的成员。
语法
名称:
标识符
运算符函数名称
转换函数名称
~类名称
限定名称
已被说明的任何标识符是一个名称。
一个运算符函数名称是一个说明为如下形式的名称:
operator 运算符名称(参量1 [,参量2]);
有关运算符函数名称的更多信息,参见第12章“重载”中的“重载的运算符”。一个转换函数名称是一个说明为如下形式的名称:
运算符 类型名称()
注意:在说明一个转换函数时,你可以用一个派生的类型名称如char 替代类型名称。
转换函数提供向用户定义类型以及从用户定义类型的转换。有关用户提供的转换的更多信息,参见第11章“特殊成员函数”中的“转换函数”。
被说明为类名称的名称被作为一个类类型对象的“析构函数”,该析构函数在一个对象生存期的结束地方执行清除操作。有关析构函数的信息,参见第11章“特殊成员函数”中的“析构函数”。
限定名称
语法
限定名称:
限定类名称::名称
如果一个限定类名称后面跟随范围分辨运算符(::),再后是那个类或那个类的基类的成员名称,那么范围分辨运算符被认为是一个限定名称,一个限定名称的类型与成员的类型相同,而且限定名称表达式的结果是成员,如果成员是l值,则限定名称也是l值。有关说明限定类名称的信息,参见第6章“说明”中的“类型指示符”或第8章“类”中的“类名称”。
一个限定类名称的类名称部分可通过在当前或包围的范围中相同名称的重新说明而加以隐藏,类名称仍能够被找到和使用。有关如何使用一个限定类名称以访问一个隐藏类名称的例子参见第2章“基本概念”中的“范围”。
注意:类名称::类名称和类名称::~类名称的形式的类构造函数和析构函数分别必须指相同的类名称。
带有多于一个限定的名称,如下面所示,指定了一个嵌套类的一个成员:
类名称::类名称::名称
后缀表达式
后缀表达式构成基本表达式或后缀运算符跟在一个基本表达式后的表达式。后缀运算符列在表4.1中。
表4.1 后缀运算符
运算符名称 | 运算符符号 |
下标运算符 | [ ] |
函数调用运算符 | ( ) |
显式类型转换运算符类型名 | type-name() |
成员选择运算符 | .或-> |
后缀增1运算符 | ++ |
后缀减1运算符 | -- |
语法
后缀表达式:
基本表达式
后缀表达式[表达式]
后缀表达式(表达式表opt)
简单类型名(表达式表opt)
后缀表达式.名称
后缀表达式->名称
后缀表达式++
后缀表达式--
表达式表:
赋值表达式
表达式表,赋值表达式
下标运算符
一个后缀表达式后面跟随着下标运算符[],用于指定数组索引。表达式之一必须为指针或数组类型,即必须已被说明为type *或type[]。其它表达式必须为一个整数类型(包括枚举类型)。在通常用法中,括在方括号中的表达式是整型之一,但并没有严格的要求,考虑以下例子:
MyType m[10]; //说明一个用户定义类型的数组
MyType n1=m[2]; //选择数组的第三元素
MyType n2=2[m]; //选择数组的第三元素
在前面例子中,表达式m[2]与2[m]完全相同。尽管m不是一个整数类型,但效果是相同的。m[2]与2[m]之所以相等是因为下标表达式e1[e2]的结果由*((e2)+(e1))给定。
由表达式产生的地址不是距离地址e1的e2字节,而是地址被定位以产生数组e2中的下一个对象,例如:
double aDb1[2];
aDb[0]和aDb[1]的地址分别是8个字节,即-个double类型的对象的尺寸,这样按对象类型确定尺寸是由C++语言自动完成的,且在本章后面的“附加的运算符”中定义,其中还讨论了指针类型操作数的加法和减法。
正和负下标
数组的第一个元素是元素0,C++数组的范围是从array[0]到array[尺寸-1],但C++支持正和负下标。负下标必须落在数组界限内或结果是不可预料的。以下例子说明了这个概念:
#include <iostream.h>
void main()
{
int iNumberArray[1024];
int *iNumberLine = &iNumberArray[512];
cout << iNumberArray[-256] << "/n"; //不可预料的
cout << iNumberLine[-256] << "/n"; //可行的
}
在iNumberArray中的负下标可能产生运行错误,因为它产生一个比原数组在存储器中低256个字节的一个地址。iNumberLine对象被初始化为iNumberArray的中间量,因此对它既可以使用正数组索引也可以使用负数组索引。数组下标错误不产生编译错误,但它们产生不可预料的结果。
下标运算符是可以互换的,因此,只要下标运算符不被重载,则表达式array[index]和index[array]被认为是相等的。(参见第12章“重载”中的“重载的运算符”)。第一种格式是最常见的编码用法,不过两者都行。
函数调用运算符
一个后缀表达式后面跟着函数调用运算符(),则指定一函数调用,函数调用运算符的参量是0个或多个表达式,函数的实际参量由逗号隔开。
后缀表达式必须是以下类型之一:l 函数返回类型T,一个说明例子为: T func(int i)l 函数返回类型T的指针,一个说明例子为:
T (*func)(int i)
* 函数返回类型T的引用,一个说明例子为:
T (&func)(int i)
* 成员指针函数间接引用返回类型T,函数调用例子为:
(pObject->*pmf)();
(Object.*pmf)();
形式参量与实际参量
调用程序以“实际参量”形式向被调用函数传递信息,被调用函数使用相应的“形式参量”访问信息。
当一个函数被调用时,执行以下任务:
* 所有的实际参量(那些由调用者提供)被求值,这些变量被求值时没有隐式地顺序,但在进入函数之前所有的变量被求值,且所有的副作用被完成。
* 每个形式参量用其表达式表中相应的实际参量进行初始化(一个形式参量是一个在函数头部已作说明,并用于函数体内的参量),转换被完成好象通过初始化,即标准的和用户定义的转换均被执行以将实际参量转换为正确的类型。初始化的执行由以下例子进行概念性地说明:
void func(int i);//函数原型
...
Func(7); //执行函数调用
调用前的概念性初始化为:
int Temp_i=7;
Func(Temp_i);
注意,初始化被执行就好象是使用等号语法而不是括号语法,i的一个拷贝在向函数传递值之前被作好(有关更多的信息,参见第7章“说明符”中的“初始化表达式”,第11章“特殊成员函数”中的“转换”、“使用特殊成员函数的初始化”和“显式初始化”)。
因此,如果函数原型(说明)调用一个long类型参量,而如果调用程序提供一个int类型的实际参量,则实际参量用标准类型转换提升为long类型(参见第3章“标准转换”)。
提供一个实际参量而没有标准的或用户定义的转换将其转换为形式参量的类型则是一个错误。
对于类类型的实际参量,形式参量通过调用类构造函数被初始化(有关这些特殊类成员函数的更多信息,参见第11章“特殊成员函数”中的“构造函数”)。
* 函数调用被执行。
以下程序段说明了一个函数调用:
void fanc(long param1,double param2);
void main()
{
int i,j;
//用实际参量i和j调用func
func(i,j);
...
}
//用形式参量param1和param2定义func
void func(long param1,double param2)
{
...
}
当从main调用func时,形式参量param1用i值(使用标准转换将i转换为long类型相对应的正确类型)进行初始化,形式参量param2用j值(使用标准转换将j转换为double类型)进行初始化。
参量类型的处理
被说明为常量类型的形式参量不能在函数体内进行改变,函数可以改变任何非const类型的变量。但是改变相对函数是局部的,不会影响实际参量的值,除非实际参量是一个非const类型对象的一个引用。
以下函数说明了一部分这些概念:
int func1(const int i,int j,char *c)
{
i=7; //错误:i是常量
j=1; //可以,但j值在返回值丢失
*c=′a′+j; //可以:在调用函数中改变c的值
return i;
}
double& func2(double& d,coust char *c)
{
d=14.387; //可以:在调用函数中改变d的值
*c=′a′; //错误:c是一个指向常量对象的指针
return d;
}
省略号和缺省参量
函数可以说明为接受比函数定义中指定参量更少的情况,这是通过使用以下两种方法之一实现的:省略号(...)或缺省参量。
省略号意味着参量可能被要求,但说明中没有指定数目和类型。这通常是糟糕的C++编程方法,因为它使C++的益处之一即类型安全性失败。不同的转换用于使用省略号说明的函数而不是那些形式和实际参量类型已知的函数:
* 如果实际参量为float类型,则它在函数调用前被提升为double类型。
* 任何有符号的或无符号的char、short、枚举类型或位域使用整型提升被转换为有符号的或无符号的int。
* 任何作为一个数据结构的值传递的类类型参量;拷贝是通过二进制拷贝创建的而不是通用调用类的拷贝构造函数(如果存在的话)实现的。
省略号,如果使用,必须在参量表的最后进行说明。有关传递可变个数参量的更多信息,参见“Microsoft Visual C++6.0参考库”中的“Microsoft Visual C++6.0运行库参考”中的va_arg、va_start和va_list的讨论。
缺省参量能使你能够指定这样的一个参量的值,假设它在函数调用中不提供。以下代码段给出了缺省参量的工作情况,有关指定缺省参量的限制的更多信息见第7章中的“缺省参量”。
#include <iostream.h>
//说明一个print函数打印字符串及一个终止符。
void print (const char *string, const char *terminator="/n");
void main()
{
print("hello,");
print("World!");
print("good morning",",");
print("sunshine.");
}
//定义print
void print(char *string, char *terminator)
{
if (string!=NULL)
cont << string;
if (terminator!=NULL)
cout << terminator;
}
前面的程序说明了一个函数print,它带两个参数,但第二个参量terminator有一个缺省值“/n”。在main中,开头两次调用print允许缺省的第二个参量提供一个换行终止打印的字符串,第三个调用为第二个参量指定了明确的值,程序的输出为:
hello,
world!
good morning, sunsline.
函数调用结果
一个函数调用求值为一个r值,除非函数被说明为一个引用类型。带引用的函数返回类型求值为l值,而且可以用于一个赋值语句的左侧,如下所示:
#include <iostream.h>
class Point
{
public:
//定义“accessor”函数为引用类型
unsigned & x() { return _x;}
unsigned & y() { return _y;}
private:
unsigned _x;
unsigned _y;
};
void main()
{
Point ThePoint;
ThePoint.x( )=7; //使用x()作为一个l值
unsigned y=ThePoint.y( ); //使用y()作为一个r值
//使用x()和y()作为r值
cout << "x=" << ThePoint.x() << "/n"
<< "y=" << ThePoint.y() << "/n";
}
000000前面的代码定义了一个类调用Point,包含表示x和y坐标的私有数据对象,这些数据对象必须被修改且它们的值被检索。该程序只是这样的一个类的几种设计之一,使用GetX和SetX或者GetY和SetY函数是另一种可能的设计。
返回类类型、类类型指针或类类型引用的函数可被用作成员选择运算符的左操作数,因此,以下代码是合法的:
class A:
{
public:
int SetA(int i){ return(I=i); }
int GetA() { return I; }
private:
int I;
};
//说明三个函数
//func1,返回类型A
//func2,返回类型A的一个指针
//func3,返回类型A的一个引用
A func1();
A* func2();
A& func3();
int iResult=func1().GetA();
func2()->SetA(3);
func3().SetA(7);
函数可被递归地调用,有关函数说明的更多信息参见第6章“说明中的函数指示符”,第8章“类”中的“指示符”和“成员函数”。相关的材料在第2章“基本概念”中的“程序和连接”中。
成员选择运算符
一个后缀表达式后面跟随着一个成员选择运算符(.)和一个名称,是一个后缀表达式的另一种例子。成员选择运算符的第一个操作数必须有类或类引用类型,第二个操作数必须标识那个类的一个成员。
表达式的结果是成员的值,而且如果被命名的成员值为1,则它也值为1。
一个后缀表达式后面跟随着一个成员选择运算符(->)和一个名称,是一个后缀表达式。成员选择运算符的第一个操作数必须有一个类对象的类型指针(说明为class、struct或union类型的一个对象);第二个操作数必须指定那个类的一个成员。
表达式的结果是成员的值,而且如果被命名的成员值为1,则它也是值为1。->运算符间接引用指针,因此,表达式e->member和(*e).member(其中e表示一个表达式)产生完全相同的结果(除了运算符->或*被重载时之外)。
当一个值通过一个联合的某成员被存储,但通过另一个成员被检索时,没有转换被执行。以下程序将数据作为int存入对象U,但检索数据却作为char类型的两个分开的字节:
#include <iostream.h>
void main()
{
struct ch
{
char b1;
char b2;
};
union u
{
struct ch uch;
short i;
};
u U;
U.i=0x6361; //“ac”的位模式
cout << U.uch.b1 << U.uch.b2 << "/n";
}
后缀增1及减1运算符
C++提供前缀及后缀增1和减1运算符;本节仅描述后缀增1和减1运算符。(更多的信息参见第12章“重载”中的“增1和减1”)。两者的区别在于,在后缀表示法中,运算符出现在后缀表达式之后,而在前缀表示法中,运算符出现在表达式前。以下例子给出了一个后缀增1运算符
:i++使用后缀增1或“后增1”运算符(++)的作用是操作数增加相应类型的一个单位量。同样地,使用后缀减1或“后减1”运算符(--)的作用是操作数减少相应类型的一个单位量。
例如,使用后缀增1运算符于一个long类型对象数组的一个指针,实际上指针内部表示法中增加了4。此动作使先前指向数组第n个元素的指针指向第(n+1)个元素。
后缀增1和后缀减1运算符的操作数必须是算术或指针类型的可修改(非const)的l值,后缀增1或后缀减1表达式的结果是使用增1运算符之前的后缀表达式的值,结果的类型与后缀表达式的相同,但不再是一个l值。
以下代码说明了后缀增1运算符:
if (var++>0)
*p++=*q++;
在此例中,变量var与0相比,而后增1。如果var在加1之前为正数,则执行下一条语句。首先,由q指向的对象的值赋给由p指向的对象,然后,q和p都增1。后增1和后减1,当被用于枚举类型时,产生整型值。因此,以下代码是非法的:
enum Days
{
Sunday=1,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
void main()
{
Days Today = Tuesday;
Days SaveToday;
SaveToday = Today++; //错误
}
这段代码的目的是保存today的星期号,然后移到下一天,但是结果是today++表达式产生一个int即在赋给枚举类型Days的对象时出现一个错误。
带单目运算符的表达式
单目运算符仅作用于表达式中的一个操作数,单目运算符有:
* 间接运算符(*)
* 取地址运算符(&
* 单目加运算符(+)
* 单目负运算符(-)
* 逻辑非运算符(!)
* “1”的求补运算符(~)l 前缀增
1运算符(++)l 前缀减
1运算符(--)l sizeof运算符
* new运算符
* delete运算符
这些运算符具有从右向左的结合律。
语法
单目表达式:
后缀表达式
++单目表达式
--单目表达式
单目运算符 造型转换表达式
sizeof.单目表达式
sizeof(类型名称)
分配表达式
撤消分配表达式
单目运算符:以下之一
* & + - ! ~
间接运算符(*)
单目间接运算符(*)“间接引用”一个指针,即它将一个指针值转换为一个l值。间接运算符的操作数必须是一个类型的指针。间接表达式的结果是指针类型从其派生的类型。在此上下文中,*运算符的使用不同于它作为双目运算符即乘法的含义。
如果操作数指向一个函数,结果是一个函数指示符。如果它指向一个存储位置,则结果是指定该存储位置的一个l值。
如果指针值是无效的,则结果是不确定的。以下表包括一些最常见的条件使指针值无效
* 指针是一个空指针。
* 指针指定一个局部项的地址,在引用时是不可见的。
* 指针指定一个地址不适当地与该对象指向的类型对齐
* 指针指定一个地址不被执行程序使用。
取地址运算符(&)
单目取地址运算符(&)取其操作数的地址,取地址运算仅能用于以下情况:
* 函数(尽管取函数地址的用法是不需要的)
* l值
* 限定名称
在上面所列的前面两种情况下,表达式的结果是从操作数类型派生出的一个指针类型(一个r值)。例如,如果操作数为char类型,则表达式的结果是char指针类型。取地址运算符被用于const或volatile对象则等于const type*或volatile type*,其中type为源对象的类型。
第三种情况将取地址运算符应用在一个限定名称上,产生的结果取决于限定名称是否指定一个静态成员,如果是,结果是指向成员说明中指定类型的一个指针;如果成员不是静态的,则结果是由限定类名称指示的类成员名称的一个指针(有关限定类名称的更多信息,参见本章前面的“基本表达式”)。以下代码段给出:根据成员是否是静态的其结果是如何不同的:
class PTM
{
public:
int iValue;
static float fValue;
};
int PTM::*piValue = &PTM::iValue; //可行:非静态的
float PTM::*pfValue = &PTM::fValue; //错误:静态的
float *spfValue = &PTM::fValue; //可行
在此例中,表达式&PTM::fValue产生float*类型而不是float PTM::*类型,因为fValue是一个静态成员。
一个重载的函数的地址仅在清楚地知道引用的是函数的哪个版本时才能被获取。关于如何获取一个特定的重载的函数的地址的信息,参见第12章“重载”中的“重载的函数的地址”。
对一个引用类型使用取地址运算符与对一个引用限定的对象上使用该运算符给
出相同的结果。以下程序说明了这个概念:
#include <iostream.h>
void main()
{
double d; //定义double类型的一个对象
double& rd=d; //定义该对象的一个引用
//比较对象的地址和对象引用的地址
if (&d==&rd)
cout << "&d equals &rd" << "/n";
else
cout << "&d is not equal to &rd" << "/n";
}
程序的输出总是:&d equals &rd。
单目加运算符(+)
单目加运算符(+)的结果是其操作数的值。单目加运算符的操作数必须是一个算术类型。
在整型操作数上执行整型提升。结果类型是操作数被提升的类型。因而,表达式+ch,其中ch为char类型,结果为int类型;值未修改。关于提升是如何完成的信息,参见第3章“标准转换”中的“整型提升”。
单目负运算符(-)
单目负运算符(-)对其操作数求反。单目负运算符的操作数必须为一个算术类型。
在整型操作数上执行整型提升,结果类型是操作数被提升的类型。有关提升是如可完成的信息,参见第3章“标准转换”的“整型提升”。
Microsoft特殊处
无符号量单目求反的执行是从2n中减去操作数的值,其中n是给定无符号类型的对象的位数(Microsoft C++运行在对2的补码运算的处理器中,在其它处理器中求反算法可能有所不同)。
Microsoft特殊处结束
逻辑非运算符(!)
如果操作数为非0值,则逻辑非运算符(!)的结果为0;仅当操作数为0时结果方为1。操作数必须为算术或指针类型,结果是int类型。
对一个表达式e,单目表达式!e等于表达式(e==0),除非包含了重载的运算符。以下例子说明了逻辑非运算符(!):
if ( !(x<y))
如果x大于等于y,表达式的结果为1(真);如果x小于y,结果为0(假)。
指针的单目算术运算是非法的。
“1”的求补运算符(~)
“1”求补运算符(~),有时被称为“按位补”运算符,产生其操作数的按位的“1”的补,即在操作数中每一位为1的在结果中为0;相反地,操作数中为0的每位在结果中为1。“1”求补运算符的操作数必须为整型。
unsigned short y=0xAAAA;
y=~y;
在此例中,赋给y的新值是无符号值0xAAAA的“1”求补或0x5555。
在整型操作数上执行整型提升,结果类型是操作数被提升的类型。关于升级是如何完成的的更多信息,参见第3章“标准转换”中的“整型提升”。
增1和减1运算符(++,--)
前缀增1运算符(++),亦被称为“前增1”运算符,在其操作数上加1,这个加1后的值是表达式的结果。操作数必须是一个非const类型的l值,结果是与操作数类型相同的一个l值。
前缀减1运算符(--),亦被称为“前减1”运算符,与前增1运算符相似,除了操作数是减1且结果是这个减1后的值。
前缀和后缀增1及减1运算符均影响其操作数,关键的不同点是表达式的值何时发生增1或减1(有关更多的信息,参见本章前面的“后缀增1和减1运算符”)。在前缀形式中,增1或减1发生在值被用于表达式求值之前,所以表达式的值与操作数的值不同;在后缀形式中,增1或减1发生在位被用于表达式求值之后,所以表达式的值与操作数的值相同。
整型或浮点类型的操作数按整数值1进行增或减,结果的类型与操作数类型相同;指针类型操作数按它所指对象的尺寸增1或减1,被增1的指针指向下一个对象,被减1的指针指向前一个对象。
这个例子说明了单目减1运算符:
if (line [--i]!=‘/n’)
return;
在此例中,变量i在用作line下标之前减1。
因为增1和减1运算符有副作用,在宏中使用带增1或减1运算符的表达式可产生意料不到的结果(有关宏的更多信息,参见本部分后面“预处理器引用”中的“宏”)。考虑以下例子:
#define max(a,b) ((a)<(b)) ? (b) : (a)
...
int i,j,k;
k=max(++i,j);
宏扩展为:k=((++i)<(j)) ? (j) : (++i);
如果i大于等于j,它将增1两次。
注意:在很多情况下,C++的内联函数(inline functions)比宏更好,因为它们消除了这里描述的副作用,而允许语言执行更完整的类型检测。
sizeof运算符
sizeof运算符产生关于char类型尺寸的操作数尺寸,sizeof运算符的结果是size_t类型,它是定义在包括文件STDDEF.H中的一个整型。sizeof操作数可以是以下之一:
* 一个类型名。要对类型名用sizeof,则名称必须用圆括号括起来。
* 一个表达式。当用于表达式时,sizeof可以使用或不使用括号指定,该表达式不求值。当sizeof运算符用于char类型的对象时,它结果为1;
当sizeof运算符用于一个数组时,它结果为那个数组中的字节总数。例如:
#include <iostream.h>
void main()
{
char szHello[]="Hello, world!";
cout << "The size of the type of " << szHello <<" is:"
<< sizeof(char) << "/n";
cout <<"The length of " << szHello << "is:"
<< sizeof szHello << "/n";
}
程序输出为:
The size of the type of Hello, world! is:1
The length of Hello, world! is:14
当sizeof运算符用于class、struct或union类型时,结果是该class、struct或union类型的一个对象的字节数加上字边界上任何添加的对齐成员的填充字节。(/Zp[紧凑结构成员]编译器选项和pack编译指示影响成员的对齐边界)。sizeof运算符从不产生0,即使对一个空类亦如此。
sizeof运算符不能用于以下操作数
* 函数(但是sizeof可应用于函数指针)
* 位域l 未定义的类l void类型
* 不完整类型
* 括号括起的不完整类型的名称
当sizeof运算符用于一个引用时,结果就如同sizeof用于对象自身一样。sizeof运算符经常被用于计算一个数组的元素个数,使用如下形式的表达式:
sizeof array/sizeof array[0]
new运算符
new运算符试图动态地分配(在运行时)类型名称的一个或多个对象。new运算符不能用于分配一个函数,但可以用于分配一个函数指针。
语法
分配表达式:
::opt new 位置opt 新类型名称 新初始化器opt
::opt new 位置opt(类型名称) 新初始化器opt
位置:
(表达式表)
新类型名称:
类型指示符表 新说明符opt
new运算符被用于分配对象及对象数组,new运算符从被称为“自由存储”的程序存储器区域中进行分配。在C中,自由存储区经常被指示为“堆”。
当new被用于分配一个单个对象时,它产生一个指向那个对象的指针;结果类型为新类型名称*或类型名称*;当new被用于分配一个单维对象数组时,它产生一个指向数组第一个元素的指针,结果类型为新类型名称*或类型名称*;当new被用于分配一个多维对象数组时,它产生一个指向数组第一个元素的指针,而且结果类型保留除最左边数组维数外的所有的尺寸,例如:
new float[10][25][10]
产生类型float(*)[25][10]。因此,以下代码不能工作,因为它试图用类型float的指针的维数[25][10]赋给一个float数组的指针:
float *fp;
fp=new float [10][25][10];
正确的表达式应是:
float (*cp)[25][10]
cp=new float[10][25][10];
cp的定义用维数[25][10]分配一个float类型数组的指针,它不分配指针数组。除最左边数组维数外的所有维数必须为等于正数的常量表达式;最左边数组维数可以为等于正数的任意表达式,当用new运算符分配一个数组时,第一个维数可以为0,new运算符返回一个唯一的指针。
类型指示符表不能包含const、volatile、类说明或枚举说明。因此,以下表达式是非法的:
volatile char *vch = new volatile char[20];
new运算符不分配引用类型,因为它们不是对象。
如果没有足够的存储器满足分配需求,缺省地operator new返回NULL。你可以通过编写定制的异常处理例程并以你的函数名称作为参量调用_set_new_handler运行库函数来改变这种缺省行为。有关这种恢复模式详细内容,参见第11章“特殊成员函数”中的“运算符new功能”。
用new分配的对象的生存期
用new运算符分配的对象当它们从定义的所在范围退出时不被销毁。因为new运算符返回它分配的对象的指针,程序必须用合适的范围定义一个指针以访问那些对象。例如:
void main()
{
//使用new运算符分配一个20个字符的数组
char *AnArray = new char[20];
for (int i=0;i<20;++i)
{
//在循环的第一次迭代时,分配另一个20个字符的数组
if (i==0)
{
char *AnotherArray = new char[20];
}
...
}
delete AnotherArray; //错误:指针超出范围
delete AnArray; //可以:指针还在范围内
}
在例子中一旦指针AnotherArray超出了范围,对象再也不能被删除了。
初始化用new分配的对象
选项new初始化器域包含在new运算符的语法内,这允许使用用户定义的构造函数去初始化new对象。关于初始化是如何完成的更多信息,参见第7章“说明符”中“初始化器”。
以下例子说明如何使用带new运算符的初始化表达式:
#include <iostream.h>
class Acct
{
public:
//定义缺省构造函数和构造函数接受初始的balance
Acct() { balance=0.0; }
Acct( double init_balance ) { balance = init_balance; }
double balance;
};
void main()
{
Acct *Checking
Acct = new Acct;
Acct *SavingAcct = new Acct(34.98);
double *HowMuch = new double(43.0);
...
}
在此例中,对象CheckingAcct使用new运算符分配,但没有指定缺省的初始化,因此,调用类的缺省构造函数Acct();然后以同样的方式分配对象SavingAcct,但被显式地初始化为34.98。由于34.98是double类型,调用以该类型为参量的构造函数进行初始化处理。最后,非类类型HowMuch初始化为43.0。
如果一个对象是一个类类型的,而且那个类有构造函数(如前面例子那样),仅在以下条件之一被满足时对象才能被new运算符初始化:
* 在初始化器中提供的参量与构造函数中的那些参量相符。
* 类有一个缺省的构造函数(一个可以不带参量调用的构造函数)。访问控制和模糊控制在operator new上和在按照第9章“派生类中的模糊性”及第11章“特殊成员函数”中的“使用特殊成员函数初始化”中提出的规则集的构造函数上执行。
当使用new运算符分配数组时没有显式地为每个元素执行初始化;如果存在的话,只有缺省构造函数被调用。有关更多的信息,参见第7章“说明符”中的“缺省参量”。
如果存储器分配失败(operator new返回一个0值),则没有初始化被执行,这阻止了对不存在的数据进行初始化。
由于用函数调用,初始化表达式被求值的顺序未定义,而且,你不能依靠这些表达式在存储器分配执行之前完全被求值。如果存储器分配失败,new运算符返回0,在初始化器中的某些表达式可能不能被完全求值。
new是如何工作的
分配表达式即包含new运算符的表达式做三件事:
* 为对象或待分配的对象定位并保留存储器,当这步完成时,分配正确的存储单元数,但还不是一个对象。
* 初始化对象,一旦初始化完成,则有足够的信息为该对象分配存储。
* 返回一个指向由new-type-name或type-name派生的指针类型对象的一个指针,程序使用该指针访问新分配的对象
。new运算符调用函数operator new。对于任何类型的数组及非class、struct或union类型的对象,全局函数::operator new被调用去分配存储器,类类型对象可以在每个类基础上定义自己的operator new静态成员。
当编译器遇到new运算符去分配一个type类型对象时,它调用type::operatornew (sizeof (type));或者若是没有用户定义的operator new被定义,则调用::operator new (sizeof (type))。因此,new运算符可以为对象分配正确的存储器数目。
注意:operator new的参量是size_t类型,这个类型定义在DIRECT.H、MALLOC.H、MEMORY.H、SEARCH.H、STDDEF.H、STDIO.H、STDLIB.H、STRING.H和TIME.H中。
语法中的一个选项允许位置的规格(参见new运算符的语法)。该位置参量仅能在operator new用户定义的实现中被使用;它允许将额外的信息传递给operatornew,带位置域的表达式如:
T *TObject = new(0x0040) T;
被翻译为:T *TObject = T::operator new(sizeof(T),0x0040);
位置域最初的目的是允许依赖于硬件的对象在用户指定的地址处分配。
注意:尽管前面的例子给出的是在位置域仅有一个参量,但对这种方式有多少个额外的参量可被传递给operator new并没有限制。
即使当operator new已为一个类类型定义,全局运算符还可以通过下面例子中的形式来使用:
T *TObject = ::new TObject:
范围分辩运算符(::)强制使用全局new运算符。
delete运算符
delete运算符撤消由new运算符创建的对象分配。delete运算符有一个void类型的结果,因此不返回值,delete的操作数必须是由new运算符返回的一个指针。
对不是用new分配的对象指针使用delete会产生意料不到的结果,但是你可以对具有0值的指针使用delete。因为在失败的情况下new总是返回0,所以这种措施意味着删除一个失败的new操作的结果是无害的。
语法
撤消分配表达式:
::opt delete 造型转换表达式
::opt delete [] 造型转换表达式
对一个对象使用delete运算符将撤消其存储器分配,一个程序在对象被删除后间接引用一个指针则会产生意料不到的结果或崩溃。
如果delete运算符的操作数是一个可修改的l值,则在对象被删除后其值是未定义的。
常量对象指针不能用delete运算符撤消分配。
delete是如何工作的
delete运算符调用函数运算符delete,对类类型(class(类)、struct(结构)和union(联合))对象,delete运算符在撤消存储器分配(如果指针非空)之前调用对象的析构函数。对非类类型对象,全局delete运算符被调用。对类类型对象,delete运算符可被定义在一个每个类基础上。如果对给定类没有这样的定义,则全局运算符被调用。
使用delete
对delete运算符有两个语法变型:一个针对单个对象的,另一个是对对象数组的。以下代码段给出这些的不同:
void main()
{
//使用new运算符在自由存储区分配一个用户定义对象
UDObject和double类型对象 UDType *
UDObject = new UDType; double *dObject = new double;
...
//删除两个对象
delete UDObject;
delete dObject;
... //使用new运算符在自由存储区分配一个用户定义对象数组
UDType (*UDArr)[7] = new UDType[5][7];
...
//使用数组语法删除对象数组
delete [] UDArr;
}
这两种情况产生未定义的结果:对一个对象使用delete的数组形式(delete[])和对一个数组使用delete的非数组形式。
带双目运算符的表达式
双目运算符作用于一个表达式中的两个操作数。双目运算符有:
乘法运算符
乘法(*)
除法(/)
取模(%)
加法运算符
加法(+)
减法(-)
位移运算符
右移(>>)
左移(<<)
关系及相等性运算符
小于(<)
大于(>)
小于等于(<=)
大于等于(>=)
等于(==)
不等于(!=)
按位运算符
按位与(&)
按位异或(^)
按位或(|)
逻辑与(&&)
逻辑或(||)
乘法运算符乘法
运算符有:
* 乘法(*)
* 除法(/)
* 取模或“除法取余”(%)
这些双目运算符具有从左到右的结合律。
语法
乘法表达式:
pm表达式
乘法表达式 * pm表达式
乘法表达式 / pm表达式
乘法表达式 % pm表达式
乘法表达式采用算术类型操作数,取模运算符(%)有一个较为严格的要求,即其操作数必须为整型(要得到浮点除法的余数,使用运行函数fmod)。第3章“标准转换”中的“算术转换”中包含的转换适用于操作数,且结果是转换后的类型。
乘法运算符产生的是第一个操作数被第二个相乘的结果。
除法运算符产生的是第一个操作数被第二个相除的结果。
取模运算符产生由表达式e1-(e1/e2)*e2给出的余数,其中e1是第一个操作数,e2是第二个操作数,其中两个操作数均为整型。
在除法或取模数表达式中被0除都是未定义的,会产生一个运行错误。因此,以下表达式产生未定义的错误的结果:
i % 0
f / 0.0
如果乘法、除法或取模表达式的两个操作数具有相同的符号,则结果为正;否则,结果为负,取模操作的符号结果是定义的实现。
Microsoft特殊处
在Microsoft C++中,取模表达式的结果总是与第一个操作数的符号相同。
Microsoft特殊处结束
如果两个整数计算的除法是不精确的,而且仅一个操作数为负,结果是比除法产生的准确值小的最大整数(在量上,不考虑符号)。例如,-11/3计算的结果为-3.666666666,则整型除法的结果为-3。
乘法运算符之间的关系由以下标识给出:
(e1/e2)*e2+e1%e2==e1
加法运算符
加法运算符有:
* 加法(+)
* 减法(-)
这些双目运算符具有从左到右的结合律。
语法
加法表达式:
乘法表达式
加法表达式 + 乘法表达式
加法表达式 - 乘法表达式
加法运算符采用算术或指针类型操作数。加法运算符(+)的结果是操作数求和;减法运算符(-)的结果是操作数之间的差。如果一个或两个操作数都是指针,它们必须是对象的指针,而不是函数的指针。
加法运算符的操作数为算术、整型或标量型,这些定义在表4.2中。
表4.2 加法运算符使用的类型
类型 | 含义 | |
算术型 | 整型和浮点类型统称为"算术"类型 | |
整型 | char和所有尺寸的int(long,short)类型和枚举都是“整数”类型 | |
标量型 | 标量型操作数是算术或指针类型的操作数 |
这些运算符的合法组合为:
算术型 + 算术型
标量型 + 整 型
整 型 + 标量型
算术型 - 算术型
标量型 - 标量型
注意:加法和减法不是等价操作。
如果两个操作数都是算术类型,则在第3章“标准转换”中“算术转换”包括的转换应用于操作数,而且结果是被转换了的类型。
指针类型的加法
如果加法操作中一个操作数为指向一个对象数组的指针,则另一个必须为整型,其结果是与源指针同类型的指针,且指向另一个数组元素。以下代码段说明这个概念:
short IntArray[10]; //short类型对象占两个字节
short *pIntArray=Int Array;
for (int i=0;i<10;++i)
{
*pIntArray=i;
cout << *pIntArray << "/n";
pIntArray = pIntArray + 1;
}
尽管整型值1被加到pIntArray上,但它并不意味着“地址加1”;而是指“调整指针指向数组中的下一个对象”,而且正好是走了两个字节(或sizeof(int))。注意:pIntArray=pIntArray+1形式的代码在C++程序中很少看到。为了执行增1,这样形式更好:pIntArray++或pIntArray+=1。
指针类型的减法
如果两个操作数都是指针,减法的结果是两个操作数之间的差(在数组元素中)。减法表达式产生一个类型ptrdiff_t(定义在标准包括文件STDDEF.H中)的有符号的整型结果。
操作数之一可以是整型,它只能是第二个操作数。减法的结果与源指针同类型,减法的值是指向第(n-i)个数组元素的指针,其中n是由源指针指向的元素,i是第二个操作数的整型值。
位移运算符
按位移位运算符有:
* 右移(>>)
* 左移(<<)
这些双目运算符具有从左到右的结合律。
语法
移位表达式:
加法表达式
移位表达式 << 加法表达式
移位表达式 >> 加法表达式
移位运算符的两个操作数都必须是整型,整型提升的执行按照第3章“标准转换”中的“整型提升”中指定的规则进行。结果的类型与左操作数类型相同。右移表达式el>>e2的值为e1/2e2;左移表达式e1<<e2的值为e1*2e2。
如果移位表达式的右操作数为负数或如果右操作数大于等于左操作数(提升了的) 的位数,则结果是未定义的。
左移运算符使得第一个操作数的位模式向左移动由第二个操作数指定的位数,由移位操作进行的空位填充是0填充,这是逻辑移位,与循环移位相反。
右移运算符使得第一个操作数的位模式向右移动由第二个操作数指定的位数。对无符号量移位操作的空位填充是0填充,对有符号量,符号位被传入空位。如果左操作数是一个无符号量,则移位是一个逻辑移位,否则是算术移位。
Microsoft特殊处
有符号的负数的右移结果是依赖于定义的实现,尽管Microsoft C++传播最重要的位以填充空位,但不保证其它的实现也这样做。
Microsoft特殊处结束
关系与相等运算符
关系与相等运算符指定其操作数的相等、不等或关系值,关系运算符在表4.3中所示。
表4.3 关系与相等运算符
运算符 | 含义 |
== | 等于 |
!= | 不等于 |
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
关系运算符
双目关系运算符指定以下关系:
* 小于
* 大于
* 小于等于
* 大于等于
语法
关系表达式:
移位表达式
关系表达式 < 移位表达式
关系表达式 > 移位表达式
关系表达式 <= 移位表达式
关系表达式 <= 移位表达式
关系运算符具有从左到右的结合律。关系运算符的两个操作数必须为算术或指针类型,它们产生int类型值。如果表达式中关系为假则返回的值为0;否则为1。考虑以下代码,其中说明了几种关系表达式:
#include <iostream.h>
void main()
{
cout << "The true expression 3 > 2 yields: "
<< (3 > 2) << "/n"; cout << "The false expression 20 < 10 yields: "
<< (20 < 10) << "/n";
cout << "The expression 10 < 20 < 5 yields: "
<< (10 < 20 < 5) << "/n";
}
此程序的输出为:
The true expression 3 > 2 yields:1
The false expression 20 < 10 yields:0
The expression 10 < 20 < 5 yields:1
在前面例子中的表达式必须包含在括号内,因为插入运算符(<<)比关系运算符优先级高。因此,不带括号的第一个表达式将等于:
(cout << "The true expression 3 > 2 yields:" << 3) < (2 << "/n");
注意第三个表达式等于1,因为关系运算符从左到右的结合律,表达式10<20<5的显式分组为:
(10 < 20) < 5
因此,测试执行的是:
1<5
结果为1(或真)。
第3章“标准转换”中的“算术转换”包含的常用的算术转换应用于算术类型的操作数上。
使用关系运算符比较指针
当同类型对象的两个指针进行比较时,结果由指向程序地址空间的对象位置指定。指针还可以和等于0或void *类型指针的常量表达式进行比较。如果指针比较相对于void*类型指针,则另一指针被隐式地转换为void*类型。然后进行比较。
不同类型的两个指针不能进行比较,除非:
* 一个类型是从另一类型派生而来的类类型。
* 至少一个指针被显式地转换(强制)为void*类型(另一个指针隐式地转换为void *类型)。
指向相同类型的相同对象的两个指针保证比较结果为相等。如果一个对象的两个非静态成员指针进行比较,使用以下规则:
* 如果类类型不是联合,而且如果两个成员不是由一个访问指示符如公共的、保护的和私有的分开,在后面说明的成员指针将比前面说明的成员的指针大些(有关访问指示符见第10章“成员访问控制”中的“访问指定符”的语法部分)。
* 如果两个成员被访问指示符分开,则结果是不确定的。
* 如果类类型是一个联合,在这个联合中的不同数据的指针相比较是相等的。
如果两个指针指向同一数组中的元素或指向的某个元素超出了数组的尾界,则带有较高下标的对象的指针相比高些。指针的比较仅当指针指向同一数组对象或指向超过数组边界的某个位置时才确保是有效的,。
相等运算符
双目相等运算符比较其操作数为严格相等或不等。
语法相等表达式:
关系表达式
相等表达式 == 关系表达式
相等表达式 != 关系表达式
相等运算符:等于(==)和不等于(!=)比关系运算符优先级低,但它们的行为是相似的。
如果两个操作数有相同的值则相等运算符(==)返回真值;否则返回假值。不等运算符(!=)当两个操作数具有不同的值时返回真值,否则返回假值。
相等运算符可以比较两个同类型成员指针,在这种比较中,执行成员指针的转换如第3章“标准转换”中的“成员指针转换”讨论的。成员指针还可以与等于
0的常量表达式进行比较。
按位运算符
按位运算符有:
* 按位与(&)
* 按位异或(^)
* 按位或(|)
这些运算符返回其操作数的按位组合。
按位与运算符
按位与运算符(&)返回两个操作数的按位与结果。左右操作数中所有均为1的位在结果中为1,而任一个操作数中为0的位在结果中为0。
语法
与表达式:
关系表达式
与表达式 & 相等表达式
按位与运算符的两个操作数都必须为整型,包含在第3章“标准转换”中的“算术转换”中的常用算术转换被用于操作数上。
按位异或运算符
按位异或运算符(^)返回两个操作数的按位异或结果。在左右之一操作数中为1的所有位但不都是1的位,出现在结果中为1;两个操作数中相同值的位(为1或0)则在结果中为0。
语法
异或表达式:
与表达式
异或表达式 ^ 与表达式
按位异或运算符的两个操作数必须为整型,包含在第3章“标准转换”中“算术转换”的常用算术转换被用于操作数上。
按位或运算符
按位或运算符返回两个操作数按位或的结果,在左或右操作数中为1的所有位在结果中均为1,在两个操作数中均为0的位在结果中为0。
语法
或表达式:
异或表达式
或表达式 | 异或表达式
按位或运算符的两个操作数都必须为整型,包含在第三章“标准转换”中“算术转换”的常用算术转换被用于操作数上。
逻辑运算符
逻辑运算符:逻辑与(&&)逻辑或(||)被用于组合使用关系或相等表达式形成的多个条件。
逻辑与运算符
逻辑与运算符在两个操作数均为非0时返回整型值1;否则返回0。逻辑与具有从左到右的结合律。
语法
逻辑与表达式:
或表达式
逻辑与表达式 && 或表达式
逻辑与运算符的操作数不要求为同一类型,但它们必须为整型或指针类型,操作数通常为关系或相等表达式。
在继续计算逻辑与表达式的值之前,第一个操作数被完全求值,且所有的副作用也被完成。
第二个操作数仅在第一个操作数被求值为真(非0)时才被计算求值,这种求值去掉了第二个操作数在逻辑与表达式为假时不必要的求值,你可以用这种短路计算防止空指针的间接引用,正如以下例中所示:
char *pch=0
...(pch) && (*pch=′a′);
如果pch为空(0),表达式的右端永远不被求值,因此,通过空指针的赋值是不可能的。
逻辑或运算符
逻辑或运算符在任意一个操作数为非0时返回整数值1;否则返回0,逻辑或具有从左到右的结合律。
语法
逻辑或表达式
: 逻辑与表达式
逻辑或表达式 || 逻辑与表达式
逻辑或运算符的操作数不要求为同一类型,但必须为整型或指针类型,操作数通常为关系或相等表达式。
在继续计算逻辑或表达式之前第一个操作数被完全求值,且所有的副作用被完成。
第二个操作数仅在第一个操作数被求值为假(0)时才被计算求值,这种求值去除了第二个操作数在逻辑或表达式为真时不必要的求值。
printf("%d",(x==w || x==y || x==z));
在此例中,如果x等于w、y或z中的任一值,printf函数的第二个变量求值为真,且打印值1。否则,它求值为假,且打印值0,只要有一个条件求值为真,求值即终止。
赋值运算符
赋值运算符在由左操作数指定的对象中存入一个值。有两种赋值操作:“简单赋值”即将第二个操作数的值存入到由第一个操作数指定的对象中;“复合赋值”即在存储结果之前执行一个算术、移位或按位操作。表4.4中除了=运算符外所有的赋值运算符都是复合赋值运算符。
表4.4 赋值运算符
运算符 | 含义 |
= | 将第二个操作数的值存入第一个操作数指定的对象中(“简单赋值”) |
*= | 将第一个操作数值乘以第二个操作数值,将结果存入第一个操作数指定 的对象中 |
/= | 将第一个操作数值除以第二个操作数值,将结果存入第一个操作数指定的对象中 |
%= | 第一个操作数除以第二个操作数的模数,将结果存入第一个操作数指定 的对象中 |
+= | 将第二个操作数的值加入第一个操作数的值中,将结果存入第一个操作数指定的对象中 |
-= | 从第一个操作数值中减去第二个操作数的值,将结果存入第一个操作数指定的对象中 |
<<= | 将第一个操作数的值左移第二个操作数值指定的位数,将结果存入第一个操作数指定的对象中 |
>>= | 将第一个操作数的值右移第二个操作数的值指定的位数,将结果存入第一个操作数指定的对象中&=获取第一和第二个操作数按位与的结果,将结果存入第一个操作数指定的对象中 |
^= | 获取第一和第二个操作数按位异或的结果,将结果存入第一个操作数指定的对象中 |
|= | 获取第一和第二个操作数按位或的结果,将结果存入第一个操作数指定的对象中 |
语法
赋值表达式:
条件表达式
单目表达式 赋值运算符 赋值表达式
赋值运算符:以下之一
= *= /= %= += -= <<= >>= &= ^= |=
赋值运算符的结果
赋值运算符返回由左操作数指定的对象赋值后的值。结果类型为左操作数类型。赋值表达式的结果总是一个l值。这些运算符具有从右到左的结合律。左操作数必须是一个可修改的l值。
注意:在ANSI C中,赋值表达式的结果不是一个l值,因此,C++的合法表达式(a+=b)+=c在C中是非法的。
简单赋值
简单赋值运算符(=)将第二个操作数的值存入第一个操作数指定的对象中。如果两个对象都是算术类型,在值存储前右操作数被转换为左操作数的类型
。const和volatile类型对象可以赋给volatile或既不是const又不是volatile类型的l值。
对类类型(struct、union及class类型)对象的赋值由命名为运算符=的函数来执行。此运算符函数的缺省行为是执行按位拷贝;但此行为可用重载的运算符进行修改(有关更多的信息参见第12章“重载”中的“重载的运算符”)。任何从一给定基类非模糊地派生的类的对象可被赋给基类的一个对象。反之则不行,因为从派生类到基类存在隐式转换,但从基类到派生类没有。例如:#include
class ABase
{
public:
ABase() { cout << "constructing ABase/n"; }
};-
+class ADerived:public ABase
{
public:
ADerived() { cout << "constructing ADerived/n"; }
};
void main()
{
ABase aBase;
ADerived aDerived;
aBase=aDerived; //可行
aDerived=aBase; //错误
}
引用类型的赋值就好象正在对引用指向的对象进行赋值一样
。对类类型对象,赋值与初始化不同。为了说明赋值和初始化如何不同,考虑以下代码:
userType1 A;
UserType2 B=A;
上面代码给出了一个初始化器,它为UserType1调用构造函数,该构造函数是有一个UserType1类型的一个参量。给出代码:
UserType1 A
;UserType2 B;
B=A;
赋值语句:
B=A;可以有以下的一个作用:
* 为UserType2调用函数运算符=,提供的运算符=用一个UserType1参量提供的。
* 调用显式转换函数UserType1::operator UserType2,如果此函数存在的话。
* 调用一个构造函数UserType2::UserType2,假如这样的构造函数存在,而且带一个UserType1参量,并拷贝结果。
复合赋值
表4.4中给出的复合赋值运算符指定为e1 op=e2形式,其中e1是一个非常量类型的可修改的l值,e2是以下之一:
* 一个算术类型
* 一个指针,如果op是+或
-e1 op=e2形式与e1=e1 op e2执行相同的动作,但e1仅被求值一次。
对枚举类型的复合赋值将产生一个错误信息。如果左操作数是一个指针类型,右操作数必须为指针类型或必须为等于0的常量表达式;如果左操作数是一个整型,右操作数一定不能为指针类型。
逗号运算符
逗号运算符允许对两个语句分组,其中一个是预期的。
语法表达式:
赋值表达式
表达式,赋值表达式
逗号运算符具有从左到右的结合律。由逗号分开的两个表达式从左到右求值。左操作数总是被求值的,而且所有副作用在右操作数求值之前完成。
考虑表达式:
e1,e2
该表达式的类型和值是e2的类型和值,e1求值的结果被丢弃。如果右操作数是一个l值则结果是一个l值。
逗号具有特殊含义时(例如,在函数的实参中或集合初始化器中),逗号运算符及其操作数必须包括在括号中。因此,以下函数调用不是等价的:
//说明函数
void Func(int ,int);
void Func(int);
Func(arg1,arg2); //调用Func(int ,int)
Func((arg1,arg2)); //调用Func(int)
该例子说明逗号运算符:for (i=j=1;i+j<20;i+=i,j--);
在此例中,for语句的第三个表达式的每个操作数被独立地求值,左操作数i+=i首先被求值;然后右操作数j--被求值。
func_one(x,y+2,z);
func_two((x--,y+2),z);
在func_one函数调用中,由逗号分开的三个参量被传递:x,y+2和z;在func_two函数调用中,括号迫使编译器将第一个逗号解释为序列求值运算符。此函数调用向func_two传递两个参量,第一个参量是序列求值操作(x--,y+2)的结果,具有表达式y+2的值和类型;第二个参量是z。
带条件运算符的表达式
条件运算符(?:)是一个三目运算符(它带三个操作数)。条件运算符的工作如下:
* 在继续之前,第一个操作数被求值,且所有的副作用被完成。
* 如果第一个操作数求值为真(一个非0值),第二个操作数被求值。
* 如果第一个操作数求值为假(0),第三个操作数被求值。
条件运算符的结果是被求值的操作数,即第二或第三个的结果。在条件表达式中后两个操作数只有一个被求值。
语法条件表达式:
逻辑或表达式
逻辑或表达式 ? 表达式 : 条件表达式
条件表达式无结合律。第一个操作数必须为整型或指针类型,以下规则用于第二及第二个操作数:
* 如果两个表达式是同类型的,则结果是那个类型的。
* 如果两个表达式都是算术类型,则常用的算术转换(包含在第3章“标准转换”中的“算术转换”)被执行以将它们转换为公用类型。
* 如果两个表达式都是指针类型或一个是指针类型,另一个是求值为0的常量表达式,则指针转换被执行去将它们转换为公用类型。
* 如果两个表达式都是引用类型,则引用转换被执行以将它们转换为公用类型。
* 如果两个表达式都是void类型,则公用类型为void类型。
* 如果两个表达式是一个给定类类型,则公用类型是那个类类型。任何未列入前面清单的第二和第三个操作数的组合都是非法的。结果的类型是公用类型,且如果第二和第三个操作数为同一类型且都是l值的则结果是一个l值。
例如:
(val>=0) ? val : -val
如果条件为真,表达式等于val;否则,表达式等于-val。
带量表达式
C++在以下说明中要求常量表达式,即等于常量的表达式:
* 数组界限
* case语句中的选择器l 位域长度规格
* 枚举初始化器
语法
常量表达式:
条件表达式
在常量表达式中唯一合法的操作数是:
* 文字
* 枚举常量
* 用常量表达式初始化的说明为常量的值
* sizeof表达式
在常量表达式中非整型常量必须转换(显式或隐式地)为合法的整型。因此,以下代码是合法的:
const double Size=11.0;
char chArray[(int)Size];
在常量表达式中显式地转换为整型是合法的,除了用作sizeof运算符的操作数之外。所有其它类型或派生类型都是非法的。
逗号运算符和赋值运算符不能用于常量表达式中。
带显式类型转换的表达式
正如第3章“标准转换”中描述的,C++提供隐式类型转换。当你需要更精确地控制应用的转换时,你还可以指定显式类型转换。
显式类型转换运算符
C++允许使用与函数调用语法类似的语法进行显式类型转换,一个简单类型名称后面跟随一个括在括号中的表达式表,构成用指定表达式指定的类型的一个对象,以下例子给出了一个转到int类型的显式类型转换:
int i=int(d);
以下例子使用定义在“函数调用结果”中的Point类的修改后的版本:
#include <iostream.h>
class Point
{
public:
//定义缺省构造函数
Point() {_x = _y = 0;}
//定义另一个构造函数
Point(int X,int Y) {_x=X;_y=Y;}
//定义作为引用类型的“accessor”函数
unsigned& x() { return _x;}
unsigred& y() { return _y;}
void Show() { cout << "x=" << _x << ","
<< "y=" << _y << "/n";}
private:
unsigned _x;
unsigned _y;};
void main ()
{
Point Point1,Point2;
//赋给point1显示转换为(10,10)
Point1=Point(10,10);
//通过对unsigned类型赋一个20的显示转换使x()作为一个l值来用
Point1.x()=unsigned(20);
Point1.Show();
//赋给Point2缺省的point对象
Point2=Point();
Point2.Show();
}
此程序的输出为
:x=20,y=1
0x=0,y=0
尽管前面的例子阐述的是使用常量的显式类型转换,在对象上用同样的技巧去执行这些转换。以下代码段说明这种情况:
int i=7;
float d:
d=float(i);
显示类型转换还可以用“造型转换”语法指定,前面的例子,用造型转换语法重写为:
d=(float)i;
当从单值转换时,造型转换和函数形式转换具有相同的结果。但在函数形式语法中,你可以为转换指定多个参量。这个不同点对于用户定义的类型是很重要的。
考虑一个Point类及其转换:
struct Point
{
Point(short x, short y) { _x=x,_y=y; }
...
short _x,_y;
};
...
Point pt=Point(3,10);
前面的例子,使用了函数形式转换,显示了如何将两个值(一个为x,一个为y)转换为用户定义的类型Point。
重要点:使用显式类型转换要小心,因为它们覆盖了C++编译器的内部类型检查。
语法
造型转换表达式:
单目表达式
(类型名)造型转换表达式
造型标记必须用于不具有一个简单类型名称(例如指针或引用类型)的类型的转换。可以用一个简单类型名称表示的类型的转换可以用任何两者之一形式书写。有关哪些构成一个简单类型名称的更多信息,参见第6章“说明”中的“类型指示符”。
在造型转换内的类型定义是非法的。
合法转换
如果转换可以用标准转换实现,则你可以从一给定类型向另一类型做显式转换,其结果是相同的。本节描述的转换是合法的,任何其它的非显式地由用户定义的转换(为一个类类型)是非法的。
如果指针足够大以便保存整型值,则一个整型值可被显式地转换为一个指针,转换到一个整型值的指针可以再转回到指针,其值是相同的。这个一致性是由以下等式给出(其中p表示任意类型的一个指针):
p==(type *)整型转换(p)
用显式转换,编译器不检查被转换的值是否适合新类型,但从指针到整型的转换除外,反之亦然。
本节描述以下转换:
* 转换指针类型
* 转换空指针
* 转换到一个前向引用类类型
* 转换到引用类型
* 在成员指针类型间转换
转换指针类型
一个对象类型的指针可被显式地转换到另一个对象类型的指针,被说明为void *的指针被认为是指向任何对象类型的指针
。一个基类指针可以显式地转换到一个派生类的指针,只要以下条件被满足:
* 有一个非模糊的转换。
* 在任意点,基类都未被说明为虚拟的。
由于到void *类型的转换可以改变一个对象的表示,所以不保证转换type1*void* type2*等价于转换type1* type2*(仅是值的改变)。
当执行这样的转换时,结果是指向表示基类的源对象的子对象的一个指针。
关于模糊的和虚拟的基类更多信息参见第9章“派生的类”。
C++允许对象或函数指针显式转换到void *类型。如果函数指针类型有足够的位容纳对象指针类型,则对象指针类型可显式地转换到函数指针。
常量对象的指针可显式地转换到非常量类型的指针,此转换的结果指向源对象。常量类型对象或常量类型对象的引用可造型转换到非常量类型的一个引用。结果是源对象的一个引用。源对象很可能说明为常量,因为它要在程序持续期间保留常量。因此,一个显式转换导致这种安全保护失败,而允许对这种对象的修改,在这种情况中的动作是不确定的。
volatile类型对象的指针可以造型转换到非volatile类型的指针,这种转换的结果指的是源对象。类似地,volatile类型对象可以造型转换为非volatile类型的引用。
转换空指针
空指针(0)被转换到它自己。
转换到一个前向引用类类型
一个类已被说明但尚未定义(一个前向引用)可被用于指针造型转换,在这种情况下,如果类的关系已知,编译器返回一个源对象指针,而不是可能的子对象的指针。
转换到引用类型
任何其地址可被转换到一个给定指针类型的对象,也可以转换到模拟的引用类型,例如,任何其地址可被转换到类型char *的对象也可转换到类型char &。没有构造函数或类转换函数被调用以产生一个到引用类型的转换。
对象或值可被转换到类类型对象,仅当为此目的已特别提供了一个构造函数或转换运算符。关于这些用户定义的函数的更多信息参见第11章“特殊成员函数”中的“转换构造函数”。
一个基类的引用到一个派生类的引用的转换(或反之亦然)与指针的作法相同。到引用类型的造型转换结果为一个l值,造型转换到其它类型的结果不是l值。在指针或引用造型转换的结果上执行的操作仍在源对象上执行。
在成员指针类型之间转换
一个成员指针可以按以下规则转换为不同的成员指针类型:两个指针必须是同一个类的成员指针或必须是两个类的成员指针,且其中之一非模糊地从另一个派生而来。当转换成员指针函数时,返回的与参量的类型必须匹配。
带成员指针运算符的表达式
成员指针运算符.*和->*返回表达式左侧指定的对象的特定的类成员的值,以下例子显示了如何使用这些运算符:
#include <iostream.h>
class Window
{
public:
void Paint(); //使窗口重画
int WindowsId;
};
//定义派生的类型pmfnPaint和pmWindowId
//这些类型分别是Paint()和WindowId成员指针
void (Window::*pmfnPaint)()=&Window::Paint;
int Window::*pmWindowId=&Window::WindowId;
void main()
{
Window AWindow;
Window *pWindow=new Window;
//正常调用Paint函数,然后使用成员指针
AWindow.Paint(); (AWindow.*pmfnPaint)();
pWindow ->Paint();
(pWindow ->*pmfnPaint)(); //由于*没有函数调用联结紧密,所以需要括号
int Id;
//检索窗口编号
Id=AWindow.*pmWindowId;
Id=pWindow->*pmWindowId;
}
在上面例子中,pmfnPaint的成员指针被用于调用成员函数Paint。另一个成员pmWindowId的指针被用于访问WindowId成员。
语法
pm-表达式:
造型转换表达式
pm-表达式.*造型转换表达式
pm-表达式->*造型转换表达式
双目运算符.*将其第一个操作数(即必须为一个类类型对象)和第二操作数(即必须为成员指针类型)组合在一起。
双目运算符->*将其第一个操作数(即必须为类类型对象的指针)和第二个操作数(即必须为成员指针类型)组合在一起。
在一个包含.*运算符的表达式中,第一个操作数必须为在第二个操作数中指定的成员指针的类类型,且可访问该指针或非模糊地从那个类派生且可访问的一个可访问类型。
在一个包含->*运算符的表达式中,第一个操作数必须为第二个操作数指定类型的“类类型指针”类型,或必须为从那个类非模糊派生出的一个类型。考虑以下类和程序段:
class BaseClass
{
publc:
BaseClass(); //基类构造函数
void Funcl();
};
//说明成员函数Func1的一个指针
void (BaseClass::*pmfnFunc1)()=&BaseClass::Func1;
class Derived : public BaseClass
{
public:
Derived(); //派生类构造函数
void Func2();
};
//说明成员函数Func2的一个指针
void (Derived::*pmfnFunc2)()=&Derived::Func2;
void main()
{
BaseClass ABase;
Derived ADerived;
(ABase.*pmfnFunc1)(); //可行:为Baseclass定义
(ABase.*pmfnFunc2)(); //错误:不能用基类去访问派生类成员指针
(ADerived.*pmfnFunc1)(); //可行:Derived是从BaseClass非模糊地派生的
(ADerived.*pmfnFunc2)(); //可行:为Derived定义
}.
*或->*成员指针运算符的结果是成员指针说明中指定的类型的一个对象或函数。因此,在前面的例子中,ADerived.*pmfnFunc1()表达式的结果是返回void的函数的一个指针。如果第二个操作数值为1,则此结果也值为1。
注意:如果成员指针运算符之一的结果是一个函数,那么结果仅能被用作函数调用运算符的一个操作数。
本节解释表达式在何时以及以什么样的顺序被求值,包括以下主题:
* 求值的顺序
* 顺序点
* 模糊表达式
* 表达式中的表示
求值的顺序
本节讨论表达式被求值的顺序,但不解释这些表达式中运算符的语法或语义,本章较前面的章节提供了这些运算符每个的完整参考。
表达式按照其运算符的优先级及分组情况被求值(第1章“词法规定”中的表1.1给出了施加于表达式上的C++运算符的关系)。考虑这个例子:
#include <iostream.h>
void main()
{
int a=2,b=4,c=9;
cout << a + b * c << "/n";
cout << a + (b * c) << "/n";
cout << (a + b) * c<< "/n";
}
前面代码的输出为:
38
38
54
4.1中给出的表达式求值顺序由运算符的优先级和结合律确定:
1. 在这个表达式中乘法(*)具有最高优先级,因此,b*c子表达式首先被求值。
2. 加法(+)具有次级优先级,所以a被加到b和c的积中。
3. 左移(<<)在表达式中具有最低优先级,但有两次出现,由于左移运算符从左到右分组,左子表达式首先被求值,而后是右子表达式。
当括号被用于分组子表达式时,它们改变表达式被求值的优先级以及求值顺序,如图4.2所示。
4.2中那些表达式纯粹是为了副作用而求值,在这种情况下,将信息转换到标准输出设备。
注意:左移运算符被用于向一个类输出流对象中插入一个对象。当与输入输出流一起使用时,有时它被称为“插入”运算符。有关输入输出流库的更多内容,参见“Microsoft Visual C++6.0参考库”的“Micosoft Visual C++6.0运行库参考”卷。
顺序点
一个表达式在连续的“顺序点”之间只能改变一个对象的值一次。
Microsoft特殊处
C++语言定义现在不指定顺序点。对任何包含C运算符和不包含重载的运算符的表达式,Microsoft C++使用与ANSI C相同的顺序点。当运算符被重载时,语义从运算符顺序变为函数调用顺序。Microsoft C++使用以下顺序点:
* 逻辑与运算符(&&)的左操作数,在继续之前,逻辑与运算符的左操作数被完全求值,且所有的副作用被完成。不保证逻辑与运算符的右操作数将被求值。
* 逻辑或运算符(||)的左操作数,在继续之前,逻辑或运算符的左操作数被完全求值,且所有的副作用被完成。不保证逻辑或运算符的右操作数将被求值。
* 逗号运算符的左操作数,在继续之前,逗号运算符的左操作数被完全求值,且所有的副作用被完成。逗号运算符的两个操作数总是被求值的。
* 函数调用运算符,一个函数的函数调用表达式和所有的参量,包括缺省参量,在进入函数之前被求值,且所有的副作用被完成。在参量或函数调用表达式之间没有特别指定求值的顺序。
* 条件运算符的第一个操作数,在继续之前,条件运算符的第一个操作数被完全求值,且所有的副作用被完成。
* 一个完整初始化表达式的结束,如在说明语句中一个初始化的结束。
* 在表达式语句中的表达式,表达式语句由可选的表达式后跟一个分号(;)组成。表达式为其副作用而被完全求值。
* 在一个选择(if或switch)语句中的控制表达式,在依赖于这样的代码被执行之前,表达式被完全求值,且所有副作用被完成。
* 在一个while或do语句中的控制表达式。在while或do循环的下一次重复的任何语句执行之前,表达式被完全求值,且所有副作用被完成。
* for语句三个表达式中的每一个,在进入下一个表达式之前,每一个表达式被完全求值,且所有副作用被完成。
* 在返回语句中的表达式,在控制返回到调用函数之前,表达式被完全求值,且所有副作用被完成。
Microsoft特殊处结束
模糊的表达式
某些表达式在含义上是模糊的,当同一个表达式中一个对象的值被不止一次地修改时,这些表达式最频繁地出现。这些表达式依赖于一个特定的求值顺序,而该语言中却没有定义一种求值顺序,考虑以下例子:
int i=7;
func(i,++i);
C++语言不保证在函数调用中参量的求值顺序。因此,在上例中,func可能为其参量接收7和8或者8和8,这取决于参量是从左到右还是从右到左求值的。
表达式中的表示法
C++语言在指定操作数时指定某些兼容性,表4.5给出了要求操作数为type类型的运算符可接受的操作数的类型。
表4.5 运算符可接受的操作数类型
期望的类型 许的类型
type nst type
volatile type
type &
const type &
volatile type &
volatile const type
volatile const type &
type* ype* coust
type* volatile
type* volatile const
const type ype
const type
const type &
volatile type type
volatile type
volatile type &
由于前面的规则总是可以用于组合中,在期望一个指针的地方可以提供一个常量指针指向一个volatile对象。
C++语言规定:如果一个从基类派生的类包含虚函数,则指向那个基类类型的指针可用于调用存在于派生类对象中的该虚函数的实现。包含虚函数的类有时被称为一个“多态类”。
由于一个派生类完全包含它所从派生的所有基类的定义,所以可安全地造型一个指针向上到类层次指向这些基类的任意一个。给定一个指向基类的指针,按层次向下造型指针可以是安全的,如果正被指向的对象实际是从基类派生的一个类型,则是安全的。在这种情况下,实际对象被说是“完整的对象”,基类指针被说是指向完整对象的“子对象”。例如,考虑图4.3中给出的类层次。
一个类型C的对象,如图4.4中所示被可视化。
给定一个类C的实例,有一个B子对象和一个A子对象。包括A和B子对象的C的实例是“完整的对象”。
使用运行类型信息,可以检查一个指针是否真的指向一个完整对象,而且是否可以安全地造型指向其层次中另一个对象。dynamic_cast运算符可用于完成这些类型的造型转换,它还执行必要的运行检查以便使操作安全。
造型运算符
有几个造型运算符是C++语言特定的。这些运算符旨在除去旧的C语言造型形式中模糊性和危险的继承。这些运算符是:
* dynamic_cast 用于多态类型的转换
* static_cast 用于非多态类型的转换
* const_cast 用于除去const,volatile和__unaligned属性
* reinterpret_cast 用于位的简单再解释
使用const_cast和reinterpret_cast作为一种最后的手段,因为这些运算符与旧的造型形式有相同的危险性。但是它们还是有必要用于完全取代旧的造型形式。
dynamic_cast运算符
表达式dynamic_cast (expression)将expression操作数转换为一个type-id类型的对象,type-id必须为以前定义的类类型或“void指针”的一个指针或一个引用。如果type-id是一个指针,则expression的类型必须为一个指针或如果type-id是一个引用,则它为一个l值。
语法
dynamic_cast (expression)
如果type-id是一个指向expression的非模糊可访问的直接或间接基类的指针,则结果是type-id类型的唯一子对象的一个指针,例如:
class B{...};
class C:public B {...};
class D:public C {...};
void f(D* pd)
{
C* pc=dynamic_cast<C*>(pd); //可行:C是一个直接基类,pc指向pd的C子对象
B* pb=dynamic_cast<B*>(pd); //可行:B是一个间接基类,pd指向pd的C子对象
...
}
这种类型转换被称为向上造型,因为它使指针按类层次向上移,从一个派生的类到一个派生源类。向上造型是一种隐式转换。
如果type-id是void*,则执行一种运行检查以确定expression的实际类型,结果是由expression指向的完整对象的一个指针。
例如:
class A{...};
class B{...};
void f()
{
A* pa=new A;
B* pb=new B;
void* pv=dynamic_cast<void*>(pa);
//pv现在指向类型A的一个对象
... pv=dynamic_cast<void*>(pb);
//pv现在指向类型B的一个对象
}
如果type-id不是void*,则执行运行检查去查看由expression指向的对象是否可转换为type-id指向的类型。如果expression的类型是type-id类型的一个基类,则执行运行检查去查看expression是否确实指向type-id类型的一个完整对象,如果是真的,则结果是type-id类型的一个完整对象的指针。
例如:
class B{...};
class D : public B{...};
void f()
{
B* pb=new D;//不清楚但是可行
B* pb2=new B;
D* pd=dynamic_cast<D*>(pb); //可行:pb确实指向一个D
...
D* pd2=dynamic_cast<D*>(pb2); //错误:pb2指向一个B,而不是一个D,pd2==NULL
...
}
这种类型转换被称为向下造型,因为它使指针按类层次向下移,从一个给定类到一个从它派生出的类。
在多重继承的情况下,模糊性的可能性被引入。可虑图4.5中给出的类层次:
类型D的一个对象的指针可被安全地造型到B或C。但是如果D被造型到指向一个A对象,结果将是A的哪个实例呢?这将导致一个模糊造型错误。为了克服这个问题,你可以执行两个非模糊的造型。例如:
void f()
{
D* pd=new D;
A* pa=dynamic_cast<A*>(pd); //错误:模糊的
B* pb=dynamic_cast<B*>(pd); //首行造型到B
A* pa2=dynamic_cast<A*>(pb); //可行:非模糊的
}
当你使用虚拟基类时还会引入更进一步的模糊性。考虑图4.6所示的类层次:
在此层次中,A是一个虚拟基类,关于一个虚拟基类的定义见第9章“派生类”中的“虚拟基类”。给定E类的一个实例及指向A子对象的一个指针,转到指向B的指针的dynamic_cast将因为模糊性而失败。你必须先造型回到完整的E对象,然后沿层次向上,以一种非模糊的方式到达正确的B对象。
考虑图4.7所示的类层次。
给定类型E的一个对象和D子对象的一个指针,从D子对象导航到最左边的A子对象,可有三种转换。你可以执行一个dynamic_cast转换从D指针转到E指针,然后一个转换(为dynamic_cast或隐式转换)从E到B,最后一个隐式转换从B到A。例如:
void f(D* pd)
{
E* pe=dynamic_cast<B*>(pd);
B* pb=pe //向上造型,隐式转换
A* pa=pb; //向上造型,隐式转换
}
dynamic_cast运算符还可以用于执行“跨越造型”。使用相同的类层次,例如可以造型一个指针从B子对象到D子对象,只要完整的对象是E类型的。
考虑跨越造型,确实有可能仅在两步内做转换从D指针到最左边的A子对象的指针。你可以从D到B造型转换,然后一个隐式转换从B到A。例如:
void f(D* pd)
{
B* pb=dynamic_cast<B*>(pd); //跨越造型转换
A* pa=pb;//向上造型,隐式转换
}
一个空指针值被dynamic_cast转换为目标类型的空指针值。
当你使用dynamic_cast (expression)时,如果expression不能被完全地转换到type_id类型,则运行检查会导致造型转换失败。例如:
class A{...};
class B{...};
void f()
{
A* pb=new A;
B* pb=dynamic_cast<B*>(pa); //失败,不安全;B不是从A派生来的
...
}
到指针类型的一个失败的造型转换的值是空指针。到引用类型的失败的造型转换丢弃了一个bad_cast异常。
bad_cast异常
dynamic_cast运算符丢弃了一个bad_cast异常,作为到引用类型的一个失败的
造型的值,bad_cast的接口为:
class bad_cast:public logic
{
public:
bad_cast(const __exString& what_arg): logic(what_ary) {}
void raise() {handle_raise(); throw *this;}
//虚拟的__exString what() const; //继承的
};
static_cast运算符
static_cast (expression)表达式将expression转换到独立地基于表达式中给出的类型type_id类型。没有运行类型检查被执行以确保转换的安全性。
语法
static_cast (expression)
static_cast运算符可被用在例如转换一个基类指针到一个派生类指针的操作,这样的转换不总是安全的,
例如:
class B{...};
class D:public B{...};
void f(B* pb,D* pd)
{
D* pd2=static_cast<D*>(pb); //不安全,pb可能只指向B
B* pb2=static_cast<B*>(pd); //安全转换
...
}
与dynamic_cast相反,在pb的static_cast转换上没有作任何运行检查。由pb指向的对象可能不是D类型的对象,使用*pd2的情况可能是灾难性的。例如,调用一个函数是D类的一个成员但不是B类的,可能导致访问冲突。
dynamic_cast和static_cast运算符可在一个类层次中移动一个指针,但static_cast仅依赖于造型转换语句中提供的信息,因此是不安全的。例如:
class B{...};
class D:public B{...};
void f(B* pb)
{
D* pd1=dynamic_cast<D*>(pb);
D* pd2=static_cast<D*>(pb);
}
如果pb真的指向类型D的一个对象,则pd1和pd2将得到相同的结果。如果pd==0,则它们也将得到相同的结果。
如果pb指向类型B的一个对象,但不是指向完整的D类,则dynamic_cast将足以知道返回0。但是static_cast依赖于程序员的坚持让pb指向类型D的一个对象而且简单地返回那个假定的D对象的指针。
因此,static_cast可以实现相反的隐式转换,在这种情况下,结果是未定义的,留给程序员去确保static_cast转换的结果为安全的。
这些动作还适用于类类型以外的其它类型。例如,static_cast可被用于从int到char的转换。但结果字符可能没有足够的位数保持整个整型值,又一次留给程序员去确保static_cast转换的结果为安全的。
static_cast运算符还可被用于执行任何隐式转换,包括标准转换和用户定义的转换。例如:
typedef unsigned char BYTE
void f()
{
char ch;
int i=65;
float f=2.5;
double db1;
ch=static_cast (i); //从int到char
db1=static_cast (f);//从float到double
...
i=static_cast (ch);
]
static_cast运算符可显式地将一个整型值转换到一个枚举类型。如果整型值未落在枚举值的范围内,枚举结果值是不确定的。
static_cast运算符将一个空指针值转换到目标类型的空指针值。
任何表达式可用static_cast运算符显式地转换到void类型,目标void类型可有选择地包括const、volatile或__unaligned属性。
static_cast运算符不能造型除去const、volatile或_ _unaligned属性。有关除去这些属性的信息参见“const_cast运算符”。
const_cast运算符
const_cast运算符可被用于从一个类中除去const、volatile和_ _unaligned属性。
语法
const_cast (expression)
任何对象类型的指针或一个数据成员的指针可被显式地转换到完全相同的类型,带const,volatile和__unaligned限定符除外。对指针和引用,结果将指向源对象,对数据成员指针结果和数据成员的源(非造型)指针一样指向同一成员。由于依赖于引用对象的类型,通过指针、引用或数据成员指针的求结果的写操作将产生未定义的动作
。const_cast运算符将一个空指针值转换为目标类型的空指针值。
reinterpret_cast运算符
reinterpret_cast运算符允许任何指针被转换到任何其它指针类型,它还允许
任何整型转换到任意指针类型,且反之亦然。滥用reinterpret_cast运算符可轻易导致不安全,除非是所期望的转换是固有的低等级,否则你应使用其它造型运算符之一。
语法
reinterpret_cast (expression)
reinterpret_cast运算符可被用于像char*到int*或One_class*到Unrelated*这种内部的非安全的转换。
reinterpret_cast的结果除了用于造型回到其源类型外,不能安全地用于其它任何情况,其它用法至多也是不可移植的。
reinterpret_cast运算符不能造型除去const、volatile或__unaligned属性,关于除去这些属性的信息参见const_cast运算符。
reinterpret_cast运算符将一个空指针值转换到目标类型的空指针值。
运行类型信息
运行类型信息(RTTI)是一种机制,允许对象的类型在程序执行期间被确定。RTTI加到C++语言中,因为很多类库销售者正自己实现这个功能,这导致库之间的不兼容。因此,很明显需要在语言级支持运行类型信息。
为了明确性,RTTI的这个讨论几乎完全限制于指针。但是讨论的概念也适用于引用。
运行类型信息有三个主要的C++语言元素:
* dynamic_cast运算符,用于多态类型的转换。更多的信息参见本章前面部分“dynamic_cast运算符”。
* typeid运算符,用于指定一个对象的确切类型。
* type_info类,用于保持由typeid运算符返回的类型信息。
typeid运算符
typeid运算符允许一个对象的类型在运行时被确定。
语法
typeid(type_id)
typeid(expression)
一个typeid表达式的结果是一个常量type_info&。其值是一个type_info对象的引用。对象表示type_id或expression的类型,这依赖于typeid使用的是哪种形式。更多的信息参见本章后面“type_info类”。
typeid运算符在被用于一个多态类类型的l值时做运行检查,对象的真正类型不能由所提供的静态信息确定。这些情况有:
* 一个类的引用
* 一个指针,用*间接引用
* 一个下标指针(如[])(注意用带多态类型的指针的下标通常是不安全的)
如果expression指向一个基类类型,且对象实际上是从基类派生的类型,则结果是派生类的type_info引用。expression必须指向一个多态类型,即带虚拟函数的类。否则,结果是在expression中指出的静态类的type_info。而且,指针必须为间接引用的,因此它所指向的对象被使用。没有间接引用指针,则结果将是指针的type_info,而不是它所指向的内容。例如:
class Base{...};
class Derived:public Base{...};
void f()
{
Derived* pd=new Derived;
Base* pb=pd;
...
const type_info& t=typeid(pb); //t保持type_info指针
const type_info& t1=typeid(*pb); //t1保持派生的info
...
}
如果expression间接引用一个指针,而且那个指针的值为0,typeid丢弃一个bad_typeid异常。如果指针不是指向一个有效的对象,异常__non_rtti_object被丢弃。
如果expression既不是对象的基类的一个指针也不是一个引用,则结果是表示expression的静态类型的一个type_info引用。
bad_typeid异常
在有些情况下,typeid运算符丢弃一个bad_typeid异常,bad_typeid的接口为:
class bad_typeid:public logic
{
public:
bad_typeid(const char * what_arg):logic(what_arg) {}
void raise() { handle_raise(); throw * this; }
//虚拟的__exString what() const;//继承的
};
更多的信息参见“typeid运算符”。
type_info类
type_info类描述由编译器在程序内产生的类型信息,该类的对象有效地存储一个指向类的名称的指针,type_info类还存储适合于比较两个类型的相等性或整理顺序的一个编码值。类型的编码规则和整理顺序是未指定的,而且在不同程序间可以有所不同。
要使用type_info类必须包含typeinfo.h头文件。
class type_info
{
public:
virtual ~type_info();
int operator==(const type_info& rhs) const;
int operator!=(const type_info& rhs) const;
int before(const type_info& rhs) const;
const char* name() const;
const char* raw_name() const;
private:
...
};
==和!=运算符可分别用于比较与其它type_info对象是相等还是不相等的。在类型的整理顺序和继承性关系之间没有联系。在成员函数之前使用type_info::去指定类型的整理顺序。不保证前面的type_info::在不同的程序或即使是同一程序的不同运行中产生相同的结果。在这种况下,前面的type_info::类似于取地址(&)运算符。
type_info::name成员函数为代表类型的、人可读的名称的0终止字符串返回一个char*常量。被指向的存储器是被高速缓存的,而且永远不应该被直接撤消分配。
type_inf::raw_name成员函数为表示对象类型装饰名的0终止字符串返回一个char*常量。名称实际上被存在其装饰格式内,以节省空间。因而,此函数比type_info::name速度快,因为它不需要非装饰名称。由type_info::raw_name函数返回的字符串在比较操作中很有用,但却不是可读的。如果你需要一个人可读的字符串,则用type_info::name函数。
只有指定/GR(使能运行类型信息)编译器选项时产生多态类的类型信息。