美文网首页
C++中的namespace机制的一些个人理解

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

作者: 谨慎的野心家 | 来源:发表于2018-04-03 20:24 被阅读0次

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

    }

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

    相关文章

      网友评论

          本文标题:C++中的namespace机制的一些个人理解

          本文链接:https://www.haomeiwen.com/subject/owmchftx.html