在可管理C++中封装值类型

  • 来源: 赛迪网 作者: 若水   2008-04-26/16:27
  • 有些时候,一些很简单的事情实现起来并不容易。例如,我们想让一个变量值显示在屏幕上。也许你知道在C++中怎么做,但在VC++6中,要用下面的方法实现:

     

     

     

    int x = 3;
    cout << "x is " << x << endl;

    就这么简单。不论你学的“C++入门课”怎样,我打赌你能发现的与这两行代码相似的东西不足你在课程中学到的10%,对吗?

     

    输出到屏幕

    现在,如果要在VC++.NET中创建可管理的C++程序该怎么做?下面是我创建的main():

    int _tmain(void)
    {
        // TODO: Please replace the sample code below 
        //       with your own.
        Console::WriteLine(S"Hello World");
        return 0;
    }

    现在你可以把应用Cout的代码拷贝到main()中,在加入了include声明后,就可以执行:

    #include <iostream.h>
    // ...
        Console::WriteLine(S"Hello World");
        int x = 3;
        cout << "x is " << x << endl;

    这时,你会看到一个警告:

    warning C4995: '_OLD_IOSTREAMS_ARE_DEPRECATED': 
    name was marked as #pragma deprecated

    解决的方法:借用STL中的IO流的代码,并且导入std 名称空间:

    #include <iostream>
    using namespace std;

    现在编译并运行这段代码。但是让我不解的是,在程序中发现作为cout应用的Console::WriteLine()。另外,Console::WriteLine很整洁。就像printf,它在字符串中使用占位符显示变量值应该放到哪。下面是一个c#控制程序中的代码:

    int x = 3;
    Console.WriteLine("x is {0}",x);

    {0}是一个占位符,第二个参数的值截止到占位符出现的位置。因此我想像在c#中一样,在可管理的c++程序中一直使用Console::WriteLine。但是如果你把代码直接拷到c++程序中,并将.改为::,程序不能通过编译。错误显示为:

    error C2665: 'System::Console::WriteLine' : none of the 19 
    overloads can convert parameter 2 from type 'int'
            boxpin.cpp(7): could be 'void System::Console::WriteLine(
    System::String __gc *,System::Object __gc *)'
            boxpin.cpp(7): or       'void System::Console::WriteLine(
    System::String __gc *,System::Object __gc * __gc[])'
            while trying to match the argument list '(char [9], int)'
    #p#分页标题#e#

    现在,我固执地希望c++能做其他.net语言能做的所有事情,甚至更多。为什么这么简单的办法行不通?没别的办法,看看错误提示吧。我给第二个参数赋一个整数值,它就像一个指针。事实上,是一类指向System::Object的指针(当然,还有由其衍生出的类),一类指向__gc object的指针。而这个整数值两种都不是。你可以试着传递&x值,而非x,那样至少是一个指针,但还是无济于事。

    WriteLine()需要的是一个指向对象的指针。你不能直接将整数值传递给WriteLine(),因为它是(处于整体性的考虑)用来处理指向垃圾收集对象的指针,而不是其他的。为什么?基本类库中的所有内容都是针对对象设计的,因为他们都可有成员函数——并不是所有的.net语言都支持模式化或是过载模式化运算符的思想。比如,由System::Object继承下来的所有对象都有一个ToString() 方法。你不想为一个非对象的整数写一个类,然后又写一个ToString()来处理它,在每次将它传递给像WriteLine()基本类库中的方法的时候,还要把它放入(或取出)。这时,你怎么把整数传递给WriteLine()?

     

    _box关键字

    可管理的c++也被称为c++可管理的扩展。扩展是指额外关键字,都是以双下划线开头,并被增加到语言中。和其他所有以双下划线开始的关键字一样,他们的编译器是特定的——不要在vc++6和其他产商的编译器中试用。在编译WriteLine() 时,你只会在错误信息中看到_gc。它代表着垃圾收集并且指向一个依靠堆栈类型,并由运行时间控制的对象。_box关键字可以解决我在上面提到的,如何将整数传给基本类库方法,它得到的是System::Object _gc而不是一个整数。下面是它的使用方法:

    Console::WriteLine("x is {0}",__box(x));

    封装一个值的类别就是把值放到一个临时对象(这个对象是System::Object继承类的实例,存于垃圾收集堆)中,然后再把临时对象的地址传递给方法调用。原有变量中所有的东西都被拷入临时对象中,这个对象提供WriteLine()需要的所有功能。__box关键字意味着值类型和可管理类型都适合于基本类库提供的所有服务。

     

    封装的替代办法

    装箱允许你在期待指向可管指针的基本类库的方法中使用值类型和可管理类型。这自然产生了一个问题:在值类型和可管理类型中究竟有什么区别?可管理类型存于垃圾收集堆中,并且被运行时间所管理。下面是一个例子:

    __gc class Foo
    {
      // internals omitted
    };
    
    // ...
    Foo* f = new Foo();

    FOO类是可管理类型。你不能在堆栈上创建Foo f2;这样的实例:

    如果你已经有一个类(也许是从以前的.net程序中获得),它一定不是一个可管理类型。它没有_gc关键字。当然,你可以加上关键字(假设类符合成为一个可管理类型的所有条件),但接下来你要找到所有创建类实例的地址,还有保证他们是在堆上创建的实例,比如:

    OldClass* poc = new OldClass(); //maybe some parameters 
                                    //to the constructor

    你要记住,在代码中调用类方法的每一处,都要把.改为->。保持原来的类型,这样你可以按照你的意愿在堆栈或未管理的堆上分配实例:

    class notmanaged
    {
    private:
      int val;
    public:
      notmanaged(int v) : val(v) {};
    };
    
    // ...
      notmanaged nm(4);
      notmanaged *p = new notmanaged(5);

    这并不难:这就是还没发布加入可管理扩展的Visual C++ .NET版本之前,C++的样子。如果你想把那些实例的其中一个传递给原来的WriteLine():

    Console::WriteLine("notmanaged holds  {0}",nm);
    #p#分页标题#e#

    你会得到和以前一样的错误信息:WriteLine() 使用老式风格类时并不比使用整数时过。你会想“我学到了一个新关键字,我知道该怎么做了”。

    Console::WriteLine("notmanaged holds  {0}",__box(nm));

    这会给你带来更模糊的信息"只有值类型能被装箱",未管理不是一种值类型吗?它的确不是可管理类型,但是它不是从基本类System::ValueType继承下来的(这些是有必要装箱的)。现在,有一个易用的值关键字,能够让类继承System::ValueType并可以被封装,但基于类和结构的考虑我并不认为封装是明智之举。你不会想让类或结构中的数据都被输出到屏幕上。你想对你的类被字符串来表示的方式进行些控制——说得明确一点,以System::String的方式。换言之,你想写个函数,以便你能保留对你的类的行为的控制。那么为何不加一个ToString() 方法?

    class notmanaged
    {
    private:
      int val;
    public:
      notmanaged(int v) : val(v) {};
      String* ToString() {return __box(val)->ToString();}
    };
    // ...
    notmanaged nm(4);
    Console::WriteLine("notmanaged holds  {0}",nm.ToString());
    notmanaged *p = new notmanaged(5);
    Console::WriteLine("notmanaged pointer has {0}",p->ToString());

    注意在ToString()方法中对__box 的灵活运用,以使它能够返回一个指向可管理类型System::String的指针。你能够很容易的将这个方法扩展到一个有多种成员变量的类中。当然,你能够随心所欲的命名方法,但是将其命为ToString能够帮助其他的.net程序员,因为这个名字在基本类库中意味着一个以System::String表达类内部的方法。

     

    拆箱

    但现在为止,你仅仅看到了_box关键字在临时环境中的应用,只是传递给一个方法。但是你可以创建一个生命期很长的指向可管理类型的指针,比如:

    __box int* bx = __box(x);

    记住这是一个复制版很重要。若你改变了它,其他的原有部分并不会变。以下的四行:

    __box int* bx = __box(x);
    *bx = 7;
    Console::WriteLine("bx is {0}", bx);
    Console::WriteLine("x is still {0}", __box(x));

    产生如下结果:

    bx is 7
    x is still 3

    如果你想把复制版改回原先的状况,只需要废除指针:

    x = *bx;

    则x会是你给装箱的复制版所赋的新值(在此例中是7)。

     

    小结

    只为了将易于处理的整数输出到屏幕上,我们就走了这么长的一段路。理解可管理类型和未被管理类型间的区别对你在清楚地使用可管理C++很重要。将未被管理类型装箱是将其转化为可管理指针的有效办法,但当未被管理类型是一个类的时候,记住你能够控制它,并且能够创建可管理指针,无论如何你选择这样做——还有一个叫做ToString的成员函数是一种卓越的解决之道。不用任何关键字你就能够拆箱---只是将值取出来。那么就不要让讨厌的错误破坏了你探索基本类库的乐趣! #p#分页标题#e#


    评论 {{userinfo.comments}}

    {{money}}

    {{question.question}}

    A {{question.A}}
    B {{question.B}}
    C {{question.C}}
    D {{question.D}}
    提交

    驱动号 更多