首页 > 文库大全 > 精品范文库 > 15号文库

第1章 命名空间与异常处理

第1章 命名空间与异常处理



第一篇:第1章 命名空间与异常处理

版本控制页

版本控制表

序号 版本号 V0.00 版本性质 初稿 一校 二校 10.16.2008

2008.10.30

孔祥萍 赵元

王文丛

创建时间

建议人

修订人

修改日期

修改内容简述

备注

第1章 命名空间与异常处理

本章主要内容:

 命名空间的定义  命名空间的访问  异常处理的概念  异常处理的机制  多层级间的异常处理  异常处理的堆栈操作

本章重点:

 命名空间的定义与命名空间的访问  异常处理的机制和实现  多层级间的异常处理

本章难点:

 命名空间的访问

 多层级间的异常处理  异常处理的堆栈操作

学完本章您将能够:

 了解命名空间概念  掌握命名空间定义  了解异常处理的机制  掌握异常处理的方法

 掌握多层级间的异常处理方法  了解异常处理中的堆栈操作

引言

在一个给定作用域中定义的每个名字在该作用域中必须是惟一的,对庞大、复杂的应用程序而言,这个要求可能难以满足。这样的应用程序在全局作用域中,一般有许多名字定义。由独立开发的库构成的复杂程序更有可能遇到名字冲突,本章用命名空间的技术来解决此类问题。

C++中的异常处理是一种灵活并且精巧的工具。它克服了C语言的传统错误处理方法的缺点,并且能够被用来解决一系列运行期的错误。但是,异常处理也像其他语言特性一样,很容易被误用。为了能够有效的使用这一特性,了解运行期机制如何工作以及相关的性能花费是非常重要的。

1.1 命名空间

1.1.1 命名空间的定义

命名空间定义以关键字namespace开始,后接命名空间的名字。命名空间名字后面由大括号括住的一块声明和定义区域构成,可以在命名空间中放入可以出现在全局作用域的任意声明:类、变量(以及它们的初始化)、函数(以及它们的定义)、模板以及其他命名空间。

namespace GameCollege {

class Matrix { /*...*/ };

void Inverse(matrix &);

const double PI = 3.1416;} 这段代码定义了名为 GameCollege的命名空间,它有3个成员:一个类、一个函数、一个常量。

像其他名字一样,命名空间的名字在定义该命名空间的作用域中必须是惟一的。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。命名空间作用域不能以分号结束。

定义在命名空间中的实体称为命名空间成员。像任意作用域的情况一样,命名空间中的每个名字必须引用该命名空间中的惟一实体。因为不同命名空间引入不同作用域,所以不同命名空间可以具有同名成员。在命名空间中定义的名字可以被命名空间中的其他成员直接访问,命名空间外部的代码必须指出名字定义在哪个命名空间中。在命名空间之外使用空间的成员必须用空间名字进行修饰。

#include int main(){ } std::cout << "Hello, World!" << std::endl;此程序,是通过直接说明名字来自std命名空间,来引用标准库中的名字。例如要从标准输入设备读取数据时,需要使用std::cin语句来完成。这些名字都用了:: 操作符,该操作符是作用域操作符。它的含义是右操作数的名字可以在左操作数的作用域中找到。因此,std::cin的意思是说所需要名字cin是在命名空间std中定义的。

与其他作用域不同,命名空间可以在几个部分中定义。命名空间由它的分离定义部分的总和构成,命名空间是累积的。一个命名空间的分离部分可以分散在多个文件中,在不同文本文件中的命名空间定义也是累积的。当然,名字只在声明名字的文件中可见,这一常规限制继续应用,所以,如果命名空间的一个部分需要使用定义在另一文件中的名字,仍然必须声明该名字。

1.1.2 嵌套命名空间

一个嵌套命名空间,即是一个嵌套作用域,其作用域嵌套在包含它的命名空间内部。嵌套命名空间中的名字遵循常规规则:外围的命名空间中声明的名字被嵌套命名空间中同一名字的声明所屏蔽。嵌套命名空间内部定义的名字局部于该命名空间。外围的命名空间之外的代码只能通过限定名访问嵌套命名空间中的名字。

嵌套命名空间可以改进库中代码的组织:

namespace GameCollege

// 外围的命名空间 {

namespace QueryLib

// GameCollege的嵌套命名空间QuerLib

{

class Query { /*...*/ };

Query operator&(const Query&, const Query&);

//...}

namespace Bookstore

// GameCollege的嵌套命名空间Bookstore {

class Item_base { /*...*/ };

class Bulk_item : public Item_base { /*...*/ };

//...} } 命名空间GameCollege现在包含两个嵌套命名空间:名为QueryLib的命名空间和名为Bookstore的命名空间。

当程序库提供者为了防止库中每个部分的名字与库中其他部分的名字起冲突的时候,嵌套命名空间是很有用的。

嵌套命名空间中成员的名字由外围命名空间的名字和嵌套命名空间的名字构成。例如,嵌套命名空间QueryLib中声明的类的名字是:

GameCollege::QueryLib::Query 1.1.3 命名空间的访问

如:命名空间名namespace_name::member_name,成员名这样引用命名空间的成员无可否认是很麻烦,特别是命名空间名字很长的时候。可以使用更简洁的方式使用命名空间的成员。

 using 声明

【实例1-1】using声明

#include using std::cout;using std::endl;int main(){

cout << "Hello, World!" << endl;

return 0;} 使用using声明能使命名空间std中的名字cout、endl在当前作用域中可见,可以无须限定符std::而使用名字cout、endl。using声明中引入的名字遵循常规作用域规则。从using声明点开始,直到包含using声明的作用域的末尾,名字都是可见的。外部作用域中定义的同名实体被屏蔽。简写名字只能在声明它的作用域及其嵌套作用域中使用,一旦该作用域结束了,就必须使用完全限定名。using声明可以出现在全局作用域、局部作用域或者命名空间作用域中。类作用域中的using声明局限于被定义类的基类中定义的名字。

 命名空间别名

可用命名空间别名将较短的同义词与命名空间名字相关联。例如:

namespace GameCollegeLib {

} const MyClass cMyClass = MyClass();void PrintfMyClass(const MyClass& myClass);class MyClass { public:

};void SetValue(int v){ iValue = v;} int GetValue()const { return iValue;} int iValue;MyClass(): iValue(0){}

private: 这样的长命名空间名字,可以像下面这样与较短的同义词相关联:

namespace gc = GameCollegeLib;命名空间别名声明以关键字namespace开头,接(较短的)命名空间别名名字,再接 =,最后接原来的命名空间名字和分号。如果原来的命名空间名字是未定义的,程序将会出错。

【实例1-2】命名空间别名

#include #include “gamecollegelib.h”

namespace gc = GameCollegeLib;

int main(){

std::cout << gc::cMyClass.GetValue()<< std::endl;gc::MyClass myClass;

myClass.SetValue(45);

gc::PrintfMyClass(myClass);

} return 0;上例程序中gc是GameCollegeLib的别名,可以通过gc访问GameCollegeLib命名空间中成员。

一个命名空间可以有许多别名,所有别名以及原来的命名空间名字都可以互换使用。

 using 指示的形式

“using指示”以关键字using开头,后接关键字namespace,再接命名空间名字。如果该名字不是已经定义的命名空间名字,程序将会出错。

using namespace std;

“using指示”使得特定命名空间所有名字可见,没有限制。短格式名字可从“using指示”点开始使用,直到出现“using指示”的作用域的末尾。

【实例1-3】using指示的形式访问命名空间

namespace GameCollegeLib {

int i = 16, j = 15, k = 23;

// 命名空间其他成员 } int j = 0;void manip(){

using namespace GameCollegeLib;// using 指示GameCollegeLib空间中名称全部可见

++i;

// 设置GameCollegeLib::i为17

++j;

// 错误:名称冲突,无法确定是全局j还是GameCollegeLib::j

++::j;

// 设置全局变量j为1

++GameCollegeLib::j;// 设置GameCollegeLib::j为16

int k = 97;

// 定义局部变量k将覆盖GameCollegeLib::k

++k;

// 设置局部变量k赋值为98 } manip中的“using指示”使manip能够直接访问GameCollegeLib中的所有名字:使用它们的简化形式,该函数可以引用这些成员的名字。

GameCollegeLib的成员看来好像是在定义GameCollegeLib和manip的作用域中定义的一样。如果在全局作用域中定义GameCollegeLib,则GameCollegeLib的成员看来好像是声明在全局作用域的一样。因为名字在不同的作用域中,manip内部的局部声明可以屏蔽命名

空间的某些成员名字,局部变量k屏蔽命名空间名字GameCollegeLib::k,在manip内部对k的引用没有二义性,它引用局部变量k。

命名空间中的名字可能会与外围作用域中定义的其他名字冲突。例如,对manip而言,GameCollegeLib命名空间的成员j看来好像声明在全局作用域中,但是,全局作用域存在另一名为j的对象。这种冲突是允许的,但为了使用该名字,必须显示指出想要的是哪个版本,因此,在manip内部的j使用是有二义性的:该名字既可引用全局变量又可引用命名空间 GameCollegeLib的成员。

为了使用像j这样的名字,必须使用作用域操作符指出想要的是哪个名字。可以编写 ::j 来获得在全局作用域中定义的变量,要使用GameCollegeLib中定义的j,必须使用它的限定名字GameCollegeLib::j。

“using指示”注入来自一个命名空间的所有名字,它的使用是不可靠的:只用一个语句,命名空间的所有成员名就突然可见了。虽然这种方法看似简单,但也有它自身的问题。如果应用程序使用许多库,并且用“using指示”使得这些库中的名字可见,那么,全局命名空间污染问题就会重新出现。

1.2 异常处理在C++中的实现

1.2.1 异常处理的概念和使用条件

程序运行中的某些错误(或意外情况)不可避免但可以预料。例如,假设y变量是一个除法运算或者模运算的除数,那么当变量y为0时,是一种严重的错误(即出现异常),需要得到处理并将错误反馈给设计者,可以在程序中添加如下形式的测试语句来达到这样的目的:

if(y == 0){

std::cout << “Error occuring--Divided by 0!”;exit(1);} 若出现y值为0的情况,则用户程序将进行干预,比如先在屏幕上给出适当的错误提示信息,然后退出程序停止运行等。这种处理工作本质上就是异常处理。从这里可以看出,异常处理就是从发生异常情况的地方停止,不再进行正常的程序流程,而是转而执行特定的异常处理流程。

1.2.2 异常处理的实现

C++中提供的对异常进行处理的机制,那就是在程序中使用try、catch和throw。

【实例1-4】在函数中使用异常机制

请看如下程序,其中使用了try、catch和throw来对除以0或模0所产生的异常进行处理。程序要求输入两个整数i1、i2以及一个运算符op(“/”或者“%”),而后进行相应运算并对所出现的异常进行处理。

#include

using std::cout;using std::cin;using std::endl;

void main(){

try

{

// try 程序块为“受监控”的块,块中所抛出的异常

// 将被本try块后的某一个catch块所捕获并处理

int i1, i2;

char op;

cout << “Input I1 I2 OP:”;

cin >> i1 >> i2 >>op;// 输入两个整数i1、i2 以及一个运算符op

if(op == '/')

{

// 分母为0 时,抛出一个字符串:char*类型的异常

// 所抛出的异常将被随后的catch块捕获并处理

if(i2 == 0)

throw “Divided by 0!”;

// 正常情况(分母非0)的处理

cout << i1 << “ / ” << i2 << '=' << i1/i2 << endl;

}

else if(op == '%')// 限制op只能为“/”或者“%”

{ // 分母为0 时,抛出整数i1 — int 型异常 // 所抛出的异常将被随后的catch 块捕获并处理

if(i2==0)

throw i1;

cout << i1 << “ % ” << i2 << '=' << i1 % i2 << endl;// 正常情况处理

}

else

cout<<“OP error--must be '/' or '%'!”<

// 再进行一些其他的处理

cout << “22 / 5=” << 22 / 5 << endl;

}

catch(int i)

{

// 捕获“int”类型的异常并进行处理

// 输出相关的异常信息后结束本catch块

cout << “Error occuring--” << i << “ % 0 ” << endl;

}

catch(char *str)

// 捕获“char *”类型的异常并进行处理

{

// 输出相关的异常信息后结束本catch块

cout<<“Error occuring--”<

} // 异常处理结束后,均转到此处执行该语句

cout<<“End main function!”<

Input I1 I2 OP:33 5 % 33 % 5=3 22 / 5=4 End main function!再次运行,显示结果为:

Input I1 I2 OP:33 0 / Error occuring--Divided by 0!End main function!注意:通过throw抛出异常后,系统将跳转到与所抛出的实参(表达式)类型完全匹配的catch块去执行,而执行完catch块后将不再返回,继而转到catch块序列的最后一个catch 块的下一语句处去执行。

对实际问题进行处理的程序中,还会碰到多种多样的产生异常(或错误)的情况。例如,磁盘中的文件被移动或缺少所需文件时将导致无法打开,内存不足使得通过new无法获取所需要的动态空间,用户提供了不恰当的输入数据等。为使程序具有更好的容错能力并体现出程序的健壮性,设计程序时,应该充分考虑程序执行过程中可能发生的各种意外情况(即异常),并对它们进行恰当的处理。也就是说,当异常出现后,要尽可能地让用户程序来进行干预,排除错误(至少给出适当的错误提示信息),而后继续运行程序。

下面对try、catch和throw的功能及其使用时要注意的内容做进一步说明:

1)

通过try可构成try块,用于标记运行时可能出现异常的程序代码。也就是说,try程序块在运行时将成为“受监控”的代码块,其中所抛出的任何异常(包括:在try 块中直接抛出的异常,以及通过所调用的“下层”函数而间接抛出的各种异常)都将被捕获并处理。

注意:抛出异常是通过throw 语句来完成的。try 块的使用格式如下: try { //“受监控”的语句序列(通常包含抛出异常的throw 语句)} 2)

通过catch可构成所谓的catch块,它紧跟在try块的后面,用于监视并捕获运行时可能出现的某一种类(类型)的异常并对此种异常进行具体的处理(处理程序代码包含在catch块之中)。即catch程序块将监视并捕获处理程序异常,若没有异常出现的话,catch程序块将不被执行。若准备处理多种类型的异常,要同时给出一个连续的catch块序列,届时系统将通过按序逐一“比对”所抛出的异常类型(既可是系统预定义的基本类型,也可是用户自定义的某种类型)来确定执行该catch块序列中的哪一块。

catch 块的使用格式如下:

catch(/*欲捕获的异常类型及形参名字*/){ //对该类型异常的具体处理语句序列 } 注意:如果catch块的具体处理语句序列中根本不使用形参(对应值)的话,则catch块首括号中可以只给出一个异常类型而不必给出形参名字。另外,还允许在catch块首括号中仅写上3 个点符号, 即“catch(…)”,其使用含义为:该catch块将捕获任何类型的异常。也就是说,它与任何一种异常类型都可以匹配成功。若使用这种catch块的话,它应该是catch块序列的最后一块,否则,其后的所有catch块都将起不到任何作用(永远不会被“匹配”成功。因为系统是按序与catch块序列中的每一块逐一进行“对比”的)。

抛出异常的throw语句的使用格式为:

throw 表达式

通常使用“表达式”的特殊情况,例如,一个变量或对象、一个常量或字符串等。系统将根据表达式的值类型来与各catch块首括号中形参的异常类型进行“比对”,若“匹配”成功(要求类型完全一致,系统并不自动进行类型转换),则跳转到那一catch块去执行,即进行相应的异常处理。若所有匹配都不成功,则将自动去执行一个隐含的系统函数“abort()”来终止整个程序的运行。

更确切地说,throw语句将把运行时遇到的异常事件信息通知给相匹配的异常处理catch块代码(throw后的“<表达式>”,实际上起着实参的作用,它代表的正是异常事件信息,而catch块首括号中设立的正是所对应的形参。与函数调用类似,通过throw抛出异常后,也同样有一个实参与形参相结合的过程,形参与实参结合后,catch块中处理的实际上是throw语句所提供的实参信息。但与函数调用不同的是,函数调用完成后仍要返回到调用处继续执行,而throw语句所进行的“调用”实际上是带有实参的跳转,跳转到的目标是形参和实参完全匹配的一个catch块,而执行完catch块后将不再返回,继而转到catch块序列的最后一个catch块的下一语句处去执行。另外注意,catch块必须有且仅有一个形参的说明(可以缺少形参名字,但不可以缺少其类型),throw语句后携带的也只是一个相对应的实参(表达式)。

1.3 多级多层捕获与处理机制

1.3.1 多级多层处理的机制

在程序中的层次级别指的是函数间的层层调用关系。在这个调用中被调函数抛出异常,而被主调函数捕获,那么这种情况下的异常处理就称为是多级多层处理。C++的异常处理中是允许处理多级多层情况的。具体如下:

1)允许抛出异常的语句处于捕获与处理异常的catch块所在函数的“下属层次”,例如,可以在主调函数中处理异常,而在被调函数中抛出异常。也就是说,若在本层次的函数中无法处理所抛出的异常的话,系统总会自动沿着“调用链”一直向上传递异常。系统将首先找到异常抛出句throw 所处函数的“父辈”函数,进一步可能又要去找其“祖辈”函数。

【实例1-5】多层间的异常处理

#include unsigned short numMul(unsigned short sNum1, unsigned short sNum2){

int nExc = 0;

unsigned short sProduct = sNum1 * sNum2;

// 当结果超出数据表达范围时就抛出异常

if(sProduct < sNum1 || sProduct < sNum2)

{

throw nExc;

}

else

{ }

int main(){

unsigned short s1 = 50000, s2 = 40000;

unsigned short sProduct;

try {

sProduct = numMul(s1, s2);

printf(“乘积=%dn”, sProduct);catch(int nExc){

printf(“错误代码为:%dn”, nExc);

}

return 0;}

return sProduct;

}

}

程序运行,结果如下:

错误代码为:0 2)允许在同一程序中使用多个try块,而且这些try块可以处于不同的位置并具有不同的层次级别。抛出异常后,系统将首先在相应的低层次级别的try块后的catch块序列中寻找匹配,若不成功,将自动沿着“调用链”向上传递该异常,并在上一层次的try块后的catch块序列中继续寻找匹配。直到最终匹配成功,或者已到达了最高层次的main函数后仍不成功时,则自动调用系统隐含函数“abort()”来终止整个程序的运行。

改写上例中的main()函数,对于抛出的异常不做处理。

#include int main(){

unsigned short s1 = 50000, s2 = 40000;

unsigned short sProduct;

sProduct = numMul(s1, s2);

printf(“乘积=%dn”, sProduct);

return 0;} 程序运行后,系统报错后结束。

1.3.2 多级多层处理的综合实例

【实例1-6】对用户提供不恰当输入数据的异常进行处理

输入一个限定范围内的整数,若输入数据不处于限定范围之内时,则视为程序运行异常,而后抛出并捕获处理这些异常。若输入正确,则在屏幕上显示出所输入的整数。另外,在程序中自定义一个异常类myExcepCla,以方便对异常的处理。

对输入数据所限定的范围如下:只允许输入-50~50之间除去0、11与22之外的那些整数。具体对异常的处理设定为:

若输入值为0,则抛出一个“char*”类异常 — 显示“main-catch::Exception : value equal 0!”; 若输入值小于-50,则抛出一个自定义类myExcepCla异常 — 显示“main-catch::Exception : value less than-50!”; 若输入值大于50,则抛出一个“char*”类异常

— 显示“InData-catch::Exception : value greater than 50!”; 若输入值为11,则利用输入值抛出一个int 型异常 — 显示“InData-catch::Caught INT exception!i=11”; 若输入值为22,则抛出一个double型异常 — 显示“main-catch::Caught unknown exception!”。

要求程序使用两个try块,一个处于main函数中,而另一个则处于被main函数调用的InData函数之中,它们具有不同的位置并具有不同的层次级别。main函数中try块后的catch块序列捕获并处理“char*”、myExcepCla以及“其他”任何类型的异常;而InData函数中try块后的catch 块序列则捕获并处理int与“char*”类型的异常。

#include using std::cout;using std::cin;using std::endl;class myExcepCla

{

char message[100];public:

// 构造函数,实参带来私有字符串数据message

myExcepCla(char* msg)

{

strcpy(message, msg);

}

// 成员函数,获取私有数据message字符串

char* GetErrMsg()

{

return message;

} };void InData();//函数声明 void main(){ // 处于main中的try程序块,它为“受监控”的程序块

try

{

InData();// 调用自定义函数InData,其中可能抛出异常

cout << “End try block!” << endl;

}

catch(char * str)// 捕获“char *”类型的异常并进行处理

{

cout << “main-catch::” << str << endl;

}

catch(myExcepCla ob)

{

// 捕获“myExcepCla”类型的异常并进行处理

cout << “main-catch::” << ob.GetErrMsg()<< endl;

}

catch(...)

{

// 捕获“其他”任何异常并进行处理

cout << “main-catch::” << “Caught unknown exception!” << endl;

} // 本层次的某个异常处理结束后,// 均转到此处执行该语句

cout << “End main function!” << endl;} void f(int val){

// 自定义函数f,负责判断val 是否为11 或22,// 输入值为11,抛出一个int 型异常

if(val == 11)

throw val;

// 输入值为22,抛出一个double型异常

if(val == 22)

throw 123.86;} 注意:f中所抛出的int型异常,将首先被自动沿着“调用链”向上传递到其“父辈”函数InData,而在InData中的catch块序列中,该异常恰好被匹配成功而得到处理。f中所抛出的double型异常,首先也被自动沿着“调用链”向上传递到其“父辈”函数InData,而在InData中的catch块序列中无法匹配成功又进一步被传递到其“祖辈”函数main,在main中与“catch(...)”匹配成功而得到处理。

void InData(){

int val;

cout << “Input a INTEGER(from-50--50, excpt 0、11、22):”;

cin >> val;

if(val == 0)// 若输入值等于0,抛出“char*”类异常,在main中被捕获处理。

throw “Exception : value equal 0!”;

try

{

// 处于InData中的try程序块,它为“受监控”的另一个程序块

if(val<-50)

{

myExcepCla exc(“Exception : value less than-50!”);

// 该异常将被main中try 后的“catch(myExcepCla ob)”

// 所捕获处理

throw exc;

}

if(val > 50)

throw “Exception : value greater than 50!”;// 在本try 后的catch 块序列中被捕获处理

else

f(val);// 被调函数f 中可能抛出异常

cout << “OK!value=” << val << endl;//不发生异常时,屏幕显示出输入值val

} // 捕获“int”类型的异常并进行处理

catch(int i)

{

cout << “InData-catch::” << “Caught INT exception!i =” << i << endl;

}

// 捕获“char *”类型的异常并进行处理

catch(char * str)

{

cout << “InData-catch::” << str << endl;

} } 注意:InData函数中try块后的某一个catch块被执行后(异常被处理结束后),将结束InData函数,返回到其主调函数main的try程序块的InData函数调用语句的下一语句处,即将接着去执行main中的“cout<<“End try block!”<

显示结果为:

Input a INTEGER(from-50--50, excpt 0、11、22):0 main-catch::Exception : value equal 0!End main function!第二次执行:

Input a INTEGER(from-50--50, excpt 0、11、22):88 InData-catch::Exception : value greater than 50!End try block!End main function!第三次执行:

Input a INTEGER(from-50--50, excpt 0、11、22):50 OK!value=50 End try block!End main function!本实例程序中使用了两个try块,一个处于main函数中,而另一个则处于被main函数调用的InData函数之中,它们具有不同的位置并具有不同的层次级别。抛出异常后,系统总是首先在相应的低层次级别(如本例的InData函数)的try块后的catch块序列中寻找匹配,若不成功,再自动沿着“调用链”向上传递该异常,并在上一层次(如本例的main函数)的try块后的catch块序列中继续寻找匹配。

另一个注意点是:若程序中含有多个不同位置并具有不同层次级别的try块时,一旦异常被某一个层次的catch块所捕获,在执行完那一catch块后,系统的继续运行“点”将是本层次catch块序列之最后一个catch块的下一语句处(若那里不再有其他语句时,则结束那一函数而返回到上一层次的主调函数中)。

本程序还自定义了一个异常类myExcepCla,通过对它的使用,可建立起一种对异常进行处理的统一模式。

1.4 异常处理中的堆栈展开

C++进行异常处理时,总要自动地进行如下所述的一个“堆栈展开”过程:每当抛出一个异常后,系统首先找到能捕获处理该异常的catch块(注意,其中可能自动沿着“调用链”多次向上传递该异常,并假定在某一层次的try块后的catch块序列中找到了相匹配的catch块),紧接着系统要利用所抛出的对象(throw句中的“实参”)对相应catch块的“形参”进行“初始化”,而后将立即检查从抛出异常的那一try块的块首到该异常被抛出之间已构造,但尚未析构的那些处于堆栈中的局部对象(与变量)。若有,系统将自动对这些局部对象进行相应的退栈与析构处理(通过自动调用各对象的析构函数来完成,析构对象的顺序与构造它们的顺序恰好相反),再去执行相应catch块中的代码完成对异常的处理。

系统之所以要这样做,是因为异常处理机制中,通过throw语句抛出异常后,系统实际上是要找到并“跳转”到捕获异常的catch块去执行,而且在执行完那一catch块后将不再返回(到throw语句之后),继而转到catch块序列的最后一个catch块的“下一语句”处去执行。正是由于这种“跳转”后的不再返回,当抛出异常的throw语句处于某一下属层次的局部作用域(如某个局部块作用域或某个函数作用域)之中时,throw的“跳转”实际上相当于跳出了那些作用域,所以系统将自动检查在那些作用域中已经构造但尚未析构的处于堆栈中的局部对象(与变量),并自动进行相应的退栈与析构处理。

【实例1-7】分析以下程序执行后会显示出什么结果

注意系统对局部块作用域中的局部对象ob1和ob2所进行的自动析构处理。程序中说明了一个具有显式构造函数和析构函数的自定义类ClaA,并在其中显示目前已经进入该构造函数或析构函数的提示信息(因为要通过输出信息来观察系统提供的自动析构处理功能)。

编写main函数,并在其中设置局部块作用域,且说明ClaA自定义类的局部对象ob1和ob2,之后通过抛出“char*”类型的异常“跳转”出该局部块作用域(此时系统将对局部对象进行自动析构处理)。

由于要抛出并处理异常,将main中的程序段置于try块的块体之中,并添加捕获“char*”类型异常的catch块。

本实例的重点在于了解异常处理过程中的“堆栈展开”,以及对象的自动析构处理功能。

#include

using std::cout;using std::cin;

using std::endl;

class ClaA {

};

void main(){

} // 捕获“char *”类型的异常并进行处理 catch(char * str){

} cout<<“After throw, in block-statement!End block-statement!”<

throw “throwing 'char*' exception in block-statement!”;cout << “Throwing exception, in block-statement!” << endl;// 局部于块的对象ob1和ob2 均被分配在系统的堆栈中 ClaA ob1(“ob1”), ob2(“ob2”);cout<<“Begin main function!”<

// try 程序块为“受监控”的程序块 cout << “Enter try-block!” << endl;// 定义一个局部块作用域 { cout << “Begin a block-statement!” << endl;~ClaA(){

} cout << “Destructing ClaA obj--” << s << endl;ClaA(char * str){

} cout << “Constructing ClaA obj--” << str << endl;strcpy(s, str);char s[100];public:

}

} cout<<“Execution resumes here.End main function!”<

cout<<“In catch-block, deal with: ”<

Begin main function!Enter try-block!Begin a block-statement!Constructing ClaA obj--ob1 Constructing ClaA obj--ob2 Throwing exception, in block-statement!Destructing ClaA obj--ob2 Destructing ClaA obj--ob1 In catch-block, deal with: throwing 'char*' exception in block-statement!Execution resumes here.End main function!注意:上一程序中,抛出异常的throw语句处于一个局部块作用域内,throw的“跳转”实际上相当于跳出了那个局部块作用域(“跳转”到相对应的那一catch块而不再返回),所以系统将自动检查在那一局部块作用域中已经构造,但尚未析构的处于堆栈中的局部对象。如本例的ob1和ob2,并自动对它们进行相应的退栈与析构处理(析构顺序为ob2而后是ob1)。

实际上,当抛出异常的throw语句处于下属层次的某个函数作用域之中时,throw的“跳转”就相当于跳出了那个函数作用域(而不再返回),那时系统同样也将自动检查在哪一函数作用域中已经构造,但尚未析构的处于堆栈中的局部对象,并自动进行相应的退栈与析构处理。

例如,若将本例程序main函数中的try块改写为:

try { cout << “Enter try-block, calling fun!” << endl;fun();} 在程序中添加如下形式的一个fun函数的话,则系统会自动对ob1与ob2进行相应的退栈与析构处理,大家可作为一个练习去完成,并上机进行调试验证。

void fun(){ cout << “Begin fun!” << endl;

ClaA ob1(“obj1”), obj2(“ob2”);

cout << “Throwing exception, in fun!” << endl;

throw “throwing 'char*' exception in fun!”;

cout << “After throw, in fun!End fun!” << endl;} 注意:fun 函数中所抛出的异常,是由其主调函数main 处的catch块所捕获并处理的。本章小结

本章介绍了命名空间概念,如何使用命名空间解决名字冲突问题。如何定义命名空间以及如何使用命名空间成员。在进行复杂系统设计过程中,名字冲突问题是不可避免的,命名空间技术可以很好的解决此类问题。嵌套命名空间可以防止库中每个部分的名字与库中其他部分的名字冲突的问题。对于命名空间的访问有“using声明”、“命名空间别名”、“using 指示”等方法。由于“using指示”使得命名空间中的名字暴露在全局区域中,所以要尽量避免使用。本章还介绍了C++中的异常处理,C++中采用异常处理机制的原因和实现方法,以及实际程序设计时会遇到多层级异常处理的问题。此外还介绍了C++中异常处理时的堆栈操作,使大家更能了解异常处理对程序稳定性的影响,并掌握在程序中使用异常处理的方法,从而使大家设计的程序更加健壮。

自测习题

判断试题

1.当程序中产生异常时,程序一定会被强制结束。()2.异常只在try块中产生,在非try块中不会产生。()

3. 在catch块首括号中仅写上3个点符号,即catch(„)时此块不处理任何异常。()4.若try块后找不到相应的catch块来处理异常,则程序将忽略这个异常继续运行。()5.在同一程序中,try块的数量一定和catch块的数量一样多。()问答题

6.解释using声明和using指示符的区别。7.考虑下面的代码例子:

namespace Exercise {

} int ivar = 0;//1 int

ivar = 0;double dvar = 0;const int limit = 1000;

void manip(){

//2 double dvar = 3.1416;int

iobj = limit + 1;++ivar;++::ivar;} 如果将命名空间Exercise成员的using声明放在 //1 处那么会对代码中的声明和表达式有什么影响?如果放在 //2 处呢?当用using指示符代替命名空间Exercise的using声明时,答案又是什么? 课后作业

1.输入一组数,实现一个累加和函数,当累加和超过数据的表示范围时,产生异常。2.在自己项目中定义自己命名空间,并分别用using声明、命名空间别名、using指示对命名空间成员进行引用。

第二篇:命名空间

本讲基本要求

* 掌握:命名空间的作用及定义;如何使用命名空间。

* 了解:使用早期的函数库

重点、难点

◆命名空间的作用及定义;如何使用命名空间。

在学习本书前面各章时,读者已经多次看到在程序中用了以下语句: using namespace std;

这就是使用了命名空间std。在本讲中将对它作较详细的介绍。

一、为什么需要命名空间(问题提出)

命名空间是ANSIC++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。

在C语言中定义了3个层次的作用域,即文件(编译单元)、函数和复合语句。C++又引入了类作用域,类是出现在文件内的。在不同的作用域中可以定义相同名字的变量,互不于扰,系统能够区别它们。

1、全局变量的作用域是整个程序,在同一作用域中不应有两个或多个同名的实体(enuty),包括变量、函数和类等。

例:如果在文件中定义了两个类,在这两个类中可以有同名的函数。在引用时,为了区别,应该加上类名作为限定: class A //声明A类

{ public:

void funl();//声明A类中的funl函数

private:

int i; };

void A::funl()//定义A类中的funl函数

{„„„„}

class B //声明B类

{ public:

void funl(); //B类中也有funl函数

void fun2(); };

void B::funl()//定义B类中的funl函数

{ „„„„} 这样不会发生混淆。

在文件中可以定义全局变量(global variable),它的作用域是整个程序。如果在文件A中定义了一个变量a int a=3;

在文件B中可以再定义一个变量a int a=5;在分别对文件A和文件B进行编译时不会有问题。但是,如果一个程序包括文件A和文件B,那么在进行连接时,会报告出错,因为在同一个程序中有两个同名的变量,认为是对变量的重复定义。

可以通过extern声明同一程序中的两个文件中的同名变量是同一个变量。如果在文件B中有以下声明: extem int a;

表示文件B中的变量a是在其他文件中已定义的变量。由于有此声明,在程序编译和连接后,文件A的变量a的作用域扩展到了文件B。如果在文件B中不再对a赋值,则在文件B中用以下语句输出的是文件A中变量a的值: cout<

2、程序中就会出现名字冲突。

在简单的程序设计中,只要人们小心注意,可以争取不发生错误。但是,一个大型的应用软件,往往不是由一个人独立完成的,而是由若干人合作完成的,不同的人分别完成不同的部分,最后组合成一个完整的程序。假如不同的人分别定义了类,放在不同的头文件中,在主文件(包含主函数的文件)需要用这些类时,就用#include命令行将这些头文件包含进来。由于各头文件是由不同的人设计的,有可能在不同的头文件中用了相同的名字来命名所定义的类或函数。例4 名字冲突

程序员甲在头文件headerl.h中定义了类Student和函数fun。//例4中的头文件header1(头文件1,没其文件名为cc8-4-h1.h)#include #include using namespace std;class Student //声明Student类

{ public: Student(int n,string nam,int a){ num=n;name=nam;age=a;} void get_data();private: int num;string name;int age;};void Student::get_data()//成员函数定义 { cout<

Wang 18 2.82843 如果程序员乙写了头文件header2.h,在其中除了定义其他类以外,还定义了类Student和函数fun,但其内容与头文件headerl.h中的Student和函数fun有所不同。//例4中的头文件header2 #include #include using namespace std;class Student //声明Student类 { public: Student(int n,string nam,char s)//参数与headerl中的student不同

{ num=n;name=nam;sex=s;} void get_data();private: int num;string name;char sex;};//此项与headerl不同

void Student::get_data()//成员函数定义 { cout<

double fun(double a,double b)//定义全局函数

{ return sqrt(a-b);} //返回值与headerl中的fun函数不同 //头文件2中可能还有其他内容

假如主程序员在其程序中要用到headerl.h中的Student和函数fun,因而在程序中包含了头文件headerl.h,同时要用到头文件header2.h中的一些内容(但对header2.h中包含与headerl.h中的Student类和fun函数同名而内容不同的类和函数并不知情,因为在一个头文件中往往包含许多不同的信息,而使用者往往只关心自己所需要的部分,而不注意其他内容),因而在程序中又包含了头文件header2.h。如果主文件(包含主函数的文件)如下: #include using namespace std;#include ”header1.h“//包含头文件l #include ”header2.h“//包含头文件2 int main(){ Student stud1(101,”Wang“,18);stud1.get_data();cout<

3、全局命名空间污染(global namespace pollution)。

在程序中还往往需要引用一些库(包括C++编译系统提供的库、由软件开发商提供的库或者用户自己开发的库),为此需要包含有关的头文件。如果在这些库中包含有与程序的全局实体同名的实体,或者不同的库中有相同的实体名,则在编译时就会出现名字冲突。

为了避免这类问题的出现,人们提出了许多方法,例如:将实体的名字写得长—些(包含十几个或几十个字母和字符);把名字起得特殊一些,包括一些特殊的字符;由编译系统提供的内部全局标识符都用下划线作为前缀,如_complex(),以避免与用户命名的实体同名;由软件开发商提供的实体的名字用特定的字符作为前缀。但是这样的效果并不理想,而且增加了阅读程序的难度,可读性降低了。c语言和早期的C++语言没有提供有效的机制来解决这个问题,没有使库的提供者能够建立自己的命名空间的工具。人们希望ANSI C++标准能够解决这个问题,提供—种机制、一种工具,使由库的设计者命名的全局标识符能够和程序的全局实体名以及其他库的全局标识符区别开来。

二、什么是命名空间(解决方案)

命名空间:实际上就是一个由程序设计者命名的内存区域,程序设计者可以根据需要指定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。

如: namespace ns1 //指定命名中间nsl

{ int a;

double b;} namespace是定义命名空间所必须写的关键字,nsl是用户自己指定的命名空间的名字(可以用任意的合法标识符,这里用ns1是因为ns是namespace的缩写,含义请楚),在花括号内是声明块,在其中声明的实体称为命名空间成员(namespace member)。现在命名空间成员包括变量a和b,注意a和b仍然是全局变量,仅仅是把它们隐藏在指定的命名空间中而已。如果在程序中要使用变量a和b,必须加上命名空间名和作用域分辨符“::”,如nsl::a,nsl::b。这种用法称为命名空间限定(qualified),这些名字(如nsl::a)称为被限定名(qualified name)。C++中命名空间的作用类似于操作系统中的目录和文件的关系,由于文件很多,不便管理,而且容易重名,于是人们设立若干子目录,把文件分别放到不同的子目录中,不同子目录中的文件可以同名。调用文件时应指出文件路径。

命名空间的作用:是建立一些互相分隔的作用域,把一些全局实体分隔开来。以免产生老点名叫李相国时,3个人都站起来应答,这就是名字冲突,因为他们无法辨别老师想叫的是哪一个李相国,同名者无法互相区分。为了避免同名混淆,学校把3个同名的学生分在3个班。这样,在小班点名叫李相国时,只会有一个人应答。也就是说,在该班的范围(即班作用域)内名字是惟一的。如果在全校集合时校长点名,需要在全校范围内找这个学生,就需要考虑作用域问题。如果校长叫李相国,全校学生中又会有3人一齐喊“到”,因为在同一作用域中存在3个同名学生。为了在全校范围内区分这3名学生,校长必须在名字前加上班号,如高三甲班的李相国,或高三乙班的李相国,即加上班名限定。这样就不致产生混淆。

可以根据需要设置许多个命名空间,每个命名空间名代表一个不同的命名空间域,不同的命名空间不能同名。这样,可以把不同的库中的实体放到不同的命名空间中,或者说,用不同的命名空间把不同的实体隐蔽起来。过去我们用的全局变量可以理解为全局命名空间,独立于所有有名的命名空间之外,它是不需要用namespace声明的,实际上是由系统隐式声明的,存在于每个程序之中。

在声明一个命名空间时,花括号内不仅可以包括变量,而且还可以包括以下类型: ·变量(可以带有初始化); ·常量;

·数(可以是定义或声明); ·结构体; ·类; ·模板;

·命名空间(在一个命名空间中又定义一个命名空间,即嵌套的命名空间)。例如

namespace nsl { const int RATE=0.08; //常量 doublepay;

//变量

doubletax()

//函数

{return a*RATE;} namespacens2

//嵌套的命名空间

{int age;} }

如果想输出命名空间nsl中成员的数据,可以采用下面的方法: cout<

cout<

可以看到命名空间的声明方法和使用方法与类差不多。但它们之间有一点差别:在声明类时在右花括号的后面有一分号,而在定义命名空间时,花括号的后面没有分号。

三、使用命名空间解决名字冲突(使用指南)有了以上的基础后,就可以利用命名空间来解决名字冲突问题。现在,对例4程序进行修改,使之能正确运行。

例5 利用命名空间来解决例4程序名字冲突问题。

修改两个头文件,把在头文件中声明的类分别放在两个不同的命名空间中。//例8.5中的头文件1,文件名为header1.h using namespace std;#include #include namespace ns1 //声明命名空间ns1 { class Student //在命名空间nsl内声明Student类

{ public: Student(int n,string nam,int a){ num=n;name=nam;age=a;} void get_data();private: int num;string name;int age;};void Student::get_data()//定义成员函数

{ cout<double fun(double a,double b)//在命名空间n引内定义fun函数 { return sqrt(a+b);} } //例8.5中的头文件2,文件名为header2.h #include #include namespace ns2 //声明命名空间ns2 { class Student { public: Student(int n,string nam,char s){ num=n;name=nam;sex=s;} void get_data();private: int num;string name;char sex;};

void Student::get_data(){ cout<

double fun(double a,double b){ return sqrt(a-b);} } //main file #include #include ”header1.h“ //包含头文件l #include ”header2.h“ //包含头文件2 int main(){ ns1::Student stud1(101,”Wang“,18);//用命名空间nsl中声明的Student类定义studt stud1.get_data();//不要写成ns1::studl.get_data();

cout<

ns2::Student stud2(102,”Li“,'f');//用命名空间ns2中声明的Student类定义stud2 stud2.get_data();cout<

分析例4程序出错的原因是:在两个头文件中有相同的类名Student和相同的函数名fun,在把它们包含在主文件中时,就产生名字冲突,存在重复定义。编译系统无法辨别用哪一个头文件中的Student来定义对象studl。现在两个Student和fun分别放在不同的命名空间中,各自有其作用域,互不相干。由于作用域不相同,不会产:生名字冲突。正如同在两个不同的类中可以有同名的变量和函数而不会产生冲突一样。

在定义对象时用ns1::Student(命名空间nsl中的Student)来定义studl,用ns2::Student(命名空间ns2中的Student)来定义stud2。显然,nsl::Student和ns2::Student是两个不同的类,不会产生混淆。同样,在调用fun函数时也需要用命名空间名ns]或ns2加以限定。ns1::fun()和ns2::fun()是两个不同的函数。注意:对象studl是用nsl::Student定义的,但对象studl并不在命名空间nsl中。studl的作用域为main函数范围内。在调用对象studl的成员函数get_data时,应写成studl.get_data(),而不应写成nsl::studl.get_data()。程序能顺利通过编译,并得到以下运行结果: 101 Wang l9(对象studl中的数据)2.82843(/5+3的值)102 Li f(对象studg中的数据)1.41421(/5-2的值)

四、使用命名空间成员的方法 从上面的介绍可以知道,在引用命名空间成员时,要用命名空间名和作用域分辨符对命名空间成员进行限定,以区别不同的命名空间中的同名标识符。即: 命名空间名::命名空间成员名

这种方法是有效的,能保证所引用的实体有惟一的名字。但是如果命名空间名字比较长,尤其在有命名空间嵌套的情况下,为引用一个实体,需要写很长的名字。在一个程序中可能要多次引用命名空间成员,就会感到很不方便。1、使用命名空间别名

可以为命名空间起一个别名(namespace alias),用来代替较长的命名空间名。如 namespace Television //声明命名空间,名为Television {...} 可以用一个较短而易记的别名代替它。如:

namespace TV=Television; //别名TV与原名Television等价

也可以说,别名TV指向原名Television,在原来出现Television的位置都可以无条件地用TV来代替。

2、使用using命名空间成员名

using后面的命名空间成员名必须是由命名空间限定的名字。例如: using nsl::Student; 以上语句声明:在本作用域(using语句所在的作用域)中会用到命名空间ns1中的成员Student,在本作用域中如果使用该命名空间成员时,不必再用命名空间限定。例如在用上面的using声明后,在其后程序中出现的Student就是隐含地指nsl::Student。

using声明的有效范围是从using语句开始到using所在的作用域结束。如果在以上的using语句之后有以下语句:

Student studl(101,”Wang“,18); //此处的Student相当于ns1::Student 上面的语句相当于

nsl::Student studl(101,”Wang“,18); 又如

using nsl::fun; //声明其后出现的fun是属于命名空间nsl中的fun cout<

但是要注意:在同一作用域中用using声明的不同命名空间的成员中不能有同名的成员。例如:

usmgnsl::Student; //声明其后出现的Student是命名空间nsl中的Student usmgns2::Student; //声明其后出现的Student是命名空间ns2小的Student Student stud1; //请问此处的Student是哪个命名中间中的Student? 产生了二义性,编译出错。

3、使用using namespace命名空间名

用上面介绍的using命名空间成员名,一次只能声明一个命名空间成员,如果在一个命名空间中定义了10个实体,就需要使用10次using命名空间成员名。能否在程序中用一个语句就能一次声明一个命名空间中的全部成员呢? C++提供了using namespace语句来实现这一目的。using namespace语句的一般格式为 using namespace 命名空间名; 例如

using nanlespace nsl;

声明了在本作用域中要用到命名空间nsl中的成员,在使用该命名空间的任何成员时都不必用命名空间限定。如果在作了上面的声明后有以下语句: Student studl(101,”Wang”,18); //Student隐含指命名中间nsl中的Student cout<

cout<

Student stud2(102,“Li”,'r'); stud2.get_data();

coutt<

五、无名的命名空间

以上介绍的是有名字的命名空间,C++还允许使用没有名字的命名空间,如在文件A中声明了以下的无名命名空间:

namespace //命名空间没有名字 { void fun()//定义命名空间成员 { cout<<“OK.”<

则执行无名命名空间中的成员fun函数,输出”OK.”。

在本程序中的其他文件中也无法使用该fun函数,也就是把fun函数的作用域限制在本文件范围中。可以联想到:在C浯言中可以用static声明一个函数,其作用也是使该函数的作用域限于本文件。C++保留了用static声明函数的用法,同时提供了用无名命名空间来实现这一功能。随着越来越多的C++编译系统实现了ANSI C++建议的命名空间的机制,相信使用无名命名空间成员的方法将会取代以前习惯用的对全局变量的静态声明。

六、标准命名空间std 为了解决C++标准库中的标识符与程序中的全局标识符之间以及不同库中的标识符之间的同名冲突,应该将不同库的标识符在不同的命名空间中定义(或声明)。标准C++库的所有的标识符都是在一个名为std的命名空间中定义的,或者说标准头文件(如iostream)中函数、类、对象和类模板是在命名空间std中定义的。std是standard(标准)的缩写,表示这是存放标准库的有关内容的命名空间,含义请楚,不必死记。

这样,在程序中用到C++标准库时,需要使用std作为限定。如

std::cout<<“OK.”<

这样,在std中定义和声明的所有标识符在本文件中都可以作为全局量来使用。但是应当绝对保证在程序中不出现与命名空间std的成员同名的标识符,例如在程序中不能再定义一个名为cout的对象。由于在命名空间std中定义的实体实在太多,有时程序设计人员也弄不请哪些标识符已在命名空间std中定义过,为减少出错机会,有的专业人员喜欢用若干个"using命名空间成员”声明来代替“using namespace命名空间”声明,如 using Std::string; using Std::cout; using Std::cin;

等。为了减少在每一个程序中都要重复书写以亡的using声明,程序开发者往往把编写应用程序时经常会用到的命名空间std成员的usmg声明组成一个头文件,然后在程序中包含此头文件即可。

如果阅读了多种介绍C++的书,可能会发现有的书的程序中有using namespace语句,有的则没有。有的读者会提出:究竟应该有还是应该没有?应当说:用标准的C++编程,是应该对命名空间std的成员进行声明或限定的(可以采取前面介绍过的任一种方法)。但是目前所用的C++库大多是几年前开发的,当时并没有命名空间,库中的有关内容也没有放在std命名空间中,因而在程序中不必对std进行声明。

七、使用早期的函数库

C语言程序中各种功能基本上都是由函数来实现的,在C语言的发展过程中建立了功能丰富的函数库,C++从C语言继承了这份宝贵的财富。在C++程序中可以使用C语言的函数库。如果要用函数库中的函数,就必须在程序文件中包含有关的头文件,在不同的头文件中,包含了不同的函数的声明。

在C++中使用这些头文件有两种方法。

1、用C语言的传统方法

头文件名包括后缀.h,如stdio.h,math.h等。由于C语言没有命名空间,头文件并不存放在命名空间中,因此在C++程序文件中如果用到带后缀.h的头文件时,不必用命名空间。只需在文件中包含所用的头文件即可。如 #include

2、用C++的新方法

C++标准要求系统提供的头文件不包括后缀.h,例如iostream、string。为了表示与C语言的头文件有联系又有区别,C++所用的头文件名是在C语言的相应的头文件名(但不包括后缀.h)之前加一字母c。例如,C语言中有关输入与输出的头文件名为stdio.h在C++中相应的头文件名为cstdio。C语言中的头文件math.h,在C++中相应的头文什名为cmath。C语言中的头文件string.h在C++中相应的头文件名为cstring。注意在C++中,头文件cstnng和头文件strmg不是同一个文件。前者提供C语言中对字符串处理的有关函数(如strcmp,ctrcpy)的声明,后者提供C++中对字符串处理的新功能。此外,由于这些函数都是在命名空间std中声明的,因此在程序中要对命名空间std作声明。如:

#include #include using namespace std;

目前所用的大多数C++编译系统既保留了c的用法,又提供丁C++的新方法。下面两种用法等价,可以任选。C传统方法 C++新方法

#include #include #include #include #include #include using namespace std;

第三篇:PHP命名空间

PHP命名空间(Namespace)的使用详解

对于命名空间,官方文档已经说得很详细[查看],我在这里做了一下实践和总结。

命名空间一个最明确的目的就是解决重名问题,PHP中不允许两个函数或者类出现相同的名字,否则会产生一个致命的错误。这种情况下只要避免命名重复就可以解决,最常见的一种做法是约定一个前缀。例:项目中有两个模块:article和message board,它们各自有一个处理用户留言的类Comment。之后我可能想要增加对所有用户留言的一些信息统计功能,比如说我想得到所有留言的数量。这时候调用它们Comment提供的方法是很好的做法,但是同时引入各自的Comment类显然是不行的,代码会出错,在另一个地方重写任何一个Comment也会降低维护性。那这时只能重构类名,我约定了一个命名规则,在类名前面加上模块名,像这样:Article_Comment、MessageBoard_Comment 可以看到,名字变得很长,那意味着以后使用Comment的时候会写上更多的代码(至少字符多了)。并且,以后如果要对各个模块增加更多的一些整合功能,或者是互相调用,发生重名的时候就需要重构名字。当然在项目开始的时候就注意到这个问题,并规定命名规则就能很好的避免这个问题。另一个解决方法可以考虑使用命名空间。

注明:

本文提到的常量:PHP5.3开始const关键字可以用在类的外部。const和define都是用来声明常量的(它们的区别不详述),但是在命名空间里,define的作用是全局的,而const则作用于当前空间。我在文中提到的常量是指使用const声明的常量。

基础

命名空间将代码划分出不同的空间(区域),每个空间的常量、函数、类(为了偷懒,我下边都将它们称为元素)的名字互不影响,这个有点类似我们常常提到的‘封装'的概念。创建一个命名空间需要使用namespace关键字,这样: 复制代码代码如下: <?php

//创建一个名为'Article'的命名空间 namespace Article;?>

要注意的是,当前脚本文件的第一个命名空间前面不能有任何代码,下面的写法都是错误的: 复制代码代码如下: //例一

//在脚本前面写了一些逻辑代码 <?php $path = “/”;class Comment { } namespace Article;?> //例二

//在脚本前面输出了一些字符 <?php namespace Article;?>

为什么要说第一个命名空间呢?因为同一脚本文件中可以创建多个命名空间。

下面我创建了两个命名空间,顺便为这两个空间各自添加了一个Comment类元素: 复制代码代码如下: <?php

//创建一个名为'Article'的命名空间 namespace Article;//此Comment属于Article空间的元素 class Comment { }

//创建一个名为'MessageBoard'的命名空间 namespace MessageBoard;//此Comment属于MessageBoard空间的元素 class Comment { } ?>

在不同空间之间不可以直接调用其它元素,需要使用命名空间的语法: 复制代码代码如下: <?php

namespace Article;class Comment { }

namespace MessageBoard;class Comment { } //调用当前空间(MessageBoard)的Comment类 $comment = new Comment();//调用Article空间的Comment类

$article_comment = new ArticleComment();?>

可以看到,在MessageBoard空间中调用article空间里的Comment类时,使用了一种像文件路径的语法: 空间名元素名

除了类之外,对函数和常量的用法是一样的,下面我为两个空间创建了新的元素,并在MessageBoard空间中输出了它们的值。复制代码代码如下: <?php namespace Article;const PATH = '/article';function getCommentTotal(){ return 100;} class Comment { }

namespace MessageBoard;const PATH = '/message_board';function getCommentTotal(){ return 300;} class Comment { } //调用当前空间的常量、函数和类 echo PATH;///message_board echo getCommentTotal();//300 $comment = new Comment();//调用Article空间的常量、函数和类 echo ArticlePATH;///article echo ArticlegetCommentTotal();//100 $article_comment = new ArticleComment();?>

然后我的确得到了Article空间的元素数据。子空间

命名空间的调用语法像文件路径一样是有道理的,它允许我们自定义子空间来描述各个空间之间的关系。抱歉我忘了说,article和message board这两个模块其实都是处于同一个blog项目内。如果用命名空间来表达它们的关系,是这样: 复制代码代码如下: <?php

//我用这样的命名空间表示处于blog下的article模块 namespace BlogArticle;class Comment { }

//我用这样的命名空间表示处于blog下的message board模块 namespace BlogMessageBoard;class Comment { } //调用当前空间的类

$comment = new Comment();//调用BlogArticle空间的类

$article_comment = new BlogArticleComment();?>

而且,子空间还可以定义很多层次,比如说 BlogArticleArchivesDate

公共空间

我有一个common_inc.php脚本文件,里面有一些好用的函数和类: 复制代码代码如下: <?php

function getIP(){ } class FilterXSS { } ?>

在一个命名空间里引入这个脚本,脚本里的元素不会归属到这个命名空间。如果这个脚本里没有定义其它命名空间,它的元素就始终处于公共空间中: 复制代码代码如下: <?php

namespace BlogArticle;//引入脚本文件

include './common_inc.php';$filter_XSS = new FilterXSS();//出现致命错误:找不到BlogArticleFilterXSS类 $filter_XSS = new FilterXSS();//正确 ?>

调用公共空间的方式是直接在元素名称前加 就可以了,否则PHP解析器会认为我想调用当前空间下的元素。除了自定义的元素,还包括PHP自带的元素,都属于公共空间。

要提一下,其实公共空间的函数和常量不用加 也可以正常调用(不明白PHP为什么要这样做),但是为了正确区分元素,还是建议调用函数的时候加上

名称术语

在说别名和导入之前,需要知道关于空间三种名称的术语,以及PHP是怎样解析它们的。官方文档说得非常好,我就直接拿来套了。

1.非限定名称,或不包含前缀的类名称,例如 $comment = new Comment()。如果当前命名空间是BlogArticle,Comment将被解析为BlogArticleComment。如果使用Comment的代码不包含在任何命名空间中的代码(全局空间中),则Comment会被解析为Comment。

2.限定名称,或包含前缀的名称,例如 $comment = new ArticleComment()。如果当前的命名空间是Blog,则Comment会被解析为BlogArticleComment。如果使用Comment的代码不包含在任何命名空间中的代码(全局空间中),则Comment会被解析为Comment。

3.完全限定名称,或包含了全局前缀操作符的名称,例如 $comment = new ArticleComment()。在这种情况下,Comment总是被解析为代码中的文字名(literal name)ArticleComment。

其实可以把这三种名称类比为文件名(例如 comment.php)、相对路径名(例如./article/comment.php)、绝对路径名(例如 /blog/article/comment.php),这样可能会更容易理解。我用了几个示例来表示它们: 复制代码代码如下: <?php

//创建空间Blog namespace Blog;class Comment { } //非限定名称,表示当前Blog空间 //这个调用将被解析成 BlogComment();$blog_comment = new Comment();//限定名称,表示相对于Blog空间

//这个调用将被解析成 BlogArticleComment();$article_comment = new ArticleComment();//类前面没有反斜杆 //完全限定名称,表示绝对于Blog空间 //这个调用将被解析成 BlogComment();$article_comment = new BlogComment();//类前面有反斜杆 //完全限定名称,表示绝对于Blog空间

//这个调用将被解析成 BlogArticleComment();$article_comment = new BlogArticleComment();//类前面有反斜杆

//创建Blog的子空间Article namespace BlogArticle;class Comment { } ?>

其实之前我就一直在使用非限定名称和完全限定名称,现在它们终于可以叫出它们的名称了。

别名和导入

别名和导入可以看作是调用命名空间元素的一种快捷方式。PHP并不支持导入函数或常量。它们都是通过使用use操作符来实现: 复制代码代码如下: <?php

namespace BlogArticle;class Comment { }

//创建一个BBS空间(我有打算开个论坛)namespace BBS;//导入一个命名空间 use BlogArticle;//导入命名空间后可使用限定名称调用元素 $article_comment = new ArticleComment();//为命名空间使用别名 use BlogArticle as Arte;//使用别名代替空间名

$article_comment = new ArteComment();//导入一个类

use BlogArticleComment;//导入类后可使用非限定名称调用元素 $article_comment = new Comment();//为类使用别名

use BlogArticleComment as Comt;//使用别名代替空间名

$article_comment = new Comt();?>

我注意到,如果导入元素的时候,当前空间有相同的名字元素将会怎样?显然结果会发生致命错误。

例:

复制代码代码如下: <?php

namespace BlogArticle;class Comment { }

namespace BBS;class Comment { } Class Comt { }

//导入一个类

use BlogArticleComment;$article_comment = new Comment();//与当前空间的Comment发生冲突,程序产生致命错误 //为类使用别名

use BlogArticleComment as Comt;$article_comment = new Comt();//与当前空间的Comt发生冲突,程序产生致命错误 ?> 动态调用

PHP提供了namespace关键字和__NAMESPACE__魔法常量动态的访问元素,__NAMESPACE__可以通过组合字符串的形式来动态访问: 复制代码代码如下: <?php

namespace BlogArticle;const PATH = '/Blog/article';class Comment { }

//namespace关键字表示当前空间 echo namespacePATH;///Blog/article $comment = new namespaceComment();//魔法常量__NAMESPACE__的值是当前空间名称 echo __NAMESPACE__;//BlogArticle //可以组合成字符串并调用

$comment_class_name = __NAMESPACE__.'Comment';$comment = new $comment_class_name();?>

字符串形式调用问题

上面的动态调用的例子中,我们看到了字符串形式的动态调用方式,如果要使用这种方式要注意两个问题。

1.使用双引号的时候特殊字符可能被转义 复制代码代码如下: <?php

namespace BlogArticle;class name { } //我是想调用BlogArticlename $class_name = __NAMESPACE__.“name”;//但是n将被转义为换行符 $name = new $class_name();//发生致命错误 ?>

2.不会认为是限定名称

PHP在编译脚本的时候就确定了元素所在的空间,以及导入的情况。而在解析脚本时字符串形式调用只能认为是非限定名称和完全限定名称,而永远不可能是限定名称。复制代码代码如下: <?php

namespace Blog;//导入Common类

use BlogArticleCommon;//我想使用非限定名称调用BlogArticleCommon $common_class_name = 'Common';//实际会被当作非限定名称,也就表示当前空间的Common类,但我当前类没有创建Common类 $common = new $common_class_name();//发生致命错误:Common类不存在 //我想使用限定名称调用BlogArticleCommon $common_class_name = 'ArticleCommon';//实际会被当作完全限定名称,也就表示Article空间下的Common类,但我下面只定义了BlogArticle空间而不是Article空间

$common = new $common_class_name();//发生致命错误:ArticleCommon类不存在

namespace BlogArticle;class Common { } ?> 总结

我对PHP的命名空间刚刚接触,也不能随便给一些没有实践的建议。我个人认为命名空间的作用和功能都很强大,如果要写插件或者通用库的时候再也不用担心重名问题。不过如果项目进行到一定程度,要通过增加命名空间去解决重名问题,我觉得工作量不会比重构名字少。也不得不承认它的语法会对项目增加一定的复杂度,因此从项目一开始的时候就应该很好的规划它,并制定一个命名规范。

第四篇:C++命名空间

C++命名空间namespace 虽然使用命名空间的方法,有多种可供选择。但是不能贪图方便,一味使用using 指令,这样就完全背离了设计命名空间的初衷,也失去了命名空间应该具有的防止名称冲突的功能。

一般情况下,对偶尔使用的命名空间成员,应该使用命名空间的作用域解析运算符来直接给名称定位。而对一个大命名空间中的经常要使用的少数几个成员,提倡使用using声明,而不应该使用using编译指令。只有需要反复使用同一个命名空间的许多数成员时,使用using编译指令,才被认为是可取的。

例如,如果一个程序(如上面的outi.cpp)只使用一两次cout,而且也不使用std命名空间中的其他成员,则可以使用命名空间的作用域解析运算符来直接定位。如: #include ……

std::cout << “Hello, World!” << std::endl;

std::cout << “Outer::i = ” << Outer::i << “, Inner::i = ” << Outer::Inner::i << std::endl;又例如,如果一个程序要反复使用std命名空间中的cin、cout和cerr(如上面的outi.cpp),而不怎么使用其他std命名空间中的其他成员,则应该使用using 声明而不是using指令。如:

#include ……

using std::cout;cout << “Hello, World!” << endl;cout << “Outer::i = ” << Outer::i << “, Inner::i = ” << Outer::Inner::i << endl;4)命名空间的名称

l

命名空间别名 标准C++引入命名空间,主要是为了避免成员的名称冲突。若果用户都给自己的命名空间取简短的名称,那么这些(往往同是全局级的)命名空间本身,也可能发生名称冲突。如果为了避免冲突,而为命名空间取很长的名称,则使用起来就会不方便。这是一个典型的两难问题。

标准C++为此提供了一种解决方案——命名空间别名,格式为: namespace 别名 = 命名空间名;例如:(AT&T美国电话电报公司)

namespace American_Telephone_and_Telegraph { // 命名空间名太长

class String {

String(const char*);

// ……

} }

American_Telephone_and_Telegraph::String s1 // 使用不方便

= new American_Telephone_and_Telegraph::String(“Grieg”);

namespace ATT = American_Telephone_and_Telegraph;// 定义别名

ATT::String s2 = new ATT::String(“Bush”);// 使用方便 ATT::String s3 = new ATT::String(“Nielsen”);l

无名命名空间 标准C++引入命名空间,除了可以避免成员的名称发生冲突之外,还可以使代码保持局部性,从而保护代码不被他人非法使用。如果你的目的主要是后者,而且又为替命名空间取一个好听、有意义、且与别人的命名空间不重名的名称而烦恼的话,标准C++还允许你定义一个无名命名空间。你可以在当前编译单元中(无名命名空间之外),直接使用无名命名空间中的成员名称,但是在当前编译单元之外,它又是不可见的。无名命名空间的定义格式为: namespace {

声明序列可选 } 实际上,上面的定义等价于:(标准C++中有一个隐含的使用指令)

namespace $$$ {

声明序列可选 } using namespace $$$;例如: namespace {

int i;

void f(){/*……*/} } int main(){

i = 0;// 可直接使用无名命名空间中的成员i

f();// 可直接使用无名命名空间中的成员f()}

第五篇:C++命名空间

基本技能 12.5:命名空间

我们曾经在第一章中对命名空间进行简单的介绍。这里我们将对命名空间进行深入的讨论。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突。在C++中,变量、函数和类都是大量存在的。如果没有命名空间,这些变量、函数、类的名称将都存在于全局命名空间中,会导致很多冲突。比如,如果我们在自己的程序中定义了一个函数toupper(),这将重写标准库中的toupper()函数,这是因为这两个函数都是位于全局命名空间中的。命名冲突还会发生在一个程序中使用两个或者更多的第三方库的情况中。此时,很有可能,其中一个库中的名称和另外一个库中的名称是相同的,这样就冲突了。这种情况会经常发生在类的名称上。比如,我们在自己的程序中定义了一个Stack类,而我们程序中使用的某个库中也可能定义了一个同名的类,此时名称就冲突了。

Namespace关键字的出现就是针对这种问题的。由于这种机制对于声明于其中的名称都进行了本地化,就使得相同的名称可以在不同的上下文中使用,而不会引起名称的冲突。或许命名空间最大的受益者就是C++中的标准库了。在命名空间出现之前,整个C++库都是定义在全局命名空间中的(这当然也是唯一的命名空间)。引入命名空间后,C++库就被定义到自己的名称空间中了,称之为std。这样就减少了名称冲突的可能性。我们也可以在自己的程序中创建自己的命名空间,这样可以对我们认为可能导致冲突的名称进行本地化。这点在我们创建类或者是函数库的时候是特别重要的。命名空间基础

namespace关键字使得我们可以通过创建作用范围来对全局命名空间进行分隔。本质上来讲,一个命名空间就定义了一个范围。定义命名空间的基本形式如下:

namespace 名称{//声明}

在命名空间中定义的任何东西都局限于该命名空间内。

下面就是一个命名空间的例子,其中对一个实现简单递减计数器的类进行了本地化。在该命名空间中定义了计数器类用来实现计数;其中的upperbound和lowerbound用来表示计数器的上界和下界。//演示命名空间

namespace CounterNameSpace {

int upperbound;

int lowerbound;

class counter {

int count;

public:

counter(int n){

if(n <= upperbound){

count = n;}

else {

count = upperbound;} }

void reset(int n){

if(n < upperbound){

count = n;} }

int run(){

if(count > lowerbound){

return count--;}

else

{

return lowerbound;

}

} };}

其中的upperbound,lowerbound和类counter都是有命名空间CounterNameSpace定义范围的组成部分。

在命名空间中声明的标识符是可以被直接引用的,不需要任何的命名空间的修饰符。例如,在CounterNameSapce命名空间中,run()函数中就可以直接在语句中引用lowerbound:

if(count > lowerbound){

return count--;}

然而,既然命名空间定义了一个范围,那么我们在命名空间之外就需要使用范围解析运算符来引用命名空间中的对象。例如,在命名空间CounterNameSpace定义的范围之外给upperbound赋值为10,就必须这样写:

CounterNameSpace::upperbound = 10;或者在CounterNameSpace定义的范围之外想要声明一个counter类的对象就必须这样写:

CounterNameSpace::counter obj;

一般来讲,在命名空间之外想要访问命名空间内部的成员需要在成员前面加上命名空间和范围解析运算符。

下面的程序演示了如何使用CounterNameSpace这个命名空间: //演示命名空间

#include using namespace std;

namespace CounterNameSpace {

int upperbound;

int lowerbound;

class counter {

int count;

public:

counter(int n){

if(n <= upperbound){

count = n;}

else {

count = upperbound;} }

void reset(int n){

if(n < upperbound){

count = n;} }

int run(){

if(count > lowerbound){

return count--;} else

return lowerbound;} };}

int main(){

CounterNameSpace::upperbound = 100;

CounterNameSpace::lowerbound = 0;

CounterNameSpace::counter ob1(10);

int i;

do {

i = ob1.run();

cout << i << “ ”;

} while(i > CounterNameSpace::lowerbound);

cout << endl;

CounterNameSpace::counter ob2(20);

do {

i = ob2.run();

cout << i << “ ”;

} while(i > CounterNameSpace::lowerbound);

cout << endl;

ob2.reset(100);

do {

i = ob2.run();

cout << i << “ ”;

} while(i > CounterNameSpace::lowerbound);

cout << endl;

return 0;}

请注意:counter类以及upperbound和lowerbound的引用都是在前面加上了CounterNameSpace修饰符。但是,一旦声明了counter类型的对象,就没有必须在对该对象的任何成员使用这种修饰符了。因此ob1.run()是可以被直接调用的。其中的命名空间是可以被解析的。

相同的空间名称是可以被多次声明的,这种声明向相互补充的。这就使得命名空间可以被分割到几个文件中甚至是同一个文件的不同地方中。例如:

namespace NS {

int i;}

//...namespace NS {

int j;}

其中命名空间NS被分割成两部分,但是两部分的内容却是位于同一命名空间中的。也就是NS。最后一点:命名空间是可以嵌套的。也就是说可以在一个命名空间内部声明另外的命名空间。using关键字

如果在程序中需要多次引用某个命名空间的成员,那么按照之前的说法,我们每次都要使用范围解析符来指定该命名空间,这是一件很麻烦的事情。为了解决这个问题,人们引入了using关键字。using语句通常有两种使用方式:

using namespace 命名空间名称;using 命名空间名称::成员;

第一种形式中的命名空间名称就是我们要访问的命名空间。该命名空间中的所有成员都会被引入到当前范围中。也就是说,他们都变成当前命名空间的一部分了,使用的时候不再需要使用范围限定符了。第二种形式只是让指定的命名空间中的指定成员在当前范围中变为可见。我们用前面的CounterNameSpace来举例,下面的using语句和赋值语句都是有效的:

using CounterNameSpace::lowerbound;//只有lowerbound当前是可见的 lowerbound = 10;//这样写是合法的,因为lowerbound成员当前是可见的 using CounterNameSpace;//所有CounterNameSpace空间的成员当前都是可见的

upperbound = 100;//这样写是合法的,因为所有的CounterNameSpace成员目前都是可见的

下面是我们对之前的程序进行修改的结果: //使用using

#include

using namespace std;

namespace CounterNameSpace { int upperbound;

int lowerbound;

class counter {

int count;

public:

counter(int n){

if(n < upperbound){

count = n;}

else {

count = upperbound;} }

void reset(int n){

if(n <= upperbound){

count = n;} }

int run(){

if(count > lowerbound){

return count--;}

else {

return lowerbound;} } };}

int main(){

//这里只是用CounterNameSpace中的upperbound using CounterNameSpace::upperbound;

//此时对upperbound的访问就不需要使用范围限定符了 upperbound = 100;

//但是使用lowerbound的时候,还是需要使用范围限定符的 CounterNameSpace::lowerbound = 0;CounterNameSpace::counter ob1(10);

int i;

do {

i = ob1.run();cout << i << “ ”;

}while(i > CounterNameSpace::lowerbound);cout << endl;

//下面我们将使用整个CounterNameSpace的命名空间

using namespace CounterNameSpace;counter ob2(20);

do {

i = ob2.run();cout << i << “ ”;

}while(i > CounterNameSpace::lowerbound);cout << endl;

ob2.reset(100);lowerbound = 90;

do {

i = ob2.run();cout << i << “ ”;}while(i > lowerbound);

return 0;}

上面的程序还为我们演示了重要的一点:当我们用using引入一个命名空间的时候,如果之前有引用过别的命名空间(或者同一个命名空间),则不会覆盖掉对之前的引入,而是对之前引入内容的补充。也就是说,到最后,上述程序中的std和CounterNameSpace这两个命名空间都变成全局空间了。没有名称的命名空间

有一种特殊的命名空间,叫做未命名的命名空间。这种没有名称的命名空间使得我们可以创建在一个文件范围里可用的命名空间。其一般形式如下: namespace {

//声明 }

我们可以使用这种没有名称的命名空间创建只有在声明他的文件中才可见的标识符。也即是说,只有在声明这个命名空间的文件中,它的成员才是可见的,它的成员才是可以被直接使用的,不需要命名空间名称来修饰。对于其他文件,该命名空间是不可见的。我们在前面曾经提到过,把全局名称的作用域限制在声明他的文件的一种方式就是把它声明为静态的。尽管C++是支持静态全局声明的,但是更好的方式就是使用这里的未命名的命名空间。std命名空间

标准C++把自己的整个库定义在std命名空间中。这就是本书的大部分程序都有下面代码的原因:

using namespace std;

这样写是为了把std命名空间的成员都引入到当前的命名空间中,以便我们可以直接使用其中的函数和类,而不用每次都写上std::。

当然,我们是可以显示地在每次使用其中成员的时候都指定std::,只要我们喜欢。例如,我们可以显示地采用如下语句指定cout:

std::cout << “显示使用std::来指定cout”;

如果我们的程序中只是少量地使用了std命名空间中的成员,或者是引入std命名空间可能导致命名空间的冲突的话,我们就没有必要使用using namespace std;了。然而,如果在程序中我们要多次使用std命名空间的成员,则采用using namespace std;的方式把std命名空间的成员都引入到当前命名空间中会显得方便很多,而不用每次都单独在使用的时候显示指定。

相关内容

热门阅读

最新更新

随机推荐