⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 chapter 11 constructors and destructors.htm

📁 英文版编译器设计:里面详细介绍啦C编译器的设计
💻 HTM
📖 第 1 页 / 共 5 页
字号:
initialization. It has a meta-constructor. An array of such classes would also 
require a meta- constructor in order to initialize each one of the elements of 
the array. A record containing an instance of such class would also require a 
meta- constructor.
<P>Symbolically, this means that meta-constructor information is stored at the 
block level. As you might recall, a block can be (aside from a function) either 
a record, an array, or a class. If one structured data type includes another 
that has a meta-constructor, then it, too must have a meta-constructor of its 
own that calls the nested structure's meta-constructor. Several 
meta-constructors can be "chained" togetner in this way, giving the advantage 
that a structure does not need to know about the intimate details of the 
low-level construction of any of its nested members. It only needs to invoke the 
meta-constructor for each nested structure that has one, and then take care of 
its own initialization--if it has any.
<P>As an example, suppose we have the following declaration: <PRE>      type
        C is class
          ...         // suppose this class has some default constructor
        end class;

        R is record    // make a record containing class C
          cc: C;
        end record

        A is array [1 to 10] of R;   // an array or 10 R records

      var
        X: A;
</PRE>The variable X is an array of 10 records. Each record contains a class 
that has a constructor that must be called. The chaining of the 
meta-constructors works like this: C has a meta-constructor that does low-level 
initiailzation of C and only C. It also will call the default constructor for C. 
Now anyone can build an instance of C. In order for there to be an instance of 
record R, the computer needs to initialize the field, cc. It does this by making 
a meta-constructor for R that simply calls the meta-constructor for cc. Now 
anyone can make an instance of R. A similar thing happens for array A. This 
array has a meta-constructor that calls the meta-constructor for R, for each of 
its elements. Most non-class meta-constructors merely chain the call to another 
meta-constructor, i.e., array A calls the meta-constructor for R on all 10 of 
its elements, the meta-constructor for record R calls the meta-constructor for 
class C for its one field, cc. Finally, the meta-constructor for class C does 
the real work.
<P><!-------------------------------------------------------------------------------->
<H2>11.5 Class Destructors</H2><!-------------------------------------------------------------------------------->In 
some ways, destructors are like constructors, and in other ways they are 
different. In this section, we will talk about destructors. For the most part, 
they are simpler than constructors. Some of the ways destructors are different 
are: 
<UL>
  <LI>There is only one destructor per class. The destructor is an automatic 
  method that is invoked whenever the object goes out of scope.
  <P></P>
  <LI>There is a meta-destructor, but its function is different. Unlike 
  constructors, there is no need for a low-level post-destruction phase. At the 
  stage of destruction, we need only worry about destructing arrays of classes 
  and records containing classes, or calling the destructors for member classes.
  <P></P>
  <LI>Destructors have no arguments. There are no named destructors that may 
  take additional arguments. This is merely a design decision for ease of 
  implementation.
  <P></P>
  <LI>Virtual destructors are possible, and are very necessary. This is to make 
  sure that the destruction of an object through a pointer to a base class also 
  destroys any and all deriving classes of that particular instance. 
  <STRONG>Destructors in SAL are virtual by default.</STRONG> Remember, there is 
  no way to un-virtual a destructor. <STRONG>In other words, in SAL, all 
  destructors are virtual.</STRONG> In C++ they are not, since virtual calls are 
  slower than normal method calls. If a class in C++ is not meant to be 
  subclassed and it has no base classes, always calling a virtual destructor for 
  such an object would be wasteful of CPU time.
  <P></P></LI></UL>In each of these ways, destructors are not necessarily opposite 
constructors. They are merely different. In other ways, destructors are very 
much like constructors: 
<UL>
  <LI>Destructors have no return type for the same reason as constructors. Due 
  to the syntax of the language, a return type is not possible.
  <P></P>
  <LI>For the same reagsons as constructors, you cannot take the address of a 
  destructor. If you wish to do so, use a wrapper function, and take the address 
  of it, instead.
  <P></P>
  <LI>Destructors are not inherited. However, they can be virtual, and like 
  constructors, it is the job of the destructor of the most derived class to 
  call the destructor for all of its immediate base classes.
  <P></P>
  <LI>The compiler will generate default destructors if a class inherits a base 
  class that has a destructor.
  <P></P>
  <LI>The <TT>super</TT> keyword can be used to invoke the destructor for base 
  classes, and can even specify the order of immediate base class destruction. A 
  destructor may only call the destructors for the base classes from which its 
  class is immediately derived.
  <P></P>
  <LI>Virtual functions do not exist in destructors for the same reason that 
  they do not exist in constructors. By their very nature, a virtual method 
  invocation will cause portions of the class to be modified that will already 
  have been destroyed. </LI></UL>In two ways specifically, destructors are exactly 
opposite their counterparts: 
<OL>
  <LI>Destruction occurrs derived-class first. Execution of the destructors is 
  in pre-order. That means that the body of the destructor of the most-derived 
  class will be executed before the destructors for its immediate base classes 
  are called. The <TT>super</TT> keyword may appear only at the end of a 
  destructor body, after all other executable statements.
  <P></P>
  <LI>Neglecting the <TT>super</TT> statements, destructors are called for base 
  classes in the opposite order that they are inherited. If class A extends B 
  and then C, construction will be in the order of B, C, then A, and destruction 
  will be in the order of A, C, and finally B.
  <P></P></LI></OL>Thus, in most respects, sal destructors are very much like 
those of C++. The only real difference is that SAL does not allow the programmer 
to invoke destructors explicitly. This means that it is impossible for a class 
to destroy itself. This is more for simplicity of design, SAL being a student 
compiler and not a professional product. Also, in SAL, <TT>self</TT> (the SAL 
counterpart to <TT>this</TT> in C++ and Java) is a reference and not a pointer. 
In fact this is precisely the reason that <TT>this</TT> is a pointer in C++ and 
Java, so that a class may destroy itself.
<P>As we said earlier, there is no need for a meta-destructor to do any sort of 
low-level unformatting of memory. Such a thing would be useless. However, sense 
there may be arrays of classes or records that have instances of classes as 
their fields, there is a need for a meta-destructor of some sort. It turns out 
that in the SAL compiler, the meta-destructor and the class destructor are one 
in the same.
<P>meta-destructors for arrays and records are chained in the same way as meta- 
constructors. Using the previous example in section 11.4, there is a meta- 
destructor for array A that iteratively invokes the meta-destructor for record R 
for each of its ten elements. Record R in its turn invokes the class destructor 
for cc. In this example, there are a total of three (meta-) destructors.
<P>A real concern in compilers is how to assure that destructors always get 
called. It is easy enough to call constructors on local and global variables; 
there is always one single point of entry into any piece of code. Modern 
structured programming practices have guaranteed that much. However, there still 
remain multiple points of exit. In SAL a function may execute a <TT>return</TT> 
statement at any time within its body, and any function can have multiple 
<TT>return</TT> statements. This presents a problem in guaranteeing that the 
destructor gets called.
<P>The solution can be handeled one of three ways. The easiest way is to put the 
code to call the destructor inline with each return statement. So, if a function 
has three local variables, all of which are classes that have a destructor, then 
every return statement will be preceeded to a call to the destructor for those 
classes. A slightly better method would be to make use of a subroutine that 
calls all of the destructors. A subroutine is basically a lightweight procedure 
(the call frame is preserved, and local variables do not change). Each return 
statement would be preceeded by a jump to the subroutine, where the destructors 
would be called. The subroutine would return to the point after which it was 
called once its task was completed, which would be the return.
<P>A goto is probably the fastest method. When the compiler sees a return 
statement, it processes the argument (if there is one), and instead of emitting 
a RTN instruction, it emits a forward jump sequence, and saves the offset in a 
table of return-fixups. When the last statement of the procedure or function has 
been compiled, the compiler then goes back through the list of return-fixups, 
and sets them to the next address in the code array. This effectively makes one 
single point of return. From this point onward, the compiler emits code to clean 
up local variables and call their destructors.
<P>To better explain, let us convert some high-level SAL code with multiple 
return statements into code that contains a single point of exit: <PRE>      proc foo(): int;
        var
          x: ClassX;

      begin

         // do something here

         if /*some condition*/ then
            return ERROR_CASE;
         end if;

         // do something else

         if not /*some condition*/ then
            // do some special case

            return SPECIAL_CASE;
         end if;

         return GENERAL_CASE;
      end proc;
</PRE>
<P>If SAL were to have a goto, here is how we would use it to make the previous 
have a single point of exit: </P><PRE>      proc foo(): int;
        var
          x: ClassX;
          retval: int;

      begin
        // Initialize the return value
        retval:= 0;

        // do something here

        if /*some condition*/ then
          retval:= ERROR_CASE;
          goto foo_exit;
        end if;

        // do something else

        if not /*some condition*/ then
          // do some special case

          retval:= SPECIAL_CASE;
          goto foo_exit;
        end if;

        retval = GENERAL_CASE;
        goto foo_exit;

      foo_exit:
        x.~x();  // Note that this is not exactly legal syntax, either.

        return retval;  // Finally!!
      end proc;
</PRE>
<P>Although such high level coding practices would be frowned upon by purists, 
we don't care what our code looks like underneath, at the machine level. Our 
only real concern is that it be reliable and fast. Actually, coding like this in 
C used to be standard practice when allocating a long list of inter-dependant 
system resources. Now the standard practice is for high level programmers to use 
exceptions. However, under the hood, the compiler still uses gotos in order to 
insure that all locally allocated classes get cleaned up. We never see them at 
the language level; it is all transparent to us. SAL VM assembly code for the 
above procedure would look something like this: </P><PRE>        ENTR      8                  ; Allocate local memory

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -