The 6th chapter: The semantic study in the implementation period
Imagine that we have the following simple formula:
if (yy ==xx.getvalue ()) ...
where xx and yy are defined as:
X xx;
Y yy;
Class y is defined as:
Class y{
Public:
Y ();
~y ();
Bool operator== (const y&) const;
//...
};
Class x is defined as:
Class x{
Public:
X ();
~x ();
Operator Y () const;
X GetValue ();
//...
};
First, let's determine the real entity that the equality (equal sign) operator refers to. In this case, it will be resolved (resolves) as the "Y member entity of the overloaded". Here is the first conversion of the equation:
If (yy.operator== (Xx.getvalue ()))
The equality (equals) operator of y requires a parameter of type Y, whereas GetValue () returns an object of type X. If there is no way to convert an X object to a Y object, then the equation is wrong. In this example, X provides a conversion operator that converts an X object to a Y object. It must be performed on the return value of the GetValue (). Here is the second conversion of the equation:
If (yy.operator== (Xx.getvalue (). operator Y ()))
Everything that has happened so far is the "gain" operation of our program code by the compiler based on the implied semantics of class. If we need to, we can also explicitly write that formula. No, we don't recommend that, but if you do that, you'll make the compiler a little bit faster.
Although the semantics of the program is correct, when its educational nature is not yet said to be correct. Next we have to produce a temporary object to place the value returned by the function call:
N produces a temporary class X object to place the return value of the GetValue ():
X Temp1=xx.getvalue ();
N produces a temporary class Y object, placing the return value of operator Y ():
Y Temp2=temp1.operator y ();
n produces a temporary int object, placing the return value of the equality (equal sign) operator:
Int temp3=yy.operator== (TEMP2);
Finally, the appropriate desturctor will be implemented on every temporary class object. This causes our equation to be converted into the following form:
The following is a conditional sentence if (Yy==xx.getvalue ()) ... The conversion
{
X Temp1=xx.getvalue ();
Y Temp2=temp1.operator y ();
Int temp3=yy.operator== (TEMP2);
If (Temp3) ...
Temp2.y::~y ();
Temp1.x::~x ();
}
Oh, there seems to be a lot of code. This is a difficult thing in C + +: It is not easy to see the complexity of an expression from the program code.
Construction and deconstruction of 6.1 objects
In general, constructor and destructor are placed as you would expect:
C + + pseudo code
{
Point Point;
Point. Point::P oint () is typically placed here
...
Point. Point::~point () is typically placed here
}
If there is more than one departure point in a section (the area enclosed by {}) or a function, the situation is slightly confusing. destructor must be placed before each departure point (when object is still alive), for example:
In this example, the destructor of point must be produced before the return operation of the switch instruction four exits. It is also likely to be generated before the end sign (closing curly brace) in this section-even if the structural discovery of the program analysis will never go there.
In general, we would place the object as far as possible near the program section where it was used, which would save unnecessary objects from manipulating and destroying operations.
Global Objects
If we have the following program fragment:
matrixidentity;
Main ()
{
Identity must be initialized here
matrixml=identity;
...
Return0;
}
C + + guarantees that the identity must be constructed before the main () function is used for the first time, and the identity is destroyed before it ends. So-called globalobject like identity if there are constructor and destructor, we say it requires static initialization and memory release operations.
All globalobjects in the C + + program are placed in the datasegment of the program. If you explicitly assign a value to it, object will take the value as the initial name. Otherwise, object is configured with 0 memory content. So in the following code:
intv1=1024;
INT v2;
Both V1 and V2 are configured with a DATASEGMENT,V1 value of 1024,v2 value of 0 (this is slightly different from C, and C does not automatically set the initial value). In C, a globalobject can only be set to an initial value by a constant expression, which can be evaluated at compile time. Of course, constructor is not a constant expression, although classobject can be placed in datasegment at compile time and content is 0, constructor is not implemented until program activation (startup). You must evaluate an initialization expression for object placed in Programdata segment, which is why an object needs to be statically initialized.
When Cfront is still the only C + + compiler, and cross-platform portability is more important than efficiency considerations, there is a portable but expensive method of static initialization (and memory release), which I call Munch. The following is the implementation of the Munch policy:
1. Generate a __sti () function for each file that requires static initialization, with the necessary constructor call operation or Inlineexpansions.
2. In a similar case, a __std () function is generated in each file that requires a static memory release operation.
3. Provides a set of RuntimeLibrary "Munch" functions: a _main () function (used to invoke all __sti () functions in the executable file), and an exit () function that invokes all the __STD () functions in a similar manner.
As shown in the figure:
Cfront a _main () function call operation in your program as the first instruction of the main () function. The exit () and Clibrary exit () are different here. In order to link the former, the C++standard library must be developed in the Cfront cc command.
The last question to be solved is how to collect the __sti () functions and the __STD () functions of each objectfiles in a program.
Our solution is to use the NM command. NM will print the symbol table item for object file. An executable file is generated from the. o file, and NM will be executed on the executable file. Its output is imported ("Pipedinto") Munch the program. The Munch program "chews" the name in the symbol table, searches for a name that begins with a __sti or __STD, and then adds the function name to the jump table of the STI () function and the STD () function. It then writes the table to a small programtext file, and then the CC command is reactivated to compile the file of the content table. The entire executable file is then relink. _main () and exit () then visit each of the tables and call each item in turn (representing a function address).
Cfront2.0 version does not support static initialization of Nonclass object: In other words, the C language limit remains. So, as in the example below, each initialization is represented as illegal:
Externint i;
All of the following operations require static initialization, which is not legal until cfront2.0.
Int J =i;
int* pi= New Int (i);
Doublesal = Cpmpute_sal (Get_employee (i));
Support for static initialization of the Nonclass objects is, in part, a by-product of virtual base classes support. How Virtual base classes is involved in this topic. Oh, accessing virtual base class Subobject with a derived class of pointers or reference is a nonconstant expression that must be evaluated at the execution time. For example, although the following fragment of the program is known during the compiler period:
Constant expression
vertex3d* pv=new Pvertex;
point3d* P3D=PV;
The subobject of its virtual base class point may change in each derived class, so it cannot be set at compile time. The following initialization actions:
Point is a virtual base class for Point3D
The initialization operation of PT requires some form of actuator evaluation
point* Pt=p3d;
The compiler is required to provide an internal extension to support static initialization of class object (at least the pointer and references of class object). For example:
point* pt=p3d->vbcpoint; This is an evaluation done during the implementation period.
Provide the necessary support to cover all Nonclass objects.
There are some drawbacks to using statically initialized objects. For example, if the exception handling is supported, those objects will not be able to be placed within the try section. This may be particularly unacceptable for statically invoked constructors. Because any throw operation will necessarily trigger the default terminate () function of the exception handling library. Another disadvantage is to control the complexity of the "need to cross modules to do static initialization" objects dependent order.
locally static objects (local static Objects)
Suppose we have the following program fragment:
Const matrix&
Identity () {
Static Matrix mat_identity;
//...
return mat_identity;
}
What semantics is guaranteed by the local static class object.
1.mat_identity constructor must be performed only once, although the above function may be invoked multiple times.
2.mat_identity destructor must be performed only once, although the above function may be invoked multiple times.
One of the strategies of the compiler is to unconditionally construct objects when the program is actually (startup). However, this causes all the local static class objects to be initialized at the start of the program, even if the function they are in is never invoked. Therefore, it is a good practice to construct the mat_identity only when the identity () is invoked (which is now mandatory in C + + standard). What should we do?
Here's how it's done in Cfront: First, import a temporary object to protect the mat_identity initialization operation. The first time the identity is processed, the temporary object is evaluated as false, so constructor is invoked, and the temporary object is changed to True. This solves the problem of construction. On the opposite end, destructor also needs conditional execution and mat_identity, but only when mat_identity has been constructed. It is simple to judge whether Mat_identity is constructed. If the temporary object is ture, it means the structure is good. The difficulty is that because Cfront produces C code, mat_identity is still local to the function, so there is no way to access it in a static memory release function (STD). The solution is somewhat bizarre: Remove the address of the local object. (Because object is static, its address will be converted to the data segment within the program used to place the global object in downstream component), the following is the Cfront output:
Finally, destructor must be called conditionally in the static memory release function associated with the text program file:
Keep in mind that the use of pointers is unique to Cfront: However, the condition is that all compilers need to deconstruct.
Array of Objects
Suppose we have the following array definition:
Point KNOTS[10];
Need to finish something. If point does not define a constructor and does not define a destructor, then our work is no more than creating an "array of built-in types."
Point does, however, define a default destructor, so this destructor must be carried out on a rotational basis with each element. This is generally achieved through one or more runtime library functions. In Cfront, a function named Vec_new () produces an array of class object constructs. function types are usually as follows:
void*
Vec_new (
void* array,//array start address
size_t elem_size,//size of each class object
Int Elem_count,//number of elements in array
Void (*constructor) (void*),
Void (*destructor) (Void*,char)
}
The constructor and destructor parameters are the function pointers for the default constructor and default destructor of this class. The address of the parameter array that is not a named array is 0. If it is 0, then the array will be configured with the new operator of the application on the heap.
The parameter elem_count represents the number of elements in the array. In Vec_new (), constructor execute with elem_count elements. For compilers that support exception handling, the provision of destructor in vec_new is necessary. Here is the Vec_nec () invoke action that the compiler might have done for our 10 point elements:
Point KNOTS[10];
Vec_new (&knots,sizeof (point), 10,&point::P oint,0);
If point also defines a destructor, when Knots's life is over, the destructor must also be performed with those 10 point elements.
void*
Vec_delete (
void* Array,
size_t Elem_size,
Int Elem_count,
Voie (*destructor) (void*, Char)
}
If the programmer provides one or more obvious initial values to an array of class objects, how would you like to:
Point knots[10]={
Point (),
Point (1.0,1.0,0.5),
-1.0
};
Vec_new () is no longer necessary for elements that have an obvious initial value. For elements that have not yet been initialized, Vec_new () is implemented in the same way as an array of class elemetns, which does not have the explicit initialization list. So the previous definition is likely to be converted to:
Point KNOTS[10];
Point::P oint (&knots[0]);
Point::P oint (&knots[1], 1.0, 1.0, 0.5);
Point::P oint (&knots[2],-1.0, 0.0, 0.0);
7 Elements initialized with Vec_new
Vec_new (&knots+3, sizeof (point), 7, &point::P oint, 0);
defaultconstructors and Array
This is not possible if you want to remove a constructor address from the program. Of course, this is what the compiler does when it supports vec_new (). However, activating constructor through a pointer will not (not be allowed) access to default argument values.
For example, before cfront2.0, declaring an array of class objects means that this class must not declare constructors or a default constructor (without arguments). A constructor can not go one or more of the default parameter values. This is counterintuitive and can lead to the following mistakes. Here is a statement of the plural library in cfront1.0, and you can see the error.
Class complex{
Complex (double=0.0,double=0.0);
...
}
Under the language rules of the time, the user of the complex function library could not declare an array composed of complex class objects.
Once again, let's take a moment to consider how to support the following sentences:
Complex::complex (double=0.0,double=0.0);
But the programmer writes:
Complex C_ARRAY[10];
, and the compiler eventually needs to invoke:
Vec_new (&c_array,sizeof (complex), 10,
&complex::complex,0);
How the default parameters can be useful for vec_new ().
Obviously, there are a number of possible implementation methods. The method used in Cfrotn is to produce an internal stub constructor with no parameters. Invokes the constructor provided by the programmer within its function and specifies the default parameter value explicitly in the past (it cannot become a inline because the address of constructor has been obtained):
Internally generated stub constructor
Used to support the construction of arrays
Complex::complex ()
{
Complex (0.0,0.0);
}
The compiler has again violated an obvious language rule: class now supports two constructors with no parameters. Of course, stub entities are produced and used only when the class objects array is actually joined.
6.2 New and delete operators
The use of the operator new appears to be a single operation, like this:
int *pi=new int (5);
But in fact it is done by the following two steps:
1. Configure the required memory by using the appropriate new operator function entity:
Calling the new operator in a function library
int *pi=__new (sizeof (int));
2, to configure the object to set the initial value:
*pi=5;
Further, the initialization operation should be performed after the memory configuration succeeds (via the new operator):
int* Pi;
If (pi=__new (sizeof (int))
*pi=5;
The delete operator is similar when the programmer writes:
Delete Pi;
, if the value of pi is the 0,c++ language, the delete operator is not required to operate. Therefore, the compiler must construct a layer of protective film for this call:
If (pi!=0)
__delete (PI);
Please note that PI will not be automatically cleared for 0, so subsequent behavior like this:
The life of the object that Pi refers to is terminated by the delete. So any subsequent reference to PI will no longer guarantee good behavior and is therefore considered a bad procedural style. However, it is still possible to continue to use PI as a pointer, for example:
If (Pi==sentinel) ...
Here, the difference between using pointer pi and using PI refers to which life is over. Although the object on this address is no longer legal, the address itself still represents a legitimate program space. Therefore PI can continue to be used when only in limited circumstances.
Configuring a class object with constructor is similar. For example:
point3d* origin=new Point3D;
is converted to:
point3d* origin;
C + + pseudo code
If (origin=__new (sizeof (Point3D))
Origin=point3d::P Oint3d (Origin);
If you implement a exception handling, the conversion result may be more complex:
C + + pseudo code
If (origin=__new (sizeof (Point3D))
{
try{
Origin=point3d::P Oint3d (Origin);
}
CATHC (...)
{
Call the Delete library function to
Free memory configured for new
__delete (origin);
Upload the original exception
Throw;
}
}
The application of destructor is very similar. The following formula:
Delete origin;
will become:
If (origin!=0)
{
C + + pseudo code
Point3D:: ~point3d (Origin);
__delete (origin);
}
If, in the case of exception handling, destructor should be placed in a try section. Exception Handler calls the delete operator, and then throws the Exception again.
The general library is straightforward for the implementation of the new operator, but there are two ingenious things to consider:
Extern void*
Operator New (size_t size)
{
If (size==0)
size=1;
Void *last_alloc;
while (!) ( Last_alloc=malloc (size))
{
If (__new_handler)
(*__new_handler) ( );
Else
return 0;
}
return last_alloc;
}
Although it is legal to write:
New T[0];
But the language requires that every call to new must return a unique pointer. The traditional way to solve this problem is to return a pointer to a memory block that defaults to 1 byte (which is why the size in the program code is set to 1). Another interesting thing about this implementation technology is that it allows the user to provide a __new_handler () function that belongs to them. This is why every cycle calls __new_handler ().
The new operator is actually always done with the standard C malloc (), although there is no rule that this must be done. In the same case, the Delete operator also attaches importance to the standard C free () Completion:
Extern void
Operator Delete (void* ptr)
{
If (PTR)
Free ((char*) PTR);
}
for new semantics of arrays
When we write this:
int* P_array=new int[5];
, Vec_new () is not really invoked because its main function is to execute the default constructor on every element of the array of class objects. The new operator function is called:
int* p_array= (int*) __new (5*sizeof (int));
The same situation, if we write:
struct Simple_aggr{float f1,f2;
simple_aggr* P_aggr=new simple_aggr[5];
Vec_new () will not be invoked either. Why, then? Because SIMPLE_AGGR does not define a constructor or destructor, configuring an array and clearing the P_AGGR array is simply a matter of acquiring memory and freeing memory. These operations are more than sufficient to be completed by the new and delete operators.
However, if the class definition has a default constructor, some versions of Vec_new () are invoked to configure and construct an array of class objects. For example, this formula:
point3d* P_array=new point3d[10];
are usually compiled to:
point3d* P_array;
P_array=vec_new (0,sizeof (Point3D), 10,
&point3d::P Oint3d,
&POINT3D::~POINT3D);
Remember, in the construction of individual array elements, if exception,destructor occurs, it is passed to Vec_new (). Only elements that have been constructed need to be destructor, because their memory has been configured, and Vec_new () is responsible for releasing the memory when exception occurs.
Programmers do not need to specify the number of array elements to delete, and the size of the array is done by the compiler, so we can now write this:
Delete[] P_array;
Finding an array dimension has a significant effect on the efficiency of the delete operator, which leads to this compromise: only when the parentheses appear, the compiler looks for the dimensions of the array, otherwise it assumes that only a single objects is to be deleted. If the programmer does not provide the necessary brackets, like this:
Delete P_array;
Then only the first element will be deconstruct. Other elements still exist-although their associated memory has been requested to be returned.
How the number of elements should be logged. An obvious way is to configure an extra word for each chunk of memory returned by Vec_new (), and then wrap the number of elements in that word. Usually the value of such a parcel is called a cookie (cookies).
If we configure an array with 10 Point3D objects, we would expect the constructor of point and Point3D to be called 10 times each, acting on one element of the array at a time:
Not a good idea at all.
point* ptr=new point3d[10]; (modern compiler VC6.0 can be handled correctly)