昨天学习了《The C++ Programming Language》中
Chapter 8:Namespaces and Exceptions(名字空间和异常)
首先我们引出Modularization and Interfaces的概念
Modularization的含义,顾名思义是模块化,为什么要实现模块化呢?首先,任何实际程序都是由一些部分组成的。例如简单的“Hello World!”程序也涉及到至少两个部分:用户代码要求将hello world打印出来,I/O系统完成打印工作。我们考虑一下Chapter 6 的一个实例:Desk Calculator,代码如下:
#include<cctype>
#include<iostream>
#include<map>
#include<string>
using namespace std;
enum Token_value{ NAME,NUMBER,END,PLUS='+',MINUS='-',MUL='*',DIV='/',PRINT=';',ASSIGN='=',LP='(',RP=')'}; //枚举类型的函数,建立了一个符号表,让这些终结符与字符建立了对应,方便维护。
Token_value curr_tok=PRINT; //一个状态,代表了目前的token是什么类型。与各个函数都有联系。类似于一个flag。 //初始赋值为PRINT。PRINT=";" 意思是一个操作完成了,等待下一个操作。
map<string,double> table; //全局变量:容器型变量table 建立了一张有对应关系的表,方便在计算器中定义变量,如a=3的储存。
double number_value; //全局变量:定义数字。string string_value;//全局变量:定义字符串。
int no_of_errors; //全局变量:定义错误个数以及出错状态。
double expr(bool get); //函数声明,下同。
double term(bool get); //同上。
double prim(bool get); //同上。
Token_value get_token(); //同上。
double error(const string& s){ //error函数。比较简洁,作用是输出错误的个数。
no_of_errors++;
cerr<<"error:"<<s<<endl;
return 1;
}
Token_value get_token(){
//辨别输入的东西是什么类型的:1.符号表中的 2.数字 3.字符 4.其他类型(错误),都返回curr_tok。
char ch = 0;
cin>>ch;
switch (ch) {
case 0:
return curr_tok=END; //返回curr_tok 值为end 意思是结束这个操作
case ';':case '*':case '/':case '+':case '-':case '(':case ')':case '=':
return curr_tok=Token_value(ch); //返回curr_tok 强制转换值为枚举型:把char转化为Token_value中的枚举//(条件是那个字符能转换成Token_value中有的终结符对应的字符)
case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9':case '.':
cin.putback(ch);
cin>>number_value;
return curr_tok=NUMBER; //返回curr_tok 值为NUMBER:数字
default:
if (isalpha(ch)) { //isalpha函数判断是否是字母。C++库自带
cin.putback(ch);
cin>>string_value;
return curr_tok=NAME; //返回curr_tok 值为NAME:字符
}
error("bad token");
return curr_tok=PRINT; //若都不是,则输出error(),返回PRINT。
}
}
double prim(bool get) //操作
{
if (get) get_token(); //如果有东西 那就调用get_token 辨别是什么类型的东西 返回curr_tok(类型为Token_value,里面有枚举的各种终结符,值为NUMBER NAME 等各种枚举的终结符)
switch (curr_tok) {
case NUMBER: //为数字型
{ double v=number_value;
get_token();
return v;
}
case NAME: //为字符型
{ double& v=table[string_value];//引用定义,为了要留住这个字符所对应的空间,故使用引用,让v获得值,table[string_value]守住空间。
if (get_token()==ASSIGN) v=expr(true);//若是"=",则运行expr(1)=term(1)=prim(1)等于递归了
return v; //返回v
}
case MINUS: //为负数
return -prim(true);
case LP: //为括号
{ double e=expr(true);
if (curr_tok!=RP) return error(") expected");
get_token();
return e;
}
default:
return error("primary expected");
}
}
double term(bool get)
{
double left=prim(get); //定义left为prim函数的值,prim函数返回结果为真数字,真字符
for (;;)
switch (curr_tok) { //判别现在输入的是什么类型,由curr_tok当变量(因为curr_tok带值,且值是对应的类型)
case MUL: //乘法运算
left*=prim(true);
break;
case DIV: //除法运算
if (double d=prim(true)) { //分母不为0
left/=d;
break;
}
return error("divide by 0"); //分母为0
default:
return left; //返回prim(get),也就是说不是乘除运算,故传到prim中进行基础运算
}
}
double expr(bool get)
{ //加减运算
double left=term(get); //定义left为term函数的值,term函数返回的结果为prim,为真数字真字符。
for(;;)
switch(curr_tok) {
case PLUS:
left+=term(true);
break;
case MINUS:
left-=term(true);
break;
default:
return left; //若不是加减运算 return term函数=return prim函数。结果为真数字真字符
}
}
int main()
{
table["pi"]=3.1415926535897932385;
table["e"]=2.718284590452354;
while (cin) {
get_token(); //取得输入
if (curr_tok==END) break;
if (curr_tok==PRINT) continue;
cout<<expr(false)<<endl;
}
return no_of_errors;
}
可以将它看成五个部分:
这五个部分的功能可细分如下:
再者,我们可以只了解一个函数的接口的具体定义,而不了解它是怎样实现的,就能够很好地使用它。例如我们熟悉的printf函数,我们会使用它,但我相信大多数人不会去看printf的源码。
类似地,即使程序的一个部件是由多个函数组成,或者其中既有自定义类型,也有全局变量,还有函数,但我们都可以这样来设想:如果这样的部件也象函数那样有一个起包装作用的接口,也同样可以只需要了解接口而不需要了解实现,就能够很好地使用它。
因此,我们可以将Desk Calculator 中的细节隐藏起来只显露出使用部分,这样代码简洁又美观。
若程序中的一个部件具有明确的边界,能够实现接口与实现的分离,并对它的用户而言在使用时只需关心其接口而不管其实现者,就叫做模块(Module)。
实现模块的接口与实现的分离,需要程序设计语言提供相应的支持机制。C++提供的支持机制是:
(I) Namespace
(II) Class
模块用接口隐蔽了数据和函数的处理细节(这也称作封装,Encapsulation),使得模块可以在保持接口不变的前提下,改变数据的结构和函数的处理细节。
接下来我们引入Namespace的概念
(i)Namespace是一种表现逻辑聚集关系的机制。换句话说,如果一些声明在逻辑上都与某个准则有关,就可以把这些声明放入一个共同的 namespace,以表现这一事实。
(ii)同一 namespace 中的声明在概念上属于同一个逻辑实体。
再从Desk Calculator实例来说:
我们可以将与某个准则有关的函数都聚集起来,例如我们在上面分好的模块中,里面的函数都可以认为是属于同一个namespace,因此可以有以下方式:
我们可以看到,这种利用namespace将一类函数聚集起来放在一个模块中的行为,我们就可以称之为模块化
Modularization。namespace是一个名字空间,它拥有封装的特性,因此它也代表了一个模块。
但我们同时也可以看到,将函数定义在namespace中,似乎这个结构也变得模糊起来了,并没有很好的完成我们的预期。因此,我们有另一种方法将界面(Interfaces)与实现(Implementations)分离开来。
我们要关注一点:实现 namespace 的接口与实现分离的关键,是在其实现部分中出现的成员被该 namespace 的名字所约束(qualified),这样的约束通过约束符( qualifier ::)来表示。
由于各个 namespace 之间经常会出现互相使用对方成员的情况,如果一使用就要约束,既繁琐又容易出错。因此,C++提供了几种“有限的统一”约束的机制。
(i)在成员的实现中对特定 namespace 的特定成员分别使用 using 声明,约束范围在该实现内
(ii)在接口中对特定 namespace 的特定成员分别使用 using 声明,约束范围在该namespace的所有实现内:
(iii)在接口中对特定 namespace 的所有成员使用 using 指示(指令),约束范围在该namespace的所有实现内:
我们可以很直观的从上面的三个实例中看出这三种方法的区别。三种方法中,我个人认为使用指令最好。
学习了namespace的相关知识后,我们又想到了一个问题,面对不同的用户,我们需要不同的Interfaces,我们要怎样才能满足所有的users呢?
我们很自然的想到多重界面的概念:面向不同的用户,我们为他们提供不同的接口,以不同的界面呈现给他们。
我们可以通过定义不同的namespace,但实际上使用同样的Implementation,以减少不必要的依赖。
以下实例:
通过以上的学习,我们可以总结出有关namespace的要点:
1. Namespace引入了成员和接口的概念。
2. 成员可以是数据,也可以是函数。
3. 成员的概念将来在类的概念中还会出现。
4. 接口的概念是由函数的非定义声明发展而来的,注意对照二者(接口与非定义声明)的异同。
5. 约束符的引入,使得Namespace的接口和实现能够分离。这种符号将来在类的实现中还要遇到。
接下来,我们来实现Namespace版的Desk Calculator
#include<cctype>
#include<iostream>
#include<map>
#include<string>
using namespace std;
namespace error_hand{
int no_of_errors;
double error(const string& s){
no_of_errors++; cerr<<"error:"<<s<<endl;
return 1;
}using namespace error_hand;
nemespace Lexer{
enum Token_value{ //枚举类型的函数,建立了一个符号表,让这些终结符与字符建立了对应,方便维护。
NAME,NUMBER,END,PLUS='+',MINUS='-',MUL='*',DIV='/',PRINT=';',ASSIGN='=',LP='(',RP=')'
};
Token_value curr_tok=PRINT; //一个状态,代表了目前的token是什么类型。与各个函数都有联系。类似于一个flag。初始赋值为PRINT。PRINT=";" 意思是一个操作完成了,等待下一个操作。
double number_value; //全局变量:定义数字。
string string_value; //全局变量:定义字符串。
Token_value get_token() //辨别输入的东西是什么类型的:1.符号表中的 2.数字 3.字符 4.其他类型(错误),都返回curr_tok。
{
char ch=0;
cin>>ch;
switch (ch) {
case 0:
return curr_tok=END; //返回curr_tok 值为end 意思是结束这个操作
case ';':case '*':case '/':case '+':case '-':case '(':case ')':case '=':
return curr_tok=Token_value(ch); //返回curr_tok 强制转换值为枚举型:把char转化为Token_value中的枚举//(条件是那个字符能转换成Token_value中有的终结符对应的字符)
case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9':case '.':
cin.putback(ch);
cin>>number_value;
return curr_tok=NUMBER; //返回curr_tok 值为NUMBER:数字
default:
if (isalpha(ch)) { //isalpha函数判断是否是字母。C++库自带
cin.putback(ch);
cin>>string_value;
return curr_tok=NAME; //返回curr_tok 值为NAME:字符
}
error("bad token");
return curr_tok=PRINT; //若都不是,则输出error(),返回PRINT。
}
}
namespace Parser{
double prim(bool);
double term(bool);
double expr(bool);
}
double Parser::prim(bool get) //操作
{
if (get) get_token();//如果有东西 那就调用get_token 辨别是什么类型的东西 返回curr_tok(类型为Token_value,里面有枚举的各种终结符)
//(值为NUMBER NAME 等各种枚举的终结符)
switch (curr_tok) {
case NUMBER: //为数字型
{ double v=number_value;
get_token();
return v;
}
case NAME: //为字符型
{ double& v=table[string_value];//引用定义,为了要留住这个字符所对应的空间,故使用引用,让v获得值,table[string_value]守住空间。
if (get_token()==ASSIGN) v=expr(true);//若是"=",则运行expr(1)=term(1)=prim(1)等于递归了
return v; //返回v
}
case MINUS: //为负数
return -prim(true);
case LP: //为括号
{ double e=expr(true);
if (curr_tok!=RP) return error(") expected");
get_token();
return e;
}
default:
return error("primary expected");
}
}
double Parser::term(bool get)
{
double left=prim(get); //定义left为prim函数的值,prim函数返回结果为真数字,真字符
for (;;)
switch (curr_tok) { //判别现在输入的是什么类型,由curr_tok当变量(因为curr_tok带值,且值是对应的类型)
case MUL: //乘法运算
left*=prim(true);
break;
case DIV: //除法运算
if (double d=prim(true)) {//分母不为0
left/=d;
break;
}
return error("divide by 0");//分母为0
default:
return left; //返回prim(get),也就是说不是乘除运算,故传到prim中进行基础运算
}
}
double Parser::expr(bool get)
{ //加减运算
double left=term(get); //定义left为term函数的值,term函数返回的结果为prim,为真数字真字符。
for(;;)
switch(curr_tok) {
case PLUS:
left+=term(true);
break;
case MINUS:
left-=term(true);
break;
default:
return left; //若不是加减运算 return term函数=return prim函数。结果为真数字真字符
}
}
using namespace Parser;
int main(){
table["pi"]=3.1415926535897932385;
table["e"]=2.718284590452354;
while (cin) {
get_token(); //取得输入
if (curr_tok==END) break;
if (curr_tok==PRINT) continue;
cout<<expr(false)<<endl;
}
return no_of_errors;
}
我们能够发现,将使用全局使用指令时,可以方便许多,但在其他方面最好避免使用。