第8章 基 本 概 念

第8章 基 本 概 念

本章意在解释对理解C++至关重要的一些概念,C程序员将对这些概念中的很多内容非常熟悉,但存在一些细微的差别可能导致意想不到的程序结果,包含了以下论题:

* 术语

* 说明和定义

* 范围

* 程序和连接

* 开始和结束

* 存储类

* 类型

附加的论题包括l值、r值和数值范围界限。


术 语

本书所使用的C++术语在表2.1中给出了定义:

表2.1 C++术语

术语含义
说明说明向一个程序引入名称及它们的类型,而不必定义相关联的对象或函数。但很多说明被作为定义
定义定义提供信息而允许编译器为对象分配存储器或为函数产生代码
生存期一个对象的生存期是对象存在包括创建和析构的时间阶段
连接名称可以有外部连接、内部连接或无连接,在一个程序内(一套转换单元),只有带外部连接的名称才表示相同的对象或函数。在一个转换单元内,带有内部或外部连接的名称均可表示相同的对象或函数(当函数重载时除外)。(关于转换单元的更多信息,参阅本书后面“预处理器参考”中“翻译阶段”)。无连接的名称只表示唯一的对象或函数
名称名称表示一个对象、函数、重载的函数集、枚举元、类型、类成员、模板、值或标号、C++程序用名称指出与它们相关联的语言元素。名称可以是类型名称或标识符
对象一个对象是用户定义类型(一个类类型)的一个实例(一个数据项)。一个对象和一个变量之间的区别是变量保留状态信息,而对象还可以拥有动作。本手册给出的对象和变量之间的区别是:“对象”指用户定义类型的实例,而“变量”指基本类型的实例。在对象或变量均可用情况下,术语“对象”被用作为包容性术语,意为“对象或变量”
范围名称只能在程序上下文的特定区域内使用,这些区域被称为名称的范围
存储类被命名的对象的存储类指其生存期、初始化及在某特定情况下指其连接
类型带有相关类型的名称指的是存储在一个对象中或由一个函数返回的值的含义
变量一个变量是一个基本类型(如int、float或double)的数据项,变量存储状态信息但不为信息如何被处理而定义动作。看前面列出的“对象”项来获得有关术语“变量”和“对象”是如何在本文档中被运用的

说明和定义

说明告诉编译器一个程序元素或名称存在,定义指定名称描述的是什么代码或数据,一个名称必须先说明而后才能使用。

说 明

一个说明向一个程序中引入一个或多个名称,说明在一个程序可出现不止一次,因此,可以为每个编译单元说明类、结构、枚举类型和其它用户定义类型。多说明的限制是所有说明必须可标识。说明还可作为定义,除了以下说明:

> 是一个函数原型(没有函数体的函数说明)

> 有extern指示符但没有初始表达式(对象和变量)或函数体(函数),这意味着该定义在当前转换单元里不是必须的,且给出了名称外部连接。

> 是类说明内部的静态数据成员。

由于静态类数据成员是由类的所有成员共享的离散变量,它们必须在类说明外部加以定义和初始化(关于类和类成员的更多的信息参见第8章“类”)。

> 是一个不带定义的类名说明,例如class T;

> 是typedef语句

说明同时也是定义的例子有:

//说明和定义整型变量i和j

int i;int j=10;

//说明枚举suits

enum suits {Spades=1,Clubs,Hearts,Diamonds};

//说明类CheckBox

class CheckBox:public Control
{
public:
Boolean IsChecked();
virtual int ChangeState()=0;
}

有些说明不是定义:

extern int I;

char *strchr(const char *Str,const char Target);

定 义

一个定义是一个对象或变量、函数、类或枚举的唯一规格,由于定义必须唯一,所以一个程序对于一给定的程序元素只能包含一个定义。

说明和定义之间可以是多对一的关系。有两种情况下一个程序元素可被说明但不被定义:

* 函数被说明但从来没有用函数调用或带有函数地址的表达式引用过。

* 一个类仅用于其定义不要求被知道的情况,但是,类必须被说明,以下代码描述了这种情况:

class WindowCounter;//前向引用,没有定义

class Window
{
static WindowCounter WindowCounter;//WindowCounter定义不被要求
}

范 围

C++的名称仅能用于一个程序的特定区域,这个区域被称为名称的“范围”。范围确定一个不表示静态范围的对象的名称的生存期,范围还指出了当类构造和析构函数被调用时,以及对范围而言是局部的变量被初始化时一个名称的可见性(详见第11章“特殊成员函数”中“构造”和“析构”部分)。

有五种范围:

> 局部范围,在一个块内说明的名称仅在此块中以及包含在此块中的块内是可访问的,且仅从说明后开始。一个函数在函数最外层块范围内的普通参量的名称具有局部范围,就好象它们在包含函数体的块内被说明一样,考虑以下代码段:

{
int i;
}

由于i的说明包含在花括号括起的块内,所以i有局部范围而且永远不能被访问,因为在闭花括号前没有代码对其进行访问操作。

> 函数范围,标号是唯一具有函数范围的名称,它们可在一个函数内的任意位置被使用,但在函数外部则不能访问。

> 文件范围,任何在所有块或类的外部说明的名称具有文件范围,在其说明后,在转换单元的任何位置均可访问。带有文件范围的不说明静态对象的名称通常被称为“全局”文字。

> 类范围,类成员名具有类范围。类成员函数仅能用对象上的成员选择运算符(.或->)或成员指针运算符(.*或->*)或指向那个类的对象的指针进行访问,非静态类成员数据被认为相对于那个类的对象是局部的。

考虑以下类说明:

class Point
{
int x ;
int y ;
}

类成员x和y被认为是在类Point范围内。

> 原型范围,在函数原型中说明的名称仅到了原型结尾处才是可见的。以下原型说明了两个名称(szDest,szSource);这些名称在原型结尾处便超出范围了:char *strcpy

(char *szDest,const char *szSource);

说明点

一个名称被认为就在其说明符后在其(可选的)初始化之前马上说明。(有关说明符的更多信息参见第7章“说明符”)。一个枚举器被认为应在命名它的标识符后,但在其(可选的)初始化之前立即说明。

考虑这个例子:

double dVar=7.0;

void main()
{
double dVar=dVar;
}

如果说明点在初始化之后,则局部dVar将被初始化为7.0,即全局变量dVar的值。但是,实际并非如此,所以dVar被初始化为一个未定义的值。

枚举遵循同样的规则,但是,枚举器被输出到枚举的包含范围外。在下例中枚举元素Spades、Clubs、Hearts和Diamonds被说明,因为枚举器被输出到封闭的范围外,它们被认为具有全局范围。例中的标识符已经在全局范围被定义。考虑以下代码:

const int Spades=1,Clubs=2,Hearts=3,Diamonds=4;
enum Suits
{
Spades=Spades, //错误
Clubs, //错误
Hearts, //错误
Diamonds //错误
}

由于在前面代码中标识符已经在全局范围内被定义,所以产生出错信息。注意:用相同的名称指定不止一个的程序元素,例如一个枚举器和一个对象,这都被认为是很糟的编程方式,应当避免使用。在前例中,这种方式导致出错。

隐藏名称

你可以通过在一个包围的块内说明名称来隐藏该名称,在图2.1中,i在更内层的块内又被说明了,而隐藏了在较外层块范围内与i相关的变量。

Sample::Func(char *szWhat)
{
int l=0;
cout<<"i="<<i<<"/n";
{
int i=7,j=9;
cout<<"i="<<i<<"/n";
外层包含局部范围对象i和<<"j="<<j<<"/n"; 式参量szWhat
}
cout<<"i="<<i<<"/n";
}

内层块包含局部范围对象i和

图2.1中给出的程序输出为:

i=0

i=7

j=9

i=0

注意:参量szWhat被认为在函数范围内,因此,它被看成好象是在函数的最外层块内被说明的。

具有文件范围的隐藏名称

你可以通过在块范围内明确地说明相同的名称而隐藏带文件范围的名称。但是文件范围名称可以使用范围分辨运算符(::)进行访问。例如:

#include 
int i=7; //i具有文件范围即在所有的块外部说明的
void main(int argc, char *argv[])
{
int i=5; //i具有块范围,隐藏了具有文件范围的I
cout << " Block_scoped i has the value : " << i << "/n" ;
cout << " File_scoped i has the vaIue : " << ::i << "/n" ;
}

前面代码的结果是:

Block_scoped i has the value : 5

File_scoped i has the value : 7

隐藏类名

你可以通过在相同的范围内说明一个函数、对象或变量、或者枚举器来隐藏类名。

当然,类名还可以通过使用关键字class前缀进行访问。

//在文件范围说明类Account

class Account
{
public:
Account(double InitialBaIance)
{ balance = InitialBalance;}
double GetBalance()
{ return balance;}
private:
double balance;
};
double Account=15.37 //隐藏类名Accountvoid main()
{
class Account checking(Account); //限定Acconut为类名
cout << "Opening account with balance of:"
<< checking.GetBalance() << "/n";
}

注意,类名(Account)在任何位置被调用时,都要用关键字class将其与文件域变量Account区别开来,当类名出现在范围分辩运算符(::)的左侧时不用此规则。范围分辩运算符左侧的名称总被认为是类名,以下例子说明如果用关键字class来说明一个类型Account的对象的指针:

class Account * Checking = new class Account(Account);

上述语句中的Account在初始化器中(在圆括号内)具有文件范围,类型为double。

注意:在这个例子中,标识符名称重新使用认为是糟糕的程序设计风格。有关指针的更多的信息参见本章后面的“派生类”。有关类对象的说明和初始化详见第8章“类”。有关使用new和delete自由存储运算符的更多信息,参见第11章“特殊的成员函数”。

函数的形式参量的范围

函数的形式参量(在函数定义内指定的参量)被认为是在函数体的最外层块的范围内。


程序和连接

一个程序由连接在一起的一个或多个转换单元组成,从包含main函数的转换单元开始执行(概念上)(有关转换单元的更多信息,参见本书后面“预处理器参考”的“翻译阶段”。

有关main函数,详见本章后面的“程序开始:main函数”)。

连接的类型

在转换单元之间共享对象或函数的名称的方式被称为“连接”。这些名称有:

> 内部连接,即它们仅指自己的转换单元中的程序元素,不与其它转换单元共享。

在其它转换单元中的相同的名称可以指不同的对象或不同的类。具有内部连接的名称有时被认为相对于它们的转换单元是局部的。

具有内部连接的名称的一个说明例子如下:

static int i //static关键字确保是内部连接

> 外部连接,即指程序中的任意一个转换单元中的程序元素,在转换单元的之间共享。在另一个转换单元中的相同名称被认定为指示同一个对象或类,具有外部连接的名称有时被认为是全局的 。具有外部连接的名称的一个说明例子如下:

extern int i;

> 无连接,即指唯一实体,在另一个范围中的同样的名称可能不是相同的对象,例如一个枚举(但注意,你可以用无连接来向一个对象传递一个指针,这使得在其它转换单元中该对象是可访问的。)具有文件范围的名称的连接以下的连接规则适用于具有文件范围的名称(不是typedef及枚举器名称):l 如果一个名称被显式说明为static,则它具有内部连接,标识在转换

单元内的一个程序元素。

> 枚举器名称及typedef名称是无连接

> 具有文件范围的所有其它的名称都是外部连接

Microsoft特殊处

> 如果一个具有文件范围的函数名称被显式说明为inline,如果它被实例化或其地址被引用时,它具有外部连接。因此,对于一个具有文件范围的函数,可能有内部或外部的连接。Microsoft特殊处结束一个类有内部连接,如果:

>不使用C++功能(例如成员访问控制、成员函数、构造函数、析构函数等)

> 不用于说明具有外部连接的另一个名称。此限制意味着,类类型对象传递给具有外部连接的函数导致类具有外部连接。

具有类范围的名称的连接

以下连接规则适用于具有类范围的名称

> 静态类成员具有外部连接

> 类成员函数具有外部连接

> 枚举器及typedef名称是无连接

Microsoft特殊处

> 被说明为friend函数的函数必须具有外部连接。把一个静态函数说明为friend会出错。

Microsoft特殊处结束

具有块范围的名称的连接

以下连接规则适用于具有块范围的名称(局部名称):

> 说明为extern的名称具有外部连接,除非它们在前面说明为static

> 具有块范围的所有其它名称是无连接

具有无连接的名称

具有无连接的名称仅仅有:

> 函数参量

> 没有被说明为extern或static的块范围名称

> 枚举器

> 在typedef语句中说明的名称,而当typedef语句用于为一个未命名的类类型提供一个名称时是一个例外。如果类具有外部连接,则该名称可能具有外部连接。以下例子给出了具有外部连接的typedef名称的情形:

typedef struct
{
short x;
short y;
} POINT;

extern int MoveTo(POINT pt);

typedef名称POINT成了未命名的结构的类名,然后它被用于说明一个具有外部连接的函数。

因为typedef名称为无连接,所以在转换单元之间它们的定义可以不同,因为编译是独立地进行的,所以编译器无法检查出这些不同处,结果是,这种类型的错误要到链接时才能被查出。

考虑以下情形:

//转换单元 1

typedef int INT

INT myInt;

...

//转换单元2

typedef short INT

extern INT myInt

...

前面的代码在链接时产生一个“未分解的外部”错误。

C++函数仅能在文件或类范围内定义,以下例子描述了如何定义函数并给出了一个错误的函数定义:

#include 
void ShowChar(char ch); //说明函数ShowChar
void ShowChar(chat ch) //定义函数ShowChar
{ //函数具有文件范围
cout << ch;
}
struct Char //定义类Char
{
char Show(); //说明Show函数
char Get(); //说明Get函数
char ch;
};
char Char::Show() // 定义具有类范围的Show函数
{
cout << ch;
return ch
}
void GoodFuncDef(char ch) // 定义具有文件范围的GoodFuncDef
{
int BadFuncDef(int i) // 错误地嵌套函数
{
return i * 7;
}
for (int i = 0;i < BadFuncDef(2); ++i)
cout << ch;
cout << "/ n" ;
}

连接到非C++函数:

只有在前面说明为具有C连接的C函数和数据才能被访问。可是,它们必须定义在分开的编译过的转换单元中。

语法

连接规格:

extern 字符串文字{说明表opt}

extern 字符串文字 说明

说明表:

说明

说明表 说明

Microsoft C++支持字符串文字域中的字符串“C”和“C++”。以下例子给出了可选择的具有C连接的名称的说明方法:

//说明printf具有C连接

extern "C" int printf (const char *fmt,...);

//使头文件"cinclude.h中的所有内容具有C连接

extern "C"
{
#include
}

//说明具有C连接的两个函数ShowChar和GetChar

extern "C"
{
char ShowChar(char ch);
char GetChar(void);
}

//定义具有C连接的两个函数ShowChar和GetChar

extern "C" char ShowChar(char ch)
{
putchar(ch);
return ch;
}
extern "C" char GetChar(void)
{
char ch;
ch = getchar();
return ch
}

//说明一个具有C连接的全局变量errno

extern "C" int errno;


启动和结束

通过使用两个函数:main和exit,使程序方便地启动和结束。其它的启动和结束代码可能被执行。

程序启动:main函数

一个被称为main的特殊函数是所有C++程序的入口点,此函数并未被编译器预定义,而是必须在程序文本中提供。如果你正在编写依附于单代码程序设计模式的代码,你可以使用main的宽字符版本wmain。main的说明语法是:

int main();

或,可选地:

int main(int argc[,char *argv[] [,char* envp[]]])

;wmain的说明语法如下:

int wmain();或,可选地:int wmain(int argc[,wchar_t *argv[][,wchar_t *envp[]]]);

另外,main和wmain函数还可被说明为返回值是void(无返回值)。如要你将main或wmain说明为返回值void,则用return语句无法向父进程或操作系统返回退出代码。当main或wmain被说明为void时,若要返回退出代码,则必须使用exit函数。

使用wmain代替main

在单代码程序设计模式中,你可以定义main函数的宽字符版本。如果你想编写可移植的依附于单代码规格的代码,则使用wmain代替main。

你用与main相似的格式说明wmain的形式参量,而后你可以向程序传递宽字符参量及可选地宽字符环境指针。wmain的argv和envp参量是wchar_t *类型。如果你的程序使用main函数,则在程序开始处由操作系统创建了一个多字节字符环境,环境的一个宽字符拷贝仅在需要时创建。(例如,通过对_wgetenv或_wputenv函数的调用)。在第一次调用_wputenv或第一次调用_wgetenv时,如果已经存在MBCS环境,则一个相应的宽字符字符串环境被创建,且由一个_wenviron全局变量指向,它是_environ全局变量的宽字符版本。在这点上,环境的两份拷贝(MBCS和单代码)同时存在,且在程序的整个生存期中由操作系统获得。

同样地,如果你的程序使用wmain函数,则在第一次调用_putenv或getenv时创建一个MBCS(ASCII)环境,且由全局变量_environ指向。

有关MBCS环境的更多信息,参见“Microsoft Visual C++ 6.0参考库”的“Microsoft Visual C++ 6.0运行库参考”中的第2章“单字节和多字节字符集”。

参量变义

在原型:

int main(int argc[,char *argv[][,char *envp[]]]);

int wmain(int argc[,wchar_t *argv[][,wchart_t *envp[]]]);

中的参量允许进行方便的参量命令行语法分析,且可选择地访问环境变量。参量定义如下:

argc

包含跟随在argv后面的参量个数的整数。argc参量总是大于或等于1。

argv

是一个以空格结尾字符串的数组,表示由程序的用户在命令行输入的参量。通过转换,argv[0]是调用程序的命令,argv[1]是第一个命令行参量,如此等等,直到argv[argc],它总是为NULL。有关截取命令行处理的更多信息,参见本章后面“定制C++命令行处理”。

第一个命令行参量总是argv[1],而最后一个是argv[argc-1]。

Microsoft特殊处

envp

envp数组用于Microsoft C++中,它也是很多UNIX系统的常见的扩充。它是一个字符串数组,用于表示在用户环境设置的变量,此数组由一个NULL项结尾。关于截取环境处理的信息,参见本章后面的“定制C++命令行处理”。该参量在C中是ANSI兼容的,但在C++中却不是。

Microsoft特殊处结束

以下例子展示如何使用main中的argc、argv和envp参量:

#include 
#include
void main(int argc, char *argv[], char *envp[])
{
int iNumberLines = 0; //缺省情况是没有行号
//如果不只提供了.EXE文件名,而且指定了命令行选项/n
//则环境变量的列表是按行编号的
if (argc==2 && srticmp(argv[1],"/n")==0)
iNumberLines=1;
//通过字符串表直到遇到NULL
for (int i=0; envp[i]!=NULL;++i)
{
if (iNumberLines)
cout << i << ": "<< envp[i]<<"/n"
}
}

通配符扩展

Microsoft特殊处

你可以在命令行使用通配符问号(?)和星号(*),指定文件名和路径变量。命令行参量由被称为_setargv的例行程序处理。缺省地,_setargv将通配符扩展为单独字符串进入argv字符串数组中,如果通配符变量未找到匹配,则参量作为文字传送。

Microsoft特殊处结束

分析C++命令行变量

Microsoft特殊处

Microsoft C/C++开始代码使用以下规则解释操作系统命令行给出的参量:

> 参量由空白定界,可以是一个空格或一个制表符

> 插入字符(^)不被认为是一个转义字符或定界符。在传递给程序中的argv数组前,字符由操作系统中命令行语法分析程序进行完全处理。

> 由双引号包围的字符串(“字符串”)被解释为单个参量,而不管其中是否包含空白。被引号括起的字符串可以嵌入参量中。

> 由反斜杠引导的一个双引号(/")被解释为一个文字双引号字符(")l 反斜杠被作为文字解释,除非它们后面紧跟一个双引号。

> 如果偶数个反斜杠后面跟随一个双引号,则为每对反斜杠放一个反斜杠到argv数组中,双引号被解释为一个字符串定界符。

> 如果奇数个反斜杠后面跟随一个双引号,为每对反斜杠放一个反斜杠到argv数组中,双引号则被剩下的反斜杠进行“转义”而使得一个文字双引号(")放入到argv中。

以下例子说明命令行参量是如何被传递的:

include void main(int argc, //数组argv中的字符串个数
char *argv[], //命令行参量字符串数组
char *envp[] )//环境变量字符串数组
{
int count;
//显示每个命令行参量
cout << "/nCommand-line arguments:/n";
for ( count = 0;count < argc;count++)
cout << " argv["<<count<<"] "
<< argv[count]<< "/n";
}

表2.2给出例子输入和预期的输出以说明前面列出的规则。

表2.2 语法分析命令行的结果

命令行输入argv[1]argv[2]argv[3]
"abc" d eabcde
a///b d"e f"g ha///bde fgh
a///"b c da/"bcd
a///"b c" d ea//bcde

Microsoft特殊处结束

定制C++命令行处理

Microsoft特殊处

如果你的程序不采用命令行参量,你可以通过截取执行命令行处理的库例行程序的使用而节省一小部分空间。这个例行程序被称为_setargv且在“通配符扩展”中描述。为了截取它的使用,在包含main函数的文件中定义一个例程,什么也不做,但要命名为_setargv;然后对_setargv的调用将被你定义的_setargv去满足,则没有加载其库版本。

类似地,如果你从未通过envp参量访问环境表,但可以提供你自己的空例程取代环境处理例程_setenvp。就如同使用_setargv函数,_setenvp必须说明为extern“C”。

你的程序可能调用C运行时库中的spawn或exec例程簇。如果是这种情况,你不能取消环境处理例程,因为这个例程用于从父进程向子进程传递一个环境。

Microsoft特殊处结束

main函数限制

main函数有几条限制不适用任何其它的C++函数。main函数:

> 不能重载(参见第12章“重载”)

> 不能说明为inlinel 不能说明为staticl 不能取其地址

> 不能被调用

程序结束

在C++中,有几种方式退出程序;

> 调用exit函数

> 调用abort函数

> 从main执行一条return语句

exit函数

在标准包括文件STDLIB.H中说明的exit函数用于终止一个C++程序。提供作为exit一个参量的值将作为程序返回码或退出码返回给操作系统。通过约定,0返回码意味着程序成功地完成了。

注意:你可以使用定义在STDLIB.H中的常量EXIT_FAILURE和EXIT_SUCCESS来指示你的程序的成功或失败。

从main函数发出一个return语句等价于用其返回值作参量去调用exit函数。有关更多的信息参见“Microsoft Visual C++ 6.0运行库参考”中的“exit”。

abort函数

也在标准包括文件STDLIB.H中说明的abort函数用于终止一个C++程序。exit和abort之间的区别是:exit允许C++发生运行终止处理(调用全局变量析构函数),而abort则是立即终止程序。有关更多的信息参见“Microsoft VisualC++6.0运行库参考”中的“abort”。

return语句

从main发出一条return语句在功能上等价于调用exit函数。考虑以下例子:int main

{
exit (3);
return 3;
}

在上例中exit和return语句功能上是完全相同的,可是C++要求在返回一个数值时函数应有返回类型而不是void。return语句允许从main返回一个值。

额外的启动考虑

在C++中,对象的构造函数和析构函数可以包含执行用户代码,因此,了解进入main之前发生了什么初始化以及从main退出以后什么析构函数被调用是很重要的(有关对象的构造函数和析构函数,参见第11章“特殊成员函数”中的“构造函数”和“析构函数”)在进入main之前发生了以下初始化:

> 静态数据缺省初始化为0。没有明确初始化的所有静态数据在执行任何其它代码包括运行初始化前均被设定为0。静态数据成员还必须加以显式地定义。

> 在一个转换单元中的全局静态对象的初始化。它的发生可能出现在进入main之前或在对象的转换单元中的任何函数或对象的第一次使用之前。

Microsoft特殊处

在Microsoft C++中,全局静态对象在进入main之前初始化。

Microsoft特殊处结束

全局静态对象是彼此相互依赖的,但在不同的转换单元中可能产生不正确的动作。

额外的终止考虑

你可以用exit、return或abort终止一个C++程序。可以使用atexit函数增加退出处理,这些在下节中讨论。

使用exit或return

当你从main中调用exit或执行一条return语句时,静态对象按照与初始化相反的顺序被销毁,下面的例子显示了如何初始化和清除工作的:#include

class ShowData
{
public:
//构造函数打开一个文件
ShowData(const char *szDev)
{
OutputDev=fopen(szDev,"w");
}
//析构函数关闭一个文件
~ShowData()
{
fclose(OutputDev);
}
//Disp函数在输出设备上显示一字符串
void Disp(char *szData)
{
fputs(szData,OutputDev);
}
private:
FILE *OutputDev;
};

//定义一个类型为ShowData的静态对象,选取的输出设备是"CON"即标准输出设备

ShowData sdl="CON";

//定义另一个类型为ShowData的静态对象,直接输出到文件"HELLO.DAT"中ShowData sd2="CON",

int main()
{
sd1 Disp("hello to default device/n");
sd2.Disp("hello to file hello.dat/n");
return 0;
}

在上面的例子中,静态对象sd1和sd2在进入main之前被创建并被初始化,在使用return语句终止该程序后,先销毁sd2而后是sd1。ShowData类的析构函数关闭这些静态对象相关的文件(有关初始化、构造函数、析构函数的更多信息,参见第11章“特殊成员函数”)。

编写这个代码的另一种方法是说明为块范围对象ShowData,且允许它们在超出

范围时被销毁:

int main()
{
ShowData sd1,sd2("hello.dat");
sd1.Disp("hello to default device/n");
sd2.Disp("hello to file hello.dat/n");
return 0
}

使用atexit

使用atexit函数,你可以指定一个退出处理函数在程序终止前执行。没有一个在atexit调用之前初始化的全局静态对象在退出该处理函数执行前被销毁的。

使用abort

调用abort函数立即引起终止,它越过初始化的全局静态对象的正常的析构过程,还越过了使用atexit函数指定的任何特殊处理。


存储类

存储类管理C++中的对象和变量的生存期、连接和处理。一个给定的对象只能有一个存储类。本节讨论数据的C++存储类:

> 自动的

> 静态的

> 寄存器的

> 外部的

自动的

自动存储的对象和变量对于一个块的给定实例是局部的。在递归或多线程代码中,自动对象和变量被保证为在一个块的不同实例中有不同的存储。MicrosoftC++在程序的栈中存储自动的对象和变量。

在一个块内定义的对象和变量为auto存储,除非用extern或static关键字加以指定。自动的对象和变量可以用auto关键字加以指定,但没有必要显式指明auto。自动的对象和变量是无连接的。

自动的对象和变量仅仅持续到它们被说明的所在块的结束。

静态的

被说明为static的对象和变量在程序执行期间保留它们的值。在递归代码中,一个静态对象或变量在一个块代码的不同实例中确保有相同的状态。

在所有块的外部定义的对象和变量缺省地具有静态生存期和外部连接。被显式地说明为static的一个全局对象或变量具有内部连接。

静态对象和变量在程序执行的期间是持续的。

寄存器的

只有函数的参量和局部变量可以说明为寄存器存储类。

象自动变量一样,寄存器的变量仅仅持续到它们被说明的所在块的结束。

编译器不允准用户对寄存器变量的要求,相反地,当全局优化在使用时,编译器作出自己的寄存器选择,可是,编译器允准所有register关键字的相关语义。

外部的

被说明为extern的对象和变量说明一个定义在另一个转换单元中的对象或一个具有外部连接的封闭的范围中的对象。

具有extern存储类的const变量的说明强制变量具有外部连接,一个externconst变量的初始化允许定义在转换单元中。在一个转换单元中初始化而不是定义在该转换单元中,这将产生不确定的结果。

以下例子给出了两个extern说明:DefinedElsewhere(指向定义在一个不同的转换单元中的名称)和DefinedHere(指向定义在一个封闭的范围中的名称):

extern int DefinedElsewhere;//定义在另一个转换单元中
void main()
{
int DefinedHere;
{
extern int DefinedHere;//指向封闭的范围中的DefinedHere
}
}

对象的初始化

一个局部的自动的对象或变量在控制流程每次到达它的定义时都被初始化。一个局部的静态对象或变量在控制流程第一次到达它的定义时被初始化。考虑以下例子,例子中定义了一个类,它历经对象的初始化和析构,而后定义了三个对象I1、I2和I3:

#include 
#include
//定义了一个类,它历经初始化和析构
class InitDemo
{
public:
InitDemo(const char *szWhat);
~InitDemo();
private:
char *szObjName;
};

//类InitDemo的析构函数

InitDemo::InitDemo (const char * szWhat)
{
if ( szWhat!=0 && strlen(szWhat)>0 )
{
//为szObjName分配存储,然后将szWhat初始值拷贝到szObjName
szObjName=new char[strlen(szWhat)+1];
strcpy (szObjName,szWhat);
cout << "Initializing" << szObjNmae << "/n";
}
else
szObjName=0;
}

//InitDemo的析构函数

InitDemo::~InitDemo()
{
if (szObjName!=0)
{
cout << "Destroying: " << szObjName << "/n";
delete szObjName;
}
}

//输入main函数

void main()
{
InitDemo I1("Auto I1")
{
cout << "In block./n";
InitDemo I2("Auto I2");
static InitDemo I3("Static I3");
}
cout << "Exited block./n";
}

上面的代码描述了对象I1、I2和I3是如何以及何时被初始化的和何时被销毁的。该程序的输出为:

Initializing: Auto I1

In block.Initializing: Auto I2

Initializing: Static I3

Destroying: Auto I2

Exited block

Destroying: Auto I1

Destrying: Static I3

关于程序有几点要注意的:

第一,I1和I2在控制流退出定义它们的块时被自动销毁。

第二,在C++中,没有必要在一个块的开始去说明对象或变量。此外,这些对象仅当控制流到达它们的定义时才被初始化(I2和I3是这种定义的例子),输出确切地显示了他们是何时被初始化的。

最后,静态局部变量例如I3在程序的持续期间保留它们的值,但是在程序结束时被销毁。


类 型

C++支持三种对象类型:

> 基本类型是建立在语言中的(如int、float或double)。这些基本类型的实例通常被称为“变量”。

> 派生类是从内部类型派生的新类型。

> 类类型是通过现存类型组合创建的新类型,这些在第8章“类”中讨论。

基本类型

C++中的基本类型分为三类:“整型的”、“浮点”和“void”。整型能够处理全部的数;浮点类型能够指定可能有小数部分的值。

void类型描述值的空集。不能指定void类型的变量,它基本上用于说明没有返回值的函数或说明无类型或任意类型的数据的“通用”指针。任何表达式可显式地转换或造型转换为类型void,可是,这些表达式限于以下应用:

> 一个表达式语句(参见第4章“表达式”)。

> 逗号运算符的左侧操作数(参见第4章“表达式”中的“逗号运算符”)。

> 条件运算符的(?:)的第二或第三个操作数(参见第4章“表达式”中的“带条件运算符的表达式”)。

表2.3解释了类型尺寸的限制,这些限制是独立于Microsoft实现的。

表2.3 C++语言的基本类型

目录类型内容
整型char类型char是整型,通常包含执行字符集的成员,在Microsoft C++中,这是ASCII码
  C++编译器把类型为char、signed char和unsigned char的变量作为具有不同的类型对待。类型为char的变量被提升为int,就好象它们缺省为signed char,除非使用/J编译选项。在这种情况下,它们被作为类型unsigned char,且被提升为不带符号扩展的int
short类型short int(或简称short)是一个整型,大于或等于char类型的尺寸,小于或等于int类型的尺寸
 short类型的对象可被说明为signed short或unsigned shortsigned short是short的同义词
int类型int是整型,它大于或等于short int类型的尺寸,小于或等于类型long的尺寸
 int类型的对象可被说明为signed int或unsigned int。signed int是int的同义词
_intn指定整型变量的尺寸,这里的n是以位为单位给出的尺寸,n的值可 以是8、16、32或64
long类型long(或long int)是大于或等于类型int的尺寸的整型
 long类型的对象可被说明为signed long或unsigned long
signed long是long的同义词
浮点float类型float是最小的浮点类型
 double类型double是一个浮点类型,它大于或等于float类型的尺寸,但小于或等于long double类型的尺寸
long double*类型long double是一个等于类型double的浮点类型

*long double和double的表示是完全相同的,可是long double和double是两种类型。

Microsoft特殊处

表2.4列出了Microsoft C++中基本类型所需的存储空间大小。

表2.4 基本类型的尺寸

类型尺寸
char,unsigned char,signed char1个字节
short,unsigned short2个字节
int,unsigned int4个字节
long,unsigned long4个字节
float4个字节
double8个字节
long double*8个字节

*long double和double的表示是完全相同的,但long double和double是两种类型。

关于类型转换的更多信息,参见第3章“标准转换”。

Microsoft特殊处结束

指定尺寸的整型

Microsoft C++还支持指定尺寸的整型,你可以使用__intn类型指示符来说明8、16、32或64位的整数变量,其中n是整型变量的尺寸,n值可为8,16,32,或64。以下例子为每个这些指定尺寸的整型数说明一个变量:

_ _int8 nSmall ; //说明8位整数

_ _int16 nMedium ; //说明16位整数

_ _int32 nLarge ; //说明32位整数

_ _int64 nHuge ;//说明64位整数类型

__int8、_ _int16和_ _int32是具有同样尺寸的ANSI类型的同义词,对编写在跨多平台中行为相同的可移植代码很有用。注意,__int8数据类型是类型char的同义词,_ _int16是类型short的同义词,__int32是类型int的同义词,__int64数据类型在ANSI中没有等价类。

由于__int8、__int16和__int32被编译器认为是对应的同义词,所以在使用这些类型作参量去重载函数调用时,应加以小心。例如,以下C++代码将产生一个编译错误。

void MyFunc(_ _int8) {}
void MyFunc(char) {}
void main()
{
_ _int8 newVal;
char MyChar;
MyFunc(MyChar); //不明确的函数调用;
MyFunc(newVal); //char与_ _int8是同义词。
}

派生类型

派生类型是可被用于程序中,可包含直接派生类型和复合派生类型的新类型。

直接派生的类型

从已存在类型直接派生的新类型是指向、指示或(在函数情形)传输类型数据以返回一个新类型的类型。

> 变量或对象数组

> 函数l 一个给定类型的指针

> 对象的引用

> 常量

> 类成员指针

变量或对象数组

变量或对象数组可以包含指定数目的特定类型,例如,从整型派生的数组是一个类型为int的数组。以下代码例子说明并定义了一个10个int变量的数组和一个含有类SampleClass的5个对象的数组:

int ArrayOfInt[10];

SampleClass aSampleClass[5];

函数

函数有0个或多个给定类型的参量以及返回指定类型的对象(或什么也不返回,如果函数具有void返回类型)。

一个给定类型的指针

变量或对象的指针选择存储器中的一个对象。对象可以是全局的、局部的(或栈结构)或动态分配的。给定类型的函数指针允许程序在一个特定对象或多个对象上的函数延迟选择直到运行时。以下例子给出了一个指向类型为char的变量的

指针定义:

char *SzPathStr;

对象的引用对象的引用为通过引用访问对象提供了一种便利的方法,但与使用通过值访问对象所要求的语法相同。以下例子描述了如何使用作为函数参量和作为函数返回类型的引用:

BigClassType &func(BigClassType &objname)
{
objname.DoSomething();//注意,使用了成员运算符(.)
objname.SomeData=7;//通过非const引用传递的数据是可修改的。
return objname;
}

关于通过引用向函数传递对象的几个要点是:

> 访问class、struct和union对象的成员的语法是相同的,就如同它们通过值:成员运算符(.)传递一样。

> 在函数调用前未拷贝对象,传递它们的地址,这可以减少函数调用的系统开销。

另外,返回一个引用的函数仅需要接收它们所指的对象的地址,而不是整个对象的拷贝。

尽管上面的例子仅仅是用函数进行上下文通信中的引用,但引用并不限于此种应用。例如,考虑一个函数需要l值的情况——重载运算符的普通要求:

class Vector
{
public:
Point &operator[](int nSubscript); //函数返回一个引用类型
...
};

上面的说明为类Vector指定了一个用户定义的下标运算符。在一个赋值语句中,出现两种可能的状态;

Vector vl;

int i;Point p;

vl[7]=p; //Vector用作一个l值

p=vl[7]; //Vector用作一个r值

后一种情况,即vl[7]用作一个r值,

可以不用引用去实现。可是,前一种情况,即vl[7]用作一个l值,如果不用引用类型的函数就不能方便地实现。从概念上讲,前面例子中最后两条语句翻译为以下代码:

vl.operator[](7)=3; //Vector用作一个l值

i=vl.operator[](7); //Vector用作一个r值

当用这种方式去看时,容易看到第一个语句必定为一个l值,在赋值语句的左侧且语义正确。

关于重载以及特殊情况的重载运算符的更多信息,参见第12章“重载”中的“重载运算符”。

你还可以使用引用去说明一个变量或对象的一个const引用。说明为const的一个引用保留了通过引用传递参量的效能,而防止被调用函数修改最初对象。考虑以下代码:

//Invalue是一个const引用。

void PrintInt(const int &IntValue)
{
printf("%d/n",IntValue);
}

引用初始化不同于对一个引用类型的变量的赋值,考虑以下代码:

int i=7;
int j=5;
//引用初始化
int &ri=i; // 初始化ri指向i
int &rj=j; // 初始化rj指向j。
//赋值
ri=3; //i现在等于3
rj=12; //j现在等于12
ri=rj; //i现在等于(12)。

常量

关于C++中允许的各种常量的更多信息参见第1章“词法规定”中的“文字”。

类成员指针

这些指针定义一个类型指向一个特殊类型的类成员,这样的一个指针可以被类类型的任何对象或类类型派生类型的任何对象所使用。

使用类成员指针增强了C++语言的类型安全性,如表2.5所列的对成员指针可以使用三种新运算符和结构。

表2.5 和成员一起使用的运算符和结构

运算符或结构语法使用
::*type::*ptr-name成员指针说明。
.*obj-name.*ptr-name使用一个对象或对象引用间接引用一个成员指针,例如:int j=Object.*pMyType
->*obj_ptr->*ptr_name使用一个对象指针间接引用一个成员指针,例如:int j=pObject->*pMyType

考虑以下例子,该例子中定义了一个类AClass和一个派生类型pDAT,它指向成

员I1:

#include 
//定义类AClass
{
public:
int I1;
Show() { cout << I1 << "/n"; }
};

//定义一个派生类型pDAT,指向类型AClass的对象的I1成员

int AClass::*pDAT=&AClass::I1;
void main()
{
AClass aClass; //定义类型AClass的一个对象。
AClass *paClass=&aClass; //定义对象的一个指针。
int i;
aClass.*pDAT=7777; //用.*运算符对aClass::I1赋值
aClass.Show();
i=paClass->*pDAT;//*运算符间接引用一个指针
cout << i << "/n";
}

成员pDAT的指针是从类AClass派生的一个新类型,它是一个比int的“普通”指针更强壮的类型,因为它仅指向类AClass的int成员(在此情况为I1)。静态成员的指针是普通指针而不是类成员指针,考虑以下例子:

class HasStaticMember
{
public:
static int SMember;
};

int HasStaticMember::SMember=0;

int *pSMember=&HasStaticMember::SMember;

注意,该指针的类型是“int指针”,而不是“HasStaticMember::int的指针”。成员指针可以和成员数据一样指向成员函数,考虑以下代码:

#include

//用虚函数Identify说明一个基类A,(注意在此上下文中,struct与类是相同的)

struct A
{
virtual void Identify()=0; //类A没有定义
};

//说明Identify成员函数一个指针

void (A::*pIdentify)()=&A::Identify;
//说明从类A派生的类Bstruct Bpublic A
{
void Identify();
};
//为类B定义Identify函数
void B:Identify()
{
printf("Identification is B::Identify/n");
}
void main()
{
B Bobject; //说明类型B的对象
A *pA ;//说明类型A的指针
PA=&Bobject;//使PA指向类型B的一个对象
(PA->*pIdentify)(); //通过成员pIdentify的指针调用Identify函数
}

该程序的输出是:

Identification is B::Identify该函数是通过类型A的一个指针调用的。可是,由于函数是一个虚函数,pA所指对象的正确函数被调用。

复合派生类型

本节描述以下复合派生类型:

> 类

> 结构

> 联合

有关集合类型和集合类型的初始化可参见第7章“说明符”中的“集合初始化”。

类是组合在一起的一组成员对象、操作这些成员的函数以及(可选的)成员对象和函数的访问控制规格。

通过将类中对象和函数进行分组,C++允许程序员创建派生类型,而不仅是定义数据,还可以定义对象的行为。

类成员在缺省情况下是私有访问和私有继承的。第8章“类”中介绍类。访问控制包含在第10章“成员访问控制”中。

结构

C++的结构与类相同,不同的是所有成员数据和函数缺省的为公共访问,且继承性缺省的为公共继承。

有关访问控制的更多信息,参见第10章“成员访问控制”。

联合

联合允许程序员在相同的存储器空间中定义能够包含不同变量的类型。以下代码给出了如何使用一个联合存储几个不同类型的变量:/

/说明可以拥有类型为char、int和char *的数据的联合

union ToPrint
{
char chVar;
int iVar;
char *szVar;
};

//说明一个枚举类型,用于描述打印的类型

enum PrintType{ CHAR_T,INT_T,STRING_T };

void Print( ToPrint Var,PrintType Type)
{
switch(Type)
{
case CHAR_T:
printf("%c",Var.chVar);
break;
case INT_T:
printf("%d",Var.iVar);
break;
case STRING_T:
printf("%s",Var.szVar);
break;
}
}

类型名称

基本的和派生的类型的同义词均可使用typedef关键字来定义。以下代码描述了typedef的使用:

typedef unsigned char BYTE; //8位无符号实体

typedef BYTE *PBYTE; //指向BYTE的指针

BYTE Ch; //说明一个类型BYTE的变量

PBYTE pbCh; //说明一个指向BYTE变量的指针

上面的例子给出了基本类型unsigned char和其派生类型unsigned char *的统一的说明语法。typedef结构对简化说明也很有用。一个typedef说明定义一个同义词,而不是一个新的独立类型。以下例子说明了一个表示指向返回类型为void的函数的指针的类型名(PVFN)。这种说明的好处是在后面的程序中这些指针的数组说明起来非常简单。

//两个函数原型

void func1();

void func2();

//定义PVFN表示一个指向返回类型为void的函数的指针

typedef void (*PVFN)();

...

//说明一个函数指针数组

PVFN pvfn[]={ func1,func2 };

//调用一个函数

(*pvfn[1])();


l值和r值

C++中表达式可以求值为“l值”或“r值”。l值是一个表达式,求值为某个非void的类型,且指定一个变量。

l值出现在赋值语句的左侧,(因此用"l"表示),通常为l值的变量可以用const关键字使其不可修改,这样不能出现在赋值语句的左侧。引用类型总是l值。

术语r值有时用于描述一个表达式的值,且与一个l值区分开来,所有的l值都是r值,但r值不都是l值。

一些正确及错误使用的例子;

i=7; //正确 变量名i是一个l值

7=i; //错误 常量7是一个r值

j*4=7; //错误 表达式j*4产生一个r值

*p=i; //正确 一个间接引用指针是一个l值

const int ci=7; //说明一个const变量

ci=9 //ci是一个不可修改的l值,所以赋值导致一条错误信息的产生

((i<3) ? i:j )=7; //正确,条件运算符(?:)返回一个l值

注意:本节的例子描述了运算符未重载时的正确的和错误的用法,通过重载运算符,你可以让一个表达式,如j*4成为一个l值。


数的限制

两个标准包括文件LIMITS.H和FLOAT.H定义了“数的限制”或一个给定类型变量所能取的最小和最大值,这些最小值和最大值确保与ANSI C使用相同数据表示的任意C++编译器都是可移植的。LIMITS.H包含文件为整型定义数的限制,FLOAT.H为浮点类型定义数的限制。

整数限制

Microsoft特殊处

整数类型的限制列在表2.6中,这些限制同时定义在标准头文件LIMITS.H中。

表2.6 整型常量的限制常量

常量含义
CHAR_BIT不是位域的最小变量的位数8
SCHAR_MINsigned char类型变量的最小值-128
SCHAR_MAXsigned char类型变量的最大值127
UCHAR_MAXunsigned char类型变量的最大值255(0xff)
CHAR_MINchar类型变量的最小值-128;如果用/J选项为0
CHAR_MAXchar类型变量的最大值127;如果用/J选项为255
MB_LEN_MAX在多字符常量中最大字节数2
SHRT_MINshort类型变量的最小值-32768
SHRT_MAXshort类型变量的最大值32767
USHRT_MAXunsigned short类型变量的最大值65535(0xffff)
INT_MIN类型变量的最小值-2147483647-1
INT_MAX类型变量的最大值2147483647
UINT_MAXunsigned int类型变量的最大值4294967295(0xffffffff)
LONG_MINlong类型变量的最小值-2147483647-1
LONG_MAXLong类型变量的最大值2147483647
ULONG_MAXunsigned long类型变量的最大值424967295(0xffffffff)

如果数值超过了最大的整数表示范围,则Microsoft编译器产生错误。

Microsoft特殊处结束

浮点数限制

Microsoft特殊处

表2.7列出了浮点常量值的限制,这些限制同时定义在标准头文件FLOAT.H中。

表2.7 浮点常量限制

常量含义
FLT_DIG 6
DBL_DIG 15
LDBL_DIG数字个数q,即具有q个十进制数字的浮点数可舍入为一个浮点表示,而且返过来不损失精度15
FLT_EPSILON最小的正数x,如x+1.0不等于1.01.19202896e-07F
DBL_EPSILON 2.2204460492503131e-016
LDBL_EPSILON 2.2204460492503131e-016
FLT_GUARD 0
FLT_MANT_DIG在浮点有效数中由FLT_RADIX指定的24
DBL_MANT_DIG基数中的数字位数。基数为2;53
LDBL_MANT_DIG因此这些值指定位数53
FLT_MAX最大的可表示的浮点数3.402823466e+38f
DBL_MAX 1.7976931348623158e+308
LDBL_MAX 1.7976931348623158e+308
FLT_MAX_10_EXP最大整数,10的这个数的乘方38
DBL_MAX_10_EXP是可表示的浮点数308
LDBL_MAX_10_EXP 308
FLT_MAX_EXP最大整数,FLT_RADOX的这个数的乘方128
DBL_MAX_EXP是可表示的浮点数1024
LDBL_MAX_EXP 1024
FLT_MIN最小的正值1.175494351e-38
FDBL_MIN 2.2250738585072014e-308
LDBL_MIN 2.2250738585072014e-308
FLT_MIN_10_EXP最小负整数,10的这个数的乘方-37
DBL_MIN_10_EXP是可表示的浮点数-307
LDBL_MIN_10_EXP -307
FLT_MIN_EXP最小负整数,FLT_RADIX这个数的乘方-125
DBT_MIN_EXP是可表达的浮点数-1021
LDBT_MIN_EXP -1021
FLT_NORMALIZE0FLT_RADIX指数表示的基数2
_DBL_RADIX2_LDBL_RADIX2FLT_ROUNDS浮点加法的舍入模式1(近) 
_DBL_ROUNDS1(近)
_LDBL_ROUNDS1(近)

注意,表2.7中的信息可能在产品的未来版本中有所不同。

Microsoft特殊处结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值