第13章 说 明 符
一个“说明符”是一个说明的一部分,用以命名一个对象、类型或函数。说明符是出现在说明中的以一个或多个被逗号分开的名称,每个名称可有一个相关的初始化器。
语法
说明符表:
初始说明符
说明符表,初始说明符
初始说明符:
说明符 初始化器opt
本章包括以下主题:
* 说明符概述
* 类型名称
* 抽象说明符
* 函数定义
* 初始化器
说明符是指定名称的一个说明的组成成分。说明符还可修饰基本的类型信息,使得名称成为函数或者对象或函数的指针(在第6章“说明”中讨论的指示符传送诸如类型和存储类属性。在本章以及附录B“Microsoft特殊修饰符”中讨论的修饰符用以修饰说明符)。
图7.1 给出了两个名称szBuf和strcpy的一个完整说明,并提出了说明的各组成部件。
Microsoft特殊处
大多数Microsoft扩充关键字可被用作形成派生类型的修饰符;它们不是指示符或说明符(见附录B“Microsoft特殊修饰符”)。
Microsoft特殊处结束
语法
说明符:
dname
ptr运算符 说明符
说明符(参量说明表) cv修饰符表
说明符[常量表达式opt]
(说明符)
ptr运算符:
* cv限定符表opt
& cv限定符表opt
完整的类名称::*cv限定符表
optcv限定符表:
cv限定符 cv限定符表
optcv限定符:
const
volatile
cv修饰符表:
cv限定符 cv修饰符表opt
pmodel cv修饰符表opt
dname:
名称
类名称
~类名称
typedef名称
限定类型名称
说明符出现在说明语法中可选的指示符表(decl-specifiers)后面,这些指示符在第6章“说明”中讨论。一个说明可包含不止一个说明符,但每个说明符仅说明一个名称。以下说明样本显示了指示符和说明符是如何组合构成一个完整的说明的:
const char *pch,ch;
在前面这个说明中,关键字const和char组成了指示符表。列出了两个说明符*pch和ch。
然后一个说明简化的语法如下,其中const char是类型,*pch和ch是说明符:type declarator1[,declarator2[...,declaratorn]];
当联结于一个说明符表的元素没有产生想要的结果,你可以使用括号使其表达清晰。但一个更好的技术是使用typedef或括号组合和typedef关键字。考虑说明一个函数的指针数组。每个函数必须遵从相同的协议,从而参量和返回值是已知的:
//函数返回类型int,带一个char *类型参量
typedef int(*PIFN)(char*);
//说明一个7个函数指针的数组,返回int,带一个char*类型参量
PIFN pifnDispatchArray[7];
等价的说明可以不用typedef说明书写,但它太复杂了,而使得错误的潜在性超出了它的任何效益:
int (*pifnDispatchArray[7](char*);
类型名称按以下方式被用于某些说明符中:
* 在显式转换中
* 作为sizeof运算符的参量
* 作为new运算符的参量
* 在函数原型中
* 在typedef语句中
一个类型名称构成类型指示符,如第6章“说明”中所描述以及下节“抽象说明符”所讨论的。
在以下例子中,函数strcpy的参量通过使用它们的类型名而提供。在source参量情况中,const char是指示符,*是抽象说明符:
static char *szBuf, *strcpy(char *dest, const char *source);
语法
类型名称:
类型指示符表 抽象说明符opt
类型指示符表:
类型指示符 类型指示符表opt
抽象说明符:
ptr运算符 抽象说明符opt
抽象说明符opt(参量说明表) cv限定符表opt
抽象说明符opt[常量表达式opt]
(抽象说明符)
一个抽象说明符是一个说明符,其中省略了标识符(相关的信息参见前一节“类型名称”)。
本节讨论以下抽象说明符:
* 指针
* 引用
* 成员指针
* 数组l 函数
* 缺省参量
一个抽象说明符是一个不说明一个名称的说明符,即标识符被省略。例如:char *
说明类型“指向char类型的指针”。这个抽象说明符可被用于如下所示的一个函数原型中:
char *strcmp(char *, char *);
在此原型(说明)中,函数的参量被指定为抽象说明符。以下是一个更复杂的抽象说明符,说明“指向一个带两个参量,且都是char*类型的函数的指针”,返回类型char *:
char *(*)(char*, char*)
由于抽象说明符完全地说明了一个类型,所以构成下面形式的表达式是合法的://获取一个char类型的10个指针的数组的尺寸
size_t nSize=sizeof(char *[10]);
//分配一个指向没有返回值且不带参量的函数的指针
typedef void(PVFN *)();
PVFN *pvfn=new PVFN;
//分配一个返回类型为WinStatus且带一个WinHandle类型的参量的//函数的指针数组
typedef WinStatus (PWSWHFN *)(WinHandle);
PWSWHFN pwswhfnArray[]=new PWSWHFN[10];
要执行从一个类型到另一个类型的显式转换,你必须使用强制转换,指出所希望的类型名称。有些类型造型转换导致语法模糊性,以下函数形式类型强制转换是模糊的:
char *aName(String(s));
不清楚它是一个函数说明,还是一个将函数形式强制转换作为初始化器的一个对象说明:它可能说明一个返回类型为char*,且带一个String类型参量函数,或是可能说明对象aName,并用s造型转换到类型string的值对其进行初始化。
如果一个说明可被认为是一个有效的函数说明,则它被这样对待。仅当它不可能是一个函数说明——即,如果它是语法错误的——是一个语句检测它是否是一个函数形式类型造型转换。因此,class="tags" href="/tags/BianYiQi.html" title=编译器>编译器把语句作为一个函数的说明,并忽略标识符S周围的括号。另一方面,语句:
char *aName((String)s);
和
char *aName=String(s);
是对象的清楚的说明,且一个用户定义的从String类型到char*类型的转换被调用以执行aName的初始化。
指针是使用说明符语法进行说明的:
*cv限定符表opt dname
一个指针拥有一个对象的地址。那么一个完全的说明是:
说明指示符 *cv限定符表opt dname;
这个说明的一个简单例子为:
char *pch;
上面的说明指定pch指向一个char类型的对象。
const和volatile指针
const 和volatile关键字改变指针是如何被对待的。const关键字指定指针在初始化之后不能被修改,指针从那以后是被保护的,不能被修改。
volatile关键字指定其后的名称相关的值可以通过不是用户应用中的那些动作而进行修改,因此,volatile关键字对在可被多个过程访问的共享存储器中或与中断服务例程通讯用的全局数据区域中说明对象是非常有用的。
当一个名称被说明为volatile时,class="tags" href="/tags/BianYiQi.html" title=编译器>编译器在它每次被程序访问时从存储器中重新装入其值,这极大地降低了可能的优化。但是当一个对象的状态可能出乎意料地改变时,它是确保程序可预言的执行的唯一方法。
要说明由const 或volatile指针指向的对象,使用这种形式的说明:
const char *cpch;
volatile char *vpch;
要说明指针值,即指针中存储的实际地址为const或volatile,使用这种形式的说明:
char * const pchc;
char * volatile pchv;
C++语言不允许对声明为const的指针或对象的修改赋值,这种赋值将去除对象或指针被说明时用的信息,因此与源说明的意图相冲突,考虑以下说明:
const char cch='A';
charch='B';
给定前面的两个对象的说明(cch为const char类型,ch为char类型),则以下说明/初始化是有效的:
const char*pch1=&cch;
const char *const pch4=&cch;
const char*pch5=&ch;
char *pch6=&ch;
char *constpch7=&ch;
const char *const pch8=&ch;
以下说明/初始化是错误的:
char *pch2=&cch; //错误
char *const pch3=&cch; //错误
pch2的说明通过一个可能被修改而因此是不允许的常量对象说明了一个指针。pch3的说明指定指针是常量,而不是对象,此说明以与pch2说明不被允许相同的理由而不被允许。
以下8个赋值显示了通过指针的赋值和对前面说明改变指针值的情况;现在,假定从pch1到pch8的初始化是正确的:
*pch1='A'; //错误:对象说明为常量
pch1=&ch; //可行:指针未被说明为常量
*pch2='A'; //可行:正常的指针
pch2=&ch; //可行:正常的指针
*pch3='A'; //可行:对象未被说明为常量
pch3=&ch; //错误:指针说明为常量
*pch4='A'; //错误:对象说明为常量
pch4=&ch; //错误:指针说明为常量
说明为volatile或const和volatile混合的指针遵从相同的规则。指向const对象的指针常被用于如下的函数说明中:
char *strcpy(char *szTarget, const char *szSource);
前面的语句说明了一个函数strcpy,带两个类型为“char指针”的参量,且返回一个指向char类型的指针。由于参量是由引用传递的,而不是通过值传递的,如果szSource没有被说明为const,则函数可自由修改szTarget和szSource。把szSource说明为const确保调用者szSource不能被调用的函数改变。
注意:由于存在一个从typename*到const typename*的标准转换,因此向strcpy传递一个类型char*的参量是合法的。但是,反之不然,不存在隐式的转换从一个对象或指针去除const属性。
给定类型的const指针可被赋给相同类型的一个指针。但是,一个不是const的指针不能赋给一个const指针,以下代码给出了正确和错误的赋值:
int *const cpObject=0;
int *pObject;
void main() { pObject=cpObject; //可行 cpObject=pObject; //错误 }
引 用
引用使用说明符语法进行说明:
语法
&cv限定符表opt dname
一个引用拥有一个对象的地址,但语法上表现得象一个对象。一个引用说明包括一个(可选的)指定符表,后跟一个引用说明符。
语法
说明指示符 &cv限定符表opt dname;
考虑用户定义的类型Date:struct Date
{ short DayOfWeek; short Month; short Day; short Year; };
以下语句说明一个Date类型对象和那个对象的一个引用:
Date Today; //说明对象
Date& TodayRef=Today;//说明引用
对象名称Today和对象的引用TodayRef在程序中可完全相同地被使用:
Today.DayOfWeek=3; //Tuesday
TodayRef.Month=7;//July
引用类型函数参量
通常向一个大对象传递引用比传递函数更有效。这允许class="tags" href="/tags/BianYiQi.html" title=编译器>编译器在保持已被用于访问对象的语法时传递对象的地址。考虑下面使用Date结构的例子:
//从阳历(格里历)日期创建一个DDDYYYY形式的Julian日期
long JulianFromGregorian(Date& GDate) { static int cDaysInMonth[]={ 31,28,31,30,31,30,31,31,30,31,30,31 }; long JDate; //为已经过去的月份加天数 for (int i=0;i<GDate.Month-1;++i) JDate+=cDaysInMonth[i]; //为本月加天数 JDate+=GDate.Day; //检查闰年 if (GDate.year%100!=0 && GDate.Year%4==0) JDate++; //加年 JDate*=10000; JDate+=GDate.Year; return JDate; }
前面的代码显示了通过引用传递的结构成员使用成员选择运算符(.)而不是指针选择运算符(->)访问的。
尽管作为引用类型传递的指针遵从非指针类型语法,但它们保留了指针类型的一个重要特征:它们是可修改的,除非被说明为const。因为前面代码的意图不是修改对象GDate,所以一个更适宜的函数原型为:
long JulianFromGregorian(const Date& GDate);
此原型保证函数JulianFromGregorian不改变其参量。
任何采用带引用类型原型化的函数可在其位置接受相同的类型的一个对象,因为从typename到typename&有一个标准的转换。
函数可被说明为返回一个引用类型。作这种说明有两个原因:
* 正在返回的信息是一个足够大的对象,返回一个引用比返回一个拷贝效率更高。
* 函数的类型必须为一个l值。
正如通过引用向函数传递大的对象可以更有效一样,通过从函数返回大对象也可以更为有效。引用返回协议去除了拷贝对象到返回之前的临时地点的必要性。
引用返回类型在函数必须求值为一个l值时也是很有用的。大多数重载的运算符属于此类,特别是赋值运算符。重载的运算符包含在第12章“重载”中的“重载的运算符”中,考虑第4章“表达式”中的Point例子:
class Point { public: //定义“访问器”函数为引用类型 unsigned& x(); unsigned& y(); private: unsigned obj_x; unsigned obj_y; }; unsigned& Point::x() { return obj_x; } unisgned& Point::y() { return obj_y; } void main() { Point ThePoint; //使用x()和y()作为l值 ThePoint.x()=7; ThePoint.y()=9; //使用x()和y()作为r值 cout << "x=" << ThePoint.x() << "/n" << "y=" << ThePoint.y() << "/n" }
注意函数x和y说明为返回引用类型。这些函数可用于一个赋值语句的任意一边。引用类型的说明必须包含初始化器,除了以下情形之外:
* 显示extern说明。
* 一个类成员的说明。
* 在一个类内的说明。
* 一个函数的参量或函数返回类型的说明。
指针引用和对象引用相同的方式进行说明。说明一个指针的引用产生一个象正常指针一样使用的可修改的值。以下代码例子说明使用一个指针的指针和一个指针的引用之间的差别:
#include
#include
//定义一个二叉树结构
struct BTree { char *szText; BTree *Left; BTree *Right; };
//定义一个指向树根的指针
BTree *btRoot=0;
int Add1(BTree **Root, char *szToAdd);
int Add2(BTree*& Root, char *szToAdd);
void PrintTree(BTree* btRoot); int main(int argc, char *argv[]) { if(argc<2) { cerr << "Usage:Refptr[1|2]" << "/n"; cerr << "/n/twhere:/n"; cerr << "/t1 uses double indirection/n"; cerr << "/t2 uses a reference to a pointer./n"; cerr << "/n/tInput is from stdin./n"; return 1; } char *szBuf=new char[132]; //从标准输入设备读一文本文件,并构造一棵二叉树 while(!cin.eof()) { cin.get(szBuf,132,'/n'); cin.get(); if(strlen(szBuf)) switch(*argv[1]) { //方法1:使用双重间接引用 case '1': Add1(btRoot,szBuf); break; //方法2:使用指针引用 case '2': Add2(btRoot,szBuf); break; default: cerr << "Illegal value "<< *argv[1] << "lsupplied for add method./n" << "Choose 1 or 2./n"; return -1; } } //显示排序表 PrintTree(btRoot); return 0; }
//PrintTree:按序显示二叉树
void PrintTree(BTree* btRoot) { //递归地遍历左子树 if(btRoot->Left) PrintTree(btRoot->Left); //打印当前结点 cout << btRoot->szText << "/n"; //递归地遍历右子树 if (btRoot->Right) PrintTree(btRoot->Right); }
//Add1:向二叉树中加入一个结点
//使用双重间接引
用int Add1(BTree **Root,char *szToAdd)
{ if ((*Root)==0) { (*Root)=new BTree; (*Root)->Left=0; (*Root)->Right=0; (*Root)->szText=new char[strlen(szToAdd)+1]; strcpy((*Root)->szText,szToAdd); return 1; } else if (strcmp((*Root)->szText,szToAdd)>0) return Add1(&((*Root)->Left),szToAdd); else return Add1(&((*Root)->Right),szToAdd); }
//Add2:向二叉树中加入一个结点// 使用指针引用int Add2(BTree*& Root, char *szToAdd)
{ if (Root==0) { Root=new BTree; Root->Left=0; Root->Right=0; Root->szText=new char[strlen(szToAdd)+1]; strcpy(Root->szText,szToAdd); return 1; } else if(strcmp(Root->szText,szToAdd)>0) return Add2(Root->Left, szToAdd); else return Add2(Root->Right, szToAdd); }
在上面程序中,函数Add1和Add2在功能上是等价的(尽管它们以不同的方式被调用)。差别在于Add1使用双重间接引用而Add2则使用方便的指针引用。
成员指针的说明是指针说明的特殊情况。
语法
明指示符
类名称::*cv限定符表opt dname;
指向一个类成员的指针与一个正常指针不同,因为它具有成员类型以及所属类的类型信息。一个正常指针仅仅标识(拥有地址)存储器中的单个对象。一个类成员指针标识类的任何实例中的那个成员。以下例子说明了一个类Window和一些成员数据指针:
class Window { public: Window(); //缺省的构造函数 Window(int x1,int y1, //构造函数指定窗口大小 int x2,int y2); BOOL SetCaption(const char *szTitle); //设置窗口标题 const char *GetCaption(); //获取窗口标题 char *szWinCaption; //窗口标题
//说明一个指向数据成员szWinCaption的指针
char *Window::*pwCaption=&Window::szWinCaption;
在上面的例子中,pwCaption是一个指向类Window的具有char*类型的任何成员的指针。pwCaption的类型是char *Window::*。下面的代码段说明指向SetCaption和GetCaption成员函数的指针。
const char * (Window::*pfnwGC)()=&Window::GetCaption;
BOOL (Window::*pfnwSC)(const char *)=&Window::SetCaption;
指针pfnwGC和pfnwSC分别指向Window类的GetCaption和SetCaption。该代码通过使用成员pwCaption指针直接拷贝信息到窗口标题中:
Window wMainWindow;
Window *pwChildWindow=new Window;
char *szUntitled="Untitled-";
int cUntitledLen=strlen(szUntitled);
strcpy(wMainWindow.*pwCaption,szUntitled);
(wMainWindow.*pwCaption)[cUntitledLen-1]='1'; //与下面一样
//wMainWindow.SzWinCaption[]='1';
strcpy(pwChildWindow->*pwCaption,szUntitled);
(pwChildWindow->*pwCaption)[szUntitledLen-1]='2';//与下面一样
//pwChildWindow->szWinCaption[]='2';.
*和->*运算符(成员指针运算符)的差别是:.*运算符通过给定一个对象或对象
引用选取成员,而->*运算符通过一个指针选取成员(关于这些运算符的更多信息参见第4章“表达式”中的“带成员指针运算符的表达式”)。
成员指针运算符的结果是成员的类型,在此例情形中为char*。
以下代码段使用成员指针调用成员函数GetCaption和SetCaption:
//分配一个缓冲区
char szCaptionBase[100];
//拷贝主窗口标题到缓冲区中,并添加“[View 1]”.
strcpy(szCaptionBase,(wMainWindow.*pfnwGC)());
strcat(szCaptionBase,"[View 1]");
//设置子窗口标题
(pwChildWindow->*pfnwSC)(szCaptionBase);
的限制一个静态成员的地址不是一个成员指针,它是一个静态成员实例的规则指针。但对一个给定类的所有对象仅存在一个静态成员实例,普通的取地址(&)和间接引用(*)运算符可以使用。
成员指针和虚拟函数
通过一个成员指针函数调用一个虚拟函数就如同函数已被直接调用一样:正确的函数在v表中查找并调用。以下代码显示了这是如何完成的:
class Base { public: virtual void Print(); }; void (Base::*bfnPrint)()=&Base::Print; void Base::Print() { cout << "Print function for class 'Base'/n"; } class Derived:public Base { public: void Print();//Print还是一个虚拟函数 }; void Derived::Print() { cout << "Print function for class 'Derived'/n; } void main() { Base *bPtr; Base bObject; Derived dObject; bPtr=&bObject; //设置指向bObject的地址的指针 (bPtr->*bfnPrint)(); bPtr=&dObject; //设置指向dObject的地址的指针 (bPtr->*bfnPrint)(); }
该程序的输出为:
Print function for Class 'Base'
Print function for Class 'Derived'
虚拟函数工作的关键,和通常一样,是通过指向一个基类的指针调用它们(关于虚拟函数的更多信息参见第9章“派生类”中的“虚拟函数”)。
使用继承表示类成员指针
在类定义之前说明类成员指针影响产生可执行文件的大小和速度。表示一个类成员指针所需要的字节数和解释所要求的代码依赖于类是否定义为不带、带单个和多个或虚拟的继承性。
总而言之,一个类用的继承越复杂,表示类成员指针所需的字节数就越多,解释指针所需的代码就越大。
如果你需要在类定义前说明一个类成员指针,你必须要么使用/vmg命令行选项,要么使用相关的class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示,或者你可以通过使用__single_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance、__multiple_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance或__virtual_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance关键字指定在类说明中用到的继承性,因而允许在每个类基础上产生代码的控制。这些选项在下面进行解释。
注意:如果你总是在类定义之后说明一个类成员指针,则你不需要使用任何这些选项。
Microsoft通过选择最紧凑的表示可能意图优化成员指针的表示和产生的代码。这要求定义成员指针类是基于成员指针被说明的地点的。class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示允许放宽此限制,并控制指针大小和解释指针所需的代码。
语法
#pragma class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members(指针说明,[最通用的表示])
指针说明参量是指定你是否已在相关的函数定义之前或之后说明了一个成员指针。指针说明参量可以是full_generality或best_case。
最通用的表示参量指定class="tags" href="/tags/BianYiQi.html" title=编译器>编译器可安全地用于引用一个转换单元中任一类成员指针的最小指针表示。此参量可以是single_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance、multiple_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance或virtual_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance。带best-case参量的class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示是class="tags" href="/tags/BianYiQi.html" title=编译器>编译器的缺省值,如果你总是在说明一个类成员指针之前定义类,则你可以使用此缺省值。当class="tags" href="/tags/BianYiQi.html" title=编译器>编译器遇到一个类成员指针的说明时,它已经知道类所用的继承种类。因而,class="tags" href="/tags/BianYiQi.html" title=编译器>编译器可以使用指针的最小可能表示,并产生为每种继承操作于指针上所要求的最少的代码量。这等价于在命令行使用/vmb去为转换单元中所有的类指定最佳情形的表示。
如果你需要在定义类之前说明一个类成员指针,则使用带full_generality参量的class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示(如果你在两个不同的类中定义成员并使用成员指针相互引用,会引起这种需要。对这种相互引用类的情况,一个类必定会在其定义之前被引用)。class="tags" href="/tags/BianYiQi.html" title=编译器>编译器为成员指针使用最通用的表示。这等价于/vmgclass="tags" href="/tags/BianYiQi.html" title=编译器>编译器选项。如果你指定full_generality,你必须还指定single_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance、multiple_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance或virtual_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance。这等价于使用带/vms、/vmm或/vmv选项的/vmgclass="tags" href="/tags/BianYiQi.html" title=编译器>编译器选项。
带full_generality、single_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance参量的class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示(/vms选项和/vmg选项一起)指定类成员指针的最通用的表示是使用无继承或单个继承的那种。这是类成员指针最小的可能表示。如果一个成员指针被说明的类定义的继承性模式是多继承或虚拟继承,则class="tags" href="/tags/BianYiQi.html" title=编译器>编译器产生一个错误。例如,将下面语句:
#pragma class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members(full_generality,single_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance)放在一个类定义之前,说明随后的所有类定义仅仅使用单继承。一旦指定了,用class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示指定的选项就不能改变了。
带full_generality、multiple_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance参量的class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示(/vmm选项和/vmg选项一起)指定类成员指针的最通用的表示是使用多继承的那种。这种表示比单继承所需要的大些。如果一个成员指针被说明的类定义的继承模式是虚拟继承,则class="tags" href="/tags/BianYiQi.html" title=编译器>编译器产生一个错误。
带full_generality、virtual_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance参量的class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members编译指示(/vmv选项和/vmg选项一起)指定类成员指针的最通用的表示是使用虚拟继承的那种。根据指针的大小和解释指针所需要的代码看,这是最昂贵的选项。但是此选项从不产生错误,并且当full_generality参量被指定给class="tags" href="/tags/POINTERS.html" title=pointers>pointers_to_members或/vmg命令行选项被使用时,它是一个缺省的。
语法
等价的语言结构使用此语法:
类说明:
类 继承类型opt 类名称;
继承类型:
__single_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance
__multiple_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance
__virtual_class="tags" href="/tags/INHERITANCE.html" title=inheritance>inheritance
正如这个例子中所示:
class __single_inhertance S;
int S::p;
不管class="tags" href="/tags/BianYiQi.html" title=编译器>编译器选项或编译指示,类S的成员指针将使用最小的可能表示。
你还可以显式给出具有前向说明的类的成员指针表示的一个前向说明。
注意:类成员指针表示的相同的前向说明应该出现在说明那个类的成员指针的每个转换单元中,而且说明应该出现在成员指针被说明之前。
一个数组是相似对象的一个集合。一个数组最简单的情况是一个向量。C++为固定大小数组的说明提供一种方便的语法:
语法
说明指示符 dname [常量表达式opt];
数组中元素个数由常量表达式给定,数组的第一个元素是第0号元素,最后一个元素是第(n-1)号元素,其中n是数组的大小。常量表达式必须是一个整型,且必须大于0。一个0尺寸数组仅当数组是一个struct或union的最后一个域,且Microsoft扩充(/Ze)时才是合法的。
数组是派生类型,因此可以由除函数、引用和void以外的任何其它派生的或基本的类型构成。
从其它数组构成的数组是多维数组。这些多维数组通过按顺序放置多个[常量表达式]指定,例如,考虑这个说明:
int i2[5][7];
它指定了一个int类型数组,概念上安排在一个5行7列的二维矩阵中,如图7.2所示。
在带有初始化器表(如初始化器中所描述的)的多维数组说明中,第一维指定边界的常量表达式可以省略。例如:
const int cMarkets=4;
//说明一个浮点类型表示运输费用
double TransportCosts[][cMarkets]=
{ {32.19,47.29,31.99,19.11},
{11.29,22.49,33.47,17.29},
{41.97,22.09,9.76,22.55} };
上面的说明定义了一个3行4列的数组;行表示工厂,列表示工厂输送到的市场,值是从工厂到市场的运输费用。数组的第一维省略了,但class="tags" href="/tags/BianYiQi.html" title=编译器>编译器通过检查初始化器将它填上。
多维数组的第一维的省略边界指定的技术还可用于函数说明中,如下所示:
#include
//包括DBL_MAX
#include
const int cMkts=4;
//说明一个浮点类型表示运输费用
double TransportCosts[][cMkts]=
{
{32.19,47.29,31.99,19.11},
{11.29,22.49,33.47,17.29},
{41.97,22.09,9.76,22.55}
};
//计算未指定的维数的大小
const int cFactories=sizeof TransportCosts /
sizeof (double[cMkts]);
double FindMinToMkt(int Mkt,double TransportCosts[][cMkts],int cFacts);
void main(int argc,char *argv[]) { double MinCost; MinCost=FindMinToMkt(*argv[1]-'0',TransportCosts,cFacts); cout << "The minimum cost to Market" << argv[1] << "is:" << MinCost << "/n"; } double FindMinToMkt(int Mkt,double TransportCosts[][cMkts],int cFacts) { double MinCost=DBL_MAX; for(int i=0;i<cFacts;++i) MinCost=(MinCost<TransportCosts[i][Mkt])?MinCost:TransportCosts[i][Mkt]; return MinCost; }
函数FindMinToMkt编写成添加新工厂,不需要改动任何代码,只要一次重新编译。
使用数组
数组中的单个元素使用数组下标运算符([])进行访问。如果是一个单维数组被用于不带下标的表达式中,数组名称求值为指向数组第一个元素的指针。例如:
char chArray[10];
...char *pch=chArray; //指向第一个元素的指针
char ch=chArray[0]; //第一个元素的值
ch=chArray[3]; //第四个元素的值
当使用多维数组时,表达式中对各种组合都可接受。以下例子说明了这点:double multi[4][4][3];//说明一个数组
double (*p2multi)[3];
double (*p1multi);
cout << multi[3][2][3] << "/n"; //使用三个下标
p2multi=multi[3]; //使p2multi指向multi的第四个“平面”
p1multi=multi[3][2]; //使p1multi指向multi的第四个“平面”,
//multi的第二行
在上面代码中,mutli是一个double类型的三维数组。p2multi指针指向一个double类型大小为3的一个数组。在此例中数组带1个、2个和3个下标使用。尽管通常指定所有的下标,如在cout语句中,但有时选取数组元素的一个特定的子集是很有用的,如后续的语句所示。
表达式中的数组
当一个数组类型的标识符出现在一个表达式中而不是在sizeof、取地址(&)或引用的初始化中,则它被转换为指向数组第一个元素的指针,例如:
char szError1[]="Error:Disk drive not ready.";char *psz=szError1;
psz指针指向数组szError1的第一个元素。注意,数组不象指针,数组是不可修改的l值。因此,以下赋值是非法的:
szError1=psz;
下标运算符的解释
象其它运算符一样,下标运算符([])可由用户重新定义。如果没有被重载,则下标运算符的缺省动作是使用以下方法将数组和下标组合在一起:
*((数组名)+(下标))
和包含指针类型的所有加法中一样,定位自动被执行以调整类型的尺寸。因此,结果值不是从数组名起始的下标字节数,而是数组的以下标为序数的那个元素(关于此转换的更多信息见第4章“表达式”中的“加法运算符”)。
类似地,对多维数组,地址是使用以下方法派生的:
*((数组名)+(下标1*max2*max3...maxn)
+下标2*max3...maxn)
...+下标n))
数组类型上的间接操作在
一个n维数组类型上使用间接运算符(*)产生一个n-1维数组。如果n是1,则产生一个标量(或数组元素)。
数组的排序
C++数组按行主顺序排序。行主顺序意味着最后的下标改变得最快。
本节包括以下主题:
* 函数说明语法
·可变的参量表
·说明不带参量的函数
·函数重载
·函数上的限制
·参量说明表
·函数原型(未定义说明)中的参量表
·函数定义中的参量表
* 缺省参量
·缺省参量表达式
·其它考虑函数定义包含在“函数定义”节中。
函数说明语法语
法说明
指示符 dname(参量说明表) cv修饰符表opt
参量说明表:
参量说明表,...
参量说明表:
参量说明
参量说明表,参量说明
参量说明表:
说明指示符 说明符
说明指示符 说明符=表达式
说明指示符 抽象说明符opt
说明指示符 抽象说明符opt=表达式
由dname给定的标识符具有类型“cv修饰表函数,带参量说明表且返回类型说明指示符”。
注意,const、volatile和许多Microsoft特殊关键字可以出现在cv修饰符表和名称的说明中。以下例子给出了两个简单的函数说明:
char *strchr(char *dest,char *src);
static int atoi(const char *ascnum) const;
以下语法解释了函数说明的细节:
语法
参量说明表:
参量说明表opt...opt
参量说明表,
...参量说明表:
参量说明
参量说明表,参量说明
参量说明:
说明指示符 说明符
说明指示符 说明符,表达式
说明指示符 抽象说明符opt
说明指示符 抽象说明符opt,表达式
可变的参量
表参量说明表的最后一个成员是省略号(...),其函数说明可以带可变数量的参量。在这些情况中,C++仅为显式说明的参量提供类型检查。当你需要一个通用函数其参量的个数和类型是可变的,你可以使用可变的参量表。printf函数簇便是一个使用可变的参量表的函数例子。
为了在那些说明之后访问参量,使用在标准包含文件STDARG.H中包含的宏,如本章后面“带可变参量的函数”中所描述的。
Microsoft特殊处
Microsoft C++允许省略号指定为一个参量,如果省略号是第一个参量且省略号前面为一个逗号。因此,说明int Func(int i,...);是合法的,但int Func(inti...);则不是。
Microsoft特殊处结束
带可变数目的参量的函数说明需要至少一个“位置占有者”参量,即使它未被使用。如果没有提供这个占位参量,则没有办法去访问剩余的参量。
当char类型的参量被作为可变参量传递时,它们被转换成int类型。类似地,当float类型的参量被作为可变参量传递时,它们被转换成double类型。其它类型的参量属于常用的整型和浮点提升。更多的信息参见第3章“标准转换”中的“整型提升”。
说明不带参量的函数
在参量说明表中用单个关键字void说明的函数不带参量,只要关键字void是第一个且是唯一的参量说明表中的一个成员。在参量说明表中任何其它位置的void类型参量将产生错误。例如:
long GetTickCount(void); //可行
long GetTickCount(int Reset, void); //错误
long GetTickCount(void,int Reset); //错误
在C++中,显式指定一个函数不需要参数的函数与说明一个不带参量说明表的函数是一样的,因此,以下两个语句是完全相同的:
long GetTickCount();long GetTickCount(void);注意:除了此处提出的,其它的指定一个void参量是非法的;但从void类型派生的类型(如指向void的指针和void数组)可出现在参量说明表中的任何位置。函数重载C++允许在相同的范围内指定不止一个的相同名称的函数。这些被称为“重载的函数”,在第12章“重载”中有详细的描述。重载函数使程序员能够为一个函数提供不同的语义,这取决于参量的类型和数目。例如,带一个字符串(或char*)参量的print函数与带一个double类型参量的函数执行极为不同的任务。重载允许统一的命名,防止程序员去发明诸如print_sz或print_d这样的名称。表7.1显示了C++使用函数说明的什么部分去区分在相同范围中具有相同名称的函数组。
表7.1 重载考虑
函数说明元素 | 用于重载? |
函数返回类型 | 否 |
参量的个数 | 是 |
参量的类型 | 是 |
省略号的出现或不出现 | 是 |
typedef名称的使用否未指定的数组边界否const或volatile(在cv修饰符表中) | 是 |
尽管函数可以在返回类型基础上加以区分,但它们却不能在此基础上重载。
以下例子说明了重载是如何被使用的。解决此同一问题的另一方法在本章后面部分“缺省参量”中给出。
#include
#include
#include
//三个print函数原型
int print(char *s); //打印一个字符串
int print(double dvalue); //打印一个double值
int print(double dvalue,int prec); //打印一个具有给定精度的double值
void main(int argc,char *argv[]) { const double d=893094.2987; if(argc<2) { //这些调用用于打印invoke print(char *s) print("this program requires one argument."); print("the argument specifies the number of"); print("digits precision for the second number"); print("printed."); } //调用print(double dvalue). print(d); //调用print(double dvalue,int prec). print(d,atoi(argv[1])); }
//打印一个字符串
int print(char *s) { cout << s << endl; return cout.good(); }
//以缺省精度打印一个double数
int print (double dvalue) { cout << dvalue << endl; retarn cout.good(); }
//以指定的精度打印一个double数
//正数精度指示小数点后显示几位数字精度
//负数精度指示小数点左边进行数的舍入的地方
int print(double dvalue,int prec) { //为舍入或截断使用表查找 static const double rgPow10[]={ 10E-7,10E-6,10E-5,10E-4,10E-3,10E-2,1E-1,10E0, 10E1, 10E2, 10E3, 10E4, 10E5, 10E6 }; const int iPowZero=6; //如果精度在范围外,则就打印此数 if (prec < -6 || prec>7) return print(dvalue); //定位,截断,然后再定位 dvalue=floor(dvalue/rgPow10[iPowZero-prec])] *rgPow10[ipowZero-prec]; cout << dvalue << endl; return cout.good(); }
上面的代码给出了在文件范围中对print函数的重载。关于重载的限制和重载如何影响其它C++元素的信息,参见第12章“重载”。
函数的限制函数
不能返回数组或函数,但是它们可以返回引用或指向数组或函数的指针。返回数组的另一个方法是仅用那个数组作为一个成员去说明一个结构:
struct Address
{
char szAddress[31]; };
Address GetAddress();
在函数说明的返回类型部分或在函数的任何参量的说明中去定义一个类型都是非法的。
以下合法的C代码在C++中是非法的:
enum Weather{ Cloudy,Rainy,Sunny } GetWeather(Date Today)
上面的代码是不允许的,因为Weather类型具有相对GetWeather局部的函数范围,则不适合使用返回值。因为函数的参量具有函数范围,所以在参量表内作的说明如果不被允许则将有同样的问题。
C++不支持函数数组。但指向函数的指针数组可能非常有用。在类Pascal语言的语法分析中,代码通常分开进入一个语言符号分析用的词法分析器和一个将语义附于语言符号用的语法分析器中。如果分析器为每个符号返回一个特定的普通值,则代码可被写成如下例子所示以执行适当的处理:
int ProcessFORTOken(char *szText);
int ProsessWHILEToken(char *szText);
int ProcessBEGINTOken(char *szText);
int ProcessENDToken(char *szText);
int ProcessIFToken(char *szText);
Int ProcessTHENToken(char *stText);
int ProcessELSEToken(char *szText);
int (*ProcessToken[])(char *)={
ProcessFORToken,ProcessWHILEToken,ProcessBEGINToken, ProcessENDToken,ProcessIFToken,ProcessTHENToken,
ProcessELSEToken};
const int MaxTOkenTD=sizeof ProcessTOken / sizeof(int(*)());
...
int DoProcessToken(int TokenID, char *szText) { if (TOkenTD < MaxTokenID) return (*ProcessToken[TokenID])(szText); else return Error(szText); }
参量说明表
一个函数说明的参量说明表部分:
* 允许class="tags" href="/tags/BianYiQi.html" title=编译器>编译器的检查函数所需参量与调用中提供的参量之间的类型一致性。
* 允许从提供的参量类型转换到所需的参量类型,转换过程或者为隐式的或者为用户定义的。l 检查函数指针的初始化或赋值。
* 检查函数引用的初始化或赋值。
函数原型(非定义说明)中的参量表
参量说明表形式是参量的类型名称的一个表。考虑函数func的参量表,带三个参量:指向char的指针、char和int类型。
这样一个参量说明表的代码可写为:
char*,char,int
因此函数说明(原型)可写成:
void func (char*,char,int);
尽管上面的说明包含了足够的信息供class="tags" href="/tags/BianYiQi.html" title=编译器>编译器执行类型检查和转换,但没有提供参量是什么的更多的信息。一个书写函数说明的更好的方法是包含标识符,因为它们将在函数定义中出现,如以下所示:
void func(char *szTarget, char chSearchChar,int nStartAt);
原型中的这些标识符仅对缺省参量有用,因为它们直接超出了范围。但是它们提供了有意义的程序文档。
函数定义中的参量表
在一个函数定义中的参量表与原型中的不同之处仅在于:如果存在标识符,则它表示函数的形参。标识符名不需要与原型中的那些匹配(如果存在)。
注意:可以定义带未命名的参量的函数。但是这些参量对它们定义的函数是不可访问的。
缺省参量
在很多情况中,函数拥有的参量使用的频度太小,只需要一个缺省值就够了。要用这个,缺省参量仅允许用于指定在一个给定的调用中是有意义的函数的那些参量,为了说明这个概念,考虑本章前面“函数重载”中给出的例子:
//三个print函数原型
int print(char *s); //打印一个字符串
int print(double dvalue); //打印一个double值
int print(double dvalne, int prec); //打印一个给定精度的double值在很多应用中,可以用prec提供合理的缺省值,而不必需要两个函数。/
/两个print函数原型int print(char *s); //打印一个字符串
int print(double dvalue,int prec=2); //打印一个给定精度的double值print函数的实现稍稍改动以反映出这个事实:即对于double类型只存在一个这样的函数:
//打印一给定精度的double值
//正精度值指示小数点后显示多少位数字
//负精度值指示小数点左边舍入的位置
int print (double dvalue,int prec) { //为舍入或截断使用查找表 static const double rgPow10[]={ 10E-7,10E-6,10E-5,10E-4,10E-3,10E-2,10E-1,10E0, 10E1, 10E2, 10E3, 10E4, 10E5, 10E6 }; const int iPowZero=6; //如果精度超出了范围,则就打印这个数 if (prec>=-6 || prec<=7) //定位,截断,然后再定位 dvalue=floor(dvalue / rgPow10[iPowZero-prec])* rgPow10[iPowZero-prec] ; cout << dvalue << endl; return cout.good(); }
为了调用新的print函数,使用如下代码:
print(d); //精度为2,由缺省参量提供
print(d,0); //绕过缺省参量以获取其它的值。
在使用缺省参量时要注意以下几点:
* 缺省参量仅用在尾部参量被省略的函数调用中,即它们必须是最后的参量。因此,以下代码是非法的:
int print(double dvalue=0.0, int prec);
* 一个缺省的参量在后面的说明中不能定义,即使再定义与原来的完全相同。因此,以下代码产生一个错误:
//print函数原型
int print(double dvalue,int prec=2);...
//print 函数定义
int print (double dvalue ,int prec=2)
{
...
}
此代码的问题是在定义中的函数说明为prec重定义了缺省值。
* 额外的缺省参量可由后面的说明添加。
* 可以为函数指针提供缺省的参量。例如:
int (*pShowIntVal)(int i=0);
缺省的参量表达式
缺省参量使用的表达式通常是常量表达式,但这不是要求的。表达式可以组合当前范围中可见的函数、常量表达式和全局变量。表达式不能包含局部变量或非静态类成员变量。
以下代码说明这点:
BOOL CreateVScrollBar(HWND hWnd, short nWidth=
GetSystemMetrics(SM_CXVSCROLL));
上面的说明指定了一个函数,为一个窗口创建一个给定宽度的垂直滚动条。如果没有提供宽度参量,则Windows API函数GetSystemMetrics被调用为一个滚动条查找缺省宽度。
在函数调用之后缺省表达式被求值,但求值在函数调用实际发生之前完成。
因为函数的形参是在函数范围内,并且因为缺省参量的求值在进入此范围之前发生,所以你不能在缺省参量表达式中使用形参或局部变量。
注意在一个缺省参量表达式之前说明的任何形参可在函数范围内隐藏一个全局名称,这可能导致错误。以下代码是非法的:
const int Categories = 9;
void Enum Categories(char *Categories[],int n=Categories);
在前面代码中,全局名称Categories在函数范围内被隐藏,使得缺省参量表达式无效。
缺省参量不被认为是函数类型的一部分,因此,不被用于选择重载的函数。两个函数若仅在其缺省参量上不同,则被认为是多个定义而不是重载的函数。
不能为重载的运算符提供缺省参量。
函数定义与函数说明的不同点在于它们提拱函数体,即组成函数的代码。
语法
函数定义:
说明指示符opt 说明符 ctor初始化器opt 函数体
函数体:
复合语句正如“函数”中讨论的语法中说明符的格式为:
dname(参量说明表) cv修饰符表opt
在参量说明表中说明的形参是在函数体的范围内。
图7.3显示了一个函数定义的各个部分。阴影区域是函数体。
说明符语法的cv修饰符表元素指示了如何对待this指针的;它仅与类成员函数一起使用。
语法中的ctor初始化器元素仅被用于构造函数中,它的目的是允许对基类和包含的对象进行初始化(有关ctor初始化器的更多信息参见第11章“特殊成员函数”中的“初始化基类和成员”)。
带可变的参量表的函数
要求可变表的函数在参量表中使用省略号(...)进行说明,如本章前面“可变的参量表”中所描述。要访问使用此方法传递给函数的参量,则使用STDARG.H标准包括文件中所描述的类型和宏。
以下例子给出了va_start、va_arg和va_end宏和va_list类型(在STDARG.H中说明的)是如何一起工作的。
#include
#include
//showVar的说明,但不是定义
int ShowVar(char *szTypes,...);
void main()
{
ShowVar("fcsi",32.4f,'a',"Test string",4);
}
//showvar带一个“fcsi”形式的格式化字符串,其中每个字符指示那个位置的
//参量类型
//
//i=int
//f=float
//c=char
//s=string(char*)
//
//以下格式说明是一个n个参量的表,其中n==strlen(szTypes)
void ShowVar(char *szTypes,...) { va_list vl; int i; //szTypes是指定的最后的参量;所有其它的必须使用可变的参量宏进行访问 va_start(vl,szTypes); //Step through the list for(i=0;szTypes[i]!='/0';++i) { union printable_t { int i; float f; char c; char *s; } Printable; switch (szTypes[i]) //预期的类型 { case 'i': Printable.i=va_arg(vl,int); printf("%i/n",Printable.i); break; case 'f': Printable.f=va_arg(vl,float); printf("%f/n",Printable.f); break; case 'c': Printable.c=va_arg(vl,char); printf("%f/n",Printable.f); break; case 's': Printable.s=va_arg(vl,char *); printf("%s/n",Printable.s); break; default: break; } } va_end(vl); }
前面的例子说明了以下这些重要概念:l 一个表标记将必须在任何可变的参量被访问之前建立为一个va_list类型的变量,在前面的例子中,标识符被叫做vl。
* 单个参量使用va_arg宏进行访问,va_arg宏需要被告知要检索的参量的类型,这样它才能从堆栈中传递正确数目的字节。如果一个与调用程序提供的大小不同的不正确的类型被指定给va_arg,则结果是难以预料的。
* 使用va_arg宏获得的结果应该被显式地造型转换到所希望的类型
* va_end宏必须被调用以终止可变的参量处理。
说明符可指定对象的初始值。为const类型的对象指定一个值的唯一方法是在说明符中指定。说明符中指定这个初始值的部分被称为“初始化器”。
语法
初始化器
=赋值表达式
={初始化器表,opt}
(表达式表)
初始化器表:
表达式
初始化器表,表达式
{初始化器表,opt}
初始化器有两个基本类型:
* 使用等号语法调用的初始化器。
* 使用函数形式调用的初始化器。仅仅只有构造函数的类的对象可以用函数形式语法进行初始化。两个语法格式的不同还在于访问控制和临时对象的潜在使用。考虑以下代码,用初始化器说明一些说明符:
int i=7;//使用等号语法
Customer Cust ("Taxpayer, Joe", //使用函数形式语法
"14 Cherry Lane", //构造函数的要求
"Manteca",
"CA");
自动的、寄存器的、静态的和外部变量的说明可以包含初始化器。但是外部变量的说明仅当变量没有被说明为extern时可以包含初始化器。
这些初始化器可以包含包括在当前范围内的常量和变量。初始化器在程序流遇到说明的地方被求值,或对全局静态对象和变量在程序开始处求值(有关全局静态对象的初始化的更多信息,参见第2章“基本概念”中的“额外的开始考虑”)。
一个指向const对象的指针可以用一个指向非const对象的指针进行初始化,但反之不可。
例如,以下初始化是合法的:
Window StandardWindow;
const Window* pStandard Window(&Standard Window);
在上面代码中,指针pStandardWindow被说明为指向一个const对象的指针。尽管StandardWindow没有被说明为const,但该说明是可接受的,因为它不允许被说明为const的对象访问一个const对象。与此相反的如下:
const Window StandardWindow;
Window* pStandardWindow(StandardWindow);
上面的代码显式说明StandardWindow为一个const对象。用StandardWindow的地址初始化非常量指针pStandardWindow会产生一个错误,因为它允许通过指针访问const对象,即它允许从对象中移走const属性。
未初始化的对象
没有用初始化器说明的static存储类的对象和简单变量被确保初始化为一个位模式0。对自动的或寄存器存储类的未初始化的对象,则不产生此种特殊处理。它们有未定义的值。
静态成员初始化出现在类范围中。因此,它们可以访问其它成员数据或函数。例如:
class DialogWindow { public: static short GetTextHeight(); private: static short nTextHeight; };
short DialogWindow::nTextHeight=GetTextHeight();
注意在前面的静态成员nTextHeight的定义中,GetTextHeight隐含指DialogWindow::GetTextHeight。
初始化集合
一个集合类型是一个这样的数组、类或结构类型:
* 没有构造函数
* 没有非公有成员
* 没有基类
* 没有虚拟函数
集合的初始化器可以指定为括在花括号中的以逗号分隔的值表。例如,此代码说明了一个10个int的数组,并初始化为:
int rgiArray[10]={9,8,4,6,5,6,3,5,6,11};
初始化器以递增的下标顺序存储在数组元素中。因此,rgiArray[0]是9、rgiArray[1]是8,等等,直到rgiArray[9]是11。要初始化一个结构,使用这样的代码:
struct RCPrompt { short nRow; short nCol; char *szPrompt; };
RCPrompt rcContinueYN={24,0,"Continue (Y/N?)"};
集合初始化器表的长度
如果一个集合初始化器表比正被初始化的数组或类类型短,则未指定初始化器的0被存储在元素中。因此,以下两个说明是等价的://显式初始化所有元素
int rgiArray[5]={3,2,0,0,0};
//允许保留元素为0初始化的
int rgiArray[5]={3,2}
正如这所看到的,初始化器可被截断,但提供太多的初始化器将产生一个错误。
初始化包含集合
的集合某些集合包含其它的集合,例如,数组的数组、结构的数组或由其它结构组成的结构,对这种结构可以通过用带花括号的表按其出现的顺序对每个初始化而提供初始化器,例如:
//说明一个RCPrompt类型的数组
RCPrompt rgRCPrompt[4]=
{{4,7,"Options Are:"},
{6, 7,"1. Main Menu"},
{8, 7,"2. Print Menu"},
{10,7,"3. File Menu"}};
注意,rgRCPrompt被用花括号包围的表的花括号包围表初始化。封闭的花括号不是语法上要求的,但它们使说明更清晰,以下程序例子显示了一个二维数组是如何用这样一个初始化器填充的:
#include
void main( ) { int rgI[2][4]={1,2,3,4,5,6,7,8}; for(int i=0;i<2; ++i) for(int j=0;j<4; ++j) cout << "rgI[" << i << "][" << j << "]=" << rgI[i][j] << endl; }
程序的输出为:
rgI[0][0]=1
rgI[0][1]=2
rgI[0][2]=3
rgI[0][3]=4
rgI[1][0]=5
rgI[1][1]=6
rgI[1][2]=7
rgI[1][3]=8
短初始化表仅与显式子集合初始化器一起且被花括号括住时才是可用的。如果rgI说明为:
int rgI[2][4]={{1,2},{3,4}};
则程序的输出将是:
rgI[0][0]=1
rgI[0][1]=2
rgI[0][2]=0
rgI[0][3]=0
rgI[1][0]=3
rgI[1][1]=4
rgI[1][2]=0
rgI[1][3]=0
不完整类型的初始化
不完整类型,如没有限定边界的数组类型,可以初始化如下:
char HomeRow[]={′a′, ′s′, ′d′, ′f′, ′h′, ′i′, ′k′, ′l′}
class="tags" href="/tags/BianYiQi.html" title=编译器>编译器从提供的初始化器数目计算数组的大小。
不完整类型,如已说明但未定义的指向类类型的指针,其说明如下:
class DefinedElseWhere; //类定义在别处
class DefinedHere { ... friend class definedElsewhere; }
使用构造函数初始化
类类型对象通过调用类的合适的构造函数进行初始化。关于类类型初始化的完整信息参见第11章“特殊成员函数”中的“显式初始化”。
初始化器和联合
union类型的对象用一个单值进行初始化(如果联合没有一个构造函数)。这是以这两种方法之一完成的:
* 用同一个union类型的另一个对象初始化联合,例如:
struct Point { unsigned x; unsigned y; } union PtLong { long l; Point pt; };
...
PtLong ptOrigin;
PtLong ptCurrentT=ptOrigin;
在上面代码中,ptCurrent用ptOrigin值相同类型的一个对象进行初始化。
* 将第一个成员用花括号括起的初始化器联合起来初始化。例如:PtLong ptCurrent={0x0a000aL};
字符数组可用以下两种方法之一进行初始化:
* 单个地,如:
char chABCD[4]={′a′, ′b′, ′c′, ′d′};
* 用一个字符串,如;
char chABCD[5]="abcd";
在第二种情况下,即字符数组用一个字符串进行初始化,class="tags" href="/tags/BianYiQi.html" title=编译器>编译器添加一个尾部的'/0'(字符串结束字符)。因此,数组必定至少比串中字符的个数大1。
因为大多数字符串处理使用标准库函数或依赖于尾部字符串结束字符的出现,所以常见用字符串初始化未限定边界的数组说明如下:
char chABCD[]="ABCD";
引用的类型变量必须用派生引用类型对象或用可以转换到派生引用类型派对对象进行初始化。例如:
int iVar;long lVar;
long& LongRef1=lVar; //不需要转换
long& LongRef2=iVar; //错误
const LongRef3=iVar; //可行
longRef1=23L; //通过一个引用改变lVar
longRef2=11L; //通过一个引用改变iVar
longRef3=11L; //错误
使用一个临时对象去初始化一个引用的唯一方法是去初始化一个常量临时对象。一旦初始化了,一个引用类型变量总是指向相同的对象;它不能被改为指向另一个对象。
尽管语法可以相同,但引用类型变量的初始化和引用类型变量的赋值在语义上是不同的。在前面的例子中,改变iVar和lVar的赋值看上去与初始化相似,但有不同的效果。初始化指定引用类型变量指向的对象,赋值通过引用赋给被指向的对象。
因为传递给一个函数一个引用类型的参量和从一个函数返回一个引用类型值都是初始化,所以函数的形参被正确地初始化,正如返回的引用。
引用类型变量仅在以下情况可以不用初始化器进行初始化:
* 函数说明(原型)。例如:
int func(int&);
* 函数返回类型说明,例如:
int & func(int&);
* 引用类型类成员的说明,例如:
class C { public: int& i; };
* 被显式指定为extern的变量的说明,例如:
extern int& iVal;
当初始化一个引用类型变量时,class="tags" href="/tags/BianYiQi.html" title=编译器>编译器使用图7.4中所示的决策图,以在创建一个对象的引用还是创建一个引用指向的临时对象之间进行选择。volatile类型的引用(说明为:volatile类型名称& 标识符)可以用相同类型的volatile对象或用没有被说明为volatile的对象进行初始化。但是它们不能用那个类型的const对象进行初始化。类似地,const类型的引用(说明为:const类型名称& 标识符)可以用相同类型的const对象(或任何可转换到那个类型的或没有被说明为const的对象)进行初始化。但是它们不能用那个类型的volatile对象进行初始化。没有用const或volatile关键字限定的引用,仅能用既不是说明为const也不是说明为volatile的对象进行初始化。