📄 chapter 11 constructors and destructors.htm
字号:
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 + -