C++中的namespace机制的一些个人理解

昨天学习了《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;

}

可以将它看成五个部分:

图1 Desk Calculator 组成结构

这五个部分的功能可细分如下:


图2 Desk Calculator 结构功能

再者,我们可以只了解一个函数的接口的具体定义,而不了解它是怎样实现的,就能够很好地使用它。例如我们熟悉的printf函数,我们会使用它,但我相信大多数人不会去看printf的源码。

类似地,即使程序的一个部件是由多个函数组成,或者其中既有自定义类型,也有全局变量,还有函数,但我们都可以这样来设想:如果这样的部件也象函数那样有一个起包装作用的接口,也同样可以只需要了解接口而不需要了解实现,就能够很好地使用它。

因此,我们可以将Desk Calculator 中的细节隐藏起来只显露出使用部分,这样代码简洁又美观。

图3 Desk Calculator的模块化

若程序中的一个部件具有明确的边界,能够实现接口与实现的分离,并对它的用户而言在使用时只需关心其接口而不管其实现者,就叫做模块(Module)

实现模块的接口与实现的分离,需要程序设计语言提供相应的支持机制。C++提供的支持机制是:

(I)  Namespace

(II) Class

模块用接口隐蔽了数据和函数的处理细节(这也称作封装Encapsulation),使得模块可以在保持接口不变的前提下,改变数据的结构和函数的处理细节。

接下来我们引入Namespace的概念

(i)Namespace是一种表现逻辑聚集关系的机制。换句话说,如果一些声明在逻辑上都与某个准则有关,就可以把这些声明放入一个共同的 namespace,以表现这一事实。

(ii)同一 namespace 中的声明在概念上属于同一个逻辑实体。

再从Desk Calculator实例来说:

我们可以将与某个准则有关的函数都聚集起来,例如我们在上面分好的模块中,里面的函数都可以认为是属于同一个namespace,因此可以有以下方式:

图4 Parser模块


图5 Lexer模块

我们可以看到,这种利用namespace将一类函数聚集起来放在一个模块中的行为,我们就可以称之为模块化

Modularization。namespace是一个名字空间,它拥有封装的特性,因此它也代表了一个模块。

但我们同时也可以看到,将函数定义在namespace中,似乎这个结构也变得模糊起来了,并没有很好的完成我们的预期。因此,我们有另一种方法将界面(Interfaces)与实现(Implementations)分离开来。


图6 界面与实现分别定义

我们要关注一点:实现 namespace 的接口与实现分离的关键,是在其实现部分中出现的成员被该 namespace 的名字所约束(qualified),这样的约束通过约束符( qualifier ::)来表示。

图7 Parser 函数

由于各个 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;

}

我们能够发现,将使用全局使用指令时,可以方便许多,但在其他方面最好避免使用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容