Initializing a function pointer. Pre-initialization of function parameters


While creating software products It is necessary to be able to clearly and efficiently operate with all the necessary terminology. For example, take a word like initialization. What is this, do you know? I doubt it, otherwise you wouldn't be reading this article. Therefore, let's look at what is meant by this term, what types there are, and also look at several problems that may arise when using electronics.

Initialization - what is it?

This is the name for the processes of creation, activation, preparation for work and determination of the necessary parameters so that the application can work efficiently and without errors. This is a mandatory step in getting any device or program into a state where it can be used. The initialization action itself can be directed externally relative to the object of influence. In most cases, this only means setting the necessary parameters, as well as the operating rules according to which the program will function.

Examples

Let's look at what initialization is. What is it? It will be easiest to understand using a few real examples:

  1. Under initialization magnetic disk hard drive understand its formatting. This may also include recording control information (volume labels, track descriptors, and similar manipulations).
  2. Program initialization means setting the necessary variables or setting program variables (counters, addresses, switches, pointers) to zero before the application itself is executed. Everything is installed according to what is done for this type of software or contained in the form of instructions in the file itself.
  3. Initialization for outputting print subsystem data to the printer. This is a definition that implies the following: on what device it is necessary to transfer data to paper and take into account all the needs. So, you should determine the print format, extension, whether to use color (if any) and other parameters. First, a control signal is sent to the device and its capabilities are scanned in order to perform the initial setup of the printer and offer the user an option that can be implemented with existing equipment.

Now let's look at the practical case of launching an application. Let's say there is an initialization error when starting the game. The Sims 3 was chosen as a base - a fairly popular application in which you need to manage a person, creating a prosperous life for him.

What causes it in Sims-3

The most common version of the problem is 0x0175dcbb. This number is used to specifically indicate problems with application initialization. This may occur due to the fact that the base game conflicts with add-ons, mods, and video card drivers that were installed on the computer. Moreover, you are not insured even when purchasing a licensed game - if there is a problem, the license will not automatically solve it.

What to do if error 0x0175dcbb already exists?

So, an error occurred. Now let's figure out how we can remove it. Although we will consider a game, much of the knowledge written below can also be useful when working with serious applications. Remember that the most old problem- These are archaic software components. First of all, we are interested in video card drivers. They can be downloaded from the manufacturers’ websites, which is exactly what we did. It would also be a good idea to install or update to the latest version of the NET Framework - it is advisable to do this from the developer's resource, which is Microsoft.

Now let’s note the most popular causes of the problem: additional mods that were written third party developers. In such cases, unfortunately, you will have to remove the extension itself. If there is information that prevents you from doing this, you can simply rename the Mods folder. If an error occurs after installation, you must uninstall the program itself using the uninstaller and reinstall it again. If all this does not help (and this happens often), then we can conclude that the disk where the game itself is located is damaged. And the only prudent solution is to exchange it (or download a pirated version from a torrent).

Conclusion

What to install on your computer is up to you. But in the future, before adding content developed by third parties to a stable product, you need to think carefully and show considerable attention to the quality of the downloaded add-ons. Because there may be an error initializing the update, and the rollback option is usually not provided. And then you will have to delete the entire program and install it again. If you have already decided to do this, then you need to take care of the safety of the accumulated data. A copy of them should be placed somewhere safe and secure, such as on your desktop or flash drive. That's all, we examined the initialization in detail. What is this, you probably understand. After all, we analyzed not only the textual component, but also looked at several specific examples.


The list of parameters in the definition and prototype of a function, in addition to matching the types of parameters, has one more purpose.

A parameter declaration may contain an initializer, that is, an expression that must provide an initial value for the parameter. The parameter initializer is not a constant expression. The initial initialization of parameters does not occur at the compilation stage (like, for example, memory allocation for arrays), but directly during program execution.

Next lines demonstrate an example of a function declaration with parameter initialization. The XX function is used to initialize the ww parameter.

Int BigVal;
int XX(int);
int ZZ(int tt, int ww = XX(BigVal));

The second parameter can be initialized in this way, without specifying its name at all. The declaration syntax allows you to do this!

Int ZZ(int tt, int = XX(BigVal));

The only condition for such initialization is that the type of the parameter matches the type of the expression whose value is used during initial initialization.

Function prototypes can be located in different scopes. It can even be placed in the body of the defined function. Each function declaration can contain own options declaration and initialization of parameters. But multiple declarations of the same function within the same scope are not allowed. reinitialization parameters. There must be a reasonable limit to everything.

In addition, C++ has another limitation related to the order in which parameters are initialized within the scope. Initialization is carried out without fail from the very last (rightmost) parameter in the list of parameter declarations. Initialization of parameters does not allow gaps: initialized parameters cannot alternate with uninitialized parameters.

Int MyF1 (int par1, int par2, int par3, int par4 = 10);
int MyF1 (int par1, int par2 = 20, int par3 = 20, int par4);
int MyF1(int par1 = 100, int, int, int);

The list of parameters in the function definition is built according to similar rules. Initializers are also allowed in the parameter list of a function definition; in some cases, parameter names can also be omitted. Of course, including an unnamed parameter in the header of a function definition makes it difficult to use this parameter in the defined function. An unnamed parameter cannot be accessed by name.

Still, not using the parameter can be justified. Such parameters, or rather their specifiers, make it possible to reduce the cost of modifying complex multi-module programs when, as a result of changing a function, the number of parameters of this function changes. Unnecessary options can be disabled without changing multiple calls to this function. In this case, it makes sense to store the total number of parameters of the function, and the name unnecessary parameter remove from the list of parameters.

33passing parameters to main c++ function

When creating a console application in the C++ programming language, a line very similar to this is automatically created:

int main(int argc, char* argv) // main() function parameters

This line is the header of the main function main(); the parameters argс and argv are declared in brackets. So, if you run a program via the command line, then it is possible to transfer some information to this program, for this there are parameters argc and argv. The argc parameter has an int data type, and contains the number of parameters passed to the main function. Moreover, argc is always at least 1, even when we do not pass any information, since the first parameter is the name of the function. The argv parameter is an array of pointers to strings. Only string data can be passed through the command line. Pointers and strings are two large topics for which separate sections have been created. So it is through the argv parameter that any information is transmitted. Let's develop a program that we will run through the Windows command line and pass some information to it.

Lifetime and scope of program objects Memory classes.

The lifetime of a variable (global or local) is determined according to the following rules.

1. A variable declared globally (that is, outside all blocks) exists throughout the entire execution of the program.

2. Local variables (i.e. declared inside a block) with a register or auto memory class have a lifetime only for the period of execution of the block in which they are declared. If a local variable is declared with a memory class of static or extern, then it has a lifetime for the duration of the entire program.

The visibility of variables and functions in a program is determined by the following rules.

1. A variable declared or defined globally is visible from the point of declaration or definition to the end of the source file. You can make a variable visible in other source files by declaring it with the extern memory class in these files.

2. A locally declared or defined variable is visible from the point of declaration or definition to the end of the current block. Such a variable is called local.

3. Variables from enclosing blocks, including variables declared at the global level, are visible in inner blocks. This visibility is called nested. If a variable declared inside a block has the same name as a variable declared in the enclosing block, then they are different variables, and the variable from the enclosing block in the inner block will be invisible.

4. Functions with memory class static are visible only in the source file in which they are defined. All other functions are visible throughout the program.

Labels in functions are visible throughout the entire function.

Formal parameter names declared in the parameter list of a function prototype are visible only from the point of the parameter declaration to the end of the function declaration.

Memory classes in C++.

2) register

3) static

4) extern

auto

int i; -> auto int i;

Memory classes in C++.

There are 4 memory class specifiers:

2) register

3) static

4) extern

If a memory class is not specified, it is determined by default from the declaration context.

Objects of the auto and register classes have a local lifetime.

The static and extern specifiers define objects with a global lifetime, but the exact meaning of each specifier depends on whether it is at the external or internal level and whether the object is a function or a variable.

At the internal level, when declaring a variable, any of the 4 memory class specifiers can be used. If the specifier is omitted, then it is implied auto

int i; -> auto int i;

A variable with memory class auto has a local lifetime. It is visible only in the block in which it is declared. Memory for this variable is allocated upon block entry and freed upon exit. When the block is re-entered, the memory for that variable may be allocated elsewhere.

register

Instructs the compiler to allocate memory for a variable in a processor register, if possible. A variable with the storage class register has the same scope as auto.

When the compiler encounters the register specifier and there is no free register, the memory for the variable is allocated auto.

Register memory, if present, can only be assigned to variables of type int and pointers.

static

Variables declared internally with the static specifier provide the ability to save the value of a local variable when exiting a block and use it the next time the block is entered.

extern

A variable declared with the extern specifier is a reference to a variable of the same name defined at the external level in any program source file.

The purpose of an extern declaration is to make the outer-level variable declaration visible within the block.

35Initialization of global and local variables

There are local and global variables. So, variables declared inside a function are called local. Local variables have their own scopes; these scopes are the functions in which the variables are declared. Thus, in different functions you can use variables with the same names, which in turn is very convenient. The division of variables into global and local corresponds to one of the main rules of programming, namely the principle of least privilege. That is, variables declared inside one function should be accessible only to this function and nothing else; after all, they were created specifically for this function. Global variables are declared outside the body of a function, and therefore the scope of such variables extends to the entire program. Typically global variables are declared before main function, but you can declare it after the main() function, but then this variable will not be available in the main() function.

Let's develop a program in which two variables, local and global, with the same name will be declared.

37Dynamic arrays. Features of memory allocation and release for multidimensional arrays.

Dynamic is an array whose size can change during program execution. To resize a dynamic array, a programming language that supports such arrays must provide a built-in function or operator. Dynamic arrays provide the opportunity for more flexible work with data, since they allow you not to predict the volume of data stored, but to adjust the size of the array in accordance with the actually required volumes. Unlike dynamic arrays There are static arrays and variable-length arrays. The size of a static array is determined at the time the program is compiled. The size of a variable-length array is determined during program execution. The difference between a dynamic array and a variable-length array is automatic change sizes, which is not difficult to implement in cases where it is absent, so they often do not distinguish between variable-length arrays and dynamic arrays.

Dynamic arrays of limited size and their volume

The simplest dynamic array is an array with a fixed length size, which is divided into two parts: the first one stores the elements of the dynamic array, and the second part is reserved or unused. You can add or remove elements to the end of a dynamic array in certain time using the array's reserved space until it is exhausted. The number of elements used in dynamic arrays is logical size or simply size, while the size of the main array is called volume dynamic array or physical size, which is the maximum possible size without overriding the data size.

In applications where logical size is limited, a fixed-size data structure is sufficient. This can backfire as more space may be needed later. However, many programmers prefer to write code using resizable arrays from the start, and then, during the optimization phase, replace some of the arrays with fixed-size arrays. Resizing the underlying array is an expensive task and typically involves copying the entire contents of the array.

38Preprocessor directives. Macro definitions.

Almost all C++ programs use special compiler commands called directives. In general, a directive is an instruction to the C++ language compiler to perform one or another action at the time of program compilation. There is a strictly defined set of possible directives, which includes the following definitions:

#define, #elif, #else, #endif, #if, #ifdef, #ifndef, #include, #undef.

The #define directive is used to define constants, keywords, operators and expressions used in the program. The general syntax of this directive is as follows:

#define<идентификатор> <текст>

#define<идентификатор> (<список параметров>) <текст>

The #undef directive undoes the definition previously introduced by the #define directive. Suppose that at some point in the program you need to undefine the FOUR constant. This is achieved next command:

The difference between the #if directive and the #ifdef and #ifndef directives is that it allows you to check more diverse conditions, not just the existence or not of any constants. For example, using the #if directive you can perform the following check:

The #include directive used in the examples above allows you to add previously written programs and saved as files to the program. For example, the line

#include< stdio.h >

Macro definition - #define directive

Macro definition, or macro substitution, is perhaps the most powerful preprocessor directive. And at the same time the most insidious. Let's see how to use it. Take a look at this piece of the program:

int x;
int y;
int i;

for (i=0; i<100; i++) {
x[i]=fx(i);
y[i]=fy(i);
}

This code counts the values ​​of the functions fx and fy at 100 points - for example, in order to then build their graphs. If we need to increase the number of points, we will have to replace 100 with another number in three places - and there is a lot of writing, and it is easy to make a mistake.

45Function templates.

Templates(English) template) is a C++ language tool designed for coding generalized algorithms, without reference to certain parameters (for example, data types, buffer sizes, default values).

In C++ it is possible to create function and class templates.

Templates allow you to create parameterized classes and functions. The parameter can be any type or a value of one of the valid types (integer, enum, pointer to any object with a globally accessible name, reference). For example, we need some kind of class:

Class SomeClass( int SomeValue; int SomeArray; ...);

For one specific purpose we can use this class. But, suddenly, the goal has changed a little, and another class is needed. Now you need 30 elements of the SomeArray array and the real type SomeValue of the SomeArray elements. Then we can abstract away from concrete types and use templates with parameters. Syntax: at the beginning, before the class declaration, we write the word template and indicate the parameters in angle brackets. In our example:

Template< int ArrayLength, typename SomeValueType >class SomeClass( SomeValueType SomeValue; SomeValueType SomeArray[ ArrayLength ]; ...);

Then for the first model we write:

SomeClass< 20, int >SomeVariable;

for the second:

SomeClass< 30, double >SomeVariable2;

Although templates provide a short form for writing a section of code, their use does not actually shorten the executable code, since the compiler creates a separate instance of the function or class for each set of parameters.

46 Class templates.

Any template begins with the word template, be it a function template or a class template. After the template keyword there are angle brackets -< >, which list a list of template parameters. Each parameter must be preceded by the reserved word class or typename. The absence of these keywords will be interpreted by the compiler as a syntax error. Some examples of template declarations.

The typename keyword indicates that the template will use a built-in data type, such as: int, double, float, char, etc. A keyword class tells the compiler that the function template will use custom data types, that is, classes, as a parameter. But under no circumstances confuse a template parameter with a class template. If we need to create a class template with one parameter of type int and char, the class template will look like this.

As you can see, the Stack class template is declared and defined in the file with the main function. Of course, this method of recycling templates is no good, but it will do for an example. Lines 7 - 20 declare the class template interface. The class declaration is done in the usual way, and before the class there is a template declaration, on line 7. When declaring a class template, always use this syntax.

Lines 47 - 100 contain the template function element of the Stack class, and before each function it is necessary to declare a template, exactly the same as before the class - template . That is, it turns out that the function element of a class template is declared in exactly the same way as ordinary function templates. If we described the implementation of methods inside a class, then the template header would be template There is no need to register for each function.

To bind each function element to a class template, as usual we use the binary scope resolution operation - :: with the name of the class template - Stack . Which is what we did in lines 49, 58, 68, 83, 96.

Pay attention to the declaration of the myStack object of the Stack class template in the main function, line 24. In the angle brackets, you must explicitly indicate the data type used; this was not necessary in the function templates. Next, main runs some functions that demonstrate how the Stack class template works. The result of the program is shown below.

47STL Library. Other container class libraries.

Classification of function pointers

Definition of a function pointer

Function pointers

Typically function parameters are used to provide a data interface between the call point context and the function context. The use of function pointers allows us to introduce the new kind parameters. Function parameters related to this new type allow you to pass other functions into the function you are developing. As a result, it becomes possible to increase the degree of universality of the developed function.

A function pointer is either an expression whose value is the address of the function, or a variable that stores the address of the function. The address of a function is most often the address of the beginning of its program code. However, on some compilers, the address of a function may be the address of an object that contains the address of the beginning of the function's program code.

There are two types of function pointers in C:

· pointer – expression,

· pointer variable.

38.9.3. Pointer - expression

As noted above, a pointer expression is an expression whose value is the address of a function. Let's give an example.

Let there be a prototype of a function for calculating the square of a number of type double. Then the &sqr expression is a pointer expression. Here & is a unary address operator, which instructs the compiler to calculate the address of the function.

38.9.4. Pointer - variable

A pointer variable is a variable designed to store the address of a function. Let's consider the format for defining such a variable.

(* <α>) (<γ>)

* - dereferencing operator,

α – identifier of the declared pointer,

· b - the type of the value returned by the function to which the pointer can be set.

· γ – list of function parameter types to which the pointer can be set.

Let's give an example of declaring a pointer-variable.

double (*pf)(double);

This is a variable declaration. Variable name pf. the value of this variable can be the address of any function that takes one parameter of type double and that returns values ​​of type double.

Given the above, we can say that the definition of a function pointer specifies the set of functions with which it can be used. The pointer variable pf declared above can work with, for example, the following functions.

double sqr(double x)
{

double cube(double x)
{

return x * x * x;

Let's return to the declaration of the pointer - the pf variable. Let's consider the purpose of the parentheses used in it. There are two pairs of them here.



double (*pf)(double);

The second pair of parentheses is the function operator. The question arises: “What role does the first pair of brackets play?” In order to understand this issue, let’s temporarily remove this pair of parentheses. Then the expression in question will take the following form.

double *pf(double);

The resulting expression is a declaration of a function that takes one parameter of type double and returns a pointer of type double*. To make sure of this, let us remember that the syntax of declarations in the C language contains the syntax of expressions. To determine the purpose of the name pf in the expression in question, consider the subexpression *pf(double). Here two operators are applied to the pf name: * and () . The interpretation of the pf name depends on the precedence of these operators. If you look at the operator precedence table, you can see that the function () operator has higher priority than the dereference operator *. It follows that the () operator binds the pf name more strongly than the * operator. So, in the new edition, pf is the name of the function, not the name of the pointer.

Now let's go back to the original ad.

double (*pf)(double);

The first parentheses in this expression combine the grouping order of operators and operands. Now the name pf becomes the name of the pointer.

Pointer – A variable can be initialized at the time of its declaration either to a null pointer or to the address of a function of the type to which the pointer can be set.

Let there be two functions that have the following prototypes.

double sqr(double x);
double cube(double x);

Let's declare three function pointers:

double (*pf1)(double) =
double (*pf2)(double)= cube;

double (*pf3)(double)= 0;

The syntax with which the first two variables (pf1 and pf2) are declared is slightly different. The syntax with which the first variable (pf1) is declared is beyond doubt as to its correctness. To the right of the assignment sign is an expression whose value is the address of the function. There may be doubts about the correctness of the second declaration, where the pf2 variable is declared. This declaration uses the function name rather than its address as the initializer. However, this code compiles fine. The fact is that the C language always in the code where a pointer to a function is expected, and the function name is encountered, automatically converts the function name into a pointer.

Comment. The initialization operation is also performed when using the function exponent as a formal parameter of the function.

In this chapter, we will study in detail the automatic initialization, assignment and destruction of class objects in a program. To support initialization, a constructor is used - a designer-defined function (possibly overloaded) that is automatically applied to each class object before its first use. The constructor's counterpart function, the destructor, is automatically applied to each class object upon completion of its use and is designed to release resources captured either in the class constructor or during its lifetime.

By default, both initialization and assignment of one class object to another are performed member by member, i.e. by sequentially copying all members. While this is usually sufficient, under some circumstances such semantics are inadequate. The class designer must then provide a special copy constructor and a copy assignment operator. The hardest part about maintaining these member functions is understanding that they have to be written.

14.1. Initializing a class

Consider the following class definition:

Class Data ( public: int ival; char *ptr; );

To use a class object safely, its members must be initialized correctly. However, the meaning of this action is different for different classes. For example, can ival contain a negative value or null? What are the correct initial values ​​for both class members? We won't answer these questions without understanding the abstraction represented by a class. If it is used to describe company employees, then ptr probably indicates the employee's last name, and ival is his unique number. Then negative or zero values ​​are erroneous. If the class represents the current temperature in the city, then any values ​​of ival are acceptable. It is also possible that the Data class represents a string with a reference count, in which case ival contains the current number of references to the string at address ptr. With this abstraction, ival is initialized to 1; as soon as the value becomes 0, the class object is destroyed.

Mnemonic names for the class and both of its members would, of course, make its purpose clearer to the reader of the program, but would not provide any additional information to the compiler. In order for the compiler to understand our intentions, we must provide one or more overloaded initialization functions - constructors. The appropriate constructor is selected depending on the set of initial values ​​specified when defining the object. For example, any of the following statements represents the correct initialization of an object of the Data class:

Data dat01("Venus and the Graces", 107925); Data dat02("about"); Data dat03(107925); Data dat04;

There are situations (as in the case of dat04) when we need a class object, but we do not yet know its initial values. Perhaps they will become known later. However, an initial value must be specified, at least one that indicates that a reasonable initial value has not yet been assigned. In other words, initializing an object sometimes amounts to indicating that it has not yet been initialized. Most classes provide special constructor default, for which you do not need to set initial values. Typically, it initializes the object in such a way that you can later realize that no actual initialization has yet been done.

Is our Data class required to have a constructor? No, because all its members are open. The mechanism inherited from the C language supports explicit initialization, similar to that used when initializing arrays:

Int main() ( // local1.ival = 0; local1.ptr = 0 Data local1 = ( 0, 0 ); // local2.ival = 1024; // local3.ptr = "Anna Livia Plurabelle" Data.local2 - ( 1024, "Anna Livia Plurabelle" ); // ... )

Values ​​are assigned positionally, based on the order in which the data members are declared. The following example results in a compilation error because ival is declared before ptr:

// error: ival = "Anna Livia Plurabelle"; // ptr = 1024 Data.local2 - ( "Anna Livia Plurabelle", 1024 );

Explicit initialization has two main disadvantages. First, it can only be applied to objects of classes whose members are all public (that is, this initialization does not support encapsulation of data and abstract types - they did not exist in the C language from which it was borrowed). And secondly, this form requires programmer intervention, which increases the likelihood of errors (forgot to include the initialization list or mixed up the order of the initializers in it).

So should we use explicit initialization instead of constructors? Yes. For some applications, it is more efficient to use a list to initialize large structures with constant values. For example, we can build a color palette in this way or include in the program text fixed coordinates of the vertices and values ​​​​at the nodes of a complex geometric model. In such cases, initialization is done at load time, which reduces the time it takes to run the constructor, even if it is defined as inline. This is especially useful when working with global objects1.

However, in general, the preferred initialization method is a constructor, which is guaranteed to be called by the compiler on every object before it is first used. In the next section we will get to know the constructors in detail.

14.2. Class constructor

What sets the constructor apart from other member functions is that its name is the same as the class name. To declare a default constructor we write2:

Class Account ( public: // default constructor... Account(); // ... private: char *_name; unsigned int _acct_nmbr; double _balance; );

The only syntactic restriction placed on a constructor is that it must not have a return type, not even void. Therefore the following declarations are erroneous:

// errors: a constructor cannot have a return type void Account::Account() ( ... ) Account* Account::Account(const char *pc) ( ... )

One class can have any number of constructors, as long as they all have different lists formal parameters.

How do we know how many and which constructors to define? At a minimum, you must assign an initial value to each member that needs it. For example, the account number is either specified explicitly or automatically generated in such a way as to ensure its uniqueness. Let's assume that it will be created automatically. Then we must allow the remaining two members _name and _balance to be initialized:

Account(const char *name, double open_balance);

An Account class object initialized by the constructor can be declared as follows:

Account newAcct("Mikey Matz", 0);

If there are many accounts that have an opening balance of 0, then it is useful to have a constructor that specifies only the owner's name and automatically initializes _balance to zero. One way to do this is to provide a view constructor:

Account(const char *name);

Another way is to include a default value of zero in the two-parameter constructor:

Account(const char *name, double open_balance = 0.0);

Both constructors have the functionality the user needs, so both solutions are acceptable. We prefer to use the default argument because this reduces the total number of constructors for the class.

Should I also support setting just the opening balance without specifying the customer name? In this case, the class specification explicitly prohibits this. Our two-parameter constructor, the second of which has a default value, provides a complete interface for specifying the initial values ​​of those members of the Account class that can be initialized by the user:

Class Account ( public: // default constructor... Account(); // parameter names in the declaration are optional Account(const char*, double=0.0); const char* name() ( return name; ) // . .. private: // ... );

Below are two examples of a proper definition of an Account class object, where one or two arguments are passed to the constructor:

Int main() ( // correct: in both cases the constructor is called // with two parameters Account acct("Ethan Stern"); Account *pact = new Account("Michael Lieberman", 5000); if (strcmp(acct.name (), pact->name())) // ... )

C++ requires that a constructor be applied to a specific object before it is used for the first time. This means that for both acct and the object pointed to by pact, the constructor will be called before the test in the if statement.

The compiler rebuilds our program by inserting constructor calls. Here's how the acct definition inside main() is likely to be modified:

// pseudo-code in C++ // illustrating the internal insertion of the constructor int main() ( Account acct; acct.Account::Account("Ethan Stern", 0.0); // ... )

Of course, if the constructor is defined as inline, then it is substituted at the point of call.

Handling the new operator is a bit more complicated. The constructor is called only when it has successfully allocated memory. A modification of the pact definition in a somewhat simplified form looks like this:

// pseudocode in C++ // illustrating internal constructor insertion when processing new int main() ( // ... Account *pact; try ( pact = _new(sizeof(Account)); pact->Acct.Account::Account ("Michael Liebarman", 5000.0); ) catch(std::bad_alloc) ( // new operator failed: // constructor is not called ) // ... )

There are three generally equivalent forms of specifying constructor arguments:

// in general these forms are equivalent to Account acct1("Anna Press"); Account acct2 = Account("Anna Press"); Account acct3 = "Anna Press";

The acct3 form can only be used when specifying a single argument. If there are two or more arguments, we recommend using the form acct1, although acct2 is also acceptable.

Beginners often make a mistake when declaring an object initialized with a default constructor:

// alas! does not work as expected Account newAccount();

This instruction compiles without errors. However, when trying to use the object in a context like this:

// compilation error... if (! newAccount.name()) ...

the compiler will not be able to apply class member access notation to the function. Definition

// defines a function newAccount, // not an object of the Account class newAccount();

is interpreted by the compiler as a definition of a parameterless function that returns an object of type Account. A correct declaration of a class object initialized with a default constructor does not contain empty parentheses:

// correct: a class object is defined... Account newAccount;

You can define a class object without specifying a list of actual arguments if it either declares a default constructor or has no constructor declarations at all. If at least one constructor is declared in a class, then it is not allowed to define an object of the class without calling one of them. In particular, if a class defines a constructor that takes one or more parameters, but does not define a default constructor, then each object definition of that class must provide the required arguments. One might argue that it makes no sense to define a default constructor for the Account class, since there are no accounts without an owner name. The revised version of the Account class eliminates this constructor:

Class Account ( public: // parameter names in the declaration are optional Account(const char*, double=0.0); const char* name() ( return name; ) // ... private: // ... );

Now, when declaring each Account object in the constructor, you must specify at least a C-string type argument, but this is most likely pointless. Why? Container classes (such as vector) require that the class of elements they contain have either a default constructor or no constructors at all. A similar situation occurs when allocating a dynamic array of class objects. So, next instruction would cause a compilation error for new version Account:

// error: default constructor required for Account class Account *pact = new Account[ new_client_cnt ];

In practice, it is often necessary to specify a default constructor if there are any other constructors.

What if there are no reasonable default values ​​for a class? For example, the Account class requires that the last name of the account owner be specified for any object. In this case, it is best to set the state of the object so that it is clear that it has not yet been initialized with the correct values:

// default constructor for the Account class inline Account:: Account() ( _name = 0; _balance = 0.0; _acct_nmbr = 0; )

However, the member functions of the Account class will have to include an integrity check on the object before using it.

There is an alternative syntax: a member initialization list, in which names and initial values ​​are specified, separated by commas. For example, the default constructor could be rewritten as follows:

// default constructor of the Account class using // member initialization list inline Account:: Account() : _name(0), _balance(0.0), _acct_nmbr(0) ()

Such a list is only valid in a definition, not in a constructor declaration. It is placed between the parameter list and the body of the constructor and is separated by a colon. Here's what our constructor looks like with two parameters, using partial member initialization list:

Inline Account:: Account(const char* name, double opening_bal) : _balance(opening_bal) ( _name = new char[ strlen(name)+1 ]; strcpy(_name, name); _acct_nmbr = get_unique_acct_nmbr(); )

get_unique_acct_nmbr() is a non-public member function that returns a guaranteed unused account number.

A constructor cannot be declared with the const or volatile keywords (see Section 13.3.5), so the following entries are incorrect:

Class Account ( public: Account() const; // error Account() volatile; // error // ... );

This does not mean that class objects with such specifiers are prohibited from being initialized by a constructor. Simply, an appropriate constructor is applied to the object, without taking into account the specifiers in the object declaration. The constancy of a class object is established after the work on its initialization is completed, and disappears at the moment the destructor is called. Thus, an object of a class with the const specifier is considered constant from the moment the constructor finishes until the destructor runs. The same applies to the volatile specifier.

Consider the following program fragment:

// in some header file extern void print(const Account &acct); // ... int main() ( // converts the string "oops" into an object of the Account class // using the Account::Account("oops", 0.0) constructor print("oops"); // ... )

By default, a constructor with one parameter (or with several - provided that all parameters except the first one have default values) acts as a conversion operator. In this program fragment, the Account constructor is used implicitly by the compiler to transform a literal string into an Account class object when calling print(), although such a conversion is not needed in this situation.

Unintentional implicit class conversions, such as transforming "oops" into an Account class object, have proven to be a source of hard-to-find bugs. Therefore, the C++ standard added the explicit keyword to tell the compiler that such conversions are not needed:

Class Account ( public: explicit Account(const char*, double=0.0); );

This modifier is only applicable to the constructor. (Conversion operators and the word explicit are discussed in Section 15.9.2.)

14.2.1. Default constructor

A default constructor is a constructor that can be called without giving any arguments. This does not mean that such a constructor cannot take arguments; It’s just that each of its formal parameters has a default value associated with it:

// all these are default constructors Account::Account() ( ... ) iStack::iStack(int size = 0) ( ... ) Complex::Complex(double re=0.0, double im=0.0) ( . ..)

When we write:

Int main() ( Account acct; // ... )

then the compiler first checks whether a default constructor is defined for the Account class. One of the following situations occurs:

  1. Such a constructor is defined. Then it is applied to acct.
  2. The constructor is defined but not public. In this case, the definition of acct is flagged by the compiler as an error: the main() function does not have access rights.
  3. There is no default constructor defined, but there are one or more constructors that require arguments. The definition of acct is flagged as an error: the constructor has too few arguments.
  4. There is no default constructor or any other. The definition is considered correct, acct is not initialized, and the constructor is not called.

Points 1 and 3 should already be quite clear (if this is not the case, re-read this chapter) Let's look more closely at points 2 and 4.

Let's assume that all members of the Account class are declared public and no constructor is declared:

Class Account ( public: char *_name; unsigned int _acct_nmbr; double _balance; );

In this case, when defining an object of the Account class, no special initialization is performed. The initial values ​​of all three terms depend only on the context in which the definition occurs. For example, for static objects it is guaranteed that all their members will be nullable (as for objects that are not instances of classes):

// static class storage // all memory associated with the object is reset to zero Account global_scope_acct; static Account file_scope_acct; Account foo() ( static Account local_static_acct; // ... )

However, objects defined locally or dynamically allocated will initially contain a random set of bits remaining on the program's stack:

// local and heap-distributed objects are not initialized // until explicit initialization or assignment Account bar() ( Account local_acct; Account *heap_acct = new Account; // ... )

Beginners often assume that the compiler automatically generates a constructor if one is not given and uses it to initialize class members. For Account as we have defined it, this is not true. No constructor is generated or called. For more complex classes that have members that are themselves classes, or that use inheritance, this is partly true: a default constructor can be generated, but it does not assign initial values ​​to members of built-in or composite types such as pointers or arrays.

If we want such members to be initialized, we must take care of this ourselves by providing one or more constructors. Otherwise, it is almost impossible to distinguish the correct value of a member of this type from an uninitialized one if the object is created locally or distributed from a heap.

14.2.2. Restricting rights to create an object

The availability of a constructor is determined by the section of the class in which it is declared. We can restrict or explicitly disable some forms of object creation by placing the corresponding constructor in a non-public section. In the example below, the default constructor of the Account class is declared private, but with two parameters it is declared public:

Class Account ( friend class vector; public: explicit Account(const char*, double = 0.0); // ... private: Account(); // ... );

A typical program will now be able to define objects of the Account class simply by specifying both the name of the account owner and the opening balance. However, Account member functions and its friend class vector can create objects using any constructor.

Non-public constructors in real programs C++ is most often used for:

  • preventing one object from being copied into another object of the same class (this problem is discussed in the next subsection);
  • instructions that the constructor should be called only when the class is serving as a base class in the inheritance hierarchy, and not to create objects that the program can manipulate directly (see the discussion of inheritance and object-oriented programming in Chapter 17).

14.2.3. Copy constructor

Initializing an object with another object of the same class is called member-by-member default initialization. Copying one object to another is accomplished by copying each non-static member sequentially. The class designer can change this behavior by providing a special copy constructor. If defined, it is called whenever one object is initialized by another object of the same class.

Often memberwise initialization does not ensure correct class behavior. Therefore, we explicitly define a copy constructor. In our Account class, this is necessary, otherwise two objects will have the same account numbers, which is prohibited by the class specification.

The copy constructor takes as a formal parameter a reference to a class object (traditionally declared with the const specifier). Here is its implementation:

Inline Account:: Account(const Account &rhs) : _balance(rhs._balance) ( _name = new char[ strlen(rhs._name) + 1 ]; strcpy(_name, rhs._name); // rhs._acct_nmbr cannot be copied _acct_nmbr = get_unique_acct_nmbr(); )

When we write:

Account acct2(acct1);

The compiler determines whether an explicit copy constructor is declared for the Account class. If it is declared and available, then it is called; and if it is not available, then the definition of acct2 is considered an error. In the case where a copy constructor is not declared, member-wise initialization is performed by default. If a copy constructor declaration is later added or removed, no changes will need to be made to the user programs. However, they still need to be recompiled. (Memberwise initialization is discussed in more detail in Section 14.6.)

Exercise 14.1

Which of the following statements is false? Why?

  1. A class must have at least one constructor.
  2. The default constructor is a constructor with an empty parameter list.
  3. If class members do not have reasonable initial values, then you should not provide a default constructor.
  4. If a class does not have a default constructor, the compiler generates one automatically and initializes each member to the default value for the corresponding type.

Exercise 14.2

Provide one or more constructors for a given set of members. Explain your choice:

Class NoName ( public: // there must be constructors here // ... protected: char *pstring; int ival; double dval; );

Exercise 14.3

Choose one of the following abstractions (or suggest your own). Decide what data (user-specified) is appropriate for the class representing this abstraction. Write an appropriate set of constructors. Explain your decision.

  1. Book
  2. Employee
  3. Vehicle
  4. An object
  5. Tree

Exercise 14.4

Using the above class definition:

Class Account ( public: Account(); explicit Account(const char*, double=0.0); // ... );

explain what happens as a result of the following definitions:

(a)Account acct; (b) Account acct2 = acct; (c) Account acct3 = "Rena Stern"; (d) Account acct4("Anna Engel ", 400.00); (e) Account acct5 = Account(acct3);

Exercise 14.5

The copy constructor parameter may not be constant, but must be a reference. Why is this instruction wrong:

Account::Account(const Account rhs);

14.3. Class destructor

One of the designer's goals is to ensure automatic resource allocation. We have already seen in the example with the Account class a constructor where, using the new operator, memory is allocated for an array of characters and a unique account number is assigned. You can also imagine a situation where you need to gain exclusive access to shared memory or to a critical section of a thread. This requires a symmetric operation that ensures automatic release of memory or return of a resource upon expiration of the object's lifetime - a destructor. A destructor is a special user-defined member function that is automatically called when an object goes out of scope or when a delete operation is applied to a pointer to an object. The name of this function is derived from the class name preceded by a tilde character (~). A destructor does not return a value and does not accept any parameters, and therefore cannot be overloaded. Although you are allowed to define multiple such member functions, only one of them will apply to all objects of the class. Here, for example, is the destructor for our Account class:

Class Account ( public: Account(); explicit Account(const char*, double=0.0); Account(const Account&); ~Account(); // ... private: char *_name; unsigned int _acct_nmbr; double _balance; ); inline Account::~Account() ( delete _name; return_acct_number(_acct_nnmbr); )

Note that our destructor does not reset member values:

Inline Account::~Account() ( // delete _name required; return_acct_number(_acct_nnmbr); // optional _name = 0; _balance = 0.0; _acct_nmbr = 0; )

This is not necessary, since the memory allocated for object members will still be freed. Consider the following class:

Class Point3d ( public: // ... private: float x, y, z; );

The constructor here is needed to initialize the members representing the coordinates of the point. Do you need a destructor? No. An object of the Point3d class does not need to free resources: memory is allocated and freed by the compiler automatically at the beginning and end of its life.

In general, if class members have simple values, say the coordinates of a point, then a destructor is not needed. Not every class needs a destructor, even if it has one or more constructors. The main purpose of a destructor is to free resources allocated either in the constructor or during the lifetime of the object, such as freeing a lock or memory allocated by the new operator.

But the functions of a destructor are not limited to just releasing resources. It can implement any operation that the class designer intends to be performed immediately upon completion of the object's use. Thus, a common technique for measuring program performance is to define a Timer class whose constructor runs some form of software timer. The destructor stops the timer and displays the measurement results. An object of this class can be conditionally defined in the critical sections of the program that we want to profile, like this:

( // the beginning of the critical section of the program #ifdef PROFILE Timer t; #endif // the critical section // t is destroyed automatically // the elapsed time is displayed... )

To make sure that we understand the behavior of the destructor (and the constructor too), let’s look at next example:

(1) #include "Account.h" (2) Account global("James Joyce"); (3) int main() (4) ( (5) Account local("Anna Livia Plurabelle", 10000); (6) Account &loc_ref = global; (7) Account *pact = 0; (8) (9) ( (10) Account local_too("Stephen Hero"); (11) pact = new Account("Stephen Dedalus"); (12) ) (13) (14) delete pact; (15) )

How many constructors are called here? Four: one for the global object at line (2); one for each of the local objects local and local_too on lines (5) and (10), respectively, and one for the heap-allocated object on line (11). Neither the declaration of a loc_ref object reference on line (6) nor the declaration of a pact pointer on line (7) results in a constructor being called. A reference is an alias for an already constructed object, in this case global. The pointer also only addresses an object created earlier (in this case, allocated in the heap, line (11)), or does not address any object (line (7)).

Four destructors are called similarly: for the global object declared in line (2), for two local objects, and for an object in the heap when calling delete in line (14). However, there is no instruction in the program with which to associate a destructor call. The compiler simply inserts these calls behind last use object, but before closing the corresponding scope.

Constructors and destructors of global objects are called at the initialization and termination stages of program execution. Although such objects behave normally when used in the file in which they are defined, their use in situations where references are made across file boundaries becomes a serious problem in C++.4

The destructor is not called when a reference or pointer to an object goes out of scope (the object itself remains).

C++ internally prevents the delete operator from being used on a pointer that does not address any object, so code checks are not necessary:

// optional: implicitly executed by the compiler if (pact != 0) delete pact;

Whenever this operator is applied within a function to a single object located on a heap, it is better to use an object of the auto_ptr class rather than a regular pointer (see the discussion of the auto_ptr class in Section 8.4). This is especially important because a missed call to delete (say, when an exception is thrown) leads not only to a memory leak, but also to a missed call to the destructor. The following is an example of a program rewritten using auto_ptr (it is slightly modified since an object of class auto_ptr can only be explicitly reassigned to address another object by assigning it to another auto_ptr):

#include #include "Account.h" Account global("James Joyce"); int main() ( Account local("Anna Livia Plurabelle", 10000); Account &loc_ref = global; auto_ptr pact(new Account("Stephen Dedalus")); ( Account local_too("Stephen Hero"); ) // auto_ptr object destroyed here)

14.3.1. Explicit call to destructor

Sometimes you have to explicitly call a destructor for an object. This need arises especially often in connection with the new operator (see Section 8.4). Let's look at an example. When we write:

Char *arena = new char[ sizeof Image ];

then memory is allocated from the heap, the size of which is equal to the size of an object of type Image, it is not initialized and is filled with random bits. If you write:

Image *ptr = new (arena) Image("Quasimodo");

then no new memory doesn't stand out. Instead, ptr is assigned the address associated with arena. Now the memory pointed to by ptr is interpreted as being occupied by an object of the Image class, and the constructor is applied to the already existing area. Thus, the new() allocation operator allows an object to be constructed in a previously allocated memory location.

Having finished working with the Quasimodo image, we can perform some operations with the Esmerelda image located at the same arena address in memory:

Image *ptr = new (arena) Image("Esmerelda");

However, the Quasimodo image will be overwritten, but we have modified it and would like to burn it to disk. Typically saving is done in the destructor of the Image class, but if we use the delete operator:

// bad: not only calls the destructor, but also frees memory delete ptr;

then, in addition to calling the destructor, we will also return it to hip memory, which should not be done. Instead, you can explicitly call the destructor of the Image class:

Ptr->~Image();

saving the memory allocated for the image for the subsequent call of the new placement operator.

Note that although ptr and arena address the same memory area in the heap, applying the delete operator to arena

// destructor is not called delete arena;

does not call the destructor of the Image class, since arena is of type char*, and the compiler calls the destructor only when the operand in delete is a pointer to an object of a class that has a destructor.

14.3.2. The dangers of increasing program size

The inline destructor can cause an unexpected increase in program size because it is inserted at every exit point within the function for every active local object. For example, in the following snippet

Account acct("Tina Lee"); int swt; // ... switch(swt) ( case 0: return; case 1: // do something return; case 2: // do something else return; // and so on )

the compiler will insert a destructor before each return statement. The destructor of the Account class is small, and the time and memory required to substitute it is also small. Otherwise, you will either have to declare the destructor non-inlined or reorganize the program. In the example above, the return statement in each case label can be replaced with a break statement so that the function has a single exit point:

// rewritten to provide a single exit point switch(swt) ( case 0: break; case 1: // do something break; case 2: // do something else break; // and so on ) // single exit point return;

Exercise 14.6

Write a suitable destructor for the given set of class members, among which pstring addresses a dynamically allocated array of characters:

Class NoName ( public: ~NoName(); // ... private: char *pstring; int ival; double dval; );

Exercise 14.7

Is a destructor required for the class you chose in Exercise 14.3? If not, explain why. Otherwise, please suggest an implementation.

Exercise 14.8

How many times are destructors called in the following snippet:

Void mumble(const char *name, fouble balance, char acct_type) ( Account acct; if (! name) return; if (balance<= 99) return; switch(acct_type) { case "z": return; case "a": case "b": return; } // ... }

14.4. Arrays and vectors of objects

An array of class objects is defined in exactly the same way as an array of built-in type elements. For example:

Account table[ 16 ];

defines an array of 16 Account objects. Each element in turn is initialized with a default constructor. You can also explicitly pass arguments to constructors inside an array initialization list enclosed in curly braces. Line:

Account pooh_pals = ("Piglet", "Eeyore", "Tigger" );

defines an array of three elements initialized by constructors:

Account("Piglet", 0.0); // first element (Piglet) Account("Eeyore", 0.0); // second element (Eeyore) Account("Tigger", 0.0); // third element (Tiger)

One argument can be specified explicitly, as in the example above. If you need to pass several arguments, you will have to use an explicit constructor call:

Account pooh_pals = ( Account("Piglet", 1000.0), Account("Eeyore", 1000.0), Account("Tigger", 1000.0) );

To include a default constructor in the array initialization list, we use an explicit call with an empty parameter list:

Account pooh_pals = ( Account("Woozle", 10.0), // Beech Account("Heffalump", 10.0), // Heffalump Account(); );

An equivalent array of three elements can be declared like this:

Account pooh_pals = ( Account("Woozle", 10.0), Account("Heffalump", 10.0) );

Thus, the members of the initialization list are used sequentially to fill the next element of the array. Those elements for which no explicit arguments are given are initialized by the default constructor. If it is not present, then the list must contain constructor arguments for each element of the array.

Individual elements of an object array are accessed using the index operator, just as for an array of elements of any of the built-in types. For example:

Pooh_pals; refers to Piglet, and pooh_pals;

to Eeyore etc. To access the members of an object located in an array element, we combine the index and member access operators:

Pooh_pals._name != pooh_pals._name;

There is no way to explicitly specify the initial values ​​of the elements of an array whose memory is allocated from a heap. If a class supports creating dynamic arrays using the new operator, it must either have a default constructor or not have any constructors. In practice, almost all classes have such a constructor.

Announcement

Account *pact = new Account[ 10 ];

creates, in memory allocated from the heap, an array of ten objects of the Account class, each initialized by a default constructor.

To destroy an array addressed by a pact pointer, you must use the delete operator. However, write

// alas! this is not entirely correct delete pact;

is not sufficient, since pact is not identified as an array of objects. As a result, the destructor of the Account class is applied only to the first element of the array. To apply it to every element, we must include an empty pair of parentheses between the delete operator and the address of the object being deleted:

// correct: // shows that pact addresses an array delete pact;

An empty pair of parentheses indicates that pact addresses an array. The compiler determines how many elements there are and applies a destructor to each of them.

14.4.1. Initializing an array distributed from heap A

By default, initializing an array of objects distributed from a heap occurs in two steps: allocating memory for the array, applying a default constructor to each element of which, if defined, and then assigning a value to each element.

To reduce initialization to a single step, the programmer must intervene and support the following semantics: provide initial values ​​for all or some elements of the array, and ensure that a default constructor is used for those elements whose initial values ​​are not specified. Below is one possible software solution that uses the new allocation operator:

#include #include #include #include #include "Accounts.h" typedef pair value_pair; /* init_heap_array() * declared as a static member function * provides memory allocation from the heap and initialization * of an array of objects * init_values: pairs of initial values ​​of the array elements * elem_count: number of elements in the array * if 0, then the size of the array is considered to be the size of the vector * init_values */ Account* Account:: init_heap_array(vector &init_values, vector ::size_type elem_count = 0) ( vector ::size_type vec_size = init_value.size(); if (vec_size == 0 && elem_count == 0) return 0; // the size of the array is either elem_count, // or, if elem_count == 0, the size of the vector... size_t elems = elem_count ? elem_count: vec_size(); // get a block of memory to accommodate the array char *p = new char; // separately initialize each element of the array int offset = sizeof(Account); for (int ix = 0; ix< elems; ++ix) { // смещение ix-ого элемента // если пара начальных значений задана, // передать ее конструктору; // в противном случае вызвать конструктор по умолчанию if (ix < vec_size) new(p+offset*ix) Account(init_values.first, init_values.second); else new(p+offset*ix) Account; } // отлично: элементы распределены и инициализированы; // вернуть указатель на первый элемент return (Account*)p; }

You must pre-allocate a block of memory large enough to hold the requested array as a byte array to avoid applying a default constructor to each element. This is done in the following instructions:

Char *p = new char;

The program then goes through this block in a loop, assigning the address of the next element to the variable p at each iteration and calling either a constructor with two parameters, if a pair of initial values ​​is given, or a default constructor:

For (int ix = 0; ix

Section 14.3 explained that the new allocation operator allows a class constructor to be applied to an already allocated area of ​​memory. In this case, we use new to apply the Account class constructor to each of the selected array elements in turn. Since when creating an initialized array we replaced the standard memory allocation mechanism, we must take care of freeing it ourselves. The delete operator will not work:

Delete ps;

Why? Because ps (we assume that this variable was initialized by calling init_heap_array()) points to a block of memory that is not obtained using the standard new operator, so the number of elements in the array is unknown to the compiler. So you have to do all the work yourself:

Void Account:: dealloc_heap_array(Account *ps, size_t elems) ( for (int ix = 0; ix< elems; ++ix) ps.Account::~Account(); delete reinterpret_cast(ps); )

If in the initialization function we used pointer arithmetic to access elements:

New(p+offset*ix) Account;

then here we access them by setting the index in the ps array:

Ps.Account::~Account();

Although both ps and p address the same memory location, ps is declared as a pointer to an Account object, and p is declared as a pointer to char. Indexing p would yield the ixth byte, not the ixth object of the Account class. Since p has the wrong type associated with it, you have to program pointer arithmetic yourself.

We declare both functions as static members of the class:

Typedef pair value_pair; class Account ( public: // ... static Account* init_heap_array(vector< value_pair>&init_values, vector< value_pair>::size_type elem_count = 0); static void dealloc_heap_array(Account*, size_t); // ... );

14.4.2. Objects vector

When a vector of five class objects is defined, for example:

Vector< Point >vec(5);

then the elements are initialized in the following order5:

  1. The default constructor creates a temporary object of the class type stored in the vector.
  2. A copy constructor is applied to each element of the vector, causing each object to be initialized with a copy of the temporary object.
  3. The temporary object is destroyed.

Although the end result is the same as when defining an array of five class objects:

Point pa[ 5 ];

the efficiency of such vector initialization is lower, since, firstly, the construction and destruction of a temporary object naturally requires resources, and secondly, the copy constructor usually turns out to be computationally more complex than the default constructor.

The general design rule is this: a vector of class objects is more convenient only for inserting elements, i.e. in the case where an empty vector is initially defined. If we have calculated in advance how many elements will have to be inserted, or have an educated guess in this regard, then we need to reserve the necessary memory and then proceed with the insertion. For example:

Vector< Point >cvs; // empty int cv_cnt = calc_control_vertices(); // reserve memory to store cv_cnt objects of the Point class // cvs is still empty... cvs.reserve(cv_cnt); // open the file and prepare to read from it ifstream infile("spriteModel"); istream_iterator< Point>cvfile(infile),eos; // now you can insert elements copy(cvfile, eos, inserter(cvs, cvs.begin()));

(The copy() algorithm, the inserter, and the istream_iterator were discussed in Chapter 12.) The behavior of list and deque objects is similar to that of vector objects. Inserting an object into any of these containers is done using the copy constructor.

Exercise 14.9

Which of the following instructions are incorrect? Correct them.

(a) Account *parray = new Account; (b) Account iA = ("Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank" "Roy", "Elena" ); (c) string *ps=string("Tina","Tim","Chyuan","Mira","Mike"); (d) string as = *ps; Exercise 14.10

Which is better to use in each of the following situations: a static array (such as Account pA), a dynamic array, or a vector? Explain your choice.

Inside the Lut() function, a set of 256 elements is needed to store objects of the Color class. The values ​​are constants.

You need to store a set of an unknown number of Account class objects. Account data is read from a file.

The gen_words(elem_size) function must generate and pass to the text processor a set of elem_size strings.

Exercise 14.11

A potential source of error when using dynamic arrays is the omission of a pair of square brackets indicating that the pointer addresses the array, i.e. invalid entry

// sad: it is not checked that parray addresses the array delete parray; instead of // correct: the size of the array addressed is determined parray delete parray;

The presence of a pair of parentheses forces the compiler to find the size of the array. Then the destructor is applied to each element in turn (size times in total). If there are no parentheses, only one element is destroyed. In any case, all memory occupied by the array is freed.

When discussing the original C++ language, there was much debate about whether the presence of square brackets should trigger a search, or whether (as in the original specification) it was better to have the programmer explicitly specify the size of the array:

// in the original version of the language, the size of the array had to be set explicitly delete p parray;

Why do you think the language was changed in such a way that explicit setting of the size is not required (and therefore you need to be able to save and retrieve it), but the parentheses, although empty, remain in the delete operator (so the compiler does not have to remember which addresses the pointer single object or array)? What language option would you suggest?

14.5. Member initialization list

Let's modify our Account class by declaring a member _name of type string:

#include >string> class Account ( public: // ... private: unsigned int _acct_nmbr; double _balance; string _name; );

We will have to change the constructors at the same time. Two problems arise: maintaining compatibility with the original interface and initializing the class object using an appropriate set of constructors.

Initial Account constructor with two parameters

Account(const char*, double = 0.0);

cannot initialize a member of type string. For example:

String new_client("Steve Hall"); Account new_acct(new_client, 25000);

will not compile because there is no implicit conversion from string to char*. Instructions

Account new_acct(new_client.c_str(), 25000);

correct, but will cause confusion among users of the class. One solution is to add a new view constructor:

Account(string, double = 0.0);

If you write:

Account new_acct(new_client, 25000);

This particular constructor is called, whereas the old code

Account *open_new_account(const char *nm) ( Account *pact = new Account(nm); // ... return pacct; )

will still result in a call to the original constructor with two parameters.

Since the string class defines a conversion from char* to string (class conversions are discussed later in this chapter), you can replace the original constructor with a new one that takes string as its first parameter. In this case, when the instruction appears:

Account myAcct(" Tinkerbell");

" Tinkerbell" is converted to a temporary object of type string. This object is then passed to a new constructor with two parameters.

The design trade-off is between increasing the number of constructors for the Account class and slightly less efficient handling of char* arguments due to the need to create a temporary object. We have provided two versions of the two-parameter constructor. Then the modified set of Account constructors will be like this:

#include class Account ( public: Account(); Account(const char*, double=0.0); Account(const string&, double=0.0); Account(const Account&); // ... private: // ... );

How to properly initialize a member that is an object of some class with its own set of constructors? This question can be divided into three:

  1. where is the default constructor called? Inside the default constructor of the Account class;
  2. where is the copy constructor called? Inside the copy constructor of the Account class and inside a constructor with two parameters that takes string as the first parameter;
  3. how to pass arguments to the constructor of a class that is a member of another class? This must be done inside the Account constructor with two parameters, taking the char* type as the first one.

The solution is to use a member initialization list (we mentioned it in section 14.2). Members that are classes can be explicitly initialized using a list of comma-separated member name/value pairs. Our two-parameter constructor now looks like this (remember that _name is a member that is an object of the string class):

The member initialization list follows the constructor signature and is separated from it by a colon. It specifies the name of the member and the initial values ​​in parentheses, which is similar to the syntax for calling a function. If the member is a class object, then these values ​​become arguments passed to the appropriate constructor, which is then used. In our example, the value of name is passed to the string constructor, which is applied to the _name member. The _balance member is initialized to opening_bal.

The second constructor with two parameters looks similar:

Inline Account:: Account(const string& name, double opening_bal) : _name(name), _balance(opening_bal) ( _acct_nmbr = het_unique_acct_nmbr(); )

In this case, the string copy constructor is called, initializing the _name member with the value of the name parameter of type string.

A common question that newbies ask is: what is the difference between using an initialization list and assigning values ​​to members in the body of a constructor? For example, what is the difference between

Inline Account:: Account(const char* name, double opening_bal) : _name(name), _balance(opening_bal) ( _acct_nmbr = het_unique_acct_nmbr(); )

Account(const char* name, double opening_bal) ( _name = name; _balance = opening_bal; _acct_nmbr = het_unique_acct_nmbr(); )

At the end of both constructors, all three terms will have the same values. The difference is that only a list provides initialization for those members that are objects of the class. In the body of a constructor, setting the value of a member is not an initialization, but an assignment. Whether this distinction is important or not depends on the nature of the member.

Conceptually, the execution of a constructor consists of two phases: an explicit or implicit initialization phase, and an evaluation phase, which includes all the instructions in the body of the constructor. Any setting of member values ​​in the second phase is considered an assignment rather than an initialization. Failure to understand this difference leads to errors and ineffective programs.

The first phase may be explicit or implicit depending on whether there is a member initialization list. Implicit initialization first calls the default constructors of all base classes in the order in which they are declared, and then the default constructors of all members that are objects of the classes. (We'll look at base classes in Chapter 17 when we discuss object-oriented programming.) For example, if you write:

Inline Account:: Account() ( _name = ""; _balance = 0.0; _acct_nmbr = 0; )

then the initialization phase will be implicit. Even before the body of the constructor is executed, the default constructor of the string class associated with the _name member is called. This means that assigning _name to an empty string is unnecessary.

For class objects, the difference between initialization and assignment is significant. A member that is an object of a class should always be initialized using a list rather than assigning a value to it in the body of the constructor. The following implementation of the default constructor of the Account class is more correct:

Inline Account:: Account() : _name(string()) ( _balance = 0.0; _acct_nmbr = 0; )

We have removed the unnecessary _name assignment from the constructor body. An explicit call to the default string constructor is unnecessary. Below is an equivalent but more compact version:

Inline Account::Account() ( _balance = 0.0; _acct_nmbr = 0; )

However, we have not yet answered the question about initializing two members of built-in types. For example, does it really matter where _balance is initialized: in the initialization list or in the body of the constructor? Initialization and assignment to non-object class members are equivalent in both result and performance (with two exceptions). We prefer to use a list:

// preferred initialization style inline Account:: Account() : _balance(0.0), _acct_nmbr(0) ()

The two exceptions mentioned above are const members and reference members, regardless of type. You must always use an initialization list for them, otherwise the compiler will throw an error:

Class ConstRef ( public: ConstRef(int ii); private: int i; const int ci; int ); ConstRef:: ConstRef(int ii) ( // assignment i = ii; // correct ci = ii; // error: cannot be assigned to a constant member ri = i; // error: ri is not initialized)

By the time the constructor body starts executing, the initialization of all constant and reference members must be complete. To do this, you need to specify them in the initialization list. The correct implementation of the previous example is:

// correct: constant members and references are initialized ConstRef:: ConstRef(int ii) : ci(ii), ri (i) ( i = ii; )

Each member must appear at most once in the initialization list. The initialization order is determined not by the order of names in the list, but by the order in which the members are declared. If given the following declaration of members of the Account class:

Class Account ( public: // ... private: unsigned int _acct_nmbr; double _balance; string _name; );

then the initialization order for such a default constructor implementation is

Inline Account:: Account() : _name(string()), _balance(0.0), _acct_nmbr(0) () will be: _acct_nmbr, _balance, _name. However, members specified in a list (or in an implicitly initialized class member object) are always initialized before the assignment to members in the body of the constructor. For example, in the following constructor: inline Account:: Account(const char* name, double bal) : _name(name), _balance(bal) ( _acct_nmbr = get_unique_acct_nmbr(); )

The initialization order is: _balance, _name, _acct_nmbr.

A discrepancy between the initialization order and the order of the members in the corresponding list can lead to hard-to-find errors when one class member is used to initialize another:

Class X ( int i; int j; public: // see the problem? X(int val) : j(val), i(j) () // ... );

it appears that j is already initialized to val before it is used to initialize i, but in fact i is initialized first using the as-yet-uninitialized member j. We recommend placing the initialization of one member by another (if you consider it necessary) in the body of the constructor:

// preferred idiom X::X(int val) : i(val) ( j = i; )

Exercise 14.12

What is wrong in the following constructor definitions? How would you fix the errors you find?

(a) Word::Word(char *ps, int count = 1) : _ps(new char), _count(count) ( if (ps) strcpy(_ps, ps); else ( _ps = 0; _count = 0; ) ) (b) class CL1 ( public: CL1() ( c.real(0.0); c.imag(0.0); s = "not set" ; ) // ... private: complex c; string s; ) (c) class CL2 ( public: CL2(map *pmap, string key) : _text(key), _loc((*pmap)) () // ... private: location _loc; string_text; );

14.6. Memberwise initialization A

Initializing one object of a class with another object of the same class, as in:

Account oldAcct("Anna Livia Plurabelle"); Account newAcct(oldAcct);

called member-by-member initialization by default. By default - because it is done automatically, regardless of whether there is an explicit constructor or not. Memberwise - because the unit of initialization is a single non-static member, and not a bitwise copy of the entire class object.

The easiest way to imagine such initialization is if we assume that the compiler creates a special internal copy constructor, where all non-static members are initialized one by one, in the order of declaration. Considering the first definition of our Account class:

Class Account ( public: // ... private: char *_name; unsigned int _acct_nmbr; double _balance; );

then you can imagine that the default copy constructor is defined like this:

Inline Account:: Account(const Account &rhs) ( _name = rhs._name; _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; )

Member-wise initialization of one class object by another occurs in the following situations:

1. explicit initialization of one object by another:

Account newAcct(oldAcct);

1. passing a class object as a function argument:

Extern bool cash_on_hand(Account acct); if (cash_on_hand(oldAcct)) // ...

1. passing a class object as the function return value:

Extern Account consolidate_accts(const vector< Account >&) ( Account final_acct; // perform a financial transaction return final_acct; )

1. definition of a non-empty sequential container:

// five copy constructors of the string vector class are called< string >svec(5);

(This example uses the default string constructor to create one temporary object, which is then copied into five vector elements using the string copy constructor.)

1. inserting a class object into the container:

Svec.push_back(string("pooh"));

For most real class definitions, the default member-wise initialization does not match the semantics of the class. This most often happens when its member is a pointer that addresses the memory freed by the destructor in the heap, as in our Account.

As a result of this initialization, newAcct._name and oldAcct._name point to the same C string. If oldAcct goes out of scope and a destructor is applied to it, then newAcct._name points to the freed memory area. On the other hand, if newAcct modifies the line addressed by _name, then it is modified for oldAcct. Errors like this are very difficult to find.

One solution to pointer aliasing is to allocate an area of ​​memory for a copy of the string and initialize newAcct._name with the address of that area. Therefore, the default member-wise initialization for the Account class should be suppressed by providing an explicit copy constructor that implements the correct initialization semantics.

The internal semantics of a class may also not correspond to the default member-wise initialization. We explained earlier that two different Account objects should not have the same account number. To ensure this behavior, we must suppress the default member-wise initialization for the Account class. Here's what a copy constructor looks like to solve both of these problems:

Inline Account:: Account(const Account &rhs) ( // solve the problem of pointer alias _name = new char[ strlen(rhs._name)+1 ]; strcpy(_name, rhs._name); // solve the problem of uniqueness of the account number _acct_nmbr = get_unique_acct_nmbr(); // copying this member works like that _balance = rhs._balance; )

An alternative to writing a copy constructor is to disable memberwise initialization entirely. This can be done as follows:

  1. Declare the copy constructor a private member. This will prevent member-wise initialization everywhere except member functions and friends of the class.
  2. Disable member-wise initialization in class member functions and friends by deliberately not providing a copy constructor definition (however, you still need to declare it as described in step 1). The language does not give us the ability to restrict access to private members of a class by member functions and friends. But if the definition is missing, then any attempt to call the copy constructor, which is legal from the compiler's point of view, will result in an error during linking because the definition of the symbol cannot be found.

To prevent member-wise initialization, the Account class can be declared like this:

Class Account ( public: Account(); Account(const char*, double=0.0); // ... private: Account(const Account&); // ... );

14.6.1. Initializing a member that is an object of a class

What happens if you replace the C string in the _name declaration with the class type string? How will this affect the default memberwise initialization? How would you change an explicit copy constructor? We will answer these questions in this subsection.

With member-by-member initialization, the default is to examine each member. If it belongs to a built-in or composite type, then this initialization is applied directly. For example, in the original definition of the Account class, the _name member is initialized directly because it is a pointer:

NewAcct._name = oldAcct._name;

Members that are objects of classes are treated differently. In the instructions

Account newAcct(oldAcct);

both objects are recognized as Account instances. If this class has an explicit copy constructor, then it is used to set the initial value, otherwise the default member-wise initialization is performed.

Thus, if a class member object is found, the above process is applied recursively. Does the class have an explicit copy constructor? If so, call it to set the initial value of the class object member. Otherwise, apply default member-by-member initialization to this member. If all members of this class belong to built-in or composite types, then each is initialized directly and the process ends there. If some members are themselves class objects, then the algorithm is applied to them recursively until there is nothing left but built-in and composite types.

In our example, the string class has an explicit copy constructor, so _name is initialized by calling it. The default copy constructor for the Account class is as follows (although it is not explicitly defined):

Inline Account:: Account(const Account &rhs) ( _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; // C++ pseudocode // illustrates calling a copy constructor // for a member that is an object of the class _name.string::string(rhs ._name); )

The default member-by-member initialization for the Account class now handles memory allocation and deallocation for _name correctly, but still copies the account number incorrectly, so an explicit copy constructor must be coded. However, the snippet below is not entirely correct. Can you tell me why?

// not quite correct... inline Account:: Account(const Account &rhs) ( _name = rhs._name; _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); )

This implementation is flawed because it does not distinguish between initialization and assignment. As a result, instead of calling the string copy constructor, we call the default string constructor in the implicit initialization phase and the string copy assignment operator in the body of the constructor. It's easy to fix:

Inline Account:: Account(const Account &rhs) : _name(rhs._name) ( _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); )

The most important thing is to understand that such a correction is necessary. (Both implementations cause _name to copy the value from rhs._name, but the first does the same job twice.) A general heuristic is to initialize all member objects of classes in the member initialization list if possible.

Exercise 14.13

Which class definition is most likely to need a copy constructor?

  1. A Point3w representation containing four floating point numbers.
  2. The matrix class, in which memory for storing the matrix is ​​dynamically allocated in the constructor and freed in the destructor.
  3. The payroll class, where each object is assigned a unique identifier.
  4. The word class contains an object of the string class and a vector that stores pairs (line number, offset in the line).

Exercise 14.14

Implement a copy constructor, a default constructor, and a destructor for each of these classes.

(a) class BinStrTreeNode ( public: // ... private: string _value; int _count; BinStrTreeNode *_leftchild; BinStrTreeNode *_rightchild; ); (b) class BinStrTree ( public: // ... private: BinStrTreeNode *_root; ); (c) class iMatrix ( public: // ... private: int _rows; int _cols; int *_matrix; ); (d) class theBigMix ( public: // ... private: BinStrTree _bst; iMatrix _im; string _name; vectorMfloat> *_pvec; );

Exercise 14.15

Do you need a copy constructor for the class you chose in Exercise 14.3 of Section 14.2? If not, explain why. If yes, implement it.

Exercise 14.16

In the following program fragment, identify all the places where memberwise initialization occurs:

Point global; Point foo_bar(Point arg) ( Point local = arg; Point *heap = new Point(global); *heap = local; Point pa[ 4 ] = ( local, *heap ); return *heap; )

14.7. Memberwise assignment A

Assigning one object of a class the value of another object of the same class is implemented by memberwise assignment by default. It differs from the default memberwise initialization only by using a copy assignment operator instead of a copy constructor:

NewAcct = oldAcct;

by default assigns each non-static newAcct member the value of the corresponding oldAcct member. The compiler generates the following copy assignment operator:

Inline Account& Account:: operator=(const Account &rhs) ( _name = rhs._name; _balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr; )

As a general rule, if a class does not have the default memberwise initialization, then the default memberwise assignment does not qualify either. For example, for the original definition of the Account class, where the member _name was declared to be char*, such an assignment would not work for either _name or _acct_nmbr.

We can suppress this by providing an explicit copy assignment operator that implements class-appropriate semantics:

// general view of the copying assignment operator className& className:: operator=(const className &rhs) ( // no need to assign to yourself if (this != &rhs) ( // class copy semantics are implemented here ) // return the object to which the value is assigned return *this; )

Here's a conditional statement

If (this != &rhs)

prevents an object of a class from being assigned to itself, which is especially unpleasant in a situation where the copy assignment operator first frees some resource associated with an object on the left side in order to assign instead a resource associated with an object on the right side. Consider the copy assignment operator for the Account class:

Account& Account:: operator=(const Account &rhs) ( // do not assign to yourself if (this != &rhs) ( delete _name; _name = new char; strcpy(_name,rhs._name); _balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr; ) return *this; )

When one object of a class is assigned to another, as in the statement:

NewAcct = oldAcct;

the following steps are performed:

  1. Determines whether the class has an explicit copy assignment operator.
  2. If there is, the access rights to it are checked to see whether it can be called at this point in the program.
  3. The operator is called to perform an assignment; if it is not available, the compiler issues an error message.
  4. If there is no explicit operator, the default memberwise assignment is performed.
  5. In member-by-member assignment, each member of a built-in or composite object member on the left side is assigned the value of the corresponding object member on the right side.
  6. For each member that is an object of the class, steps 1-6 are recursively applied until only members of built-in and composite types remain.

If we again modify the definition of the Account class so that _name is of type string, then the default memberwise assignment

NewAcct = oldAcct;

will be executed in the same way as when created by the compiler next operator assignments:

Inline Account& Account:: operator=(const Account &rhs) ( _balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr; // this call is also correct from the programmer’s point of view name.string::operator=(rhs._name); )

However, the default memberwise assignment for objects of the Account class is not suitable due to _acct_nmbr. We need to implement an explicit copy assignment operator, taking into account that _name is an object of the string class:

Account& Account:: operator=(const Account &rhs) ( // no need to assign to yourself if (this != &rhs) ( // called string::operator=(const string&) _name = rhs._name; _balance = rhs._balance ; ) return *this; )

To prevent member-wise copying, we do the same thing as in the case of member-wise initialization: we declare the operator private and do not provide its definition.

The copy constructor and the copy assignment operator are usually considered together. If one is necessary, then, as a rule, the other is also necessary. If one is banned, then the other should probably be banned too.

Exercise 14.17

Implement a copy assignment operator for each of the classes defined in Exercise 14.14 from Section 14.6.

Exercise 14.18

Is a copy assignment operator necessary for the class you chose in Exercise 14.3 of Section 14.2? If yes, implement it. Otherwise, explain why it is not needed.

14.8. Efficiency Considerations A

In general, it is more efficient to pass a class object to functions by pointer or reference rather than by value. For example, if given a function with signature:

Bool sufficient_funds(Account acct, double);

then each time it is called, it is required to perform member-by-member initialization of the formal parameter acct with the value of the actual argument-object of the Account class. If the function has any of these signatures:

Bool sufficient_funds(Account *pacct, double); bool sufficient_funds(Account &acct, double);

then just copy the address of the Account object. In this case, no class initialization occurs (see the discussion of the relationship between reference and pointer parameters in Section 7.3).

// the problem is solved, but for large matrices the efficiency may // be unacceptably low Matrix operator+(const Matrix& m1, const Matrix& m2) ( Matrix result; // perform arithmetic operations... return result; )

This overloaded operator allows the user to write

Matrix a, b; // ... // in both cases operator+() is called Matrix c = a + b; a = b + c;

However, returning a result by value may be too time-consuming and memory-intensive if Matrix is ​​a large, complex class. If this operation is performed frequently, it will likely reduce performance dramatically.

The following revised implementation greatly improves speed:

// more efficient, but after returning the address is invalid // this may cause the program to crash Matrix& operator+(const Matrix& m1, const Matrix& m2) ( Matrix result; // perform addition... return result; )

But at the same time, frequent program crashes occur. The point is that the value of the result variable is not defined after exiting the function in which it was declared. (We're returning a reference to a local object that doesn't exist when returned.)

The return address value must remain valid after the function exits. In the above implementation, the returned address is not overwritten:

// there is no way to guarantee that there will be no memory leaks // since the matrix can be large, the leaks will be quite noticeable Matrix& operator+(const Matrix& m1, const Matrix& m2) ( Matrix *result = new Matrix; // perform addition... return *result; )

However, this is unacceptable: a large memory leak occurs, since no part of the program is responsible for applying the delete operator to the object when it is finished using it.

Instead of the addition operator, it is better to use a named function, which is passed as a third parameter a link where the result should be stored:

// this provides the desired efficiency, // but is not intuitive for the user void mat_add(Matrix &result, const Matrix& m1, const Matrix& m3) ( // calculate the result)

This solves the performance problem, but the class can no longer use operator syntax, so you lose the ability to initialize objects

// no longer supported Matrix c = a + b;

and use them in expressions:

// also not supported if (a + b > c) ...

Inefficient return of a class object is a weak point of C++. One solution was to extend the language by introducing the name of the object returned by the function:

Matrix& operator+(const Matrix& m1, const Matrix& m2) name result ( Matrix result; // ... return result; )

Then the compiler could independently rewrite the function by adding a third link parameter to it:

// function rewritten by the compiler // if the proposed language extension is accepted void operator+(Matrix &result, const Matrix& m1, const Matrix& m2) name result ( // calculate the result )

and transform all calls to that function, placing the result directly in the scope referenced by the first parameter. For example:

Matrix c = a + b;

would be transformed into

Matrix c; operator+(c, a, b);

This extension never became part of the language, but the proposed optimization stuck. The compiler is able to recognize that a class object is being returned and perform a transformation of its value without an explicit language extension. If a general function is given:

ClassType functionName(paramList) ( classType namedResult; // perform some actions... return namedResult; )

then the compiler independently transforms both the function itself and all calls to it:

Void functionName(classType &namedResult, paramList) ( // calculate the result and place it at namedResult )

which allows you to avoid the need to return the value of the object and call the copy constructor. For this optimization to apply, the same named class object must be returned from the function at each return point.

One final note about the efficiency of working with objects in C++. Initializing a view class object

Matrix c = a + b;

is always more efficient than assignment. For example, the result of the following two instructions is the same as in the previous case:

Matrix c; c = a + b;

but the amount of calculations required is much greater. Similarly, it is more efficient to write:

For (int ix = 0; ix

Matrix matSum; for (int ix = 0; ix

The reason assignment is always less efficient is that the returned local object cannot be substituted for the object on the left side of the assignment statement. In other words, while the instructions

Point3d p3 = operator+(p1, p2);

can be safely transformed:

// Pseudocode in C++ Point3d p3; operator+(p3, p1, p2);

transformation

Point3d p3; p3 = operator+(p1, p2);

// Pseudocode in C++ // unsafe in case of assignment operator+(p3, p1, p2); unsafe.

The converted function requires that the object passed to it be a raw memory region. Why? Because a constructor that has already been applied to a named local object is immediately applied to the object. If the passed object has already been constructed, then doing it again is semantically incorrect.

As for the object being initialized, the memory allocated for it has not yet been processed. If a value is assigned to an object and constructors are declared in the class (and this is the case we are considering), it can be argued that this memory has already been formatted by one of them, so it is not safe to directly pass the object to the function.

Instead, the compiler must create a raw memory region as a temporary class object, pass it to the function, and then assign the returned temporary object member-by-member to the object on the left side of the assignment operator. Finally, if a class has a destructor, then it is applied to a temporary object. For example, the following snippet

Point3d p3; p3 = operator+(p1, p2);

transforms into this:

// Pseudocode in C++ Point3d temp; operator+(temp, p1, p2); p3.Point3d::operator=(temp); temp.Point3d::~Point3d();

Michael Tiemann, author of the GNU C++ compiler, suggested calling this language extension named return value(return value language extension). His point of view is presented in the work. Our book “Inside the C++ Object Model” () provides a detailed discussion of the topics covered in this chapter.

The list of parameters in the definition and prototype of a function, in addition to matching the types of parameters, has one more purpose.

A parameter declaration may contain an initializer, that is, an expression that must provide an initial value for the parameter. The parameter initializer is not a constant expression. The initial initialization of parameters does not occur at the compilation stage (like, for example, memory allocation for arrays), but directly during program execution.

The following lines show an example of a function declaration with parameter initialization. The XX function is used to initialize the ww parameter.

int ZZ(int tt, int ww = XX(BigVal));

The second parameter can be initialized in this way, without specifying its name at all. The declaration syntax allows you to do this!

int ZZ(int tt, int = XX(BigVal));

The only condition for such initialization is that the type of the parameter matches the type of the expression whose value is used during initial initialization.

Function prototypes can be located in different scopes. It can even be placed in the body of the defined function. Each function declaration can contain its own options for declaring and initializing parameters. But multiple declarations of the same function within the same scope do not allow parameters to be reinitialized. There must be a reasonable limit to everything.

In addition, C++ has another limitation related to the order in which parameters are initialized within the scope. Initialization is carried out without fail from the very last (rightmost) parameter in the list of parameter declarations. Initialization of parameters does not allow gaps: initialized parameters cannot alternate with uninitialized parameters.

int MyF1 (int par1, int par2, int par3, int par4 = 10);

………………………………….

int MyF1 (int par1, int par2 = 20, int par3 = 20, int par4);

………………………………….

int MyF1(int par1 = 100, int, int, int);

#include

int f(int, int=4);

int main(int argc, char* argv)

printf("%d\n", f(2)); //8

printf("%d\n", f(2,3)); //6

int f(int a, int b)

Functions with a variable number of parameters

When calling a function with a variable number of parameters, any required number of arguments is specified in the call to this function. In the declaration and definition of such a function, a variable number of arguments is specified by an ellipsis at the end of the list of formal parameters or the list of argument types.

All arguments given in a function call are pushed onto the stack. The number of formal parameters declared for a function is determined by the number of arguments that are taken from the stack and assigned to the formal parameters. The programmer is responsible for correctly selecting additional arguments from the stack and determining the number of arguments that are on the stack.

Examples of functions with a variable number of parameters are functions from the SI language function library that perform information input/output operations (printf, scanf, etc.). The printf() function in the library is declared as follows:

int printf(const char* ...);

This ensures that any call to printf() will be passed the first argument of type const char*. The contents of this string, called a format string, determine whether additional arguments are needed when calling. If there are metacharacters in the format string that begin with the % symbol, the function waits for the presence of these arguments.

The programmer can develop his functions with a variable number of parameters. To provide convenient way To access the arguments of a function with a variable number of parameters, there are three macro definitions (macros) va_start, va_arg, va_end, located in the header file stdarg.h. These macros indicate that a user-developed function has a number of required arguments followed by a variable number of optional arguments. Required arguments are accessible through their names, just like when calling a regular function. To retrieve optional arguments, use the macros va_start, va_arg, va_end in the following order.

The va_start macro is designed to set the arg_ptr argument to the beginning of the list of optional parameters and looks like a function with two parameters:

void va_start(arg_ptr, prav_param);

The prav_param parameter must be the last required parameter of the called function, and the arg_prt pointer must be declared with a predefinition in the list of variables of type va_list in the form:

va_list arg_ptr;

The va_start macro must be used before the first use of the va_arg macro.

The va_arg macro provides access to the current parameter of the called function and also looks like a function with two parameters

type_arg va_arg(arg_ptr,type);

This macro retrieves the value of type at the address specified by the arg_ptr pointer, increments the value of the arg_ptr pointer by the length of the parameter used (type length), and thus the arg_ptr parameter will point to the next parameter of the called function. The va_arg macro is used as many times as necessary to retrieve all the parameters of the called function.

The va_end macro is used after all function parameters have been processed and sets the optional parameter list pointer to NULL.

Let's consider using these macros to process the parameters of a function that calculates the average value of an arbitrary sequence of integers. Since the function has a variable number of parameters, we will consider the value equal to -1 to be the end of the list. Since the list must have at least one element, the function will have one required parameter.

#include

#include

int sred_znach(int,...);

n=sred_znach(2,3,4,-1); /* call with four parameters */

printf("n=%d\n",n);

n=sred_znach(5,6,7,8,9,-1); /* call with six parameters */

printf("n=%d\n",n);

int sred_znach(int x,...)

int i=0, j=0, sum=0;

va_start(uk_arg,x); /* set the uk_arg pointer to */

/* first optional parameter */

if (x!=-1) sum=x; /* check if the list is empty */

else return (0);

while ((i=va_arg(uk_arg,int))!=-1)

/* selecting the next one */

( /* parameters and check */

sum+=i; /* to the end of the list */

va_end(uk_arg); /* close the list of parameters */

Example 2: Another option is possible - the first parameter of the function is responsible for the number of elements being summed. In addition, you can refuse to use macros and parse the stack in the function code.

#include

#include

int sred_znach2(int,...);

n=sred_znach2(3,2,3,4);

printf("n=%d\n",n);

int sred_znach2(int n,...)

int *pPointer =

for (int i=n ; i; i--) Sum += *(++pPointer);







2024 gtavrl.ru.